aboutsummaryrefslogtreecommitdiffstats
path: root/contrib
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 /contrib
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 'contrib')
-rwxr-xr-xcontrib/shader-repl150
1 files changed, 150 insertions, 0 deletions
diff --git a/contrib/shader-repl b/contrib/shader-repl
new file mode 100755
index 0000000..0fd9c39
--- /dev/null
+++ b/contrib/shader-repl
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+"""A prompt for prototyping shader chains, run inside the pane it repaints.
+
+ $ contrib/shader-repl
+ chrome> tint color="#ff5fd7" amount="abs(t / 8 % 510 - 255)"
+ ok
+ chrome> :paste
+
+The line you type is a chain in exactly the syntax `config.kdl` uses, sent to the
+session with OSC 5577 and applied to *this* pane. Nothing is written anywhere, so
+a chain you like has to be copied out -- which `:paste` does, in the form a config
+file wants.
+
+Needs `in_band_shaders true` in your config. Without it the session answers "off"
+rather than doing it, which is the answer this asks for: a program restyling the
+session it happens to be running in is a hazard, and prototyping is worth one line
+of consent (D13).
+
+Commands:
+ :chrome aim the next chain at the frame (the default)
+ :content aim it at the pane's contents
+ :clear take this pane's chain away
+ :both clear both chains
+ :paste print what you have as a `shaders { }` block
+ :quit leave (the chain stays until the pane closes)
+
+Python 3, standard library only.
+"""
+import os
+import select
+import sys
+import termios
+import time
+import tty
+
+OSC = "\033]5577;1;"
+ST = "\033\\"
+TIMEOUT = 2.0
+
+
+def send(payload):
+ sys.stdout.write(OSC + payload + ST)
+ sys.stdout.flush()
+
+
+def reply(fd, timeout=TIMEOUT):
+ """One answer, read raw off the terminal.
+
+ Replies carry no newline -- they are escape sequences, not lines -- so this
+ reads bytes until the terminator rather than calling readline() and hanging.
+ """
+ out = ""
+ deadline = time.monotonic() + timeout
+ while True:
+ left = deadline - time.monotonic()
+ if left <= 0 or not select.select([fd], [], [], left)[0]:
+ break
+ out += os.read(fd, 256).decode("utf-8", "replace")
+ if out.endswith(ST) or out.endswith("\a"):
+ break
+ if OSC + "shader-reply;" not in out:
+ return None
+ body = out.split(OSC + "shader-reply;", 1)[1]
+ return body.rstrip(ST).rstrip("\a")
+
+
+def ask(fd, where, chain):
+ send("shader;%s;%s" % (where, chain))
+ answer = reply(fd)
+ if answer is None:
+ return ("no answer -- is this pane inside sl0ppty?", False)
+ if answer == "ok":
+ return ("ok", True)
+ return (answer.replace("error;", "", 1), False)
+
+
+def paste(chains):
+ """What a config file would say. Two `where=` entries rather than two blocks,
+ because one `shaders { }` holds both and that is the shape to copy."""
+ if not any(chains.values()):
+ return "shaders { } // nothing set"
+ lines = ["shaders {"]
+ for where, chain in chains.items():
+ for entry in [e.strip() for e in chain.split(";") if e.strip()]:
+ if "where=" in entry:
+ lines.append(" %s" % entry)
+ else:
+ head, _, rest = entry.partition(" ")
+ lines.append(' %s where="%s"%s'
+ % (head, where, (" " + rest) if rest else ""))
+ lines.append("}")
+ return "\n".join(lines)
+
+
+def main():
+ if not sys.stdin.isatty():
+ sys.exit("shader-repl: needs a terminal (run it in a pane)")
+ fd = sys.stdin.fileno()
+ saved = termios.tcgetattr(fd)
+ where = "chrome"
+ chains = {"chrome": "", "content": ""}
+
+ print("shader-repl: a chain in config syntax, applied to this pane.")
+ print(" :chrome :content :clear :both :paste :quit")
+ try:
+ while True:
+ try:
+ line = input("%s> " % where)
+ except EOFError:
+ break
+ line = line.strip()
+ if not line:
+ continue
+
+ if line in (":quit", ":q"):
+ break
+ if line in (":chrome", ":content"):
+ where = line[1:]
+ continue
+ if line == ":paste":
+ print(paste(chains))
+ continue
+ if line in (":clear", ":both"):
+ targets = ["chrome", "content"] if line == ":both" else [where]
+ for target in targets:
+ tty.setraw(fd)
+ try:
+ msg, ok = ask(fd, target, "")
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, saved)
+ if ok:
+ chains[target] = ""
+ print("%s: %s" % (target, msg))
+ continue
+
+ # raw only around the exchange, so the prompt keeps its line editing
+ tty.setraw(fd)
+ try:
+ msg, ok = ask(fd, where, line)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, saved)
+ if ok:
+ chains[where] = line
+ print(msg)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, saved)
+
+
+if __name__ == "__main__":
+ main()