aboutsummaryrefslogtreecommitdiffstats
path: root/src/cmd.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 12:42:15 +0200
committeruser <user@clank>2026-09-22 12:42:15 +0200
commitfaa3fb6ee575a900abc574b0f9ed5ff291761837 (patch)
treece644998c144687e205d1f4e97e6f999de98723b /src/cmd.c
parenttabs: idle tabs you can actually read across the room (diff)
downloadslosh-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/cmd.c')
-rw-r--r--src/cmd.c69
1 files changed, 69 insertions, 0 deletions
diff --git a/src/cmd.c b/src/cmd.c
index 4be505d..14835af 100644
--- a/src/cmd.c
+++ b/src/cmd.c
@@ -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");
}