<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_responsive.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_responsive.py?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_responsive.py?h=docs-new'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-19T11:46:57Z</updated>
<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: a pane has to clear the floor in both directions</title>
<updated>2026-08-14T15:01:47Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T15:01:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=954989dc58c6f98593cc97cb1cc43a3833cb8d22'/>
<id>urn:sha1:954989dc58c6f98593cc97cb1cc43a3833cb8d22</id>
<content type='text'>
Two panes stacked on top of each other could be squeezed to four columns wide
and nothing collapsed, at any width.

The fit check lived only on split nodes, and a split node only ever asks about
the dimension it divides: a row split asks about height and never about width.
A leaf was asked nothing at all. So for a stacked layout there was no node in
the tree responsible for noticing that its panes had become unusably narrow —
the check existed, it was just looking the other way.

A leaf is the only node that can see both of its own sides, so it now checks
both. The floor is per pane and the same in every arrangement, which is what
made this a hole rather than a missing feature.

That immediately made resizing dangerous, which is the more interesting half.
Dragging a divider one cell too far could push a pane under the floor and
collapse the entire tab into a list — a drastic answer to a nudge, and it
looked like the session falling over rather than a limit being reached. So the
transfer stops at the floor instead, the way every other resizable thing stops.

The clamp went into transfer_weight rather than into the caller, because the
mouse reaches it through drag_edge and the keyboard through resize_focus, and
my first attempt clamped only the mouse. A limit that one of two paths respects
is worse than none: it looks fixed until you use the other hand.

It also has to round the weight *up*. The weight that buys exactly `floor_`
cells is a fraction, and rounding it down buys one cell fewer — which is
precisely the cell that puts the pane under the floor. The first version
rounded down and collapsed anyway, one drag later than before.

Two tests changed with real content rather than arithmetic: one used a
seventy-six by eight screen whose panes were four rows tall, which is under the
floor and should always have collapsed, and one asserted a squeezed pane stayed
"at least 3 columns" when it now stops at 24 exactly.
</content>
</entry>
<entry>
<title>sl0ppty: a collapsed pane is drawn as a pane, closed</title>
<updated>2026-08-14T14:31:52Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T14:31:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=09314c235d6ae63766f14b70417a961f3fd046fe'/>
<id>urn:sha1:09314c235d6ae63766f14b70417a961f3fd046fe</id>
<content type='text'>
Three things about the stack, and the third is the one that matters.

The hover feedback loses its filled bar and highlights the foreground instead:
the rule takes the focus colour, the title goes white and bold. A solid block
was louder than the thing it was telling you, and these rows have to sit in a
list of their own kind without shouting.

Titles get air. A header read `charlie─────` with the rule welded to the name,
which parses as one longer word; it is now `╭── charlie ───────╮`, with a space
guaranteed on each side and a configurable `title_inset` between the corner and
the title. The inset applies to the open pane's frame too, for whichever edge
its title is anchored to, so both agree about what a title looks like.

And the corners. A collapsed row is not a label that happens to sit above a
pane — it *is* a pane, closed — so it now draws the top edge of one, corners
included, in the same style the open pane below it wears. A stack of them
reads as a stack of panes rather than as a table of contents, which is what
the thing actually is. It costs two characters and a comment.

Headers stay left-anchored at the inset while the open pane keeps title_align
(centred by default). That looks inconsistent written down and right on screen:
a list is scanned down its left edge, and a frame is looked at whole.
</content>
</entry>
<entry>
<title>sl0ppty: a row in the stack lights up under the pointer</title>
<updated>2026-08-14T14:18:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T14:18:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b0e42c5ff20098fc2fa5f44cbb28cc4f7b1e11e6'/>
<id>urn:sha1:b0e42c5ff20098fc2fa5f44cbb28cc4f7b1e11e6</id>
<content type='text'>
Once a tab is a list, its rows are what you are picking from, and until now
they gave no sign of being pickable at all — a flat rule that looked the same
whether the mouse was on it or on the other side of the screen.

Lit immediately rather than on dwell. The dwell that keeps a split guide from
flashing as the pointer crosses a border is exactly wrong here: this is
feedback about where the pointer *is*, not an action being armed, and a list
you are scanning has to track the mouse continuously or it feels broken.

The finder's language for "this is the row", a solid bar, rather than a
brighter rule. FRAME_FOCUS would have read as "this pane is focused", which is
a different claim and one a collapsed row cannot make.

Hovering deliberately does not open the row. focus: already ignored everything
but a press, so this needed no guard — but it is the property worth keeping and
now has a test: a list that opened panes as the mouse crossed it would shuffle
under you while you read it. Clicking is still what opens one.

The lit row is decided from the same rect that is registered as the row's hit,
in the same function, so what lights up and what would be clicked cannot drift
apart.
</content>
</entry>
<entry>
<title>sl0ppty: a tab is laid out or it is a list, never half of each</title>
<updated>2026-08-14T14:12:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T14:12:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b2d7da47ef233c8175e27309ab0f741949420283'/>
<id>urn:sha1:b2d7da47ef233c8175e27309ab0f741949420283</id>
<content type='text'>
Collapsing was a local decision: the subtree that could not fit flattened and
everything else carried on. In practice that produces a screen with a
full-height pane on the left and a stack of headers on the right, which
explains neither what just happened nor what to do about it. Seeing it is
enough to know it is wrong.

The question is now asked of the whole tab and answered once. If any node
cannot give its children their floor, every pane in the tab becomes a row and
the focused one opens below them — including the panes that would have fitted
perfectly well on their own.

That costs something real and it is worth being honest about it: a wide screen
with one cramped column now flattens entirely, where before it would have kept
the rest of the layout. The trade is that there is one collapsed state instead
of a spectrum of partial ones, and a rule you can hold in your head is worth
more here than squeezing the last working layout out of a bad size.

Mechanically this made layout simpler rather than more complex. layout_node no
longer decides anything about presentation; a node that cannot fit sets a flag
and returns. Laying out happens twice — once to ask, once to do — and the probe
is the real layout with resizing switched off rather than a second copy of the
fitting arithmetic, because two copies of "does this fit" is exactly how the
split guide and the layout would begin to disagree. The probe not resizing is
the load-bearing part: a pane it lays out normally may be a header by the time
the real pass runs, and a pane about to be collapsed must not be told a size it
will never show.
</content>
</entry>
<entry>
<title>sl0ppty: collapsing flattens the tree instead of nesting headers</title>
<updated>2026-08-14T14:03:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T14:03:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=f655fe0958378986d5476b1e3e130f756a9f976b'/>
<id>urn:sha1:f655fe0958378986d5476b1e3e130f756a9f976b</id>
<content type='text'>
Collapse was a local decision that stayed local all the way down, so a tab
whose root did not fit could show one pane as a header and then, inside the
child that did fit, three more panes side by side. That is a degraded mode
still spending rows on an arrangement it has no room to express, next to a
pane it has reduced to a single line.

Now a subtree that does not fit flattens: every pane under it becomes one row,
in one list, with the focused one opened below them. Nesting is precisely what
there is no space for, and a reader in that state is asking which pane, not
which arrangement.

The better reason is that the old shape made panes unreachable. mark_collapsed
gave every leaf under a collapsed subtree the *same* rect, and draw_collapsed
registered one hit for first_leaf_of(). Three stacked panes therefore became
one header whose only action was focus on the first of them: the other two
could not be clicked at all, and directional focus could not separate them
either, because their rects were identical. One row per pane fixes that by
construction rather than by adding a case.

Headers sit together with the body below rather than the open pane keeping its
position in the list. Switching then moves only the content while the list you
are picking from holds still, and the tree order that an accordion would have
preserved is the thing being flattened away.

The stack's guard counts panes rather than children now, since that is what it
spends a row on each of, and layout_solo flattens the same way so there is one
mental model rather than two. subtree_has() went with them.

One regression, caught by the test that exists for it: collapse_leaf set
`content` to the header rect, and a hidden pane's content is the size the
program inside still believes it has. It is not resized on the way in and must
not appear to have been. Nothing draws a collapsed pane's content, so writing a
header rect there would only have made the lie more convincing.
</content>
</entry>
<entry>
<title>sl0ptty: M5 — responsive collapse, status bar, pane finder</title>
<updated>2026-08-13T21:14:29Z</updated>
<author>
<name>sl0p</name>
<email>sl0p@sl0p.foo</email>
</author>
<published>2026-08-13T21:14:29Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=a194e3f84299c943447c43f5d48d953a095cd4ca'/>
<id>urn:sha1:a194e3f84299c943447c43f5d48d953a095cd4ca</id>
<content type='text'>
D6 as promised: layout(tree, rect) recomputed every frame, and a split that
cannot give every child a floor (24 cols / 6 rows) collapses *itself* into a
stack — one expanded child, every sibling a one-row header. No swap layouts,
no remembered position, no synthesized anything.

Three properties fall out of the design rather than being coded:
- the expanded child is the one holding focus, so focusing a collapsed pane
  expands it: from the finder, by clicking its header, or by walking into it;
- a hidden pane is never resized, so its program does not reflow off-screen;
- a narrow/wide cycle returns the exact rects it started with, and a pane
  added afterwards is not stacked.

That last one is the fork's 'panes stacked after a responsive collapse/expand
cycle' bug, asserted directly. It cannot regress here without the layout
function growing memory it does not have.

A collapsed header shows the pane's title and its OSC 5577 status, so a
stacked agent is still legible — the monitored strip, without a 'rail' mode.

Below the size where headers fit, a node shows only the focused subtree: at
12x4 you get one pane instead of rects that do not fit on the screen. Every
size from 200x60 down to 12x4 is asserted for overlap and containment.

Status bar: tabs, a +tab button, live pane count, and the held prefix shown as
a mode. The button is '+tab', not '+', because the frame already has a '+' for
splitting — two verbs sharing a glyph is a UI bug the fork shipped, noticed,
and wrote down; the collision showed up here as two hit-list tests failing.

Pane finder (C-a f): substring filter over title, purpose and tab name across
every tab, keyboard and mouse, painted last so its rows win the hit list. It
is how you reach a pane the screen has collapsed away, which is tested.

Deferred to M6: drag-to-reorder, which wants the same drag-state machine as
pane resizing and should be built with it.

213 checks green (186 headless + 27 live).
</content>
</entry>
</feed>
