diff options
| author | user <user@clank> | 2026-08-16 18:57:22 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-16 18:57:44 +0200 |
| commit | b4b066c8c4e26191132f99014555d0a1d3232340 (patch) | |
| tree | f8b26e3cc107d85069c043f2ade92baad3e3322b /src | |
| parent | sl0ppty: `:load` a preset, without a second KDL reader (diff) | |
| download | slosh-b4b066c8c4e26191132f99014555d0a1d3232340.tar.gz slosh-b4b066c8c4e26191132f99014555d0a1d3232340.tar.xz slosh-b4b066c8c4e26191132f99014555d0a1d3232340.zip | |
sl0ppty: the prompt speaks config, not a dialect of it
chrome> include "contrib/chrome/heartbeat.kdl"
heartbeat.kdl: 1 chrome, 0 content
chrome> shaders {
...> tint where="chrome" color="#ff5fd7" amount="abs(sin(t / 5))"
...> dim where="content" amount=80
...> }
1 chrome, 1 content
Both of those were errors an hour ago, and the second one was my fault twice
over: `:paste` printed `include "..."` and printing something you cannot type
back is a prompt lying about its own output. Pasting it answered `unexpected
argument for include`, and `load "..."` answered the same about `load`.
So the grammar is now the config's, all of it: an entry, several with `;`, an
entry with its own `where=`, a whole `shaders { }` block over as many lines as it
takes, `include "f.kdl"`, `load f`. There is a test that reads `:paste`'s block
off the screen, clears the pane, types it back line by line and compares the
cells -- round-tripping is the property, not a nice-to-have.
**The text is a document.** What it says replaces both chains, exactly as naming
`shaders { }` in a config replaces the block rather than adding to it, and an
entry's `where=` decides which rect it lands on. That drops a rule I invented last
week -- "one chain, one rect", which refused an entry that named the other one --
because it made the prompt's mode a law rather than a default and made a two-rect
block, the thing `:paste` prints, impossible to paste back. The prompt now keeps
the document and sends all of it, so a line aimed at the frame leaves the contents
alone, and the reply says `1 chrome, 1 content` rather than `ok`: with routing
visible, the counts are the answer worth reading.
`route_entries` is the one place routing is decided now -- config file, preset
file, typed line, same function. `config_parse_chain_doc` takes the wrapper or the
bare entries, because both are things a config contains and therefore both are
things somebody will paste.
Still nothing in the prompt parses KDL. A file goes by path; a pasted block goes
via a temporary file, since an OSC payload cannot carry a newline -- a control byte
ends the sequence, by the spec -- and joining lines with `;` would need exactly the
comment-and-quote scanner I refused to write. The only syntax the script looks at
is counting braces to know whether you have finished typing, and an empty line
gives up on that.
One hint, for the mistake that started this: when the session says `unknown
shader`, the prompt adds `(a file? :load <path>, or include "<path>")`. After the
fact rather than before, so a plugin's shader name is never second-guessed.
Verified: `tests/test_shader_repl.py` is 41 checks -- every input shape, the
multi-line paste and giving up on one, the round trip, the hint -- and
`tests/test_inband_shaders.py` gained the document rule in both directions,
including that a document naming only the frame takes the contents pass off. Full
clean suite green.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 63 | ||||
| -rw-r--r-- | src/app.h | 20 | ||||
| -rw-r--r-- | src/config.c | 191 | ||||
| -rw-r--r-- | src/config.h | 30 |
4 files changed, 189 insertions, 115 deletions
@@ -455,9 +455,13 @@ static void on_pane_shader(app_t *a, pane_t *p, const char *payload) { char err[192] = {0}; const char *text = semi ? semi + 1 : ""; - bool ok = app_set_pane_shaders(a, b.found->id, chrome, text, err, sizeof err); + size_t nchrome = 0, ncontent = 0; + bool ok = app_set_pane_shaders(a, b.found->id, chrome, text, &nchrome, &ncontent, + err, sizeof err); if (ok) - snprintf(reply, sizeof reply, "\033]5577;1;shader-reply;ok\033\\"); + 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"); @@ -3014,8 +3018,11 @@ bool app_clear_pane_shaders(app_t *a, uint32_t pane_id) { return had; } -bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool chrome, - const char *text, char *err, size_t errcap) { +bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool default_chrome, + const char *text, 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"); @@ -3027,20 +3034,50 @@ bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool chrome, return false; } - /* Parsed into a fresh chain and swapped in, so a chain that turns out to be - * unreadable leaves the pane looking like it did rather than half-restyled. */ - inband_chain_t next = {0}; - next.n = config_parse_chain(text, CFG.frame_focus, chrome, next.sh, SHADE_MAX, - next.exprs, &next.nexprs, err, errcap); + /* Both chains, because the text is a *document*: `where=` inside it decides + * where each pass goes, exactly as it does in the config file this syntax comes + * from, and `default_chrome` is only what a pass that keeps quiet means. An + * earlier version refused an entry that named the other rect, which made the + * prompt's mode a rule rather than a default -- and made a two-rect block, the + * thing `:paste` prints, impossible to paste back. + * + * Parsed beside the live chains and swapped in together, so text that turns out + * to be unreadable leaves the pane as it was rather than half-restyled. */ + inband_chain_t con = {0}, chr = {0}; + expr_prog_t *exprs[SHADE_MAX * 2]; + size_t nexprs = 0; + config_parse_chain_doc(text, CFG.frame_focus, default_chrome, con.sh, &con.n, + chr.sh, &chr.n, exprs, &nexprs, err, errcap); if (err && errcap && err[0]) { - for (size_t i = 0; i < next.nexprs; i++) expr_free(next.exprs[i]); + for (size_t i = 0; i < nexprs; i++) expr_free(exprs[i]); return false; } - inband_chain_t *slot = chrome ? &n->chrome_chain : &n->content_chain; - chain_clear(slot); - *slot = next; + /* Each program to the chain whose shader points at it. Walked rather than + * assumed: freeing one that the other chain still uses is the bug here 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]); + } + + 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; } @@ -138,15 +138,19 @@ uint32_t app_focused_pane_id(app_t *a); uint32_t app_current_tab_id(app_t *a); /* Colour passes over one pane (shader.h), set by the program running in it over - * OSC 5577 -- `chrome` picks the chain that runs over its frame rather than its - * contents, and empty text clears that chain. + * OSC 5577. `text` is a *document* in the config's own syntax -- one entry, several + * separated by `;`, or a whole `shaders { }` block -- and each pass goes where its + * own `where=` says, so both chains are set from one call. `default_chrome` is what + * a pass that does not say means, which is how a prompt aimed at the frame works + * without every line repeating itself. Empty text clears both. * - * `text` is what a config's `shaders { }` block holds, one or more entries - * separated by `;` or newlines, so a chain you arrive at by prototyping is a - * chain you can paste. Refused with a reason in `err`: `in_band_shaders` off, - * no such pane, or a chain that does not read. */ -bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool chrome, - const char *text, char *err, size_t errcap); + * Counts come back so a caller can say what happened: "ok" alone cannot tell a + * block that filled both chains from one whose entries were all dropped. Refused + * with a reason in `err`: `in_band_shaders` off, no such pane, or text that does + * not read. */ +bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool default_chrome, + const char *text, size_t *nchrome, size_t *ncontent, + 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 diff --git a/src/config.c b/src/config.c index 9d42b2a..52188ed 100644 --- a/src/config.c +++ b/src/config.c @@ -382,24 +382,104 @@ static void parse_shader_list(config_t *c, const kdl_node_t *node, } } -/* A chain from text, for whoever is not a config file: `dim amount=90` or - * `tint where="chrome" channel="fg" amount="(since<250)*255"`, several separated - * by `;` or newlines. The same entries a `shaders { }` block holds, parsed by - * the same function, so what you prototype in a pane is what you can paste into - * a config. +/* Entry nodes into two chains, routed by each one's `where=` -- the only place + * that routing is decided, whether the entries came from a config file, from a + * preset file, or from a line somebody typed at a prompt. * - * Expressions come back in `exprs` for the caller to free. Returns how many - * shaders were understood; `err` gets the first reason one was not. */ + * `default_chrome` is what an entry that does not say `where=` means. A config + * file's default is content; a prompt aimed at the frame passes true, so a line + * lifted out of a config still means what it meant there and a line typed at + * `chrome>` means what the prompt says. Either way an explicit `where=` wins: a + * document says where its own passes go, and the caller's is a *default*. + * + * Expressions come back in `exprs` for the caller to free; entries past SHADE_MAX + * for their rect are dropped, and the first refusal lands in `err`. */ +static size_t route_entries(const kdl_node_t *const *kids, size_t nkids, + color_t default_color, bool default_chrome, + shader_t *content, size_t *ncontent, shader_t *chrome, + size_t *nchrome, expr_prog_t **exprs, size_t *nexprs, + char *err, size_t errcap) { + size_t total = 0; + for (size_t i = 0; i < nkids; i++) { + const kdl_node_t *k = kids[i]; + if (!k || !k->name) continue; + + bool on_chrome = false; + shader_t made; + expr_prog_t *aexpr = NULL; + char why[160] = {0}; + bool ok = parse_shader_entry(k, default_color, default_chrome, &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) { + if (err && errcap && !err[0]) + snprintf(err, errcap, "more than %d passes for the %s", SHADE_MAX, + on_chrome ? "frame" : "contents"); + expr_free(aexpr); + continue; + } + out[(*n)++] = made; + if (aexpr) exprs[(*nexprs)++] = aexpr; + total++; + } + return total; +} + +/* Chains from text: what a `shaders { }` block holds, as a person types it at a + * prompt -- one entry, several separated by `;`, or the whole block with its + * braces around it. Every shape a config file has, parsed by the config's own + * parser, so what you prototype is what you can paste and the other way round. */ +size_t config_parse_chain_doc(const char *text, color_t default_color, + bool default_chrome, 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; + if (!text) return 0; + + char perr[192] = {0}; + kdl_node_t *root = kdl_parse(text, perr, sizeof perr); + if (!root) { + if (err && errcap) snprintf(err, errcap, "%s", perr[0] ? perr : "bad chain"); + return 0; + } + + /* `shaders { ... }` around the entries, or the entries on their own. Both are + * things a config file contains, so both are things this takes: the wrapper is + * what `:paste` prints, and therefore what somebody will paste back. */ + const kdl_node_t *block = NULL; + for (size_t i = 0; i < root->nkids; i++) { + const kdl_node_t *k = root->kids[i]; + if (k && k->name && strcmp(k->name, "shaders") == 0) block = k; + } + const kdl_node_t *const *kids = + block ? (const kdl_node_t *const *)block->kids + : (const kdl_node_t *const *)root->kids; + size_t nkids = block ? block->nkids : root->nkids; + + size_t n = route_entries(kids, nkids, default_color, default_chrome, content, + ncontent, chrome, nchrome, exprs, nexprs, err, errcap); + kdl_free(root); + return n; +} + /* 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. + * quoted strings -- 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 + * 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, @@ -420,84 +500,31 @@ size_t config_parse_chain_file(const char *path, color_t default_color, } size_t total = 0; - bool found = false; + bool found = false, states = 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; + if (!block || !block->name) continue; + if (strcmp(block->name, "states") == 0) states = true; + if (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++; - } + /* A config file's default is content, and this is a config file. */ + total += route_entries((const kdl_node_t *const *)block->kids, block->nkids, + default_color, false, content, ncontent, chrome, + nchrome, exprs, nexprs, err, errcap); } 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) { - *nexprs = 0; - if (err && errcap) err[0] = 0; - if (!text) return 0; - - char perr[192] = {0}; - kdl_node_t *root = kdl_parse(text, perr, sizeof perr); - if (!root) { - if (err && errcap) snprintf(err, errcap, "%s", perr[0] ? perr : "bad chain"); - return 0; + if (!found && err && errcap && !err[0]) { + /* Naming the likely mistake rather than the general one: a file of nothing but + * `states { }` is a config's opinion about every pane, and a pane cannot wear + * one -- an in-band chain has no state to hang off. */ + if (states) + snprintf(err, errcap, + "only states { } in it, which is a config's and not a pane's -- " + "paste the pass itself"); + else + snprintf(err, errcap, "no shaders { } block in it"); } - - size_t n = 0; - for (size_t i = 0; i < root->nkids && n < max; i++) { - const kdl_node_t *k = root->kids[i]; - if (!k || !k->name) continue; - bool on_chrome = false; - shader_t made; - expr_prog_t *aexpr = NULL; - char why[160] = {0}; - bool ok = parse_shader_entry(k, default_color, chrome, &on_chrome, &made, - &aexpr, why, sizeof why); - if (why[0] && err && errcap && !err[0]) snprintf(err, errcap, "%s", why); - if (!ok) continue; - /* One chain, one rect. The caller's rect is the *default*, so a line lifted - * out of a config still means what it meant there, and an explicit `where=` - * naming the other one is refused rather than quietly moved. */ - if (on_chrome != chrome) { - if (err && errcap && !err[0]) - snprintf(err, errcap, "%s: this chain is %s", k->name, - chrome ? "chrome" : "content"); - expr_free(aexpr); - continue; - } - out[n++] = made; - if (aexpr) exprs[(*nexprs)++] = aexpr; - } - kdl_free(root); - return n; + return total; } /* `direct` is part of the identity, not a property of it: `x` after the leader diff --git a/src/config.h b/src/config.h index d5f45f6..062291e 100644 --- a/src/config.h +++ b/src/config.h @@ -384,19 +384,25 @@ void config_free(config_t *c); * was loaded, the rest are what it included. Returns how many were written. */ size_t config_files(const config_t *c, const char **out, size_t max); -/* One shader chain from text rather than from a file: the same entries a - * `shaders { }` block holds, separated by `;` or newlines. `chrome` says which - * rect the chain is for, and is the default an entry's `where=` starts from. +/* Shader chains from text rather than from a file: one entry, several separated by + * `;`, or a whole `shaders { ... }` block. Every shape a config file has, because + * the point of using the config's syntax at a prompt is that the two directions + * match -- what you prototype is what you paste, and what you paste is something + * you can type back. * - * Here because the parser belongs with the config -- a chain typed at a running - * pane and a chain in a config file have to mean the same thing, or prototyping - * teaches you something that then does not work. Compiled expressions come back - * in `exprs` (room for `max`) and belong to the caller, since a pane's chain - * outlives no load in particular. Returns how many shaders were understood, - * writing the first refusal to `err`. */ -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); + * Each pass goes where its own `where=` says; `default_chrome` is only what a pass + * that keeps quiet means. Here because the parser belongs with the config: a chain + * typed at a running pane and a chain in a config file have to mean the same + * thing, or prototyping teaches you something that then does not work. + * + * Both arrays hold SHADE_MAX and `exprs` twice that; the compiled programs belong + * to the caller, since a pane's chain outlives no load in particular. Returns how + * many entries were understood, writing the first refusal to `err`. */ +size_t config_parse_chain_doc(const char *text, color_t default_color, + bool default_chrome, shader_t *content, + size_t *ncontent, shader_t *chrome, size_t *nchrome, + 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 |
