<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_shader_repl.py, branch docs-new</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/tests/test_shader_repl.py?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_shader_repl.py?h=docs-new'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-18T19:26:00Z</updated>
<entry>
<title>slosh: rename the project from sl0ppty</title>
<updated>2026-08-18T19:26:00Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-18T19:26:00Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b9e53cace4eb75263077df8623b011e66f5f2241'/>
<id>urn:sha1:b9e53cace4eb75263077df8623b011e66f5f2241</id>
<content type='text'>
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.

Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-&lt;uid&gt;,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -&gt; src/slosh.h, contrib/sl0ppty-dev -&gt; contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.

The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
</content>
</entry>
<entry>
<title>sl0ppty: ruff format the Python</title>
<updated>2026-08-17T12:36:59Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:36:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c0022776d0912042819e4ea330cf7491a8196b45'/>
<id>urn:sha1:c0022776d0912042819e4ea330cf7491a8196b45</id>
<content type='text'>
The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/`
scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no
behaviour changed. Nothing here is worth reading -- the next commit adds it to
`.git-blame-ignore-revs` so `git blame` walks past it.

It is a bigger reformat than the C was, 60% of the lines against 17%, and for a
reason worth writing down rather than tuning away: ruff and black break a call that
does not fit onto one argument per line, where this code aligned the continuation
under the opening paren. The test suite is thousands of `check("some prose", cond,
detail)` calls, so nearly every one of them changed shape and the suite grew 3,308
lines. Line length was the one lever that moved it -- 120 columns would have been a
40% reformat and no growth at all -- and it was left at 88, because picking an
unusual width to dodge a one-time diff is paying forever to avoid paying once.

Import sorting came with it. Two files had `import os, pty, uuid, select, struct,
sys, termios, time, fcntl, signal` on one line and now have ten.

Verified after, not before, because a formatter that alters behaviour would be
worse than none:

- `make retest`, 1801 checks green -- and this reformats the suite that does the
  checking, including `harness.py`, which every one of those files imports.
- `make test-live`, the tty tests the fast suite skips. They matter here more than
  usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports
  were split, and nothing in the fast suite would have noticed if that broke them.
- All 60 files still compile, `make fmt` twice in a row changes nothing the second
  time, and `ruff check --select I` has nothing left to say.
</content>
</entry>
<entry>
<title>sl0ppty: the prompt speaks config, not a dialect of it</title>
<updated>2026-08-16T16:57:44Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T16:57:22Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b4b066c8c4e26191132f99014555d0a1d3232340'/>
<id>urn:sha1:b4b066c8c4e26191132f99014555d0a1d3232340</id>
<content type='text'>
    chrome&gt; include "contrib/chrome/heartbeat.kdl"
      heartbeat.kdl: 1 chrome, 0 content
    chrome&gt; shaders {
    ...&gt;     tint where="chrome" color="#ff5fd7" amount="abs(sin(t / 5))"
    ...&gt;     dim where="content" amount=80
    ...&gt; }
    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 &lt;path&gt;, or include "&lt;path&gt;")`. 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.
</content>
</entry>
<entry>
<title>sl0ppty: `:load` a preset, without a second KDL reader</title>
<updated>2026-08-16T16:40:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T16:40:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=80b98bbf4c6ffe1a95c389525af30374df21d8eb'/>
<id>urn:sha1:80b98bbf4c6ffe1a95c389525af30374df21d8eb</id>
<content type='text'>
    chrome&gt; :load sine-comet
    sine-comet.kdl: 1 chrome, 0 content
    chrome&gt; :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.
</content>
</entry>
<entry>
<title>sl0ppty: PI is 180</title>
<updated>2026-08-16T14:16:19Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T14:16:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=a540b74dd3bb04d33a69219487cb2db32f2022f1'/>
<id>urn:sha1:a540b74dd3bb04d33a69219487cb2db32f2022f1</id>
<content type='text'>
Angles here are degrees, and pi in an angle language is not a number -- it is a
half turn. So `PI` is 180, `TAU` is 360, and a formula written the radian way
ports across as written:

    sin(TAU * x / cols)      one cycle over the pane's width
    sin(TAU * p * 3 / perim) three lobes fitted exactly to a border
    PI / 2                   a quarter turn
    PI / 6                   thirty degrees, exactly

That last line is the argument. A numeric pi would be 3 in this language, and
`3.14159 / 6` in integers is 0 -- so radians cannot express a sixth of a turn
here at all, while degrees give it as a whole number. The pun is not a
compromise; it is the more precise reading of the two.

Which is why there is no `deg2rad` or `rad2deg` beside them, and asking for one
says `unknown function`. There is one angle unit. A conversion could only lose
the angle (`deg2rad(90)` is 1, and `sin(1)` is nearly nothing) or invent a second
scale for the language to disagree with itself about.

Spelled-out numbers rather than variables: they read no environment, carry no
dependency, and fold into the program like the literals they are. The browser
preview learned the same two, from the same table.

Verified: 12 cases in `tests/expr_test.c`, including that `deg2rad` and
`rad2deg` are refused; the cross-check agrees with the compiler on 700
evaluations, now including `sin(TAU * x / cols)` and friends.

Two smaller things ride along, both from the repl. `PI`/`TAU` tab-complete, and
its drift test grew a third table to check -- it already caught `sin`/`cos`
missing before I did. And it now checks the script *parses* before starting a
pane that runs it: I broke the indentation of one line here, and the failure
arrived as `IndexError: list index out of range` from a pane that died before
its prompt, which is a long way from "line 212".
</content>
</entry>
<entry>
<title>sl0ppty: a prompt with a memory</title>
<updated>2026-08-16T09:12:20Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T09:12:20Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=a4e09657ca89fe3cc553ec894e3776abfd4a3e99'/>
<id>urn:sha1:a4e09657ca89fe3cc553ec894e3776abfd4a3e99</id>
<content type='text'>
`contrib/shader-repl` imports readline, which is most of it: editing,
up/down, ctrl-r, and history kept between runs in
`$XDG_DATA_HOME/sl0ppty/shader-repl.history`. Chains only -- a `:` command
is taken back out of the history it would otherwise fill, because pressing
up four times past `:quit` and `:paste` to reach the line you were working
on is the annoyance history exists to remove.

Tab completes the commands, the nine shader names, the property keys, the
two rects, the three channels, and the expression language's own variables
and functions. `:help` prints the lot at once, which is the same list from
the same place.

That list is a claim about what exists, so `tests/test_shader_repl.py`
checks it against `src/shader.c` and `src/expr.c` and fails if either grows
a name it does not have -- verified by removing `spotlight` and watching it
fail. Then it types every name it offers at a real prompt and requires `ok`
from a real session, because a name that completes and does not parse is a
lie with a keystroke attached. History across two runs, a tab completion,
and `:help` are driven the same way, through a pty.

Two things that cost me time and are now written down where they bite:
keys go into a pane escaped (`\r`, `\t`, `\e[A`) because `raw` takes the rest
of one command line and a literal newline ends the command instead of
arriving; and Enter is CR, which is what a terminal sends and what readline
waits for.

Also: **a bare argument on a shader entry is now a complaint.** Every
parameter is a property, so `tint color="#000000" spotlight` is not a pass
with a spare word, it is a missing `;` -- KDL reads one node, the second
name does nothing, and the session used to answer `ok`. It now says
`unexpected argument for tint: spotlight (separate entries with ';')`. Found
by watching the prompt get used.
</content>
</entry>
</feed>
