Expose visible window regions through swmctl

db047604e9fe6c12bbab99554c6dcf537d72f2d1
Alexis Sellier committed ago 1 parent cce36366
README +2 -2
125 125
output. When `-s` is used, that output is connected to the startup command's
126 126
standard input. Workspace metadata uses `workspace N title VALUE` and
127 127
`workspace N color #RRGGBBAA` records; empty values mean unset. Use
128 128
`swmctl events --filter=active-window` to receive the active window title for
129 129
a custom bar module.
130 -
Visible window rectangles are published atomically to
131 -
`$XDG_RUNTIME_DIR/swm-windows` in the format accepted by `slurp -r`.
130 +
`swmctl window regions` prints visible window rectangles in the format accepted
131 +
by `slurp -r`.
132 132
133 133
134 134
# TESTING AND DEVELOPMENT
135 135
136 136
Run the complete test suite with:
protocols/swm-toplevel-v1.xml +38 -3
13 13
    AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
14 14
    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
15 15
    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 16
  </copyright>
17 17
18 -
  <interface name="swm_toplevel_manager_v1" version="1">
19 -
    <description summary="activate swm toplevels">
18 +
  <interface name="swm_toplevel_manager_v1" version="2">
19 +
    <description summary="inspect and activate swm toplevels">
20 20
      This interface activates mapped toplevels by the stable identifier from
21 -
      ext-foreign-toplevel-list-v1.
21 +
      ext-foreign-toplevel-list-v1 and provides visible window regions.
22 22
    </description>
23 23
24 24
    <enum name="error">
25 25
      <entry name="invalid_identifier" value="1" summary="the toplevel identifier does not exist"/>
26 26
    </enum>
33 33
    </request>
34 34
35 35
    <request name="destroy" type="destructor">
36 36
      <description summary="destroy the manager object"/>
37 37
    </request>
38 +
39 +
    <request name="get_regions" since="2">
40 +
      <description summary="get visible window regions">
41 +
        The new object receives one snapshot of visible window regions.
42 +
      </description>
43 +
      <arg name="regions" type="new_id" interface="swm_region_list_v1"/>
44 +
    </request>
45 +
  </interface>
46 +
47 +
  <interface name="swm_region_list_v1" version="1">
48 +
    <description summary="one visible window region snapshot">
49 +
      This object sends visible window rectangles and then a done event.
50 +
    </description>
51 +
52 +
    <event name="region">
53 +
      <description summary="one visible window rectangle">
54 +
        Coordinates and dimensions use compositor layout units. The title is
55 +
        a display label with control characters replaced by spaces.
56 +
      </description>
57 +
      <arg name="x" type="int"/>
58 +
      <arg name="y" type="int"/>
59 +
      <arg name="width" type="int"/>
60 +
      <arg name="height" type="int"/>
61 +
      <arg name="title" type="string"/>
62 +
    </event>
63 +
64 +
    <event name="done">
65 +
      <description summary="end of the region snapshot">
66 +
        No more regions follow this event.
67 +
      </description>
68 +
    </event>
69 +
70 +
    <request name="destroy" type="destructor">
71 +
      <description summary="destroy the region snapshot"/>
72 +
    </request>
38 73
  </interface>
39 74
</protocol>
swm.c +43 -33
608 608
    client_t *c, struct wlr_surface *surface, double sx, double sy, uint32_t time, bool refocus
609 609
);
610 610
static void         print_status(void);
611 611
static void         publish_status(void);
612 612
static void         status_idle_notify(void *data);
613 -
static void         publish_windows(const char *runtime);
614 613
static void         execute_command(char *const argv[], const char *context);
615 614
static void         prepare_child(void);
616 615
static void         power_manager_set_mode(struct wl_listener *listener, void *data);
617 616
static void         quit(const arg_t *arg);
618 617
static void         raise_client(const arg_t *arg);
5011 5010
/* Sanitize a field for status output, leaving an unknown value empty. */
5012 5011
static void status_field(char *dst, size_t size, const char *src) {
5013 5012
    swm_sanitize_field(dst, size, src, nullptr);
5014 5013
}
5015 5014
5016 -
/* Publish visible window rectangles in slurp's predefined-region format. */
5017 -
void publish_windows(const char *runtime) {
5018 -
    client_t *c;
5019 -
    FILE     *file;
5020 -
    char      path[MAX_STATE_PATH], tmppath[MAX_STATE_PATH], title[MAX_WINDOW_STATE_FIELD];
5021 -
5022 -
    if (!runtime || snprintf(path, sizeof(path), "%s/swm-windows", runtime) >= (int)sizeof(path) ||
5023 -
        !(file = file_replace_begin(path, tmppath, sizeof(tmppath))))
5024 -
        return;
5025 -
5026 -
    wl_list_for_each(c, &clients, link) {
5027 -
        if (!c->mon || !client_surface(c)->mapped || !c->scene->node.enabled)
5028 -
            continue;
5029 -
        status_field(title, sizeof(title), client_get_title(c));
5030 -
        fprintf(
5031 -
            file, "%d,%d %dx%d %s\n", c->geom.x, c->geom.y, c->geom.width, c->geom.height, title
5032 -
        );
5033 -
    }
5034 -
    file_replace_commit(file, tmppath, path);
5035 -
}
5036 -
5037 5015
/* Publish the status once the current batch of changes has settled. */
5038 5016
void status_idle_notify(void *data) {
5039 5017
    status_idle = nullptr;
5040 5018
    publish_status();
5041 5019
}
5054 5032
        publish_status();
5055 5033
}
5056 5034
5057 5035
/* Publish current display, workspace, and focus state on standard output. */
5058 5036
void publish_status(void) {
5059 -
    monitor_t  *m = nullptr;
5060 -
    client_t   *c;
5061 -
    char        title[MAX_STATUS_FIELD], appid[MAX_STATUS_FIELD];
5062 -
    const char *runtime;
5063 -
    uint32_t    occ, urg;
5064 -
    int         i;
5037 +
    monitor_t *m = nullptr;
5038 +
    client_t  *c;
5039 +
    char       title[MAX_STATUS_FIELD], appid[MAX_STATUS_FIELD];
5040 +
    uint32_t   occ, urg;
5041 +
    int        i;
5065 5042
5066 5043
    occ = urg = 0;
5067 5044
    wl_list_for_each(c, &clients, link) {
5068 5045
        ftl_sync(c);
5069 5046
5105 5082
        if (workspaces[i].color)
5106 5083
            printf("workspace %d color #%08" PRIx32 "\n", i + 1, workspaces[i].color);
5107 5084
        else
5108 5085
            printf("workspace %d color \n", i + 1);
5109 5086
    }
5110 -
    runtime = getenv("XDG_RUNTIME_DIR");
5111 -
    publish_windows(runtime);
5112 5087
    fflush(stdout);
5113 5088
    workspace_broadcast();
5114 5089
}
5115 5090
5116 5091
/* Replace a command child with its program or an error notification. */
5863 5838
        "toplevel identifier '%s' does not exist",
5864 5839
        identifier
5865 5840
    );
5866 5841
}
5867 5842
5843 +
/* Release one completed region snapshot. */
5844 +
static void region_list_destroy(struct wl_client *client, struct wl_resource *resource) {
5845 +
    wl_resource_destroy(resource);
5846 +
}
5847 +
5848 +
static const struct swm_region_list_v1_interface region_list_impl = {
5849 +
    .destroy = region_list_destroy,
5850 +
};
5851 +
5852 +
/* Send the current visible window rectangles to one client. */
5853 +
static void toplevel_control_get_regions(
5854 +
    struct wl_client *client, struct wl_resource *resource, uint32_t id
5855 +
) {
5856 +
    struct wl_resource *regions;
5857 +
    client_t           *c;
5858 +
    char                title[MAX_WINDOW_STATE_FIELD];
5859 +
5860 +
    regions = wl_resource_create(client, &swm_region_list_v1_interface, 1, id);
5861 +
    if (!regions) {
5862 +
        wl_client_post_no_memory(client);
5863 +
        return;
5864 +
    }
5865 +
    wl_resource_set_implementation(regions, &region_list_impl, nullptr, nullptr);
5866 +
    wl_list_for_each(c, &clients, link) {
5867 +
        if (!c->mon || !client_surface(c)->mapped || !c->scene->node.enabled)
5868 +
            continue;
5869 +
        status_field(title, sizeof(title), client_get_title(c));
5870 +
        swm_region_list_v1_send_region(
5871 +
            regions, c->geom.x, c->geom.y, c->geom.width, c->geom.height, title
5872 +
        );
5873 +
    }
5874 +
    swm_region_list_v1_send_done(regions);
5875 +
}
5876 +
5868 5877
/* Release a toplevel control manager. */
5869 5878
static void toplevel_control_destroy(struct wl_client *client, struct wl_resource *resource) {
5870 5879
    wl_resource_destroy(resource);
5871 5880
}
5872 5881
5873 5882
static const struct swm_toplevel_manager_v1_interface toplevel_control_impl = {
5874 -
    .activate = toplevel_control_activate,
5875 -
    .destroy  = toplevel_control_destroy,
5883 +
    .activate    = toplevel_control_activate,
5884 +
    .destroy     = toplevel_control_destroy,
5885 +
    .get_regions = toplevel_control_get_regions,
5876 5886
};
5877 5887
5878 5888
/* Publish swm's toplevel control interface to a client. */
5879 5889
static void toplevel_control_bind(
5880 5890
    struct wl_client *client, void *data, uint32_t version, uint32_t id
6383 6393
    wl_list_init(&window_states);
6384 6394
    wl_list_init(&input_popups);
6385 6395
    load_window_states();
6386 6396
    wl_global_create(dpy, &ext_workspace_manager_v1_interface, 1, nullptr, workspace_manager_bind);
6387 6397
    wl_global_create(dpy, &swm_workspace_manager_v1_interface, 1, nullptr, workspace_metadata_bind);
6388 -
    wl_global_create(dpy, &swm_toplevel_manager_v1_interface, 1, nullptr, toplevel_control_bind);
6398 +
    wl_global_create(dpy, &swm_toplevel_manager_v1_interface, 2, nullptr, toplevel_control_bind);
6389 6399
6390 6400
    /* Publish window lists for taskbars and switchers. */
6391 6401
    ftl_mgr      = wlr_foreign_toplevel_manager_v1_create(dpy);
6392 6402
    ext_ftl_list = wlr_ext_foreign_toplevel_list_v1_create(dpy, 1);
6393 6403
    wlr_ext_image_copy_capture_manager_v1_create(dpy, 1);
swmctl.1.adoc +6 -0
17 17
18 18
*swmctl workspace clear* _N_ (*title*|*color*)
19 19
20 20
*swmctl workspace subscribe* [*--format=waybar*]
21 21
22 +
*swmctl window regions*
23 +
22 24
*swmctl events --filter=active-window*
23 25
24 26
== DESCRIPTION
25 27
26 28
*swmctl* connects to the running *swm* Wayland compositor and reads or changes
50 52
*events --filter=active-window* writes the current active window title, then
51 53
writes a new line when the active window or its title changes. It writes an
52 54
empty line when no window is active. Control characters in titles become spaces.
53 55
The command stays connected until it is stopped or the compositor exits.
54 56
57 +
*window regions* prints one snapshot of visible window rectangles. Each line
58 +
contains a position, a size, and a window title in the format used by
59 +
`slurp -r`.
60 +
55 61
== WAYBAR
56 62
57 63
The `ext/workspaces` module continues to display the numeric workspace names
58 64
`1` through `10`. A custom module can display the additional title and color
59 65
independently:
swmctl.c +66 -7
29 29
/* Wayland globals required by one command family. */
30 30
enum command_kind {
31 31
    COMMAND_WORKSPACE,
32 32
    COMMAND_WINDOW_LIST,
33 33
    COMMAND_WINDOW_ACTIVATE,
34 +
    COMMAND_WINDOW_REGIONS,
34 35
    COMMAND_EVENTS,
35 36
};
36 37
37 38
/* Cached state for one ext-foreign-toplevel handle. */
38 39
typedef struct {
54 55
55 56
static struct wl_display                   *display;
56 57
static struct swm_workspace_manager_v1     *manager;
57 58
static struct ext_foreign_toplevel_list_v1 *toplevel_list;
58 59
static struct swm_toplevel_manager_v1      *toplevel_control;
60 +
static bool                                 regions_done;
59 61
static struct wl_list                       windows;
60 62
static workspace_t                          workspaces[MAX_WORKSPACES];
61 63
static bool                                 subscribing;
62 64
static bool                                 waybar;
63 65
static bool                                 toplevel_list_finished;
78 80
        "  swmctl workspace set N (title TITLE | color COLOR)\n"
79 81
        "  swmctl workspace clear N (title | color)\n"
80 82
        "  swmctl workspace subscribe [--format=waybar]\n"
81 83
        "  swmctl window list\n"
82 84
        "  swmctl window activate IDENTIFIER\n"
85 +
        "  swmctl window regions\n"
83 86
        "  swmctl events --filter=active-window\n"
84 87
    );
85 88
    exit(EXIT_FAILURE);
86 89
}
87 90
485 488
    ) {
486 489
        toplevel_list =
487 490
            wl_registry_bind(registry, name, &ext_foreign_toplevel_list_v1_interface, 1);
488 491
        ext_foreign_toplevel_list_v1_add_listener(toplevel_list, &window_list_listener, nullptr);
489 492
    } else if (
490 -
        requested_kind == COMMAND_WINDOW_ACTIVATE &&
493 +
        (requested_kind == COMMAND_WINDOW_ACTIVATE || requested_kind == COMMAND_WINDOW_REGIONS) &&
491 494
        !strcmp(interface, swm_toplevel_manager_v1_interface.name)
492 495
    ) {
493 -
        toplevel_control = wl_registry_bind(registry, name, &swm_toplevel_manager_v1_interface, 1);
496 +
        if (requested_kind == COMMAND_WINDOW_REGIONS && version < 2)
497 +
            return;
498 +
        toplevel_control = wl_registry_bind(
499 +
            registry,
500 +
            name,
501 +
            &swm_toplevel_manager_v1_interface,
502 +
            requested_kind == COMMAND_WINDOW_REGIONS ? 2 : 1
503 +
        );
494 504
    } else if (
495 505
        requested_kind == COMMAND_EVENTS &&
496 506
        !strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name)
497 507
    ) {
498 508
        event_manager =
597 607
        putchar('\t');
598 608
        puts(window->identifier);
599 609
    }
600 610
}
601 611
612 +
/* Print one region in slurp's format. */
613 +
static void region_snapshot_region(
614 +
    void                      *data,
615 +
    struct swm_region_list_v1 *regions,
616 +
    int32_t                    x,
617 +
    int32_t                    y,
618 +
    int32_t                    width,
619 +
    int32_t                    height,
620 +
    const char                *title
621 +
) {
622 +
    printf("%d,%d %dx%d %s\n", x, y, width, height, title);
623 +
}
624 +
625 +
/* Mark the region snapshot complete. */
626 +
static void region_snapshot_done(void *data, struct swm_region_list_v1 *regions) {
627 +
    regions_done = true;
628 +
}
629 +
630 +
static const struct swm_region_list_v1_listener region_list_listener = {
631 +
    .region = region_snapshot_region,
632 +
    .done   = region_snapshot_done,
633 +
};
634 +
635 +
/* Print a complete visible-window snapshot in slurp's region format. */
636 +
static bool print_regions(void) {
637 +
    struct swm_region_list_v1 *regions;
638 +
    bool                       ok;
639 +
640 +
    regions = swm_toplevel_manager_v1_get_regions(toplevel_control);
641 +
    if (!regions)
642 +
        return false;
643 +
    swm_region_list_v1_add_listener(regions, &region_list_listener, nullptr);
644 +
    ok = wl_display_roundtrip(display) >= 0 && regions_done && fflush(stdout) == 0;
645 +
    swm_region_list_v1_destroy(regions);
646 +
    return ok;
647 +
}
648 +
602 649
/* Release cached foreign-toplevel state. */
603 650
static void destroy_windows(void) {
604 651
    window_t *window, *tmp;
605 652
606 653
    wl_list_for_each_safe(window, tmp, &windows, link) {
640 687
    registry       = wl_display_get_registry(display);
641 688
    wl_registry_add_listener(registry, &registry_listener, nullptr);
642 689
643 690
    if (wl_display_roundtrip(display) < 0 || (kind == COMMAND_WORKSPACE && !manager) ||
644 691
        (kind == COMMAND_WINDOW_LIST && !toplevel_list) ||
645 -
        (kind == COMMAND_WINDOW_ACTIVATE && !toplevel_control) ||
692 +
        ((kind == COMMAND_WINDOW_ACTIVATE || kind == COMMAND_WINDOW_REGIONS) &&
693 +
         !toplevel_control) ||
646 694
        (kind == COMMAND_EVENTS && !event_manager)) {
647 695
        fprintf(
648 696
            stderr,
649 697
            kind == COMMAND_WORKSPACE ? "swmctl: compositor does not support swm-workspace-v1\n"
650 698
            : kind == COMMAND_WINDOW_LIST
651 699
                ? "swmctl: compositor does not support ext-foreign-toplevel-list-v1\n"
652 700
            : kind == COMMAND_EVENTS
653 701
                ? "swmctl: compositor does not support wlr-foreign-toplevel-management-v1\n"
702 +
            : kind == COMMAND_WINDOW_REGIONS
703 +
                ? "swmctl: compositor does not support window regions\n"
654 704
                : "swmctl: compositor does not support swm-toplevel-v1\n"
655 705
        );
656 706
        exit(EXIT_FAILURE);
657 707
    }
658 708
    if (kind != COMMAND_WINDOW_ACTIVATE && wl_display_roundtrip(display) < 0) {
693 743
                usage();
694 744
        } else if (!strcmp(command, "activate")) {
695 745
            if (argc != 4)
696 746
                usage();
697 747
            identifier = argv[3];
748 +
        } else if (!strcmp(command, "regions")) {
749 +
            if (argc != 3)
750 +
                usage();
698 751
        } else {
699 752
            usage();
700 753
        }
701 754
    } else if (strcmp(argv[1], "workspace")) {
702 755
        usage();
743 796
            usage();
744 797
    } else {
745 798
        usage();
746 799
    }
747 800
    connect_manager(
748 -
        !window_command            ? COMMAND_WORKSPACE
749 -
        : !strcmp(command, "list") ? COMMAND_WINDOW_LIST
750 -
                                   : COMMAND_WINDOW_ACTIVATE
801 +
        !window_command               ? COMMAND_WORKSPACE
802 +
        : !strcmp(command, "list")    ? COMMAND_WINDOW_LIST
803 +
        : !strcmp(command, "regions") ? COMMAND_WINDOW_REGIONS
804 +
                                      : COMMAND_WINDOW_ACTIVATE
751 805
    );
752 806
753 807
    if (window_command) {
754 808
        if (!strcmp(command, "list"))
755 809
            print_windows();
756 -
        else
810 +
        else if (!strcmp(command, "regions")) {
811 +
            if (!print_regions()) {
812 +
                fprintf(stderr, "swmctl: failed to read window regions\n");
813 +
                return EXIT_FAILURE;
814 +
            }
815 +
        } else
757 816
            swm_toplevel_manager_v1_activate(toplevel_control, identifier);
758 817
    } else if (subscribing) {
759 818
        while (wl_display_dispatch(display) >= 0)
760 819
            ;
761 820
        return EXIT_FAILURE;
test/clients.py +4 -3
707 707
    connection = Connection(WlCompositor, WlShm, XdgWmBase)
708 708
    window = Window(connection, "output-float", 0xFF336699, fixed_size=True)
709 709
    connection.roundtrips(4)
710 710
711 711
    def geometry() -> tuple[int, ...]:
712 -
        """Read this window's published outer rectangle."""
712 +
        """Read this window's outer rectangle from the control snapshot."""
713 713
714 714
        window.toplevel.set_title("output-float")
715 715
        connection.roundtrips(3)
716 -
        contents = (Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-windows").read_text()
716 +
        control = Path(os.environ["SWM_TEST_BUILD"]) / "bin/swmctl"
717 +
        contents = subprocess.check_output([control, "window", "regions"], text=True)
717 718
        match = re.search(r"^(-?\d+),(-?\d+) (\d+)x(\d+) output-float$", contents, re.MULTILINE)
718 719
        if not match:
719 -
            raise Failure("floating window has no published geometry")
720 +
            raise Failure("floating window has no region")
720 721
        return tuple(map(int, match.groups()))
721 722
722 723
    before = geometry()
723 724
    subprocess.run(["wlr-randr", "--output", "HEADLESS-1", "--pos", "100,80"], check=True)
724 725
    moved = geometry()
test/run.py +6 -5
313 313
    path = Path(os.environ["SWM_TEST_DIR"]) / f"{name}.geometry"
314 314
    return list(map(int, path.read_text().split()))
315 315
316 316
317 317
def published_geometry(title: str) -> list[int]:
318 -
    """Return a client's compositor-published outer geometry."""
318 +
    """Return a client's outer geometry from the control snapshot."""
319 319
320 -
    path = Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-windows"
321 320
    match = re.search(
322 -
        rf"^(\d+),(\d+) (\d+)x(\d+) {re.escape(title)}$", path.read_text(), re.MULTILINE
321 +
        rf"^(\d+),(\d+) (\d+)x(\d+) {re.escape(title)}$",
322 +
        run(BIN / "swmctl", "window", "regions"),
323 +
        re.MULTILINE,
323 324
    )
324 325
    if not match:
325 326
        raise Failure(f"missing published geometry for {title}")
326 327
    return list(map(int, match.groups()))
327 328
370 371
    start_title_events()
371 372
    one = start_client("one", "ff5588dd")
372 373
    two = start_client("two", "ff55aa88")
373 374
374 375
    def publication() -> None:
375 -
        windows = (Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-windows").read_text()
376 +
        windows = run(BIN / "swmctl", "window", "regions")
376 377
        if not re.search(r"^\d+,\d+ \d+x\d+ one$", windows, re.MULTILINE):
377 378
            raise Failure("missing published rectangle for one")
378 379
        if not re.search(r"^\d+,\d+ \d+x\d+ two$", windows, re.MULTILINE):
379 380
            raise Failure("missing published rectangle for two")
380 381
        before = current_title()
381 382
        virtual_keyboard(36)
382 383
        wait_title("two" if before == "one" else "one")
383 384
384 -
    test("window publication and focus", publication)
385 +
    test("window regions and focus", publication)
385 386
386 387
    def active_window_events() -> None:
387 388
        title = current_title()
388 389
        log = client_log("active-window-events")
389 390
        subscriber = spawn(
test/unit.c +0 -1
849 849
    unsetenv("SWM_TEST_MISSING");
850 850
    expand_argv(command, expanded, storage);
851 851
    assert(!strcmp(expanded[0], "before-expanded value"));
852 852
    assert(!strcmp(expanded[1], ""));
853 853
    assert(expanded[2] == nullptr);
854 -
    publish_windows(nullptr);
855 854
}
856 855
857 856
/* Verify configured autostart commands and child tracking with harmless stubs. */
858 857
static void test_autostart_execution(void) {
859 858
    const char     *stub_path = getenv("SWM_TEST_STUB_PATH");