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 wkline, xfce4-panel, tint2, and others.

fixes #1241

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

b/include/atoms.xmacro

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

b/include/data.h

54
@@ -498,6 +498,11 @@ struct Con {
55
      * workspace is not a named workspace (for named workspaces, num == -1) */
56
     int num;
57
 
58
+    /** if this is a workspace, this is the index of the desktop in the context
59
+     * of ewmh standards compliance. See:
60
+     * http://standards.freedesktop.org/wm-spec/latest */
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);

b/src/ewmh.c

88
@@ -11,33 +11,91 @@
89
  */
90
 #include "all.h"
91
 
92
+static void update_wm_desktop_recursive(Con *con, uint32_t idx) {
93
+    /* _NET_WM_DESKTOP
94
+     * Cardinal to determine the desktop the window is in (or wants to
95
+     * be) starting with 0 for the first desktop.
96
+     * http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm140251368061040
97
+     */
98
+    if (con->window) {
99
+        xcb_change_property(conn, XCB_PROP_MODE_REPLACE, con->window->id,
100
+                A__NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx);
101
+    } else {
102
+        NODES_FOREACH(con) {
103
+            update_wm_desktop_recursive(child, idx);
104
+        }
105
+    }
106
+}
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
+             * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368135008
139
+             */
140
             if (ws == focused_ws) {
141
                 xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root,
142
                         A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx);
143
-                return;
144
             }
145
+
146
+            /* The index is stored on each of the child nodes of the workspace.
147
+             * This is the primary way other clients have of discovering the
148
+             * structure of i3's managed windows. */
149
+            NODES_FOREACH(ws) {
150
+                update_wm_desktop_recursive(child, idx);
151
+            }
152
+
153
+            /* _NET_DESKTOP_VIEWPORT
154
+             * Array of pairs of cardinals that define the top left corner of each desktop's viewport.
155
+             * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368138800
156
+             */
157
+            static uint32_t viewport[2];
158
+
159
+            viewport[0] = output->rect.x;
160
+            viewport[1] = output->rect.y;
161
+
162
+            xcb_change_property(conn,
163
+                    (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND),
164
+                    root, A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, 2, viewport);
165
+
166
+            /* _NET_DESKTOP_NAMES
167
+             * The names of all virtual desktops
168
+             * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368131760
169
+             */
170
+            xcb_change_property(conn,
171
+                    (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND),
172
+                    root, A__NET_DESKTOP_NAMES, A_UTF8_STRING, 8,
173
+                    strlen(ws->name) + 1, ws->name);
174
+
175
             ++idx;
176
         }
177
     }
178
+
179
+    /* _NET_NUMBER_OF_DESKTOPS
180
+     * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368147520
181
+     */
182
+    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root,
183
+            A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx);
184
 }
185
 
186
 /*
187
@@ -138,5 +196,5 @@ void ewmh_setup_hints(void) {
188
     /* I’m not entirely sure if we need to keep _NET_WM_NAME on root. */
189
     xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_WM_NAME, A_UTF8_STRING, 8, strlen("i3"), "i3");
190
 
191
-    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, 19, supported_atoms);
192
+    xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 35, 19, supported_atoms);
193
 }

b/src/handlers.c

198
@@ -765,6 +765,29 @@ static void handle_client_message(xcb_client_message_event_t *event) {
199
                 XCB_ATOM_CARDINAL, 32, 4,
200
                 &r);
201
         xcb_flush(conn);
202
+    } else if (event->type == A__NET_CURRENT_DESKTOP) {
203
+        if (event->format != 32)
204
+            return;
205
+
206
+        DLOG("_NET_CURRENT_DESKTOP for ewmh desktop index %d\n", event->data.data32[0]);
207
+
208
+        Con *output;
209
+        TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
210
+            Con *ws;
211
+            TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) {
212
+                if (STARTS_WITH(ws->name, "__"))
213
+                    continue;
214
+
215
+                if (event->data.data32[0] == ws->ewmh_desktop_index) {
216
+                    DLOG("Client message requests \"%s\" become the current desktop (con = %p). \n",
217
+                            ws->name, ws);
218
+
219
+                    workspace_show(ws);
220
+                    tree_render();
221
+                    return;
222
+                }
223
+            }
224
+        }
225
     } else {
226
         DLOG("unhandled clientmessage\n");
227
         return;

b/src/tree.c

232
@@ -521,6 +521,9 @@ void tree_render(void) {
233
     render_con(croot, false);
234
 
235
     x_push_changes(croot);
236
+
237
+    /* broadcast the changes to other clients with ewmh properties */
238
+    ewmh_update_current_desktop();
239
     DLOG("-- END RENDERING --\n");
240
 }
241
 

b/src/workspace.c

246
@@ -432,9 +432,6 @@ static void _workspace_show(Con *workspace) {
247
     if (old_output != new_output) {
248
         x_set_warp_to(&next->rect);
249
     }
250
-
251
-    /* Update the EWMH hints */
252
-    ewmh_update_current_desktop();
253
 }
254
 
255
 /*

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

261
@@ -0,0 +1,249 @@
262
+#!perl
263
+# vim:ts=4:sw=4:expandtab
264
+#
265
+# Please read the following documents before working on tests:
266
+# • http://build.i3wm.org/docs/testsuite.html
267
+#   (or docs/testsuite)
268
+#
269
+# • http://build.i3wm.org/docs/lib-i3test.html
270
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
271
+#
272
+# • http://build.i3wm.org/docs/ipc.html
273
+#   (or docs/ipc)
274
+#
275
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
276
+#   (unless you are already familiar with Perl)
277
+#
278
+# Tests that EWMH desktop hints and client messages work well enough for a
279
+# pager or taskbar like wkline, xfce4-panel, tint2, etc.
280
+# Ticket: #1241
281
+# Bug still in: 4.7.2-149-g708996b
282
+use i3test i3_autostart => 0;
283
+
284
+my $config = <<EOT;
285
+# i3 config file (v4)
286
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
287
+
288
+workspace "1:op1" output fake-1
289
+workspace "2:op0" output fake-0
290
+workspace "3:op1" output fake-1
291
+workspace "4:op0" output fake-0
292
+
293
+fake-outputs 1000x500+1+2,1000x500+1000+500
294
+EOT
295
+
296
+my $pid = launch_with_config($config);
297
+
298
+# constants for readability
299
+my $OUTPUT_0_X = 1;
300
+my $OUTPUT_0_Y = 2;
301
+my $OUTPUT_1_X = 1000;
302
+my $OUTPUT_1_Y = 500;
303
+
304
+# boilerplate for property access
305
+sub x_get_property {
306
+    my ($prop, $prop_type, $win_id) = @_;
307
+
308
+    $win_id = $x->get_root_window() unless $win_id;
309
+
310
+    my $cookie = $x->get_property(
311
+        0,
312
+        $win_id,
313
+        $x->atom(name => $prop)->id,
314
+        $x->atom(name => $prop_type)->id,
315
+        0,
316
+        4096,
317
+    );
318
+    return $x->get_property_reply($cookie->{sequence});
319
+}
320
+
321
+# _NET_NUMBER_OF_DESKTOPS
322
+# http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368147520
323
+sub get_number_of_desktops {
324
+    my $reply = x_get_property('_NET_NUMBER_OF_DESKTOPS', 'CARDINAL');
325
+    my $len = $reply->{length};
326
+
327
+    return -1 if $len == 0;
328
+
329
+    return unpack("L", $reply->{value});
330
+}
331
+
332
+# _NET_DESKTOP_NAMES
333
+# The names of all virtual desktops
334
+# http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368131760
335
+sub get_desktop_names {
336
+    my $reply = x_get_property('_NET_DESKTOP_NAMES', 'UTF8_STRING');
337
+    my $len = $reply->{value_len} - 1;
338
+
339
+    return () if $len < 1;
340
+
341
+    return split(/\0/, unpack("a$len", $reply->{value}));
342
+}
343
+
344
+# _NET_CURRENT_DESKTOP
345
+# The index of the current desktop. This is always an integer
346
+# between 0 and _NET_NUMBER_OF_DESKTOPS - 1.
347
+# http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368135008
348
+sub get_current_desktop {
349
+    my $reply = x_get_property('_NET_CURRENT_DESKTOP', 'CARDINAL');
350
+
351
+    my $len = $reply->{length};
352
+    return -1 if $len == 0;
353
+
354
+    return unpack("L", $reply->{value});
355
+}
356
+
357
+# _NET_DESKTOP_VIEWPORT
358
+# Array of pairs of cardinals that define the top left corner of each desktop's viewport.
359
+# http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368138800
360
+sub get_desktop_viewports {
361
+    my $reply = x_get_property('_NET_DESKTOP_VIEWPORT', 'CARDINAL');
362
+
363
+    my $len = $reply->{length};
364
+    return () if $len == 0;
365
+
366
+    my @value = unpack("L$len", $reply->{value});
367
+    my @viewports = ();
368
+
369
+    while (@value) {
370
+        my %vp = (
371
+            'x' => shift @value,
372
+            'y' => shift @value,
373
+        );
374
+        push @viewports, \%vp;
375
+    }
376
+
377
+    return @viewports;
378
+}
379
+
380
+# _NET_WM_DESKTOP
381
+# Cardinal to determine the desktop the window is in (or wants to
382
+# be) starting with 0 for the first desktop.
383
+# http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm140251368061040
384
+sub get_desktop_for_window {
385
+    my ($win) = @_;
386
+    my $reply = x_get_property('_NET_WM_DESKTOP', 'CARDINAL', $win->{id});
387
+
388
+    my $len = $reply->{length};
389
+    return -1 if $len == 0;
390
+
391
+    return unpack("L", $reply->{value});
392
+}
393
+
394
+sub send_current_desktop_message {
395
+    my ($desktop_index) = @_;
396
+
397
+    my $msg = pack "CCSLLLLLLL",
398
+        X11::XCB::CLIENT_MESSAGE, # response_type
399
+        32, # format
400
+        0,
401
+        0,
402
+        $x->atom(name => '_NET_CURRENT_DESKTOP')->id,
403
+        $desktop_index,
404
+        0,
405
+        0,
406
+        0,
407
+        0;
408
+
409
+    $x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
410
+}
411
+
412
+# The point of these hints is so any application such as a taskbar or pager can
413
+# look at the properties of the root window and have enough to meaningfully
414
+# display information for the user and send us some basic commands based on the
415
+# user input they receive, so this objective will guide the tests.
416
+
417
+#####################################################################
418
+# Get basic layout information with ewmh hints, such as the names of the
419
+# workspaces, where they are on the screen, which one is active, and so on.
420
+#####################################################################
421
+
422
+# Open a window on each of our two outputs
423
+cmd 'workspace "2:op0"';
424
+my $win_ws2 = open_window;
425
+
426
+cmd 'workspace "1:op1"';
427
+my $win_ws1 = open_window;
428
+
429
+my $desktop_count = get_number_of_desktops();
430
+is($desktop_count, 2, 'correct number of desktops');
431
+
432
+# The current desktop is an index that is not related to the i3 concept of a
433
+# "workspace number"
434
+my $current_desktop = get_current_desktop();
435
+
436
+# This index should be the same on every window within the respective workspace
437
+is($current_desktop, get_desktop_for_window($win_ws1), 'ws1: its window has the correct ws index');
438
+
439
+# We know the current desktop is named "1:op1", so if we put the desktop
440
+# index into the array of workspace names, it should give us that name.
441
+my @desktop_names = get_desktop_names();
442
+is_deeply(\@desktop_names, get_workspace_names(), 'desktop names match workspace names');
443
+is($desktop_names[$current_desktop], focused_ws(), 'ws1: correct active desktop name');
444
+
445
+# The hints should also tell us a little about where the workspaces are
446
+# positioned on the screen
447
+my @desktop_viewports = get_desktop_viewports();
448
+is($desktop_viewports[$current_desktop]{x}, $OUTPUT_1_X, 'ws1: correct viewport x');
449
+is($desktop_viewports[$current_desktop]{y}, $OUTPUT_1_Y, 'ws1: correct viewport y');
450
+
451
+#####################################################################
452
+# Send basic commands, like switching to a particular workspace
453
+#####################################################################
454
+
455
+# Now we should be able to switch to a workspace based on the index of an
456
+# arbitrary window and retest
457
+send_current_desktop_message(get_desktop_for_window($win_ws2));
458
+sync_with_i3;
459
+
460
+$current_desktop = get_current_desktop();
461
+
462
+@desktop_names = get_desktop_names();
463
+is_deeply(\@desktop_names, get_workspace_names(), 'desktop names match workspace names');
464
+
465
+is($desktop_names[$current_desktop], focused_ws(), 'ws2: correct active desktop name');
466
+is($current_desktop, get_desktop_for_window($win_ws2),
467
+    'ws2: its window has the correct index');
468
+
469
+@desktop_viewports = get_desktop_viewports();
470
+is($desktop_viewports[$current_desktop]{x}, $OUTPUT_0_X, 'ws2: correct viewport x');
471
+is($desktop_viewports[$current_desktop]{y}, $OUTPUT_0_Y, 'ws2: correct viewport y');
472
+
473
+#####################################################################
474
+# The list should dynamically expand for an arbitrary number of workspaces
475
+#####################################################################
476
+
477
+cmd 'workspace "3:op1"';
478
+my $win_ws3 = open_window;
479
+
480
+cmd 'workspace "4:op0"';
481
+my $win_ws4 = open_window;
482
+
483
+send_current_desktop_message(get_desktop_for_window($win_ws3));
484
+sync_with_i3;
485
+
486
+$current_desktop = get_current_desktop();
487
+
488
+@desktop_names = get_desktop_names();
489
+is_deeply(\@desktop_names, get_workspace_names(), 'desktop names match workspace names');
490
+
491
+is($desktop_names[$current_desktop], focused_ws(), 'ws3: correct active desktop name');
492
+is($current_desktop, get_desktop_for_window($win_ws3), 'ws3: its window has the correct ws index');
493
+
494
+@desktop_viewports = get_desktop_viewports();
495
+is($desktop_viewports[$current_desktop]{x}, $OUTPUT_1_X, 'ws3: correct viewport x');
496
+is($desktop_viewports[$current_desktop]{y}, $OUTPUT_1_Y, 'ws3: correct viewport y');
497
+
498
+#####################################################################
499
+# Changes in the workspace names should be reflected in ewmh properties
500
+# immediately
501
+#####################################################################
502
+
503
+cmd 'rename workspace "4:op0" to "8:web"';
504
+@desktop_names = get_desktop_names();
505
+is(@desktop_names[get_desktop_for_window($win_ws4)], "8:web",
506
+    'renaming a workspace changes the ewmh property right away');
507
+
508
+exit_gracefully($pid);
509
+
510
+done_testing;