i3 - improved tiling WM


Add click events to i3bar

Patch status: superseded

Patch by Michael Stapelberg

Long description:

If the statusline generator (i.e. i3status) specifies click_events:true
in the protocol header, i3bar will write a JSON array on it's stdin
notifying it if the user clicks on a block.

The exact protocol is documented in docs/i3bar-protocol.

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

b/docs/i3bar-protocol

23
@@ -51,7 +51,7 @@ consists of a single JSON hash:
24
 
25
 *All features example*:
26
 ------------------------------
27
-{ "version": 1, "stop_signal": 10, "cont_signal": 12 }
28
+{ "version": 1, "stop_signal": 10, "cont_signal": 12, "click_events": true }
29
 ------------------------------
30
 
31
 (Note that before i3 v4.3 the precise format had to be +{"version":1}+,
32
@@ -110,6 +110,9 @@ cont_signal::
33
 	Specify to i3bar the signal (as an integer)to send to continue your
34
 	processing.
35
 	The default value (if none is specified) is SIGCONT.
36
+click_events::
37
+	If specified and true i3bar will write a infinite array (same as above)
38
+	to your stdin.
39
 
40
 === Blocks in detail
41
 
42
@@ -183,3 +186,28 @@ An example of a block which uses all possible entries follows:
43
  "instance": "eth0"
44
 }
45
 ------------------------------------------
46
+
47
+=== Click events
48
+
49
+If enabled i3bar will send you notifications if the user clicks on a block and
50
+looks like this:
51
+
52
+name::
53
+	Name of the block, if set
54
+instance::
55
+	Instance of the block, if set
56
+x, y::
57
+	X11 root window coordinates where the click occured
58
+button:
59
+	X11 button ID (for example 1 to 3 for left/middle/right mouse button)
60
+
61
+*Example*:
62
+------------------------------------------
63
+{
64
+ "name": "ethernet",
65
+ "instance": "eth0",
66
+ "button": 1,
67
+ "x": 1320,
68
+ "y": 1400
69
+}
70
+------------------------------------------

b/i3bar/include/child.h

75
@@ -33,6 +33,12 @@ typedef struct {
76
      * The signal requested by the client to inform it of theun hidden state of i3bar
77
      */
78
     int cont_signal;
79
+
80
+    /**
81
+     * Enable click events
82
+     */
83
+    bool click_events;
84
+    bool click_events_init;
85
 } i3bar_child;
86
 
87
 /*
88
@@ -68,4 +74,10 @@ void stop_child(void);
89
  */
90
 void cont_child(void);
91
 
92
+/*
93
+ * Generates a click event, if enabled.
94
+ *
95
+ */
96
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y);
97
+
98
 #endif

b/i3bar/include/common.h

103
@@ -50,6 +50,10 @@ struct status_block {
104
     uint32_t x_offset;
105
     uint32_t x_append;
106
 
107
+    /* Optional */
108
+    char *name;
109
+    char *instance;
110
+
111
     TAILQ_ENTRY(status_block) blocks;
112
 };
113
 

b/i3bar/src/child.c

118
@@ -21,6 +21,7 @@
119
 #include <yajl/yajl_common.h>
120
 #include <yajl/yajl_parse.h>
121
 #include <yajl/yajl_version.h>
122
+#include <yajl/yajl_gen.h>
123
 
124
 #include "common.h"
125
 
126
@@ -35,6 +36,9 @@ ev_child *child_sig;
127
 yajl_callbacks callbacks;
128
 yajl_handle parser;
129
 
130
+/* JSON generator for stdout */
131
+yajl_gen gen;
132
+
133
 typedef struct parser_ctx {
134
     /* True if one of the parsed blocks was urgent */
135
     bool has_urgent;
136
@@ -85,6 +89,8 @@ static int stdin_start_array(void *context) {
137
         first = TAILQ_FIRST(&statusline_head);
138
         I3STRING_FREE(first->full_text);
139
         FREE(first->color);
140
+        FREE(first->name);
141
+        FREE(first->instance);
142
         TAILQ_REMOVE(&statusline_head, first, blocks);
143
         free(first);
144
     }
145
@@ -141,6 +147,18 @@ static int stdin_string(void *context, const unsigned char *val, unsigned int le
146
             ctx->block.align = ALIGN_CENTER;
147
         }
148
     }
149
+    if (strcasecmp(ctx->last_map_key, "name") == 0) {
150
+        char *copy = (char*)malloc(len+1);
151
+        strncpy(copy, (const char *)val, len);
152
+        copy[len] = 0;
153
+        ctx->block.name = copy;
154
+    }
155
+    if (strcasecmp(ctx->last_map_key, "instance") == 0) {
156
+        char *copy = (char*)malloc(len+1);
157
+        strncpy(copy, (const char *)val, len);
158
+        copy[len] = 0;
159
+        ctx->block.instance = copy;
160
+    }
161
     return 1;
162
 }
163
 
164
@@ -322,6 +340,18 @@ void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) {
165
     cleanup();
166
 }
167
 
168
+void child_write_output(void) {
169
+    if (child.click_events) {
170
+        const unsigned char *output;
171
+        size_t size;
172
+        yajl_gen_get_buf(gen, &output, &size);
173
+        fwrite(output, 1, size, stdout);
174
+        fwrite("\n", 1, 1, stdout);
175
+        fflush(stdout);
176
+        yajl_gen_clear(gen);
177
+    }
178
+}
179
+
180
 /*
181
  * Start a child-process with the specified command and reroute stdin.
182
  * We actually start a $SHELL to execute the command so we don't have to care
183
@@ -347,10 +377,16 @@ void start_child(char *command) {
184
     parser = yajl_alloc(&callbacks, NULL, &parser_context);
185
 #endif
186
 
187
+    gen = yajl_gen_alloc(NULL);
188
+
189
     if (command != NULL) {
190
-        int fd[2];
191
-        if (pipe(fd) == -1)
192
-            err(EXIT_FAILURE, "pipe(fd)");
193
+        int pipe_in[2]; /* pipe we read from */
194
+        int pipe_out[2]; /* pipe we write to */
195
+
196
+        if (pipe(pipe_in) == -1)
197
+            err(EXIT_FAILURE, "pipe(pipe_in)");
198
+        if (pipe(pipe_out) == -1)
199
+            err(EXIT_FAILURE, "pipe(pipe_out)");
200
 
201
         child.pid = fork();
202
         switch (child.pid) {
203
@@ -358,10 +394,13 @@ void start_child(char *command) {
204
                 ELOG("Couldn't fork(): %s\n", strerror(errno));
205
                 exit(EXIT_FAILURE);
206
             case 0:
207
-                /* Child-process. Reroute stdout and start shell */
208
-                close(fd[0]);
209
+                /* Child-process. Reroute streams and start shell */
210
 
211
-                dup2(fd[1], STDOUT_FILENO);
212
+                close(pipe_in[0]);
213
+                close(pipe_out[1]);
214
+
215
+                dup2(pipe_in[1], STDOUT_FILENO);
216
+                dup2(pipe_out[0], STDIN_FILENO);
217
 
218
                 static const char *shell = NULL;
219
 
220
@@ -371,10 +410,13 @@ void start_child(char *command) {
221
                 execl(shell, shell, "-c", command, (char*) NULL);
222
                 return;
223
             default:
224
-                /* Parent-process. Rerout stdin */
225
-                close(fd[1]);
226
+                /* Parent-process. Reroute streams */
227
+
228
+                close(pipe_in[1]);
229
+                close(pipe_out[0]);
230
 
231
-                dup2(fd[0], STDIN_FILENO);
232
+                dup2(pipe_in[0], STDIN_FILENO);
233
+                dup2(pipe_out[1], STDOUT_FILENO);
234
 
235
                 break;
236
         }
237
@@ -395,6 +437,52 @@ void start_child(char *command) {
238
     atexit(kill_child_at_exit);
239
 }
240
 
241
+void child_click_events_initialize(void) {
242
+    if (!child.click_events_init) {
243
+        yajl_gen_array_open(gen);
244
+        child_write_output();
245
+        child.click_events_init = true;
246
+    }
247
+}
248
+
249
+void child_click_events_key(const char *key) {
250
+    yajl_gen_string(gen, (const unsigned char *)key, strlen(key));
251
+}
252
+
253
+/*
254
+ * Generates a click event, if enabled.
255
+ *
256
+ */
257
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y) {
258
+    if (child.click_events) {
259
+        child_click_events_initialize();
260
+
261
+        yajl_gen_map_open(gen);
262
+
263
+        if (name) {
264
+            child_click_events_key("name");
265
+            yajl_gen_string(gen, (const unsigned char *)name, strlen(name));
266
+        }
267
+
268
+        if (instance) {
269
+            child_click_events_key("instance");
270
+            yajl_gen_string(gen, (const unsigned char *)instance, strlen(instance));
271
+        }
272
+
273
+        child_click_events_key("button");
274
+        yajl_gen_integer(gen, button);
275
+
276
+        child_click_events_key("x");
277
+        yajl_gen_integer(gen, x);
278
+
279
+        child_click_events_key("y");
280
+        yajl_gen_integer(gen, y);
281
+
282
+        yajl_gen_map_close(gen);
283
+        child_write_output();
284
+    }
285
+}
286
+
287
 /*
288
  * kill()s the child-process (if any). Called when exit()ing.
289
  *

b/i3bar/src/parse_json_header.c

294
@@ -31,6 +31,7 @@ static enum {
295
     KEY_VERSION,
296
     KEY_STOP_SIGNAL,
297
     KEY_CONT_SIGNAL,
298
+    KEY_CLICK_EVENTS,
299
     NO_KEY
300
 } current_key;
301
 
302
@@ -54,6 +55,21 @@ static int header_integer(void *ctx, long val) {
303
         default:
304
             break;
305
     }
306
+
307
+    return 1;
308
+}
309
+
310
+static int header_boolean(void *ctx, int val) {
311
+    i3bar_child *child = ctx;
312
+
313
+    switch (current_key) {
314
+        case KEY_CLICK_EVENTS:
315
+            child->click_events = val;
316
+            break;
317
+        default:
318
+            break;
319
+    }
320
+
321
     return 1;
322
 }
323
 
324
@@ -71,13 +87,15 @@ static int header_map_key(void *ctx, const unsigned char *stringval, unsigned in
325
         current_key = KEY_STOP_SIGNAL;
326
     } else if (CHECK_KEY("cont_signal")) {
327
         current_key = KEY_CONT_SIGNAL;
328
+    } else if (CHECK_KEY("click_events")) {
329
+        current_key = KEY_CLICK_EVENTS;
330
     }
331
     return 1;
332
 }
333
 
334
 static yajl_callbacks version_callbacks = {
335
     NULL, /* null */
336
-    NULL, /* boolean */
337
+    &header_boolean, /* boolean */
338
     &header_integer,
339
     NULL, /* double */
340
     NULL, /* number */

b/i3bar/src/xcb.c

345
@@ -306,24 +306,11 @@ void handle_button(xcb_button_press_event_t *event) {
346
     }
347
 
348
     int32_t x = event->event_x >= 0 ? event->event_x : 0;
349
+    int32_t original_x = x;
350
 
351
     DLOG("Got Button %d\n", event->detail);
352
 
353
     switch (event->detail) {
354
-        case 1:
355
-            /* Left 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
-                if (x >= 0 && x < cur_ws->name_width + 10) {
360
-                    break;
361
-                }
362
-                x -= cur_ws->name_width + 11;
363
-            }
364
-            if (cur_ws == NULL) {
365
-                return;
366
-            }
367
-            break;
368
         case 4:
369
             /* Mouse wheel up. We select the previous ws, if any.
370
              * If there is no more workspace, don’t even send the workspace
371
@@ -344,6 +331,52 @@ void handle_button(xcb_button_press_event_t *event) {
372
 
373
             cur_ws = TAILQ_NEXT(cur_ws, tailq);
374
             break;
375
+        default:
376
+            /* Check if this event regards a workspace button */
377
+            TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
378
+                DLOG("x = %d\n", x);
379
+                if (x >= 0 && x < cur_ws->name_width + 10) {
380
+                    break;
381
+                }
382
+                x -= cur_ws->name_width + 11;
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;
403
+                if (x < 0)
404
+                    return;
405
+
406
+                struct status_block *block;
407
+
408
+                TAILQ_FOREACH(block, &statusline_head, blocks) {
409
+                    last_block_x = block_x;
410
+                    block_x += block->width + block->x_offset + block->x_append;
411
+
412
+                    if (x <= block_x && x >= last_block_x) {
413
+                        send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y);
414
+                        return;
415
+                    }
416
+                }
417
+                return;
418
+            }
419
+            if (event->detail != 1)
420
+                return;
421
     }
422
 
423
     /* To properly handle workspace names with double quotes in them, we need