From ff30991b16c9b62cf5f04353f419b09912b83ede Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sun, 22 Jan 2023 18:34:14 +0100 Subject: i3bar: Add protocol for workspace buttons Closes #3818 (parent issue) Fixes #1808 Fixes #2333 Fixes #2617 Fixes #3548 --- docs/i3bar-workspace-protocol | 184 +++++++++++ docs/userguide | 24 ++ i3bar/include/child.h | 69 +++- i3bar/include/configuration.h | 11 +- i3bar/include/mode.h | 4 +- i3bar/include/outputs.h | 4 +- i3bar/include/workspaces.h | 5 +- i3bar/src/child.c | 508 ++++++++++++++++++++++-------- i3bar/src/config.c | 30 +- i3bar/src/ipc.c | 88 ++++-- i3bar/src/main.c | 4 +- i3bar/src/mode.c | 18 +- i3bar/src/outputs.c | 21 +- i3bar/src/workspaces.c | 94 +++--- i3bar/src/xcb.c | 87 ++--- include/config_directives.h | 1 + include/configuration.h | 4 + man/i3bar.man | 5 +- meson.build | 2 + parser-specs/config.spec | 5 + release-notes/changes/1-workspace_command | 1 + src/config.c | 1 + src/config_directives.c | 5 + src/ipc.c | 1 + testcases/t/201-config-parser.t | 2 +- 25 files changed, 886 insertions(+), 292 deletions(-) create mode 100644 docs/i3bar-workspace-protocol create mode 100644 release-notes/changes/1-workspace_command diff --git a/docs/i3bar-workspace-protocol b/docs/i3bar-workspace-protocol new file mode 100644 index 00000000..31acfd90 --- /dev/null +++ b/docs/i3bar-workspace-protocol @@ -0,0 +1,184 @@ +i3bar workspace buttons protocol +================================ + +This document explains the protocol in which i3bar expects input for +configuring workspace buttons. This feature is a available since i3 version +4.23. + +Programs defined by the +workspace_command+ configuration option for i3bar can +modify the workspace buttons displayed by i3bar. The command should constantly +print in its standard output a stream of messages following the protocol +defined in this page. + +If you are looking for the status line protocol instead, see https://i3wm.org/docs/i3bar-protocol.html. + +== The protocol + +Each message should be a newline-delimited JSON array. The array is in the same +format as the +GET_WORKSPACES+ ipc event, see +https://i3wm.org/docs/ipc.html#_workspaces_reply. + +As an example, this is the output of the +i3-msg -t get_workspaces+ command: +------------------------------ +[ + { + "id": 94131549984064, + "num": 1, + "name": "1", + "visible": false, + "focused": false, + "output": "HDMI-A-0", + "urgent": false + }, + { + "id": 94131550477584, + "num": 2, + "name": "2", + "visible": true, + "focused": true, + "output": "HDMI-A-0", + "urgent": false + }, + { + "id": 94131550452704, + "num": 3, + "name": "3:some workspace", + "visible": false, + "focused": false, + "output": "HDMI-A-0", + "urgent": false + } +] +------------------------------ + +Please note that this example was pretty printed for human consumption, with +the +"rect"+ field removed. Workspace button commands should output each array +in one line. + +Each element in the array represents a workspace. i3bar creates one workspace +button for each element in the array. The order of these buttons is the same as +the order of the elements in the array. + +In general, we recommend subscribing to the +workspace+ and +output+ +https://i3wm.org/docs/ipc.html#_workspace_event[events], +fetching the current workspace information with +GET_WORKSPACES+, modifying the +JSON array in the response according to your needs and then printing it to the +standard output. However, you are free to build a new message from the ground +up. + +=== Workspace objects in detail + +The documentation of +GET_WORKSPACES+ should be sufficient to understand the +meaning of each property but here we provide extra notes for each property and +its meaning with respect to i3bar. + +All properties but +name+ are optional. + +id (integer):: + If it is included it will be used to switch to that workspace when you + click the corresponding button. If it's not provided, the +name+ will be + used. You can use the +id+ field to present workspaces under a modified + name. +num (integer):: + The only use of a workspace's number is if the +strip_workspace_numbers+ + setting is enabled. +name (string):: + The only required property. If an +id+ is provided you can freely change + the +name+ as you wish, effectively renaming the buttons of i3bar. +visible (boolean):: + Defaults to +true+ if not included. +focused (boolean):: + Defaults to +false+ if not included. Generally, exactly one of the + workspaces should be +focused+. If not, no button will have the + +focused_workspace+ color. +urgent (boolean):: + Defaults to +false+ if not included. +rect (map):: + Not used by i3bar but will be ignored. +output (string):: + Defaults to the primary output if not included. + +== Examples + +These example scripts require the https://stedolan.github.io/jq/[jq] utility to +be installed but otherwise just use the standard +i3-msg+ utility included with +i3. However, you can write your own scripts in your preferred language, with +the help of one of the +https://i3wm.org/docs/ipc.html#_see_also_existing_libraries[pre-existing i3 +libraries] + +=== Base configuration + +------------------------------ +bar { + … + workspace_command /path/to/your/script.sh + … +} +------------------------------ + +=== Re-create the default behaviour of i3bar + +Not very useful by itself but this will be the basic building block of all the +following scripts. This one does not require +jq+. + +------------------------------ +#!/bin/sh +i3-msg -t subscribe -m '["workspace", "output"]' | { + # Initially print the current workspaces before we receive any events. This + # avoids having an empty bar when starting up. + i3-msg -t get_workspaces; + # Then, while we receive events, update the workspace information. + while read; do i3-msg -t get_workspaces; done; +} +------------------------------ + +=== Hide workspace named +foo+ unless if it is focused. + +------------------------------ +#!/bin/sh +i3-msg -t subscribe -m '["workspace", "output"]' | { + i3-msg -t get_workspaces; + while read; do i3-msg -t get_workspaces; done; +} | jq --unbuffered -c '[ .[] | select(.name != "foo" or .focused) ]' +------------------------------ + +Important! Make sure you use the +--unbuffered+ flag with +jq+, otherwise you +might not get the changes in real-time but whenever they are flushed, which +might mean that you are getting an empty bar until enough events are written. + +=== Show empty workspaces +foo+ and +bar+ on LVDS1 even if they do not exist at the moment. + +------------------------------ +#!/bin/sh +i3-msg -t subscribe -m '["workspace", "output"]' | { + i3-msg -t get_workspaces; + while read; do i3-msg -t get_workspaces; done; +} | jq --unbuffered -c ' + def fake_ws(name): { + name: name, + output: "LVDS1", + }; + . + [ fake_ws("foo"), fake_ws("bar") ] | unique_by(.name) +' +------------------------------ + +=== Sort workspaces in reverse alphanumeric order + +------------------------------ +#!/bin/sh +i3-msg -t subscribe -m '["workspace", "output"]' | { + i3-msg -t get_workspaces; + while read; do i3-msg -t get_workspaces; done; +} | jq --unbuffered -c 'sort_by(.name) | reverse' +------------------------------ + +=== Append "foo" to the name of each workspace + +------------------------------ +#!/bin/sh +i3-msg -t subscribe -m '["workspace", "output"]' | { + i3-msg -t get_workspaces; + while read; do i3-msg -t get_workspaces; done; +} | jq --unbuffered -c '[ .[] | .name |= . + " foo" ]' +------------------------------ diff --git a/docs/userguide b/docs/userguide index 7fe0b9af..1bc5c73b 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1611,6 +1611,30 @@ bar { } ------------------------------------------------- +[[workspace_command]] +=== Workspace buttons command + +Since i3 4.23, i3bar can run a program and use its +stdout+ output to define +the workspace buttons displayed on the left hand side of the bar. With this +feature, you can, for example, rename the buttons of workspaces, hide specific +workspaces, always show a workspace button even if the workspace does not exist +or change the order of the buttons. + +Also see <> for the statusline option and +https://i3wm.org/docs/i3bar-workspace-protocol.html for the detailed protocol. + +*Syntax*: +------------------------ +workspace_command +------------------------ + +*Example*: +------------------------------------------------- +bar { + workspace_command /path/to/script.sh +} +------------------------------------------------- + === Display mode You can either have i3bar be visible permanently at one edge of the screen diff --git a/i3bar/include/child.h b/i3bar/include/child.h index ae523bc0..e77b51e3 100644 --- a/i3bar/include/child.h +++ b/i3bar/include/child.h @@ -11,7 +11,7 @@ #include -#include +#include #define STDIN_CHUNK_SIZE 1024 @@ -40,6 +40,18 @@ typedef struct { */ bool click_events; bool click_events_init; + + /** + * stdin- and SIGCHLD-watchers + */ + ev_io *stdin_io; + ev_child *child_sig; + int stdin_fd; + + /** + * Line read from child that did not include a newline character. + */ + char *pending_line; } i3bar_child; /* @@ -50,36 +62,66 @@ void clear_statusline(struct statusline_head *head, bool free_resources); /* * Start a child process with the specified command and reroute stdin. - * We actually start a $SHELL to execute the command so we don't have to care - * about arguments and such + * We actually start a shell to execute the command so we don't have to care + * about arguments and such. + * + * If `command' is NULL, such as in the case when no `status_command' is given + * in the bar config, no child will be started. * */ void start_child(char *command); +/* + * Same as start_child but starts the configured client that manages workspace + * buttons. + * + */ +void start_ws_child(char *command); + +/* + * Returns true if the status child process is alive. + * + */ +bool status_child_is_alive(void); + +/* + * Returns true if the workspace child process is alive. + * + */ +bool ws_child_is_alive(void); + /* * kill()s the child process (if any). Called when exit()ing. * */ -void kill_child_at_exit(void); +void kill_children_at_exit(void); /* - * kill()s the child process (if any) and closes and - * free()s the stdin- and SIGCHLD-watchers + * kill()s the child process (if any) and closes and free()s the stdin- and + * SIGCHLD-watchers * */ void kill_child(void); +/* + * kill()s the workspace child process (if any) and closes and free()s the + * stdin- and SIGCHLD-watchers. + * Similar to kill_child. + * + */ +void kill_ws_child(void); + /* * Sends a SIGSTOP to the child process (if existent) * */ -void stop_child(void); +void stop_children(void); /* * Sends a SIGCONT to the child process (if existent) * */ -void cont_child(void); +void cont_children(void); /* * Whether or not the child want click events @@ -92,3 +134,14 @@ bool child_want_click_events(void); * */ void send_block_clicked(int button, const char *name, const char *instance, int x, int y, int x_rel, int y_rel, int out_x, int out_y, int width, int height, int mods); + +/* + * When workspace_command is enabled this function is used to re-parse the + * latest received JSON from the client. + */ +void repeat_last_ws_json(void); + +/* + * Replaces the workspace buttons with an error message. + */ +void set_workspace_button_error(const char *message); diff --git a/i3bar/include/configuration.h b/i3bar/include/configuration.h index 24079c5d..c9bae7c3 100644 --- a/i3bar/include/configuration.h +++ b/i3bar/include/configuration.h @@ -62,6 +62,7 @@ typedef struct config_t { bool strip_ws_name; char *bar_id; char *command; + char *workspace_command; char *fontname; i3String *separator_symbol; TAILQ_HEAD(tray_outputs_head, tray_output_t) tray_outputs; @@ -79,17 +80,17 @@ typedef struct config_t { extern config_t config; /** - * Start parsing the received bar configuration JSON string + * Parse the received bar configuration JSON string * */ -void parse_config_json(char *json); +void parse_config_json(const unsigned char *json, size_t size); /** - * Start parsing the received bar configuration list. The only usecase right - * now is to automatically get the first bar id. + * Parse the received bar configuration list. The only usecase right now is to + * automatically get the first bar id. * */ -void parse_get_first_i3bar_config(char *json); +void parse_get_first_i3bar_config(const unsigned char *json, size_t size); /** * free()s the color strings as soon as they are not needed anymore. diff --git a/i3bar/include/mode.h b/i3bar/include/mode.h index e8e4296d..4646b9f4 100644 --- a/i3bar/include/mode.h +++ b/i3bar/include/mode.h @@ -24,7 +24,7 @@ struct mode { typedef struct mode mode; /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_mode_json(char *json); +void parse_mode_json(const unsigned char *json, size_t size); diff --git a/i3bar/include/outputs.h b/i3bar/include/outputs.h index 4685e51e..560abe53 100644 --- a/i3bar/include/outputs.h +++ b/i3bar/include/outputs.h @@ -22,10 +22,10 @@ SLIST_HEAD(outputs_head, i3_output); extern struct outputs_head* outputs; /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_outputs_json(char* json); +void parse_outputs_json(const unsigned char* json, size_t size); /* * Initiate the outputs list diff --git a/i3bar/include/workspaces.h b/i3bar/include/workspaces.h index ff61450c..6c8e7145 100644 --- a/i3bar/include/workspaces.h +++ b/i3bar/include/workspaces.h @@ -18,10 +18,10 @@ typedef struct i3_ws i3_ws; TAILQ_HEAD(ws_head, i3_ws); /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_workspaces_json(char *json); +void parse_workspaces_json(const unsigned char *json, size_t size); /* * free() all workspace data structures @@ -38,7 +38,6 @@ struct i3_ws { bool visible; /* If the ws is currently visible on an output */ bool focused; /* If the ws is currently focused */ bool urgent; /* If the urgent hint of the ws is set */ - rect rect; /* The rect of the ws (not used (yet)) */ struct i3_output *output; /* The current output of the ws */ TAILQ_ENTRY(i3_ws) tailq; /* Pointer for the TAILQ-Macro */ diff --git a/i3bar/src/child.c b/i3bar/src/child.c index df4c6601..20858f68 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -10,6 +10,7 @@ #include "common.h" #include "yajl_utils.h" +#include /* isspace */ #include #include #include @@ -27,14 +28,30 @@ #include /* Global variables for child_*() */ -i3bar_child child = {0}; -#define DLOG_CHILD DLOG("%s: pid=%ld stopped=%d stop_signal=%d cont_signal=%d click_events=%d click_events_init=%d\n", \ - __func__, (long)child.pid, child.stopped, child.stop_signal, child.cont_signal, child.click_events, child.click_events_init) - -/* stdin- and SIGCHLD-watchers */ -ev_io *stdin_io; -int stdin_fd; -ev_child *child_sig; +i3bar_child status_child = {0}; +i3bar_child ws_child = {0}; + +#define DLOG_CHILD(c) \ + do { \ + if ((c).pid == 0) { \ + DLOG("%s: child pid = 0\n", __func__); \ + } else if ((c).pid == status_child.pid) { \ + DLOG("%s: status_command: pid=%ld stopped=%d stop_signal=%d cont_signal=%d click_events=%d click_events_init=%d\n", \ + __func__, (long)(c).pid, (c).stopped, (c).stop_signal, (c).cont_signal, (c).click_events, (c).click_events_init); \ + } else if ((c).pid == ws_child.pid) { \ + DLOG("%s: workspace_command: pid=%ld stopped=%d stop_signal=%d cont_signal=%d click_events=%d click_events_init=%d\n", \ + __func__, (long)(c).pid, (c).stopped, (c).stop_signal, (c).cont_signal, (c).click_events, (c).click_events_init); \ + } else { \ + ELOG("%s: unknown child, this should never happen " \ + "pid=%ld stopped=%d stop_signal=%d cont_signal=%d click_events=%d click_events_init=%d\n", \ + __func__, (long)(c).pid, (c).stopped, (c).stop_signal, (c).cont_signal, (c).click_events, (c).click_events_init); \ + } \ + } while (0) +#define DLOG_CHILDREN \ + do { \ + DLOG_CHILD(status_child); \ + DLOG_CHILD(ws_child); \ + } while (0) /* JSON parser for stdin */ yajl_handle parser; @@ -127,7 +144,7 @@ __attribute__((format(printf, 1, 2))) static void set_statusline_error(const cha TAILQ_INSERT_TAIL(&statusline_head, message_block, blocks); finish: - FREE(message); + free(message); va_end(args); } @@ -135,22 +152,27 @@ finish: * Stop and free() the stdin- and SIGCHLD-watchers * */ -static void cleanup(void) { - if (stdin_io != NULL) { - ev_io_stop(main_loop, stdin_io); - FREE(stdin_io); - close(stdin_fd); - stdin_fd = 0; - close(child_stdin); - child_stdin = 0; +static void cleanup(i3bar_child *c) { + DLOG_CHILD(*c); + + if (c->stdin_io != NULL) { + ev_io_stop(main_loop, c->stdin_io); + FREE(c->stdin_io); + + if (c->pid == status_child.pid) { + close(child_stdin); + child_stdin = 0; + } + close(c->stdin_fd); } - if (child_sig != NULL) { - ev_child_stop(main_loop, child_sig); - FREE(child_sig); + if (c->child_sig != NULL) { + ev_child_stop(main_loop, c->child_sig); + FREE(c->child_sig); } - memset(&child, 0, sizeof(i3bar_child)); + FREE(c->pending_line); + memset(c, 0, sizeof(i3bar_child)); } /* @@ -362,15 +384,13 @@ static int stdin_end_array(void *context) { * Returns NULL on EOF. * */ -static unsigned char *get_buffer(ev_io *watcher, int *ret_buffer_len) { - int fd = watcher->fd; - int n = 0; +static unsigned char *get_buffer(int fd, int *ret_buffer_len) { int rec = 0; int buffer_len = STDIN_CHUNK_SIZE; unsigned char *buffer = smalloc(buffer_len + 1); buffer[0] = '\0'; while (1) { - n = read(fd, buffer + rec, buffer_len - rec); + const ssize_t n = read(fd, buffer + rec, buffer_len - rec); if (n == -1) { if (errno == EAGAIN) { /* finish up */ @@ -390,10 +410,11 @@ static unsigned char *get_buffer(ev_io *watcher, int *ret_buffer_len) { if (rec == buffer_len) { buffer_len += STDIN_CHUNK_SIZE; - buffer = srealloc(buffer, buffer_len); + buffer = srealloc(buffer, buffer_len + 1); } } - if (*buffer == '\0') { + buffer[rec] = '\0'; + if (buffer[0] == '\0') { FREE(buffer); rec = -1; } @@ -443,13 +464,14 @@ static bool read_json_input(unsigned char *input, int length) { * in statusline * */ -static void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { +static void stdin_io_cb(int fd) { int rec; - unsigned char *buffer = get_buffer(watcher, &rec); - if (buffer == NULL) + unsigned char *buffer = get_buffer(fd, &rec); + if (buffer == NULL) { return; + } bool has_urgent = false; - if (child.version > 0) { + if (status_child.version > 0) { has_urgent = read_json_input(buffer, rec); } else { read_flat_input((char *)buffer, rec); @@ -463,22 +485,23 @@ static void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { * whether this is JSON or plain text * */ -static void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int revents) { +static void stdin_io_first_line_cb(int fd) { int rec; - unsigned char *buffer = get_buffer(watcher, &rec); - if (buffer == NULL) + unsigned char *buffer = get_buffer(fd, &rec); + if (buffer == NULL) { return; + } DLOG("Detecting input type based on buffer *%.*s*\n", rec, buffer); /* Detect whether this is JSON or plain text. */ unsigned int consumed = 0; /* At the moment, we don’t care for the version. This might change * in the future, but for now, we just discard it. */ - parse_json_header(&child, buffer, rec, &consumed); - if (child.version > 0) { - /* If hide-on-modifier is set, we start of by sending the - * child a SIGSTOP, because the bars aren't mapped at start */ + parse_json_header(&status_child, buffer, rec, &consumed); + if (status_child.version > 0) { + /* If hide-on-modifier is set, we start of by sending the status_child + * a SIGSTOP, because the bars aren't mapped at start */ if (config.hide_on_modifier) { - stop_child(); + stop_children(); } draw_bars(read_json_input(buffer + consumed, rec - consumed)); } else { @@ -489,9 +512,133 @@ static void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int rev read_flat_input((char *)buffer, rec); } free(buffer); - ev_io_stop(main_loop, stdin_io); - ev_io_init(stdin_io, &stdin_io_cb, stdin_fd, EV_READ); - ev_io_start(main_loop, stdin_io); +} + +static bool isempty(char *s) { + while (*s != '\0') { + if (!isspace(*s)) { + return false; + } + s++; + } + return true; +} + +static char *append_string(const char *previous, const char *str) { + if (previous != NULL) { + char *result; + sasprintf(&result, "%s%s", previous, str); + return result; + } + return sstrdup(str); +} + +static char *ws_last_json; + +static void ws_stdin_io_cb(int fd) { + int rec; + unsigned char *buffer = get_buffer(fd, &rec); + if (buffer == NULL) { + return; + } + + gchar **strings = g_strsplit((const char *)buffer, "\n", 0); + for (int idx = 0; strings[idx] != NULL; idx++) { + if (ws_child.pending_line == NULL && isempty(strings[idx])) { + /* In the normal case where the buffer ends with '\n', the last + * string should be empty */ + continue; + } + + if (strings[idx + 1] == NULL) { + /* This is the last string but it is not empty, meaning that we have + * read data that is incomplete, save it for later. */ + char *new = append_string(ws_child.pending_line, strings[idx]); + free(ws_child.pending_line); + ws_child.pending_line = new; + continue; + } + + free(ws_last_json); + ws_last_json = append_string(ws_child.pending_line, strings[idx]); + FREE(ws_child.pending_line); + + parse_workspaces_json((const unsigned char *)ws_last_json, strlen(ws_last_json)); + } + + g_strfreev(strings); + free(buffer); + + draw_bars(false); +} + +static void common_stdin_cb(struct ev_loop *loop, ev_io *watcher, int revents) { + if (watcher == status_child.stdin_io) { + if (status_child.version == (uint32_t)-1) { + stdin_io_first_line_cb(watcher->fd); + } else { + stdin_io_cb(watcher->fd); + } + } else if (watcher == ws_child.stdin_io) { + ws_stdin_io_cb(watcher->fd); + } else { + ELOG("Got callback for unknown watcher fd=%d\n", watcher->fd); + } +} + +/* + * When workspace_command is enabled this function is used to re-parse the + * latest received JSON from the client. + */ +void repeat_last_ws_json(void) { + if (ws_last_json) { + DLOG("Repeating last workspace JSON\n"); + parse_workspaces_json((const unsigned char *)ws_last_json, strlen(ws_last_json)); + } +} + +/* + * Wrapper around set_workspace_button_error to mimic the call of + * set_statusline_error. + */ +__attribute__((format(printf, 1, 2))) static void set_workspace_button_error_f(const char *format, ...) { + char *message; + va_list args; + va_start(args, format); + if (vasprintf(&message, format, args) == -1) { + goto finish; + } + + set_workspace_button_error(message); + +finish: + free(message); + va_end(args); +} + +/* + * Replaces the workspace buttons with an error message. + */ +void set_workspace_button_error(const char *message) { + free_workspaces(); + + char *name = NULL; + sasprintf(&name, "Error: %s", message); + + i3_output *output; + SLIST_FOREACH (output, outputs, slist) { + i3_ws *fake_ws = scalloc(1, sizeof(i3_ws)); + /* Don't set the canonical_name field to make this workspace unfocusable. */ + fake_ws->name = i3string_from_utf8(name); + fake_ws->name_width = predict_text_width(fake_ws->name); + fake_ws->num = -1; + fake_ws->urgent = fake_ws->visible = true; + fake_ws->output = output; + + TAILQ_INSERT_TAIL(output->workspaces, fake_ws, tailq); + } + + free(name); } /* @@ -501,27 +648,45 @@ static void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int rev * */ static void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) { - int exit_status = WEXITSTATUS(watcher->rstatus); + const int exit_status = WEXITSTATUS(watcher->rstatus); ELOG("Child (pid: %d) unexpectedly exited with status %d\n", - child.pid, + watcher->pid, exit_status); + void (*error_function_pointer)(const char *, ...) = NULL; + const char *command_type = ""; + i3bar_child *c = NULL; + if (watcher->pid == status_child.pid) { + command_type = "status_command"; + error_function_pointer = set_statusline_error; + c = &status_child; + } else if (watcher->pid == ws_child.pid) { + command_type = "workspace_command"; + error_function_pointer = set_workspace_button_error_f; + c = &ws_child; + } else { + ELOG("Unknown child pid, this should never happen\n"); + return; + } + DLOG_CHILD(*c); + /* this error is most likely caused by a user giving a nonexecutable or * nonexistent file, so we will handle those cases separately. */ - if (exit_status == 126) - set_statusline_error("status_command is not executable (exit %d)", exit_status); - else if (exit_status == 127) - set_statusline_error("status_command not found or is missing a library dependency (exit %d)", exit_status); - else - set_statusline_error("status_command process exited unexpectedly (exit %d)", exit_status); + if (exit_status == 126) { + error_function_pointer("%s is not executable (exit %d)", command_type, exit_status); + } else if (exit_status == 127) { + error_function_pointer("%s not found or is missing a library dependency (exit %d)", command_type, exit_status); + } else { + error_function_pointer("%s process exited unexpectedly (exit %d)", command_type, exit_status); + } - cleanup(); + cleanup(c); draw_bars(false); } static void child_write_output(void) { - if (child.click_events) { + if (status_child.click_events) { const unsigned char *output; size_t size; ssize_t n; @@ -535,7 +700,7 @@ static void child_write_output(void) { yajl_gen_clear(gen); if (n == -1) { - child.click_events = false; + status_child.click_events = false; kill_child(); set_statusline_error("child_write_output failed"); draw_bars(false); @@ -543,6 +708,41 @@ static void child_write_output(void) { } } +static pid_t sfork(void) { + const pid_t pid = fork(); + if (pid == -1) { + ELOG("Couldn't fork(): %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + return pid; +} + +static void spipe(int pipedes[2]) { + if (pipe(pipedes) == -1) { + err(EXIT_FAILURE, "pipe(pipe_in)"); + } +} + +static void exec_shell(char *command) { + execl(_PATH_BSHELL, _PATH_BSHELL, "-c", command, (char *)NULL); +} + +static void setup_child_cb(i3bar_child *child) { + /* We set O_NONBLOCK because blocking is evil in event-driven software */ + fcntl(child->stdin_fd, F_SETFL, O_NONBLOCK); + + child->stdin_io = smalloc(sizeof(ev_io)); + ev_io_init(child->stdin_io, &common_stdin_cb, child->stdin_fd, EV_READ); + ev_io_start(main_loop, child->stdin_io); + + /* We must cleanup, if the child unexpectedly terminates */ + child->child_sig = smalloc(sizeof(ev_child)); + ev_child_init(child->child_sig, &child_sig_cb, child->pid, 0); + ev_child_start(main_loop, child->child_sig); + + DLOG_CHILD(*child); +} + /* * Start a child process with the specified command and reroute stdin. * We actually start a shell to execute the command so we don't have to care @@ -553,8 +753,9 @@ static void child_write_output(void) { * */ void start_child(char *command) { - if (command == NULL) + if (command == NULL) { return; + } /* Allocate a yajl parser which will be used to parse stdin. */ static yajl_callbacks callbacks = { @@ -568,69 +769,77 @@ void start_child(char *command) { .yajl_end_array = stdin_end_array, }; parser = yajl_alloc(&callbacks, NULL, &parser_context); - gen = yajl_gen_alloc(NULL); int pipe_in[2]; /* pipe we read from */ int pipe_out[2]; /* pipe we write to */ + spipe(pipe_in); + spipe(pipe_out); - if (pipe(pipe_in) == -1) - err(EXIT_FAILURE, "pipe(pipe_in)"); - if (pipe(pipe_out) == -1) - err(EXIT_FAILURE, "pipe(pipe_out)"); - - child.pid = fork(); - switch (child.pid) { - case -1: - ELOG("Couldn't fork(): %s\n", strerror(errno)); - exit(EXIT_FAILURE); - case 0: - /* Child-process. Reroute streams and start shell */ - - close(pipe_in[0]); - close(pipe_out[1]); + status_child.pid = sfork(); + if (status_child.pid == 0) { + /* Child-process. Reroute streams and start shell */ + close(pipe_in[0]); + close(pipe_out[1]); - dup2(pipe_in[1], STDOUT_FILENO); - dup2(pipe_out[0], STDIN_FILENO); + dup2(pipe_in[1], STDOUT_FILENO); + dup2(pipe_out[0], STDIN_FILENO); - setpgid(child.pid, 0); - execl(_PATH_BSHELL, _PATH_BSHELL, "-c", command, (char *)NULL); - return; - default: - /* Parent-process. Reroute streams */ + setpgid(status_child.pid, 0); + exec_shell(command); + return; + } + /* Parent-process. Reroute streams */ + close(pipe_in[1]); + close(pipe_out[0]); - close(pipe_in[1]); - close(pipe_out[0]); + status_child.stdin_fd = pipe_in[0]; + child_stdin = pipe_out[1]; + status_child.version = -1; - stdin_fd = pipe_in[0]; - child_stdin = pipe_out[1]; + setup_child_cb(&status_child); +} - break; +/* + * Same as start_child but starts the configured client that manages workspace + * buttons. + * + */ +void start_ws_child(char *command) { + if (command == NULL) { + return; } - /* We set O_NONBLOCK because blocking is evil in event-driven software */ - fcntl(stdin_fd, F_SETFL, O_NONBLOCK); + ws_child.stop_signal = SIGSTOP; + ws_child.cont_signal = SIGCONT; - stdin_io = smalloc(sizeof(ev_io)); - ev_io_init(stdin_io, &stdin_io_first_line_cb, stdin_fd, EV_READ); - ev_io_start(main_loop, stdin_io); + int pipe_in[2]; /* pipe we read from */ + spipe(pipe_in); - /* We must cleanup, if the child unexpectedly terminates */ - child_sig = smalloc(sizeof(ev_child)); - ev_child_init(child_sig, &child_sig_cb, child.pid, 0); - ev_child_start(main_loop, child_sig); + ws_child.pid = sfork(); + if (ws_child.pid == 0) { + /* Child-process. Reroute streams and start shell */ + close(pipe_in[0]); + dup2(pipe_in[1], STDOUT_FILENO); - atexit(kill_child_at_exit); - DLOG_CHILD; + setpgid(ws_child.pid, 0); + exec_shell(command); + return; + } + /* Parent-process. Reroute streams */ + close(pipe_in[1]); + ws_child.stdin_fd = pipe_in[0]; + + setup_child_cb(&ws_child); } static void child_click_events_initialize(void) { - DLOG_CHILD; + DLOG_CHILD(status_child); - if (!child.click_events_init) { + if (!status_child.click_events_init) { yajl_gen_array_open(gen); child_write_output(); - child.click_events_init = true; + status_child.click_events_init = true; } } @@ -639,7 +848,7 @@ static void child_click_events_initialize(void) { * */ void send_block_clicked(int button, const char *name, const char *instance, int x, int y, int x_rel, int y_rel, int out_x, int out_y, int width, int height, int mods) { - if (!child.click_events) { + if (!status_child.click_events) { return; } @@ -706,35 +915,85 @@ void send_block_clicked(int button, const char *name, const char *instance, int child_write_output(); } +static bool is_alive(i3bar_child *c) { + return c->pid > 0; +} + +/* + * Returns true if the status child process is alive. + * + */ +bool status_child_is_alive(void) { + return is_alive(&status_child); +} + +/* + * Returns true if the workspace child process is alive. + * + */ +bool ws_child_is_alive(void) { + return is_alive(&ws_child); +} + /* * kill()s the child process (if any). Called when exit()ing. * */ -void kill_child_at_exit(void) { - DLOG_CHILD; +void kill_children_at_exit(void) { + DLOG_CHILDREN; + cont_children(); - if (child.pid > 0) { - if (child.cont_signal > 0 && child.stopped) - killpg(child.pid, child.cont_signal); - killpg(child.pid, SIGTERM); + if (is_alive(&status_child)) { + killpg(status_child.pid, SIGTERM); + } + if (is_alive(&ws_child)) { + killpg(ws_child.pid, SIGTERM); } } +static void cont_child(i3bar_child *c) { + if (is_alive(c) && c->cont_signal > 0 && c->stopped) { + c->stopped = false; + killpg(c->pid, c->cont_signal); + } +} + +static void kill_and_wait(i3bar_child *c) { + DLOG_CHILD(*c); + if (!is_alive(c)) { + return; + } + + cont_child(c); + killpg(c->pid, SIGTERM); + int status; + waitpid(c->pid, &status, 0); + cleanup(c); +} + /* - * kill()s the child process (if existent) and closes and - * free()s the stdin- and SIGCHLD-watchers + * kill()s the child process (if any) and closes and free()s the stdin- and + * SIGCHLD-watchers * */ void kill_child(void) { - DLOG_CHILD; + kill_and_wait(&status_child); +} - if (child.pid > 0) { - if (child.cont_signal > 0 && child.stopped) - killpg(child.pid, child.cont_signal); - killpg(child.pid, SIGTERM); - int status; - waitpid(child.pid, &status, 0); - cleanup(); +/* + * kill()s the workspace child process (if any) and closes and free()s the + * stdin- and SIGCHLD-watchers. + * Similar to kill_child. + * + */ +void kill_ws_child(void) { + kill_and_wait(&ws_child); +} + +static void stop_child(i3bar_child *c) { + if (c->stop_signal > 0 && !c->stopped) { + c->stopped = true; + killpg(c->pid, c->stop_signal); } } @@ -742,26 +1001,21 @@ void kill_child(void) { * Sends a SIGSTOP to the child process (if existent) * */ -void stop_child(void) { - DLOG_CHILD; - - if (child.stop_signal > 0 && !child.stopped) { - child.stopped = true; - killpg(child.pid, child.stop_signal); - } +void stop_children(void) { + DLOG_CHILDREN; + stop_child(&status_child); + stop_child(&ws_child); } /* * Sends a SIGCONT to the child process (if existent) * */ -void cont_child(void) { - DLOG_CHILD; +void cont_children(void) { + DLOG_CHILDREN; - if (child.cont_signal > 0 && child.stopped) { - child.stopped = false; - killpg(child.pid, child.cont_signal); - } + cont_child(&status_child); + cont_child(&ws_child); } /* @@ -769,5 +1023,5 @@ void cont_child(void) { * */ bool child_want_click_events(void) { - return child.click_events; + return status_child.click_events; } diff --git a/i3bar/src/config.c b/i3bar/src/config.c index ccea937d..cebd5d5d 100644 --- a/i3bar/src/config.c +++ b/i3bar/src/config.c @@ -188,11 +188,17 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len } if (!strcmp(cur_key, "status_command")) { - DLOG("command = %.*s\n", len, val); + DLOG("status_command = %.*s\n", len, val); sasprintf(&config.command, "%.*s", len, val); return 1; } + if (!strcmp(cur_key, "workspace_command")) { + DLOG("workspace_command = %.*s\n", len, val); + sasprintf(&config.workspace_command, "%.*s", len, val); + return 1; + } + if (!strcmp(cur_key, "font")) { DLOG("font = %.*s\n", len, val); FREE(config.fontname); @@ -396,16 +402,15 @@ static yajl_callbacks outputs_callbacks = { }; /* - * Start parsing the received bar configuration JSON string + * Parse the received bar configuration JSON string * */ -void parse_config_json(char *json) { - yajl_handle handle = yajl_alloc(&outputs_callbacks, NULL, NULL); - +void parse_config_json(const unsigned char *json, size_t size) { TAILQ_INIT(&(config.bindings)); TAILQ_INIT(&(config.tray_outputs)); - yajl_status state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); + yajl_handle handle = yajl_alloc(&outputs_callbacks, NULL, NULL); + yajl_status state = yajl_parse(handle, json, size); /* FIXME: Proper error handling for JSON parsing */ switch (state) { @@ -418,6 +423,11 @@ void parse_config_json(char *json) { break; } + if (config.disable_ws && config.workspace_command) { + ELOG("You have specified 'workspace_buttons no'. Your 'workspace_command %s' will be ignored.\n", config.workspace_command); + FREE(config.workspace_command); + } + yajl_free(handle); } @@ -427,16 +437,16 @@ static int i3bar_config_string_cb(void *params_, const unsigned char *val, size_ } /* - * Start parsing the received bar configuration list. The only usecase right - * now is to automatically get the first bar id. + * Parse the received bar configuration list. The only usecase right now is to + * automatically get the first bar id. * */ -void parse_get_first_i3bar_config(char *json) { +void parse_get_first_i3bar_config(const unsigned char *json, size_t size) { yajl_callbacks configs_callbacks = { .yajl_string = i3bar_config_string_cb, }; yajl_handle handle = yajl_alloc(&configs_callbacks, NULL, NULL); - yajl_parse(handle, (const unsigned char *)json, strlen(json)); + yajl_parse(handle, json, size); yajl_free(handle); } diff --git a/i3bar/src/ipc.c b/i3bar/src/ipc.c index 3ab4738c..95130209 100644 --- a/i3bar/src/ipc.c +++ b/i3bar/src/ipc.c @@ -24,14 +24,24 @@ ev_io *i3_connection; const char *sock_path; -typedef void (*handler_t)(char *); +typedef void (*handler_t)(const unsigned char *, size_t); + +/* + * Returns true when i3bar is configured to read workspace information from i3 + * via JSON over the i3 IPC interface, as opposed to reading workspace + * information from the workspace_command via JSON over stdout. + * + */ +static bool i3_provides_workspaces(void) { + return !config.disable_ws && config.workspace_command == NULL; +} /* * Called, when we get a reply to a command from i3. * Since i3 does not give us much feedback on commands, we do not much * */ -static void got_command_reply(char *reply) { +static void got_command_reply(const unsigned char *reply, size_t size) { /* TODO: Error handling for command replies */ } @@ -39,9 +49,9 @@ static void got_command_reply(char *reply) { * Called, when we get a reply with workspaces data * */ -static void got_workspace_reply(char *reply) { +static void got_workspace_reply(const unsigned char *reply, size_t size) { DLOG("Got workspace data!\n"); - parse_workspaces_json(reply); + parse_workspaces_json(reply, size); draw_bars(false); } @@ -50,7 +60,7 @@ static void got_workspace_reply(char *reply) { * Since i3 does not give us much feedback on commands, we do not much * */ -static void got_subscribe_reply(char *reply) { +static void got_subscribe_reply(const unsigned char *reply, size_t size) { DLOG("Got subscribe reply: %s\n", reply); /* TODO: Error handling for subscribe commands */ } @@ -59,12 +69,12 @@ static void got_subscribe_reply(char *reply) { * Called, when we get a reply with outputs data * */ -static void got_output_reply(char *reply) { +static void got_output_reply(const unsigned char *reply, size_t size) { DLOG("Clearing old output configuration...\n"); free_outputs(); DLOG("Parsing outputs JSON...\n"); - parse_outputs_json(reply); + parse_outputs_json(reply, size); DLOG("Reconfiguring windows...\n"); reconfig_windows(false); @@ -73,8 +83,19 @@ static void got_output_reply(char *reply) { kick_tray_clients(o_walk); } - if (!config.disable_ws) { + if (i3_provides_workspaces()) { i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); + } else if (config.workspace_command) { + /* Communication with the workspace child is one-way. Since we called + * free_outputs() and free_workspaces() we have lost our workspace + * information which will result in no workspace buttons. A + * well-behaving client should be subscribed to output events as well + * and re-send the output information to i3bar. Even in that case + * though there is a race condition where the child can send the new + * workspace information after the output change before i3bar receives + * the output event from i3. For this reason, we re-parse the latest + * received JSON. */ + repeat_last_ws_json(); } draw_bars(false); @@ -84,10 +105,10 @@ static void got_output_reply(char *reply) { * Called when we get the configuration for our bar instance * */ -static void got_bar_config(char *reply) { +static void got_bar_config(const unsigned char *reply, size_t size) { if (!config.bar_id) { DLOG("Received bar list \"%s\"\n", reply); - parse_get_first_i3bar_config(reply); + parse_get_first_i3bar_config(reply, size); if (!config.bar_id) { ELOG("No bar configuration found, please configure a bar block in your i3 config file.\n"); @@ -106,13 +127,14 @@ static void got_bar_config(char *reply) { i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_OUTPUTS, NULL); free_colors(&(config.colors)); - parse_config_json(reply); + parse_config_json(reply, size); /* Now we can actually use 'config', so let's subscribe to the appropriate * events and request the workspaces if necessary. */ subscribe_events(); - if (!config.disable_ws) + if (i3_provides_workspaces()) { i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); + } /* Initialize the rest of XCB */ init_xcb_late(config.fontname); @@ -121,6 +143,7 @@ static void got_bar_config(char *reply) { init_colors(&(config.colors)); start_child(config.command); + start_ws_child(config.workspace_command); } /* Data structure to easily call the reply handlers later */ @@ -143,7 +166,7 @@ handler_t reply_handlers[] = { * Called, when a workspace event arrives (i.e. the user changed the workspace) * */ -static void got_workspace_event(char *event) { +static void got_workspace_event(const unsigned char *event, size_t size) { DLOG("Got workspace event!\n"); i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); } @@ -152,7 +175,7 @@ static void got_workspace_event(char *event) { * Called, when an output event arrives (i.e. the screen configuration changed) * */ -static void got_output_event(char *event) { +static void got_output_event(const unsigned char *event, size_t size) { DLOG("Got output event!\n"); i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_OUTPUTS, NULL); } @@ -161,9 +184,9 @@ static void got_output_event(char *event) { * Called, when a mode event arrives (i3 changed binding mode). * */ -static void got_mode_event(char *event) { +static void got_mode_event(const unsigned char *event, size_t size) { DLOG("Got mode event!\n"); - parse_mode_json(event); + parse_mode_json(event, size); draw_bars(false); } @@ -183,11 +206,11 @@ static bool strings_differ(char *a, char *b) { * Called, when a barconfig_update event arrives (i.e. i3 changed the bar hidden_state or mode) * */ -static void got_bar_config_update(char *event) { +static void got_bar_config_update(const unsigned char *event, size_t size) { /* check whether this affect this bar instance by checking the bar_id */ char *expected_id; sasprintf(&expected_id, "\"id\":\"%s\"", config.bar_id); - char *found_id = strstr(event, expected_id); + char *found_id = strstr((const char *)event, expected_id); FREE(expected_id); if (found_id == NULL) return; @@ -201,10 +224,12 @@ static void got_bar_config_update(char *event) { DLOG("Received bar config update \"%s\"\n", event); char *old_command = config.command; + char *old_workspace_command = config.workspace_command; config.command = NULL; + config.workspace_command = NULL; bar_display_mode_t old_mode = config.hide_on_modifier; - parse_config_json(event); + parse_config_json(event, size); if (old_mode != config.hide_on_modifier) { reconfig_windows(true); } @@ -214,13 +239,21 @@ static void got_bar_config_update(char *event) { init_colors(&(config.colors)); /* restart status command process */ - if (strings_differ(old_command, config.command)) { + if (!status_child_is_alive() || strings_differ(old_command, config.command)) { kill_child(); clear_statusline(&statusline_head, true); start_child(config.command); } free(old_command); + /* restart workspace command process */ + if (!ws_child_is_alive() || strings_differ(old_workspace_command, config.workspace_command)) { + free_workspaces(); + kill_ws_child(); + start_ws_child(config.workspace_command); + } + free(old_workspace_command); + draw_bars(false); } @@ -284,7 +317,7 @@ static void got_data(struct ev_loop *loop, ev_io *watcher, int events) { /* Now that we know, what to expect, we can start read()ing the rest * of the message */ - char *buffer = smalloc(size + 1); + unsigned char *buffer = smalloc(size + 1); rec = 0; while (rec < size) { @@ -304,10 +337,11 @@ static void got_data(struct ev_loop *loop, ev_io *watcher, int events) { /* And call the callback (indexed by the type) */ if (type & (1UL << 31)) { type ^= 1UL << 31; - event_handlers[type](buffer); + event_handlers[type](buffer, size); } else { - if (reply_handlers[type]) - reply_handlers[type](buffer); + if (reply_handlers[type]) { + reply_handlers[type](buffer, size); + } } FREE(header); @@ -377,9 +411,9 @@ void destroy_connection(void) { * */ void subscribe_events(void) { - if (config.disable_ws) { - i3_send_msg(I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[ \"output\", \"mode\", \"barconfig_update\" ]"); - } else { + if (i3_provides_workspaces()) { i3_send_msg(I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[ \"workspace\", \"output\", \"mode\", \"barconfig_update\" ]"); + } else { + i3_send_msg(I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[ \"output\", \"mode\", \"barconfig_update\" ]"); } } diff --git a/i3bar/src/main.c b/i3bar/src/main.c index 4e93bb02..ce5257bf 100644 --- a/i3bar/src/main.c +++ b/i3bar/src/main.c @@ -185,12 +185,12 @@ int main(int argc, char **argv) { ev_signal_start(main_loop, sig_int); ev_signal_start(main_loop, sig_hup); + atexit(kill_children_at_exit); + /* From here on everything should run smooth for itself, just start listening for * events. We stop simply stop the event loop, when we are finished */ ev_loop(main_loop, 0); - kill_child(); - clean_xcb(); ev_default_destroy(); diff --git a/i3bar/src/mode.c b/i3bar/src/mode.c index 13d02110..aea43ab4 100644 --- a/i3bar/src/mode.c +++ b/i3bar/src/mode.c @@ -16,7 +16,6 @@ /* A datatype to pass through the callbacks to save the state */ struct mode_json_params { - char *json; char *cur_key; char *name; bool pango_markup; @@ -96,26 +95,17 @@ static yajl_callbacks mode_callbacks = { }; /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_mode_json(char *json) { - /* FIXME: Fasciliate stream processing, i.e. allow starting to interpret - * JSON in chunks */ +void parse_mode_json(const unsigned char *json, size_t size) { struct mode_json_params params; - mode binding; - params.cur_key = NULL; - params.json = json; params.mode = &binding; - yajl_handle handle; - yajl_status state; - - handle = yajl_alloc(&mode_callbacks, NULL, (void *)¶ms); - - state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); + yajl_handle handle = yajl_alloc(&mode_callbacks, NULL, (void *)¶ms); + yajl_status state = yajl_parse(handle, json, size); /* FIXME: Proper error handling for JSON parsing */ switch (state) { diff --git a/i3bar/src/outputs.c b/i3bar/src/outputs.c index 168f3eef..5aca53cd 100644 --- a/i3bar/src/outputs.c +++ b/i3bar/src/outputs.c @@ -18,10 +18,8 @@ /* A datatype to pass through the callbacks to save the state */ struct outputs_json_params { - struct outputs_head *outputs; i3_output *outputs_walk; char *cur_key; - char *json; bool in_rect; }; @@ -263,21 +261,17 @@ void init_outputs(void) { } /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_outputs_json(char *json) { +void parse_outputs_json(const unsigned char *json, size_t size) { struct outputs_json_params params; params.outputs_walk = NULL; params.cur_key = NULL; - params.json = json; params.in_rect = false; - yajl_handle handle; - yajl_status state; - handle = yajl_alloc(&outputs_callbacks, NULL, (void *)¶ms); - - state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); + yajl_handle handle = yajl_alloc(&outputs_callbacks, NULL, (void *)¶ms); + yajl_status state = yajl_parse(handle, json, size); /* FIXME: Proper errorhandling for JSON-parsing */ switch (state) { @@ -291,6 +285,7 @@ void parse_outputs_json(char *json) { } yajl_free(handle); + free(params.cur_key); } /* @@ -319,12 +314,14 @@ void free_outputs(void) { * */ i3_output *get_output_by_name(char *name) { - i3_output *walk; if (name == NULL) { return NULL; } + const bool is_primary = !strcasecmp(name, "primary"); + + i3_output *walk; SLIST_FOREACH (walk, outputs, slist) { - if (!strcmp(walk->name, name)) { + if ((is_primary && walk->primary) || !strcmp(walk->name, name)) { break; } } diff --git a/i3bar/src/workspaces.c b/i3bar/src/workspaces.c index bd56f5d0..10c9fcf0 100644 --- a/i3bar/src/workspaces.c +++ b/i3bar/src/workspaces.c @@ -19,7 +19,8 @@ struct workspaces_json_params { struct ws_head *workspaces; i3_ws *workspaces_walk; char *cur_key; - char *json; + bool need_output; + bool parsing_rect; }; /* @@ -71,26 +72,23 @@ static int workspaces_integer_cb(void *params_, long long val) { return 1; } + /* rect is unused, so we don't bother to save it */ if (!strcmp(params->cur_key, "x")) { - params->workspaces_walk->rect.x = (int)val; FREE(params->cur_key); return 1; } if (!strcmp(params->cur_key, "y")) { - params->workspaces_walk->rect.y = (int)val; FREE(params->cur_key); return 1; } if (!strcmp(params->cur_key, "width")) { - params->workspaces_walk->rect.w = (int)val; FREE(params->cur_key); return 1; } if (!strcmp(params->cur_key, "height")) { - params->workspaces_walk->rect.h = (int)val; FREE(params->cur_key); return 1; } @@ -156,15 +154,16 @@ static int workspaces_string_cb(void *params_, const unsigned char *val, size_t sasprintf(&output_name, "%.*s", len, val); i3_output *target = get_output_by_name(output_name); + i3_ws *ws = params->workspaces_walk; if (target != NULL) { - params->workspaces_walk->output = target; - - TAILQ_INSERT_TAIL(params->workspaces_walk->output->workspaces, - params->workspaces_walk, - tailq); + ws->output = target; + TAILQ_INSERT_TAIL(ws->output->workspaces, ws, tailq); } + params->need_output = false; FREE(output_name); + FREE(params->cur_key); + return 1; } @@ -172,28 +171,42 @@ static int workspaces_string_cb(void *params_, const unsigned char *val, size_t } /* - * We hit the start of a JSON map (rect or a new output) + * We hit the start of a JSON map (rect or a new workspace) * */ static int workspaces_start_map_cb(void *params_) { struct workspaces_json_params *params = (struct workspaces_json_params *)params_; - i3_ws *new_workspace = NULL; - if (params->cur_key == NULL) { - new_workspace = smalloc(sizeof(i3_ws)); + i3_ws *new_workspace = scalloc(1, sizeof(i3_ws)); new_workspace->num = -1; - new_workspace->name = NULL; - new_workspace->visible = 0; - new_workspace->focused = 0; - new_workspace->urgent = 0; - memset(&new_workspace->rect, 0, sizeof(rect)); - new_workspace->output = NULL; params->workspaces_walk = new_workspace; + params->need_output = true; + params->parsing_rect = false; + } else { + params->parsing_rect = true; + } + + return 1; +} + +static int workspaces_end_map_cb(void *params_) { + struct workspaces_json_params *params = (struct workspaces_json_params *)params_; + i3_ws *ws = params->workspaces_walk; + const bool parsing_rect = params->parsing_rect; + params->parsing_rect = false; + + if (parsing_rect || !ws || !ws->name || !params->need_output) { return 1; } + ws->output = get_output_by_name("primary"); + if (ws->output == NULL) { + ws->output = SLIST_FIRST(outputs); + } + TAILQ_INSERT_TAIL(ws->output->workspaces, ws, tailq); + return 1; } @@ -216,43 +229,42 @@ static yajl_callbacks workspaces_callbacks = { .yajl_integer = workspaces_integer_cb, .yajl_string = workspaces_string_cb, .yajl_start_map = workspaces_start_map_cb, + .yajl_end_map = workspaces_end_map_cb, .yajl_map_key = workspaces_map_key_cb, }; /* - * Start parsing the received JSON string + * Parse the received JSON string * */ -void parse_workspaces_json(char *json) { - /* FIXME: Fasciliate stream processing, i.e. allow starting to interpret - * JSON in chunks */ - struct workspaces_json_params params; - +void parse_workspaces_json(const unsigned char *json, size_t size) { free_workspaces(); - params.workspaces_walk = NULL; - params.cur_key = NULL; - params.json = json; - - yajl_handle handle; - yajl_status state; - handle = yajl_alloc(&workspaces_callbacks, NULL, (void *)¶ms); - - state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); + struct workspaces_json_params params = {0}; + yajl_handle handle = yajl_alloc(&workspaces_callbacks, NULL, (void *)¶ms); + yajl_status state = yajl_parse(handle, json, size); /* FIXME: Proper error handling for JSON parsing */ switch (state) { case yajl_status_ok: break; case yajl_status_client_canceled: - case yajl_status_error: - ELOG("Could not parse workspaces reply!\n"); - exit(EXIT_FAILURE); + case yajl_status_error: { + unsigned char *err = yajl_get_error(handle, 1, json, size); + ELOG("Could not parse workspaces reply, error:\n%s\njson:---%s---\n", err, json); + yajl_free_error(handle, err); + + if (config.workspace_command) { + kill_ws_child(); + set_workspace_button_error("Could not parse workspace_command's JSON"); + } else { + exit(EXIT_FAILURE); + } break; + } } yajl_free(handle); - FREE(params.cur_key); } @@ -261,14 +273,14 @@ void parse_workspaces_json(char *json) { * */ void free_workspaces(void) { - i3_output *outputs_walk; if (outputs == NULL) { return; } - i3_ws *ws_walk; + i3_output *outputs_walk; SLIST_FOREACH (outputs_walk, outputs, slist) { if (outputs_walk->workspaces != NULL && !TAILQ_EMPTY(outputs_walk->workspaces)) { + i3_ws *ws_walk; TAILQ_FOREACH (ws_walk, outputs_walk->workspaces, tailq) { I3STRING_FREE(ws_walk->name); FREE(ws_walk->canonical_name); diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 0cda125c..4ff44d27 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -334,7 +334,7 @@ static void hide_bars(void) { } xcb_unmap_window(xcb_connection, walk->bar.id); } - stop_child(); + stop_children(); } /* @@ -351,7 +351,7 @@ static void unhide_bars(void) { uint32_t mask; uint32_t values[5]; - cont_child(); + cont_children(); SLIST_FOREACH (walk, outputs, slist) { if (walk->bar.id == XCB_NONE) { @@ -500,6 +500,49 @@ static int predict_button_width(int name_width) { logical_px(config.ws_min_width)); } +static char *quote_workspace_name(const char *in) { + /* To properly handle workspace names with double quotes in them, we need + * to escape the double quotes. We allocate a large enough buffer (twice + * the unescaped size is always enough), then we copy character by + * character. */ + const size_t namelen = strlen(in); + const size_t len = namelen + strlen("workspace \"\"") + 1; + char *out = scalloc(2 * len, 1); + memcpy(out, "workspace \"", strlen("workspace \"")); + size_t inpos, outpos; + for (inpos = 0, outpos = strlen("workspace \""); + inpos < namelen; + inpos++, outpos++) { + if (in[inpos] == '"' || in[inpos] == '\\') { + out[outpos] = '\\'; + outpos++; + } + out[outpos] = in[inpos]; + } + out[outpos] = '"'; + return out; +} + +static void focus_workspace(i3_ws *ws) { + char *buffer = NULL; + if (ws->id != 0) { + /* Workspace ID has higher precedence since the workspace_command is + * allowed to change workspace names as long as it provides a valid ID. */ + sasprintf(&buffer, "[con_id=%lld] focus workspace", ws->id); + goto done; + } + + if (ws->canonical_name == NULL) { + return; + } + + buffer = quote_workspace_name(ws->canonical_name); + +done: + i3_send_msg(I3_IPC_MESSAGE_TYPE_RUN_COMMAND, buffer); + free(buffer); +} + /* * Handle a button press event (i.e. a mouse click on one of our bars). * We determine, whether the click occurred on a workspace button or if the scroll- @@ -620,37 +663,7 @@ static void handle_button(xcb_button_press_event_t *event) { return; } - /* To properly handle workspace names with double quotes in them, we need - * to escape the double quotes. Unfortunately, that’s rather ugly in C: We - * first count the number of double quotes, then we allocate a large enough - * buffer, then we copy character by character. */ - int num_quotes = 0; - size_t namelen = 0; - const char *utf8_name = cur_ws->canonical_name; - for (const char *walk = utf8_name; *walk != '\0'; walk++) { - if (*walk == '"' || *walk == '\\') - num_quotes++; - /* While we’re looping through the name anyway, we can save one - * strlen(). */ - namelen++; - } - - const size_t len = namelen + strlen("workspace \"\"") + 1; - char *buffer = scalloc(len + num_quotes, 1); - memcpy(buffer, "workspace \"", strlen("workspace \"")); - size_t inpos, outpos; - for (inpos = 0, outpos = strlen("workspace \""); - inpos < namelen; - inpos++, outpos++) { - if (utf8_name[inpos] == '"' || utf8_name[inpos] == '\\') { - buffer[outpos] = '\\'; - outpos++; - } - buffer[outpos] = utf8_name[inpos]; - } - buffer[outpos] = '"'; - i3_send_msg(I3_IPC_MESSAGE_TYPE_RUN_COMMAND, buffer); - free(buffer); + focus_workspace(cur_ws); } /* @@ -674,9 +687,9 @@ static void handle_visibility_notify(xcb_visibility_notify_event_t *event) { } if (num_visible == 0) { - stop_child(); + stop_children(); } else { - cont_child(); + cont_children(); } } @@ -1945,10 +1958,10 @@ void reconfig_windows(bool redraw_bars) { /* Unmap the window, and draw it again when in dock mode */ umap_cookie = xcb_unmap_window_checked(xcb_connection, walk->bar.id); if (config.hide_on_modifier == M_DOCK) { - cont_child(); + cont_children(); map_cookie = xcb_map_window_checked(xcb_connection, walk->bar.id); } else { - stop_child(); + stop_children(); } if (config.hide_on_modifier == M_HIDE) { diff --git a/include/config_directives.h b/include/config_directives.h index 600226e9..f910d591 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -105,6 +105,7 @@ CFGFUN(bar_tray_output, const char *output); CFGFUN(bar_tray_padding, const long spacing_px); CFGFUN(bar_color_single, const char *colorclass, const char *color); CFGFUN(bar_status_command, const char *command); +CFGFUN(bar_workspace_command, const char *command); CFGFUN(bar_binding_mode_indicator, const char *value); CFGFUN(bar_workspace_buttons, const char *value); CFGFUN(bar_workspace_min_width, const long width); diff --git a/include/configuration.h b/include/configuration.h index 99f4b64e..19d2f714 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -335,6 +335,10 @@ struct Barconfig { * Will be passed to the shell. */ char *status_command; + /** Command that should be run to get the workspace buttons. Will be passed + * to the shell. */ + char *workspace_command; + /** Font specification for all text rendered on the bar. */ char *font; diff --git a/man/i3bar.man b/man/i3bar.man index 479e10fc..761748f3 100644 --- a/man/i3bar.man +++ b/man/i3bar.man @@ -46,9 +46,12 @@ Be verbose. workspace switching buttons and a statusline generated by i3status(1) or similar. It is automatically invoked (and configured through) i3. -i3bar supports colors via a JSON protocol starting from v4.2, see +i3bar supports using a JSON protocol for setting the status line, see https://i3wm.org/docs/i3bar-protocol.html +Since i3 4.23, i3bar supports another JSON protocol for setting workspace +buttons. See https://i3wm.org/docs/i3bar-workspace-protocol.html. + == ENVIRONMENT === I3SOCK diff --git a/meson.build b/meson.build index 25080cea..30beb055 100644 --- a/meson.build +++ b/meson.build @@ -86,6 +86,7 @@ if get_option('docs') 'docs/wsbar', 'docs/testsuite', 'docs/i3bar-protocol', + 'docs/i3bar-workspace-protocol', 'docs/layout-saving', ] foreach m : doc_toc_inputs @@ -135,6 +136,7 @@ else 'docs/wsbar.html', 'docs/testsuite.html', 'docs/i3bar-protocol.html', + 'docs/i3bar-workspace-protocol.html', 'docs/layout-saving.html', 'docs/debugging.html', ], diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 52bd3212..33708b52 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -528,6 +528,7 @@ state BAR: 'set' -> BAR_IGNORE_LINE 'i3bar_command' -> BAR_BAR_COMMAND 'status_command' -> BAR_STATUS_COMMAND + 'workspace_command' -> BAR_WORKSPACE_COMMAND 'socket_path' -> BAR_SOCKET_PATH 'mode' -> BAR_MODE 'hidden_state' -> BAR_HIDDEN_STATE @@ -567,6 +568,10 @@ state BAR_STATUS_COMMAND: command = string -> call cfg_bar_status_command($command); BAR +state BAR_WORKSPACE_COMMAND: + command = string + -> call cfg_bar_workspace_command($command); BAR + state BAR_SOCKET_PATH: path = string -> call cfg_bar_socket_path($path); BAR diff --git a/release-notes/changes/1-workspace_command b/release-notes/changes/1-workspace_command new file mode 100644 index 00000000..7a6bba7b --- /dev/null +++ b/release-notes/changes/1-workspace_command @@ -0,0 +1 @@ +add workspace_command option in i3bar diff --git a/src/config.c b/src/config.c index f06a3f8d..bf3ec6dc 100644 --- a/src/config.c +++ b/src/config.c @@ -105,6 +105,7 @@ static void free_configuration(void) { FREE(barconfig->outputs); FREE(barconfig->socket_path); FREE(barconfig->status_command); + FREE(barconfig->workspace_command); FREE(barconfig->i3bar_command); FREE(barconfig->font); FREE(barconfig->colors.background); diff --git a/src/config_directives.c b/src/config_directives.c index 9077fe98..81adf351 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -873,6 +873,11 @@ CFGFUN(bar_status_command, const char *command) { current_bar->status_command = sstrdup(command); } +CFGFUN(bar_workspace_command, const char *command) { + FREE(current_bar->workspace_command); + current_bar->workspace_command = sstrdup(command); +} + CFGFUN(bar_binding_mode_indicator, const char *value) { current_bar->hide_binding_mode_indicator = !boolstr(value); } diff --git a/src/ipc.c b/src/ipc.c index 28a86092..d20090c6 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -827,6 +827,7 @@ static void dump_bar_config(yajl_gen gen, Barconfig *config) { ystr("top"); YSTR_IF_SET(status_command); + YSTR_IF_SET(workspace_command); YSTR_IF_SET(font); if (config->bar_height) { diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index af50d81d..90fc8115 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -776,7 +776,7 @@ EOT $expected = <<'EOT'; cfg_bar_start() cfg_bar_output(LVDS-1) -ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'i3bar_command', 'status_command', 'socket_path', 'mode', 'hidden_state', 'id', 'modifier', 'wheel_up_cmd', 'wheel_down_cmd', 'bindsym', 'position', 'output', 'tray_output', 'tray_padding', 'font', 'separator_symbol', 'binding_mode_indicator', 'workspace_buttons', 'workspace_min_width', 'strip_workspace_numbers', 'strip_workspace_name', 'verbose', 'height', 'padding', 'colors', '}' +ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'i3bar_command', 'status_command', 'workspace_command', 'socket_path', 'mode', 'hidden_state', 'id', 'modifier', 'wheel_up_cmd', 'wheel_down_cmd', 'bindsym', 'position', 'output', 'tray_output', 'tray_padding', 'font', 'separator_symbol', 'binding_mode_indicator', 'workspace_buttons', 'workspace_min_width', 'strip_workspace_numbers', 'strip_workspace_name', 'verbose', 'height', 'padding', 'colors', '}' ERROR: CONFIG: (in file ) ERROR: CONFIG: Line 1: bar { ERROR: CONFIG: Line 2: output LVDS-1 -- cgit v1.2.3-54-g00ecf