aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 18:40:42 +0200
committeruser <user@clank>2026-08-16 18:40:42 +0200
commit80b98bbf4c6ffe1a95c389525af30374df21d8eb (patch)
treec9ac16b33c85465db06cfbfea374ffb232fbab05 /src
parentsl0ppty: six chrome presets that need the new operators (diff)
downloadslosh-80b98bbf4c6ffe1a95c389525af30374df21d8eb.tar.gz
slosh-80b98bbf4c6ffe1a95c389525af30374df21d8eb.tar.xz
slosh-80b98bbf4c6ffe1a95c389525af30374df21d8eb.zip
sl0ppty: `:load` a preset, without a second KDL reader
chrome> :load sine-comet sine-comet.kdl: 1 chrome, 0 content chrome> :paste // sine-comet.kdl include "/home/user/dev/sl0ppty/contrib/chrome/sine-comet.kdl" A path, or the bare name of anything in `chrome/` or `shaders/` beside the script, from any directory. Entries land wherever their own `where=` says, so one file can fill both chains and a preset goes where its author put it. Tab completes filenames after `:load` and vocabulary everywhere else. **The prompt does not parse the file.** I started writing the scanner -- find the `shaders {` block, count braces, strip `//` outside quoted strings so that `color="#ff0033"` and `t / 8` survive -- and stopped, because that is a second reader of a format that already has one, and the two would disagree exactly where comments meet strings. Instead the session gets the path and reads it with the parser it already has: `config_parse_chain_file`, built on the same `parse_shader_entry` a config file goes through. Anything that is not a top-level `shaders { }` block is ignored, so a whole `config.kdl` can be handed over and only its shaders apply. The alternative was regenerating all 45 presets into something easier to read from Python. That would have cost them what makes them useful: each one is a config fragment today -- pasteable, `include`-able, checkable with `sl0ppty --check`, and loaded as a config by `tests/test_chrome_presets.py`. A format only the prompt can read would be a fourth thing to keep in step. `:paste` after a load answers with an `include` line rather than the file's thirty: the file is on disk, it is already the block, and reprinting it scrolled the pane you were looking at -- which is how I noticed. Both chains are built beside the live ones and swapped in together, so a file that goes wrong half way leaves the pane as it was rather than wearing the half that parsed. Each compiled expression is matched to the chain whose shader points at it before either is installed; freeing a program the other chain still uses is the one bug here that would not appear until the next repaint. Verified: nine checks in `tests/test_shader_repl.py` -- a preset by bare name, a file with both rects going to both chains, the three ways to be wrong (no such file, no `shaders { }` block, no argument), what `:paste` says afterwards, and tab completion. Then all 45 shipped presets loaded by name in one session: none refused.
Diffstat (limited to 'src')
-rw-r--r--src/app.c99
-rw-r--r--src/app.h10
-rw-r--r--src/config.c67
-rw-r--r--src/config.h13
4 files changed, 189 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
index 01a5536..d5c24c0 100644
--- a/src/app.c
+++ b/src/app.c
@@ -464,6 +464,28 @@ static void on_pane_shader(app_t *a, pane_t *p, const char *payload) {
pane_write(p, reply, strlen(reply));
}
+/* `shader-load;<path>`: the file's `shaders { }` block, on this pane. Answered with
+ * how much of it ran, because "ok" alone cannot tell a preset that filled both
+ * chains from a file whose entries were all dropped for one bad word. */
+static void on_pane_shader_load(app_t *a, pane_t *p, const char *payload) {
+ struct bypane b = {p, NULL};
+ walk_all(a, bypane_cb, &b);
+ if (!b.found) return;
+
+ char err[192] = {0}, reply[320];
+ size_t nchrome = 0, ncontent = 0;
+ bool ok = app_load_pane_shaders(a, b.found->id, payload, &nchrome, &ncontent,
+ err, sizeof err);
+ if (ok)
+ snprintf(reply, sizeof reply,
+ "\033]5577;1;shader-reply;ok;%zu chrome, %zu content\033\\", nchrome,
+ ncontent);
+ else
+ snprintf(reply, sizeof reply, "\033]5577;1;shader-reply;error;%s\033\\",
+ err[0] ? err : "refused");
+ pane_write(p, reply, strlen(reply));
+}
+
static void on_pane_osc(pane_t *p, const char *verb, const char *payload,
void *ud) {
app_t *a = ud;
@@ -471,6 +493,10 @@ static void on_pane_osc(pane_t *p, const char *verb, const char *payload,
on_pane_shader(a, p, payload);
return;
}
+ if (strcmp(verb, "shader-load") == 0) {
+ on_pane_shader_load(a, p, payload);
+ return;
+ }
if (strcmp(verb, "purpose") != 0) return;
struct bypane b = {p, NULL};
walk_all(a, bypane_cb, &b);
@@ -2895,6 +2921,79 @@ static void draw_collapsed(app_t *a, screen_t *s, node_t *n) {
* Refused, with a reason, when `in_band_shaders` is off -- a program restyling
* itself by accident is exactly what that decision was about, and the answer is
* a line of config rather than a rule nobody can lift. */
+/* A preset file, applied to this pane: both chains at once, routed by each entry's
+ * own `where=`. The session reads the file, because the session has the parser --
+ * a program that wants to prototype should not have to reimplement KDL, and a
+ * second reader of the format would be a second answer about what a file says.
+ *
+ * Gated like setting a chain by hand, and for the same reason: this is the program
+ * in the pane restyling the pane. Relative paths resolve against the *session's*
+ * working directory, which is why `contrib/shader-repl` sends an absolute one --
+ * the pane's own cwd is the pane's business and the session cannot see it. */
+bool app_load_pane_shaders(app_t *a, uint32_t pane_id, const char *path,
+ size_t *nchrome, size_t *ncontent, char *err,
+ size_t errcap) {
+ if (nchrome) *nchrome = 0;
+ if (ncontent) *ncontent = 0;
+ if (err && errcap) err[0] = 0;
+ if (!CFG.in_band_shaders) {
+ if (err && errcap) snprintf(err, errcap, "in_band_shaders is off");
+ return false;
+ }
+ node_t *n = pane_by_id(a, pane_id ? pane_id : app_focused_pane_id(a));
+ if (!n) {
+ if (err && errcap) snprintf(err, errcap, "no such pane");
+ return false;
+ }
+ if (!path || !path[0]) {
+ if (err && errcap) snprintf(err, errcap, "no path");
+ return false;
+ }
+
+ /* Both chains built beside the live ones and swapped in together, so a file
+ * that turns out to be unreadable half way through leaves the pane as it was
+ * rather than wearing the half that parsed. */
+ inband_chain_t con = {0}, chr = {0};
+ expr_prog_t *exprs[SHADE_MAX * 2];
+ size_t nexprs = 0;
+ size_t got = config_parse_chain_file(path, CFG.frame_focus, con.sh, &con.n,
+ chr.sh, &chr.n, exprs, &nexprs, err,
+ errcap);
+ if (!got) {
+ for (size_t i = 0; i < nexprs; i++) expr_free(exprs[i]);
+ if (err && errcap && !err[0]) snprintf(err, errcap, "nothing in it to run");
+ return false;
+ }
+
+ /* The programs belong to whichever chain holds the shader that points at one.
+ * Walking the two chains rather than trusting the order they came back in: a
+ * chain that frees a program another chain is still using is the one bug in
+ * this that would not show up until the next repaint. */
+ for (size_t i = 0; i < nexprs; i++) {
+ bool mine = false;
+ for (size_t j = 0; j < chr.n && !mine; j++)
+ if (chr.sh[j].amount_expr == exprs[i]) {
+ chr.exprs[chr.nexprs++] = exprs[i];
+ mine = true;
+ }
+ for (size_t j = 0; j < con.n && !mine; j++)
+ if (con.sh[j].amount_expr == exprs[i]) {
+ con.exprs[con.nexprs++] = exprs[i];
+ mine = true;
+ }
+ if (!mine) expr_free(exprs[i]); /* its entry was dropped */
+ }
+
+ chain_clear(&n->chrome_chain);
+ chain_clear(&n->content_chain);
+ n->chrome_chain = chr;
+ n->content_chain = con;
+ pane_touch(n->pane);
+ if (nchrome) *nchrome = chr.n;
+ if (ncontent) *ncontent = con.n;
+ return true;
+}
+
/* Everything this pane painted on itself, gone: both chains at once.
*
* Not gated on `in_band_shaders`, unlike setting one. A chain can outlive the
diff --git a/src/app.h b/src/app.h
index 226aad2..9ad73cf 100644
--- a/src/app.h
+++ b/src/app.h
@@ -148,6 +148,16 @@ uint32_t app_current_tab_id(app_t *a);
bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool chrome,
const char *text, char *err, size_t errcap);
+/* A preset file -- `contrib/chrome/sine-comet.kdl` and its neighbours -- applied
+ * to one pane, both chains at once, routed by each entry's own `where=`. The
+ * session reads the file so that nothing else has to know this format; a relative
+ * path resolves against the session's working directory. Counts come back so a
+ * caller can say what happened. Gated on `in_band_shaders` like setting a chain by
+ * hand: it is the same act with a shorter spelling. */
+bool app_load_pane_shaders(app_t *a, uint32_t pane_id, const char *path,
+ size_t *nchrome, size_t *ncontent, char *err,
+ size_t errcap);
+
/* Undo both of one pane's own chains. 0 means the focused pane. False when there
* was nothing to undo, so a caller can say so rather than flash a frame nobody
* changed.
diff --git a/src/config.c b/src/config.c
index 24837cb..9d42b2a 100644
--- a/src/config.c
+++ b/src/config.c
@@ -390,6 +390,73 @@ static void parse_shader_list(config_t *c, const kdl_node_t *node,
*
* Expressions come back in `exprs` for the caller to free. Returns how many
* shaders were understood; `err` gets the first reason one was not. */
+/* A preset file into two chains: the same `shaders { }` block a config carries,
+ * routed by each entry's `where=` exactly as the config routes it.
+ *
+ * Here rather than in the caller because the parsing is already here, and a second
+ * reader of this format -- a scanner counting braces and skipping `//` outside
+ * quotes -- would be a second answer to "what does this file say". The KDL parser
+ * knows about comments and strings; nobody else needs to.
+ *
+ * Anything else at the top level is ignored, so a whole config.kdl can be handed
+ * over and only its shaders apply: `theme` and `keys` are not this pane's business
+ * and refusing the file over them would make the useful case the awkward one. */
+size_t config_parse_chain_file(const char *path, color_t default_color,
+ shader_t *content, size_t *ncontent,
+ shader_t *chrome, size_t *nchrome,
+ expr_prog_t **exprs, size_t *nexprs,
+ char *err, size_t errcap) {
+ *ncontent = 0;
+ *nchrome = 0;
+ *nexprs = 0;
+ if (err && errcap) err[0] = 0;
+
+ char perr[192] = {0};
+ kdl_node_t *root = kdl_parse_file(path, perr, sizeof perr);
+ if (!root) {
+ if (err && errcap)
+ snprintf(err, errcap, "%s", perr[0] ? perr : "cannot read it");
+ return 0;
+ }
+
+ size_t total = 0;
+ bool found = false;
+ for (size_t i = 0; i < root->nkids; i++) {
+ const kdl_node_t *block = root->kids[i];
+ if (!block || !block->name || strcmp(block->name, "shaders") != 0) continue;
+ found = true;
+ for (size_t j = 0; j < block->nkids; j++) {
+ const kdl_node_t *k = block->kids[j];
+ if (!k || !k->name) continue;
+
+ bool on_chrome = false;
+ shader_t made;
+ expr_prog_t *aexpr = NULL;
+ char why[160] = {0};
+ /* `content` is the default here, as it is in a config file: the entries are
+ * the file's own words and mean what they meant there. */
+ bool ok = parse_shader_entry(k, default_color, false, &on_chrome, &made,
+ &aexpr, why, sizeof why);
+ if (why[0] && err && errcap && !err[0]) snprintf(err, errcap, "%s", why);
+ if (!ok) continue;
+
+ shader_t *out = on_chrome ? chrome : content;
+ size_t *n = on_chrome ? nchrome : ncontent;
+ if (*n >= SHADE_MAX) {
+ expr_free(aexpr);
+ continue;
+ }
+ out[(*n)++] = made;
+ if (aexpr) exprs[(*nexprs)++] = aexpr;
+ total++;
+ }
+ }
+ kdl_free(root);
+ if (!found && err && errcap && !err[0])
+ snprintf(err, errcap, "no shaders { } block in it");
+ return total;
+}
+
size_t config_parse_chain(const char *text, color_t default_color, bool chrome,
shader_t *out, size_t max, expr_prog_t **exprs,
size_t *nexprs, char *err, size_t errcap) {
diff --git a/src/config.h b/src/config.h
index 17c759c..d5f45f6 100644
--- a/src/config.h
+++ b/src/config.h
@@ -398,6 +398,19 @@ size_t config_parse_chain(const char *text, color_t default_color, bool chrome,
shader_t *out, size_t max, expr_prog_t **exprs,
size_t *nexprs, char *err, size_t errcap);
+/* The same, from a file: a `shaders { }` block as `contrib/chrome` and
+ * `contrib/shaders` ship them, read with the parser that already knows this
+ * format. Entries are routed to the two chains by their own `where=`, defaulting
+ * to content as a config file does, so a preset lands where its author said.
+ * Anything else at the top level is ignored -- hand it a whole config.kdl and
+ * only the shaders apply. Both arrays hold SHADE_MAX; `exprs` holds twice that.
+ * Returns how many entries were understood. */
+size_t config_parse_chain_file(const char *path, color_t default_color,
+ shader_t *content, size_t *ncontent,
+ shader_t *chrome, size_t *nchrome,
+ expr_prog_t **exprs, size_t *nexprs,
+ char *err, size_t errcap);
+
/* What a chord does after the leader. Direct bindings answer here too, so
* pressing the leader first never makes a binding stop working. */
action_t config_lookup(const config_t *c, int key, uint16_t mods);