Add `swmctl` command
c3e974a9d73f88e798acc282c55601436a61f0dc
1 parent
f50d353f
.gitignore
+2 -0
| 1 | 1 | swm |
|
| 2 | 2 | swm.1 |
|
| 3 | + | swmctl |
|
| 4 | + | swmctl.1 |
|
| 3 | 5 | config.h |
|
| 4 | 6 | *.zst |
|
| 5 | 7 | *.o |
|
| 6 | 8 | *-protocol.c |
|
| 7 | 9 | *-protocol.h |
Makefile
+48 -10
| 16 | 16 | -Werror=incompatible-pointer-types -Wfloat-conversion \ |
|
| 17 | 17 | -fno-common -fstack-protector-all -mcmodel=medium |
|
| 18 | 18 | LDFLAGS := -fuse-ld=lld |
|
| 19 | 19 | LDLIBS := `$(PKG_CONFIG) --libs $(PKGS)` $(WLR_LIBS) -lm $(LIBS) |
|
| 20 | 20 | ||
| 21 | - | SRC := swm.c util.c ext-workspace-v1-protocol.c |
|
| 21 | + | # swmctl |
|
| 22 | + | CTL_PKGS = wayland-client |
|
| 23 | + | CTL_CPPFLAGS := `$(PKG_CONFIG) --cflags $(CTL_PKGS)` |
|
| 24 | + | CTL_LDLIBS := `$(PKG_CONFIG) --libs $(CTL_PKGS)` |
|
| 25 | + | ||
| 26 | + | SRC := swm.c util.c ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c |
|
| 22 | 27 | OBJ := $(SRC:.c=.o) |
|
| 23 | 28 | PROTO := \ |
|
| 24 | 29 | cursor-shape-v1-protocol.h \ |
|
| 25 | 30 | ext-workspace-v1-protocol.h \ |
|
| 26 | 31 | pointer-constraints-unstable-v1-protocol.h \ |
|
| 27 | 32 | wlr-layer-shell-unstable-v1-protocol.h \ |
|
| 28 | 33 | wlr-output-power-management-unstable-v1-protocol.h \ |
|
| 34 | + | swm-workspace-v1-protocol.h \ |
|
| 29 | 35 | xdg-shell-protocol.h |
|
| 30 | 36 | HDRS := config.h swm.h util.h $(PROTO) |
|
| 31 | 37 | ||
| 32 | 38 | WL_SCANNER := $(shell $(PKG_CONFIG) --variable=wayland_scanner wayland-scanner) |
|
| 33 | 39 | WL_PROTO := $(shell $(PKG_CONFIG) --variable=pkgdatadir wayland-protocols) |
| 41 | 47 | ||
| 42 | 48 | ifeq ($(TEST_COVERAGE),1) |
|
| 43 | 49 | TEST_CFLAGS += -fprofile-instr-generate -fcoverage-mapping |
|
| 44 | 50 | endif |
|
| 45 | 51 | ||
| 46 | - | all: man swm |
|
| 52 | + | all: man swm swmctl |
|
| 47 | 53 | ||
| 48 | - | man: swm.1 |
|
| 54 | + | man: swm.1 swmctl.1 |
|
| 49 | 55 | ||
| 50 | 56 | swm.1: swm.1.adoc |
|
| 51 | 57 | @echo "man $< => $@" |
|
| 52 | 58 | @asciidoctor -b manpage -o $@ $< |
|
| 53 | 59 | ||
| 60 | + | swmctl.1: swmctl.1.adoc |
|
| 61 | + | @echo "man $< => $@" |
|
| 62 | + | @asciidoctor -b manpage -o $@ $< |
|
| 63 | + | ||
| 54 | 64 | swm: $(OBJ) |
|
| 55 | 65 | @echo "ld $^ => $@" |
|
| 56 | 66 | @$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ |
|
| 57 | 67 | @echo "ok $@" |
|
| 58 | 68 | ||
| 69 | + | swmctl: swmctl.o swm-workspace-v1-protocol.o |
|
| 70 | + | @echo "ld $^ => $@" |
|
| 71 | + | @$(CC) $(LDFLAGS) $^ $(CTL_LDLIBS) -o $@ |
|
| 72 | + | @echo "ok $@" |
|
| 73 | + | ||
| 74 | + | swmctl.o: swmctl.c swm-workspace-v1-client-protocol.h Makefile config.mk |
|
| 75 | + | @echo "cc $< => $@" |
|
| 76 | + | @$(CC) $(CTL_CPPFLAGS) $(CFLAGS) -c $< -o $@ |
|
| 77 | + | ||
| 59 | 78 | $(OBJ): Makefile config.mk |
|
| 60 | 79 | ||
| 61 | 80 | %.o: %.c $(HDRS) |
|
| 62 | 81 | @echo "cc $< => $@" |
|
| 63 | 82 | @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ |
| 72 | 91 | @$(WL_SCANNER) server-header $< $@ |
|
| 73 | 92 | ext-workspace-v1-protocol.c: \ |
|
| 74 | 93 | $(WL_PROTO)/staging/ext-workspace/ext-workspace-v1.xml |
|
| 75 | 94 | @echo "hdr $@" |
|
| 76 | 95 | @$(WL_SCANNER) private-code $< $@ |
|
| 96 | + | swm-workspace-v1-protocol.h: protocols/swm-workspace-v1.xml |
|
| 97 | + | @echo "hdr $@" |
|
| 98 | + | @$(WL_SCANNER) server-header $< $@ |
|
| 99 | + | swm-workspace-v1-client-protocol.h: protocols/swm-workspace-v1.xml |
|
| 100 | + | @echo "hdr $@" |
|
| 101 | + | @$(WL_SCANNER) client-header $< $@ |
|
| 102 | + | swm-workspace-v1-protocol.c: protocols/swm-workspace-v1.xml |
|
| 103 | + | @echo "src $@" |
|
| 104 | + | @$(WL_SCANNER) private-code $< $@ |
|
| 77 | 105 | pointer-constraints-unstable-v1-protocol.h: \ |
|
| 78 | 106 | $(WL_PROTO)/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml |
|
| 79 | 107 | @echo "hdr $@" |
|
| 80 | 108 | @$(WL_SCANNER) enum-header $< $@ |
|
| 81 | 109 | wlr-layer-shell-unstable-v1-protocol.h: \ |
| 95 | 123 | @test -e $@ || cp config.def.h $@ |
|
| 96 | 124 | ||
| 97 | 125 | fmt: |
|
| 98 | 126 | git ls-files "*.c" "*.h" | xargs clang-format -i |
|
| 99 | 127 | ||
| 100 | - | install: swm swm.1 |
|
| 128 | + | install: swm swmctl swm.1 swmctl.1 |
|
| 101 | 129 | install -Dm755 swm $(DESTDIR)$(PREFIX)/bin/swm |
|
| 130 | + | install -Dm755 swmctl $(DESTDIR)$(PREFIX)/bin/swmctl |
|
| 102 | 131 | install -Dm644 swm.1 $(DESTDIR)$(MANDIR)/man1/swm.1 |
|
| 132 | + | install -Dm644 swmctl.1 $(DESTDIR)$(MANDIR)/man1/swmctl.1 |
|
| 103 | 133 | install -Dm644 swm.desktop \ |
|
| 104 | 134 | $(DESTDIR)$(DATADIR)/wayland-sessions/swm.desktop |
|
| 105 | 135 | ||
| 106 | 136 | uninstall: |
|
| 107 | 137 | rm -f $(DESTDIR)$(PREFIX)/bin/swm |
|
| 138 | + | rm -f $(DESTDIR)$(PREFIX)/bin/swmctl |
|
| 108 | 139 | rm -f $(DESTDIR)$(MANDIR)/man1/swm.1 |
|
| 140 | + | rm -f $(DESTDIR)$(MANDIR)/man1/swmctl.1 |
|
| 109 | 141 | rm -f $(DESTDIR)$(DATADIR)/wayland-sessions/swm.desktop |
|
| 110 | 142 | ||
| 111 | 143 | archive: |
|
| 112 | 144 | git archive --format=tar.gz --prefix=swm/ -o swm.tar.gz HEAD |
|
| 113 | 145 | ||
| 114 | 146 | package: PKGBUILD |
|
| 115 | 147 | makepkg --force |
|
| 116 | 148 | ||
| 117 | 149 | clean: |
|
| 118 | - | rm -f swm swm.1 *.o *-protocol.h *-protocol.c |
|
| 150 | + | rm -f swm swmctl swm.1 swmctl.1 *.o *-protocol.h *-protocol.c |
|
| 119 | 151 | ||
| 120 | 152 | $(TEST_SRC) $(TEST_BIN): |
|
| 121 | 153 | @mkdir -p $@ |
|
| 122 | 154 | ||
| 123 | 155 | $(TEST_SRC)/swm.c: swm.c | $(TEST_SRC) |
| 125 | 157 | ||
| 126 | 158 | $(TEST_SRC)/config.h: config.def.h | $(TEST_SRC) |
|
| 127 | 159 | @cp -p $< $@ |
|
| 128 | 160 | ||
| 129 | 161 | TEST_COMMON := Makefile config.mk $(TEST_SRC)/swm.c $(TEST_SRC)/config.h \ |
|
| 130 | - | swm.h util.c util.h $(PROTO) ext-workspace-v1-protocol.c |
|
| 162 | + | swm.h util.c util.h $(PROTO) ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c |
|
| 131 | 163 | ||
| 132 | 164 | $(TEST_BIN)/unit: test/unit.c $(TEST_COMMON) | $(TEST_BIN) |
|
| 133 | 165 | @echo "cc test/unit.c => $@" |
|
| 134 | 166 | @$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) test/unit.c util.c \ |
|
| 135 | - | ext-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 167 | + | ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 136 | 168 | ||
| 137 | 169 | $(TEST_BIN)/swm: $(TEST_COMMON) | $(TEST_BIN) |
|
| 138 | 170 | @echo "cc swm.c => $@" |
|
| 139 | 171 | @$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) $(TEST_SRC)/swm.c util.c \ |
|
| 140 | - | ext-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 172 | + | ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 173 | + | ||
| 174 | + | $(TEST_BIN)/swmctl: swmctl.c swm-workspace-v1-client-protocol.h \ |
|
| 175 | + | swm-workspace-v1-protocol.c | $(TEST_BIN) |
|
| 176 | + | @echo "cc swmctl.c => $@" |
|
| 177 | + | @$(CC) $(CTL_CPPFLAGS) $(TEST_CFLAGS) swmctl.c \ |
|
| 178 | + | swm-workspace-v1-protocol.c $(CTL_LDLIBS) -o $@ |
|
| 141 | 179 | ||
| 142 | - | test-build: $(TEST_BIN)/unit $(TEST_BIN)/swm |
|
| 180 | + | test-build: $(TEST_BIN)/unit $(TEST_BIN)/swm $(TEST_BIN)/swmctl |
|
| 143 | 181 | ||
| 144 | 182 | test: test-build |
|
| 145 | 183 | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py |
|
| 146 | 184 | ||
| 147 | 185 | test-unit: $(TEST_BIN)/unit |
|
| 148 | 186 | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py unit |
|
| 149 | 187 | ||
| 150 | - | test-integration: $(TEST_BIN)/swm |
|
| 188 | + | test-integration: $(TEST_BIN)/swm $(TEST_BIN)/swmctl |
|
| 151 | 189 | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py integration |
|
| 152 | 190 | ||
| 153 | 191 | coverage: |
|
| 154 | 192 | @$(MAKE) --no-print-directory TEST_BUILD=$(TEST_BUILD)/coverage TEST_COVERAGE=1 test-build |
|
| 155 | 193 | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)/coverage) PKG_CONFIG=$(PKG_CONFIG) \ |
PKGBUILD
+2 -2
| 26 | 26 | ) |
|
| 27 | 27 | source=() |
|
| 28 | 28 | b2sums=() |
|
| 29 | 29 | ||
| 30 | 30 | prepare() { |
|
| 31 | - | cp "${startdir}"/{Makefile,config.mk,swm.c,swm.h,util.c,util.h} \ |
|
| 31 | + | cp "${startdir}"/{Makefile,config.mk,swm.c,swm.h,swmctl.c,util.c,util.h} \ |
|
| 32 | 32 | "${srcdir}/" |
|
| 33 | - | cp "${startdir}"/{config.def.h,swm.1.adoc,swm.desktop} \ |
|
| 33 | + | cp "${startdir}"/{config.def.h,swm.1.adoc,swmctl.1.adoc,swm.desktop} \ |
|
| 34 | 34 | "${srcdir}/" |
|
| 35 | 35 | install -d "${srcdir}/protocols" |
|
| 36 | 36 | cp "${startdir}"/protocols/*.xml "${srcdir}/protocols/" |
|
| 37 | 37 | } |
|
| 38 | 38 |
README
+5 -2
| 103 | 103 | # BARS AND STATUS |
|
| 104 | 104 | ||
| 105 | 105 | swm has no built-in bar. A layer-shell bar such as Waybar[2] can use the |
|
| 106 | 106 | `ext/workspaces` module, backed by swm's ext-workspace-v1 implementation. |
|
| 107 | 107 | Empty inactive workspaces are advertised as hidden; occupied and selected |
|
| 108 | - | workspaces remain visible. |
|
| 108 | + | workspaces remain visible. See `swmctl(1)` for workspace metadata and Waybar |
|
| 109 | + | integration. |
|
| 109 | 110 | ||
| 110 | 111 | On each state change swm also writes line-oriented status records to standard |
|
| 111 | 112 | output. When `-s` is used, that output is connected to the startup command's |
|
| 112 | - | standard input. The active window title is published atomically to |
|
| 113 | + | standard input. Workspace metadata uses `workspace N title VALUE` and |
|
| 114 | + | `workspace N color #RRGGBBAA` records; empty values mean unset. The active window |
|
| 115 | + | title is published atomically to |
|
| 113 | 116 | `$XDG_RUNTIME_DIR/swm-title`, which can be consumed by a custom bar module. |
|
| 114 | 117 | Visible window rectangles are published atomically to |
|
| 115 | 118 | `$XDG_RUNTIME_DIR/swm-windows` in the format accepted by `slurp -r`. |
|
| 116 | 119 | ||
| 117 | 120 |
protocols/swm-workspace-v1.xml
added
+70 -0
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | + | <protocol name="swm_workspace_v1"> |
|
| 3 | + | <copyright> |
|
| 4 | + | Copyright (C) 2026 swm contributors |
|
| 5 | + | ||
| 6 | + | This program is free software: you can redistribute it and/or modify |
|
| 7 | + | it under the terms of the GNU General Public License as published by |
|
| 8 | + | the Free Software Foundation, either version 3 of the License, or |
|
| 9 | + | (at your option) any later version. |
|
| 10 | + | </copyright> |
|
| 11 | + | ||
| 12 | + | <interface name="swm_workspace_manager_v1" version="1"> |
|
| 13 | + | <description summary="inspect and change swm workspace metadata"> |
|
| 14 | + | This interface exposes ephemeral titles and colors associated with |
|
| 15 | + | swm's fixed workspaces. Metadata exists only for the lifetime of the |
|
| 16 | + | compositor process. |
|
| 17 | + | ||
| 18 | + | When a client binds, the compositor sends one metadata event for every |
|
| 19 | + | workspace followed by done. Later changes are sent in the same way. |
|
| 20 | + | </description> |
|
| 21 | + | ||
| 22 | + | <enum name="error"> |
|
| 23 | + | <entry name="invalid_workspace" value="1" summary="workspace number is out of range"/> |
|
| 24 | + | <entry name="title_too_long" value="2" summary="workspace title is too long"/> |
|
| 25 | + | </enum> |
|
| 26 | + | ||
| 27 | + | <enum name="metadata"> |
|
| 28 | + | <entry name="title" value="1" summary="the title is non-empty"/> |
|
| 29 | + | <entry name="color" value="2" summary="the color is set"/> |
|
| 30 | + | <entry name="selected" value="4" summary="the workspace is selected by swm"/> |
|
| 31 | + | </enum> |
|
| 32 | + | ||
| 33 | + | <event name="metadata"> |
|
| 34 | + | <description summary="workspace metadata"> |
|
| 35 | + | The workspace number starts at one. Color is encoded as 0xRRGGBBAA; |
|
| 36 | + | zero means unset and omits the color bit from flags. An empty |
|
| 37 | + | title means the additional label is unset. Selected identifies the |
|
| 38 | + | workspace on swm's currently selected output. |
|
| 39 | + | </description> |
|
| 40 | + | <arg name="workspace" type="uint" summary="one-based workspace number"/> |
|
| 41 | + | <arg name="flags" type="uint" enum="metadata"/> |
|
| 42 | + | <arg name="title" type="string"/> |
|
| 43 | + | <arg name="color" type="uint" summary="color encoded as 0xRRGGBBAA"/> |
|
| 44 | + | </event> |
|
| 45 | + | ||
| 46 | + | <event name="done"> |
|
| 47 | + | <description summary="metadata update is complete"/> |
|
| 48 | + | </event> |
|
| 49 | + | ||
| 50 | + | <request name="set_title"> |
|
| 51 | + | <description summary="set a workspace title"> |
|
| 52 | + | Set the title of a workspace. An empty title clears it. |
|
| 53 | + | </description> |
|
| 54 | + | <arg name="workspace" type="uint" summary="one-based workspace number"/> |
|
| 55 | + | <arg name="title" type="string"/> |
|
| 56 | + | </request> |
|
| 57 | + | ||
| 58 | + | <request name="set_color"> |
|
| 59 | + | <description summary="set a workspace color"> |
|
| 60 | + | Set the color of a workspace. Zero clears it. |
|
| 61 | + | </description> |
|
| 62 | + | <arg name="workspace" type="uint" summary="one-based workspace number"/> |
|
| 63 | + | <arg name="color" type="uint" summary="color encoded as 0xRRGGBBAA"/> |
|
| 64 | + | </request> |
|
| 65 | + | ||
| 66 | + | <request name="destroy" type="destructor"> |
|
| 67 | + | <description summary="release the manager object"/> |
|
| 68 | + | </request> |
|
| 69 | + | </interface> |
|
| 70 | + | </protocol> |
swm.1.adoc
+6 -1
| 24 | 24 | *swm* has no built-in bar and no runtime configuration file. Bars speaking |
|
| 25 | 25 | ext-workspace-v1 and wlr-layer-shell, such as *waybar*(5), work out of the box. |
|
| 26 | 26 | Users are encouraged to customize *swm* by editing the sources, in particular |
|
| 27 | 27 | _config.h_. |
|
| 28 | 28 | ||
| 29 | + | *swmctl*(1) can associate additional ephemeral titles and colors with |
|
| 30 | + | workspaces. These do not replace the numbered ext-workspace names and are only |
|
| 31 | + | shown by explicitly configured consumers. All workspace metadata is discarded |
|
| 32 | + | when *swm* exits. |
|
| 33 | + | ||
| 29 | 34 | == OPTIONS |
|
| 30 | 35 | ||
| 31 | 36 | *-v*:: |
|
| 32 | 37 | Write the name and version to standard error and exit unsuccessfully. |
|
| 33 | 38 |
| 105 | 110 | Window focus follows the mouse. Windows can be moved with Mod-left-click and |
|
| 106 | 111 | resized with Mod-right-click. Mod-middle-click toggles floating. |
|
| 107 | 112 | ||
| 108 | 113 | == SEE ALSO |
|
| 109 | 114 | ||
| 110 | - | *foot*(1), *wmenu*(1) |
|
| 115 | + | *foot*(1), *swmctl*(1), *wmenu*(1) |
|
| 111 | 116 | ||
| 112 | 117 | == AUTHORS |
|
| 113 | 118 | ||
| 114 | 119 | *swm* is a fork of *dwl*; see the LICENSE files for its authors. |
swm.c
+178 -15
| 81 | 81 | #include <xcb/xcb.h> |
|
| 82 | 82 | #include <xcb/xcb_icccm.h> |
|
| 83 | 83 | #include <xkbcommon/xkbcommon.h> |
|
| 84 | 84 | ||
| 85 | 85 | #include "ext-workspace-v1-protocol.h" |
|
| 86 | + | #include "swm-workspace-v1-protocol.h" |
|
| 86 | 87 | #include "swm.h" |
|
| 87 | 88 | #include "util.h" |
|
| 88 | 89 | #include "xdg-shell-protocol.h" |
|
| 89 | 90 | ||
| 90 | 91 | /* Constants and helper macros. */ |
| 107 | 108 | #define MAX_PENDING_SPAWNS 256 |
|
| 108 | 109 | #define MAX_WINDOW_STATES 512 |
|
| 109 | 110 | #define MAX_WINDOW_STATE_FIELD 256 |
|
| 110 | 111 | #define MAX_WINDOW_STATE_LINE 1024 |
|
| 111 | 112 | #define MAX_STATE_PATH 4096 |
|
| 113 | + | #define MAX_WORKSPACE_TITLE 256 |
|
| 112 | 114 | #define MAX_WS_ID 16 |
|
| 113 | 115 | #define MAX_STATIC_LISTENERS 512 |
|
| 114 | 116 | #define MAX_WS_MANAGERS 64 |
|
| 115 | 117 | #define MAX_WS_HANDLES (MAX_WS_MANAGERS * WSCOUNT) |
|
| 116 | 118 | #define MAX_AUTOSTART 64 |
| 316 | 318 | } stack_state_t; |
|
| 317 | 319 | ||
| 318 | 320 | /* A workspace owns its windows and layout settings, and can appear on only one display. */ |
|
| 319 | 321 | struct workspace_t { |
|
| 320 | 322 | char name[4]; |
|
| 323 | + | char title[MAX_WORKSPACE_TITLE]; /* Ephemeral human-readable label. */ |
|
| 324 | + | uint32_t color; /* Ephemeral color encoded as 0xRRGGBBAA; zero means unset. */ |
|
| 321 | 325 | int idx; |
|
| 322 | 326 | monitor_t *mon; /* Display showing this workspace, or nullptr if hidden. */ |
|
| 323 | 327 | const layout_t *lt; |
|
| 324 | 328 | const layout_t *prevlt; |
|
| 325 | 329 | stack_state_t v, h; |
| 341 | 345 | workspace_t *ws; |
|
| 342 | 346 | workspace_manager_t *mgr; /* nullptr after the manager is destroyed. */ |
|
| 343 | 347 | struct wl_list link; /* Entry in workspace_t.handles. */ |
|
| 344 | 348 | } workspace_handle_t; |
|
| 345 | 349 | ||
| 350 | + | /* Bound client for swm's ephemeral workspace metadata protocol. */ |
|
| 351 | + | typedef struct { |
|
| 352 | + | struct wl_resource *resource; |
|
| 353 | + | struct wl_list link; |
|
| 354 | + | } metadata_manager_t; |
|
| 355 | + | ||
| 346 | 356 | /* Spawn awaiting workspace assignment. */ |
|
| 347 | 357 | typedef struct { |
|
| 348 | 358 | pid_t pid; |
|
| 349 | 359 | workspace_t *ws; |
|
| 350 | 360 | struct wl_list link; |
| 573 | 583 | static bool clients_related(client_t *a, client_t *b); |
|
| 574 | 584 | static void assign_workspace(workspace_t *ws, monitor_t *m); |
|
| 575 | 585 | static workspace_t *free_workspace(void); |
|
| 576 | 586 | static void view_workspace(workspace_t *ws, monitor_t *m); |
|
| 577 | 587 | static void workspace_broadcast(void); |
|
| 578 | - | static void workspace_handle_create(workspace_manager_t *mgr, workspace_t *ws); |
|
| 579 | - | static void workspace_manager_bind( |
|
| 588 | + | static void workspace_metadata_bind( |
|
| 589 | + | struct wl_client *client, void *data, uint32_t version, uint32_t id |
|
| 590 | + | ); |
|
| 591 | + | static void workspace_metadata_broadcast(void); |
|
| 592 | + | static void workspace_handle_create(workspace_manager_t *mgr, workspace_t *ws); |
|
| 593 | + | static void workspace_manager_bind( |
|
| 580 | 594 | struct wl_client *client, void *data, uint32_t version, uint32_t id |
|
| 581 | 595 | ); |
|
| 582 | 596 | static void workspace_output_bind(struct wl_listener *listener, void *data); |
|
| 583 | 597 | static uint32_t workspace_state(workspace_t *ws); |
|
| 584 | 598 | static void set_primary_selection(struct wl_listener *listener, void *data); |
| 693 | 707 | static struct wlr_box grabgeom; |
|
| 694 | 708 | ||
| 695 | 709 | static struct wlr_output_layout *output_layout; |
|
| 696 | 710 | static struct wlr_box sgeom; |
|
| 697 | 711 | static struct wl_list mons; |
|
| 698 | - | static struct wl_list ws_managers; /* workspace_manager_t.link */ |
|
| 699 | - | static struct wl_list pending_spawns; /* pending_spawn_t.link */ |
|
| 700 | - | static struct wl_list window_states; /* window_state_t.link */ |
|
| 701 | - | static struct wl_list input_popups; /* input_popup_t.link */ |
|
| 712 | + | static struct wl_list ws_managers; /* workspace_manager_t.link */ |
|
| 713 | + | static struct wl_list metadata_managers; /* metadata_manager_t.link */ |
|
| 714 | + | static struct wl_list pending_spawns; /* pending_spawn_t.link */ |
|
| 715 | + | static struct wl_list window_states; /* window_state_t.link */ |
|
| 716 | + | static struct wl_list input_popups; /* input_popup_t.link */ |
|
| 702 | 717 | static int virtual_keyboards; |
|
| 703 | 718 | static monitor_t *selmon; |
|
| 704 | 719 | static bool arranging_pointer_focus; |
|
| 705 | 720 | ||
| 706 | 721 | /* Event handlers shared for the lifetime of the compositor. */ |
| 808 | 823 | static pending_spawn_t pending_spawn_items[MAX_PENDING_SPAWNS]; |
|
| 809 | 824 | static window_state_t window_state_items[MAX_WINDOW_STATES]; |
|
| 810 | 825 | static static_listener_t static_listener_items[MAX_STATIC_LISTENERS]; |
|
| 811 | 826 | static workspace_manager_t workspace_manager_items[MAX_WS_MANAGERS]; |
|
| 812 | 827 | static workspace_handle_t workspace_handle_items[MAX_WS_HANDLES]; |
|
| 828 | + | static metadata_manager_t metadata_manager_items[MAX_WS_MANAGERS]; |
|
| 813 | 829 | ||
| 814 | 830 | static bool client_used[MAX_CLIENTS], monitor_used[MAX_MONITORS]; |
|
| 815 | 831 | static bool layer_surface_used[MAX_LAYER_SURFACES], keyboard_group_used[MAX_KEYBOARD_GROUPS]; |
|
| 816 | 832 | static bool pointer_constraint_used[MAX_POINTER_CONSTRAINTS], text_input_used[MAX_TEXT_INPUTS]; |
|
| 817 | 833 | static bool input_popup_used[MAX_INPUT_POPUPS], session_lock_used[MAX_SESSION_LOCKS]; |
|
| 818 | 834 | static bool pending_spawn_used[MAX_PENDING_SPAWNS], window_state_used[MAX_WINDOW_STATES]; |
|
| 819 | 835 | static bool static_listener_used[MAX_STATIC_LISTENERS], workspace_manager_used[MAX_WS_MANAGERS]; |
|
| 820 | - | static bool workspace_handle_used[MAX_WS_HANDLES]; |
|
| 836 | + | static bool workspace_handle_used[MAX_WS_HANDLES], metadata_manager_used[MAX_WS_MANAGERS]; |
|
| 821 | 837 | ||
| 822 | 838 | static pool_t client_pool = { |
|
| 823 | 839 | client_items, client_used, LENGTH(client_items), sizeof *client_items, "client" |
|
| 824 | 840 | }; |
|
| 825 | 841 | static pool_t monitor_pool = { |
| 878 | 894 | static pool_t workspace_handle_pool = { workspace_handle_items, |
|
| 879 | 895 | workspace_handle_used, |
|
| 880 | 896 | LENGTH(workspace_handle_items), |
|
| 881 | 897 | sizeof *workspace_handle_items, |
|
| 882 | 898 | "workspace_handle" }; |
|
| 899 | + | static pool_t metadata_manager_pool = { metadata_manager_items, |
|
| 900 | + | metadata_manager_used, |
|
| 901 | + | LENGTH(metadata_manager_items), |
|
| 902 | + | sizeof *metadata_manager_items, |
|
| 903 | + | "metadata_manager" }; |
|
| 883 | 904 | ||
| 884 | 905 | /* Backend-specific client operations. */ |
|
| 885 | 906 | /* Client helpers shared by the XDG shell and Xwayland implementations. These |
|
| 886 | 907 | * are inline so unused helpers are omitted for builds that disable a backend. */ |
|
| 887 | 908 |
| 4451 | 4472 | client_t *c; |
|
| 4452 | 4473 | FILE *titlefile; |
|
| 4453 | 4474 | char titlepath[4096], tmppath[4096], title[1024], appid[1024]; |
|
| 4454 | 4475 | const char *runtime, *src; |
|
| 4455 | 4476 | uint32_t occ, urg; |
|
| 4477 | + | int i; |
|
| 4456 | 4478 | ||
| 4457 | 4479 | occ = urg = 0; |
|
| 4458 | 4480 | wl_list_for_each(c, &clients, link) { |
|
| 4459 | 4481 | ftl_sync(c); |
|
| 4460 | 4482 |
| 4488 | 4510 | m->ws ? 1u << m->ws->idx : 0, |
|
| 4489 | 4511 | m->ws ? 1u << m->ws->idx : 0, |
|
| 4490 | 4512 | urg |
|
| 4491 | 4513 | ); |
|
| 4492 | 4514 | } |
|
| 4515 | + | for (i = 0; i < WSCOUNT; i++) { |
|
| 4516 | + | status_field(title, sizeof(title), workspaces[i].title); |
|
| 4517 | + | printf("workspace %d title %s\n", i + 1, title); |
|
| 4518 | + | if (workspaces[i].color) |
|
| 4519 | + | printf("workspace %d color #%08" PRIx32 "\n", i + 1, workspaces[i].color); |
|
| 4520 | + | else |
|
| 4521 | + | printf("workspace %d color \n", i + 1); |
|
| 4522 | + | } |
|
| 4493 | 4523 | runtime = getenv("XDG_RUNTIME_DIR"); |
|
| 4494 | 4524 | publish_windows(runtime); |
|
| 4495 | 4525 | ||
| 4496 | 4526 | if (runtime && selmon) { |
|
| 4497 | 4527 | src = (c = focus_top(selmon)) ? client_get_title(c) : ""; |
| 5030 | 5060 | * recently focused client. */ |
|
| 5031 | 5061 | focus_client(focus_top(m), 1); |
|
| 5032 | 5062 | print_status(); |
|
| 5033 | 5063 | } |
|
| 5034 | 5064 | ||
| 5065 | + | /* Send one workspace's ephemeral metadata to a control client. */ |
|
| 5066 | + | static void workspace_metadata_send(struct wl_resource *resource, workspace_t *ws) { |
|
| 5067 | + | uint32_t flags = 0; |
|
| 5068 | + | ||
| 5069 | + | if (ws->title[0]) |
|
| 5070 | + | flags |= SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE; |
|
| 5071 | + | if (ws->color) |
|
| 5072 | + | flags |= SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR; |
|
| 5073 | + | if (selmon && selmon->ws == ws) |
|
| 5074 | + | flags |= SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED; |
|
| 5075 | + | swm_workspace_manager_v1_send_metadata( |
|
| 5076 | + | resource, (uint32_t)ws->idx + 1, flags, ws->title, ws->color |
|
| 5077 | + | ); |
|
| 5078 | + | } |
|
| 5079 | + | ||
| 5080 | + | /* Send a complete workspace metadata snapshot to one control client. */ |
|
| 5081 | + | static void workspace_metadata_snapshot(struct wl_resource *resource) { |
|
| 5082 | + | int i; |
|
| 5083 | + | ||
| 5084 | + | for (i = 0; i < WSCOUNT; i++) |
|
| 5085 | + | workspace_metadata_send(resource, &workspaces[i]); |
|
| 5086 | + | swm_workspace_manager_v1_send_done(resource); |
|
| 5087 | + | } |
|
| 5088 | + | ||
| 5089 | + | /* Publish all workspace metadata to every connected control client. */ |
|
| 5090 | + | static void workspace_metadata_broadcast(void) { |
|
| 5091 | + | metadata_manager_t *mgr; |
|
| 5092 | + | ||
| 5093 | + | wl_list_for_each(mgr, &metadata_managers, link) workspace_metadata_snapshot(mgr->resource); |
|
| 5094 | + | } |
|
| 5095 | + | ||
| 5096 | + | /* Return a requested workspace or report a protocol error. */ |
|
| 5097 | + | static workspace_t *workspace_metadata_get(struct wl_resource *resource, uint32_t number) { |
|
| 5098 | + | if (!number || number > WSCOUNT) { |
|
| 5099 | + | wl_resource_post_error( |
|
| 5100 | + | resource, |
|
| 5101 | + | SWM_WORKSPACE_MANAGER_V1_ERROR_INVALID_WORKSPACE, |
|
| 5102 | + | "workspace %" PRIu32 " is outside 1..%d", |
|
| 5103 | + | number, |
|
| 5104 | + | WSCOUNT |
|
| 5105 | + | ); |
|
| 5106 | + | return nullptr; |
|
| 5107 | + | } |
|
| 5108 | + | return &workspaces[number - 1]; |
|
| 5109 | + | } |
|
| 5110 | + | ||
| 5111 | + | /* Change an ephemeral workspace title. */ |
|
| 5112 | + | static void workspace_metadata_set_title( |
|
| 5113 | + | struct wl_client *client, struct wl_resource *resource, uint32_t number, const char *title |
|
| 5114 | + | ) { |
|
| 5115 | + | workspace_t *ws = workspace_metadata_get(resource, number); |
|
| 5116 | + | ||
| 5117 | + | if (!ws) |
|
| 5118 | + | return; |
|
| 5119 | + | if (strlen(title) >= sizeof(ws->title)) { |
|
| 5120 | + | wl_resource_post_error( |
|
| 5121 | + | resource, |
|
| 5122 | + | SWM_WORKSPACE_MANAGER_V1_ERROR_TITLE_TOO_LONG, |
|
| 5123 | + | "workspace title exceeds %zu bytes", |
|
| 5124 | + | sizeof(ws->title) - 1 |
|
| 5125 | + | ); |
|
| 5126 | + | return; |
|
| 5127 | + | } |
|
| 5128 | + | if (!strcmp(ws->title, title)) |
|
| 5129 | + | return; |
|
| 5130 | + | strcpy(ws->title, title); |
|
| 5131 | + | print_status(); |
|
| 5132 | + | } |
|
| 5133 | + | ||
| 5134 | + | /* Change an ephemeral workspace color. */ |
|
| 5135 | + | static void workspace_metadata_set_color( |
|
| 5136 | + | struct wl_client *client, struct wl_resource *resource, uint32_t number, uint32_t color |
|
| 5137 | + | ) { |
|
| 5138 | + | workspace_t *ws = workspace_metadata_get(resource, number); |
|
| 5139 | + | ||
| 5140 | + | if (!ws) |
|
| 5141 | + | return; |
|
| 5142 | + | if (ws->color == color) |
|
| 5143 | + | return; |
|
| 5144 | + | ws->color = color; |
|
| 5145 | + | print_status(); |
|
| 5146 | + | } |
|
| 5147 | + | ||
| 5148 | + | /* Release an ephemeral workspace metadata manager. */ |
|
| 5149 | + | static void workspace_metadata_destroy_request( |
|
| 5150 | + | struct wl_client *client, struct wl_resource *resource |
|
| 5151 | + | ) { |
|
| 5152 | + | wl_resource_destroy(resource); |
|
| 5153 | + | } |
|
| 5154 | + | ||
| 5155 | + | static const struct swm_workspace_manager_v1_interface workspace_metadata_impl = { |
|
| 5156 | + | .set_title = workspace_metadata_set_title, |
|
| 5157 | + | .set_color = workspace_metadata_set_color, |
|
| 5158 | + | .destroy = workspace_metadata_destroy_request, |
|
| 5159 | + | }; |
|
| 5160 | + | ||
| 5161 | + | /* Stop publishing workspace metadata after a client disconnects. */ |
|
| 5162 | + | static void workspace_metadata_destroy(struct wl_resource *resource) { |
|
| 5163 | + | metadata_manager_t *mgr = wl_resource_get_user_data(resource); |
|
| 5164 | + | ||
| 5165 | + | if (!mgr) |
|
| 5166 | + | return; |
|
| 5167 | + | wl_list_remove(&mgr->link); |
|
| 5168 | + | pool_release(&metadata_manager_pool, mgr); |
|
| 5169 | + | } |
|
| 5170 | + | ||
| 5171 | + | /* Publish all ephemeral workspace metadata to a newly connected client. */ |
|
| 5172 | + | static void workspace_metadata_bind( |
|
| 5173 | + | struct wl_client *client, void *data, uint32_t version, uint32_t id |
|
| 5174 | + | ) { |
|
| 5175 | + | metadata_manager_t *mgr = pool_take(&metadata_manager_pool); |
|
| 5176 | + | ||
| 5177 | + | if (!mgr) { |
|
| 5178 | + | wl_client_post_no_memory(client); |
|
| 5179 | + | return; |
|
| 5180 | + | } |
|
| 5181 | + | mgr->resource = |
|
| 5182 | + | wl_resource_create(client, &swm_workspace_manager_v1_interface, (int)version, id); |
|
| 5183 | + | ||
| 5184 | + | if (!mgr->resource) { |
|
| 5185 | + | pool_release(&metadata_manager_pool, mgr); |
|
| 5186 | + | wl_client_post_no_memory(client); |
|
| 5187 | + | return; |
|
| 5188 | + | } |
|
| 5189 | + | wl_resource_set_implementation( |
|
| 5190 | + | mgr->resource, &workspace_metadata_impl, mgr, workspace_metadata_destroy |
|
| 5191 | + | ); |
|
| 5192 | + | wl_list_insert(&metadata_managers, &mgr->link); |
|
| 5193 | + | workspace_metadata_snapshot(mgr->resource); |
|
| 5194 | + | } |
|
| 5195 | + | ||
| 5035 | 5196 | /* |
|
| 5036 | 5197 | * ext-workspace-v1 implementation. wlroots 0.19 ships no helper for this |
|
| 5037 | 5198 | * protocol, so the resources are managed by hand. swm advertises a single |
|
| 5038 | 5199 | * workspace group spanning all outputs, containing every workspace; the |
|
| 5039 | 5200 | * only capability is activate, which behaves like a workspace switch. |
| 5289 | 5450 | workspace_manager_t *mgr; |
|
| 5290 | 5451 | workspace_handle_t *h; |
|
| 5291 | 5452 | uint32_t state; |
|
| 5292 | 5453 | int i; |
|
| 5293 | 5454 | ||
| 5294 | - | if (wl_list_empty(&ws_managers)) |
|
| 5295 | - | return; |
|
| 5296 | - | ||
| 5297 | - | for (i = 0; i < WSCOUNT; i++) { |
|
| 5298 | - | state = workspace_state(&workspaces[i]); |
|
| 5299 | - | wl_list_for_each(h, &workspaces[i].handles, link) |
|
| 5300 | - | ext_workspace_handle_v1_send_state(h->resource, state); |
|
| 5455 | + | if (!wl_list_empty(&ws_managers)) { |
|
| 5456 | + | for (i = 0; i < WSCOUNT; i++) { |
|
| 5457 | + | state = workspace_state(&workspaces[i]); |
|
| 5458 | + | wl_list_for_each(h, &workspaces[i].handles, link) |
|
| 5459 | + | ext_workspace_handle_v1_send_state(h->resource, state); |
|
| 5460 | + | } |
|
| 5461 | + | wl_list_for_each(mgr, &ws_managers, link) ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5301 | 5462 | } |
|
| 5302 | - | wl_list_for_each(mgr, &ws_managers, link) ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5463 | + | workspace_metadata_broadcast(); |
|
| 5303 | 5464 | } |
|
| 5304 | 5465 | ||
| 5305 | 5466 | /* Accept an application's request to replace the primary selection. */ |
|
| 5306 | 5467 | void set_primary_selection(struct wl_listener *listener, void *data) { |
|
| 5307 | 5468 | /* Honor an application's request to set the primary selection. */ |
| 5428 | 5589 | workspaces[i].prevlt = nullptr; |
|
| 5429 | 5590 | workspaces[i].v = workspaces[i].h = (stack_state_t){ SLICE / 2, 1, 1 }; |
|
| 5430 | 5591 | wl_list_init(&workspaces[i].handles); |
|
| 5431 | 5592 | } |
|
| 5432 | 5593 | wl_list_init(&ws_managers); |
|
| 5594 | + | wl_list_init(&metadata_managers); |
|
| 5433 | 5595 | wl_list_init(&pending_spawns); |
|
| 5434 | 5596 | wl_list_init(&window_states); |
|
| 5435 | 5597 | wl_list_init(&input_popups); |
|
| 5436 | 5598 | load_window_states(); |
|
| 5437 | 5599 | wl_global_create(dpy, &ext_workspace_manager_v1_interface, 1, nullptr, workspace_manager_bind); |
|
| 5600 | + | wl_global_create(dpy, &swm_workspace_manager_v1_interface, 1, nullptr, workspace_metadata_bind); |
|
| 5438 | 5601 | ||
| 5439 | 5602 | /* Publish window lists for taskbars and switchers. */ |
|
| 5440 | 5603 | ftl_mgr = wlr_foreign_toplevel_manager_v1_create(dpy); |
|
| 5441 | 5604 | ext_ftl_list = wlr_ext_foreign_toplevel_list_v1_create(dpy, 1); |
|
| 5442 | 5605 | wlr_ext_image_copy_capture_manager_v1_create(dpy, 1); |
swmctl.1.adoc
added
+86 -0
| 1 | + | = swmctl(1) |
|
| 2 | + | :doctype: manpage |
|
| 3 | + | :manmanual: SWM Manual |
|
| 4 | + | :mansource: swm 1.0.0 |
|
| 5 | + | ||
| 6 | + | == NAME |
|
| 7 | + | ||
| 8 | + | swmctl - inspect and change swm workspace metadata |
|
| 9 | + | ||
| 10 | + | == SYNOPSIS |
|
| 11 | + | ||
| 12 | + | *swmctl workspace list* |
|
| 13 | + | ||
| 14 | + | *swmctl workspace get* _N_ (*title*|*color*|*selected*) |
|
| 15 | + | ||
| 16 | + | *swmctl workspace set* _N_ (*title* _TITLE_|*color* _#RRGGBB[AA]_) |
|
| 17 | + | ||
| 18 | + | *swmctl workspace clear* _N_ (*title*|*color*) |
|
| 19 | + | ||
| 20 | + | *swmctl workspace subscribe* [*--format=waybar*] |
|
| 21 | + | ||
| 22 | + | == DESCRIPTION |
|
| 23 | + | ||
| 24 | + | *swmctl* connects to the running *swm* Wayland compositor and reads or changes |
|
| 25 | + | the title and color associated with a workspace. This metadata is ephemeral: it |
|
| 26 | + | is stored only in compositor memory and is discarded when *swm* exits. |
|
| 27 | + | ||
| 28 | + | An empty title clears the additional label. It does not change the numeric |
|
| 29 | + | workspace name. A cleared color tells consumers to use their default color. |
|
| 30 | + | Colors use six-digit RGB or eight-digit RGBA notation. RGB implies full |
|
| 31 | + | opacity. `#00000000` means unset, while `#000000` represents opaque black. |
|
| 32 | + | ||
| 33 | + | *set* changes one named field. *clear* clears one named field. |
|
| 34 | + | ||
| 35 | + | *get* prints one field value. An unset title or color produces an empty line; |
|
| 36 | + | *selected* produces `true` or `false`. |
|
| 37 | + | ||
| 38 | + | *subscribe* writes a JSON array containing the current metadata, then writes a |
|
| 39 | + | new array after every change. Each array occupies one line, making the command |
|
| 40 | + | suitable for custom bars and other long-running consumers. |
|
| 41 | + | ||
| 42 | + | With *--format=waybar*, *subscribe* instead writes Waybar custom-module JSON |
|
| 43 | + | for the workspace on swm's selected output. Its additional title becomes the |
|
| 44 | + | module text and its color becomes the text foreground. An unset title produces |
|
| 45 | + | empty text. Configure the module with `return-type` set to `json` and `escape` |
|
| 46 | + | set to `false`. |
|
| 47 | + | ||
| 48 | + | == WAYBAR |
|
| 49 | + | ||
| 50 | + | The `ext/workspaces` module continues to display the numeric workspace names |
|
| 51 | + | `1` through `10`. A custom module can display the additional title and color |
|
| 52 | + | independently: |
|
| 53 | + | ||
| 54 | + | "modules-center": ["custom/workspace-title"], |
|
| 55 | + | "custom/workspace-title": { |
|
| 56 | + | "exec": "swmctl workspace subscribe --format=waybar", |
|
| 57 | + | "return-type": "json", |
|
| 58 | + | "escape": false, |
|
| 59 | + | "hide-empty-text": true |
|
| 60 | + | } |
|
| 61 | + | ||
| 62 | + | The custom module follows the workspace on swm's selected output and uses its |
|
| 63 | + | color for the title text. |
|
| 64 | + | ||
| 65 | + | == EXAMPLES |
|
| 66 | + | ||
| 67 | + | Set both fields for workspace 3: |
|
| 68 | + | ||
| 69 | + | swmctl workspace set 3 title "Prototyping new window manager" |
|
| 70 | + | swmctl workspace set 3 color "#81a1c1" |
|
| 71 | + | ||
| 72 | + | Print its title: |
|
| 73 | + | ||
| 74 | + | swmctl workspace get 3 title |
|
| 75 | + | ||
| 76 | + | Clear only its color: |
|
| 77 | + | ||
| 78 | + | swmctl workspace clear 3 color |
|
| 79 | + | ||
| 80 | + | List every workspace number: |
|
| 81 | + | ||
| 82 | + | swmctl workspace list |
|
| 83 | + | ||
| 84 | + | == SEE ALSO |
|
| 85 | + | ||
| 86 | + | *swm*(1), *waybar*(5) |
swmctl.c
added
+393 -0
| 1 | + | /* |
|
| 2 | + | * swmctl.c |
|
| 3 | + | * Control and inspect swm's state. |
|
| 4 | + | */ |
|
| 5 | + | #include <errno.h> |
|
| 6 | + | #include <inttypes.h> |
|
| 7 | + | #include <stdbool.h> |
|
| 8 | + | #include <stdint.h> |
|
| 9 | + | #include <stdio.h> |
|
| 10 | + | #include <stdlib.h> |
|
| 11 | + | #include <string.h> |
|
| 12 | + | #include <wayland-client.h> |
|
| 13 | + | ||
| 14 | + | #include "swm-workspace-v1-client-protocol.h" |
|
| 15 | + | ||
| 16 | + | #define MAX_WORKSPACES 32 |
|
| 17 | + | #define MAX_WORKSPACE_TITLE 256 |
|
| 18 | + | ||
| 19 | + | typedef struct { |
|
| 20 | + | bool seen; |
|
| 21 | + | uint32_t flags; |
|
| 22 | + | char title[MAX_WORKSPACE_TITLE]; |
|
| 23 | + | uint32_t color; |
|
| 24 | + | } workspace_t; |
|
| 25 | + | ||
| 26 | + | static struct wl_display *display; |
|
| 27 | + | static struct swm_workspace_manager_v1 *manager; |
|
| 28 | + | static workspace_t workspaces[MAX_WORKSPACES]; |
|
| 29 | + | static bool subscribing; |
|
| 30 | + | static bool waybar; |
|
| 31 | + | ||
| 32 | + | [[noreturn]] static void usage(void) { |
|
| 33 | + | fprintf( |
|
| 34 | + | stderr, |
|
| 35 | + | "usage:\n" |
|
| 36 | + | " swmctl workspace list\n" |
|
| 37 | + | " swmctl workspace get N (title | color | selected)\n" |
|
| 38 | + | " swmctl workspace set N (title TITLE | color COLOR)\n" |
|
| 39 | + | " swmctl workspace clear N (title | color)\n" |
|
| 40 | + | " swmctl workspace subscribe [--format=waybar]\n" |
|
| 41 | + | ); |
|
| 42 | + | exit(EXIT_FAILURE); |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | /* Print a color in its shortest lossless notation. */ |
|
| 46 | + | static void print_color(uint32_t color) { |
|
| 47 | + | if ((color & 0xff) == 0xff) |
|
| 48 | + | printf("#%06" PRIx32, color >> 8); |
|
| 49 | + | else |
|
| 50 | + | printf("#%08" PRIx32, color); |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | /* Write a string with JSON quoting. */ |
|
| 54 | + | static void json_string(const char *value) { |
|
| 55 | + | const unsigned char *p; |
|
| 56 | + | ||
| 57 | + | putchar('"'); |
|
| 58 | + | for (p = (const unsigned char *)value; *p; p++) { |
|
| 59 | + | if (*p == '"' || *p == '\\') |
|
| 60 | + | printf("\\%c", *p); |
|
| 61 | + | else if (*p == '\n') |
|
| 62 | + | fputs("\\n", stdout); |
|
| 63 | + | else if (*p == '\r') |
|
| 64 | + | fputs("\\r", stdout); |
|
| 65 | + | else if (*p == '\t') |
|
| 66 | + | fputs("\\t", stdout); |
|
| 67 | + | else if (*p < 0x20) |
|
| 68 | + | printf("\\u%04x", *p); |
|
| 69 | + | else |
|
| 70 | + | putchar(*p); |
|
| 71 | + | } |
|
| 72 | + | putchar('"'); |
|
| 73 | + | } |
|
| 74 | + | ||
| 75 | + | /* Print one workspace as a JSON object. */ |
|
| 76 | + | static void print_json_workspace(uint32_t number, workspace_t *ws) { |
|
| 77 | + | printf("{\"workspace\":%" PRIu32 ",\"title\":", number); |
|
| 78 | + | if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE) |
|
| 79 | + | json_string(ws->title); |
|
| 80 | + | else |
|
| 81 | + | fputs("null", stdout); |
|
| 82 | + | fputs(",\"color\":", stdout); |
|
| 83 | + | if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR) { |
|
| 84 | + | putchar('"'); |
|
| 85 | + | print_color(ws->color); |
|
| 86 | + | putchar('"'); |
|
| 87 | + | } else { |
|
| 88 | + | fputs("null", stdout); |
|
| 89 | + | } |
|
| 90 | + | printf( |
|
| 91 | + | ",\"selected\":%s}", |
|
| 92 | + | ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED ? "true" : "false" |
|
| 93 | + | ); |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | /* Write a JSON string safe for use as Pango markup in Waybar. */ |
|
| 97 | + | static void json_markup_string(const char *value, bool has_color, uint32_t color) { |
|
| 98 | + | const unsigned char *p; |
|
| 99 | + | ||
| 100 | + | putchar('"'); |
|
| 101 | + | if (has_color) { |
|
| 102 | + | printf("<span foreground=\\\"#%06" PRIx32 "\\\"", color >> 8); |
|
| 103 | + | if ((color & 0xff) != 0xff) |
|
| 104 | + | printf(" foreground_alpha=\\\"%" PRIu32 "\\\"", (color & 0xff) * 257); |
|
| 105 | + | putchar('>'); |
|
| 106 | + | } |
|
| 107 | + | for (p = (const unsigned char *)value; *p; p++) { |
|
| 108 | + | if (*p == '&') |
|
| 109 | + | fputs("&", stdout); |
|
| 110 | + | else if (*p == '<') |
|
| 111 | + | fputs("<", stdout); |
|
| 112 | + | else if (*p == '>') |
|
| 113 | + | fputs(">", stdout); |
|
| 114 | + | else if (*p == '"') |
|
| 115 | + | fputs(""", stdout); |
|
| 116 | + | else if (*p == '\'') |
|
| 117 | + | fputs("'", stdout); |
|
| 118 | + | else if (*p == '\\') |
|
| 119 | + | fputs("\\\\", stdout); |
|
| 120 | + | else if (*p == '\n') |
|
| 121 | + | fputs("\\n", stdout); |
|
| 122 | + | else if (*p == '\r') |
|
| 123 | + | fputs("\\r", stdout); |
|
| 124 | + | else if (*p == '\t') |
|
| 125 | + | fputs("\\t", stdout); |
|
| 126 | + | else if (*p < 0x20) |
|
| 127 | + | printf("\\u%04x", *p); |
|
| 128 | + | else |
|
| 129 | + | putchar(*p); |
|
| 130 | + | } |
|
| 131 | + | if (has_color) |
|
| 132 | + | fputs("</span>", stdout); |
|
| 133 | + | ||
| 134 | + | putchar('"'); |
|
| 135 | + | } |
|
| 136 | + | ||
| 137 | + | /* Print the selected workspace label as a Waybar custom-module update. */ |
|
| 138 | + | static void print_waybar(void) { |
|
| 139 | + | workspace_t *selected = nullptr; |
|
| 140 | + | uint32_t number = 0; |
|
| 141 | + | bool has_title; |
|
| 142 | + | ||
| 143 | + | for (uint32_t i = 0; i < MAX_WORKSPACES; i++) { |
|
| 144 | + | if (workspaces[i].seen && |
|
| 145 | + | workspaces[i].flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED) { |
|
| 146 | + | selected = &workspaces[i]; |
|
| 147 | + | number = i + 1; |
|
| 148 | + | break; |
|
| 149 | + | } |
|
| 150 | + | } |
|
| 151 | + | has_title = selected && selected->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE; |
|
| 152 | + | fputs("{\"text\":", stdout); |
|
| 153 | + | json_markup_string( |
|
| 154 | + | has_title ? selected->title : "", |
|
| 155 | + | has_title && selected->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR, |
|
| 156 | + | selected ? selected->color : 0 |
|
| 157 | + | ); |
|
| 158 | + | printf(",\"tooltip\":\"Workspace %" PRIu32 "\"}\n", number); |
|
| 159 | + | fflush(stdout); |
|
| 160 | + | } |
|
| 161 | + | ||
| 162 | + | /* Print the complete cached workspace metadata as one JSON value. */ |
|
| 163 | + | static void print_json_workspaces(void) { |
|
| 164 | + | bool first = true; |
|
| 165 | + | ||
| 166 | + | putchar('['); |
|
| 167 | + | for (uint32_t i = 0; i < MAX_WORKSPACES; i++) { |
|
| 168 | + | if (!workspaces[i].seen) |
|
| 169 | + | continue; |
|
| 170 | + | if (!first) |
|
| 171 | + | putchar(','); |
|
| 172 | + | print_json_workspace(i + 1, &workspaces[i]); |
|
| 173 | + | first = false; |
|
| 174 | + | } |
|
| 175 | + | puts("]"); |
|
| 176 | + | fflush(stdout); |
|
| 177 | + | } |
|
| 178 | + | ||
| 179 | + | /* Cache one workspace metadata event. */ |
|
| 180 | + | static void metadata( |
|
| 181 | + | void *data, |
|
| 182 | + | struct swm_workspace_manager_v1 *object, |
|
| 183 | + | uint32_t number, |
|
| 184 | + | uint32_t flags, |
|
| 185 | + | const char *title, |
|
| 186 | + | uint32_t color |
|
| 187 | + | ) { |
|
| 188 | + | workspace_t *ws; |
|
| 189 | + | ||
| 190 | + | if (!number || number > MAX_WORKSPACES) |
|
| 191 | + | return; |
|
| 192 | + | ws = &workspaces[number - 1]; |
|
| 193 | + | ws->seen = true; |
|
| 194 | + | ws->flags = flags; |
|
| 195 | + | ws->color = color; |
|
| 196 | + | ||
| 197 | + | snprintf(ws->title, sizeof(ws->title), "%s", title); |
|
| 198 | + | } |
|
| 199 | + | ||
| 200 | + | /* Finish a snapshot, printing it when following changes. */ |
|
| 201 | + | static void done(void *data, struct swm_workspace_manager_v1 *object) { |
|
| 202 | + | if (!subscribing) |
|
| 203 | + | return; |
|
| 204 | + | if (waybar) |
|
| 205 | + | print_waybar(); |
|
| 206 | + | else |
|
| 207 | + | print_json_workspaces(); |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | static const struct swm_workspace_manager_v1_listener manager_listener = { |
|
| 211 | + | .metadata = metadata, |
|
| 212 | + | .done = done, |
|
| 213 | + | }; |
|
| 214 | + | ||
| 215 | + | /* Bind swm's workspace metadata global. */ |
|
| 216 | + | static void global( |
|
| 217 | + | void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version |
|
| 218 | + | ) { |
|
| 219 | + | if (strcmp(interface, swm_workspace_manager_v1_interface.name)) |
|
| 220 | + | return; |
|
| 221 | + | manager = wl_registry_bind(registry, name, &swm_workspace_manager_v1_interface, 1); |
|
| 222 | + | swm_workspace_manager_v1_add_listener(manager, &manager_listener, nullptr); |
|
| 223 | + | } |
|
| 224 | + | ||
| 225 | + | static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {} |
|
| 226 | + | ||
| 227 | + | static const struct wl_registry_listener registry_listener = { |
|
| 228 | + | .global = global, |
|
| 229 | + | .global_remove = global_remove, |
|
| 230 | + | }; |
|
| 231 | + | ||
| 232 | + | /* Parse a one-based workspace number. */ |
|
| 233 | + | static uint32_t parse_workspace(const char *value) { |
|
| 234 | + | char *end; |
|
| 235 | + | unsigned long number; |
|
| 236 | + | ||
| 237 | + | errno = 0; |
|
| 238 | + | number = strtoul(value, &end, 10); |
|
| 239 | + | ||
| 240 | + | if (errno || *end || !number || number > MAX_WORKSPACES) |
|
| 241 | + | usage(); |
|
| 242 | + | ||
| 243 | + | return (uint32_t)number; |
|
| 244 | + | } |
|
| 245 | + | ||
| 246 | + | /* Parse a color in #RRGGBB or #RRGGBBAA notation. */ |
|
| 247 | + | static uint32_t parse_color(const char *value) { |
|
| 248 | + | char *end; |
|
| 249 | + | unsigned long color; |
|
| 250 | + | size_t length = strlen(value); |
|
| 251 | + | ||
| 252 | + | if ((length != 7 && length != 9) || value[0] != '#') |
|
| 253 | + | usage(); |
|
| 254 | + | errno = 0; |
|
| 255 | + | color = strtoul(value + 1, &end, 16); |
|
| 256 | + | if (errno || *end || color > 0xffffffff) |
|
| 257 | + | usage(); |
|
| 258 | + | ||
| 259 | + | return length == 7 ? (uint32_t)(color << 8) | 0xff : (uint32_t)color; |
|
| 260 | + | } |
|
| 261 | + | ||
| 262 | + | /* Print one field from a cached workspace. */ |
|
| 263 | + | static void print_workspace_field(uint32_t number, const char *field) { |
|
| 264 | + | workspace_t *ws = &workspaces[number - 1]; |
|
| 265 | + | ||
| 266 | + | if (!strcmp(field, "title")) { |
|
| 267 | + | puts(ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE ? ws->title : ""); |
|
| 268 | + | } else if (!strcmp(field, "color")) { |
|
| 269 | + | if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR) |
|
| 270 | + | print_color(ws->color); |
|
| 271 | + | putchar('\n'); |
|
| 272 | + | } else { |
|
| 273 | + | puts(ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED ? "true" : "false"); |
|
| 274 | + | } |
|
| 275 | + | } |
|
| 276 | + | ||
| 277 | + | /* Print every cached workspace number. */ |
|
| 278 | + | static void print_workspaces(void) { |
|
| 279 | + | for (uint32_t i = 0; i < MAX_WORKSPACES; i++) { |
|
| 280 | + | if (workspaces[i].seen) |
|
| 281 | + | printf("%" PRIu32 "\n", i + 1); |
|
| 282 | + | } |
|
| 283 | + | } |
|
| 284 | + | ||
| 285 | + | /* Connect to swm and collect its initial metadata snapshot. */ |
|
| 286 | + | static void connect_manager(void) { |
|
| 287 | + | struct wl_registry *registry; |
|
| 288 | + | ||
| 289 | + | if (!(display = wl_display_connect(nullptr))) { |
|
| 290 | + | fprintf(stderr, "swmctl: cannot connect to Wayland display\n"); |
|
| 291 | + | exit(EXIT_FAILURE); |
|
| 292 | + | } |
|
| 293 | + | registry = wl_display_get_registry(display); |
|
| 294 | + | wl_registry_add_listener(registry, ®istry_listener, nullptr); |
|
| 295 | + | ||
| 296 | + | if (wl_display_roundtrip(display) < 0 || !manager) { |
|
| 297 | + | fprintf(stderr, "swmctl: compositor does not support swm-workspace-v1\n"); |
|
| 298 | + | exit(EXIT_FAILURE); |
|
| 299 | + | } |
|
| 300 | + | if (wl_display_roundtrip(display) < 0) { |
|
| 301 | + | fprintf(stderr, "swmctl: failed to read workspace metadata\n"); |
|
| 302 | + | exit(EXIT_FAILURE); |
|
| 303 | + | } |
|
| 304 | + | wl_registry_destroy(registry); |
|
| 305 | + | } |
|
| 306 | + | ||
| 307 | + | int main(int argc, char **argv) { |
|
| 308 | + | const char *command; |
|
| 309 | + | const char *field = nullptr; |
|
| 310 | + | const char *value = nullptr; |
|
| 311 | + | uint32_t workspace = 0; |
|
| 312 | + | ||
| 313 | + | if (argc < 3 || strcmp(argv[1], "workspace")) |
|
| 314 | + | usage(); |
|
| 315 | + | command = argv[2]; |
|
| 316 | + | subscribing = !strcmp(command, "subscribe"); |
|
| 317 | + | ||
| 318 | + | if (!strcmp(command, "list")) { |
|
| 319 | + | if (argc != 3) |
|
| 320 | + | usage(); |
|
| 321 | + | } else if (!strcmp(command, "get")) { |
|
| 322 | + | if (argc != 5) |
|
| 323 | + | usage(); |
|
| 324 | + | workspace = parse_workspace(argv[3]); |
|
| 325 | + | field = argv[4]; |
|
| 326 | + | if (strcmp(field, "title") && strcmp(field, "color") && strcmp(field, "selected")) |
|
| 327 | + | usage(); |
|
| 328 | + | } else if (!strcmp(command, "set")) { |
|
| 329 | + | if (argc != 6) |
|
| 330 | + | usage(); |
|
| 331 | + | workspace = parse_workspace(argv[3]); |
|
| 332 | + | field = argv[4]; |
|
| 333 | + | value = argv[5]; |
|
| 334 | + | if (!strcmp(field, "title")) { |
|
| 335 | + | if (strlen(value) >= MAX_WORKSPACE_TITLE) |
|
| 336 | + | usage(); |
|
| 337 | + | } else if (!strcmp(field, "color")) { |
|
| 338 | + | parse_color(value); |
|
| 339 | + | } else { |
|
| 340 | + | usage(); |
|
| 341 | + | } |
|
| 342 | + | } else if (!strcmp(command, "clear")) { |
|
| 343 | + | if (argc != 5) |
|
| 344 | + | usage(); |
|
| 345 | + | workspace = parse_workspace(argv[3]); |
|
| 346 | + | field = argv[4]; |
|
| 347 | + | if (strcmp(field, "title") && strcmp(field, "color")) |
|
| 348 | + | usage(); |
|
| 349 | + | } else if (subscribing) { |
|
| 350 | + | if (argc == 4 && !strcmp(argv[3], "--format=waybar")) |
|
| 351 | + | waybar = true; |
|
| 352 | + | else if (argc != 3) |
|
| 353 | + | usage(); |
|
| 354 | + | } else { |
|
| 355 | + | usage(); |
|
| 356 | + | } |
|
| 357 | + | connect_manager(); |
|
| 358 | + | ||
| 359 | + | if (subscribing) { |
|
| 360 | + | while (wl_display_dispatch(display) >= 0) |
|
| 361 | + | ; |
|
| 362 | + | return EXIT_FAILURE; |
|
| 363 | + | } |
|
| 364 | + | if (!strcmp(command, "list")) { |
|
| 365 | + | print_workspaces(); |
|
| 366 | + | } else { |
|
| 367 | + | if (!workspaces[workspace - 1].seen) { |
|
| 368 | + | fprintf(stderr, "swmctl: workspace %" PRIu32 " does not exist\n", workspace); |
|
| 369 | + | return EXIT_FAILURE; |
|
| 370 | + | } |
|
| 371 | + | if (!strcmp(command, "get")) { |
|
| 372 | + | print_workspace_field(workspace, field); |
|
| 373 | + | } else if (!strcmp(command, "set")) { |
|
| 374 | + | if (!strcmp(field, "title")) |
|
| 375 | + | swm_workspace_manager_v1_set_title(manager, workspace, value); |
|
| 376 | + | else |
|
| 377 | + | swm_workspace_manager_v1_set_color(manager, workspace, parse_color(value)); |
|
| 378 | + | } else { |
|
| 379 | + | if (!strcmp(field, "title")) |
|
| 380 | + | swm_workspace_manager_v1_set_title(manager, workspace, ""); |
|
| 381 | + | else |
|
| 382 | + | swm_workspace_manager_v1_set_color(manager, workspace, 0); |
|
| 383 | + | } |
|
| 384 | + | } |
|
| 385 | + | if (wl_display_flush(display) < 0 || wl_display_roundtrip(display) < 0) { |
|
| 386 | + | fprintf(stderr, "swmctl: command failed\n"); |
|
| 387 | + | return EXIT_FAILURE; |
|
| 388 | + | } |
|
| 389 | + | swm_workspace_manager_v1_destroy(manager); |
|
| 390 | + | wl_display_disconnect(display); |
|
| 391 | + | ||
| 392 | + | return EXIT_SUCCESS; |
|
| 393 | + | } |
test/run.py
+58 -1
| 188 | 188 | def prepare_suite(unit: bool, integration: bool) -> None: |
|
| 189 | 189 | """Prepare runtime files for the selected tests.""" |
|
| 190 | 190 | ||
| 191 | 191 | binaries = [ |
|
| 192 | 192 | BIN / name |
|
| 193 | - | for name, selected in (("unit", unit), ("swm", integration)) |
|
| 193 | + | for name, selected in (("unit", unit), ("swm", integration), ("swmctl", integration)) |
|
| 194 | 194 | if selected |
|
| 195 | 195 | ] |
|
| 196 | 196 | missing = [binary for binary in binaries if not binary.exists()] |
|
| 197 | 197 | if missing: |
|
| 198 | 198 | raise Failure(f"test binary not built: {missing[0]}; run tests through make") |
| 375 | 375 | virtual_keyboard(108) |
|
| 376 | 376 | wait_title(current) |
|
| 377 | 377 | ||
| 378 | 378 | test("workspaces", workspaces) |
|
| 379 | 379 | ||
| 380 | + | def workspace_metadata() -> None: |
|
| 381 | + | def field(name: str) -> str: |
|
| 382 | + | return run(BIN / "swmctl", "workspace", "get", 3, name).removesuffix("\n") |
|
| 383 | + | ||
| 384 | + | subscription_log = client_log("workspace-subscribe") |
|
| 385 | + | subscriber = spawn( |
|
| 386 | + | subscription_log, |
|
| 387 | + | BIN / "swmctl", |
|
| 388 | + | "workspace", |
|
| 389 | + | "subscribe", |
|
| 390 | + | "--format=waybar", |
|
| 391 | + | env=os.environ.copy(), |
|
| 392 | + | ) |
|
| 393 | + | eventually("initial workspace metadata", lambda: subscription_log.stat().st_size) |
|
| 394 | + | run(BIN / "swmctl", "workspace", "set", 3, "title", "code") |
|
| 395 | + | run(BIN / "swmctl", "workspace", "set", 3, "color", "#81a1c180") |
|
| 396 | + | state = { |
|
| 397 | + | "workspace": 3, |
|
| 398 | + | "title": field("title"), |
|
| 399 | + | "color": field("color"), |
|
| 400 | + | "selected": field("selected") == "true", |
|
| 401 | + | } |
|
| 402 | + | if state != { |
|
| 403 | + | "workspace": 3, |
|
| 404 | + | "title": "code", |
|
| 405 | + | "color": "#81a1c180", |
|
| 406 | + | "selected": False, |
|
| 407 | + | }: |
|
| 408 | + | raise Failure(f"unexpected workspace metadata: {state!r}") |
|
| 409 | + | listed = run(BIN / "swmctl", "workspace", "list").splitlines() |
|
| 410 | + | if "3" not in listed: |
|
| 411 | + | raise Failure(f"workspace missing from list: {listed!r}") |
|
| 412 | + | run(BIN / "swmctl", "workspace", "set", 3, "color", "#00000000") |
|
| 413 | + | if field("color"): |
|
| 414 | + | raise Failure("zero RGBA color was not cleared") |
|
| 415 | + | run(BIN / "swmctl", "workspace", "set", 3, "color", "#000000") |
|
| 416 | + | if field("color") != "#000000": |
|
| 417 | + | raise Failure("opaque black color was not retained") |
|
| 418 | + | run(BIN / "swmctl", "workspace", "clear", 3, "color") |
|
| 419 | + | if field("title") != "code" or field("color"): |
|
| 420 | + | raise Failure("workspace color was not cleared") |
|
| 421 | + | run(BIN / "swmctl", "workspace", "set", 3, "color", "#81a1c1") |
|
| 422 | + | protocol("workspace", 3) |
|
| 423 | + | eventually( |
|
| 424 | + | "subscribed workspace metadata", |
|
| 425 | + | lambda: "#81a1c1" in subscription_log.read_text() |
|
| 426 | + | and "code" in subscription_log.read_text(), |
|
| 427 | + | ) |
|
| 428 | + | run(BIN / "swmctl", "workspace", "clear", 3, "title") |
|
| 429 | + | run(BIN / "swmctl", "workspace", "clear", 3, "color") |
|
| 430 | + | if field("title") or field("color") or field("selected") != "true": |
|
| 431 | + | raise Failure("workspace metadata was not cleared") |
|
| 432 | + | protocol("workspace", 1) |
|
| 433 | + | terminate(subscriber) |
|
| 434 | + | ||
| 435 | + | test("workspace metadata", workspace_metadata) |
|
| 436 | + | ||
| 380 | 437 | def layout_configuration() -> None: |
|
| 381 | 438 | before_one = geometry("one") |
|
| 382 | 439 | before_two = geometry("two") |
|
| 383 | 440 | virtual_keyboard(36, 1) |
|
| 384 | 441 | eventually( |
test/unit.c
+5 -0
| 54 | 54 | test_pool(&pending_spawn_pool); |
|
| 55 | 55 | test_pool(&window_state_pool); |
|
| 56 | 56 | test_pool(&static_listener_pool); |
|
| 57 | 57 | test_pool(&workspace_manager_pool); |
|
| 58 | 58 | test_pool(&workspace_handle_pool); |
|
| 59 | + | test_pool(&metadata_manager_pool); |
|
| 59 | 60 | } |
|
| 60 | 61 | ||
| 61 | 62 | /* Require a geometry helper to return the expected rectangle. */ |
|
| 62 | 63 | static void assert_box(struct swm_box box, int x, int y, int width, int height) { |
|
| 63 | 64 | assert(box.x == x); |
| 165 | 166 | ||
| 166 | 167 | assert(setenv("SWM_TEST_ENV", "hello world", 1) == 0); |
|
| 167 | 168 | assert(env_expand(output, sizeof(output), "$SWM_TEST_ENV/tail") == 17); |
|
| 168 | 169 | assert(!strcmp(output, "hello world/tail")); |
|
| 169 | 170 | assert(env_expand(output, 4, "$SWM_TEST_ENV") == 0); |
|
| 171 | + | assert(env_expand(output, 0, "value") == 0); |
|
| 172 | + | assert(env_expand(output, sizeof(output), "$1") == 3); |
|
| 173 | + | assert(!strcmp(output, "$1")); |
|
| 170 | 174 | } |
|
| 171 | 175 | ||
| 172 | 176 | /* Verify free-workspace selection and published workspace states. */ |
|
| 173 | 177 | static void test_workspace_queries(void) { |
|
| 174 | 178 | client_t *client; |
| 427 | 431 | workspaces[i].mon = (monitor_t *)1; |
|
| 428 | 432 | wl_list_init(&clients); |
|
| 429 | 433 | wl_list_init(&fstack); |
|
| 430 | 434 | wl_list_init(&mons); |
|
| 431 | 435 | wl_list_init(&ws_managers); |
|
| 436 | + | wl_list_init(&metadata_managers); |
|
| 432 | 437 | selmon = &monitor; |
|
| 433 | 438 | unsetenv("XDG_RUNTIME_DIR"); |
|
| 434 | 439 | ||
| 435 | 440 | cycle_layout(nullptr); |
|
| 436 | 441 | assert(workspaces[0].lt == &layouts[1]); |