diff options
| author | user <user@clank> | 2026-09-22 12:42:15 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 12:42:15 +0200 |
| commit | faa3fb6ee575a900abc574b0f9ed5ff291761837 (patch) | |
| tree | ce644998c144687e205d1f4e97e6f999de98723b /src | |
| parent | tabs: idle tabs you can actually read across the room (diff) | |
| download | slosh-faa3fb6ee575a900abc574b0f9ed5ff291761837.tar.gz slosh-faa3fb6ee575a900abc574b0f9ed5ff291761837.tar.xz slosh-faa3fb6ee575a900abc574b0f9ed5ff291761837.zip | |
theme: a name and a directory, not sixty colours in your config
Wearing a theme meant pasting its `theme { }` block into config.kdl, or
including a path to a file you had copied there yourself. Both make the
palette a literal in your config, and a literal is the one thing you cannot
change cheaply: not from a script, not from a running session, not by a list
of what is available, and never again from upstream -- the colours you pasted
are the colours that theme had the day you pasted them.
So a theme is a file in a directory now, and a config names it:
theme_name "phosphor"
theme { frame_focus "#00ff88" } // ...but that one colour is mine
theme_name is applied after the includes and before everything the file says
itself, which is the rule includes already follow and for the same reason: the
loader reads a document by asking it for names rather than walking it in order,
so "here" is not a position it could honour, and "what you asked for is the
base, what you wrote is yours" is the rule worth having.
Two directories, yours first: `themes/` beside the root config -- beside the
file that was loaded, so a config somewhere else (SLOSH_CONFIG, or one handed
to --check) takes its themes with it -- and then <prefix>/share/slosh/themes,
compiled in from PREFIX. install(1) has been putting themes there all along
and nothing could read them; now a shipped theme is nameable without being
copied, and a file of yours with the same name shadows it, which is how a
root-owned theme gets edited.
A name, not a path. The directory is what makes a theme switchable, listable
and shadowable, and a path silently does none of those, so `theme_name
"../x.kdl"` is refused with a line pointing at `include`, which still works
and is still the answer for a file somewhere a directory of names cannot
reach. A theme that is not there is one complaint and no more (D9): the rest
of the config applies, because losing your keybindings over a mistyped colour
scheme would be the wrong trade. A theme file is read by the same loader, so
it is watched like any other: saving one reloads the session wearing it, and
the derivations (tab_status_fg, tab_idle_bg) run over the theme's own colours
exactly as before.
Switching without editing anything, over the socket a session already has:
slosh -s main cmd theme # amber default* ...
slosh -s main cmd '{"cmd":"theme","name":"amber"}' # now
slosh -s main cmd '{"cmd":"theme","name":"amber","save":true}'
The session holds the name over later reloads, so saving your config for an
unrelated reason does not put the old colours back; `save` writes the
theme_name line into the file, replacing the one already there and leaving
every other line and comment alone -- dumping the whole config back over
itself would reformat a hand-written file and drop its comments, which is not
a fair price for changing a colour scheme. A name is tried on a throwaway
config first, so a typo costs nothing. An empty name gives the answer back to
the file.
--dump-config stops writing colours: it writes the theme's name and the
colours a config overrode by hand, so a dumped config keeps following its
theme instead of freezing it. --dump-theme is the other half and is new: the
resolved palette as a theme file, which is how you start one from the session
in front of you rather than from an empty file. It is also what keeps
contrib/themes/default.kdl honest -- that file is now the annotated reference
(the commented palette moved there out of config.kdl, where it was 119 lines
of colour in the middle of the prose), and a test compares it against the
compiled-in one.
While in there: tests/test_themes.py's "every theme sets every colour" guard
had been grepping for `&c->`, the spelling before the colour table moved to
offsetof, so it had been finding zero colours and passing for months. Fixed,
with the derived and opt-in colours exempted for the reasons they are derived
and opt-in, and with a check that the guard found a table at all.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 66 | ||||
| -rw-r--r-- | src/cmd.c | 69 | ||||
| -rw-r--r-- | src/config.c | 335 | ||||
| -rw-r--r-- | src/main.c | 11 |
4 files changed, 477 insertions, 4 deletions
@@ -63,6 +63,12 @@ const char *app_config_complaint(void) { return CFG_COMPLAINT; } +/* A theme this session was told to wear, which outranks the config file's own + * answer until it is cleared. Held here rather than in the config because a + * reload builds a whole new config from disk: anything that has to survive + * that cannot live inside the thing being replaced. */ +static char THEME_OVERRIDE[THEME_NAME_MAX]; + bool app_reload_config(char *err, size_t errcap) { config_t fresh; config_defaults(&fresh); @@ -71,6 +77,17 @@ bool app_reload_config(char *err, size_t errcap) { config_free(&fresh); return false; /* keep what works */ } + /* After the file, over whatever it asked for: a session that was switched + * to a theme stays switched when the config is saved. A name that has gone + * missing since (the file was deleted, the directory moved) complains like + * any other bad line and leaves the file's own colours standing -- it must + * not cost you the reload. */ + if (THEME_OVERRIDE[0]) { + char terr[192] = {0}; + if (!config_apply_theme(&fresh, THEME_OVERRIDE, terr, sizeof terr) && err && + errcap && !err[0]) + snprintf(err, errcap, "%s", terr[0] ? terr : "cannot apply that theme"); + } config_free(&CFG); CFG = fresh; CFG_LOADED = true; @@ -79,6 +96,55 @@ bool app_reload_config(char *err, size_t errcap) { return true; } +const char *app_theme(void) { + ensure_config(); + return CFG.theme_name ? CFG.theme_name : ""; +} + +size_t app_themes(char (*out)[THEME_NAME_MAX], size_t max) { + ensure_config(); + return config_themes(&CFG, out, max); +} + +size_t app_theme_dirs(char (*out)[512], size_t max) { + ensure_config(); + return config_theme_dirs(&CFG, out, max); +} + +bool app_set_theme(const char *name, bool save, char *err, size_t errcap) { + ensure_config(); + if (err && errcap) err[0] = 0; + + /* Nothing named: forget the session's answer and take the file's, which is + * the only way back to a config that names no theme at all. */ + if (!name || !*name) { + THEME_OVERRIDE[0] = 0; + return app_reload_config(err, errcap); + } + + /* Tried on a throwaway config first: a name that is not there must not cost + * the session the colours it is wearing. */ + config_t probe; + config_defaults(&probe); + bool ok = config_apply_theme(&probe, name, err, errcap); + config_free(&probe); + if (!ok) return false; + + if (save && + !config_write_theme_name(config_default_path(), name, err, errcap)) + return false; + + snprintf(THEME_OVERRIDE, sizeof THEME_OVERRIDE, "%s", name); + char rerr[192] = {0}; + if (!app_reload_config(rerr, sizeof rerr)) { + /* The config file is broken, so a reload cannot be how the theme lands -- + * but the theme itself was fine, and refusing the switch over somebody + * else's syntax error would be the wrong half to give up. */ + if (!config_apply_theme(&CFG, name, err, errcap)) return false; + } + return true; +} + /* Which files the config in force was read from. The server watches every one * of them, so a theme that can be included is a theme that reloads when you * save it — asked of the app rather than read from a config the front ends do @@ -501,6 +501,48 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, } return jok_int(NULL, 0); } + /* Colour, by name. Without one it answers what there is to pick from, which + * is the question a picker (or a person) has first; with one it switches the + * session now. `save` writes the name into the config as well, so the same + * verb covers "let me see it" and "keep it". */ + if (strcmp(cmd, "theme") == 0) { + const char *name = jv_gets(req, "name", NULL); + bool save = jv_getb(req, "save", false); + char err[192] = {0}; + if (name && *name && !app_set_theme(name, save, err, sizeof err)) + return jerr(err[0] ? err : "cannot use that theme"); + if (name && !*name && !app_set_theme("", false, err, sizeof err)) + return jerr(err[0] ? err : "cannot reload"); + if (name) { + app_resize(a, s->cols, s->rows); /* a theme may move the geometry */ + s->force_full = true; + char msg[128]; + snprintf(msg, sizeof msg, "theme: %s%s", + *app_theme() ? app_theme() : "the config's own", + save ? " (saved)" : ""); + app_toast(a, msg); + } + char names[64][64]; + size_t n = app_themes(names, 64); + char dirs[4][512]; + size_t nd = app_theme_dirs(dirs, 4); + json_t j; + json_init(&j); + json_obj_open(&j, NULL); + json_bool(&j, "ok", true); + json_str(&j, "theme", app_theme(), strlen(app_theme())); + if (name) json_bool(&j, "saved", save); + json_arr_open(&j, "themes"); + for (size_t i = 0; i < n; i++) + json_str(&j, NULL, names[i], strlen(names[i])); + json_arr_close(&j); + json_arr_open(&j, "dirs"); + for (size_t i = 0; i < nd; i++) + json_str(&j, NULL, dirs[i], strlen(dirs[i])); + json_arr_close(&j); + json_obj_close(&j); + return j.buf; + } if (strcmp(cmd, "alive") == 0) { json_t j; json_init(&j); @@ -644,6 +686,33 @@ char *cmd_exec(app_t *a, screen_t *s, input_parser_t *in, const char *line, s->force_full = true; return strdup(ok ? "ok" : err); } + /* `theme` lists, `theme phosphor` switches. The bare form has no room for + * `save`, which is the right way round: the driver is for trying things. */ + if (strcmp(verb, "theme") == 0) { + char err[192] = {0}; + if (*arg && !app_set_theme(arg, false, err, sizeof err)) + return strdup(err[0] ? err : "cannot use that theme"); + if (*arg) { + app_resize(a, s->cols, s->rows); + s->force_full = true; + return strdup(app_theme()); + } + char names[64][64]; + size_t n = app_themes(names, 64); + /* One line, because a bare verb's answer is one line -- the driver reads + * replies by line and a list down the page would put it out of step. The + * one in force is marked, so "which am I wearing" needs no second call. */ + char out[64 * 68]; + size_t used = 0; + for (size_t i = 0; i < n && used + 68 < sizeof out; i++) { + int wrote = + snprintf(out + used, sizeof out - used, "%s%s%s", used ? " " : "", + names[i], strcmp(names[i], app_theme()) == 0 ? "*" : ""); + if (wrote > 0) used += (size_t)wrote; + } + out[used] = 0; + return strdup(out); + } if (strcmp(verb, "alive") == 0) { return strdup(app_should_quit(a) ? "false" : "true"); } diff --git a/src/config.c b/src/config.c index d2b89b7..0943935 100644 --- a/src/config.c +++ b/src/config.c @@ -2,14 +2,17 @@ #include "config.h" #include <ghostty/vt.h> +#include <dirent.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <stddef.h> #include <string.h> +#include <unistd.h> #include "input.h" #include "kdl.h" +#include "slosh.h" static const struct { const char *name; @@ -1266,6 +1269,8 @@ void config_free(config_t *c) { free(c->binds); free(c->shell); free(c->editor); + free(c->theme_dir); + free(c->theme_name); free(c->project_layout); for (size_t i = 0; i < c->nproject_roots; i++) free(c->project_roots[i]); for (size_t i = 0; i < c->nfiles; i++) free(c->files[i]); @@ -1814,10 +1819,39 @@ char *config_render(const config_t *c) { else cb_add(&b, "// project_layout \"~/.config/slosh/project.layout\"\n"); - cb_add(&b, "\n// ---- colour ----\ntheme {\n"); + /* Colour is a name and a short list of exceptions, never sixty resolved + * values. Writing the whole palette out would pin the theme as it looked at + * the moment of the dump: the file would keep working and every later + * change to that theme -- including one you make yourself -- would silently + * stop arriving. So the dump says which theme, and then only what this + * config said for itself. */ + cb_add(&b, "\n// ---- colour ----\n"); + if (c->theme_dir) + cb_qstr(&b, "theme_dir", c->theme_dir, "where named themes live"); + else + cb_add(&b, + "// theme_dir \"~/.config/slosh/themes\" // unset: themes/ beside " + "this file,\n// then the " + "installed ones\n"); + if (c->theme_name) + cb_qstr(&b, "theme_name", c->theme_name, "a file in there, without .kdl"); + else + cb_add(&b, + "// theme_name \"phosphor\" // unset: the compiled-in palette\n"); + size_t nset = 0; for (size_t i = 0; i < sizeof THEME_COLORS / sizeof *THEME_COLORS; i++) - cb_color(&b, THEME_COLORS[i].name, *THEME_COLOR((config_t *)c, i)); - cb_add(&b, "}\n"); + if (c->theme_set[i]) nset++; + if (nset) { + cb_add(&b, "// ...and the colours this config names itself, which beat " + "the theme's:\ntheme {\n"); + for (size_t i = 0; i < sizeof THEME_COLORS / sizeof *THEME_COLORS; i++) + if (c->theme_set[i]) + cb_color(&b, THEME_COLORS[i].name, *THEME_COLOR((config_t *)c, i)); + cb_add(&b, "}\n"); + } else { + cb_add(&b, "// theme { frame_focus \"#00ff88\" } // ...and the colours " + "you want anyway\n"); + } /* Written even when empty: an empty block says "this exists and you have * none", where nothing at all says "we forgot to tell you". Chrome passes @@ -1912,6 +1946,41 @@ char *config_dump_defaults(void) { return text; } +/* The palette itself, as a theme file. + * + * The config dump stopped writing sixty colours when themes got names -- it + * writes the name -- so this is where "what exactly am I wearing" is answered: + * the resolved colours, derivations included, in the one format a theme is + * written in. `slosh --dump-theme > ~/.config/slosh/themes/mine.kdl` is how a + * theme is started from the one in front of you rather than from nothing. */ +char *config_render_theme(const config_t *c) { + cfgbuf_t b = {0}; + cb_add(&b, "// A slosh theme: every colour the compositor draws, as it\n"); + cb_add(&b, "// currently stands. Written by `slosh --dump-theme`.\n"); + cb_add(&b, "//\n"); + cb_add(&b, "// Drop it in your theme directory and name it with\n"); + cb_add(&b, "// `theme_name`. What each colour is for is written out in\n"); + cb_add(&b, "// the shipped `default` theme.\n"); + if (c->theme_name) cb_add(&b, "// Started from: %s\n", c->theme_name); + cb_add(&b, "theme {\n"); + for (size_t i = 0; i < sizeof THEME_COLORS / sizeof *THEME_COLORS; i++) + cb_color(&b, THEME_COLORS[i].name, *THEME_COLOR((config_t *)c, i)); + cb_add(&b, "}\n"); + return b.buf ? b.buf : strdup(""); +} + +char *config_dump_theme(void) { + config_t fresh; + config_defaults(&fresh); + /* The config's own answer, not the compiled one: dumping a theme is asking + * what this session looks like, and a session reads a config. A broken file + * leaves the defaults standing, which is what the session would wear too. */ + config_load(&fresh, config_default_path(), NULL, 0); + char *text = config_render_theme(&fresh); + config_free(&fresh); + return text; +} + action_t config_lookup(const config_t *c, int key, uint16_t mods) { /* caps/num lock must not make a binding stop working */ mods &= (uint16_t)(MOD_SHIFT | MOD_CTRL | MOD_ALT | MOD_SUPER); @@ -1950,6 +2019,95 @@ static action_t action_by_name(const char *name) { return ACT_NONE; } +/* Defined with the rest of the theme lookup, below: what a theme may be + * called is one rule, and the writer has to refuse exactly what the loader + * would refuse. */ +static bool theme_name_ok(const char *name); + +/* Keep a theme switch past a restart, by editing the one line that says which + * theme it is. + * + * A line rather than a rewrite: dumping the whole config back over itself + * would reformat a file somebody wrote by hand and drop every comment in it, + * which is not a fair price for changing a colour scheme. So the existing + * `theme_name` line is replaced in place where there is one, and appended + * where there is not -- and a config that does not exist yet becomes a + * one-line one, since that is exactly what was asked for. */ +bool config_write_theme_name(const char *path, const char *name, char *err, + size_t errcap) { + if (err && errcap) err[0] = 0; + if (!theme_name_ok(name)) { + if (err && errcap) snprintf(err, errcap, "not a theme name: %s", name); + return false; + } + + char *text = NULL; + size_t len = 0; + FILE *f = fopen(path, "rb"); + if (f) { + fseek(f, 0, SEEK_END); + long size = ftell(f); + if (size < 0) size = 0; + fseek(f, 0, SEEK_SET); + text = malloc((size_t)size + 1); + if (!text) { + fclose(f); + if (err && errcap) snprintf(err, errcap, "out of memory"); + return false; + } + len = fread(text, 1, (size_t)size, f); + text[len] = 0; + fclose(f); + } + + char dir[1024]; + path_mkdirs(path_dir(path, dir, sizeof dir)); + /* Written beside the original and renamed over it: a config half-written by + * a crash is a session that will not start. */ + char tmp[1100]; + snprintf(tmp, sizeof tmp, "%s.new", path); + FILE *out = fopen(tmp, "wb"); + if (!out) { + free(text); + if (err && errcap) snprintf(err, errcap, "cannot write %s", path); + return false; + } + + bool replaced = false; + for (char *line = text; line && *line;) { + char *nl = strchr(line, '\n'); + size_t n = nl ? (size_t)(nl - line) : strlen(line); + const char *p = line; + while (p < line + n && (*p == ' ' || *p == '\t')) p++; + /* The line this owns, and only at the top level -- a `theme_name` inside + * a block is not this setting, and indentation is the only thing that + * tells them apart without parsing the file again. */ + bool mine = !replaced && p == line && strncmp(p, "theme_name", 10) == 0 && + (p[10] == ' ' || p[10] == '\t' || p[10] == '"'); + if (mine) { + fprintf(out, "theme_name \"%s\"\n", name); + replaced = true; + } else { + fwrite(line, 1, n, out); + fputc('\n', out); + } + if (!nl) break; + line = nl + 1; + } + if (!replaced) { + if (len && text[len - 1] != '\n') fputc('\n', out); + fprintf(out, "theme_name \"%s\"\n", name); + } + free(text); + bool ok = fclose(out) == 0; + if (ok) ok = rename(tmp, path) == 0; + if (!ok) { + remove(tmp); + if (err && errcap) snprintf(err, errcap, "cannot write %s", path); + } + return ok; +} + const char *config_default_path(void) { static char path[512]; const char *explicit_ = getenv("SLOSH_CONFIG"); @@ -2037,6 +2195,8 @@ static const char *const KNOWN_TOP[] = { "tab_bar_width", "tab_gap", "theme", + "theme_dir", + "theme_name", "title_align", "title_inset", "toast_ms", @@ -2091,6 +2251,144 @@ static void include_path(const char *base_file, const char *ref, char *out, snprintf(out, cap, "%s/%s", path_dir(base_file, dir, sizeof dir), r); } +/* The directories a named theme is looked for in, in order. + * + * `themes/` beside the config first -- beside the *root* config, not beside + * whichever file named the theme, because it is where a person keeps their + * themes and a config elsewhere (SLOSH_CONFIG, or a file handed to --check) + * takes its themes with it rather than borrowing yours. Then the + * ones `make install` wrote to `<prefix>/share/slosh/themes`, if this build + * knows where that is: a theme slosh ships with should be nameable without + * being copied anywhere first, and a theme of yours with the same name wins + * by being looked at first. `theme_dir` replaces the first of the two, not + * both, so naming your own directory never hides the shipped set. */ +size_t config_theme_dirs(const config_t *c, char (*out)[512], size_t max) { + size_t n = 0; + if (n < max) { + if (c && c->theme_dir && *c->theme_dir) { + snprintf(out[n++], 512, "%s", c->theme_dir); + } else { + /* files[0] is the file this config was loaded from -- remembered before + * it was parsed, so it is right even for a file that turned out not to + * exist. Only a config nobody has loaded falls back to where one would + * have been. */ + const char *root = c && c->nfiles ? c->files[0] : config_default_path(); + char dir[512]; + snprintf(out[n++], 512, "%s/themes", path_dir(root, dir, sizeof dir)); + } + } +#ifdef SLOSH_DATADIR + if (n < max) snprintf(out[n++], 512, "%s/themes", SLOSH_DATADIR); +#endif + return n; +} + +/* A theme's file, or false with the places that were looked in. */ +static bool theme_path(const config_t *c, const char *name, char *out, + size_t cap, char *tried, size_t triedcap) { + char dirs[4][512]; + size_t nd = config_theme_dirs(c, dirs, 4); + if (tried && triedcap) tried[0] = 0; + for (size_t i = 0; i < nd; i++) { + char path[1024]; + snprintf(path, sizeof path, "%s/%s.kdl", dirs[i], name); + if (access(path, R_OK) == 0) { + snprintf(out, cap, "%s", path); + return true; + } + if (tried && triedcap) { + size_t used = strlen(tried); + snprintf(tried + used, triedcap - used, "%s%s", used ? ", " : "", + dirs[i]); + } + } + return false; +} + +/* A theme is named, never spelled as a path: `theme_name "phosphor"`, not + * `theme_name "../../etc/x.kdl"`. The directory is the whole point -- it is + * what makes a theme switchable at runtime, listable in a picker, and + * shadowable by one of yours -- and a path silently does none of that. A file + * somewhere else is what `include` has always been for, and the complaint + * says so rather than half-honouring it. */ +static bool theme_name_ok(const char *name) { + if (!name || !*name || strlen(name) >= THEME_NAME_MAX) return false; + for (const char *p = name; *p; p++) + if (*p == '/' || *p == '\\' || *p == ':' || *p < 0x20) return false; + return strcmp(name, ".") != 0 && strcmp(name, "..") != 0; +} + +static int by_theme_name(const void *a, const void *b) { + return strcmp((const char *)a, (const char *)b); +} + +size_t config_themes(const config_t *c, char (*out)[THEME_NAME_MAX], + size_t max) { + char dirs[4][512]; + size_t nd = config_theme_dirs(c, dirs, 4), n = 0; + for (size_t i = 0; i < nd && n < max; i++) { + DIR *d = opendir(dirs[i]); + if (!d) continue; /* a directory nobody made is the normal case */ + struct dirent *e; + while ((e = readdir(d)) && n < max) { + size_t len = strlen(e->d_name); + if (len < 5 || strcmp(e->d_name + len - 4, ".kdl") != 0) continue; + if (e->d_name[0] == '.' || len - 4 >= THEME_NAME_MAX) continue; + char name[THEME_NAME_MAX]; + snprintf(name, sizeof name, "%.*s", (int)(len - 4), e->d_name); + bool seen = false; /* the first directory to offer a name owns it */ + for (size_t j = 0; j < n && !seen; j++) seen = strcmp(out[j], name) == 0; + if (!seen) snprintf(out[n++], THEME_NAME_MAX, "%s", name); + } + closedir(d); + } + qsort(out, n, THEME_NAME_MAX, by_theme_name); + return n; +} + +/* Read the theme the file asked for, before the file's own settings. + * + * `in_theme` is saved and restored rather than simply cleared, because a theme + * may name a theme: the flags have to keep meaning "somebody outside the theme + * chain said this" however deep the chain goes. */ +static void apply_theme_name(config_t *c, const char *name, int line, int depth, + char *err, size_t errcap) { + if (!theme_name_ok(name)) { + complain(c, err, errcap, line, + "theme_name is a name, not a path: %s (use include for a file " + "elsewhere)", + name); + return; + } + char path[1024], tried[512]; + if (!theme_path(c, name, path, sizeof path, tried, sizeof tried)) { + complain(c, err, errcap, line, "no theme named %s in %s", name, tried); + return; + } + free(c->theme_name); + c->theme_name = strdup(name); + bool was = c->in_theme; + c->in_theme = true; + char terr[256] = {0}; + if (!load_into(c, path, depth + 1, terr, sizeof terr) && !terr[0]) + snprintf(terr, sizeof terr, "cannot read it"); + c->in_theme = was; + if (terr[0] && err && errcap && !err[0]) snprintf(err, errcap, "%s", terr); +} + +bool config_apply_theme(config_t *c, const char *name, char *err, + size_t errcap) { + if (err && errcap) err[0] = 0; + const char *was = c->loading; + c->loading = NULL; + apply_theme_name(c, name, 0, 0, err, errcap); + c->loading = was; + /* Named it and got it: the name is on the config, which is the only signal + * that cannot lie about whether the file was there. */ + return c->theme_name && strcmp(c->theme_name, name) == 0 && + (!err || !errcap || !err[0]); +} + /* Every `include` at the top level of this file, in order, applied *before* the * file's own settings — wherever the line happens to sit. The loader reads a * document by asking it for the keys it knows rather than walking it in order, @@ -2190,6 +2488,31 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, /* After the includes, because each of those set it to its own file while it * was being read. From here the complaints belong to this file. */ c->loading = path; + /* The named theme, after the includes and before everything this file says + * itself -- the same rule includes follow, for the same reason: the loader + * reads a document by asking it for names rather than walking it in order, + * so "here" is not a position it could honour, and "what you asked for is + * the base, what you wrote is yours" is the rule worth having. A theme + * beats an include because it is the more specific request. */ + { + const kdl_node_t *dn = kdl_child(root, "theme_dir"); + const char *dir = kdl_arg(dn, 0, NULL); + if (dir) { + /* Resolved against the file that said it, like an include's path: a + * config is a thing on disk that refers to its neighbours. */ + char resolved[512]; + include_path(path, dir, resolved, sizeof resolved); + free(c->theme_dir); + c->theme_dir = strdup(resolved); + } + const kdl_node_t *tn = kdl_child(root, "theme_name"); + const char *name = kdl_arg(tn, 0, NULL); + if (tn && !name) + complain(c, err, errcap, tn->line, "theme_name needs a name"); + else if (name) + apply_theme_name(c, name, tn->line, depth, err, errcap); + c->loading = path; /* the theme file borrowed it while it was read */ + } /* ...which includes "I have no idea what this document is", so it has to be * said after the file is known and not before. */ complain_unknown_top(c, root, err, errcap); @@ -2490,6 +2813,12 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, if (!parse_color(v, THEME_COLOR(c, i))) complain(c, err, errcap, kdl_child(theme, THEME_COLORS[i].name)->line, "bad colour for %s: %s", THEME_COLORS[i].name, v); + /* Not while reading the theme the config asked for: those colours are + * the theme's answer, and the dump wants to write the theme's *name* + * rather than its palette. A colour named anywhere else -- your config, + * a file it includes -- is yours, and survives the dump. */ + else if (!c->in_theme) + c->theme_set[i] = true; } /* A theme that restyles the chrome but has never heard of the sidebar's * status rows gets coherent ones anyway, mixed from two colours it does @@ -147,7 +147,8 @@ static int check_config(const char *path) { static void usage(void) { fputs("usage: slosh [-s NAME] [--layout FILE] [--no-reload]\n" - " [--version] [--dump-config] [--check [FILE]]\n" + " [--version] [--dump-config] [--dump-theme]\n" + " [--check [FILE]]\n" " [--install-terminfo] [ls | cmd LINE | -- CMD...]\n", stderr); } @@ -203,6 +204,14 @@ int main(int argc, char **argv) { fputs(text, stdout); free(text); return 0; + } else if (strcmp(a, "--dump-theme") == 0) { + /* The other half of --dump-config, which writes a theme's *name* rather + * than its colours: this writes the colours, so a theme can be started + * from the one in front of you instead of from a blank file. */ + char *text = config_dump_theme(); + fputs(text, stdout); + free(text); + return 0; } else if (strcmp(a, "--install-terminfo") == 0) { #ifdef _WIN32 fputs("slosh: terminfo is not a Windows concept; nothing to install\n", |
