i3 - improved tiling WM


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

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.

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

b/docs/i3bar-protocol

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

b/i3bar/include/child.h

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

b/i3bar/include/common.h

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

b/i3bar/src/child.c

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

b/i3bar/src/parse_json_header.c

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

b/i3bar/src/xcb.c

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