aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.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/app.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/app.c')
-rw-r--r--src/app.c66
1 files changed, 66 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
index 74d49ff..c17831f 100644
--- a/src/app.c
+++ b/src/app.c
@@ -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