i3 - improved tiling WM


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

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.

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

125
@@ -50,6 +50,10 @@ struct status_block {
126
     uint32_t x_offset;
127
     uint32_t x_append;
128
 
129
+    /* Optional */
130
+    char *name;
131
+    char *instance;
132
+
133
     TAILQ_ENTRY(status_block) blocks;
134
 };
135
 

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

324
@@ -31,6 +31,7 @@ static enum {
325
     KEY_VERSION,
326
     KEY_STOP_SIGNAL,
327
     KEY_CONT_SIGNAL,
328
+    KEY_BIDIRECTIONAL,
329
     NO_KEY
330
 } current_key;
331
 
332
@@ -51,6 +52,8 @@ static int header_integer(void *ctx, long val) {
333
         case KEY_CONT_SIGNAL:
334
             child->cont_signal = val;
335
             break;
336
+        case KEY_BIDIRECTIONAL:
337
+            child->bidirectional = val;
338
         default:
339
             break;
340
     }
341
@@ -71,6 +74,8 @@ static int header_map_key(void *ctx, const unsigned char *stringval, unsigned in
342
         current_key = KEY_STOP_SIGNAL;
343
     } else if (CHECK_KEY("cont_signal")) {
344
         current_key = KEY_CONT_SIGNAL;
345
+    } else if (CHECK_KEY("bidirectional")) {
346
+        current_key = KEY_BIDIRECTIONAL;
347
     }
348
     return 1;
349
 }

b/i3bar/src/xcb.c

354
@@ -306,23 +306,56 @@ void handle_button(xcb_button_press_event_t *event) {
355
     }
356
 
357
     int32_t x = event->event_x >= 0 ? event->event_x : 0;
358
+    int32_t original_x = x;
359
 
360
     DLOG("Got Button %d\n", event->detail);
361
 
362
-    switch (event->detail) {
363
-        case 1:
364
-            /* Left Mousbutton. We determine, which button was clicked
365
-             * and set cur_ws accordingly */
366
-            TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
367
-                DLOG("x = %d\n", x);
368
-                if (x >= 0 && x < cur_ws->name_width + 10) {
369
-                    break;
370
-                }
371
-                x -= cur_ws->name_width + 11;
372
-            }
373
-            if (cur_ws == NULL) {
374
+
375
+    /* Check if this event regards a workspace button */
376
+    TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
377
+        DLOG("x = %d\n", x);
378
+        if (x >= 0 && x < cur_ws->name_width + 10) {
379
+            break;
380
+        }
381
+        x -= cur_ws->name_width + 11;
382
+    }
383
+
384
+    if (cur_ws == NULL) {
385
+        /* No workspace button was pressed.
386
+         * Check if a status block has been clicked.
387
+         * This of course only has an effect,
388
+         * if the child reported bidirectional protocol usage. */
389
+
390
+        /* First calculate width of tray area */
391
+        trayclient *trayclient;
392
+        int tray_width = 0;
393
+        TAILQ_FOREACH_REVERSE(trayclient, walk->trayclients, tc_head, tailq) {
394
+            if (!trayclient->mapped)
395
+                continue;
396
+            tray_width += (font.height + 2);
397
+        }
398
+
399
+        int block_x = 0, last_block_x;
400
+        int offset = (walk->rect.w - (statusline_width + tray_width)) - 10;
401
+
402
+        x = original_x - offset > 0 ? original_x - offset : 0;
403
+
404
+        struct status_block *block;
405
+
406
+        TAILQ_FOREACH(block, &statusline_head, blocks) {
407
+            last_block_x = block_x;
408
+            block_x += block->width + block->x_offset + block->x_append;
409
+
410
+            if(x <= block_x && x >= last_block_x) {
411
+                send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y);
412
                 return;
413
             }
414
+        }
415
+        return;
416
+    }
417
+
418
+    switch (event->detail) {
419
+        case 1:
420
             break;
421
         case 4:
422
             /* Mouse wheel up. We select the previous ws, if any.
423
@@ -344,6 +377,9 @@ void handle_button(xcb_button_press_event_t *event) {
424
 
425
             cur_ws = TAILQ_NEXT(cur_ws, tailq);
426
             break;
427
+        default:
428
+            /* Return if other mouse button */
429
+            return;
430
     }
431
 
432
     /* To properly handle workspace names with double quotes in them, we need