aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOrestis Floros <orestisflo@gmail.com>2023-01-22 18:34:14 +0100
committerOrestis Floros <orestisflo@gmail.com>2023-01-22 18:34:14 +0100
commitff30991b16c9b62cf5f04353f419b09912b83ede (patch)
tree4928f60370ebd34bc942a7e00867e37422ba8d1e
parentc52f13900df5da04d86809abbfe25178a4bbe305 (diff)
downloadi3bar-ws-protocol.tar.gz
i3bar-ws-protocol.zip
i3bar: Add protocol for workspace buttonsi3bar-ws-protocol
Closes #3818 (parent issue) Fixes #1808 Fixes #2333 Fixes #2617 Fixes #3548
-rw-r--r--docs/i3bar-workspace-protocol184
-rw-r--r--docs/userguide24
-rw-r--r--i3bar/include/child.h69
-rw-r--r--i3bar/include/configuration.h11
-rw-r--r--i3bar/include/mode.h4
-rw-r--r--i3bar/include/outputs.h4
-rw-r--r--i3bar/include/workspaces.h5
-rw-r--r--i3bar/src/child.c508
-rw-r--r--i3bar/src/config.c30
-rw-r--r--i3bar/src/ipc.c88
-rw-r--r--i3bar/src/main.c4
-rw-r--r--i3bar/src/mode.c18
-rw-r--r--i3bar/src/outputs.c21
-rw-r--r--i3bar/src/workspaces.c94
-rw-r--r--i3bar/src/xcb.c87
-rw-r--r--include/config_directives.h1
-rw-r--r--include/configuration.h4
-rw-r--r--man/i3bar.man5
-rw-r--r--meson.build2
-rw-r--r--parser-specs/config.spec5
-rw-r--r--release-notes/changes/1-workspace_command1
-rw-r--r--src/config.c1
-rw-r--r--src/config_directives.c5
-rw-r--r--src/ipc.c1
-rw-r--r--testcases/t/201-config-parser.t2
25 files changed, 886 insertions, 292 deletions
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 <<status_command>> for the statusline option and
+https://i3wm.org/docs/i3bar-workspace-protocol.html for the detailed protocol.
+
+*Syntax*:
+------------------------
+workspace_command <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 <config.h>
-#include <stdbool.h>
+#include <ev.h>
#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 <ctype.h> /* isspace */
#include <err.h>
#include <errno.h>
#include <ev.h>
@@ -27,14 +28,30 @@
#include <yajl/yajl_parse.h>
/* 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 *)&params);
-
- state = yajl_parse(handle, (const unsigned char *)json, strlen(json));
+ yajl_handle handle = yajl_alloc(&mode_callbacks, NULL, (void *)&params);
+ 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 *)&params);
-
- state = yajl_parse(handle, (const unsigned char *)json, strlen(json));
+ yajl_handle handle = yajl_alloc(&outputs_callbacks, NULL, (void *)&params);
+ 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 *)&params);
-
- 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 *)&params);
+ 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: <end>, '#', '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: <end>, '#', '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 <stdin>)
ERROR: CONFIG: Line 1: bar {
ERROR: CONFIG: Line 2: output LVDS-1