i3 - improved tiling WM


Add optional bidirectional interface to i3bar. (ver.7)

Patch status: superseded

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.

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

130
@@ -50,6 +50,10 @@ struct status_block {
131
     uint32_t x_offset;
132
     uint32_t x_append;
133
 
134
+    /* Optional */
135
+    char *name;
136
+    char *instance;
137
+
138
     TAILQ_ENTRY(status_block) blocks;
139
 };
140
 

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

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

b/i3bar/src/xcb.c

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