Add optional bidirectional interface to i3bar (v10)
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. 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 The 9th version is a one-line fix. The 10th version doesn't send click commands anymore if the empty area left of the last block is clicked. I would consider this patch now feature-complete.
To apply this patch, use:
curl http://cr.i3wm.org/patch/80/raw.patch | git am
b/docs/i3bar-protocol
54 |
@@ -51,7 +51,7 @@ consists of a single JSON hash: |
55 |
|
56 |
*All features example*: |
57 |
------------------------------ |
58 |
-{ "version": 1, "stop_signal": 10, "cont_signal": 12 } |
59 |
+{ "version": 1, "stop_signal": 10, "cont_signal": 12, "bidirectional": 1 } |
60 |
------------------------------ |
61 |
|
62 |
(Note that before i3 v4.3 the precise format had to be +{"version":1}+, |
63 |
@@ -110,6 +110,9 @@ cont_signal:: |
64 |
Specify to i3bar the signal (as an integer)to send to continue your |
65 |
processing. |
66 |
The default value (if none is specified) is SIGCONT. |
67 |
+bidirectional:: |
68 |
+ If specified and true i3bar will write a infinite array (same as above) |
69 |
+ to your stdin. |
70 |
|
71 |
=== Blocks in detail |
72 |
|
73 |
@@ -183,3 +186,32 @@ An example of a block which uses all possible entries follows: |
74 |
"instance": "eth0" |
75 |
} |
76 |
------------------------------------------ |
77 |
+ |
78 |
+=== Bidirectional communication |
79 |
+ |
80 |
+If enabled i3bar will send you notifications about certain events, currently |
81 |
+only one such notification is implemented: block_clicked. |
82 |
+It is sent if the user clicks on a block and looks like this: |
83 |
+ |
84 |
+command:: |
85 |
+ Always block_clicked at the moment, but more are maybe added later. |
86 |
+name:: |
87 |
+ Name of the block, if set |
88 |
+instance:: |
89 |
+ Instance of the block, if set |
90 |
+x, y:: |
91 |
+ X11 root window coordinates where the click occured |
92 |
+button: |
93 |
+ X11 button ID (for example 1 to 3 for left/middle/right mouse button) |
94 |
+ |
95 |
+*Example*: |
96 |
+------------------------------------------ |
97 |
+{ |
98 |
+ "command": "block_clicked", |
99 |
+ "name": "ethernet", |
100 |
+ "instance": "eth0", |
101 |
+ "button": 1, |
102 |
+ "x": 1320, |
103 |
+ "y": 1400 |
104 |
+} |
105 |
+------------------------------------------ |
b/i3bar/include/child.h
110 |
@@ -33,6 +33,12 @@ typedef struct { |
111 |
* The signal requested by the client to inform it of theun hidden state of i3bar |
112 |
*/ |
113 |
int cont_signal; |
114 |
+ |
115 |
+ /** |
116 |
+ * Enable bi-directional communication, i.e. on-click events |
117 |
+ */ |
118 |
+ bool bidirectional; |
119 |
+ bool bidirectional_init; |
120 |
} i3bar_child; |
121 |
|
122 |
/* |
123 |
@@ -68,4 +74,10 @@ void stop_child(void); |
124 |
*/ |
125 |
void cont_child(void); |
126 |
|
127 |
+/* |
128 |
+ * ends the block_clicked command to the child |
129 |
+ * |
130 |
+ */ |
131 |
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y); |
132 |
+ |
133 |
#endif |
b/i3bar/include/common.h
138 |
@@ -50,6 +50,10 @@ struct status_block { |
139 |
uint32_t x_offset; |
140 |
uint32_t x_append; |
141 |
|
142 |
+ /* Optional */ |
143 |
+ char *name; |
144 |
+ char *instance; |
145 |
+ |
146 |
TAILQ_ENTRY(status_block) blocks; |
147 |
}; |
148 |
|
b/i3bar/src/child.c
153 |
@@ -21,6 +21,7 @@ |
154 |
#include <yajl/yajl_common.h> |
155 |
#include <yajl/yajl_parse.h> |
156 |
#include <yajl/yajl_version.h> |
157 |
+#include <yajl/yajl_gen.h> |
158 |
|
159 |
#include "common.h" |
160 |
|
161 |
@@ -35,6 +36,9 @@ ev_child *child_sig; |
162 |
yajl_callbacks callbacks; |
163 |
yajl_handle parser; |
164 |
|
165 |
+/* JSON generator for stdout */ |
166 |
+yajl_gen gen; |
167 |
+ |
168 |
typedef struct parser_ctx { |
169 |
/* True if one of the parsed blocks was urgent */ |
170 |
bool has_urgent; |
171 |
@@ -85,6 +89,10 @@ static int stdin_start_array(void *context) { |
172 |
first = TAILQ_FIRST(&statusline_head); |
173 |
I3STRING_FREE(first->full_text); |
174 |
FREE(first->color); |
175 |
+ if(first->name != NULL) |
176 |
+ FREE(first->name); |
177 |
+ if(first->instance != NULL) |
178 |
+ FREE(first->instance); |
179 |
TAILQ_REMOVE(&statusline_head, first, blocks); |
180 |
free(first); |
181 |
} |
182 |
@@ -141,6 +149,18 @@ static int stdin_string(void *context, const unsigned char *val, unsigned int le |
183 |
ctx->block.align = ALIGN_CENTER; |
184 |
} |
185 |
} |
186 |
+ if (strcasecmp(ctx->last_map_key, "name") == 0) { |
187 |
+ char *copy = (char*)malloc(len+1); |
188 |
+ strncpy(copy, (const char *)val, len); |
189 |
+ copy[len] = 0; |
190 |
+ ctx->block.name = copy; |
191 |
+ } |
192 |
+ if (strcasecmp(ctx->last_map_key, "instance") == 0) { |
193 |
+ char *copy = (char*)malloc(len+1); |
194 |
+ strncpy(copy, (const char *)val, len); |
195 |
+ copy[len] = 0; |
196 |
+ ctx->block.instance = copy; |
197 |
+ } |
198 |
return 1; |
199 |
} |
200 |
|
201 |
@@ -322,6 +342,18 @@ void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) { |
202 |
cleanup(); |
203 |
} |
204 |
|
205 |
+void child_write_output(void) { |
206 |
+ if(child.bidirectional) { |
207 |
+ const unsigned char *output; |
208 |
+ size_t size; |
209 |
+ yajl_gen_get_buf(gen, &output, &size); |
210 |
+ fwrite(output, 1, size, stdout); |
211 |
+ fwrite("\n", 1, 1, stdout); |
212 |
+ fflush(stdout); |
213 |
+ yajl_gen_clear(gen); |
214 |
+ } |
215 |
+} |
216 |
+ |
217 |
/* |
218 |
* Start a child-process with the specified command and reroute stdin. |
219 |
* We actually start a $SHELL to execute the command so we don't have to care |
220 |
@@ -347,10 +379,16 @@ void start_child(char *command) { |
221 |
parser = yajl_alloc(&callbacks, NULL, &parser_context); |
222 |
#endif |
223 |
|
224 |
+ gen = yajl_gen_alloc(NULL); |
225 |
+ |
226 |
if (command != NULL) { |
227 |
- int fd[2]; |
228 |
- if (pipe(fd) == -1) |
229 |
- err(EXIT_FAILURE, "pipe(fd)"); |
230 |
+ int pipe_in[2]; /* pipe we read from */ |
231 |
+ int pipe_out[2]; /* pipe we write to */ |
232 |
+ |
233 |
+ if (pipe(pipe_in) == -1) |
234 |
+ err(EXIT_FAILURE, "pipe(pipe_in)"); |
235 |
+ if (pipe(pipe_out) == -1) |
236 |
+ err(EXIT_FAILURE, "pipe(pipe_out)"); |
237 |
|
238 |
child.pid = fork(); |
239 |
switch (child.pid) { |
240 |
@@ -358,10 +396,13 @@ void start_child(char *command) { |
241 |
ELOG("Couldn't fork(): %s\n", strerror(errno)); |
242 |
exit(EXIT_FAILURE); |
243 |
case 0: |
244 |
- /* Child-process. Reroute stdout and start shell */ |
245 |
- close(fd[0]); |
246 |
+ /* Child-process. Reroute streams and start shell */ |
247 |
|
248 |
- dup2(fd[1], STDOUT_FILENO); |
249 |
+ close(pipe_in[0]); |
250 |
+ close(pipe_out[1]); |
251 |
+ |
252 |
+ dup2(pipe_in[1], STDOUT_FILENO); |
253 |
+ dup2(pipe_out[0], STDIN_FILENO); |
254 |
|
255 |
static const char *shell = NULL; |
256 |
|
257 |
@@ -371,10 +412,13 @@ void start_child(char *command) { |
258 |
execl(shell, shell, "-c", command, (char*) NULL); |
259 |
return; |
260 |
default: |
261 |
- /* Parent-process. Rerout stdin */ |
262 |
- close(fd[1]); |
263 |
+ /* Parent-process. Reroute streams */ |
264 |
+ |
265 |
+ close(pipe_in[1]); |
266 |
+ close(pipe_out[0]); |
267 |
|
268 |
- dup2(fd[0], STDIN_FILENO); |
269 |
+ dup2(pipe_in[0], STDIN_FILENO); |
270 |
+ dup2(pipe_out[1], STDOUT_FILENO); |
271 |
|
272 |
break; |
273 |
} |
274 |
@@ -396,6 +440,69 @@ void start_child(char *command) { |
275 |
} |
276 |
|
277 |
/* |
278 |
+ * Internal helper functions for bidirectional comms |
279 |
+ * |
280 |
+ */ |
281 |
+void child_bidi_initialize(void) { |
282 |
+ if(!child.bidirectional_init) { |
283 |
+ yajl_gen_array_open(gen); |
284 |
+ child_write_output(); |
285 |
+ child.bidirectional_init = true; |
286 |
+ } |
287 |
+} |
288 |
+ |
289 |
+void child_bidi_key(const char *key) { |
290 |
+ yajl_gen_string(gen, (const unsigned char *)key, strlen(key)); |
291 |
+} |
292 |
+ |
293 |
+void child_bidi_open(const char *command) { |
294 |
+ child_bidi_initialize(); |
295 |
+ |
296 |
+ yajl_gen_map_open(gen); |
297 |
+ |
298 |
+ child_bidi_key("command"); |
299 |
+ yajl_gen_string(gen, (const unsigned char *)command, strlen(command)); |
300 |
+} |
301 |
+ |
302 |
+void child_bidi_close(void) { |
303 |
+ yajl_gen_map_close(gen); |
304 |
+ child_write_output(); |
305 |
+} |
306 |
+ |
307 |
+/* |
308 |
+ * sends the block_clicked command to the child |
309 |
+ * |
310 |
+ */ |
311 |
+void send_block_clicked(int button, const char *name, const char *instance, int x, int y) { |
312 |
+ if(child.bidirectional) { |
313 |
+ child_bidi_open("block_clicked"); |
314 |
+ |
315 |
+ if(name) { |
316 |
+ child_bidi_key("name"); |
317 |
+ yajl_gen_string(gen, (const unsigned char *)name, strlen(name)); |
318 |
+ } |
319 |
+ |
320 |
+ if(instance) { |
321 |
+ child_bidi_key("instance"); |
322 |
+ yajl_gen_string(gen, (const unsigned char *)instance, strlen(instance)); |
323 |
+ } |
324 |
+ |
325 |
+ child_bidi_key("button"); |
326 |
+ yajl_gen_integer(gen, button); |
327 |
+ |
328 |
+ child_bidi_key("x"); |
329 |
+ yajl_gen_integer(gen, x); |
330 |
+ |
331 |
+ child_bidi_key("y"); |
332 |
+ yajl_gen_integer(gen, y); |
333 |
+ |
334 |
+ yajl_gen_map_close(gen); |
335 |
+ |
336 |
+ child_write_output(); |
337 |
+ } |
338 |
+} |
339 |
+ |
340 |
+/* |
341 |
* kill()s the child-process (if any). Called when exit()ing. |
342 |
* |
343 |
*/ |
b/i3bar/src/parse_json_header.c
348 |
@@ -31,6 +31,7 @@ static enum { |
349 |
KEY_VERSION, |
350 |
KEY_STOP_SIGNAL, |
351 |
KEY_CONT_SIGNAL, |
352 |
+ KEY_BIDIRECTIONAL, |
353 |
NO_KEY |
354 |
} current_key; |
355 |
|
356 |
@@ -54,6 +55,21 @@ static int header_integer(void *ctx, long val) { |
357 |
default: |
358 |
break; |
359 |
} |
360 |
+ |
361 |
+ return 1; |
362 |
+} |
363 |
+ |
364 |
+static int header_boolean(void *ctx, int val) { |
365 |
+ i3bar_child *child = ctx; |
366 |
+ |
367 |
+ switch (current_key) { |
368 |
+ case KEY_BIDIRECTIONAL: |
369 |
+ child->bidirectional = val ? true : false; |
370 |
+ break; |
371 |
+ default: |
372 |
+ break; |
373 |
+ } |
374 |
+ |
375 |
return 1; |
376 |
} |
377 |
|
378 |
@@ -71,13 +87,15 @@ static int header_map_key(void *ctx, const unsigned char *stringval, unsigned in |
379 |
current_key = KEY_STOP_SIGNAL; |
380 |
} else if (CHECK_KEY("cont_signal")) { |
381 |
current_key = KEY_CONT_SIGNAL; |
382 |
+ } else if (CHECK_KEY("bidirectional")) { |
383 |
+ current_key = KEY_BIDIRECTIONAL; |
384 |
} |
385 |
return 1; |
386 |
} |
387 |
|
388 |
static yajl_callbacks version_callbacks = { |
389 |
NULL, /* null */ |
390 |
- NULL, /* boolean */ |
391 |
+ &header_boolean, /* boolean */ |
392 |
&header_integer, |
393 |
NULL, /* double */ |
394 |
NULL, /* number */ |
b/i3bar/src/xcb.c
399 |
@@ -306,24 +306,11 @@ void handle_button(xcb_button_press_event_t *event) { |
400 |
} |
401 |
|
402 |
int32_t x = event->event_x >= 0 ? event->event_x : 0; |
403 |
+ int32_t original_x = x; |
404 |
|
405 |
DLOG("Got Button %d\n", event->detail); |
406 |
|
407 |
switch (event->detail) { |
408 |
- case 1: |
409 |
- /* Left Mousbutton. We determine, which button was clicked |
410 |
- * and set cur_ws accordingly */ |
411 |
- TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) { |
412 |
- DLOG("x = %d\n", x); |
413 |
- if (x >= 0 && x < cur_ws->name_width + 10) { |
414 |
- break; |
415 |
- } |
416 |
- x -= cur_ws->name_width + 11; |
417 |
- } |
418 |
- if (cur_ws == NULL) { |
419 |
- return; |
420 |
- } |
421 |
- break; |
422 |
case 4: |
423 |
/* Mouse wheel up. We select the previous ws, if any. |
424 |
* If there is no more workspace, don’t even send the workspace |
425 |
@@ -344,6 +331,52 @@ void handle_button(xcb_button_press_event_t *event) { |
426 |
|
427 |
cur_ws = TAILQ_NEXT(cur_ws, tailq); |
428 |
break; |
429 |
+ default: |
430 |
+ /* Check if this event regards a workspace button */ |
431 |
+ TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) { |
432 |
+ DLOG("x = %d\n", x); |
433 |
+ if (x >= 0 && x < cur_ws->name_width + 10) { |
434 |
+ break; |
435 |
+ } |
436 |
+ x -= cur_ws->name_width + 11; |
437 |
+ } |
438 |
+ if (cur_ws == NULL) { |
439 |
+ /* No workspace button was pressed. |
440 |
+ * Check if a status block has been clicked. |
441 |
+ * This of course only has an effect, |
442 |
+ * if the child reported bidirectional protocol usage. */ |
443 |
+ |
444 |
+ /* First calculate width of tray area */ |
445 |
+ trayclient *trayclient; |
446 |
+ int tray_width = 0; |
447 |
+ TAILQ_FOREACH_REVERSE(trayclient, walk->trayclients, tc_head, tailq) { |
448 |
+ if (!trayclient->mapped) |
449 |
+ continue; |
450 |
+ tray_width += (font.height + 2); |
451 |
+ } |
452 |
+ |
453 |
+ int block_x = 0, last_block_x; |
454 |
+ int offset = (walk->rect.w - (statusline_width + tray_width)) - 10; |
455 |
+ |
456 |
+ x = original_x - offset; |
457 |
+ if(x < 0) |
458 |
+ return; |
459 |
+ |
460 |
+ struct status_block *block; |
461 |
+ |
462 |
+ TAILQ_FOREACH(block, &statusline_head, blocks) { |
463 |
+ last_block_x = block_x; |
464 |
+ block_x += block->width + block->x_offset + block->x_append; |
465 |
+ |
466 |
+ if(x <= block_x && x >= last_block_x) { |
467 |
+ send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y); |
468 |
+ return; |
469 |
+ } |
470 |
+ } |
471 |
+ return; |
472 |
+ } |
473 |
+ if (event->detail != 1) |
474 |
+ return; |
475 |
} |
476 |
|
477 |
/* To properly handle workspace names with double quotes in them, we need |