config: support merging multiple config files

Add the -m|--merge-config command line option to iterate backwards over
XDG Base Dir paths and read config/theme files multiple times.

For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml
exist, the latter will be read first and then the former (if
--merge-config is enabled).

When $XDG_CONFIG_HOME is defined, make it replace (not augment)
$HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when
defined.

XDG Base Dir Spec does not specify whether or not an application (or a
compositor!) should (a) define that only the file under the most important
base directory should be used, or (b) define rules for merging the
information from the different files.

ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

In the case of labwc there is a use-case for both positions, just to be
clear, the default behaviour, described by position (a) above, does NOT
change.

This change affects the following config/theme files:
  - rc.xml
  - menu.xml
  - autostart
  - environment
  - themerc
  - themerc-override
  - Theme buttons, for example max.xbm

Instead of caching global config/theme directories, create lists of paths
(e.g.  '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc).
This creates more common parsing logic and just reversing the direction
of iteration and breaks early if config-merge is not wanted.

Enable better fallback for themes. For example if a particular theme does
not exist in $HOME/.local/share/themes, it will be searched for in
~/.themes/ and so on. This also applies to theme buttons which now
fallback on an individual basis.

Avoid using stat() in most situations and just go straight to fopen().

Fixes #1406
This commit is contained in:
Johan Malm 2024-01-09 22:00:45 +00:00 committed by Johan Malm
parent d0aff49c81
commit 698c7ace07
14 changed files with 330 additions and 240 deletions

View file

@ -1,11 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <stdio.h>
#include <unistd.h>
#include "button/common.h"
#include "common/dir.h"
#include "config/rcxml.h"
#include "labwc.h"
void
button_filename(const char *name, char *buf, size_t len)
{
snprintf(buf, len, "%s/%s", theme_dir(rc.theme_name), name);
struct wl_list paths;
paths_theme_create(&paths, rc.theme_name, name);
/*
* You can't really merge buttons, so let's just iterate forwards
* and stop on the first hit
*/
struct path *path;
wl_list_for_each(path, &paths, link) {
if (access(path->string, R_OK) == 0) {
snprintf(buf, len, "%s", path->string);
break;
}
}
paths_destroy(&paths);
}

View file

@ -4,143 +4,190 @@
*
* Copyright Johan Malm 2020
*/
#include <assert.h>
#include <glib.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include "common/dir.h"
#include "common/buf.h"
#include "common/list.h"
#include "common/mem.h"
#include "common/string-helpers.h"
#include "labwc.h"
struct dir {
const char *prefix;
const char *default_prefix;
const char *path;
};
static struct dir config_dirs[] = {
{ "XDG_CONFIG_HOME", "labwc" },
{ "HOME", ".config/labwc" },
{ "XDG_CONFIG_DIRS", "labwc" },
{ NULL, "/etc/xdg/labwc" },
{ NULL, NULL }
{
.prefix = "XDG_CONFIG_HOME",
.default_prefix = "$HOME/.config",
.path = "labwc"
}, {
.prefix = "XDG_CONFIG_DIRS",
.default_prefix = "/etc/xdg",
.path = "labwc",
}, {
.path = NULL,
}
};
static struct dir theme_dirs[] = {
{ "XDG_DATA_HOME", "themes" },
{ "HOME", ".local/share/themes" },
{ "HOME", ".themes" },
{ "XDG_DATA_DIRS", "themes" },
{ NULL, "/usr/share/themes" },
{ NULL, "/usr/local/share/themes" },
{ NULL, "/opt/share/themes" },
{ NULL, NULL }
{
.prefix = "XDG_DATA_HOME",
.default_prefix = "$HOME/.local/share",
.path = "themes",
}, {
.prefix = "HOME",
.path = ".themes",
}, {
.prefix = "XDG_DATA_DIRS",
.default_prefix = "/usr/share:/usr/local/share:/opt/share",
.path = "themes",
}, {
.path = NULL,
}
};
static bool
isdir(const char *path)
{
struct stat st;
return (!stat(path, &st) && S_ISDIR(st.st_mode));
}
struct ctx {
void (*build_path_fn)(struct ctx *ctx, char *prefix, const char *path);
const char *filename;
char *buf;
size_t len;
struct dir *dirs;
const char *theme_name;
struct wl_list *list;
};
struct wl_list *paths_get_prev(struct wl_list *elm) { return elm->prev; }
struct wl_list *paths_get_next(struct wl_list *elm) { return elm->next; }
static void
build_config_path(struct ctx *ctx, char *prefix, const char *path)
{
if (!prefix) {
snprintf(ctx->buf, ctx->len, "%s", path);
} else {
snprintf(ctx->buf, ctx->len, "%s/%s", prefix, path);
}
assert(prefix);
snprintf(ctx->buf, ctx->len, "%s/%s/%s", prefix, path, ctx->filename);
}
static void
build_theme_path(struct ctx *ctx, char *prefix, const char *path)
{
if (!prefix) {
snprintf(ctx->buf, ctx->len, "%s/%s/openbox-3", path,
ctx->theme_name);
} else {
snprintf(ctx->buf, ctx->len, "%s/%s/%s/openbox-3", prefix, path,
ctx->theme_name);
}
assert(prefix);
snprintf(ctx->buf, ctx->len, "%s/%s/%s/openbox-3/%s", prefix, path,
ctx->theme_name, ctx->filename);
}
static char *
static void
find_dir(struct ctx *ctx)
{
char *debug = getenv("LABWC_DEBUG_DIR_CONFIG_AND_THEME");
for (int i = 0; ctx->dirs[i].path; i++) {
struct dir d = ctx->dirs[i];
if (!d.prefix) {
/* handle /etc/xdg... */
ctx->build_path_fn(ctx, NULL, d.path);
struct buf prefix;
buf_init(&prefix);
/*
* Replace (rather than augment) $HOME/.config with
* $XDG_CONFIG_HOME if defined, and so on for the other
* XDG Base Directories.
*/
char *pfxenv = getenv(d.prefix);
buf_add(&prefix, pfxenv ? pfxenv : d.default_prefix);
if (!prefix.len) {
free(prefix.buf);
continue;
}
/* Handle .default_prefix shell variables such as $HOME */
buf_expand_shell_variables(&prefix);
/*
* Respect that $XDG_DATA_DIRS can contain multiple colon
* separated paths and that we have structured the
* .default_prefix in the same way.
*/
gchar * *prefixes;
prefixes = g_strsplit(prefix.buf, ":", -1);
for (gchar * *p = prefixes; *p; p++) {
ctx->build_path_fn(ctx, *p, d.path);
if (debug) {
fprintf(stderr, "%s\n", ctx->buf);
}
if (isdir(ctx->buf)) {
return ctx->buf;
}
} else {
/* handle $HOME/.config/... and $XDG_* */
char *prefix = getenv(d.prefix);
if (!prefix) {
continue;
}
gchar * *prefixes;
prefixes = g_strsplit(prefix, ":", -1);
for (gchar * *p = prefixes; *p; p++) {
ctx->build_path_fn(ctx, *p, d.path);
if (debug) {
fprintf(stderr, "%s\n", ctx->buf);
}
if (isdir(ctx->buf)) {
g_strfreev(prefixes);
return ctx->buf;
}
}
g_strfreev(prefixes);
/*
* TODO: We could stat() and continue here if we really
* wanted to only respect only the first hit, but feels
* like it is probably overkill.
*/
struct path *path = znew(*path);
path->string = xstrdup(ctx->buf);
wl_list_append(ctx->list, &path->link);
}
g_strfreev(prefixes);
free(prefix.buf);
}
/* no directory was found */
ctx->buf[0] = '\0';
return ctx->buf;
}
char *
config_dir(void)
void
paths_config_create(struct wl_list *paths, const char *filename)
{
static char buf[4096] = { 0 };
if (buf[0] != '\0') {
return buf;
char buf[4096] = { 0 };
wl_list_init(paths);
/*
* If user provided a config directory with the -C command line option,
* then that trumps everything else and we do not create the
* XDG-Base-Dir list.
*/
if (rc.config_dir) {
struct path *path = znew(*path);
path->string = strdup_printf("%s/%s", rc.config_dir, filename);
wl_list_append(paths, &path->link);
return;
}
struct ctx ctx = {
.build_path_fn = build_config_path,
.filename = filename,
.buf = buf,
.len = sizeof(buf),
.dirs = config_dirs
.dirs = config_dirs,
.list = paths,
};
return find_dir(&ctx);
find_dir(&ctx);
}
char *
theme_dir(const char *theme_name)
void
paths_theme_create(struct wl_list *paths, const char *theme_name,
const char *filename)
{
static char buf[4096] = { 0 };
wl_list_init(paths);
struct ctx ctx = {
.build_path_fn = build_theme_path,
.filename = filename,
.buf = buf,
.len = sizeof(buf),
.dirs = theme_dirs,
.theme_name = theme_name
.theme_name = theme_name,
.list = paths,
};
return find_dir(&ctx);
find_dir(&ctx);
}
void
paths_destroy(struct wl_list *paths)
{
struct path *path, *next;
wl_list_for_each_safe(path, next, paths, link) {
free(path->string);
wl_list_remove(&path->link);
free(path);
}
}

View file

@ -14,6 +14,7 @@
#include <wlr/util/box.h>
#include <wlr/util/log.h>
#include "action.h"
#include "common/dir.h"
#include "common/list.h"
#include "common/macros.h"
#include "common/mem.h"
@ -1473,69 +1474,68 @@ validate(void)
validate_actions();
}
static void
rcxml_path(char *buf, size_t len)
{
if (!rc.config_dir) {
return;
}
snprintf(buf, len, "%s/rc.xml", rc.config_dir);
}
static void
find_config_file(char *buffer, size_t len, const char *filename)
{
if (filename) {
snprintf(buffer, len, "%s", filename);
return;
}
rcxml_path(buffer, len);
}
void
rcxml_read(const char *filename)
{
FILE *stream;
char *line = NULL;
size_t len = 0;
struct buf b;
static char rcxml[4096] = {0};
rcxml_init();
/*
* rcxml_read() can be called multiple times, but we only set rcxml[]
* the first time. The specified 'filename' is only respected the first
* time.
*/
if (rcxml[0] == '\0') {
find_config_file(rcxml, sizeof(rcxml), filename);
}
if (rcxml[0] == '\0') {
wlr_log(WLR_INFO, "cannot find rc.xml config file");
goto no_config;
struct wl_list paths;
if (filename) {
/* Honour command line argument -c <filename> */
wl_list_init(&paths);
struct path *path = znew(*path);
path->string = xstrdup(filename);
wl_list_append(&paths, &path->link);
} else {
paths_config_create(&paths, "rc.xml");
}
/* Reading file into buffer before parsing - better for unit tests */
stream = fopen(rcxml, "r");
if (!stream) {
wlr_log(WLR_ERROR, "cannot read (%s)", rcxml);
goto no_config;
}
wlr_log(WLR_INFO, "read config file %s", rcxml);
buf_init(&b);
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
if (p) {
*p = '\0';
struct buf b;
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
/*
* This is the equivalent of a wl_list_for_each() which optionally
* iterates in reverse depending on 'should_merge_config'
*
* If not merging, we iterate forwards and break after the first
* iteration.
*
* If merging, we iterate backwards (least important XDG Base Dir first)
* and keep going.
*/
for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
FILE *stream = fopen(path->string, "r");
if (!stream) {
continue;
}
buf_add(&b, line);
}
free(line);
fclose(stream);
rcxml_parse_xml(&b);
free(b.buf);
no_config:
wlr_log(WLR_INFO, "read config file %s", path->string);
buf_init(&b);
char *line = NULL;
size_t len = 0;
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
if (p) {
*p = '\0';
}
buf_add(&b, line);
}
zfree(line);
fclose(stream);
rcxml_parse_xml(&b);
zfree(b.buf);
if (!should_merge_config) {
break;
}
};
paths_destroy(&paths);
post_processing();
validate();
}

View file

@ -7,10 +7,12 @@
#include <sys/stat.h>
#include <wlr/util/log.h>
#include "common/buf.h"
#include "common/dir.h"
#include "common/file-helpers.h"
#include "common/spawn.h"
#include "common/string-helpers.h"
#include "config/session.h"
#include "labwc.h"
static bool
string_empty(const char *s)
@ -45,14 +47,15 @@ error:
free(value.buf);
}
static void
/* return true on successful read */
static bool
read_environment_file(const char *filename)
{
char *line = NULL;
size_t len = 0;
FILE *stream = fopen(filename, "r");
if (!stream) {
return;
return false;
}
wlr_log(WLR_INFO, "read environment file %s", filename);
while (getline(&line, &len, stream) != -1) {
@ -64,15 +67,7 @@ read_environment_file(const char *filename)
}
free(line);
fclose(stream);
}
static char *
build_path(const char *dir, const char *filename)
{
if (string_empty(dir) || string_empty(filename)) {
return NULL;
}
return strdup_printf("%s/%s", dir, filename);
return true;
}
static void
@ -96,7 +91,7 @@ update_activation_env(const char *env_keys)
}
void
session_environment_init(const char *dir)
session_environment_init(void)
{
/*
* Set default for XDG_CURRENT_DESKTOP so xdg-desktop-portal-wlr is happy.
@ -114,32 +109,49 @@ session_environment_init(const char *dir)
*/
setenv("_JAVA_AWT_WM_NONREPARENTING", "1", 0);
char *environment = build_path(dir, "environment");
if (!environment) {
return;
struct wl_list paths;
paths_config_create(&paths, "environment");
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
bool success = read_environment_file(path->string);
if (success && !should_merge_config) {
break;
}
}
read_environment_file(environment);
free(environment);
paths_destroy(&paths);
}
void
session_autostart_init(const char *dir)
session_autostart_init(void)
{
/* Update dbus and systemd user environment, each may fail gracefully */
update_activation_env("DISPLAY WAYLAND_DISPLAY XDG_CURRENT_DESKTOP");
char *autostart = build_path(dir, "autostart");
if (!autostart) {
return;
struct wl_list paths;
paths_config_create(&paths, "autostart");
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
if (!file_exists(path->string)) {
continue;
}
wlr_log(WLR_INFO, "run autostart file %s", path->string);
char *cmd = strdup_printf("sh %s", path->string);
spawn_async_no_shell(cmd);
free(cmd);
if (!should_merge_config) {
break;
}
}
if (!file_exists(autostart)) {
wlr_log(WLR_ERROR, "no autostart file");
goto out;
}
wlr_log(WLR_INFO, "run autostart file %s", autostart);
char *cmd = strdup_printf("sh %s", autostart);
spawn_async_no_shell(cmd);
free(cmd);
out:
free(autostart);
paths_destroy(&paths);
}

View file

@ -21,6 +21,7 @@ static const struct option long_options[] = {
{"debug", no_argument, NULL, 'd'},
{"exit", no_argument, NULL, 'e'},
{"help", no_argument, NULL, 'h'},
{"merge-config", no_argument, NULL, 'm'},
{"reconfigure", no_argument, NULL, 'r'},
{"startup", required_argument, NULL, 's'},
{"version", no_argument, NULL, 'v'},
@ -35,6 +36,7 @@ static const char labwc_usage[] =
" -d, --debug Enable full logging, including debug information\n"
" -e, --exit Exit the compositor\n"
" -h, --help Show help message and quit\n"
" -m, --merge-config Merge user config files/theme in all XDG Base Dirs\n"
" -r, --reconfigure Reload the compositor configuration\n"
" -s, --startup <command> Run command on startup\n"
" -v, --version Show version number and quit\n"
@ -91,7 +93,7 @@ main(int argc, char *argv[])
int c;
while (1) {
int index = 0;
c = getopt_long(argc, argv, "c:C:dehrs:vV", long_options, &index);
c = getopt_long(argc, argv, "c:C:dehmrs:vV", long_options, &index);
if (c == -1) {
break;
}
@ -100,7 +102,7 @@ main(int argc, char *argv[])
config_file = optarg;
break;
case 'C':
rc.config_dir = xstrdup(optarg);
rc.config_dir = optarg;
break;
case 'd':
verbosity = WLR_DEBUG;
@ -108,6 +110,9 @@ main(int argc, char *argv[])
case 'e':
send_signal_to_labwc_pid(SIGTERM);
exit(0);
case 'm':
rc.merge_config = true;
break;
case 'r':
send_signal_to_labwc_pid(SIGHUP);
exit(0);
@ -133,11 +138,7 @@ main(int argc, char *argv[])
die_on_detecting_suid();
if (!rc.config_dir) {
rc.config_dir = config_dir();
}
wlr_log(WLR_INFO, "using config dir (%s)\n", rc.config_dir);
session_environment_init(rc.config_dir);
session_environment_init();
rcxml_read(config_file);
/*
@ -171,7 +172,7 @@ main(int argc, char *argv[])
menu_init(&server);
session_autostart_init(rc.config_dir);
session_autostart_init();
if (startup_cmd) {
spawn_async_no_shell(startup_cmd);
}

View file

@ -11,6 +11,7 @@
#include <wlr/util/log.h>
#include "action.h"
#include "common/buf.h"
#include "common/dir.h"
#include "common/font.h"
#include "common/list.h"
#include "common/match.h"
@ -548,21 +549,27 @@ err:
static void
parse_xml(const char *filename, struct server *server)
{
static char buf[4096] = { 0 };
struct wl_list paths;
paths_config_create(&paths, filename);
if (!rc.config_dir) {
return;
}
snprintf(buf, sizeof(buf), "%s/%s", rc.config_dir, filename);
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
FILE *stream = fopen(buf, "r");
if (!stream) {
wlr_log(WLR_ERROR, "cannot read %s", buf);
return;
for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
FILE *stream = fopen(path->string, "r");
if (!stream) {
return;
}
wlr_log(WLR_INFO, "read menu file %s", path->string);
parse(server, stream);
fclose(stream);
if (!should_merge_config) {
break;
}
}
wlr_log(WLR_INFO, "read menu file %s", buf);
parse(server, stream);
fclose(stream);
paths_destroy(&paths);
}
static int

View file

@ -67,7 +67,7 @@ reload_config_and_theme(void)
static int
handle_sighup(int signal, void *data)
{
session_environment_init(rc.config_dir);
session_environment_init();
reload_config_and_theme();
return 0;
}

View file

@ -23,6 +23,7 @@
#include "common/font.h"
#include "common/graphic-helpers.h"
#include "common/match.h"
#include "common/mem.h"
#include "common/string-helpers.h"
#include "config/rcxml.h"
#include "button/button-png.h"
@ -664,60 +665,36 @@ process_line(struct theme *theme, char *line)
}
static void
theme_read(struct theme *theme, const char *theme_name)
theme_read(struct theme *theme, struct wl_list *paths)
{
FILE *stream = NULL;
char *line = NULL;
size_t len = 0;
char themerc[4096];
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
if (strlen(theme_dir(theme_name))) {
snprintf(themerc, sizeof(themerc), "%s/themerc",
theme_dir(theme_name));
stream = fopen(themerc, "r");
}
if (!stream) {
if (theme_name) {
wlr_log(WLR_INFO, "cannot find theme %s", theme_name);
for (struct wl_list *elm = iter(paths); elm != paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
FILE *stream = fopen(path->string, "r");
if (!stream) {
continue;
}
return;
}
wlr_log(WLR_INFO, "read theme %s", themerc);
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
if (p) {
*p = '\0';
wlr_log(WLR_INFO, "read theme %s", path->string);
char *line = NULL;
size_t len = 0;
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
if (p) {
*p = '\0';
}
process_line(theme, line);
}
process_line(theme, line);
}
free(line);
fclose(stream);
}
static void
theme_read_override(struct theme *theme)
{
char f[4096] = { 0 };
snprintf(f, sizeof(f), "%s/themerc-override", rc.config_dir);
FILE *stream = fopen(f, "r");
if (!stream) {
wlr_log(WLR_INFO, "no theme override '%s'", f);
return;
}
wlr_log(WLR_INFO, "read theme-override %s", f);
char *line = NULL;
size_t len = 0;
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
if (p) {
*p = '\0';
zfree(line);
fclose(stream);
if (!should_merge_config) {
break;
}
process_line(theme, line);
}
free(line);
fclose(stream);
}
struct rounded_corner_ctx {
@ -1001,10 +978,15 @@ theme_init(struct theme *theme, const char *theme_name)
theme_builtin(theme);
/* Read <data-dir>/share/themes/$theme_name/openbox-3/themerc */
theme_read(theme, theme_name);
struct wl_list paths;
paths_theme_create(&paths, theme_name, "themerc");
theme_read(theme, &paths);
paths_destroy(&paths);
/* Read <config-dir>/labwc/themerc-override */
theme_read_override(theme);
paths_config_create(&paths, "themerc-override");
theme_read(theme, &paths);
paths_destroy(&paths);
post_processing(theme);
create_corners(theme);