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 |