aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 01:06:42 +0200
committeruser <user@clank>2026-08-16 01:06:42 +0200
commit3b9c1d987db7dae1db68a8ea304e8227344a1cdb (patch)
treeb2aa66a61e2111720115442611ca092d261c161c /src/app.c
parentsl0ppty: a reply is never a request (diff)
downloadslosh-3b9c1d987db7dae1db68a8ea304e8227344a1cdb.tar.gz
slosh-3b9c1d987db7dae1db68a8ea304e8227344a1cdb.tar.xz
slosh-3b9c1d987db7dae1db68a8ea304e8227344a1cdb.zip
sl0ppty: a pane can set its own shaders
printf '\033]5577;1;shader;chrome;tint color="#ff5fd7" amount="abs(t / 8 % 510 - 255)"\033\\' The field after `shader` is which rect -- `content` or `chrome` -- and the rest of the payload is a chain in exactly the syntax a `shaders { }` block uses, taken verbatim so that `;` can go on separating entries. Empty text clears that chain. The session answers on the program's stdin, `shader-reply;ok` or `shader-reply;error;bad amount for tint: ...`, because a prototyping loop that gets no answer cannot tell a typo from a build without the feature. `contrib/shader-repl` is the prompt this exists for: type a chain, watch the pane, and `:paste` prints what you have as a config block. Both ends are the same parser, so what you arrive at is what you can keep -- verified by pasting `:paste`'s output into a file, linting it, and checking the frame comes out the same colour. **D13 said this was impossible on purpose, and the reasoning stands**: a program that can restyle the session it happens to be running in is a hazard, and `cat hostile.txt` should not dim your panes. But *impossible* was the wrong shape for it -- editing a file, saving, looking and guessing again is slow enough that shaders go unwritten. So it takes `in_band_shaders true`, one line of consent, and it is scoped to the pane that asked: its own two chains, never its neighbour's, never what the config said about anyone else. There is a test for the neighbour. Each chain owns the programs its expressions compiled to, and is swapped in whole, so a chain that turns out not to read leaves the pane looking like it did rather than half-restyled. Measured: 1,800 replacements of an animated chain, resident size flat after the first pass. Also here, because it is the same paint order: the chrome chain now sits between the config's and policy's, exactly where the content chain does. Dead code out -- `app_shade_add`/`app_shade_clear`/`app_shade_count` had no callers and their comment claimed the opposite of what this commit does.
Diffstat (limited to 'src/app.c')
-rw-r--r--src/app.c135
1 files changed, 113 insertions, 22 deletions
diff --git a/src/app.c b/src/app.c
index e339a33..3c500a3 100644
--- a/src/app.c
+++ b/src/app.c
@@ -141,6 +141,17 @@ static const color_t NO_COLOR = {0};
/* ---- tree --------------------------------------------------------------- */
+/* A chain attached to one pane, with the programs its expressions compiled to
+ * kept beside it. Together because they are one lifetime: freeing the chain
+ * without the programs leaks, and freeing the programs without the chain leaves
+ * shaders pointing at freed code. */
+typedef struct {
+ shader_t sh[SHADE_MAX];
+ size_t n;
+ expr_prog_t *exprs[SHADE_MAX];
+ size_t nexprs;
+} inband_chain_t;
+
struct node {
enum { NODE_LEAF, NODE_SPLIT } kind;
node_t *parent;
@@ -161,11 +172,17 @@ struct node {
uint32_t id;
rect_t content; /* where the pane's cells go */
- /* Colour passes over this pane's contents, applied in order. Attached by
- * policy inside this file; nothing in-band or over the control API can set
- * them yet, so a program cannot restyle itself by accident. */
- shader_t shaders[SHADE_MAX];
- size_t nshaders;
+ /* Colour passes over this pane, applied in order: one chain for its contents
+ * and one for its frame, because those are two rects and not one list with a
+ * sort key. Sent in-band by the program inside the pane, which `shaders.md`
+ * calls prototyping and D13 called a hazard -- both are true, so it takes
+ * `in_band_shaders true` to be possible at all.
+ *
+ * Each chain owns the programs its expressions compiled to. The config owns
+ * the ones a file produced and frees them on reload; these live exactly as
+ * long as the chain that was sent, which is until the next one replaces it or
+ * the pane goes away, and that is nobody else's schedule. */
+ inband_chain_t content_chain, chrome_chain;
char purpose[64];
bool purpose_locked; /* declared by a layout: in-band cannot override */
@@ -398,9 +415,52 @@ static void bypane_cb(node_t *n, void *ud) {
/* OSC 5577 verbs pane.c does not own. `purpose` arrives from a pane's own
* output, so it is in-band by definition and can never be declared: a pane
* that prints one is asking, not telling (D8). */
+/* `shader;<where>;<chain>` from the program in the pane: `where` is `content` or
+ * `chrome`, and the chain is the rest of the payload *verbatim*, `;` and all,
+ * because a chain is several entries separated by `;` and escaping the one
+ * character the syntax uses would make a pasteable line unpasteable.
+ *
+ * Answered either way, on the pane's own stdin: `shader;ok` or
+ * `shader;error;<why>`. A prototyping loop that gets no answer cannot tell "you
+ * typed it wrong" from "this build does not have it", and a program asking for
+ * something the session refuses deserves to hear so rather than watch nothing
+ * happen. */
+static void on_pane_shader(app_t *a, pane_t *p, const char *payload) {
+ const char *semi = strchr(payload, ';');
+ size_t wlen = semi ? (size_t)(semi - payload) : strlen(payload);
+ bool chrome = wlen == 6 && memcmp(payload, "chrome", 6) == 0;
+ bool content = wlen == 7 && memcmp(payload, "content", 7) == 0;
+
+ char reply[256];
+ if (!chrome && !content) {
+ snprintf(reply, sizeof reply,
+ "\033]5577;1;shader-reply;error;where must be content or chrome\033\\");
+ pane_write(p, reply, strlen(reply));
+ return;
+ }
+
+ struct bypane b = {p, NULL};
+ walk_all(a, bypane_cb, &b);
+ if (!b.found) return;
+
+ 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);
+ if (ok)
+ snprintf(reply, sizeof reply, "\033]5577;1;shader-reply;ok\033\\");
+ 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;
+ if (strcmp(verb, "shader") == 0) {
+ on_pane_shader(a, p, payload);
+ return;
+ }
if (strcmp(verb, "purpose") != 0) return;
struct bypane b = {p, NULL};
walk_all(a, bypane_cb, &b);
@@ -824,9 +884,18 @@ static node_t *leaf_new(app_t *a) {
return n;
}
+/* Empty a chain, freeing the programs it owned. */
+static void chain_clear(inband_chain_t *c) {
+ for (size_t i = 0; i < c->nexprs; i++) expr_free(c->exprs[i]);
+ c->nexprs = 0;
+ c->n = 0;
+}
+
static void node_free(node_t *n) {
if (!n) return;
if (n->kind == NODE_LEAF) {
+ chain_clear(&n->content_chain);
+ chain_clear(&n->chrome_chain);
pane_free(n->pane);
} else {
for (size_t i = 0; i < n->nkids; i++) node_free(n->kids[i]);
@@ -2809,24 +2878,41 @@ static void draw_collapsed(app_t *a, screen_t *s, node_t *n) {
/* ---- shaders ------------------------------------------------------------ */
-bool app_shade_add(app_t *a, uint32_t pane_id, const char *kind, color_t color,
- uint8_t amount) {
+/* A chain the program in the pane asked for, in the config's own syntax so that
+ * what you prototype is what you can paste (D13, revisited). Replaces that
+ * chain: empty text clears it, which is how a program puts a pane back.
+ *
+ * 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. */
+bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool chrome,
+ const char *text, char *err, size_t errcap) {
+ 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);
- if (!n || n->nshaders >= SHADE_MAX) return false;
- shader_t sh;
- if (!shader_make(&sh, kind, color, amount)) return false;
- n->shaders[n->nshaders++] = sh;
- return true;
-}
+ if (!n) {
+ if (err && errcap) snprintf(err, errcap, "no such pane");
+ return false;
+ }
-void app_shade_clear(app_t *a, uint32_t pane_id) {
- node_t *n = pane_by_id(a, pane_id);
- if (n) n->nshaders = 0;
-}
+ /* 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);
+ if (err && errcap && err[0]) {
+ for (size_t i = 0; i < next.nexprs; i++) expr_free(next.exprs[i]);
+ return false;
+ }
-size_t app_shade_count(app_t *a, uint32_t pane_id) {
- node_t *n = pane_by_id(a, pane_id);
- return n ? n->nshaders : 0;
+ inband_chain_t *slot = chrome ? &n->chrome_chain : &n->content_chain;
+ chain_clear(slot);
+ *slot = next;
+ pane_touch(n->pane);
+ return true;
}
/* The session's opinion about this pane at this moment, as a shader.
@@ -2925,8 +3011,8 @@ static void shade_leaf(app_t *a, screen_t *s, node_t *n) {
* whatever the other two produced. */
for (size_t i = 0; i < CFG.nshaders && nc < SHADE_CHAIN_MAX; i++)
chain[nc++] = CFG.shaders[i];
- for (size_t i = 0; i < n->nshaders && nc < SHADE_CHAIN_MAX; i++)
- chain[nc++] = n->shaders[i];
+ for (size_t i = 0; i < n->content_chain.n && nc < SHADE_CHAIN_MAX; i++)
+ chain[nc++] = n->content_chain.sh[i];
nc += policy_shaders(a, n, &chain[nc], SHADE_CHAIN_MAX - nc);
if (!nc) return;
@@ -2974,8 +3060,13 @@ static void shade_chrome(app_t *a, screen_t *s, node_t *n, rect_t r,
shader_t chain[SHADE_CHAIN_MAX];
size_t nc = 0;
+ /* Configured, then this pane's own, then policy -- the same order the
+ * contents use, and for the same reason: the session's opinion about this
+ * moment has to be able to grey out whatever the other two produced. */
for (size_t i = 0; i < CFG.nchrome_shaders && nc < SHADE_CHAIN_MAX; i++)
chain[nc++] = CFG.chrome_shaders[i];
+ for (size_t i = 0; i < n->chrome_chain.n && nc < SHADE_CHAIN_MAX; i++)
+ chain[nc++] = n->chrome_chain.sh[i];
nc += chrome_policy_shaders(a, n, &chain[nc], SHADE_CHAIN_MAX - nc);
if (!nc) return;