Add window control to swmctl

4ce74dba5ca0664059816507285c3974f9729123
Alexis Sellier committed ago 1 parent 3e943cc1
Makefile +35 -7
21 21
# swmctl
22 22
CTL_PKGS      = wayland-client
23 23
CTL_CPPFLAGS := `$(PKG_CONFIG) --cflags $(CTL_PKGS)`
24 24
CTL_LDLIBS   := `$(PKG_CONFIG) --libs $(CTL_PKGS)`
25 25
26 -
SRC   := swm.c util.c ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c
26 +
SRC   := swm.c util.c ext-workspace-v1-protocol.c swm-toplevel-v1-protocol.c \
27 +
	swm-workspace-v1-protocol.c
27 28
OBJ   := $(SRC:.c=.o)
28 29
PROTO := \
29 30
	cursor-shape-v1-protocol.h \
30 31
	ext-workspace-v1-protocol.h \
31 32
	pointer-constraints-unstable-v1-protocol.h \
32 33
	wlr-layer-shell-unstable-v1-protocol.h \
33 34
	wlr-output-power-management-unstable-v1-protocol.h \
34 35
	swm-workspace-v1-protocol.h \
36 +
	swm-toplevel-v1-protocol.h \
35 37
	xdg-shell-protocol.h
36 38
HDRS  := config.h swm.h util.h $(PROTO)
37 39
38 40
WL_SCANNER := $(shell $(PKG_CONFIG) --variable=wayland_scanner wayland-scanner)
39 41
WL_PROTO   := $(shell $(PKG_CONFIG) --variable=pkgdatadir wayland-protocols)
64 66
swm: $(OBJ)
65 67
	@echo "ld   $^ => $@"
66 68
	@$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
67 69
	@echo "ok   $@"
68 70
69 -
swmctl: swmctl.o swm-workspace-v1-protocol.o
71 +
swmctl: swmctl.o ext-foreign-toplevel-list-v1-protocol.o \
72 +
		swm-toplevel-v1-protocol.o swm-workspace-v1-protocol.o
70 73
	@echo "ld   $^ => $@"
71 74
	@$(CC) $(LDFLAGS) $^ $(CTL_LDLIBS) -o $@
72 75
	@echo "ok   $@"
73 76
74 -
swmctl.o: swmctl.c swm-workspace-v1-client-protocol.h Makefile config.mk
77 +
swmctl.o: swmctl.c ext-foreign-toplevel-list-v1-client-protocol.h \
78 +
		swm-toplevel-v1-client-protocol.h swm-workspace-v1-client-protocol.h \
79 +
		Makefile config.mk
75 80
	@echo "cc   $< => $@"
76 81
	@$(CC) $(CTL_CPPFLAGS) $(CFLAGS) -c $< -o $@
77 82
78 83
$(OBJ): Makefile config.mk
79 84
91 96
	@$(WL_SCANNER) server-header $< $@
92 97
ext-workspace-v1-protocol.c: \
93 98
	$(WL_PROTO)/staging/ext-workspace/ext-workspace-v1.xml
94 99
	@echo "hdr  $@"
95 100
	@$(WL_SCANNER) private-code $< $@
101 +
ext-foreign-toplevel-list-v1-client-protocol.h: \
102 +
		$(WL_PROTO)/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml
103 +
	@echo "hdr  $@"
104 +
	@$(WL_SCANNER) client-header $< $@
105 +
ext-foreign-toplevel-list-v1-protocol.c: \
106 +
		$(WL_PROTO)/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml
107 +
	@echo "src  $@"
108 +
	@$(WL_SCANNER) private-code $< $@
109 +
swm-toplevel-v1-protocol.h: protocols/swm-toplevel-v1.xml
110 +
	@echo "hdr  $@"
111 +
	@$(WL_SCANNER) server-header $< $@
112 +
swm-toplevel-v1-client-protocol.h: protocols/swm-toplevel-v1.xml
113 +
	@echo "hdr  $@"
114 +
	@$(WL_SCANNER) client-header $< $@
115 +
swm-toplevel-v1-protocol.c: protocols/swm-toplevel-v1.xml
116 +
	@echo "src  $@"
117 +
	@$(WL_SCANNER) private-code $< $@
96 118
swm-workspace-v1-protocol.h: protocols/swm-workspace-v1.xml
97 119
	@echo "hdr  $@"
98 120
	@$(WL_SCANNER) server-header $< $@
99 121
swm-workspace-v1-client-protocol.h: protocols/swm-workspace-v1.xml
100 122
	@echo "hdr  $@"
158 180
159 181
$(TEST_SRC)/config.h: config.def.h | $(TEST_SRC)
160 182
	@cp -p $< $@
161 183
162 184
TEST_COMMON := Makefile config.mk $(TEST_SRC)/swm.c $(TEST_SRC)/config.h \
163 -
	swm.h util.c util.h $(PROTO) ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c
185 +
	swm.h util.c util.h $(PROTO) ext-workspace-v1-protocol.c \
186 +
	swm-toplevel-v1-protocol.c swm-workspace-v1-protocol.c
164 187
165 188
$(TEST_BIN)/unit: test/unit.c $(TEST_COMMON) | $(TEST_BIN)
166 189
	@echo "cc   test/unit.c => $@"
167 190
	@$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) test/unit.c util.c \
168 -
		ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c $(LDLIBS) -o $@
191 +
		ext-workspace-v1-protocol.c swm-toplevel-v1-protocol.c \
192 +
		swm-workspace-v1-protocol.c $(LDLIBS) -o $@
169 193
170 194
$(TEST_BIN)/swm: $(TEST_COMMON) | $(TEST_BIN)
171 195
	@echo "cc   swm.c => $@"
172 196
	@$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) $(TEST_SRC)/swm.c util.c \
173 -
		ext-workspace-v1-protocol.c swm-workspace-v1-protocol.c $(LDLIBS) -o $@
197 +
		ext-workspace-v1-protocol.c swm-toplevel-v1-protocol.c \
198 +
		swm-workspace-v1-protocol.c $(LDLIBS) -o $@
174 199
175 -
$(TEST_BIN)/swmctl: swmctl.c swm-workspace-v1-client-protocol.h \
200 +
$(TEST_BIN)/swmctl: swmctl.c ext-foreign-toplevel-list-v1-client-protocol.h \
201 +
		swm-toplevel-v1-client-protocol.h swm-workspace-v1-client-protocol.h \
202 +
		ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \
176 203
		swm-workspace-v1-protocol.c | $(TEST_BIN)
177 204
	@echo "cc   swmctl.c => $@"
178 205
	@$(CC) $(CTL_CPPFLAGS) $(TEST_CFLAGS) swmctl.c \
206 +
		ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \
179 207
		swm-workspace-v1-protocol.c $(CTL_LDLIBS) -o $@
180 208
181 209
test-build: $(TEST_BIN)/unit $(TEST_BIN)/swm $(TEST_BIN)/swmctl
182 210
183 211
test: test-build
protocols/swm-toplevel-v1.xml added +39 -0
1 +
<?xml version="1.0" encoding="UTF-8"?>
2 +
<protocol name="swm_toplevel_v1">
3 +
  <copyright>
4 +
    Copyright (C) 2026 swm contributors
5 +
6 +
    Permission to use, copy, modify, distribute, and sell this software and its
7 +
    documentation for any purpose is hereby granted without fee, provided that
8 +
    the above copyright notice and this permission notice appear in all copies.
9 +
10 +
    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11 +
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12 +
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13 +
    AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
14 +
    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
15 +
    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 +
  </copyright>
17 +
18 +
  <interface name="swm_toplevel_manager_v1" version="1">
19 +
    <description summary="activate swm toplevels">
20 +
      This interface activates mapped toplevels by the stable identifier from
21 +
      ext-foreign-toplevel-list-v1.
22 +
    </description>
23 +
24 +
    <enum name="error">
25 +
      <entry name="invalid_identifier" value="1" summary="the toplevel identifier does not exist"/>
26 +
    </enum>
27 +
28 +
    <request name="activate">
29 +
      <description summary="show and focus a toplevel">
30 +
        Show the toplevel's workspace on the selected output and focus it.
31 +
      </description>
32 +
      <arg name="identifier" type="string" summary="ext-foreign-toplevel-list identifier"/>
33 +
    </request>
34 +
35 +
    <request name="destroy" type="destructor">
36 +
      <description summary="destroy the manager object"/>
37 +
    </request>
38 +
  </interface>
39 +
</protocol>
swm.c +51 -0
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-toplevel-v1-protocol.h"
86 87
#include "swm-workspace-v1-protocol.h"
87 88
#include "swm.h"
88 89
#include "util.h"
89 90
#include "xdg-shell-protocol.h"
90 91
612 613
static void         view_workspace(workspace_t *ws, monitor_t *m);
613 614
static void         workspace_broadcast(void);
614 615
static void         workspace_metadata_bind(
615 616
    struct wl_client *client, void *data, uint32_t version, uint32_t id
616 617
);
618 +
static void toplevel_control_bind(
619 +
    struct wl_client *client, void *data, uint32_t version, uint32_t id
620 +
);
617 621
static void workspace_metadata_broadcast(void);
618 622
static void workspace_handle_create(workspace_manager_t *mgr, workspace_t *ws);
619 623
static void workspace_manager_bind(
620 624
    struct wl_client *client, void *data, uint32_t version, uint32_t id
621 625
);
5380 5384
    );
5381 5385
    wl_list_insert(&metadata_managers, &mgr->link);
5382 5386
    workspace_metadata_snapshot(mgr->resource);
5383 5387
}
5384 5388
5389 +
/* Show and focus a toplevel selected by its ext-foreign-toplevel identifier. */
5390 +
static void toplevel_control_activate(
5391 +
    struct wl_client *client, struct wl_resource *resource, const char *identifier
5392 +
) {
5393 +
    client_t *c;
5394 +
5395 +
    wl_list_for_each(c, &clients, link) {
5396 +
        if (!c->extftl || strcmp(c->extftl->identifier, identifier))
5397 +
            continue;
5398 +
        if (c->ws && c->ws->mon != selmon)
5399 +
            view_workspace(c->ws, selmon);
5400 +
        focus_client(c, 1);
5401 +
        return;
5402 +
    }
5403 +
    wl_resource_post_error(
5404 +
        resource,
5405 +
        SWM_TOPLEVEL_MANAGER_V1_ERROR_INVALID_IDENTIFIER,
5406 +
        "toplevel identifier '%s' does not exist",
5407 +
        identifier
5408 +
    );
5409 +
}
5410 +
5411 +
/* Release a toplevel control manager. */
5412 +
static void toplevel_control_destroy(struct wl_client *client, struct wl_resource *resource) {
5413 +
    wl_resource_destroy(resource);
5414 +
}
5415 +
5416 +
static const struct swm_toplevel_manager_v1_interface toplevel_control_impl = {
5417 +
    .activate = toplevel_control_activate,
5418 +
    .destroy  = toplevel_control_destroy,
5419 +
};
5420 +
5421 +
/* Publish swm's toplevel control interface to a client. */
5422 +
static void toplevel_control_bind(
5423 +
    struct wl_client *client, void *data, uint32_t version, uint32_t id
5424 +
) {
5425 +
    struct wl_resource *resource =
5426 +
        wl_resource_create(client, &swm_toplevel_manager_v1_interface, (int)version, id);
5427 +
5428 +
    if (!resource) {
5429 +
        wl_client_post_no_memory(client);
5430 +
        return;
5431 +
    }
5432 +
    wl_resource_set_implementation(resource, &toplevel_control_impl, nullptr, nullptr);
5433 +
}
5434 +
5385 5435
/*
5386 5436
 * ext-workspace-v1 implementation. wlroots 0.19 ships no helper for this
5387 5437
 * protocol, so the resources are managed by hand. swm advertises a single
5388 5438
 * workspace group spanning all outputs, containing every workspace; the
5389 5439
 * only capability is activate, which behaves like a workspace switch.
5785 5835
    wl_list_init(&window_states);
5786 5836
    wl_list_init(&input_popups);
5787 5837
    load_window_states();
5788 5838
    wl_global_create(dpy, &ext_workspace_manager_v1_interface, 1, nullptr, workspace_manager_bind);
5789 5839
    wl_global_create(dpy, &swm_workspace_manager_v1_interface, 1, nullptr, workspace_metadata_bind);
5840 +
    wl_global_create(dpy, &swm_toplevel_manager_v1_interface, 1, nullptr, toplevel_control_bind);
5790 5841
5791 5842
    /* Publish window lists for taskbars and switchers. */
5792 5843
    ftl_mgr      = wlr_foreign_toplevel_manager_v1_create(dpy);
5793 5844
    ext_ftl_list = wlr_ext_foreign_toplevel_list_v1_create(dpy, 1);
5794 5845
    wlr_ext_image_copy_capture_manager_v1_create(dpy, 1);
swmctl.c +264 -26
9 9
#include <stdio.h>
10 10
#include <stdlib.h>
11 11
#include <string.h>
12 12
#include <wayland-client.h>
13 13
14 +
#include "ext-foreign-toplevel-list-v1-client-protocol.h"
15 +
#include "swm-toplevel-v1-client-protocol.h"
14 16
#include "swm-workspace-v1-client-protocol.h"
15 17
16 18
#define MAX_WORKSPACES      32
17 19
#define MAX_WORKSPACE_TITLE 256
18 20
21 23
    uint32_t flags;
22 24
    char     title[MAX_WORKSPACE_TITLE];
23 25
    uint32_t color;
24 26
} workspace_t;
25 27
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;
28 +
/* Wayland globals required by one command family. */
29 +
enum command_kind {
30 +
    COMMAND_WORKSPACE,
31 +
    COMMAND_WINDOW_LIST,
32 +
    COMMAND_WINDOW_ACTIVATE,
33 +
};
34 +
35 +
/* Cached state for one ext-foreign-toplevel handle. */
36 +
typedef struct {
37 +
    struct wl_list                         link;
38 +
    struct ext_foreign_toplevel_handle_v1 *handle;
39 +
    char                                  *title;
40 +
    char                                  *app_id;
41 +
    char                                   identifier[33];
42 +
    bool                                   closed;
43 +
} window_t;
44 +
45 +
static struct wl_display                   *display;
46 +
static struct swm_workspace_manager_v1     *manager;
47 +
static struct ext_foreign_toplevel_list_v1 *toplevel_list;
48 +
static struct swm_toplevel_manager_v1      *toplevel_control;
49 +
static struct wl_list                       windows;
50 +
static workspace_t                          workspaces[MAX_WORKSPACES];
51 +
static bool                                 subscribing;
52 +
static bool                                 waybar;
53 +
static bool                                 toplevel_list_finished;
54 +
static enum command_kind                    requested_kind;
31 55
32 56
[[noreturn]] static void usage(void) {
33 57
    fprintf(
34 58
        stderr,
35 59
        "usage:\n"
36 60
        "  swmctl workspace list\n"
37 61
        "  swmctl workspace get N (title | color | selected)\n"
38 62
        "  swmctl workspace set N (title TITLE | color COLOR)\n"
39 63
        "  swmctl workspace clear N (title | color)\n"
40 64
        "  swmctl workspace subscribe [--format=waybar]\n"
65 +
        "  swmctl window list\n"
66 +
        "  swmctl window activate IDENTIFIER\n"
41 67
    );
42 68
    exit(EXIT_FAILURE);
43 69
}
44 70
45 71
/* Print a color in its shortest lossless notation. */
210 236
static const struct swm_workspace_manager_v1_listener manager_listener = {
211 237
    .metadata = metadata,
212 238
    .done     = done,
213 239
};
214 240
215 -
/* Bind swm's workspace metadata global. */
241 +
/* Replace one cached toplevel string. */
242 +
static void replace_string(char **target, const char *value) {
243 +
    char *copy = strdup(value);
244 +
245 +
    if (!copy) {
246 +
        fprintf(stderr, "swmctl: out of memory\n");
247 +
        exit(EXIT_FAILURE);
248 +
    }
249 +
    free(*target);
250 +
    *target = copy;
251 +
}
252 +
253 +
/* Release a closed toplevel handle. */
254 +
static void window_closed(void *data, struct ext_foreign_toplevel_handle_v1 *handle) {
255 +
    window_t *window = data;
256 +
257 +
    ext_foreign_toplevel_handle_v1_destroy(handle);
258 +
    window->handle = nullptr;
259 +
    window->closed = true;
260 +
}
261 +
262 +
/* Accept a complete toplevel state update. */
263 +
static void window_done(void *data, struct ext_foreign_toplevel_handle_v1 *handle) {}
264 +
265 +
/* Cache a toplevel title. */
266 +
static void window_title(
267 +
    void *data, struct ext_foreign_toplevel_handle_v1 *handle, const char *title
268 +
) {
269 +
    window_t *window = data;
270 +
271 +
    replace_string(&window->title, title);
272 +
}
273 +
274 +
/* Cache a toplevel application ID. */
275 +
static void window_app_id(
276 +
    void *data, struct ext_foreign_toplevel_handle_v1 *handle, const char *app_id
277 +
) {
278 +
    window_t *window = data;
279 +
280 +
    replace_string(&window->app_id, app_id);
281 +
}
282 +
283 +
/* Cache a toplevel's stable identifier. */
284 +
static void window_identifier(
285 +
    void *data, struct ext_foreign_toplevel_handle_v1 *handle, const char *identifier
286 +
) {
287 +
    window_t *window = data;
288 +
289 +
    if (strlen(identifier) >= sizeof(window->identifier)) {
290 +
        fprintf(stderr, "swmctl: compositor sent an invalid toplevel identifier\n");
291 +
        exit(EXIT_FAILURE);
292 +
    }
293 +
    strcpy(window->identifier, identifier);
294 +
}
295 +
296 +
static const struct ext_foreign_toplevel_handle_v1_listener window_listener = {
297 +
    .closed     = window_closed,
298 +
    .done       = window_done,
299 +
    .title      = window_title,
300 +
    .app_id     = window_app_id,
301 +
    .identifier = window_identifier,
302 +
};
303 +
304 +
/* Cache a toplevel announced by ext-foreign-toplevel-list. */
305 +
static void window_announced(
306 +
    void                                  *data,
307 +
    struct ext_foreign_toplevel_list_v1   *list,
308 +
    struct ext_foreign_toplevel_handle_v1 *handle
309 +
) {
310 +
    window_t *window = calloc(1, sizeof(*window));
311 +
312 +
    if (!window) {
313 +
        fprintf(stderr, "swmctl: out of memory\n");
314 +
        exit(EXIT_FAILURE);
315 +
    }
316 +
    window->handle = handle;
317 +
    wl_list_insert(windows.prev, &window->link);
318 +
    ext_foreign_toplevel_handle_v1_add_listener(handle, &window_listener, window);
319 +
}
320 +
321 +
/* Record that the compositor accepted the list's stop request. */
322 +
static void window_list_finished(void *data, struct ext_foreign_toplevel_list_v1 *list) {
323 +
    toplevel_list_finished = true;
324 +
}
325 +
326 +
static const struct ext_foreign_toplevel_list_v1_listener window_list_listener = {
327 +
    .toplevel = window_announced,
328 +
    .finished = window_list_finished,
329 +
};
330 +
331 +
/* Bind swm's workspace and toplevel control globals. */
216 332
static void global(
217 333
    void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version
218 334
) {
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);
335 +
    if (requested_kind == COMMAND_WORKSPACE &&
336 +
        !strcmp(interface, swm_workspace_manager_v1_interface.name)) {
337 +
        manager = wl_registry_bind(registry, name, &swm_workspace_manager_v1_interface, 1);
338 +
        swm_workspace_manager_v1_add_listener(manager, &manager_listener, nullptr);
339 +
    } else if (
340 +
        requested_kind == COMMAND_WINDOW_LIST &&
341 +
        !strcmp(interface, ext_foreign_toplevel_list_v1_interface.name)
342 +
    ) {
343 +
        toplevel_list =
344 +
            wl_registry_bind(registry, name, &ext_foreign_toplevel_list_v1_interface, 1);
345 +
        ext_foreign_toplevel_list_v1_add_listener(toplevel_list, &window_list_listener, nullptr);
346 +
    } else if (
347 +
        requested_kind == COMMAND_WINDOW_ACTIVATE &&
348 +
        !strcmp(interface, swm_toplevel_manager_v1_interface.name)
349 +
    ) {
350 +
        toplevel_control = wl_registry_bind(registry, name, &swm_toplevel_manager_v1_interface, 1);
351 +
    }
223 352
}
224 353
225 354
static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
226 355
227 356
static const struct wl_registry_listener registry_listener = {
280 409
        if (workspaces[i].seen)
281 410
            printf("%" PRIu32 "\n", i + 1);
282 411
    }
283 412
}
284 413
285 -
/* Connect to swm and collect its initial metadata snapshot. */
286 -
static void connect_manager(void) {
414 +
/* Write a field with line and column separators escaped. */
415 +
static void print_window_field(const char *value) {
416 +
    const unsigned char *p;
417 +
418 +
    for (p = (const unsigned char *)(value ? value : ""); *p; p++) {
419 +
        if (*p == '\\')
420 +
            fputs("\\\\", stdout);
421 +
        else if (*p == '\t')
422 +
            fputs("\\t", stdout);
423 +
        else if (*p == '\n')
424 +
            fputs("\\n", stdout);
425 +
        else if (*p == '\r')
426 +
            fputs("\\r", stdout);
427 +
        else
428 +
            putchar(*p);
429 +
    }
430 +
}
431 +
432 +
/* Print mapped toplevels as title, application ID, and identifier fields. */
433 +
static void print_windows(void) {
434 +
    window_t *window;
435 +
436 +
    wl_list_for_each(window, &windows, link) {
437 +
        if (window->closed || !window->identifier[0])
438 +
            continue;
439 +
        print_window_field(window->title);
440 +
        putchar('\t');
441 +
        print_window_field(window->app_id);
442 +
        putchar('\t');
443 +
        puts(window->identifier);
444 +
    }
445 +
}
446 +
447 +
/* Release cached foreign-toplevel state. */
448 +
static void destroy_windows(void) {
449 +
    window_t *window, *tmp;
450 +
451 +
    wl_list_for_each_safe(window, tmp, &windows, link) {
452 +
        if (window->handle)
453 +
            ext_foreign_toplevel_handle_v1_destroy(window->handle);
454 +
        wl_list_remove(&window->link);
455 +
        free(window->title);
456 +
        free(window->app_id);
457 +
        free(window);
458 +
    }
459 +
}
460 +
461 +
/* Stop the list, then release its child handles before the list object. */
462 +
static bool destroy_window_list(void) {
463 +
    if (!toplevel_list_finished) {
464 +
        ext_foreign_toplevel_list_v1_stop(toplevel_list);
465 +
        if (wl_display_roundtrip(display) < 0 || !toplevel_list_finished)
466 +
            return false;
467 +
    }
468 +
    destroy_windows();
469 +
    ext_foreign_toplevel_list_v1_destroy(toplevel_list);
470 +
    toplevel_list = nullptr;
471 +
    return wl_display_flush(display) >= 0;
472 +
}
473 +
474 +
/* Connect to swm and collect the requested initial state. */
475 +
static void connect_manager(enum command_kind kind) {
287 476
    struct wl_registry *registry;
288 477
289 478
    if (!(display = wl_display_connect(nullptr))) {
290 479
        fprintf(stderr, "swmctl: cannot connect to Wayland display\n");
291 480
        exit(EXIT_FAILURE);
292 481
    }
293 -
    registry = wl_display_get_registry(display);
482 +
    wl_list_init(&windows);
483 +
    requested_kind = kind;
484 +
    registry       = wl_display_get_registry(display);
294 485
    wl_registry_add_listener(registry, &registry_listener, nullptr);
295 486
296 -
    if (wl_display_roundtrip(display) < 0 || !manager) {
297 -
        fprintf(stderr, "swmctl: compositor does not support swm-workspace-v1\n");
487 +
    if (wl_display_roundtrip(display) < 0 || (kind == COMMAND_WORKSPACE && !manager) ||
488 +
        (kind == COMMAND_WINDOW_LIST && !toplevel_list) ||
489 +
        (kind == COMMAND_WINDOW_ACTIVATE && !toplevel_control)) {
490 +
        fprintf(
491 +
            stderr,
492 +
            kind == COMMAND_WORKSPACE ? "swmctl: compositor does not support swm-workspace-v1\n"
493 +
            : kind == COMMAND_WINDOW_LIST
494 +
                ? "swmctl: compositor does not support ext-foreign-toplevel-list-v1\n"
495 +
                : "swmctl: compositor does not support swm-toplevel-v1\n"
496 +
        );
298 497
        exit(EXIT_FAILURE);
299 498
    }
300 -
    if (wl_display_roundtrip(display) < 0) {
301 -
        fprintf(stderr, "swmctl: failed to read workspace metadata\n");
499 +
    if (kind != COMMAND_WINDOW_ACTIVATE && wl_display_roundtrip(display) < 0) {
500 +
        fprintf(
501 +
            stderr,
502 +
            kind == COMMAND_WORKSPACE ? "swmctl: failed to read workspace metadata\n"
503 +
                                      : "swmctl: failed to read toplevels\n"
504 +
        );
302 505
        exit(EXIT_FAILURE);
303 506
    }
304 507
    wl_registry_destroy(registry);
305 508
}
306 509
307 510
int main(int argc, char **argv) {
511 +
    const char *identifier = nullptr;
512 +
    bool        window_command;
308 513
    const char *command;
309 514
    const char *field     = nullptr;
310 515
    const char *value     = nullptr;
311 516
    uint32_t    workspace = 0;
312 517
313 -
    if (argc < 3 || strcmp(argv[1], "workspace"))
518 +
    if (argc < 3)
314 519
        usage();
315 -
    command     = argv[2];
316 -
    subscribing = !strcmp(command, "subscribe");
520 +
    window_command = !strcmp(argv[1], "window");
521 +
    command        = argv[2];
522 +
    if (window_command) {
523 +
        if (!strcmp(command, "list")) {
524 +
            if (argc != 3)
525 +
                usage();
526 +
        } else if (!strcmp(command, "activate")) {
527 +
            if (argc != 4)
528 +
                usage();
529 +
            identifier = argv[3];
530 +
        } else {
531 +
            usage();
532 +
        }
533 +
    } else if (strcmp(argv[1], "workspace")) {
534 +
        usage();
535 +
    }
536 +
    subscribing = !window_command && !strcmp(command, "subscribe");
317 537
318 -
    if (!strcmp(command, "list")) {
538 +
    if (window_command) {
539 +
        ;
540 +
    } else if (!strcmp(command, "list")) {
319 541
        if (argc != 3)
320 542
            usage();
321 543
    } else if (!strcmp(command, "get")) {
322 544
        if (argc != 5)
323 545
            usage();
352 574
        else if (argc != 3)
353 575
            usage();
354 576
    } else {
355 577
        usage();
356 578
    }
357 -
    connect_manager();
579 +
    connect_manager(
580 +
        !window_command            ? COMMAND_WORKSPACE
581 +
        : !strcmp(command, "list") ? COMMAND_WINDOW_LIST
582 +
                                   : COMMAND_WINDOW_ACTIVATE
583 +
    );
358 584
359 -
    if (subscribing) {
585 +
    if (window_command) {
586 +
        if (!strcmp(command, "list"))
587 +
            print_windows();
588 +
        else
589 +
            swm_toplevel_manager_v1_activate(toplevel_control, identifier);
590 +
    } else if (subscribing) {
360 591
        while (wl_display_dispatch(display) >= 0)
361 592
            ;
362 593
        return EXIT_FAILURE;
363 594
    }
364 -
    if (!strcmp(command, "list")) {
595 +
    if (!window_command && !strcmp(command, "list")) {
365 596
        print_workspaces();
366 -
    } else {
597 +
    } else if (!window_command) {
367 598
        if (!workspaces[workspace - 1].seen) {
368 599
            fprintf(stderr, "swmctl: workspace %" PRIu32 " does not exist\n", workspace);
369 600
            return EXIT_FAILURE;
370 601
        }
371 602
        if (!strcmp(command, "get")) {
384 615
    }
385 616
    if (wl_display_flush(display) < 0 || wl_display_roundtrip(display) < 0) {
386 617
        fprintf(stderr, "swmctl: command failed\n");
387 618
        return EXIT_FAILURE;
388 619
    }
389 -
    swm_workspace_manager_v1_destroy(manager);
620 +
    if (requested_kind == COMMAND_WINDOW_LIST && !destroy_window_list()) {
621 +
        fprintf(stderr, "swmctl: failed to release toplevel list\n");
622 +
        return EXIT_FAILURE;
623 +
    }
624 +
    if (toplevel_control)
625 +
        swm_toplevel_manager_v1_destroy(toplevel_control);
626 +
    if (manager)
627 +
        swm_workspace_manager_v1_destroy(manager);
390 628
    wl_display_disconnect(display);
391 629
392 630
    return EXIT_SUCCESS;
393 631
}
test/run.py +25 -0
344 344
        virtual_keyboard(36)
345 345
        wait_title("two" if before == "one" else "one")
346 346
347 347
    test("window publication and focus", publication)
348 348
349 +
    def window_control() -> None:
350 +
        target = current_title()
351 +
        virtual_keyboard(3, 1)
352 +
        eventually("focus after moving switch target", lambda: current_title() != target)
353 +
        listed = run(BIN / "swmctl", "window", "list").splitlines()
354 +
        matches = [line for line in listed if line.split("\t", 1)[0] == target]
355 +
        if len(matches) != 1 or len(matches[0].split("\t")) != 3:
356 +
            raise Failure(f"unexpected toplevel list: {listed!r}")
357 +
        identifier = matches[0].rsplit("\t", 1)[1]
358 +
        duplicate = start_client(target, "5588ffdd")
359 +
        listed = run(BIN / "swmctl", "window", "list").splitlines()
360 +
        matches = [line for line in listed if line.split("\t", 1)[0] == target]
361 +
        if len(matches) != 2:
362 +
            raise Failure(f"duplicate title missing from toplevel list: {listed!r}")
363 +
        run(BIN / "swmctl", "window", "activate", identifier)
364 +
        wait_title(target)
365 +
        selected = run(BIN / "swmctl", "workspace", "get", 2, "selected")
366 +
        if selected != "true\n":
367 +
            raise Failure("identifier activation selected the duplicate window")
368 +
        terminate(duplicate)
369 +
        virtual_keyboard(2, 1)
370 +
        virtual_keyboard(2)
371 +
372 +
    test("window control", window_control)
373 +
349 374
    def layouts() -> None:
350 375
        three = start_client("three-layout", "ffcc8855")
351 376
        names = ("one", "two", "three-layout")
352 377
        normal = {name: geometry(name)[:2] for name in names}
353 378
        virtual_keyboard(33)