swmctl.c 19.6 KiB raw
1
/*
2
 * swmctl.c
3
 * Control and inspect swm's state.
4
 */
5
#include <errno.h>
6
#include <inttypes.h>
7
#include <stdbool.h>
8
#include <stdint.h>
9
#include <stdio.h>
10
#include <stdlib.h>
11
#include <string.h>
12
#include <wayland-client.h>
13
14
#include "ext-foreign-toplevel-list-v1-client-protocol.h"
15
#include "swm-toplevel-v1-client-protocol.h"
16
#include "swm-workspace-v1-client-protocol.h"
17
18
#define MAX_WORKSPACES      32
19
#define MAX_WORKSPACE_TITLE 256
20
21
typedef struct {
22
    bool     seen;
23
    uint32_t flags;
24
    char     title[MAX_WORKSPACE_TITLE];
25
    uint32_t color;
26
} workspace_t;
27
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;
55
56
[[noreturn]] static void usage(void) {
57
    fprintf(
58
        stderr,
59
        "usage:\n"
60
        "  swmctl workspace list\n"
61
        "  swmctl workspace get N (title | color | selected)\n"
62
        "  swmctl workspace set N (title TITLE | color COLOR)\n"
63
        "  swmctl workspace clear N (title | color)\n"
64
        "  swmctl workspace subscribe [--format=waybar]\n"
65
        "  swmctl window list\n"
66
        "  swmctl window activate IDENTIFIER\n"
67
    );
68
    exit(EXIT_FAILURE);
69
}
70
71
/* Print a color in its shortest lossless notation. */
72
static void print_color(uint32_t color) {
73
    if ((color & 0xff) == 0xff)
74
        printf("#%06" PRIx32, color >> 8);
75
    else
76
        printf("#%08" PRIx32, color);
77
}
78
79
/* Write a string with JSON quoting. */
80
static void json_string(const char *value) {
81
    const unsigned char *p;
82
83
    putchar('"');
84
    for (p = (const unsigned char *)value; *p; p++) {
85
        if (*p == '"' || *p == '\\')
86
            printf("\\%c", *p);
87
        else if (*p == '\n')
88
            fputs("\\n", stdout);
89
        else if (*p == '\r')
90
            fputs("\\r", stdout);
91
        else if (*p == '\t')
92
            fputs("\\t", stdout);
93
        else if (*p < 0x20)
94
            printf("\\u%04x", *p);
95
        else
96
            putchar(*p);
97
    }
98
    putchar('"');
99
}
100
101
/* Print one workspace as a JSON object. */
102
static void print_json_workspace(uint32_t number, workspace_t *ws) {
103
    printf("{\"workspace\":%" PRIu32 ",\"title\":", number);
104
    if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE)
105
        json_string(ws->title);
106
    else
107
        fputs("null", stdout);
108
    fputs(",\"color\":", stdout);
109
    if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR) {
110
        putchar('"');
111
        print_color(ws->color);
112
        putchar('"');
113
    } else {
114
        fputs("null", stdout);
115
    }
116
    printf(
117
        ",\"selected\":%s}",
118
        ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED ? "true" : "false"
119
    );
120
}
121
122
/* Write a JSON string safe for use as Pango markup in Waybar. */
123
static void json_markup_string(const char *value, bool has_color, uint32_t color) {
124
    const unsigned char *p;
125
126
    putchar('"');
127
    if (has_color) {
128
        printf("<span foreground=\\\"#%06" PRIx32 "\\\"", color >> 8);
129
        if ((color & 0xff) != 0xff)
130
            printf(" foreground_alpha=\\\"%" PRIu32 "\\\"", (color & 0xff) * 257);
131
        putchar('>');
132
    }
133
    for (p = (const unsigned char *)value; *p; p++) {
134
        if (*p == '&')
135
            fputs("&amp;", stdout);
136
        else if (*p == '<')
137
            fputs("&lt;", stdout);
138
        else if (*p == '>')
139
            fputs("&gt;", stdout);
140
        else if (*p == '"')
141
            fputs("&quot;", stdout);
142
        else if (*p == '\'')
143
            fputs("&apos;", stdout);
144
        else if (*p == '\\')
145
            fputs("\\\\", stdout);
146
        else if (*p == '\n')
147
            fputs("\\n", stdout);
148
        else if (*p == '\r')
149
            fputs("\\r", stdout);
150
        else if (*p == '\t')
151
            fputs("\\t", stdout);
152
        else if (*p < 0x20)
153
            printf("\\u%04x", *p);
154
        else
155
            putchar(*p);
156
    }
157
    if (has_color)
158
        fputs("</span>", stdout);
159
160
    putchar('"');
161
}
162
163
/* Print the selected workspace label as a Waybar custom-module update. */
164
static void print_waybar(void) {
165
    workspace_t *selected = nullptr;
166
    uint32_t     number   = 0;
167
    bool         has_title;
168
169
    for (uint32_t i = 0; i < MAX_WORKSPACES; i++) {
170
        if (workspaces[i].seen &&
171
            workspaces[i].flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED) {
172
            selected = &workspaces[i];
173
            number   = i + 1;
174
            break;
175
        }
176
    }
177
    has_title = selected && selected->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE;
178
    fputs("{\"text\":", stdout);
179
    json_markup_string(
180
        has_title ? selected->title : "",
181
        has_title && selected->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR,
182
        selected ? selected->color : 0
183
    );
184
    printf(",\"tooltip\":\"Workspace %" PRIu32 "\"}\n", number);
185
    fflush(stdout);
186
}
187
188
/* Print the complete cached workspace metadata as one JSON value. */
189
static void print_json_workspaces(void) {
190
    bool first = true;
191
192
    putchar('[');
193
    for (uint32_t i = 0; i < MAX_WORKSPACES; i++) {
194
        if (!workspaces[i].seen)
195
            continue;
196
        if (!first)
197
            putchar(',');
198
        print_json_workspace(i + 1, &workspaces[i]);
199
        first = false;
200
    }
201
    puts("]");
202
    fflush(stdout);
203
}
204
205
/* Cache one workspace metadata event. */
206
static void metadata(
207
    void                            *data,
208
    struct swm_workspace_manager_v1 *object,
209
    uint32_t                         number,
210
    uint32_t                         flags,
211
    const char                      *title,
212
    uint32_t                         color
213
) {
214
    workspace_t *ws;
215
216
    if (!number || number > MAX_WORKSPACES)
217
        return;
218
    ws        = &workspaces[number - 1];
219
    ws->seen  = true;
220
    ws->flags = flags;
221
    ws->color = color;
222
223
    snprintf(ws->title, sizeof(ws->title), "%s", title);
224
}
225
226
/* Finish a snapshot, printing it when following changes. */
227
static void done(void *data, struct swm_workspace_manager_v1 *object) {
228
    if (!subscribing)
229
        return;
230
    if (waybar)
231
        print_waybar();
232
    else
233
        print_json_workspaces();
234
}
235
236
static const struct swm_workspace_manager_v1_listener manager_listener = {
237
    .metadata = metadata,
238
    .done     = done,
239
};
240
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. */
332
static void global(
333
    void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version
334
) {
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
    }
352
}
353
354
static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
355
356
static const struct wl_registry_listener registry_listener = {
357
    .global        = global,
358
    .global_remove = global_remove,
359
};
360
361
/* Parse a one-based workspace number. */
362
static uint32_t parse_workspace(const char *value) {
363
    char         *end;
364
    unsigned long number;
365
366
    errno  = 0;
367
    number = strtoul(value, &end, 10);
368
369
    if (errno || *end || !number || number > MAX_WORKSPACES)
370
        usage();
371
372
    return (uint32_t)number;
373
}
374
375
/* Parse a color in #RRGGBB or #RRGGBBAA notation. */
376
static uint32_t parse_color(const char *value) {
377
    char         *end;
378
    unsigned long color;
379
    size_t        length = strlen(value);
380
381
    if ((length != 7 && length != 9) || value[0] != '#')
382
        usage();
383
    errno = 0;
384
    color = strtoul(value + 1, &end, 16);
385
    if (errno || *end || color > 0xffffffff)
386
        usage();
387
388
    return length == 7 ? (uint32_t)(color << 8) | 0xff : (uint32_t)color;
389
}
390
391
/* Print one field from a cached workspace. */
392
static void print_workspace_field(uint32_t number, const char *field) {
393
    workspace_t *ws = &workspaces[number - 1];
394
395
    if (!strcmp(field, "title")) {
396
        puts(ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_TITLE ? ws->title : "");
397
    } else if (!strcmp(field, "color")) {
398
        if (ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_COLOR)
399
            print_color(ws->color);
400
        putchar('\n');
401
    } else {
402
        puts(ws->flags & SWM_WORKSPACE_MANAGER_V1_METADATA_SELECTED ? "true" : "false");
403
    }
404
}
405
406
/* Print every cached workspace number. */
407
static void print_workspaces(void) {
408
    for (uint32_t i = 0; i < MAX_WORKSPACES; i++) {
409
        if (workspaces[i].seen)
410
            printf("%" PRIu32 "\n", i + 1);
411
    }
412
}
413
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) {
476
    struct wl_registry *registry;
477
478
    if (!(display = wl_display_connect(nullptr))) {
479
        fprintf(stderr, "swmctl: cannot connect to Wayland display\n");
480
        exit(EXIT_FAILURE);
481
    }
482
    wl_list_init(&windows);
483
    requested_kind = kind;
484
    registry       = wl_display_get_registry(display);
485
    wl_registry_add_listener(registry, &registry_listener, nullptr);
486
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
        );
497
        exit(EXIT_FAILURE);
498
    }
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
        );
505
        exit(EXIT_FAILURE);
506
    }
507
    wl_registry_destroy(registry);
508
}
509
510
int main(int argc, char **argv) {
511
    const char *identifier = nullptr;
512
    bool        window_command;
513
    const char *command;
514
    const char *field     = nullptr;
515
    const char *value     = nullptr;
516
    uint32_t    workspace = 0;
517
518
    if (argc < 3)
519
        usage();
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");
537
538
    if (window_command) {
539
        ;
540
    } else if (!strcmp(command, "list")) {
541
        if (argc != 3)
542
            usage();
543
    } else if (!strcmp(command, "get")) {
544
        if (argc != 5)
545
            usage();
546
        workspace = parse_workspace(argv[3]);
547
        field     = argv[4];
548
        if (strcmp(field, "title") && strcmp(field, "color") && strcmp(field, "selected"))
549
            usage();
550
    } else if (!strcmp(command, "set")) {
551
        if (argc != 6)
552
            usage();
553
        workspace = parse_workspace(argv[3]);
554
        field     = argv[4];
555
        value     = argv[5];
556
        if (!strcmp(field, "title")) {
557
            if (strlen(value) >= MAX_WORKSPACE_TITLE)
558
                usage();
559
        } else if (!strcmp(field, "color")) {
560
            parse_color(value);
561
        } else {
562
            usage();
563
        }
564
    } else if (!strcmp(command, "clear")) {
565
        if (argc != 5)
566
            usage();
567
        workspace = parse_workspace(argv[3]);
568
        field     = argv[4];
569
        if (strcmp(field, "title") && strcmp(field, "color"))
570
            usage();
571
    } else if (subscribing) {
572
        if (argc == 4 && !strcmp(argv[3], "--format=waybar"))
573
            waybar = true;
574
        else if (argc != 3)
575
            usage();
576
    } else {
577
        usage();
578
    }
579
    connect_manager(
580
        !window_command            ? COMMAND_WORKSPACE
581
        : !strcmp(command, "list") ? COMMAND_WINDOW_LIST
582
                                   : COMMAND_WINDOW_ACTIVATE
583
    );
584
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) {
591
        while (wl_display_dispatch(display) >= 0)
592
            ;
593
        return EXIT_FAILURE;
594
    }
595
    if (!window_command && !strcmp(command, "list")) {
596
        print_workspaces();
597
    } else if (!window_command) {
598
        if (!workspaces[workspace - 1].seen) {
599
            fprintf(stderr, "swmctl: workspace %" PRIu32 " does not exist\n", workspace);
600
            return EXIT_FAILURE;
601
        }
602
        if (!strcmp(command, "get")) {
603
            print_workspace_field(workspace, field);
604
        } else if (!strcmp(command, "set")) {
605
            if (!strcmp(field, "title"))
606
                swm_workspace_manager_v1_set_title(manager, workspace, value);
607
            else
608
                swm_workspace_manager_v1_set_color(manager, workspace, parse_color(value));
609
        } else {
610
            if (!strcmp(field, "title"))
611
                swm_workspace_manager_v1_set_title(manager, workspace, "");
612
            else
613
                swm_workspace_manager_v1_set_color(manager, workspace, 0);
614
        }
615
    }
616
    if (wl_display_flush(display) < 0 || wl_display_roundtrip(display) < 0) {
617
        fprintf(stderr, "swmctl: command failed\n");
618
        return EXIT_FAILURE;
619
    }
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);
628
    wl_display_disconnect(display);
629
630
    return EXIT_SUCCESS;
631
}