diff options
| author | user <user@clank> | 2026-08-16 18:40:42 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-16 18:40:42 +0200 |
| commit | 80b98bbf4c6ffe1a95c389525af30374df21d8eb (patch) | |
| tree | c9ac16b33c85465db06cfbfea374ffb232fbab05 /src | |
| parent | sl0ppty: six chrome presets that need the new operators (diff) | |
| download | slosh-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.c | 99 | ||||
| -rw-r--r-- | src/app.h | 10 | ||||
| -rw-r--r-- | src/config.c | 67 | ||||
| -rw-r--r-- | src/config.h | 13 |
4 files changed, 189 insertions, 0 deletions
@@ -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 @@ -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); |
