Add window control to swmctl
4ce74dba5ca0664059816507285c3974f9729123
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, ®istry_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) |