<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_shaders.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_shaders.py?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_shaders.py?h=docs-new'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-20T21:22:33Z</updated>
<entry>
<title>slosh: floating panes and the drop grammar</title>
<updated>2026-08-20T21:22:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-20T21:22:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b33e8c13ff7981effc7ec2735e6aeaaaf1705af7'/>
<id>urn:sha1:b33e8c13ff7981effc7ec2735e6aeaaaf1705af7</id>
<content type='text'>
A pane can float: lifted out of the layout, drawn over the tiled panes and
under the modals, freely moved and shaped. The model is a flag and a rect on
the leaf, exactly like `minimized` -- the seat in the tree is kept, layout
skips it, the siblings absorb its share, and un-floating returns it home.
The drawn rect is the wanted one clamped to the tab every frame, never
written back, so a terminal squeeze round-trips exactly; an explicit move or
resize *does* write back, because the hand is the author of the intent.
Floating pops the pane to the centre: the jump is what says it happened.
Painted after everything tiled, so the hit list -- searched backwards --
resolves an overlap to the float you can see: the z-order and the click
order are the same list. It casts a shadow (float_shadow, one number), and
`floating` joins the states table above `unfocused`, so a float is never
dimmed as merely-unfocused.

The hands: the title moves it, any other border resizes it (a bottom corner
on both axes; the grabbed edge pins at walls and floors, never sliding the
window), H J K L move a focused float, the =/+ key grows it about its
centre, `-` shrinks it (its split was a refusal anyway), and hovering shows
which edges a grab would take, from the same derivation the press uses.
Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`,
where it reads as search); `C-a F` opens a new floating shell -- the
throwaway terminal; the function row f1..f12 became nameable in configs
along the way. Guards throughout: the last tiled pane cannot float or
minimize away, closing it lands the top float, splits are refused with
their own words, floats are not swap targets.

Dropping got a grammar: while a pane is carried, the pane under the pointer
subdivides into a swap centre and four edge bands meaning "insert me beside
this pane, on this side" -- detach_leaf + place_beside, the surgery
cross-tab moves already had. Bands are thirds (the first map of quarters
showed the swap owning half of each axis), the hovered band fills the half
the drop would hand over, the release resolves against the painted frame's
hits so it cannot disagree with the fill, and a drop between siblings along
the axis is a reorder that always fits -- refusing it was refusing the most
ordinary drag there is.

Also: rename-pane/rename-tab as palette actions (renaming was a
double-click and nothing else, the one verb the palette could not reach),
and C-u clears the rename editor like it clears the picker's query.

Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
</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>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: dim the panes you are not in, and a knob to say how much</title>
<updated>2026-08-15T11:45:21Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-15T11:45:21Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=015ba12fde4fcce15440b39cb6369a4ad66238cf'/>
<id>urn:sha1:015ba12fde4fcce15440b39cb6369a4ad66238cf</id>
<content type='text'>
`dim_unfocused 60`, on by default, 0 to turn it off.

The dimming itself was already possible -- `states { unfocused { dim } }` --
and deliberately not the default. The argument then was that ambient contrast
is a taste, and shipping a taste as a default is how a tool earns a
reputation for fighting you. That argument was mostly about the escape hatch:
"turn it off" meant knowing the states block exists and what a shader chain
is. A single number does not have that problem, so the objection goes away
and the default can be the one most people want.

The knob and the table are one mechanism -- the knob fills the unfocused
chain in when the config has not -- so writing the chain by hand still wins,
including writing an empty one. `states { unfocused { } }` means nothing at
all, and is not quietly refilled, which needed the parser to distinguish
"said nothing" from "said none".

60 by choice: white text lands on #c3c3c3, which reads as "not this one"
without reading as "not available", because half the time you are reading the
other pane. Picked by looking at 40, 55, 70, 90 and 120 rather than by taste
alone.

The tests that had to change are the interesting part. Six in test_shaders.py
asserted "back to green after the drag", which is no longer what normal looks
like -- and rewriting them to compare against a snapshot taken before the
drag failed too, because a drag *moves focus*: press a title and that pane is
yours, so the two panes legitimately swap appearances across one. They assert
the rule now (the pane you are in keeps its colour, the others are pushed
back), which is both what we mean and immune to the next change of defaults.

test_states.py's "an unfocused pane is left alone" is inverted, with the
reason written where the assertion used to be, because a test that flips
should say why it flipped.
</content>
</entry>
<entry>
<title>sl0ppty: status_pad defaults to 4</title>
<updated>2026-08-14T14:50:52Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T14:50:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=cb2a7f5c3436a7cde1b431cf8de6e7ad75b452ad'/>
<id>urn:sha1:cb2a7f5c3436a7cde1b431cf8de6e7ad75b452ad</id>
<content type='text'>
Wider than the panes' own margin (gap * gap_aspect, 2 at the defaults) rather
than equal to it, so the strip and the line read as chrome sitting outside the
layout instead of as another row of it. Picked by looking at 2, 3, 4 and 6 on a
real screen; the difference between adjacent values is one column and is not
something to choose from a config file.

One test had an opinion about where the tab strip starts. It sampled a style at
column 3 to prove the shader pass leaves the strip alone, which was true only
because the strip happened to begin there; it now finds the strip's first
painted column and asks about that. The assertion was never about column 3 and
should not have said so.
</content>
</entry>
<entry>
<title>sl0ppty: the test cycle was 80s, it is now 5.5s</title>
<updated>2026-08-14T13:16:06Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T13:16:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c20b226d44af64ac476173361dad9676dbc74043'/>
<id>urn:sha1:c20b226d44af64ac476173361dad9676dbc74043</id>
<content type='text'>
Measured before changing anything, because the obvious suspect was wrong: the
build is not slow. zig caches objects, so even a clean rebuild is ~0.15s and an
incremental one is 0.08s. The C unit tests are 0.00s. All eighty seconds were
in the python suite, and almost all of that was waiting on durations that were
guesses.

Four things, in order of what they bought.

Run the python tests in parallel. They are separate processes that share
nothing — script mode opens no socket — so each is now its own make target with
a stamp, and `make -j` runs them at once. 78s -&gt; 21s for free. The stamps are
worth as much as the parallelism day to day: an untouched test whose binary has
not changed does not run again, so `make test` after editing one test file is
1.1s and after editing nothing is 0.02s. The binary is a prerequisite of every
stamp, so changing the code still re-runs everything.

Then stop guessing at durations. `settle N` is the only thing that reads a
pane's pty, so it is how you wait for a program's output — but `send` and
`snapshot` are already synchronous with the app, which means a settle after a
mouse event, waiting to assert on our own chrome, is waiting for nothing at
all. Those are removed. Where a program's output really is the thing being
waited for, harness.until()/until_text() polls in 10ms steps and returns the
instant it lands: measured, that is one poll, against the 200-300ms those calls
used to donate. The default settle drops 120ms -&gt; 20ms on the same reasoning.

Three effects are gated on a real clock rather than on anything observable —
the hover dwell, and a toast's lifetime. Those genuinely have to wait, so the
tests that are not *about* the timing now configure the clock down instead
(hover_delay_ms 20, toast_ms 150) and the two tests that are about it keep the
real defaults, because a dwell test that cannot observe not-yet-elapsed proves
nothing.

Two tests turned out to be passing by accident, which is the useful kind of
thing a speedup shakes loose: both asserted the resize hint was gone after a
drop while the pointer was still sitting on the handle. It was only gone
because the dwell had not re-elapsed — resting on a handle *should* re-arm the
hint. They now move off the handle first and assert the thing they meant.

Unrelated but found on the way: the Makefile tracked no header dependencies, so
editing a .h rebuilt nothing and the only safe move was `make clean`. -MMD -MP
fixes it; touching shader.h now rebuilds exactly the six objects that include
it. That one was a correctness bug wearing a performance costume.

.fastfeedback/SPEED.md records the measurements and how to write a fast test
here, so the next person does not rediscover that settle-after-a-click is free
to delete.
</content>
</entry>
<entry>
<title>sl0ppty: pane states are a table, not three hardcoded knobs</title>
<updated>2026-08-14T12:25:13Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T12:25:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7'/>
<id>urn:sha1:858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7</id>
<content type='text'>
dim_unfocused, drag_grayscale and drag_dim were three knobs describing two
situations, and there was no way to say anything about the third, fourth or
fifth. They are replaced by a table: a state names a chain, and the chain is
exactly as expressive as the global `shaders` block because it is the same
parser.

  states {
      dragging { }
      drop_target { grayscale amount=200; dim amount=140 }
      suspended { grayscale amount=180; dim amount=90 }
      scrolled { tint amount=30 color="#ffcc88" }
      unfocused { dim amount=90 }
  }

A pane is usually in several states at once, so exactly one wins: the first to
match, in a fixed order running from the most deliberate to the most ambient.
Fixed rather than read from the block, because it is a ranking by urgency
rather than a preference — a pane you are holding should not be recoloured by
anything, and a mode the whole screen is in outranks an ambient hint about one
pane. They do not stack, which is the same call the drag policy already had to
make by hand: two reasons to be grey compound into a muddy grey that reads as
neither. Defaults are the old drag behaviour and nothing else, so out of the
box this changes nothing.

drop_hover is now separable from drop_target. The pane under the pointer was
already distinguished by its border; it can now differ in colour too, and
defaults to the same chain so it does not have to.

Two states asked for did not survive contact with the code, and it is worth
writing down why:

`dead` cannot exist. app_reap() runs every iteration of the server loop, before
the paint, and closing the last pane closes the tab — so a pane whose program
exits is gone before a frame containing it is ever composed. A `dead` shader
would name something that could never draw. Making it real means keeping dead
panes until they are dismissed, which is a feature about lifetimes and not
about colour.

`suspended` could not be seen either, for a much smaller reason: draw_cb drew
its "press a key to run" label and returned before reaching the shader pass. It
falls through now. A pane that has not started is a state worth colouring, and
its label is the only contents it has.
</content>
</entry>
<entry>
<title>sl0ppty: positional shaders, and a config block to ask for them</title>
<updated>2026-08-14T12:13:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T12:13:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=6e2786c0ca394a77fe9db22362d1932728c24012'/>
<id>urn:sha1:6e2786c0ca394a77fe9db22362d1932728c24012</id>
<content type='text'>
Six passes that are pure functions of where a cell is: vignette, gradient,
zebra, ruler, margin, spotlight. The position was already in the context and
nothing had used it; these are what it was for.

The one worth having on its own merits is `ruler`. A column guide that costs
no columns, needs no support from the program in the pane, and works in
anything — an editor that has never heard of one, a pager, a shell. It marks
the background only: a guide that recoloured the text would be making the code
it marks harder to read, which is the opposite of the job. `margin` is the
same idea from the other side, everything past a column receding so an
over-long line visibly runs out of the pane you meant to use.

`spotlight` is the first thing to need something new in the context. The
cursor belongs to whichever pane is focused, so it is handed only to that
pane — otherwise every other pane's light would chase a cursor somewhere else.
It leaves everything inside its radius completely alone, because dimming the
text you are actually working on to make it easier to find would be silly, and
ramps rather than stepping at the edge.

`vignette` and `spotlight` both measure distance with a row counting double,
because a cell is about twice as tall as it is wide and an effect that claims
to be round has to say so in cells. The layout's own centre-distance already
does this. The test for it is that on a 41x21 pane — square on screen, not in
cells — the top edge and the left edge come out equally dark.

Scanlines are not a seventh shader. A CRT scanline is `zebra` with a band of
one row, and shipping the same arithmetic under two names would be pretending
they are different things.

These are not policy, so they needed a way to be asked for, and a KDL block
whose children are the passes gets it without inventing a syntax: order in the
block is order in the chain, because a chain is a sequence, and listing a
shader twice is how you get two rulers. Unknown names are refused with a
warning rather than guessed at. Empty by default — these are yours to want,
not opinions we hold.
</content>
</entry>
<entry>
<title>sl0ppty: dashed borders and a deeper push-back while dragging</title>
<updated>2026-08-14T11:59:05Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T11:59:05Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=4bb2bb5f05dda628722ae363fa4580aa69f37e7c'/>
<id>urn:sha1:4bb2bb5f05dda628722ae363fa4580aa69f37e7c</id>
<content type='text'>
Two changes to the same moment, on opposite sides of the chrome boundary.

Every pane that is not being dragged now draws a dashed border, because every
one of them is somewhere the pane could be dropped and a dashed frame says so
without needing a legend. The pane in your hand keeps a solid one, so the two
states are distinguishable by shape and not only by colour — which matters on
a terminal whose palette makes the greying subtle, and to anyone who would not
see the greying at all. The pane actually under the pointer still takes the
existing drop-target highlight on top of the dashes: these are all targets,
that one is the one you are on.

Borders are chrome, so this is not a shader and could not have been. It lives
in draw_frame, keyed off the same drag.moved that gates the greying, so a press
that turns out to be a click neither greys nor dashes anything.

The greying itself gets a second pass: desaturated *and* darkened, rather than
only desaturated. Grey alone still competes for attention at the same
brightness, which is what made the dragged pane fail to pop. Two shaders
instead of one that does both, because "colourless" and "recessed" are
different things to want and want separate knobs — drag_grayscale 200 and
drag_dim 140. This is the first thing to actually use the chain, and it is
what the chain was for.

The knob that used to mean "off" no longer does, and the test that asserted it
caught that immediately: drag_grayscale 0 now leaves drag_dim still dimming.
Rather than couple them, the test became three — both off, each alone — which
is better coverage of a two-knob effect than the single case was of a one-knob
one.
</content>
</entry>
<entry>
<title>sl0ppty: drag greys the other panes, and dim_unfocused</title>
<updated>2026-08-14T11:52:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T11:52:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c7c8111bb3d20acc926fa28db6bccc6752a5e87e'/>
<id>urn:sha1:c7c8111bb3d20acc926fa28db6bccc6752a5e87e</id>
<content type='text'>
The first two consumers of the shader pass, and between them they settle how a
pane gets one.

Dragging a pane by its title now greys every *other* pane, so the one that is
moving lifts off the page — which is the thing that made a colour pass worth
building. It engages only once the pointer has actually moved, because a press
on a title that turns out to be a click would otherwise flash the whole
session grey on its way to doing nothing. dim_unfocused fades everything but
the focused pane and is off by default: an always-on effect is the kind you
stop seeing and start fighting, so it should be asked for.

Both are derived every frame rather than attached and cleared. Focus and drags
move through far too many paths — hover, click, the finder, a close, a layout
apply, any keystroke ending a drag — to keep an attachment in sync with, and a
pane left grey by the one path that forgot is a bug that only turns up in
front of someone. The rect and the collapsed flag are recomputed every pass
for exactly this reason; this is the same rule applied to colour. It also
makes the awkward cases free: dropping into the margin, a keystroke ending the
drag, or the dragged pane being closed under it all un-grey without anywhere
having to say so, and there are tests for each because "it turns off again" is
the whole risk.

The two policies replace rather than stack. Dragging an unfocused pane while
dim_unfocused is on would otherwise give it two reasons to be grey, and they
compound into one muddy grey that reads as neither; the drag wins, so the
dragged pane is at full colour whatever the focus says.

shade_apply no longer caps at SHADE_MAX. That cap was right when the only
shaders were the ones a pane had attached, and wrong the moment the session
derives one on top of them: a full stack would have silently dropped it. The
count is the caller's to bound.

The chrome check that mattered was already there: test_drop_target_is_visible
samples a frame corner mid-drag, and it still passes, because the pass runs
between the contents and the chrome drawn over them.
</content>
</entry>
</feed>
