aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/main.yml21
-rw-r--r--docs/hacking-howto62
-rw-r--r--docs/i3bar-workspace-protocol184
-rw-r--r--docs/userguide36
-rw-r--r--i3-input/main.c39
-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/include/xcb.h1
-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/commands.h2
-rw-r--r--include/config_directives.h1
-rw-r--r--include/configuration.h4
-rw-r--r--man/i3.man23
-rw-r--r--man/i3bar.man5
-rw-r--r--meson.build2
-rw-r--r--parser-specs/commands.spec5
-rw-r--r--parser-specs/config.spec5
-rw-r--r--release-notes/changes/1-workspace_command1
-rw-r--r--release-notes/changes/2-focus-workspace1
-rw-r--r--release-notes/changes/3-global-fullscreen1
-rw-r--r--src/commands.c45
-rw-r--r--src/config.c1
-rw-r--r--src/config_directives.c5
-rw-r--r--src/ipc.c1
-rw-r--r--testcases/t/100-fullscreen.t12
-rw-r--r--testcases/t/187-commands-parser.t15
-rw-r--r--testcases/t/201-config-parser.t2
-rwxr-xr-xtravis/skip-pkg.sh14
-rw-r--r--travis/travis-base-ubuntu.Dockerfile2
-rw-r--r--travis/travis-base.Dockerfile2
40 files changed, 1011 insertions, 424 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ae47c92d..7e9cbecd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -33,8 +33,9 @@ jobs:
run: |
docker pull ${{ env.BASENAME }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME }} travis/travis-base.Dockerfile
- name: fetch or build extra Docker containers
+ if: github.ref == 'refs/heads/next' && matrix.compiler == 'gcc'
run: |
- ./travis/skip-pkg.sh || docker pull ${{ env.BASENAME_UBUNTU }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME_UBUNTU }} travis/travis-base-ubuntu.Dockerfile
+ docker pull ${{ env.BASENAME_UBUNTU }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME_UBUNTU }} travis/travis-base-ubuntu.Dockerfile
- name: build i3
run: |
docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${{ env.BASENAME }} /bin/sh -c 'rm -rf build; mkdir -p build && cd build && CFLAGS="-Wformat -Wformat-security -Wextra -Wno-unused-parameter -Wstrict-prototypes -Wmissing-prototypes -Werror -fno-common -D_FORTIFY_SOURCE=3" meson setup .. -Ddocs=true -Dmans=true -Db_sanitize=address --buildtype=debugoptimized && ninja -v'
@@ -54,22 +55,26 @@ jobs:
run: |
docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${{ env.BASENAME }} /bin/sh -c 'rm -rf distbuild; mkdir distbuild && cd distbuild && meson setup .. -Ddocs=true -Dmans=true && meson dist --no-tests'
- name: build Debian packages
+ if: github.ref == 'refs/heads/next' && matrix.compiler == 'gcc'
run: |
echo "::group::Debian amd64"
- ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/debian-build.sh deb/debian-amd64/DIST
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/debian-build.sh deb/debian-amd64/DIST
echo "::endgroup::"
echo "::group::Ubuntu amd64"
- ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME_UBUNTU }} ./travis/debian-build.sh deb/ubuntu-amd64/DIST
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME_UBUNTU }} ./travis/debian-build.sh deb/ubuntu-amd64/DIST
echo "::endgroup::"
- name: push Debian packages to balto
+ if: github.ref == 'refs/heads/next' && matrix.compiler == 'gcc'
run: |
- ./travis/skip-pkg.sh || travis/push-balto.sh
+ travis/push-balto.sh
- name: build docs
+ if: github.ref == 'refs/heads/next' && matrix.compiler == 'gcc'
run: |
- ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/docs.sh
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/docs.sh
- name: push docs to GitHub pages
+ if: github.ref == 'refs/heads/next' && matrix.compiler == 'gcc'
run: |
- ./travis/skip-pkg.sh || travis/deploy-github-pages.sh
+ travis/deploy-github-pages.sh
formatting:
name: Check formatting
runs-on: ubuntu-latest
@@ -79,8 +84,8 @@ jobs:
run: ./release-notes/generator.pl
- name: Install dependencies
run: |
- sudo apt-get install -y clang-format-12
+ sudo apt-get install -y clang-format-14
- name: Check formatting
- run: clang-format-12 --dry-run --Werror $(git ls-files '*.c' '*.h')
+ run: clang-format-14 --dry-run --Werror $(git ls-files '*.c' '*.h')
- name: Verify safe wrapper functions are used
run: ./travis/check-safe-wrappers.sh
diff --git a/docs/hacking-howto b/docs/hacking-howto
index c6dd6fa4..ae48c448 100644
--- a/docs/hacking-howto
+++ b/docs/hacking-howto
@@ -14,7 +14,7 @@ you find necessary, please do not hesitate to contact me.
<p>
++++
This document is not 100% up to date. Specifically, everything up to and
-including <<data_structures>> has been updated recently. The rest might contain
+including <<startup>> has been updated recently. The rest might contain
outdated information.
++++
</p>
@@ -34,30 +34,18 @@ Guide → Compiling a Meson project]. In case you’re unfamiliar:
=== Build system features
-* We use the +AX_ENABLE_BUILDDIR+ macro to enforce builds happening in a separate
- directory. This is a prerequisite for the +AX_EXTEND_SRCDIR+ macro and building
- in a separate directory is common practice anyway. In case this causes any
- trouble when packaging i3 for your distribution, please open an issue.
+* +ninja test+ runs the i3 testsuite. See docs/testsuite for details.
-* +make check+ runs the i3 testsuite. See docs/testsuite for details.
+* +meson dist+ builds a release tarball and runs tests on the result.
-* +make distcheck+ (runs testsuite on +make dist+ result, tiny bit quicker
- feedback cycle than waiting for the travis build to catch the issue).
+* +meson -Ddocs=true -Dmans=true+ will enable the options to build docs and
+ manpages. These options require additional dependencies that are normally not
+ required for users who just want to build i3.
-* +make uninstall+ (occasionally requested by users who compile from source)
-
-* +make+ will build manpages/docs by default if the tools are installed.
- Conversely, manpages/docs are not tried to be built for users who don’t want
- to install all these dependencies to get started hacking on i3. Manpages and
- docs can be disabled with the +--disable-mans++ and ++--disable-docs++
- configure options respectively.
-
-* non-release builds will enable address sanitizer by default. Use the
- +--disable-sanitizers+ configure option to turn off all sanitizers, and see
- +--help+ for available sanitizers.
-
-* Coverage reports are now generated using +make check-code-coverage+, which
- requires specifying +--enable-code-coverage+ when calling configure.
+* +meson -Db_sanitize=address+ will enable the address sanitizer which is
+ disabled by default. A summary of memory leaks will be printed on program
+ exit. This can include false-positives. For other options of the +b_sanitize+
+ flag see https://mesonbuild.com/Builtin-options.html.
== Pull requests
@@ -341,30 +329,26 @@ ensure that the operating system on which i3 is compiled has all the expected
features, i3 comes with +include/queue.h+. On BSD systems, you can use +man
queue(3)+. On Linux, you have to use google (or read the source).
-The lists used are +SLIST+ (single linked lists), +CIRCLEQ+ (circular
-queues) and +TAILQ+ (tail queues). Usually, only forward traversal is necessary,
-so an +SLIST+ works fine. If inserting elements at arbitrary positions or at
-the end of a list is necessary, a +TAILQ+ is used instead. However, for the
-windows inside a container, a +CIRCLEQ+ is necessary to go from the currently
-selected window to the window above/below.
-
-== Naming conventions
-
-There is a row of standard variables used in many events. The following names
-should be chosen for those:
+The lists used are +SLIST+ (single linked lists), +CIRCLEQ+ (circular queues)
+and +TAILQ+ (tail queues). Usually, +TAILQ+ is used which allows inserting
+elements at arbitrary positions or at the end of the list. If only forward
+traversal is necessary, an +SLIST+ can be used. +CIRCLEQ+ is used just to
+manage the X11 state of each window.
- * +conn+ is the xcb_connection_t
- * +event+ is the event of the particular type
- * +con+ names a container
- * +current+ is a loop variable when using +TAILQ_FOREACH+ etc.
+[[startup]]
+== Startup (src/main.c, main())
-== Startup (src/mainx.c, main())
+Among other things, the main() function does the following:
* Establish the xcb connection
+ * Load the i3 config
* Check for XKB extension on the separate X connection, load Xcursor
- * Check for RandR screens (with a fall-back to Xinerama)
+ * Set up EWMH hints
* Grab the keycodes for which bindings exist
+ * Check for XRandR screens
* Manage all existing windows
+ * Exec configured startup processes
+ * Start i3bar if configured
* Enter the event loop
== Keybindings
diff --git a/docs/i3bar-workspace-protocol b/docs/i3bar-workspace-protocol
new file mode 100644
index 00000000..016fc965
--- /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 available since i3 version 4.23.
+
+The program 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 +false+ if not included. +focused+ takes precedence over it,
+ however +visible+ is important for more than one monitors.
+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 120e4b90..8b09c9a3 100644
--- a/docs/userguide
+++ b/docs/userguide
@@ -317,7 +317,7 @@ single workspace on which you open three terminal windows. All these terminal
windows are directly attached to one node inside i3’s layout tree, the
workspace node. By default, the workspace node’s orientation is +horizontal+.
-Now you move one of these terminals down (+$mod+Shift+j+ by default). The
+Now you move one of these terminals down (+$mod+Shift+k+ by default). The
workspace node’s orientation will be changed to +vertical+. The terminal window
you moved down is directly attached to the workspace and appears on the bottom
of the screen. A new (horizontal) container was created to accommodate the
@@ -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
@@ -2397,6 +2421,9 @@ available:
<criteria>::
Sets focus to the container that matches the specified criteria.
See <<command_criteria>>.
+workspace::
+ Sets focus to the workspace that contains the container that matches the
+ specified criteria.
left|right|up|down::
Sets focus to the nearest container in the given direction.
parent::
@@ -2423,6 +2450,7 @@ output::
*Syntax*:
----------------------------------------------
<criteria> focus
+<criteria> focus workspace
focus left|right|down|up
focus parent|child|floating|tiling|mode_toggle
focus next|prev [sibling]
@@ -2434,6 +2462,10 @@ focus output left|right|down|up|current|primary|nonprimary|next|<output1> [outpu
# Focus firefox
bindsym $mod+F1 [class="Firefox"] focus
+# Focus the workspace where firefox is, without necessarily focusing firefox
+# itself.
+bindsym $mod+x [class="Firefox"] focus workspace
+
# Focus container on the left, bottom, top, right
bindsym $mod+j focus left
bindsym $mod+k focus down
@@ -2462,7 +2494,7 @@ bindsym $mod+x focus output primary
bindsym $mod+x focus output nonprimary
# Cycle focus between outputs VGA1 and LVDS1 but not DVI0
-bindsym $mod+x move workspace to output VGA1 LVDS1
+bindsym $mod+x focus output VGA1 LVDS1
-------------------------------------------------
Note that you might not have a primary output configured yet. To do so, run:
diff --git a/i3-input/main.c b/i3-input/main.c
index ef9e0701..afe16ccc 100644
--- a/i3-input/main.c
+++ b/i3-input/main.c
@@ -163,29 +163,32 @@ static void finish_input(void) {
char *command = (char *)concat_strings(glyphs_utf8, input_position);
/* count the occurrences of %s in the string */
- int c;
- int len = strlen(format);
- int cnt = 0;
- for (c = 0; c < (len - 1); c++)
- if (format[c] == '%' && format[c + 1] == 's')
+ const size_t len = strlen(format);
+ size_t cnt = 0;
+ for (size_t c = 0; c < (len - 1); c++) {
+ if (format[c] == '%' && format[c + 1] == 's') {
cnt++;
- printf("occurrences = %d\n", cnt);
+ }
+ }
+ printf("occurrences = %ld\n", cnt);
/* allocate space for the output */
- int inputlen = strlen(command);
- char *full = scalloc(strlen(format) - (2 * cnt) /* format without all %s */
- + (inputlen * cnt) /* replaced %s */
- + 1, /* trailing NUL */
- 1);
+ const size_t input_len = strlen(command);
+ const size_t full_len = MAX(input_len, /* avoid compiler warning */
+ strlen(format) - (2 * cnt) /* format without all %s */
+ + (input_len * cnt) /* replaced %s */
+ + 1 /* trailing NUL */
+ );
+ char *full = scalloc(full_len, 1);
char *dest = full;
- for (c = 0; c < len; c++) {
- /* if this is not % or it is % but without a following 's',
- * just copy the character */
- if (format[c] != '%' || (c == (len - 1)) || format[c + 1] != 's')
+ for (size_t c = 0; c < len; c++) {
+ /* if this is not % or it is % but without a following 's', just copy
+ * the character */
+ if (format[c] != '%' || (c == (len - 1)) || format[c + 1] != 's') {
*(dest++) = format[c];
- else {
- strncat(dest, command, inputlen);
- dest += inputlen;
+ } else {
+ strncat(dest, command, input_len);
+ dest += input_len;
/* skip the following 's' of '%s' */
c++;
}
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/include/xcb.h b/i3bar/include/xcb.h
index 0e3ca22d..fa274d01 100644
--- a/i3bar/include/xcb.h
+++ b/i3bar/include/xcb.h
@@ -12,7 +12,6 @@
#include <config.h>
#include <stdint.h>
-//#include "outputs.h"
#define _NET_SYSTEM_TRAY_ORIENTATION_HORZ 0
#define _NET_SYSTEM_TRAY_ORIENTATION_VERT 1
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/commands.h b/include/commands.h
index 2ae2643c..17d06681 100644
--- a/include/commands.h
+++ b/include/commands.h
@@ -198,7 +198,7 @@ void cmd_focus_level(I3_CMD, const char *level);
* Implementation of 'focus'.
*
*/
-void cmd_focus(I3_CMD);
+void cmd_focus(I3_CMD, bool focus_workspace);
/**
* Implementation of 'fullscreen [enable|disable|toggle] [global]'.
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/i3.man b/man/i3.man
index 1974f0b9..b2433108 100644
--- a/man/i3.man
+++ b/man/i3.man
@@ -317,32 +317,25 @@ xsetroot -solid "#333333"
# Enable core dumps in case something goes wrong
ulimit -c unlimited
-# Start i3 and log to ~/.i3/logfile
-echo "Starting at $(date)" >> ~/.i3/logfile
-exec /usr/bin/i3 -V -d all >> ~/.i3/logfile
+# Start i3 and log to ~/.local/share/i3/log
+mkdir -p ~/.local/share/i3
+echo "Starting at $(date)" >> ~/.local/share/i3/log
+exec /usr/bin/i3 -V -d all >> ~/.local/share/i3/log
-------------------------------------------------------------
== ENVIRONMENT
=== I3SOCK
-This variable overwrites the IPC socket path (placed in
-/tmp/i3-%u.XXXXXX/ipc-socket.%p by default, where %u is replaced with your UNIX
-username, %p is replaced with i3’s PID and XXXXXX is a string of random
-characters from the portable filename character set (see mkdtemp(3))). The IPC
-socket is used by external programs like i3-msg(1) or i3bar(1).
-
-== TODO
-
-There is still lot of work to do. Please check our bugtracker for up-to-date
-information about tasks which are still not finished.
+This variable overwrites the IPC socket path (see userguide for default
+location). The IPC socket is used by external programs like i3-msg(1), i3bar(1)
+or user-made scripts.
== SEE ALSO
You should have a copy of the userguide (featuring nice screenshots/graphics
which is why this is not integrated into this manpage), the debugging guide,
-and the "how to hack" guide. If you are building from source, run:
- +make -C docs+
+and the "how to hack" guide.
You can also access these documents online at https://i3wm.org/
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/commands.spec b/parser-specs/commands.spec
index 336a8d42..c0dcca7c 100644
--- a/parser-specs/commands.spec
+++ b/parser-specs/commands.spec
@@ -186,6 +186,7 @@ state WORKSPACE_NUMBER:
# focus output <output>
# focus tiling|floating|mode_toggle
# focus parent|child
+# focus workspace
# focus
state FOCUS:
direction = 'left', 'right', 'up', 'down'
@@ -198,8 +199,10 @@ state FOCUS:
-> call cmd_focus_window_mode($window_mode)
level = 'parent', 'child'
-> call cmd_focus_level($level)
+ workspace = 'workspace'
+ -> call cmd_focus(1)
end
- -> call cmd_focus()
+ -> call cmd_focus(0)
state FOCUS_AUTO:
'sibling'
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/release-notes/changes/2-focus-workspace b/release-notes/changes/2-focus-workspace
new file mode 100644
index 00000000..638266f2
--- /dev/null
+++ b/release-notes/changes/2-focus-workspace
@@ -0,0 +1 @@
+add "focus workspace" command
diff --git a/release-notes/changes/3-global-fullscreen b/release-notes/changes/3-global-fullscreen
new file mode 100644
index 00000000..e09aaf44
--- /dev/null
+++ b/release-notes/changes/3-global-fullscreen
@@ -0,0 +1 @@
+Allow switching workspaces when in global fullscreen mode
diff --git a/src/commands.c b/src/commands.c
index ae02ab75..b5bc2416 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -844,6 +844,13 @@ out:
free(buf);
}
+static void disable_global_fullscreen(void) {
+ Con *fs = con_get_fullscreen_con(croot, CF_GLOBAL);
+ if (fs) {
+ con_disable_fullscreen(fs);
+ }
+}
+
/*
* Implementation of 'workspace next|prev|next_on_output|prev_on_output'.
*
@@ -853,10 +860,7 @@ void cmd_workspace(I3_CMD, const char *which) {
DLOG("which=%s\n", which);
- if (con_get_fullscreen_con(croot, CF_GLOBAL)) {
- yerror("Cannot switch workspace while in global fullscreen");
- return;
- }
+ disable_global_fullscreen();
if (strcmp(which, "next") == 0)
ws = workspace_next();
@@ -885,10 +889,7 @@ void cmd_workspace(I3_CMD, const char *which) {
void cmd_workspace_number(I3_CMD, const char *which, const char *_no_auto_back_and_forth) {
const bool no_auto_back_and_forth = (_no_auto_back_and_forth != NULL);
- if (con_get_fullscreen_con(croot, CF_GLOBAL)) {
- yerror("Cannot switch workspace while in global fullscreen");
- return;
- }
+ disable_global_fullscreen();
long parsed_num = ws_name_to_number(which);
if (parsed_num == -1) {
@@ -920,10 +921,7 @@ void cmd_workspace_number(I3_CMD, const char *which, const char *_no_auto_back_a
*
*/
void cmd_workspace_back_and_forth(I3_CMD) {
- if (con_get_fullscreen_con(croot, CF_GLOBAL)) {
- yerror("Cannot switch workspace while in global fullscreen");
- return;
- }
+ disable_global_fullscreen();
workspace_back_and_forth();
@@ -944,10 +942,7 @@ void cmd_workspace_name(I3_CMD, const char *name, const char *_no_auto_back_and_
return;
}
- if (con_get_fullscreen_con(croot, CF_GLOBAL)) {
- yerror("Cannot switch workspace while in global fullscreen");
- return;
- }
+ disable_global_fullscreen();
DLOG("should switch to workspace %s\n", name);
if (!no_auto_back_and_forth && maybe_back_and_forth(cmd_output, name)) {
@@ -1459,7 +1454,7 @@ void cmd_focus_level(I3_CMD, const char *level) {
* Implementation of 'focus'.
*
*/
-void cmd_focus(I3_CMD) {
+void cmd_focus(I3_CMD, bool focus_workspace) {
DLOG("current_match = %p\n", current_match);
if (match_is_empty(current_match)) {
@@ -1481,11 +1476,12 @@ void cmd_focus(I3_CMD) {
Con *ws = con_get_workspace(current->con);
/* If no workspace could be found, this was a dock window.
* Just skip it, you cannot focus dock windows. */
- if (!ws)
+ if (!ws) {
continue;
+ }
/* In case this is a scratchpad window, call scratchpad_show(). */
- if (ws == __i3_scratch) {
+ if (ws == __i3_scratch && !focus_workspace) {
scratchpad_show(current->con);
/* While for the normal focus case we can change focus multiple
* times and only a single window ends up focused, we could show
@@ -1493,8 +1489,15 @@ void cmd_focus(I3_CMD) {
break;
}
- LOG("focusing %p / %s\n", current->con, current->con->name);
- con_activate_unblock(current->con);
+ if (focus_workspace) {
+ /* Show the workspace of the matched container, without necessarily
+ * focusing it. */
+ LOG("focusing workspace %p / %s - %p / %s\n", current->con, current->con->name, ws, ws->name);
+ workspace_show(ws);
+ } else {
+ LOG("focusing %p / %s\n", current->con, current->con->name);
+ con_activate_unblock(current->con);
+ }
}
cmd_output->needs_tree_render = true;
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/100-fullscreen.t b/testcases/t/100-fullscreen.t
index d817bee0..a14cb8ba 100644
--- a/testcases/t/100-fullscreen.t
+++ b/testcases/t/100-fullscreen.t
@@ -185,7 +185,9 @@ cmd 'focus left';
is($x->input_focus, $window->id, 'fullscreen window still focused');
################################################################################
-# Verify that changing workspace while in global fullscreen does not work.
+# Verify that changing workspace while in global fullscreen disables fullscreen
+# first.
+# See #2974
################################################################################
$tmp = fresh_workspace;
@@ -197,11 +199,11 @@ is(focused_ws(), $tmp, 'workspace selected');
$other = get_unused_workspace;
cmd "workspace $other";
-is($x->input_focus, $window->id, 'window still focused');
-is(focused_ws(), $tmp, 'workspace still selected');
+isnt($x->input_focus, $window->id, 'window not focused anymore');
+is(focused_ws(), $other, 'workspace switched');
+is_num_fullscreen($other, 0, 'no fullscreen windows');
+is_num_fullscreen($tmp, 0, 'no fullscreen windows');
-# leave global fullscreen so that is does not interfere with the other tests
-$window->fullscreen(0);
sync_with_i3;
################################################################################
diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t
index 3404df1f..c2ff4a26 100644
--- a/testcases/t/187-commands-parser.t
+++ b/testcases/t/187-commands-parser.t
@@ -106,30 +106,35 @@ is(parser_calls('resize shrink left 25 px or 33 ppt;'),
is(parser_calls('[con_mark=yay] focus'),
"cmd_criteria_add(con_mark, yay)\n" .
- "cmd_focus()",
+ "cmd_focus(0)",
'criteria focus ok');
+is(parser_calls('[con_mark=yay] focus workspace'),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_focus(1)",
+ 'criteria focus workspace ok');
+
is(parser_calls("[con_mark=yay con_mark=bar] focus"),
"cmd_criteria_add(con_mark, yay)\n" .
"cmd_criteria_add(con_mark, bar)\n" .
- "cmd_focus()",
+ "cmd_focus(0)",
'criteria focus ok');
is(parser_calls("[con_mark=yay\tcon_mark=bar] focus"),
"cmd_criteria_add(con_mark, yay)\n" .
"cmd_criteria_add(con_mark, bar)\n" .
- "cmd_focus()",
+ "cmd_focus(0)",
'criteria focus ok');
is(parser_calls("[con_mark=yay\tcon_mark=bar]\tfocus"),
"cmd_criteria_add(con_mark, yay)\n" .
"cmd_criteria_add(con_mark, bar)\n" .
- "cmd_focus()",
+ "cmd_focus(0)",
'criteria focus ok');
is(parser_calls('[con_mark="yay"] focus'),
"cmd_criteria_add(con_mark, yay)\n" .
- "cmd_focus()",
+ "cmd_focus(0)",
'quoted criteria focus ok');
# Make sure trailing whitespace is stripped off: While this is not an issue for
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
diff --git a/travis/skip-pkg.sh b/travis/skip-pkg.sh
deleted file mode 100755
index 0ca1d749..00000000
--- a/travis/skip-pkg.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-# Returns true if Debian/Ubuntu packages should be skipped because this CI run
-# was triggered by a pull request.
-
-# Verify BALTO_TOKEN is present (only set on github.com/i3/i3),
-# otherwise the CI run was triggered by a pull request.
-# Verify CC=gcc so that we only build packages once for each commit,
-# not twice (with gcc and clang).
-if [ ! -z "$BALTO_TOKEN" ] && [ "$CC" = "gcc" ]
-then
- exit 1
-fi
-
-exit 0
diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile
index c99cd603..253e6382 100644
--- a/travis/travis-base-ubuntu.Dockerfile
+++ b/travis/travis-base-ubuntu.Dockerfile
@@ -1,7 +1,7 @@
# vim:ft=Dockerfile
# Same as travis-base.Dockerfile, but without the test suite dependencies since
# we only build Debian packages on Ubuntu, we don’t run the tests.
-FROM ubuntu:focal
+FROM ubuntu:jammy
RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup
# Paper over occasional network flakiness of some mirrors.
diff --git a/travis/travis-base.Dockerfile b/travis/travis-base.Dockerfile
index 52ae0656..3733a9b8 100644
--- a/travis/travis-base.Dockerfile
+++ b/travis/travis-base.Dockerfile
@@ -16,7 +16,7 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
dpkg-dev devscripts git equivs \
- build-essential clang \
+ build-essential clang libclang-rt-dev \
lintian \
libmodule-install-perl libanyevent-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libinline-c-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libipc-run-perl libxcb-xtest0-dev libx11-xcb-perl libjson-xs-perl x11-xserver-utils && \
rm -rf /var/lib/apt/lists/*