i3 - improved tiling WM


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

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

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

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

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

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

b/i3bar/src/xcb.c

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