Expose active window title through swmctl events
b56dddf6bf4d21206f36747e9b0ad4cfee429fb2
1 parent
abb61208
Makefile
+22 -4
| 67 | 67 | @echo "ld $^ => $@" |
|
| 68 | 68 | @$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ |
|
| 69 | 69 | @echo "ok $@" |
|
| 70 | 70 | ||
| 71 | 71 | swmctl: swmctl.o ext-foreign-toplevel-list-v1-protocol.o \ |
|
| 72 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.o \ |
|
| 72 | 73 | swm-toplevel-v1-protocol.o swm-workspace-v1-protocol.o |
|
| 73 | 74 | @echo "ld $^ => $@" |
|
| 74 | 75 | @$(CC) $(LDFLAGS) $^ $(CTL_LDLIBS) -o $@ |
|
| 75 | 76 | @echo "ok $@" |
|
| 76 | 77 | ||
| 77 | 78 | swmctl.o: swmctl.c ext-foreign-toplevel-list-v1-client-protocol.h \ |
|
| 79 | + | wlr-foreign-toplevel-management-unstable-v1-client-protocol.h \ |
|
| 78 | 80 | swm-toplevel-v1-client-protocol.h swm-workspace-v1-client-protocol.h \ |
|
| 79 | 81 | Makefile config.mk |
|
| 80 | 82 | @echo "cc $< => $@" |
|
| 81 | 83 | @$(CC) $(CTL_CPPFLAGS) $(CFLAGS) -c $< -o $@ |
|
| 82 | 84 |
| 104 | 106 | @$(WL_SCANNER) client-header $< $@ |
|
| 105 | 107 | ext-foreign-toplevel-list-v1-protocol.c: \ |
|
| 106 | 108 | $(WL_PROTO)/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml |
|
| 107 | 109 | @echo "src $@" |
|
| 108 | 110 | @$(WL_SCANNER) private-code $< $@ |
|
| 111 | + | ||
| 112 | + | wlr-foreign-toplevel-management-unstable-v1-client-protocol.h: \ |
|
| 113 | + | protocols/wlr-foreign-toplevel-management-unstable-v1.xml |
|
| 114 | + | @echo "hdr $@" |
|
| 115 | + | @$(WL_SCANNER) client-header $< $@ |
|
| 116 | + | ||
| 117 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.c: \ |
|
| 118 | + | protocols/wlr-foreign-toplevel-management-unstable-v1.xml |
|
| 119 | + | @echo "src $@" |
|
| 120 | + | @$(WL_SCANNER) private-code $< $@ |
|
| 109 | 121 | swm-toplevel-v1-protocol.h: protocols/swm-toplevel-v1.xml |
|
| 110 | 122 | @echo "hdr $@" |
|
| 111 | 123 | @$(WL_SCANNER) server-header $< $@ |
|
| 112 | 124 | swm-toplevel-v1-client-protocol.h: protocols/swm-toplevel-v1.xml |
|
| 113 | 125 | @echo "hdr $@" |
| 197 | 209 | -Wl,--wrap=wlr_cursor_set_xcursor \ |
|
| 198 | 210 | -Wl,--wrap=wlr_output_layout_output_at $(LDLIBS) -o $@ |
|
| 199 | 211 | ||
| 200 | 212 | $(TEST_BIN)/ctl-unit: test/ctl_unit.c swmctl.c \ |
|
| 201 | 213 | ext-foreign-toplevel-list-v1-client-protocol.h \ |
|
| 214 | + | wlr-foreign-toplevel-management-unstable-v1-client-protocol.h \ |
|
| 202 | 215 | swm-toplevel-v1-client-protocol.h swm-workspace-v1-client-protocol.h \ |
|
| 203 | - | ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 216 | + | ext-foreign-toplevel-list-v1-protocol.c \ |
|
| 217 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 204 | 218 | swm-workspace-v1-protocol.c | $(TEST_BIN) |
|
| 205 | 219 | @echo "cc test/ctl_unit.c => $@" |
|
| 206 | 220 | @$(CC) -I. $(CTL_CPPFLAGS) $(TEST_CFLAGS) test/ctl_unit.c \ |
|
| 207 | - | ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 221 | + | ext-foreign-toplevel-list-v1-protocol.c \ |
|
| 222 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 208 | 223 | swm-workspace-v1-protocol.c $(CTL_LDLIBS) -o $@ |
|
| 209 | 224 | ||
| 210 | 225 | $(TEST_BIN)/swm: $(TEST_COMMON) | $(TEST_BIN) |
|
| 211 | 226 | @echo "cc swm.c => $@" |
|
| 212 | 227 | @$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) $(TEST_SRC)/swm.c util.c \ |
|
| 213 | 228 | ext-workspace-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 214 | 229 | swm-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 215 | 230 | ||
| 216 | 231 | $(TEST_BIN)/swmctl: swmctl.c ext-foreign-toplevel-list-v1-client-protocol.h \ |
|
| 232 | + | wlr-foreign-toplevel-management-unstable-v1-client-protocol.h \ |
|
| 217 | 233 | swm-toplevel-v1-client-protocol.h swm-workspace-v1-client-protocol.h \ |
|
| 218 | - | ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 234 | + | ext-foreign-toplevel-list-v1-protocol.c \ |
|
| 235 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 219 | 236 | swm-workspace-v1-protocol.c | $(TEST_BIN) |
|
| 220 | 237 | @echo "cc swmctl.c => $@" |
|
| 221 | 238 | @$(CC) $(CTL_CPPFLAGS) $(TEST_CFLAGS) swmctl.c \ |
|
| 222 | - | ext-foreign-toplevel-list-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 239 | + | ext-foreign-toplevel-list-v1-protocol.c \ |
|
| 240 | + | wlr-foreign-toplevel-management-unstable-v1-protocol.c swm-toplevel-v1-protocol.c \ |
|
| 223 | 241 | swm-workspace-v1-protocol.c $(CTL_LDLIBS) -o $@ |
|
| 224 | 242 | ||
| 225 | 243 | test-build: $(TEST_BIN)/unit $(TEST_BIN)/ctl-unit $(TEST_BIN)/swm $(TEST_BIN)/swmctl |
|
| 226 | 244 | ||
| 227 | 245 | test: test-build |
swmctl.1.adoc
+17 -1
| 3 | 3 | :manmanual: SWM Manual |
|
| 4 | 4 | :mansource: swm 1.0.0 |
|
| 5 | 5 | ||
| 6 | 6 | == NAME |
|
| 7 | 7 | ||
| 8 | - | swmctl - inspect and change swm workspace metadata |
|
| 8 | + | swmctl - inspect swm state and change workspace metadata |
|
| 9 | 9 | ||
| 10 | 10 | == SYNOPSIS |
|
| 11 | 11 | ||
| 12 | 12 | *swmctl workspace list* |
|
| 13 | 13 |
| 17 | 17 | ||
| 18 | 18 | *swmctl workspace clear* _N_ (*title*|*color*) |
|
| 19 | 19 | ||
| 20 | 20 | *swmctl workspace subscribe* [*--format=waybar*] |
|
| 21 | 21 | ||
| 22 | + | *swmctl events --filter=active-window* |
|
| 23 | + | ||
| 22 | 24 | == DESCRIPTION |
|
| 23 | 25 | ||
| 24 | 26 | *swmctl* connects to the running *swm* Wayland compositor and reads or changes |
|
| 25 | 27 | the title and color associated with a workspace. This metadata is ephemeral: it |
|
| 26 | 28 | is stored only in compositor memory and is discarded when *swm* exits. |
| 43 | 45 | for the workspace on swm's selected output. Its additional title becomes the |
|
| 44 | 46 | module text and its color becomes the text foreground. An unset title produces |
|
| 45 | 47 | empty text. Configure the module with `return-type` set to `json` and `escape` |
|
| 46 | 48 | set to `false`. |
|
| 47 | 49 | ||
| 50 | + | *events --filter=active-window* writes the current active window title, then |
|
| 51 | + | writes a new line when the active window or its title changes. It writes an |
|
| 52 | + | empty line when no window is active. Control characters in titles become spaces. |
|
| 53 | + | The command stays connected until it is stopped or the compositor exits. |
|
| 54 | + | ||
| 48 | 55 | == WAYBAR |
|
| 49 | 56 | ||
| 50 | 57 | The `ext/workspaces` module continues to display the numeric workspace names |
|
| 51 | 58 | `1` through `10`. A custom module can display the additional title and color |
|
| 52 | 59 | independently: |
| 61 | 68 | ||
| 62 | 69 | ||
| 63 | 70 | The custom module follows the workspace on swm's selected output and uses its |
|
| 64 | 71 | color for the title text. |
|
| 65 | 72 | ||
| 73 | + | To show the active window title in Waybar, use: |
|
| 74 | + | ||
| 75 | + | "modules-center": ["custom/swm-window"], |
|
| 76 | + | "custom/swm-window": { |
|
| 77 | + | "exec": "swmctl events --filter=active-window", |
|
| 78 | + | "max-length": 80, |
|
| 79 | + | "tooltip": false |
|
| 80 | + | } |
|
| 81 | + | ||
| 66 | 82 | == EXAMPLES |
|
| 67 | 83 | ||
| 68 | 84 | Set both fields for workspace 3: |
|
| 69 | 85 | ||
| 70 | 86 | swmctl workspace set 3 title "Prototyping new window manager" |
swmctl.c
+166 -1
| 12 | 12 | #include <wayland-client.h> |
|
| 13 | 13 | ||
| 14 | 14 | #include "ext-foreign-toplevel-list-v1-client-protocol.h" |
|
| 15 | 15 | #include "swm-toplevel-v1-client-protocol.h" |
|
| 16 | 16 | #include "swm-workspace-v1-client-protocol.h" |
|
| 17 | + | #include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" |
|
| 17 | 18 | ||
| 18 | 19 | #define MAX_WORKSPACES 32 |
|
| 19 | 20 | #define MAX_WORKSPACE_TITLE 256 |
|
| 20 | 21 | ||
| 21 | 22 | typedef struct { |
| 28 | 29 | /* Wayland globals required by one command family. */ |
|
| 29 | 30 | enum command_kind { |
|
| 30 | 31 | COMMAND_WORKSPACE, |
|
| 31 | 32 | COMMAND_WINDOW_LIST, |
|
| 32 | 33 | COMMAND_WINDOW_ACTIVATE, |
|
| 34 | + | COMMAND_EVENTS, |
|
| 33 | 35 | }; |
|
| 34 | 36 | ||
| 35 | 37 | /* Cached state for one ext-foreign-toplevel handle. */ |
|
| 36 | 38 | typedef struct { |
|
| 37 | 39 | struct wl_list link; |
| 40 | 42 | char *app_id; |
|
| 41 | 43 | char identifier[33]; |
|
| 42 | 44 | bool closed; |
|
| 43 | 45 | } window_t; |
|
| 44 | 46 | ||
| 47 | + | /* Cached title and activation state for one event-stream toplevel. */ |
|
| 48 | + | typedef struct { |
|
| 49 | + | struct wl_list link; |
|
| 50 | + | char *title; |
|
| 51 | + | bool active; |
|
| 52 | + | bool ready; |
|
| 53 | + | } event_window_t; |
|
| 54 | + | ||
| 45 | 55 | static struct wl_display *display; |
|
| 46 | 56 | static struct swm_workspace_manager_v1 *manager; |
|
| 47 | 57 | static struct ext_foreign_toplevel_list_v1 *toplevel_list; |
|
| 48 | 58 | static struct swm_toplevel_manager_v1 *toplevel_control; |
|
| 49 | 59 | static struct wl_list windows; |
| 51 | 61 | static bool subscribing; |
|
| 52 | 62 | static bool waybar; |
|
| 53 | 63 | static bool toplevel_list_finished; |
|
| 54 | 64 | static enum command_kind requested_kind; |
|
| 55 | 65 | ||
| 66 | + | static struct zwlr_foreign_toplevel_manager_v1 *event_manager; |
|
| 67 | + | static struct wl_list event_windows; |
|
| 68 | + | static event_window_t *previous_event_window; |
|
| 69 | + | static char *previous_event_title; |
|
| 70 | + | static bool event_published; |
|
| 71 | + | ||
| 56 | 72 | [[noreturn]] static void usage(void) { |
|
| 57 | 73 | fprintf( |
|
| 58 | 74 | stderr, |
|
| 59 | 75 | "usage:\n" |
|
| 60 | 76 | " swmctl workspace list\n" |
| 62 | 78 | " swmctl workspace set N (title TITLE | color COLOR)\n" |
|
| 63 | 79 | " swmctl workspace clear N (title | color)\n" |
|
| 64 | 80 | " swmctl workspace subscribe [--format=waybar]\n" |
|
| 65 | 81 | " swmctl window list\n" |
|
| 66 | 82 | " swmctl window activate IDENTIFIER\n" |
|
| 83 | + | " swmctl events --filter=active-window\n" |
|
| 67 | 84 | ); |
|
| 68 | 85 | exit(EXIT_FAILURE); |
|
| 69 | 86 | } |
|
| 70 | 87 | ||
| 71 | 88 | /* Print a color in its shortest lossless notation. */ |
| 326 | 343 | static const struct ext_foreign_toplevel_list_v1_listener window_list_listener = { |
|
| 327 | 344 | .toplevel = window_announced, |
|
| 328 | 345 | .finished = window_list_finished, |
|
| 329 | 346 | }; |
|
| 330 | 347 | ||
| 348 | + | /* Cache a toplevel title for the active-window event stream. */ |
|
| 349 | + | static void event_title( |
|
| 350 | + | void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title |
|
| 351 | + | ) { |
|
| 352 | + | event_window_t *window = data; |
|
| 353 | + | ||
| 354 | + | replace_string(&window->title, title); |
|
| 355 | + | window->ready = false; |
|
| 356 | + | } |
|
| 357 | + | ||
| 358 | + | static void event_app_id( |
|
| 359 | + | void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id |
|
| 360 | + | ) {} |
|
| 361 | + | ||
| 362 | + | static void event_output( |
|
| 363 | + | void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output |
|
| 364 | + | ) {} |
|
| 365 | + | ||
| 366 | + | static void event_parent( |
|
| 367 | + | void *data, |
|
| 368 | + | struct zwlr_foreign_toplevel_handle_v1 *handle, |
|
| 369 | + | struct zwlr_foreign_toplevel_handle_v1 *parent |
|
| 370 | + | ) {} |
|
| 371 | + | ||
| 372 | + | /* Cache whether a toplevel is active. */ |
|
| 373 | + | static void event_state( |
|
| 374 | + | void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_array *state |
|
| 375 | + | ) { |
|
| 376 | + | event_window_t *window = data; |
|
| 377 | + | uint32_t *value; |
|
| 378 | + | ||
| 379 | + | window->active = false; |
|
| 380 | + | window->ready = false; |
|
| 381 | + | wl_array_for_each(value, state) { |
|
| 382 | + | if (*value == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) |
|
| 383 | + | window->active = true; |
|
| 384 | + | } |
|
| 385 | + | } |
|
| 386 | + | ||
| 387 | + | /* Mark a complete toplevel update ready for publication. */ |
|
| 388 | + | static void event_done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) { |
|
| 389 | + | event_window_t *window = data; |
|
| 390 | + | ||
| 391 | + | window->ready = true; |
|
| 392 | + | } |
|
| 393 | + | ||
| 394 | + | /* Release a closed toplevel and invalidate its last published identity. */ |
|
| 395 | + | static void event_closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) { |
|
| 396 | + | event_window_t *window = data; |
|
| 397 | + | ||
| 398 | + | zwlr_foreign_toplevel_handle_v1_destroy(handle); |
|
| 399 | + | if (window == previous_event_window) { |
|
| 400 | + | previous_event_window = nullptr; |
|
| 401 | + | event_published = false; |
|
| 402 | + | } |
|
| 403 | + | wl_list_remove(&window->link); |
|
| 404 | + | free(window->title); |
|
| 405 | + | free(window); |
|
| 406 | + | } |
|
| 407 | + | ||
| 408 | + | static const struct zwlr_foreign_toplevel_handle_v1_listener event_window_listener = { |
|
| 409 | + | .title = event_title, |
|
| 410 | + | .app_id = event_app_id, |
|
| 411 | + | .output_enter = event_output, |
|
| 412 | + | .output_leave = event_output, |
|
| 413 | + | .state = event_state, |
|
| 414 | + | .done = event_done, |
|
| 415 | + | .closed = event_closed, |
|
| 416 | + | .parent = event_parent, |
|
| 417 | + | }; |
|
| 418 | + | ||
| 419 | + | /* Track a new toplevel for active-window updates. */ |
|
| 420 | + | static void event_toplevel( |
|
| 421 | + | void *data, |
|
| 422 | + | struct zwlr_foreign_toplevel_manager_v1 *object, |
|
| 423 | + | struct zwlr_foreign_toplevel_handle_v1 *handle |
|
| 424 | + | ) { |
|
| 425 | + | event_window_t *window = calloc(1, sizeof(*window)); |
|
| 426 | + | ||
| 427 | + | if (!window) { |
|
| 428 | + | fprintf(stderr, "swmctl: out of memory\n"); |
|
| 429 | + | exit(EXIT_FAILURE); |
|
| 430 | + | } |
|
| 431 | + | wl_list_insert(event_windows.prev, &window->link); |
|
| 432 | + | zwlr_foreign_toplevel_handle_v1_add_listener(handle, &event_window_listener, window); |
|
| 433 | + | } |
|
| 434 | + | ||
| 435 | + | static void event_finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *object) { |
|
| 436 | + | event_manager = nullptr; |
|
| 437 | + | } |
|
| 438 | + | ||
| 439 | + | static const struct zwlr_foreign_toplevel_manager_v1_listener event_manager_listener = { |
|
| 440 | + | .toplevel = event_toplevel, |
|
| 441 | + | .finished = event_finished, |
|
| 442 | + | }; |
|
| 443 | + | ||
| 444 | + | /* Write one title as a single line suitable for a Waybar custom module. */ |
|
| 445 | + | static void print_event_title(const char *title) { |
|
| 446 | + | const unsigned char *p; |
|
| 447 | + | ||
| 448 | + | for (p = (const unsigned char *)title; *p; p++) |
|
| 449 | + | putchar(*p < ' ' || *p == 0x7f ? ' ' : *p); |
|
| 450 | + | putchar('\n'); |
|
| 451 | + | fflush(stdout); |
|
| 452 | + | } |
|
| 453 | + | ||
| 454 | + | /* Publish the active title after a complete batch of Wayland events. */ |
|
| 455 | + | static void publish_active_window(void) { |
|
| 456 | + | event_window_t *window, *active = nullptr; |
|
| 457 | + | const char *title; |
|
| 458 | + | ||
| 459 | + | wl_list_for_each(window, &event_windows, link) { |
|
| 460 | + | if (!window->ready) |
|
| 461 | + | return; |
|
| 462 | + | if (window->active) |
|
| 463 | + | active = window; |
|
| 464 | + | } |
|
| 465 | + | title = active && active->title ? active->title : ""; |
|
| 466 | + | if (event_published && active == previous_event_window && !strcmp(title, previous_event_title)) |
|
| 467 | + | return; |
|
| 468 | + | replace_string(&previous_event_title, title); |
|
| 469 | + | previous_event_window = active; |
|
| 470 | + | event_published = true; |
|
| 471 | + | print_event_title(title); |
|
| 472 | + | } |
|
| 473 | + | ||
| 331 | 474 | /* Bind swm's workspace and toplevel control globals. */ |
|
| 332 | 475 | static void global( |
|
| 333 | 476 | void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version |
|
| 334 | 477 | ) { |
|
| 335 | 478 | if (requested_kind == COMMAND_WORKSPACE && |
| 346 | 489 | } else if ( |
|
| 347 | 490 | requested_kind == COMMAND_WINDOW_ACTIVATE && |
|
| 348 | 491 | !strcmp(interface, swm_toplevel_manager_v1_interface.name) |
|
| 349 | 492 | ) { |
|
| 350 | 493 | toplevel_control = wl_registry_bind(registry, name, &swm_toplevel_manager_v1_interface, 1); |
|
| 494 | + | } else if ( |
|
| 495 | + | requested_kind == COMMAND_EVENTS && |
|
| 496 | + | !strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) |
|
| 497 | + | ) { |
|
| 498 | + | event_manager = |
|
| 499 | + | wl_registry_bind(registry, name, &zwlr_foreign_toplevel_manager_v1_interface, 1); |
|
| 500 | + | zwlr_foreign_toplevel_manager_v1_add_listener( |
|
| 501 | + | event_manager, &event_manager_listener, nullptr |
|
| 502 | + | ); |
|
| 351 | 503 | } |
|
| 352 | 504 | } |
|
| 353 | 505 | ||
| 354 | 506 | static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {} |
|
| 355 | 507 |
| 481 | 633 | if (!(display = wl_display_connect(nullptr))) { |
|
| 482 | 634 | fprintf(stderr, "swmctl: cannot connect to Wayland display\n"); |
|
| 483 | 635 | exit(EXIT_FAILURE); |
|
| 484 | 636 | } |
|
| 485 | 637 | wl_list_init(&windows); |
|
| 638 | + | wl_list_init(&event_windows); |
|
| 486 | 639 | requested_kind = kind; |
|
| 487 | 640 | registry = wl_display_get_registry(display); |
|
| 488 | 641 | wl_registry_add_listener(registry, ®istry_listener, nullptr); |
|
| 489 | 642 | ||
| 490 | 643 | if (wl_display_roundtrip(display) < 0 || (kind == COMMAND_WORKSPACE && !manager) || |
|
| 491 | 644 | (kind == COMMAND_WINDOW_LIST && !toplevel_list) || |
|
| 492 | - | (kind == COMMAND_WINDOW_ACTIVATE && !toplevel_control)) { |
|
| 645 | + | (kind == COMMAND_WINDOW_ACTIVATE && !toplevel_control) || |
|
| 646 | + | (kind == COMMAND_EVENTS && !event_manager)) { |
|
| 493 | 647 | fprintf( |
|
| 494 | 648 | stderr, |
|
| 495 | 649 | kind == COMMAND_WORKSPACE ? "swmctl: compositor does not support swm-workspace-v1\n" |
|
| 496 | 650 | : kind == COMMAND_WINDOW_LIST |
|
| 497 | 651 | ? "swmctl: compositor does not support ext-foreign-toplevel-list-v1\n" |
|
| 652 | + | : kind == COMMAND_EVENTS |
|
| 653 | + | ? "swmctl: compositor does not support wlr-foreign-toplevel-management-v1\n" |
|
| 498 | 654 | : "swmctl: compositor does not support swm-toplevel-v1\n" |
|
| 499 | 655 | ); |
|
| 500 | 656 | exit(EXIT_FAILURE); |
|
| 501 | 657 | } |
|
| 502 | 658 | if (kind != COMMAND_WINDOW_ACTIVATE && wl_display_roundtrip(display) < 0) { |
| 518 | 674 | const char *value = nullptr; |
|
| 519 | 675 | uint32_t workspace = 0; |
|
| 520 | 676 | ||
| 521 | 677 | if (argc < 3) |
|
| 522 | 678 | usage(); |
|
| 679 | + | if (!strcmp(argv[1], "events")) { |
|
| 680 | + | if (argc != 3 || strcmp(argv[2], "--filter=active-window")) |
|
| 681 | + | usage(); |
|
| 682 | + | connect_manager(COMMAND_EVENTS); |
|
| 683 | + | publish_active_window(); |
|
| 684 | + | while (wl_display_dispatch(display) >= 0) |
|
| 685 | + | publish_active_window(); |
|
| 686 | + | return EXIT_FAILURE; |
|
| 687 | + | } |
|
| 523 | 688 | window_command = !strcmp(argv[1], "window"); |
|
| 524 | 689 | command = argv[2]; |
|
| 525 | 690 | if (window_command) { |
|
| 526 | 691 | if (!strcmp(command, "list")) { |
|
| 527 | 692 | if (argc != 3) |
test/clients.py
+24 -0
| 262 | 262 | connection.display.dispatch(block=True) |
|
| 263 | 263 | window.destroy() |
|
| 264 | 264 | connection.close() |
|
| 265 | 265 | ||
| 266 | 266 | ||
| 267 | + | def xdg_title_change(arguments: list[str]) -> None: |
|
| 268 | + | """Change a mapped window title when the runner requests it.""" |
|
| 269 | + | ||
| 270 | + | if len(arguments) != 2: |
|
| 271 | + | raise Failure("xdg-title-change requires INITIAL CHANGED") |
|
| 272 | + | trigger = Path(os.environ["SWM_TITLE_TRIGGER"]) |
|
| 273 | + | connection = Connection(WlCompositor, WlShm, XdgWmBase) |
|
| 274 | + | window = Window(connection, arguments[0], 0xFF336699) |
|
| 275 | + | connection.roundtrips(3) |
|
| 276 | + | deadline = time.monotonic() + 5 |
|
| 277 | + | while not trigger.exists(): |
|
| 278 | + | if time.monotonic() >= deadline: |
|
| 279 | + | raise Failure("timed out waiting for title trigger") |
|
| 280 | + | time.sleep(0.01) |
|
| 281 | + | window.toplevel.set_title(arguments[1]) |
|
| 282 | + | window.surface.commit() |
|
| 283 | + | connection.display.flush() |
|
| 284 | + | while window.running: |
|
| 285 | + | connection.display.dispatch(block=True) |
|
| 286 | + | window.destroy() |
|
| 287 | + | connection.close() |
|
| 288 | + | ||
| 289 | + | ||
| 267 | 290 | def xdg_resize(arguments: list[str]) -> None: |
|
| 268 | 291 | """Commit a client-selected size after an XDG window maps. |
|
| 269 | 292 | ||
| 270 | 293 | The window maps with fixed size limits so the compositor cannot choose its |
|
| 271 | 294 | initial size, then drops those limits and commits each requested size on |
| 1476 | 1499 | connection.disconnect() |
|
| 1477 | 1500 | ||
| 1478 | 1501 | ||
| 1479 | 1502 | ROLES = { |
|
| 1480 | 1503 | "xdg": xdg_client, |
|
| 1504 | + | "xdg-title-change": xdg_title_change, |
|
| 1481 | 1505 | "xdg-deferred-configure": xdg_deferred_configure, |
|
| 1482 | 1506 | "xdg-resize": xdg_resize, |
|
| 1483 | 1507 | "xdg-lifecycle": xdg_lifecycle, |
|
| 1484 | 1508 | "premap-fullscreen": premap_fullscreen, |
|
| 1485 | 1509 | "transient": transient, |
test/ctl_unit.c
+40 -0
| 1 | + | #define _POSIX_C_SOURCE 200809L |
|
| 1 | 2 | #include <assert.h> |
|
| 2 | 3 | #include <fcntl.h> |
|
| 3 | 4 | #include <sys/wait.h> |
|
| 4 | 5 | #include <unistd.h> |
|
| 5 | 6 |
| 24 | 25 | assert(waitpid(child, &status, 0) == child); |
|
| 25 | 26 | assert(WIFEXITED(status)); |
|
| 26 | 27 | assert(WEXITSTATUS(status) == EXIT_FAILURE); |
|
| 27 | 28 | } |
|
| 28 | 29 | ||
| 30 | + | /* Verify active-window updates and single-line title output. */ |
|
| 31 | + | static void test_active_window_events(void) { |
|
| 32 | + | event_window_t first = { .title = "one", .active = true, .ready = true }; |
|
| 33 | + | event_window_t second = { .title = "two", .ready = true }; |
|
| 34 | + | char output[128] = { 0 }; |
|
| 35 | + | FILE *capture = tmpfile(); |
|
| 36 | + | int saved; |
|
| 37 | + | ||
| 38 | + | assert(capture && fflush(stdout) == 0); |
|
| 39 | + | saved = dup(STDOUT_FILENO); |
|
| 40 | + | assert(saved >= 0 && dup2(fileno(capture), STDOUT_FILENO) >= 0); |
|
| 41 | + | wl_list_init(&event_windows); |
|
| 42 | + | wl_list_insert(event_windows.prev, &first.link); |
|
| 43 | + | wl_list_insert(event_windows.prev, &second.link); |
|
| 44 | + | ||
| 45 | + | publish_active_window(); |
|
| 46 | + | publish_active_window(); |
|
| 47 | + | second.title = "renamed"; |
|
| 48 | + | publish_active_window(); |
|
| 49 | + | first.title = "one\nchanged"; |
|
| 50 | + | first.ready = false; |
|
| 51 | + | publish_active_window(); |
|
| 52 | + | first.ready = true; |
|
| 53 | + | publish_active_window(); |
|
| 54 | + | first.active = false; |
|
| 55 | + | second.active = true; |
|
| 56 | + | publish_active_window(); |
|
| 57 | + | second.active = false; |
|
| 58 | + | publish_active_window(); |
|
| 59 | + | ||
| 60 | + | assert(dup2(saved, STDOUT_FILENO) >= 0); |
|
| 61 | + | close(saved); |
|
| 62 | + | rewind(capture); |
|
| 63 | + | assert(fread(output, 1, sizeof(output) - 1, capture) > 0); |
|
| 64 | + | assert(!strcmp(output, "one\none changed\nrenamed\n\n")); |
|
| 65 | + | fclose(capture); |
|
| 66 | + | } |
|
| 67 | + | ||
| 29 | 68 | int main(void) { |
|
| 30 | 69 | char escaped[64] = {}; |
|
| 31 | 70 | FILE *stream = tmpfile(); |
|
| 32 | 71 | ||
| 33 | 72 | assert(stream); |
| 38 | 77 | assert(fclose(stream) == 0); |
|
| 39 | 78 | assert(parse_color("#012345") == 0x012345ff); |
|
| 40 | 79 | assert(parse_color("#01234567") == 0x01234567); |
|
| 41 | 80 | expect_invalid_color("# 12345"); |
|
| 42 | 81 | expect_invalid_color("#+12345"); |
|
| 82 | + | test_active_window_events(); |
|
| 43 | 83 | return EXIT_SUCCESS; |
|
| 44 | 84 | } |
test/run.py
+35 -0
| 363 | 363 | virtual_keyboard(36) |
|
| 364 | 364 | wait_title("two" if before == "one" else "one") |
|
| 365 | 365 | ||
| 366 | 366 | test("window publication and focus", publication) |
|
| 367 | 367 | ||
| 368 | + | def active_window_events() -> None: |
|
| 369 | + | title = current_title() |
|
| 370 | + | log = client_log("active-window-events") |
|
| 371 | + | subscriber = spawn( |
|
| 372 | + | log, |
|
| 373 | + | BIN / "swmctl", |
|
| 374 | + | "events", |
|
| 375 | + | "--filter=active-window", |
|
| 376 | + | env=os.environ.copy(), |
|
| 377 | + | ) |
|
| 378 | + | eventually("initial active-window event", lambda: log.read_text().splitlines() == [title]) |
|
| 379 | + | virtual_keyboard(36) |
|
| 380 | + | next_title = "two" if title == "one" else "one" |
|
| 381 | + | wait_title(next_title) |
|
| 382 | + | eventually("changed active-window event", lambda: log.read_text().splitlines()[-1] == next_title) |
|
| 383 | + | trigger = Path(os.environ["SWM_TEST_DIR"]) / "active-window-title.trigger" |
|
| 384 | + | env = os.environ.copy() |
|
| 385 | + | env["SWM_TITLE_TRIGGER"] = str(trigger) |
|
| 386 | + | window = spawn( |
|
| 387 | + | client_log("active-window-title"), |
|
| 388 | + | *client_command("xdg-title-change", "initial-title", "changed-title"), |
|
| 389 | + | env=env, |
|
| 390 | + | ) |
|
| 391 | + | wait_title("initial-title") |
|
| 392 | + | eventually("new active-window event", lambda: log.read_text().splitlines()[-1] == "initial-title") |
|
| 393 | + | trigger.write_text("change\n") |
|
| 394 | + | wait_title("changed-title") |
|
| 395 | + | eventually("active title change", lambda: log.read_text().splitlines()[-1] == "changed-title") |
|
| 396 | + | terminate(window) |
|
| 397 | + | eventually("focus after title client closes", lambda: current_title() in {"one", "two"}) |
|
| 398 | + | eventually("active window after close", lambda: log.read_text().splitlines()[-1] == current_title()) |
|
| 399 | + | terminate(subscriber) |
|
| 400 | + | ||
| 401 | + | test("active-window events", active_window_events) |
|
| 402 | + | ||
| 368 | 403 | def deferred_configure() -> None: |
|
| 369 | 404 | report = Path(os.environ["SWM_TEST_DIR"]) / "deferred-configure.geometry" |
|
| 370 | 405 | env = os.environ.copy() |
|
| 371 | 406 | env["SWM_CLIENT_REPORT"] = str(report) |
|
| 372 | 407 | client = spawn( |