Add `swmctl` command

c3e974a9d73f88e798acc282c55601436a61f0dc
Alexis Sellier committed ago 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("&amp;", stdout);
110 +
        else if (*p == '<')
111 +
            fputs("&lt;", stdout);
112 +
        else if (*p == '>')
113 +
            fputs("&gt;", stdout);
114 +
        else if (*p == '"')
115 +
            fputs("&quot;", stdout);
116 +
        else if (*p == '\'')
117 +
            fputs("&apos;", 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, &registry_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]);