<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/contrib/README.md, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/contrib/README.md?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/contrib/README.md?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-09-02T12:43:09Z</updated>
<entry>
<title>shaders: remove the .so plugin loader</title>
<updated>2026-09-02T12:43:09Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-02T12:43:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=18d1aef0f9bf4620135fd9f77f26683e6f2f8c00'/>
<id>urn:sha1:18d1aef0f9bf4620135fd9f77f26683e6f2f8c00</id>
<content type='text'>
The static musl release builds cannot dlopen -- musl compiles it out
-- so loadable shader plugins were a feature most shipped binaries
could not use. Nothing we ship used it either: every shader in
contrib/shaders is an expression. Gone: the dlopen/glob loader in
shader.c, shader_dir in the config, the plugin ABI table and entry
macro, the Windows dl*/glob shims that existed only for it, the
contrib/shader-plugin skeleton, its tests, and every doc mention.
shader_abi.h stays as the shader model (cell_t, shade_ctx_t,
shader_t) with a note about what it used to be.
</content>
</entry>
<entry>
<title>slosh: the website lives in web/ -- one directory, one build, make www</title>
<updated>2026-08-19T18:11:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T18:11:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=76f3868cd7b775cb59b85a32c2b223732f6afc30'/>
<id>urn:sha1:76f3868cd7b775cb59b85a32c2b223732f6afc30</id>
<content type='text'>
Consolidate everything the website is made of:

- web_new/ -&gt; web/: the front page, install.sh, and build-site
- contrib/webdemo/ -&gt; web/demo/: the in-browser machine, committed for
  the first time (it had never been checked in)
- make www at the top level assembles the whole site into build/www
  (NO_DEMO=1 skips the guest cross-compile)
- web_plan.md deleted: the plan is fulfilled and web/README.md is now
  the record

Path fallout: the overlay theme symlink gains one ../ (web/demo is a
level shallower relative to contrib/themes), PYSRC in the Makefile
follows the demo scripts, contrib/README.md drops its webdemo section.
ROOT := ../.. in the demo Makefile survives unchanged -- web/demo sits
at the same depth contrib/webdemo did.

Verified: make www builds front page + docs + demo end to end,
web/demo make check boots the image to slosh, tests/test_docs.py and
make fmt-check green.
</content>
</entry>
<entry>
<title>slosh: defaults most people can live in, said by shaders</title>
<updated>2026-08-19T11:46:57Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T11:46:57Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=42b3c10995eb67778d9dd3aa2fd80624896acd5f'/>
<id>urn:sha1:42b3c10995eb67778d9dd3aa2fd80624896acd5f</id>
<content type='text'>
The out-of-the-box look, rethought as one piece:

- the compiled-in palette calms down: muted blue #7aa2f7 on the same
  neutral darks, with the colours that mean something kept semantic --
  amber bell, red-orange dead. sl0p stays one include away as the house
  style, and contrib/themes/default.kdl is the starting point written
  down (theme-tour shows it).

- derived state defaults now actually follow the theme: the scrolled
  wash is rebuilt from scroll_bg after the theme block (it used to bake
  the compiled accent), and declaring a state -- in any file of an
  include chain -- still wins. The dump writes derived states as
  comments so C-a e cannot pin what is meant to keep following.

- a bell you can see: the frame blinks three times in theme's bell
  colour then breathes until answered, and a soft diagonal shimmer --
  the bell colour halved towards default_fg, width breathing on a sine
  -- crosses the body for the same 900ms. bell_indicator false silences
  all of it; states { bell { } } replaces it like any state. since
  carries the clock dependency, so an idle session pays nothing.

- scrollback says where it continues: while scrolled, the viewport's
  top and bottom rows melt towards an edge with content past it and
  turn solid at the buffer's end. New expression variables above/below
  (lines hidden past each edge, own dependency bit, cached per scroll
  position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -&gt; 4.

- expressions survive the dump: a program keeps its source and cb_chain
  writes it back, so amount="(y % 2) * 40" no longer dumps as a number.

- headers move to include/, -Iinclude everywhere; the plugin Makefile's
  SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every
  pane it runs, and building from inside a session read that as a path.

- comment drift culled where features outran the prose, and the
  derivation boilerplate folded into derived_pass()/ctx_scroll().
</content>
</entry>
<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: a skill for driving it, and the two variables that made one possible</title>
<updated>2026-08-17T11:08:06Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:08:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=244068e86c03b6c1bf457fac7e576bf5f68a43b7'/>
<id>urn:sha1:244068e86c03b6c1bf457fac7e576bf5f68a43b7</id>
<content type='text'>
`.agents/skills/driving-sl0ppty/SKILL.md`. The convention is
`.agents/skills/&lt;name&gt;/SKILL.md` with YAML frontmatter -- already in this tree
under `vendor/libghostty-vt`, so it is not an invention -- and it means an agent
working in a checkout picks the skill up without being told, while a directory per
skill leaves room for adjuncts later. `contrib/README.md` and
`docs/scripting.md` say where it is and how to use it elsewhere: copy or symlink
the directory into wherever your agent keeps skills.

**Writing it turned up the reason there was no skill worth writing.** A pane is
told `SL0PPTY=1` and nothing else, so a program inside one knows it is in a
session and cannot find out *which* -- `ls` lists them all and none of them is
labelled "the one you are in". So the whole most valuable case, an agent driving
the session it is running in, was guesswork. The server now sets
`SL0PPTY_SESSION` and `SL0PPTY_BIN` in its own environment, which every pane
inherits through fork: one place, no signatures threaded, and unset under
`--script`, which has no socket to name. `SL0PPTY_BIN` is read from
`/proc/self/exe` rather than `argv[0]`, and exists because the failure it prevents
is the one I hit writing the examples -- a session started from a build tree, and
`sl0ppty` not on the pane's `PATH`, so the skill's own commands did not run.

What the skill actually teaches, none of which `docs/scripting.md` says because a
reference is not allowed to have opinions:

- Work belongs in a pane that was **given** a command, not typed into somebody's
  shell. Then `alive` and `exit_code` off `panes` are how you wait, and the output
  survives the program (D14) so it is still there to read.
- There is **no wait primitive over the socket**: `settle` is headless-only, and
  `deadline` is about repaints and says nothing about program output. Poll.
- `purpose` is the handle. Titles change underneath you; a declared purpose cannot
  be relabelled by the program in the pane, which is what makes it safe to address.
- `snapshot` is the whole composited session, and only what is on screen. Cut a
  pane out with its `content_*` rect; do not expect scrollback.
- **The echoed command line matches your own marker.** `echo DONE` puts `DONE` on
  screen twice. Count, or use a marker you did not send.
- Write `\e`, never `\033`: the unescaper takes `\0` first, so `\033` arrives as
  NUL followed by `33` and nothing complains. That bug was live in this repo's own
  test suite until last week.
- A program in a pane can draw its own status and real buttons with OSC 5577 and
  read the click off its stdin, which is how it asks a question instead of printing
  a prompt and hoping.
- Close what you made; do not `quit` a session you did not start.

`tests/test_skill.py` is the part that keeps it true. A skill is documentation an
agent acts on without a human reading it first, so a stale one is worse than a
missing one: a wrong verb is a failed tool call somebody else has to debug. Every
verb the file names is sent to a live session and must not answer `unknown cmd`;
every `panes` field its central recipe polls must exist; the environment it
promises is read out of a real pane; the recipe itself is run end to end and must
report exit status 3 with its output still on screen. It also greps the file for
paths out of a home directory, because "plug and play" is a property somebody has
to keep.

That test found `close-pane` in the draft, which is the *keybinding* action name --
the verb is `close`. Exactly the class of error the test exists for, found before
anybody's agent ran it.

Verified: 1795 checks, all green, 43 of them in `tests/test_skill.py`. Then the
skill's recipe run verbatim against a real session -- tagged pane, polled to
`alive:false`, `exit status: 3`, `compiling`/`built`/`exited: status 3` still
readable -- and from inside a pane, `session=agentdemo bin=/…/sl0ppty` and a
`building 3/7` status drawn in its own frame.
</content>
</entry>
<entry>
<title>make coverage: a number, cheaply</title>
<updated>2026-08-15T14:07:32Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-15T14:07:32Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=5585c0736e876c9c54ec4feb5139a8607139daa7'/>
<id>urn:sha1:5585c0736e876c9c54ec4feb5139a8607139daa7</id>
<content type='text'>
`make coverage` builds a second binary with gcc's --coverage into build/cov,
runs every suite against it, and prints a per-file table from gcov. About a
minute. Nothing installed, nothing about the normal zig build changed.

gcc rather than clang because the counters need a gcov that matches the
compiler, and gcov is what this machine has -- no llvm-profdata, no llvm-cov,
no lcov, no gcovr. The aggregation is forty lines of python reading gcov's own
summaries, which is the whole reason this was cheap.

The suites drive it through $SL0PPTY_BIN, one line in the harness, so there is
no second copy of the test code and no risk of the coverage run testing
something else.

Two things about the result, both in the report itself:

- the C unit tests link the *same* instrumented objects, so input.c, kdl.c,
  shader.c and expr.c are credited for the tests that exercise them. Built
  separately, input.c read 54% while having a dedicated decoder test -- a lie
  about where the tests are rather than about the code.
- server.c reads ~8% and is not untested. The session server is a daemonised
  child ending in _exit(), which is right there and skips the flush gcov needs.
  The counters are lost, not the tests. Said in the output, because an
  unexplained low number invites someone to "fix" working code -- and changing
  correct code to make a tool happier is the wrong trade.

Where it lands today: 85.4% of 6298 lines. The gaps that are real rather than
mechanical are jsonval.c (58%), screen.c (64%) and pty.c (59%) -- error paths
and the parts of the diff emitter that only a resize or a weird terminal
reaches.
</content>
</entry>
<entry>
<title>sl0ppty: write the session back out as a layout, and a loop that uses it</title>
<updated>2026-08-15T11:13:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-15T11:13:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=d9dee7f725b68332982f34ab6c52da0552b821d3'/>
<id>urn:sha1:d9dee7f725b68332982f34ab6c52da0552b821d3</id>
<content type='text'>
Dogfooding this while building it has one sharp edge: a running session keeps
the binary it started with. `reload` re-reads the config, it cannot re-read
the code, so every build you want to try means losing your screen.

The only thing worth carrying across a restart is the shape, and sl0ppty
already knows how to read one -- it just could not write one. `dump-layout`
is the inverse of `--layout`: tabs with their names and purposes, how the
panes are split, in what proportion, in which directory, running what, which
tab was active and which pane had focus.

Two details make it worth trusting. The directory is where the shell *is*,
read from /proc, not where the pane was started -- after any amount of `cd`
those differ and only one of them is useful. And a pane running the session's
default shell is dumped with no `command`, so it comes back as a shell rather
than as a shell wrapped in a shell, one layer deeper every restart.

contrib/sl0ppty-dev is the loop: attach, and when the session ends because you
asked for a restart, come straight back with the layout. It is two halves
because it has to be -- `restart` runs inside the session, and the moment the
session ends so does the shell it was typed in, so it leaves the layout and a
note and the loop outside picks them up.

What does not come back is what was running *inside* a program. A shell
returns as a shell, in the right directory, with an empty history. That is the
limit of restoring a layout rather than a session, and the script says so
rather than implying otherwise.

Three bugs found by the round-trip test, all the same kind -- a dump that
loads without complaint and rebuilds something subtly different:

- `split=` belongs on the node that *has* the children, not on the children.
  Written the other way it parses fine and gives the wrong tree.
- a split's weight was never restored, because the edit meant to read it
  silently matched nothing and was never applied.
- `active=true` landed on the wrong tab: the index is recorded while the new
  tabs sit after the old ones, and `replace` then moves them to the front.

Which is why the test asserts that a dump of a reload is byte-identical to
the first dump. Anything the dumper loses shows up there at once, where
reading it and nodding would not.
</content>
</entry>
<entry>
<title>contrib: the shadertoy presets, usable in sl0ppty</title>
<updated>2026-08-14T22:09:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T22:09:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=f4e4e52ff9293b413bd477464ccf492c063e9442'/>
<id>urn:sha1:f4e4e52ff9293b413bd477464ccf492c063e9442</id>
<content type='text'>
They only existed in a browser, which is a strange place for a terminal
multiplexer's shaders to live. Now each one is a file you can paste.

contrib/shaders/ holds all thirty-two, one per file, *generated* from the
presets in shadertoy.html by contrib/gen-shaders. The page stays the source:
it is where they are written, previewed and conformance-tested against the
compiler, and a hand-kept second copy would drift the day after it was
written. `gen-shaders --check` says whether the files are current, which is a
test, so the drift is a failing build rather than a stale example.

contrib/shader-tour cycles a session through them, in the shape theme-tour
already established — write the config the session watches and let the
watcher do the rest. It takes one shader, one group, or nothing for all of
them, and it resolves a short name (`torch`) as well as the whole slug
(`guides-torch`), because nobody remembers which group a shader is in. The
tour writes the theme alongside the shader: shaders resolve default-coloured
text through theme's default_fg/default_bg, so a preset judged without one is
being judged against a guess.

The new test is the one that matters. Every shipped file is loaded into a
real session and has to *change the cells of a pane* -- an example that
previews beautifully and does nothing when pasted is worse than no example,
and neither the toy's conformance test nor a config parse would notice. Three
of them are then checked for the specific thing they claim: scanlines
alternate row by row, indent guides land on every fourth column, a flat dim
is flat. One session, reloaded per preset, which is both quick and a fair
exercise of the path anyone will actually use.

Renamed the "the built-ins, as expressions" group to drop the qualifier from
its filenames; it was generating built-ins-as-expressions-scanlines.kdl,
which is a sentence rather than a name.
</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>
