i3 - improved tiling WM


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

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.

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

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

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

313
@@ -31,6 +31,7 @@ static enum {
314
     KEY_VERSION,
315
     KEY_STOP_SIGNAL,
316
     KEY_CONT_SIGNAL,
317
+    KEY_BIDIRECTIONAL,
318
     NO_KEY
319
 } current_key;
320
 
321
@@ -51,6 +52,8 @@ static int header_integer(void *ctx, long val) {
322
         case KEY_CONT_SIGNAL:
323
             child->cont_signal = val;
324
             break;
325
+        case KEY_BIDIRECTIONAL:
326
+            child->bidirectional = val;
327
         default:
328
             break;
329
     }
330
@@ -71,6 +74,8 @@ static int header_map_key(void *ctx, const unsigned char *stringval, unsigned in
331
         current_key = KEY_STOP_SIGNAL;
332
     } else if (CHECK_KEY("cont_signal")) {
333
         current_key = KEY_CONT_SIGNAL;
334
+    } else if (CHECK_KEY("bidirectional")) {
335
+        current_key = KEY_BIDIRECTIONAL;
336
     }
337
     return 1;
338
 }

b/i3bar/src/xcb.c

343
@@ -306,12 +306,14 @@ void handle_button(xcb_button_press_event_t *event) {
344
     }
345
 
346
     int32_t x = event->event_x >= 0 ? event->event_x : 0;
347
+    int32_t original_x = x;
348
 
349
     DLOG("Got Button %d\n", event->detail);
350
 
351
     switch (event->detail) {
352
         case 1:
353
-            /* Left Mousbutton. We determine, which button was clicked
354
+        case 3:
355
+            /* Left or right Mousbutton. We determine, which button was clicked
356
              * and set cur_ws accordingly */
357
             TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
358
                 DLOG("x = %d\n", x);
359
@@ -321,8 +323,43 @@ void handle_button(xcb_button_press_event_t *event) {
360
                 x -= cur_ws->name_width + 11;
361
             }
362
             if (cur_ws == NULL) {
363
+                /* No workspace button was pressed.
364
+                 * Check if a status block has been clicked.
365
+                 * This of course only has an effect,
366
+                 * if the child reported bidirectional protocol usage. */
367
+
368
+                /* First calculate width of tray area */
369
+                trayclient *trayclient;
370
+                int tray_width = 0;
371
+                TAILQ_FOREACH_REVERSE(trayclient, walk->trayclients, tc_head, tailq) {
372
+                    if (!trayclient->mapped)
373
+                        continue;
374
+                    tray_width += (font.height + 2);
375
+                }
376
+
377
+                int block_x = 0, last_block_x;
378
+                int offset = (walk->rect.w - (statusline_width + tray_width)) - 10;
379
+
380
+                x = original_x - offset > 0 ? original_x - offset : 0;
381
+
382
+                struct status_block *block;
383
+
384
+                TAILQ_FOREACH(block, &statusline_head, blocks) {
385
+                    last_block_x = block_x;
386
+                    block_x += block->width + block->x_offset + block->x_append;
387
+
388
+                    if(x <= block_x && x >= last_block_x) {
389
+                        send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y);
390
+                        return;
391
+                    }
392
+                }
393
                 return;
394
             }
395
+            if (event->detail == 3)
396
+                /* Don't actually switch the
397
+                 * workspace if it was the right mouse button
398
+                 */
399
+                return;
400
             break;
401
         case 4:
402
             /* Mouse wheel up. We select the previous ws, if any.