i3 - improved tiling WM


Add optional bidirectional interface to i3bar (v10)

Patch status: needinfo

Patch by enkore

Long description:

If the child specifies bidirectional:1 in the protocl header,
a JSON array will be streamed to it's stdin.
It consists of maps with at least one key (command).

Such a map is emitted if the user clicks on a status block. i.e.
{"command":"block_clicked","name":"some_block_name","instance":"optional_instance"}

The basic output format is like the rest of the i3bar protocol (i.e.
a new line after each element (map in this case))

The second version of this patch mainly introduced a better API for sending
commands.

The third version of this patch introduced the additional button parameter and thus
added support for right clicks. Now root window coordinates are passed instead of
window coordinates.

The fourth version of this patch (re-)added closing of unused pipe fd's.
Some superfluous whitespace was removed, too.

The fifth version of this patch changed some implementation details and
added handling of all mouse buttons by the child.

The sixth version of this patch changed the bidirectional-key in the
protocol header to a boolean.

The seventh version of this patch fixed a memory leak.

The 8th version of this patch fixed switching workspaces by scrolling

The 9th version is a one-line fix.

The 10th version doesn't send click commands anymore if the empty area left
of the last block is clicked.

I would consider this patch now feature-complete.

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

b/docs/i3bar-protocol

54
@@ -51,7 +51,7 @@ consists of a single JSON hash:
55
 
56
 *All features example*:
57
 ------------------------------
58
-{ "version": 1, "stop_signal": 10, "cont_signal": 12 }
59
+{ "version": 1, "stop_signal": 10, "cont_signal": 12, "bidirectional": 1 }
60
 ------------------------------
61
 
62
 (Note that before i3 v4.3 the precise format had to be +{"version":1}+,
63
@@ -110,6 +110,9 @@ cont_signal::
64
 	Specify to i3bar the signal (as an integer)to send to continue your
65
 	processing.
66
 	The default value (if none is specified) is SIGCONT.
67
+bidirectional::
68
+	If specified and true i3bar will write a infinite array (same as above)
69
+	to your stdin.
70
 
71
 === Blocks in detail
72
 
73
@@ -183,3 +186,32 @@ An example of a block which uses all possible entries follows:
74
  "instance": "eth0"
75
 }
76
 ------------------------------------------
77
+
78
+=== Bidirectional communication
79
+
80
+If enabled i3bar will send you notifications about certain events, currently
81
+only one such notification is implemented: block_clicked.
82
+It is sent if the user clicks on a block and looks like this:
83
+
84
+command::
85
+	Always block_clicked at the moment, but more are maybe added later.
86
+name::
87
+	Name of the block, if set
88
+instance::
89
+	Instance of the block, if set
90
+x, y::
91
+	X11 root window coordinates where the click occured
92
+button:
93
+	X11 button ID (for example 1 to 3 for left/middle/right mouse button)
94
+
95
+*Example*:
96
+------------------------------------------
97
+{
98
+ "command": "block_clicked",
99
+ "name": "ethernet",
100
+ "instance": "eth0",
101
+ "button": 1,
102
+ "x": 1320,
103
+ "y": 1400
104
+}
105
+------------------------------------------

b/i3bar/include/child.h

110
@@ -33,6 +33,12 @@ typedef struct {
111
      * The signal requested by the client to inform it of theun hidden state of i3bar
112
      */
113
     int cont_signal;
114
+
115
+    /**
116
+     * Enable bi-directional communication, i.e. on-click events
117
+     */
118
+    bool bidirectional;
119
+    bool bidirectional_init;
120
 } i3bar_child;
121
 
122
 /*
123
@@ -68,4 +74,10 @@ void stop_child(void);
124
  */
125
 void cont_child(void);
126
 
127
+/*
128
+ * ends the block_clicked command to the child
129
+ *
130
+ */
131
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y);
132
+
133
 #endif

b/i3bar/include/common.h

138
@@ -50,6 +50,10 @@ struct status_block {
139
     uint32_t x_offset;
140
     uint32_t x_append;
141
 
142
+    /* Optional */
143
+    char *name;
144
+    char *instance;
145
+
146
     TAILQ_ENTRY(status_block) blocks;
147
 };
148
 

b/i3bar/src/child.c

153
@@ -21,6 +21,7 @@
154
 #include <yajl/yajl_common.h>
155
 #include <yajl/yajl_parse.h>
156
 #include <yajl/yajl_version.h>
157
+#include <yajl/yajl_gen.h>
158
 
159
 #include "common.h"
160
 
161
@@ -35,6 +36,9 @@ ev_child *child_sig;
162
 yajl_callbacks callbacks;
163
 yajl_handle parser;
164
 
165
+/* JSON generator for stdout */
166
+yajl_gen gen;
167
+
168
 typedef struct parser_ctx {
169
     /* True if one of the parsed blocks was urgent */
170
     bool has_urgent;
171
@@ -85,6 +89,10 @@ static int stdin_start_array(void *context) {
172
         first = TAILQ_FIRST(&statusline_head);
173
         I3STRING_FREE(first->full_text);
174
         FREE(first->color);
175
+        if(first->name != NULL)
176
+            FREE(first->name);
177
+        if(first->instance != NULL)
178
+            FREE(first->instance);
179
         TAILQ_REMOVE(&statusline_head, first, blocks);
180
         free(first);
181
     }
182
@@ -141,6 +149,18 @@ static int stdin_string(void *context, const unsigned char *val, unsigned int le
183
             ctx->block.align = ALIGN_CENTER;
184
         }
185
     }
186
+    if (strcasecmp(ctx->last_map_key, "name") == 0) {
187
+        char *copy = (char*)malloc(len+1);
188
+        strncpy(copy, (const char *)val, len);
189
+        copy[len] = 0;
190
+        ctx->block.name = copy;
191
+    }
192
+    if (strcasecmp(ctx->last_map_key, "instance") == 0) {
193
+        char *copy = (char*)malloc(len+1);
194
+        strncpy(copy, (const char *)val, len);
195
+        copy[len] = 0;
196
+        ctx->block.instance = copy;
197
+    }
198
     return 1;
199
 }
200
 
201
@@ -322,6 +342,18 @@ void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) {
202
     cleanup();
203
 }
204
 
205
+void child_write_output(void) {
206
+    if(child.bidirectional) {
207
+        const unsigned char *output;
208
+        size_t size;
209
+        yajl_gen_get_buf(gen, &output, &size);
210
+        fwrite(output, 1, size, stdout);
211
+        fwrite("\n", 1, 1, stdout);
212
+        fflush(stdout);
213
+        yajl_gen_clear(gen);
214
+    }
215
+}
216
+
217
 /*
218
  * Start a child-process with the specified command and reroute stdin.
219
  * We actually start a $SHELL to execute the command so we don't have to care
220
@@ -347,10 +379,16 @@ void start_child(char *command) {
221
     parser = yajl_alloc(&callbacks, NULL, &parser_context);
222
 #endif
223
 
224
+    gen = yajl_gen_alloc(NULL);
225
+
226
     if (command != NULL) {
227
-        int fd[2];
228
-        if (pipe(fd) == -1)
229
-            err(EXIT_FAILURE, "pipe(fd)");
230
+        int pipe_in[2]; /* pipe we read from */
231
+        int pipe_out[2]; /* pipe we write to */
232
+
233
+        if (pipe(pipe_in) == -1)
234
+            err(EXIT_FAILURE, "pipe(pipe_in)");
235
+        if (pipe(pipe_out) == -1)
236
+            err(EXIT_FAILURE, "pipe(pipe_out)");
237
 
238
         child.pid = fork();
239
         switch (child.pid) {
240
@@ -358,10 +396,13 @@ void start_child(char *command) {
241
                 ELOG("Couldn't fork(): %s\n", strerror(errno));
242
                 exit(EXIT_FAILURE);
243
             case 0:
244
-                /* Child-process. Reroute stdout and start shell */
245
-                close(fd[0]);
246
+                /* Child-process. Reroute streams and start shell */
247
 
248
-                dup2(fd[1], STDOUT_FILENO);
249
+                close(pipe_in[0]);
250
+                close(pipe_out[1]);
251
+
252
+                dup2(pipe_in[1], STDOUT_FILENO);
253
+                dup2(pipe_out[0], STDIN_FILENO);
254
 
255
                 static const char *shell = NULL;
256
 
257
@@ -371,10 +412,13 @@ void start_child(char *command) {
258
                 execl(shell, shell, "-c", command, (char*) NULL);
259
                 return;
260
             default:
261
-                /* Parent-process. Rerout stdin */
262
-                close(fd[1]);
263
+                /* Parent-process. Reroute streams */
264
+
265
+                close(pipe_in[1]);
266
+                close(pipe_out[0]);
267
 
268
-                dup2(fd[0], STDIN_FILENO);
269
+                dup2(pipe_in[0], STDIN_FILENO);
270
+                dup2(pipe_out[1], STDOUT_FILENO);
271
 
272
                 break;
273
         }
274
@@ -396,6 +440,69 @@ void start_child(char *command) {
275
 }
276
 
277
 /*
278
+ * Internal helper functions for bidirectional comms
279
+ *
280
+ */
281
+void child_bidi_initialize(void) {
282
+    if(!child.bidirectional_init) {
283
+        yajl_gen_array_open(gen);
284
+        child_write_output();
285
+        child.bidirectional_init = true;
286
+    }
287
+}
288
+
289
+void child_bidi_key(const char *key) {
290
+    yajl_gen_string(gen, (const unsigned char *)key, strlen(key));
291
+}
292
+
293
+void child_bidi_open(const char *command) {
294
+    child_bidi_initialize();
295
+
296
+    yajl_gen_map_open(gen);
297
+
298
+    child_bidi_key("command");
299
+    yajl_gen_string(gen, (const unsigned char *)command, strlen(command));
300
+}
301
+
302
+void child_bidi_close(void) {
303
+    yajl_gen_map_close(gen);
304
+    child_write_output();
305
+}
306
+
307
+/*
308
+ * sends the block_clicked command to the child
309
+ *
310
+ */
311
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y) {
312
+    if(child.bidirectional) {
313
+        child_bidi_open("block_clicked");
314
+
315
+        if(name) {
316
+            child_bidi_key("name");
317
+            yajl_gen_string(gen, (const unsigned char *)name, strlen(name));
318
+        }
319
+
320
+        if(instance) {
321
+            child_bidi_key("instance");
322
+            yajl_gen_string(gen, (const unsigned char *)instance, strlen(instance));
323
+        }
324
+
325
+        child_bidi_key("button");
326
+        yajl_gen_integer(gen, button);
327
+
328
+        child_bidi_key("x");
329
+        yajl_gen_integer(gen, x);
330
+
331
+        child_bidi_key("y");
332
+        yajl_gen_integer(gen, y);
333
+
334
+        yajl_gen_map_close(gen);
335
+
336
+        child_write_output();
337
+    }
338
+}
339
+
340
+/*
341
  * kill()s the child-process (if any). Called when exit()ing.
342
  *
343
  */

b/i3bar/src/parse_json_header.c

348
@@ -31,6 +31,7 @@ static enum {
349
     KEY_VERSION,
350
     KEY_STOP_SIGNAL,
351
     KEY_CONT_SIGNAL,
352
+    KEY_BIDIRECTIONAL,
353
     NO_KEY
354
 } current_key;
355
 
356
@@ -54,6 +55,21 @@ static int header_integer(void *ctx, long val) {
357
         default:
358
             break;
359
     }
360
+
361
+    return 1;
362
+}
363
+
364
+static int header_boolean(void *ctx, int val) {
365
+    i3bar_child *child = ctx;
366
+
367
+    switch (current_key) {
368
+        case KEY_BIDIRECTIONAL:
369
+            child->bidirectional = val ? true : false;
370
+            break;
371
+        default:
372
+            break;
373
+    }
374
+
375
     return 1;
376
 }
377
 
378
@@ -71,13 +87,15 @@ static int header_map_key(void *ctx, const unsigned char *stringval, unsigned in
379
         current_key = KEY_STOP_SIGNAL;
380
     } else if (CHECK_KEY("cont_signal")) {
381
         current_key = KEY_CONT_SIGNAL;
382
+    } else if (CHECK_KEY("bidirectional")) {
383
+        current_key = KEY_BIDIRECTIONAL;
384
     }
385
     return 1;
386
 }
387
 
388
 static yajl_callbacks version_callbacks = {
389
     NULL, /* null */
390
-    NULL, /* boolean */
391
+    &header_boolean, /* boolean */
392
     &header_integer,
393
     NULL, /* double */
394
     NULL, /* number */

b/i3bar/src/xcb.c

399
@@ -306,24 +306,11 @@ void handle_button(xcb_button_press_event_t *event) {
400
     }
401
 
402
     int32_t x = event->event_x >= 0 ? event->event_x : 0;
403
+    int32_t original_x = x;
404
 
405
     DLOG("Got Button %d\n", event->detail);
406
 
407
     switch (event->detail) {
408
-        case 1:
409
-            /* Left Mousbutton. We determine, which button was clicked
410
-             * and set cur_ws accordingly */
411
-            TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
412
-                DLOG("x = %d\n", x);
413
-                if (x >= 0 && x < cur_ws->name_width + 10) {
414
-                    break;
415
-                }
416
-                x -= cur_ws->name_width + 11;
417
-            }
418
-            if (cur_ws == NULL) {
419
-                return;
420
-            }
421
-            break;
422
         case 4:
423
             /* Mouse wheel up. We select the previous ws, if any.
424
              * If there is no more workspace, don’t even send the workspace
425
@@ -344,6 +331,52 @@ void handle_button(xcb_button_press_event_t *event) {
426
 
427
             cur_ws = TAILQ_NEXT(cur_ws, tailq);
428
             break;
429
+        default:
430
+            /* Check if this event regards a workspace button */
431
+            TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
432
+                DLOG("x = %d\n", x);
433
+                if (x >= 0 && x < cur_ws->name_width + 10) {
434
+                    break;
435
+                }
436
+                x -= cur_ws->name_width + 11;
437
+            }
438
+            if (cur_ws == NULL) {
439
+                /* No workspace button was pressed.
440
+                 * Check if a status block has been clicked.
441
+                 * This of course only has an effect,
442
+                 * if the child reported bidirectional protocol usage. */
443
+
444
+                /* First calculate width of tray area */
445
+                trayclient *trayclient;
446
+                int tray_width = 0;
447
+                TAILQ_FOREACH_REVERSE(trayclient, walk->trayclients, tc_head, tailq) {
448
+                    if (!trayclient->mapped)
449
+                        continue;
450
+                    tray_width += (font.height + 2);
451
+                }
452
+
453
+                int block_x = 0, last_block_x;
454
+                int offset = (walk->rect.w - (statusline_width + tray_width)) - 10;
455
+
456
+                x = original_x - offset;
457
+                if(x < 0)
458
+                    return;
459
+
460
+                struct status_block *block;
461
+
462
+                TAILQ_FOREACH(block, &statusline_head, blocks) {
463
+                    last_block_x = block_x;
464
+                    block_x += block->width + block->x_offset + block->x_append;
465
+
466
+                    if(x <= block_x && x >= last_block_x) {
467
+                        send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y);
468
+                        return;
469
+                    }
470
+                }
471
+                return;
472
+            }
473
+            if (event->detail != 1)
474
+                return;
475
     }
476
 
477
     /* To properly handle workspace names with double quotes in them, we need