<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_shadertoy.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_shadertoy.py?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_shadertoy.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: 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: sin and cos</title>
<updated>2026-08-16T13:58:11Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T13:58:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=72ffc4cc5cc5f9b95d21a115e761de6a632d68a5'/>
<id>urn:sha1:72ffc4cc5cc5f9b95d21a115e761de6a632d68a5</id>
<content type='text'>
    128 + sin(t / 4) / 2          a breathe that is actually a sine
    abs(sin(x * 6 + t / 5))       a wave travelling along a frame
    abs(sin(dist(x, y, curx, cury) * 20 - t / 4)) / 2    ripples from the cursor

Degrees in, -255..255 out. Two decisions, both for the same reason -- this is a
language people type into a config file, not one they read a manual for.
Degrees because that is what anybody guesses, and 255 because it is the scale
every strength here already uses: `128 + sin(t / 4) / 2` is a breathe centred
half way up with nothing to rescale, and `abs(sin(...))` is an amount as it
stands.

Every angle wraps, negative ones included. `t / 4` does not stop at 360 and an
expression that died at one turn would be no use for the one thing this is for.

A 91-entry table of round(255*sin) at one-degree steps, three quadrants by
symmetry, no interpolation -- the argument is an integer and there is nothing
between two degrees to interpolate. A table and not a polynomial because
`contrib/shadertoy.html` reimplements this language in JavaScript so the preview
can run in a browser, and `tests/test_shadertoy.py` compares the two on every
expression: identical data with identical reduction cannot drift the way two
approximations of one curve would. The JS carries the same 91 numbers.

Until now the only oscillator was `abs(t / 6 % 510 - 255)`, a triangle wave
written as arithmetic. It works and it looks like what it is -- a ramp with a
corner in it. A sine has no corner, which is the whole difference between a pane
that pulses and a pane that flickers.

Verified: 19 cases in `tests/expr_test.c` (the cardinal angles, quadrant
symmetry, wrapping past a turn and below zero, and the breathe idiom); the
cross-check now agrees with the compiler on 660 evaluations including generated
trig; the repl's completion drift test caught `sin`/`cos` missing from its list
before I did, which is what it is for. Then six chains in a real pane -- breathe,
travelling wave, clipped crests, plasma, rolling scanlines, cursor ripples --
each measured over eight distinct frames.
</content>
</entry>
<entry>
<title>sl0ppty: bits</title>
<updated>2026-08-16T13:49:07Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-16T13:49:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972'/>
<id>urn:sha1:7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972</id>
<content type='text'>
`&amp;` `|` `^` `~` `&lt;&lt;` `&gt;&gt;`, on the 32-bit value the VM already used. There was
nothing: `x &amp; 7` answered `unexpected trailing input at offset 2` and
`(x ^ y) % 16 &lt; 2` answered `expected ) at offset 3`, which is a fair
description of a parser that had never heard of either.

`^` is the reason to bother. `(x ^ y) &amp; 1` is the cheapest interesting texture
there is, and the language could express a sweep, a ramp and a comparison but
not one bit of pattern without `%` gymnastics.

**Bits bind tighter than comparison, which is not what C does.** `x &amp; 7 == 0`
means `x &amp; (7 == 0)` in C -- a precedence Ritchie put on record as a mistake,
kept for a decade of compatibility that a shader expression in a config file
does not have. Here it asks what it looks like it asks. Among themselves the
usual `&amp;` then `^` then `|`; shifts stay where C has them, just under `+ -`.
`&amp;&amp;` and `||` still parse as themselves: the bitwise rungs decline the first
character when a second one follows it.

Semantics are JavaScript's, deliberately and to the letter -- the shift count
masked to five bits (`1 &lt;&lt; 33` is 2), `&gt;&gt;` keeping the sign (`-8 &gt;&gt; 1` is -4).
Not taste: `contrib/shadertoy.html` reimplements this language in JS so the
preview can run in a browser, and `tests/test_shadertoy.py` compares the two on
every expression it can think of. Anything C leaves implementation-defined had
to be pinned to what JS would show, so the signed right shift is written out in
the VM rather than left to the compiler.

Verified: 22 new cases in `tests/expr_test.c` covering the operators, both
precedence questions, the sign, and the masked count; the preview learned the
same six operators and the same ladder, and the cross-check now agrees with the
compiler on 564 evaluations, including generated ones that reach for bits. Then
seven bitwise chains in a real pane -- an XOR texture, a drifting one, a stepped
pulse that walks eight shades of `t &gt;&gt; 7 &amp; 7` and needs no `abs`.
</content>
</entry>
<entry>
<title>sl0ppty: shaders on the chrome, and two keys for the arrangement</title>
<updated>2026-08-15T21:06:32Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-15T21:06:32Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=f6281fbec93d92bfc7e9164492e436d0236d9179'/>
<id>urn:sha1:f6281fbec93d92bfc7e9164492e436d0236d9179</id>
<content type='text'>
Five things, in one commit because they land in the same few files --
the action table, the shader pass, config.kdl -- and splitting them by
hunk would buy a tidier log at the price of commits that do not build.
Each is verified end to end; the suite is 46 files green.

Chrome shaders. `where="chrome"` on any entry in `shaders` or in a
`states` block runs that pass over a pane's frame -- border, padding,
title, buttons -- instead of its contents. Nothing else changes: same
registry, same `amount`, same expressions, same chain order, one new
word, because a frame is another rect and so it is another pass. It runs
over the frame's whole rect with the contents punched out as a hole,
rather than four passes over four sides: an effect has to be able to
travel round a border, and four passes would each count x/y from zero
and restart at every corner. The hole keeps the two honest -- a skipped
cell is never visited, so a full-strength chrome tint cannot reach your
text. It is also the only kind of pass that can colour a pane a small
window has collapsed to a single row (D6), which is the state where a
colour is worth the most.

`channel="fg"|"bg"|"both"` came out of looking at the first border
flash: `tint` mixes both of a cell's colours, and a frame's background
is the terminal's own default, so the pass materialised it to our guess
at that default and mixed towards the colour -- a dark rectangle behind
the glyphs, left there until the pane changed state. The mask is
enforced by the pass, not the shader: the other colour is snapshotted
before materialising and put back after, so an unset colour goes back to
unset, it works for a built-in and a plugin alike, and no shader grows
an fg-only twin of itself.

A `bell` pane state and a `since` variable, because "flash the border
when a pane rings" needed both and neither existed. `since` is
milliseconds since the pane entered its current state, stamped in the
one function that decides the state -- a transition is the one thing a
frame cannot tell you, so it is remembered where it cannot disagree with
the state it dates. `bell` ranks above the ambient states and below
`dead`: a pane that rang while you were elsewhere is news, and a pane
that rang and then exited is a pane that exited. It ends when you look
at the pane, because that is what answering a bell is.

Animation needed a clock and did not have one: a shader reading `t` only
advanced when something else caused a frame, so a pulse froze the moment
you stopped typing. `app_next_deadline_ms` now reports `anim_ms` while a
chain that reads the clock ran over the last frame -- derived from what
was applied, so a pulse hung off `dead` costs a clock while a pane is
dead and nothing for the rest of the session. A `deadline` verb makes
that observable; the server's poll loop already lived on the number.

contrib/chrome has seven presets and the perimeter trick they are built
on: on a frame the coordinate you want is distance along the ring, which
the language can work out, and `min(d, P - d)` is what makes an effect
cross the corner it started from. Hand-written, unlike contrib/shaders,
because the toy previews contents and a frame has no preview to give --
so tests/test_chrome_presets.py checks each one the only way left: load
it into a real session, assert the frame moves and the contents do not.

`equalize` (C-a =) puts every share back to even: each split's children
weighted by how many visible panes are behind them, so one pane beside a
stack of three is a quarter of the width and "even" means the same thing
at every depth. Exactly equal cell counts are not something a tree can
always express, so the action promises the even division it can.

`rotate-layout` (C-a Space) turns the whole tab a quarter turn
clockwise. Every split changes axis and a row split's children reverse,
because the pane on top of a stack is the pane on the right once you
turn it; get that asymmetry wrong and you have a mirror image, which
looks almost right until four turns fail to come back. They do come
back, exactly, which is why the inverse is three more turns rather than
a second mirrored routine with its own chance of being wrong -- and why
a key as easy as space is the right one rather than a hazard. A turn is
refused when it would not fit: a share of the height is not always a
share the width can afford, and a node that cannot give its children
their floor takes the whole tab down to a list. So the turn is tried,
the layout is asked what it made of it, and one that flattened the tab
is put back with a word. Same rule `split_fits` applies to splitting.

Keys: the palette moves from `shift+p` to `p`, and cycling tabs from
`n`/`p` to tab/shift+tab. `p` is pressed far more often than "the tab
before this one" and was the only shifted letter in the defaults; tab is
what every other tabbed thing cycles with, and `\e[Z` decodes as
shift+tab on a plain terminal, so this costs nothing on a client without
the kitty protocol. `n` is free again. The actions are untouched: two
lines of config put the old binds back.

The shader ABI goes to 3: `shade_ctx_t` gained `state_ms` and struct
shader gained `channels`, both the pass's business rather than a
shader's. Plugins need a rebuild, which is what the version is for.
</content>
</entry>
<entry>
<title>contrib: presets worth stealing, and a test that they do anything</title>
<updated>2026-08-14T22:02:51Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T22:02:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=599da705d93f27efb2a4c62b0747356d97e8f9b3'/>
<id>urn:sha1:599da705d93f27efb2a4c62b0747356d97e8f9b3</id>
<content type='text'>
Eleven examples, grouped, and most of them are less "effect" than
*instrument*: a cursor line, a crosshair, indent guides, a right margin at
three quarters, a torch that dims everything more than eight cells from where
you are looking. Things that tell you where you are, inside a program that
has never heard of any of them -- which is the argument for shading contents
rather than chrome, made in examples instead of prose.

Then the fun: phosphor and amber CRTs (grayscale, tint, scanlines, vignette
as a four-pass chain), film grain, a rolling bar, an interlace flicker; sonar
pings expanding from the cursor, ripples, a barber pole, plasma from two
triangle waves, matrix rain with a fading tail; and every built-in written as
an expression, because `vignette` being `dim` with an argument is the whole
design and is better shown than argued.

Two rules the presets follow, both about staying identical to the C: never
multiply `t` (it is a millisecond clock and gets large -- divide it down
first and the arithmetic stays in an int32 forever), and keep hash
coefficients small for the same reason. `(x * 11) % 23` is fine; the usual
Knuth constant is a different number in C than in JavaScript.

The test now lifts the *presets* out of the page as well as the evaluator, so
one added later is conformance-checked without anyone remembering to add it.
And it evaluates every preset over a whole pane at four points in time and
fails any that is zero everywhere -- an example that does nothing reads as a
broken shader system rather than a bad example.

That check caught its own first draft: sampling every other row made three
`y % 2` effects look dead when they were fine. It samples every cell now,
which costs six hundredths of a second for four hundred thousand
evaluations, because the thing being tested is quick.
</content>
</entry>
<entry>
<title>contrib: a shadertoy for shader expressions</title>
<updated>2026-08-14T21:53:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T21:53:53Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=e16834a81a7111a4d954369af7e42601e0190a7f'/>
<id>urn:sha1:e16834a81a7111a4d954369af7e42601e0190a7f</id>
<content type='text'>
You cannot see an expression by reading it. `(y % 2) * 40` is scanlines and
`(x &gt; cols - 10) * 120` is a right margin, and neither is obvious until it is
on a screen -- so here is a screen. One HTML file, no build and no server:
open it and it works, offline, including from a checkout on a machine that
has never run sl0ppty.

It draws a fake pane with plausible contents, applies a chain of passes to
every cell, and prints the config line to paste. The mouse moves the cursor,
so `dist(x, y, curx, cury)` behaves the way it will in a pane rather than the
way it does at coordinates you imagined. Anything reading `t` animates. The
presets are every built-in written as an expression, which is the equivalence
the whole design rests on: `vignette` and `spotlight` are not special, they
are `dim` with an argument.

It also reports what each expression *reads* and whether that makes it
cacheable, because the dependency analysis is the interesting half of the
implementation and is otherwise invisible.

The hazard is that this is a second implementation of the language, in
another language, and it would disagree with the compiler quietly -- in the
third digit of a colour, on one preset, at one cursor position. So
tests/test_shadertoy.py lifts the evaluator straight out of the page (not a
copy of it, the page itself) and runs a few hundred expression/environment
pairs through both it and src/expr.c, including the corners: division by
zero, the ternary's untaken branch, `dist` counting a row double, and six
expressions that both sides must refuse. build/expr_eval is the real compiler
as a filter, which is what makes that comparison possible at all.

It found a bug on its first run, in the page: unary `-` and `!` did not
thread the environment through their closures, so `!x` threw instead of
evaluating. Which is the argument for the test, made better than I could have
made it.

Skipped with a message when node is missing -- a checker for a contrib toy is
not a reason the suite cannot run.
</content>
</entry>
</feed>
