i3 - improved tiling WM


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

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.

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

The 9th version is a one-line fix.

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

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

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

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

b/i3bar/src/xcb.c

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