From 47562b414389711a82db25a694b064ad39117f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sun, 8 May 2016 12:55:27 +0200 Subject: [PATCH] Introduce support for specifying variables from X resources. (#2286) This patch introduces a new 'set_from_resource' config directive which allows defining a variable by retrieving its value from the X resource database. This avoids having to configure a color scheme in multiple files. The directive takes an additional fallback value which is used in case the resource cannot be found or during config validation where no X connection is available. Furthermore, this patch includes the following changes: - If the same variable is defined twice, we now properly overwrite the value of the assignment rather than inserting two variable definitions with the same key. - We now depend on xcb-util-xrm to query the resource. - Increase the buffer size for variable / resource assignments. fixes #2130 --- DEPENDS | 45 ++++---- common.mk | 4 + debian/control | 1 + docs/userguide | 32 ++++++ include/config_parser.h | 1 + parser-specs/config.spec | 1 + src/config_parser.c | 132 +++++++++++++++++------ src/i3.mk | 4 +- testcases/t/201-config-parser.t | 2 +- testcases/t/532-xresources.t | 64 +++++++++++ travis/travis-base-ubuntu-386.Dockerfile | 11 ++ travis/travis-base-ubuntu.Dockerfile | 11 ++ 12 files changed, 253 insertions(+), 55 deletions(-) create mode 100644 testcases/t/532-xresources.t diff --git a/DEPENDS b/DEPENDS index fad11da5..227aaec3 100644 --- a/DEPENDS +++ b/DEPENDS @@ -4,28 +4,29 @@ "min" means minimum required version "lkgv" means last known good version -┌──────────────┬────────┬────────┬────────────────────────────────────────┐ -│ dependency │ min. │ lkgv │ URL │ -├──────────────┼────────┼────────┼────────────────────────────────────────┤ -│ pkg-config │ 0.25 │ 0.28 │ http://pkgconfig.freedesktop.org/ │ -│ libxcb │ 1.1.93 │ 1.11 │ http://xcb.freedesktop.org/dist/ │ -│ xcb-util │ 0.3.3 │ 0.4.1 │ http://xcb.freedesktop.org/dist/ │ -│ xkbcommon │ 0.4.0 │ 0.5.0 │ http://xkbcommon.org/ │ -│ xkbcommon-x11│ 0.4.0 │ 0.5.0 │ http://xkbcommon.org/ │ -│ util-cursor³⁴│ 0.0.99 │ 0.1.2 │ http://xcb.freedesktop.org/dist/ │ -│ util-wm⁴ │ 0.3.8 │ 0.3.8 │ http://xcb.freedesktop.org/dist/ │ -│ util-keysyms⁴│ 0.3.8 │ 0.4.0 │ http://xcb.freedesktop.org/dist/ │ -│ libev │ 4.0 │ 4.19 │ http://libev.schmorp.de/ │ -│ yajl │ 2.0.1 │ 2.1.0 │ http://lloyd.github.com/yajl/ │ -│ asciidoc │ 8.3.0 │ 8.6.8 │ http://www.methods.co.nz/asciidoc/ │ -│ xmlto │ 0.0.23 │ 0.0.23 │ http://www.methods.co.nz/asciidoc/ │ -│ Pod::Simple² │ 3.22 │ 3.22 │ http://search.cpan.org/~dwheeler/Pod-Simple-3.23/ -│ docbook-xml │ 4.5 │ 4.5 │ http://www.methods.co.nz/asciidoc/ │ -│ PCRE │ 8.12 │ 8.35 │ http://www.pcre.org/ │ -│ libsn¹ │ 0.10 │ 0.12 │ http://freedesktop.org/wiki/Software/startup-notification -│ pango │ 1.30.0 | 1.36.8 │ http://www.pango.org/ │ -│ cairo │ 1.14.4 │ 1.14.4 │ http://cairographics.org/ │ -└──────────────┴────────┴────────┴────────────────────────────────────────┘ +┌──────────────┬────────┬────────┬───────────────────────────────────────────────────────────┐ +│ dependency │ min. │ lkgv │ URL │ +├──────────────┼────────┼────────┼───────────────────────────────────────────────────────────┤ +│ pkg-config │ 0.25 │ 0.28 │ http://pkgconfig.freedesktop.org/ │ +│ libxcb │ 1.1.93 │ 1.11 │ http://xcb.freedesktop.org/dist/ │ +│ xcb-util │ 0.3.3 │ 0.4.1 │ http://xcb.freedesktop.org/dist/ │ +│ xkbcommon │ 0.4.0 │ 0.5.0 │ http://xkbcommon.org/ │ +│ xkbcommon-x11│ 0.4.0 │ 0.5.0 │ http://xkbcommon.org/ │ +│ util-cursor³⁴│ 0.0.99 │ 0.1.2 │ http://xcb.freedesktop.org/dist/ │ +│ util-wm⁴ │ 0.3.8 │ 0.3.8 │ http://xcb.freedesktop.org/dist/ │ +│ util-keysyms⁴│ 0.3.8 │ 0.4.0 │ http://xcb.freedesktop.org/dist/ │ +│ util-xrm⁴ │ 1.0.0 │ 1.0.0 │ https://github.com/Airblader/xcb-util-xrm │ +│ libev │ 4.0 │ 4.19 │ http://libev.schmorp.de/ │ +│ yajl │ 2.0.1 │ 2.1.0 │ http://lloyd.github.com/yajl/ │ +│ asciidoc │ 8.3.0 │ 8.6.8 │ http://www.methods.co.nz/asciidoc/ │ +│ xmlto │ 0.0.23 │ 0.0.23 │ http://www.methods.co.nz/asciidoc/ │ +│ Pod::Simple² │ 3.22 │ 3.22 │ http://search.cpan.org/~dwheeler/Pod-Simple-3.23/ │ +│ docbook-xml │ 4.5 │ 4.5 │ http://www.methods.co.nz/asciidoc/ │ +│ PCRE │ 8.12 │ 8.35 │ http://www.pcre.org/ │ +│ libsn¹ │ 0.10 │ 0.12 │ http://freedesktop.org/wiki/Software/startup-notification │ +│ pango │ 1.30.0 | 1.36.8 │ http://www.pango.org/ │ +│ cairo │ 1.14.4 │ 1.14.4 │ http://cairographics.org/ │ +└──────────────┴────────┴────────┴───────────────────────────────────────────────────────────┘ ¹ libsn = libstartup-notification ² Pod::Simple is a Perl module required for converting the testsuite documentation to HTML. See http://michael.stapelberg.de/cpan/#Pod::Simple diff --git a/common.mk b/common.mk index 1e738b04..0215d35b 100644 --- a/common.mk +++ b/common.mk @@ -126,6 +126,10 @@ XKB_COMMON_LIBS := $(call ldflags_for_lib, xkbcommon,xkbcommon) XKB_COMMON_X11_CFLAGS := $(call cflags_for_lib, xkbcommon-x11,xkbcommon-x11) XKB_COMMON_X11_LIBS := $(call ldflags_for_lib, xkbcommon-x11,xkbcommon-x11) +# XCB xrm +XCB_XRM_CFLAGS := $(call cflags_for_lib, xcb-xrm) +XCB_XRM_LIBS := $(call ldflags_for_lib, xcb-xrm,xcb-xrm) + # yajl YAJL_CFLAGS := $(call cflags_for_lib, yajl) YAJL_LIBS := $(call ldflags_for_lib, yajl,yajl) diff --git a/debian/control b/debian/control index 577bb4d4..6b1530b8 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: debhelper (>= 9), libxcb-randr0-dev, libxcb-icccm4-dev, libxcb-cursor-dev, + libxcb-xrm-dev, libxcb-xkb-dev, libxkbcommon-dev (>= 0.4.0), libxkbcommon-x11-dev (>= 0.4.0), diff --git a/docs/userguide b/docs/userguide index d0ac2ddb..d9cfc642 100644 --- a/docs/userguide +++ b/docs/userguide @@ -700,6 +700,38 @@ absolutely no plans to change this. If you need a more dynamic configuration you should create a little script which generates a configuration file and run it before starting i3 (for example in your +~/.xsession+ file). +Also see <> to learn how to create variables based on resources +loaded from the X resource database. + +[[xresources]] +=== X resources + +<> can also be created using a value configured in the X resource +database. This is useful, for example, to avoid configuring color values within +the i3 configuration. Instead, the values can be configured, once, in the X +resource database to achieve an easily maintainable, consistent color theme +across many X applications. + +Defining a resource will load this resource from the resource database and +assign its value to the specified variable. A fallback must be specified in +case the resource cannot be loaded from the database. + +*Syntax*: +---------------------------------------------------- +set_from_resource $ +---------------------------------------------------- + +*Example*: +---------------------------------------------------------------------------- +# The ~/.Xresources should contain a line such as +# *color0: #121212 +# and must be loaded properly, e.g., by using +# xrdb ~/.Xresources +# This value is picked up on by other applications (e.g., the URxvt terminal +# emulator) and can be used in i3 like this: +set_from_resource $black i3wm.color0 #000000 +---------------------------------------------------------------------------- + [[assign_workspace]] === Automatically putting clients on specific workspaces diff --git a/include/config_parser.h b/include/config_parser.h index 9c23a11d..2ba79a68 100644 --- a/include/config_parser.h +++ b/include/config_parser.h @@ -11,6 +11,7 @@ #include +SLIST_HEAD(variables_head, Variable); extern pid_t config_error_nagbar_pid; /* diff --git a/parser-specs/config.spec b/parser-specs/config.spec index ef3bc2e0..24f40802 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -18,6 +18,7 @@ state INITIAL: error -> '#' -> IGNORE_LINE 'set' -> IGNORE_LINE + 'set_from_resource' -> IGNORE_LINE bindtype = 'bindsym', 'bindcode', 'bind' -> BINDING 'bar' -> BARBRACE 'font' -> FONT diff --git a/src/config_parser.c b/src/config_parser.c index b197eb22..5b5e2363 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -35,6 +35,7 @@ #include #include #include +#include #include "all.h" @@ -42,6 +43,8 @@ #define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__) #define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str)) +xcb_xrm_database_t *database = NULL; + #ifndef TEST_PARSER pid_t config_error_nagbar_pid = -1; static struct context *context; @@ -811,18 +814,80 @@ void start_config_error_nagbar(const char *configpath, bool has_errors) { free(pageraction); } +/* + * Inserts or updates a variable assignment depending on whether it already exists. + * + */ +static void upsert_variable(struct variables_head *variables, char *key, char *value) { + struct Variable *current; + SLIST_FOREACH(current, variables, variables) { + if (strcmp(current->key, key) != 0) { + continue; + } + + DLOG("Updated variable: %s = %s -> %s\n", key, current->value, value); + FREE(current->value); + current->value = sstrdup(value); + return; + } + + DLOG("Defined new variable: %s = %s\n", key, value); + struct Variable *new = scalloc(1, sizeof(struct Variable)); + struct Variable *test = NULL, *loc = NULL; + new->key = sstrdup(key); + new->value = sstrdup(value); + /* ensure that the correct variable is matched in case of one being + * the prefix of another */ + SLIST_FOREACH(test, variables, variables) { + if (strlen(new->key) >= strlen(test->key)) + break; + loc = test; + } + + if (loc == NULL) { + SLIST_INSERT_HEAD(variables, new, variables); + } else { + SLIST_INSERT_AFTER(loc, new, variables); + } +} + +static char *get_resource(char *name) { + if (conn == NULL) { + return NULL; + } + + /* Load the resource database lazily. */ + if (database == NULL) { + database = xcb_xrm_database_from_default(conn); + + if (database == NULL) { + ELOG("Failed to open the resource database.\n"); + + /* Load an empty database so we don't keep trying to load the + * default database over and over again. */ + database = xcb_xrm_database_from_string(""); + + return NULL; + } + } + + char *resource; + xcb_xrm_resource_get_string(database, name, NULL, &resource); + return resource; +} + /* * Parses the given file by first replacing the variables, then calling * parse_config and possibly launching i3-nagbar. * */ bool parse_file(const char *f, bool use_nagbar) { - SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); + struct variables_head variables = SLIST_HEAD_INITIALIZER(&variables); int fd; struct stat stbuf; char *buf; FILE *fstr; - char buffer[4096], key[512], value[512], *continuation = NULL; + char buffer[4096], key[512], value[4096], *continuation = NULL; if ((fd = open(f, O_RDONLY)) == -1) die("Could not open configuration file: %s\n", strerror(errno)); @@ -848,8 +913,9 @@ bool parse_file(const char *f, bool use_nagbar) { } /* sscanf implicitly strips whitespace. */ - const bool skip_line = (sscanf(buffer, "%511s %511[^\n]", key, value) < 1 || strlen(key) < 3); + const bool skip_line = (sscanf(buffer, "%511s %4095[^\n]", key, value) < 1 || strlen(key) < 3); const bool comment = (key[0] == '#'); + value[4095] = '\n'; continuation = strstr(buffer, "\\\n"); if (continuation) { @@ -868,49 +934,55 @@ bool parse_file(const char *f, bool use_nagbar) { } if (strcasecmp(key, "set") == 0) { - if (value[0] != '$') { + char v_key[512]; + char v_value[4096]; + + if (sscanf(value, "%511s %4095[^\n]", v_key, v_value) < 1) { + ELOG("Failed to parse variable specification '%s', skipping it.\n", value); + continue; + } + + if (v_key[0] != '$') { ELOG("Malformed variable assignment, name has to start with $\n"); continue; } - /* get key/value for this variable */ - char *v_key = value, *v_value; - if (strstr(value, " ") == NULL && strstr(value, "\t") == NULL) { - ELOG("Malformed variable assignment, need a value\n"); + upsert_variable(&variables, v_key, v_value); + continue; + } else if (strcasecmp(key, "set_from_resource") == 0) { + char res_name[512]; + char v_key[512]; + char fallback[4096]; + + if (sscanf(value, "%511s %511s %4095[^\n]", v_key, res_name, fallback) < 1) { + ELOG("Failed to parse resource specification '%s', skipping it.\n", value); continue; } - if (!(v_value = strstr(value, " "))) - v_value = strstr(value, "\t"); - - *(v_value++) = '\0'; - while (*v_value == '\t' || *v_value == ' ') - v_value++; - - struct Variable *new = scalloc(1, sizeof(struct Variable)); - struct Variable *test = NULL, *loc = NULL; - new->key = sstrdup(v_key); - new->value = sstrdup(v_value); - /* ensure that the correct variable is matched in case of one being - * the prefix of another */ - SLIST_FOREACH(test, &variables, variables) { - if (strlen(new->key) >= strlen(test->key)) - break; - loc = test; + if (v_key[0] != '$') { + ELOG("Malformed variable assignment, name has to start with $\n"); + continue; } - if (loc == NULL) { - SLIST_INSERT_HEAD(&variables, new, variables); - } else { - SLIST_INSERT_AFTER(loc, new, variables); + char *res_value = get_resource(res_name); + if (res_value == NULL) { + DLOG("Could not get resource '%s', using fallback '%s'.\n", res_name, fallback); + res_value = sstrdup(fallback); } - DLOG("Got new variable %s = %s\n", v_key, v_value); + upsert_variable(&variables, v_key, res_value); + FREE(res_value); continue; } } fclose(fstr); + if (database != NULL) { + xcb_xrm_database_free(database); + /* Explicitly set the database to NULL again in case the config gets reloaded. */ + database = NULL; + } + /* For every custom variable, see how often it occurs in the file and * how much extra bytes it requires when replaced. */ struct Variable *current, *nearest; diff --git a/src/i3.mk b/src/i3.mk index ed8e3ae9..6cb4abf4 100644 --- a/src/i3.mk +++ b/src/i3.mk @@ -5,8 +5,8 @@ CLEAN_TARGETS += clean-i3 i3_SOURCES := $(filter-out $(i3_SOURCES_GENERATED),$(wildcard src/*.c)) i3_HEADERS_CMDPARSER := $(wildcard include/GENERATED_*.h) i3_HEADERS := $(filter-out $(i3_HEADERS_CMDPARSER),$(wildcard include/*.h)) -i3_CFLAGS = $(XKB_COMMON_CFLAGS) $(XKB_COMMON_X11_CFLAGS) $(XCB_CFLAGS) $(XCB_KBD_CFLAGS) $(XCB_WM_CFLAGS) $(XCB_CURSOR_CFLAGS) $(PANGO_CFLAGS) $(YAJL_CFLAGS) $(LIBEV_CFLAGS) $(PCRE_CFLAGS) $(LIBSN_CFLAGS) -i3_LIBS = $(XKB_COMMON_LIBS) $(XKB_COMMON_X11_LIBS) $(XCB_LIBS) $(XCB_XKB_LIBS) $(XCB_KBD_LIBS) $(XCB_WM_LIBS) $(XCB_CURSOR_LIBS) $(PANGO_LIBS) $(YAJL_LIBS) $(LIBEV_LIBS) $(PCRE_LIBS) $(LIBSN_LIBS) -lm -lpthread +i3_CFLAGS = $(XKB_COMMON_CFLAGS) $(XKB_COMMON_X11_CFLAGS) $(XCB_CFLAGS) $(XCB_KBD_CFLAGS) $(XCB_WM_CFLAGS) $(XCB_CURSOR_CFLAGS) $(XCB_XRM_CFLAGS) $(PANGO_CFLAGS) $(YAJL_CFLAGS) $(LIBEV_CFLAGS) $(PCRE_CFLAGS) $(LIBSN_CFLAGS) +i3_LIBS = $(XKB_COMMON_LIBS) $(XKB_COMMON_X11_LIBS) $(XCB_LIBS) $(XCB_XKB_LIBS) $(XCB_KBD_LIBS) $(XCB_WM_LIBS) $(XCB_CURSOR_LIBS) $(XCB_XRM_LIBS) $(PANGO_LIBS) $(YAJL_LIBS) $(LIBEV_LIBS) $(PCRE_LIBS) $(LIBSN_LIBS) -lm -lpthread # When using clang, we use pre-compiled headers to speed up the build. With # gcc, this actually makes the build slower. diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index e8835005..bcccc5a2 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -441,7 +441,7 @@ client.focused #4c7899 #285577 #ffffff #2e9ef4 EOT my $expected_all_tokens = <<'EOT'; -ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'bindsym', 'bindcode', 'bind', 'bar', 'font', 'mode', 'floating_minimum_size', 'floating_maximum_size', 'floating_modifier', 'default_orientation', 'workspace_layout', 'new_window', 'new_float', 'hide_edge_borders', 'for_window', 'assign', 'no_focus', 'focus_follows_mouse', 'mouse_warping', 'force_focus_wrapping', 'force_xinerama', 'force-xinerama', 'workspace_auto_back_and_forth', 'fake_outputs', 'fake-outputs', 'force_display_urgency_hint', 'focus_on_window_activation', 'show_marks', 'workspace', 'ipc_socket', 'ipc-socket', 'restart_state', 'popup_during_fullscreen', 'exec_always', 'exec', 'client.background', 'client.focused_inactive', 'client.focused', 'client.unfocused', 'client.urgent', 'client.placeholder' +ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'set_from_resource', 'bindsym', 'bindcode', 'bind', 'bar', 'font', 'mode', 'floating_minimum_size', 'floating_maximum_size', 'floating_modifier', 'default_orientation', 'workspace_layout', 'new_window', 'new_float', 'hide_edge_borders', 'for_window', 'assign', 'no_focus', 'focus_follows_mouse', 'mouse_warping', 'force_focus_wrapping', 'force_xinerama', 'force-xinerama', 'workspace_auto_back_and_forth', 'fake_outputs', 'fake-outputs', 'force_display_urgency_hint', 'focus_on_window_activation', 'show_marks', 'workspace', 'ipc_socket', 'ipc-socket', 'restart_state', 'popup_during_fullscreen', 'exec_always', 'exec', 'client.background', 'client.focused_inactive', 'client.focused', 'client.unfocused', 'client.urgent', 'client.placeholder' EOT my $expected_end = <<'EOT'; diff --git a/testcases/t/532-xresources.t b/testcases/t/532-xresources.t new file mode 100644 index 00000000..438fad20 --- /dev/null +++ b/testcases/t/532-xresources.t @@ -0,0 +1,64 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests for using X resources in the config. +# Ticket: #2130 +use i3test i3_autostart => 0; +use X11::XCB qw(PROP_MODE_REPLACE); + +sub get_marks { + return i3(get_socket_path())->get_marks->recv; +} + +my $config = <change_property( + PROP_MODE_REPLACE, + $x->get_root_window(), + $x->atom(name => 'RESOURCE_MANAGER')->id, + $x->atom(name => 'STRING')->id, + 32, + length('*mark: works'), + '*mark: works'); +$x->flush; + +my $pid = launch_with_config($config); + +open_window(wm_class => 'worksforme'); +sync_with_i3; +is_deeply(get_marks(), [ 'works' ], 'the resource has loaded correctly'); + +cmd 'kill'; + +open_window(wm_class => 'doesnotworkforme'); +sync_with_i3; +is_deeply(get_marks(), [ 'none' ], 'the resource fallback was used'); + +exit_gracefully($pid); + +done_testing; diff --git a/travis/travis-base-ubuntu-386.Dockerfile b/travis/travis-base-ubuntu-386.Dockerfile index b1ee1c0c..ae6b7c82 100644 --- a/travis/travis-base-ubuntu-386.Dockerfile +++ b/travis/travis-base-ubuntu-386.Dockerfile @@ -27,3 +27,14 @@ COPY debian/control /usr/src/i3-debian-packaging/control RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* + +# Install xcb-util-xrm. This is a workaround until it is available in the +# distribution packages. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xutils-dev +RUN git clone --recursive https://github.com/Airblader/xcb-util-xrm.git && \ + cd xcb-util-xrm && \ + ./autogen.sh --prefix=/usr && \ + make && \ + make install && \ + cd - diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile index e76229eb..62b81283 100644 --- a/travis/travis-base-ubuntu.Dockerfile +++ b/travis/travis-base-ubuntu.Dockerfile @@ -28,3 +28,14 @@ COPY debian/control /usr/src/i3-debian-packaging/control RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* + +# Install xcb-util-xrm. This is a workaround until it is available in the +# distribution packages. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xutils-dev +RUN git clone --recursive https://github.com/Airblader/xcb-util-xrm.git && \ + cd xcb-util-xrm && \ + ./autogen.sh --prefix=/usr && \ + make && \ + make install && \ + cd -