i3 - improved tiling WM


Feature: EWMH desktop properties

Patch status: needinfo

Patch by Tony Crisci

Long description:

Implement the following EWMH desktop properties:

* _NET_WM_DESKTOP
* _NET_DESKTOP_VIEWPORT
* _NET_DESKTOP_NAMES
* _NET_NUMBER_OF_DESKTOPS

And one client message:

* _NET_CURRENT_DESKTOP

For more information see the EWMH spec at:

http://standards.freedesktop.org/wm-spec/latest

This should enable most of the features of most taskbars and pagers such
as candybar, xfce4-panel, tint2, and others.

fixes #1241

To apply this patch, use:
curl http://cr.i3wm.org/patch/543/raw.patch | git am

b/include/atoms.xmacro

41
@@ -16,6 +16,9 @@ xmacro(_NET_WM_STRUT_PARTIAL)
42
 xmacro(_NET_CLIENT_LIST)
43
 xmacro(_NET_CLIENT_LIST_STACKING)
44
 xmacro(_NET_CURRENT_DESKTOP)
45
+xmacro(_NET_NUMBER_OF_DESKTOPS)
46
+xmacro(_NET_DESKTOP_NAMES)
47
+xmacro(_NET_DESKTOP_VIEWPORT)
48
 xmacro(_NET_ACTIVE_WINDOW)
49
 xmacro(_NET_STARTUP_ID)
50
 xmacro(_NET_WORKAREA)

b/include/data.h

55
@@ -501,6 +501,10 @@ struct Con {
56
      * workspace is not a named workspace (for named workspaces, num == -1) */
57
     int num;
58
 
59
+    /** if this is a workspace, this is the index of the desktop in the context
60
+     * of ewmh standards compliance. */
61
+    uint32_t ewmh_desktop_index;
62
+
63
     struct Con *parent;
64
 
65
     struct Rect rect;

b/include/ewmh.h

70
@@ -10,10 +10,9 @@
71
 #pragma once
72
 
73
 /**
74
- * Updates _NET_CURRENT_DESKTOP with the current desktop number.
75
- *
76
- * EWMH: The index of the current desktop. This is always an integer between 0
77
- * and _NET_NUMBER_OF_DESKTOPS - 1.
78
+ * Updates EWMH properties of the root and managed windows related to
79
+ * _NET_CURRENT_DESKTOP so that other clients such as taskbars and pagers can
80
+ * find information about the state of the managed windows.
81
  *
82
  */
83
 void ewmh_update_current_desktop(void);
84
@@ -44,6 +43,16 @@ void ewmh_update_client_list(xcb_window_t *list, int num_windows);
85
  */
86
 void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows);
87
 
88
+/*
89
+ * Updates the _NET_WM_DESKTOP hint.
90
+ *
91
+ * Cardinal to determine the desktop the window is in (or wants to
92
+ * be) starting with 0 for the first desktop.
93
+ * http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm140251368061040
94
+ *
95
+ */
96
+void ewmh_update_wm_desktop(xcb_window_t window, uint32_t idx);
97
+
98
 /**
99
  * Set up the EWMH hints on the root window.
100
  *

b/src/ewmh.c

105
@@ -12,32 +12,63 @@
106
 #include "all.h"
107
 
108
 /*
109
- * Updates _NET_CURRENT_DESKTOP with the current desktop number.
110
- *
111
- * EWMH: The index of the current desktop. This is always an integer between 0
112
- * and _NET_NUMBER_OF_DESKTOPS - 1.
113
+ * Updates EWMH properties of the root and managed windows related to
114
+ * _NET_CURRENT_DESKTOP so that other clients such as taskbars and pagers can
115
+ * find information about the state of the managed windows.
116
  *
117
  */
118
 void ewmh_update_current_desktop(void) {
119
     Con *focused_ws = con_get_workspace(focused);
120
     Con *output;
121
     uint32_t idx = 0;
122
-    /* We count to get the index of this workspace because named workspaces
123
-     * don’t have the ->num property */
124
+
125
     TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
126
         Con *ws;
127
         TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) {
128
             if (STARTS_WITH(ws->name, "__"))
129
                 continue;
130
 
131
+            /* this index can be used in the context of client requests
132
+             * specified by ewmh for simple window manager commands */
133
+            ws->ewmh_desktop_index = idx;
134
+
135
+            /* _NET_CURRENT_DESKTOP
136
+             * The index of the current desktop. This is always an integer
137
+             * between 0 and _NET_NUMBER_OF_DESKTOPS - 1.
138
+             */
139
             if (ws == focused_ws) {
140
                 xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root,
141
                         A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx);
142
-                return;
143
             }
144
+
145
+            /* _NET_DESKTOP_VIEWPORT
146
+             * Array of pairs of cardinals that define the top left corner of each desktop's viewport.
147
+             */
148
+            uint32_t viewport[] = {
149
+                output->rect.x,
150
+                output->rect.y,
151
+            };
152
+
153
+            xcb_change_property(conn,
154
+                    (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND),
155
+                    root, A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, 2, viewport);
156
+
157
+            /* _NET_DESKTOP_NAMES
158
+             * The names of all virtual desktops
159
+             */
160
+            xcb_change_property(conn,
161
+                    (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND),
162
+                    root, A__NET_DESKTOP_NAMES, A_UTF8_STRING, 8,
163
+                    strlen(ws->name) + 1, ws->name);
164
+
165
             ++idx;
166
         }
167
     }
168
+
169
+    /* _NET_NUMBER_OF_DESKTOPS
170
+     */
171
+    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root,
172
+            A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx);
173
 }
174
 
175
 /*
176
@@ -104,6 +135,24 @@ void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows) {
177
 }
178
 
179
 /*
180
+ * Updates the _NET_WM_DESKTOP hint.
181
+ *
182
+ * Cardinal to determine the desktop the window is in (or wants to
183
+ * be) starting with 0 for the first desktop.
184
+ *
185
+ */
186
+void ewmh_update_wm_desktop(xcb_window_t window, uint32_t idx) {
187
+    xcb_change_property(conn,
188
+            XCB_PROP_MODE_REPLACE,
189
+            window,
190
+            A__NET_WM_DESKTOP,
191
+            XCB_ATOM_CARDINAL,
192
+            32,
193
+            1,
194
+            &idx);
195
+}
196
+
197
+/*
198
  * Set up the EWMH hints on the root window.
199
  *
200
  */
201
@@ -138,5 +187,5 @@ void ewmh_setup_hints(void) {
202
     /* I’m not entirely sure if we need to keep _NET_WM_NAME on root. */
203
     xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_WM_NAME, A_UTF8_STRING, 8, strlen("i3"), "i3");
204
 
205
-    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, 19, supported_atoms);
206
+    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 35, 19, supported_atoms);
207
 }

b/src/handlers.c

212
@@ -786,6 +786,29 @@ static void handle_client_message(xcb_client_message_event_t *event) {
213
                 XCB_ATOM_CARDINAL, 32, 4,
214
                 &r);
215
         xcb_flush(conn);
216
+    } else if (event->type == A__NET_CURRENT_DESKTOP) {
217
+        if (event->format != 32)
218
+            return;
219
+
220
+        DLOG("_NET_CURRENT_DESKTOP for ewmh desktop index %d\n", event->data.data32[0]);
221
+
222
+        Con *output;
223
+        TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
224
+            Con *ws;
225
+            TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) {
226
+                if (STARTS_WITH(ws->name, "__"))
227
+                    continue;
228
+
229
+                if (event->data.data32[0] == ws->ewmh_desktop_index) {
230
+                    DLOG("Client message requests \"%s\" become the current workspace (con = %p). \n",
231
+                            ws->name, ws);
232
+
233
+                    workspace_show(ws);
234
+                    tree_render();
235
+                    return;
236
+                }
237
+            }
238
+        }
239
     } else {
240
         DLOG("unhandled clientmessage\n");
241
         return;

b/src/ipc.c

246
@@ -88,6 +88,9 @@ void ipc_send_event(const char *event, uint32_t message_type, const char *payloa
247
 
248
         ipc_send_message(current->fd, strlen(payload), message_type, (const uint8_t*)payload);
249
     }
250
+
251
+    if (message_type == I3_IPC_EVENT_WORKSPACE)
252
+        ewmh_update_current_desktop();
253
 }
254
 
255
 /*
256
@@ -1067,6 +1070,8 @@ void ipc_send_workspace_focus_event(Con *current, Con *old) {
257
     ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, (const char *)payload);
258
     y(free);
259
     setlocale(LC_NUMERIC, "");
260
+
261
+    ewmh_update_current_desktop();
262
 }
263
 
264
 /**

b/src/workspace.c

269
@@ -436,9 +436,6 @@ static void _workspace_show(Con *workspace) {
270
     if (old_output != new_output) {
271
         x_set_warp_to(&next->rect);
272
     }
273
-
274
-    /* Update the EWMH hints */
275
-    ewmh_update_current_desktop();
276
 }
277
 
278
 /*

b/src/x.c

283
@@ -954,10 +954,15 @@ void x_push_changes(Con *con) {
284
 
285
         walk = client_list_windows;
286
 
287
-        /* reorder by initial mapping */
288
         TAILQ_FOREACH(state, &initial_mapping_head, initial_mapping_order) {
289
-            if (con_has_managed_window(state->con))
290
+            if (con_has_managed_window(state->con)) {
291
+                /* update _NET_WM_DESKTOP */
292
+                ewmh_update_wm_desktop(state->con->window->id,
293
+                        con_get_workspace(state->con)->ewmh_desktop_index);
294
+
295
+                /* reorder by initial mapping */
296
                 *walk++ = state->con->window->id;
297
+            }
298
         }
299
 
300
         ewmh_update_client_list(client_list_windows, client_list_count);

b/testcases/t/518-ewmh-desktops.t

306
@@ -0,0 +1,236 @@
307
+#!perl
308
+# vim:ts=4:sw=4:expandtab
309
+#
310
+# Please read the following documents before working on tests:
311
+# • http://build.i3wm.org/docs/testsuite.html
312
+#   (or docs/testsuite)
313
+#
314
+# • http://build.i3wm.org/docs/lib-i3test.html
315
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
316
+#
317
+# • http://build.i3wm.org/docs/ipc.html
318
+#   (or docs/ipc)
319
+#
320
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
321
+#   (unless you are already familiar with Perl)
322
+#
323
+# Tests that EWMH desktop hints and client messages work well enough for a
324
+# pager or taskbar like candybar, xfce4-panel, tint2, etc.
325
+# Ticket: #1241
326
+# Bug still in: 4.7.2-149-g708996b
327
+use i3test i3_autostart => 0;
328
+
329
+my $config = <<EOT;
330
+# i3 config file (v4)
331
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
332
+
333
+workspace "1:L" output fake-0
334
+workspace "2:R" output fake-1
335
+workspace "3:L" output fake-0
336
+workspace "4:R" output fake-1
337
+
338
+fake-outputs 1000x500+1+2,1000x500+1000+500
339
+EOT
340
+
341
+my $pid = launch_with_config($config);
342
+
343
+# constants for readability
344
+my $OUTPUT_L_X = 1;
345
+my $OUTPUT_L_Y = 2;
346
+my $OUTPUT_R_X = 1000;
347
+my $OUTPUT_R_Y = 500;
348
+
349
+# boilerplate for property access
350
+sub x_get_property {
351
+    my ($prop, $prop_type, $win_id) = @_;
352
+
353
+    $win_id = $x->get_root_window() unless $win_id;
354
+
355
+    my $cookie = $x->get_property(
356
+        0,
357
+        $win_id,
358
+        $x->atom(name => $prop)->id,
359
+        $x->atom(name => $prop_type)->id,
360
+        0,
361
+        4096,
362
+    );
363
+    return $x->get_property_reply($cookie->{sequence});
364
+}
365
+
366
+# _NET_NUMBER_OF_DESKTOPS
367
+sub get_number_of_desktops {
368
+    my $reply = x_get_property('_NET_NUMBER_OF_DESKTOPS', 'CARDINAL');
369
+    my $len = $reply->{length};
370
+
371
+    return -1 if $len == 0;
372
+
373
+    return unpack("L", $reply->{value});
374
+}
375
+
376
+# _NET_DESKTOP_NAMES
377
+# The names of all virtual desktops
378
+sub get_desktop_names {
379
+    my $reply = x_get_property('_NET_DESKTOP_NAMES', 'UTF8_STRING');
380
+    my $len = $reply->{value_len} - 1;
381
+
382
+    return () if $len < 1;
383
+
384
+    return split(/\0/, unpack("a$len", $reply->{value}));
385
+}
386
+
387
+# _NET_CURRENT_DESKTOP
388
+# The index of the current desktop. This is always an integer
389
+# between 0 and _NET_NUMBER_OF_DESKTOPS - 1.
390
+sub get_current_desktop {
391
+    my $reply = x_get_property('_NET_CURRENT_DESKTOP', 'CARDINAL');
392
+
393
+    my $len = $reply->{length};
394
+    return -1 if $len == 0;
395
+
396
+    return unpack("L", $reply->{value});
397
+}
398
+
399
+# _NET_DESKTOP_VIEWPORT
400
+# Array of pairs of cardinals that define the top left corner of each desktop's viewport.
401
+sub get_desktop_viewports {
402
+    my $reply = x_get_property('_NET_DESKTOP_VIEWPORT', 'CARDINAL');
403
+
404
+    my $len = $reply->{length};
405
+    return () if $len == 0;
406
+
407
+    my @value = unpack("L$len", $reply->{value});
408
+    my @viewports = ();
409
+
410
+    while (@value) {
411
+        my %vp = (
412
+            'x' => shift @value,
413
+            'y' => shift @value,
414
+        );
415
+        push @viewports, \%vp;
416
+    }
417
+
418
+    return @viewports;
419
+}
420
+
421
+# _NET_WM_DESKTOP
422
+# Cardinal to determine the desktop the window is in (or wants to
423
+# be) starting with 0 for the first desktop.
424
+sub get_desktop_for_window {
425
+    my ($win) = @_;
426
+    my $reply = x_get_property('_NET_WM_DESKTOP', 'CARDINAL', $win);
427
+
428
+    my $len = $reply->{length};
429
+    return -1 if $len == 0;
430
+
431
+    return unpack("L", $reply->{value});
432
+}
433
+
434
+sub send_current_desktop_message {
435
+    my ($desktop_index) = @_;
436
+
437
+    my $msg = pack "CCSLLLLLLL",
438
+        X11::XCB::CLIENT_MESSAGE, # response_type
439
+        32, # format
440
+        0,
441
+        0,
442
+        $x->atom(name => '_NET_CURRENT_DESKTOP')->id,
443
+        $desktop_index,
444
+        0,
445
+        0,
446
+        0,
447
+        0;
448
+
449
+    $x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
450
+}
451
+
452
+# The point of these hints is so any application such as a taskbar or pager can
453
+# look at the properties of the root window and have enough to meaningfully
454
+# display information for the user and send us some basic commands based on the
455
+# user input they receive, so this objective will guide the tests.
456
+sub compare_desktops_to_workspaces {
457
+    my $output = shift @_;
458
+
459
+    my $note = '-- comparing ewmh desktop properties to i3wm workspace properties';
460
+
461
+    my $output_x;
462
+    my $output_y;
463
+
464
+    if ($output =~ '^L') {
465
+        $note .= ' on the left output';
466
+        $output_x = $OUTPUT_L_X;
467
+        $output_y = $OUTPUT_L_Y;
468
+    } else {
469
+        $note .= ' on the right output';
470
+        $output_x = $OUTPUT_R_X;
471
+        $output_y = $OUTPUT_R_Y;
472
+    }
473
+
474
+    note $note, ' for workspace ', focused_ws();
475
+
476
+    is(get_number_of_desktops(), @{get_workspace_names()}, 'the number of desktops should match the number of workspaces');
477
+
478
+    # The current desktop is an index that is not related to the i3 concept of a
479
+    # "workspace number"
480
+    my $current_desktop = get_current_desktop();
481
+
482
+    is($current_desktop, get_desktop_for_window(@{get_ws_content(focused_ws)}[0]->{window}),
483
+        'a window on a workspace should have the correct desktop index');
484
+
485
+    my @desktop_names = get_desktop_names();
486
+    is_deeply(\@desktop_names, get_workspace_names(),
487
+        'the names of the desktops should match the names of the workspaces');
488
+
489
+    is($desktop_names[$current_desktop], focused_ws(),
490
+        'the value at the index of a workspace in the desktop names list should match the workspace name');
491
+
492
+    my @desktop_viewports = get_desktop_viewports();
493
+    is($desktop_viewports[$current_desktop]{x}, $output_x,
494
+        'the desktop viewport should match the workspace output x');
495
+    is($desktop_viewports[$current_desktop]{y}, $output_y,
496
+        'the desktop viewport should match the workspace output y');
497
+}
498
+
499
+#####################################################################
500
+# Get basic layout information with ewmh hints, such as the names of the
501
+# workspaces, where they are on the screen, which one is active, and so on.
502
+#####################################################################
503
+
504
+# Open a window on each of our two outputs
505
+cmd 'workspace "2:R"';
506
+my $win_ws2 = open_window;
507
+
508
+cmd 'workspace "1:L"';
509
+my $win_ws1 = open_window;
510
+
511
+note 'When a workspace is focused, the ewmh desktop hints should be updated correctly';
512
+compare_desktops_to_workspaces('Left');
513
+
514
+#####################################################################
515
+# Send basic commands, like switching to a particular workspace
516
+#####################################################################
517
+
518
+send_current_desktop_message(get_desktop_for_window($win_ws2->{id}));
519
+sync_with_i3;
520
+
521
+note 'Sending a current desktop client message should switch to the workspace with the given ewmh desktop index';
522
+compare_desktops_to_workspaces('Right');
523
+
524
+cmd 'workspace "3:L"';
525
+my $win_ws3 = open_window;
526
+
527
+cmd 'workspace "4:R"';
528
+my $win_ws4 = open_window;
529
+
530
+note 'The desktop properties should stay up to date for any number of workspaces';
531
+send_current_desktop_message(get_desktop_for_window($win_ws3->{id}));
532
+sync_with_i3;
533
+compare_desktops_to_workspaces('Left');
534
+
535
+note 'When a workspace is renamed, the ewmh desktop name property should be updated immediately to reflect the change.';
536
+cmd 'workspace 4:R';
537
+cmd 'rename workspace "4:R" to "8:web"';
538
+compare_desktops_to_workspaces('Right');
539
+
540
+exit_gracefully($pid);
541
+
542
+done_testing;