<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_pane_title.py, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/tests/test_pane_title.py?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_pane_title.py?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-18T13:24:25Z</updated>
<entry>
<title>sl0ppty: the middle of a border is the button, not all of it</title>
<updated>2026-08-18T13:24:25Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-18T13:24:25Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=8231838e92e5a31e85f53f8efa1b2a1e407e7cd1'/>
<id>urn:sha1:8231838e92e5a31e85f53f8efa1b2a1e407e7cd1</id>
<content type='text'>
Clicking a pane's border to split toward it is the right gesture and was the
wrong target. A border is the longest thing on the screen and it sits between
two things you did mean to click -- a title, a gap, the pane beyond it -- so
reaching a cell too far changed the layout, and the top row was worse than the
sides: clicking a pane's header to focus it split it upward.

So the target shrank. Each side carries a *handle* of three to seven cells in the
middle, and only the handle splits; the rest of the edge is the *rim*, which
arms the guide and does nothing when pressed. Three is the floor the pane
buttons already settled on for the reason written there -- a one-cell target is
a thing you miss with a mouse -- and seven is the ceiling, because a third of a
tall pane's border is most of that border again and brings the accidents back
with it.

Hover now answers two questions in two stages, because they are two questions.
Anywhere on an edge, the edge goes heavy and the handle thickens by a cell into
the pane: that says where the button is, which is the thing you can no longer
infer. On the handle, the dashed boundary and the arrow as well: that says what
the click will do. The hint follows the same split, so sweeping an edge neither
draws a boundary nor offers a split you cannot get from where the pointer is.

Three things this had to get right.

**The thickened handle is registered, not just painted.** The guide adds the
hit for the inner strip in the frame that draws it, and reads the outer rect
back from the hit list rather than recomputing it, so there is one opinion about
where the handle is (D6). A handle you can see and cannot click would be worse
than either mistake alone.

**The top row cannot put its handle in the middle.** The buttons own its right
and a centred title -- the default -- owns exactly the middle. The title winning
is the existing rule, and it is what keeps a double-click rename from splitting
twice on its way, so the handle is placed last, in the widest run of cells the
row has left and nearest the middle among equals. A row with nothing left offers
no upward split; its other three edges are unaffected. Each handle cell is then
painted only if the hit list still agrees it belongs to the handle, so a block
never lands on a pane's name.

**The row keeps the drag and loses the split.** `title:&lt;id&gt;` takes no side now
and `title:&lt;id&gt;:t` -- the handle -- carries one, which is why the press path
reads the side out of the action instead of assuming it. Press-and-hold skips
the dwell on a handle, where a press means something, rather than anywhere on an
edge.

Verified: `make retest`, 1801 checks, all green. Nine of them are new or
rewritten: the two-stage guide, the caption belonging to the handle, the rim
splitting nothing on any of the four sides, the top row's handle still splitting
upward on a titled pane, and the rim of the top row not. Tests read the handle's
cell out of the hit list (`Snapshot.handle`/`.rim`) rather than deriving the
geometry, so a test cannot agree with a broken formula.

Also checked by hand on a live session, which is where the placement rule earned
itself: hovering the rim draws the heavy edge and the block and no dashed line,
hovering the handle adds the line and the arrow, three clicks on pane headers
split nothing, and on a pane titled `sl0ppty` the top handle lands to the right
of the title and left of the buttons -- `drag to move · click to split up` at
x=42, `drag to move` either side of it.
</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: `set-name target="pane"`, so a script can overrule a program's title</title>
<updated>2026-08-17T11:38:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:38:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=7a91885c3b063c8d231daceb2b2e29e1f1a81aa6'/>
<id>urn:sha1:7a91885c3b063c8d231daceb2b2e29e1f1a81aa6</id>
<content type='text'>
A pane's name already outranks the title its program sets -- `pane_title` prefers
the name while there is one -- and that is the whole remedy for a program
announcing something it stopped meaning: an agent still spinning the summary of a
task it finished, a build script that titled itself once. The remedy was reachable
by double-clicking the title on somebody's screen, and from a layout file, but not
from the socket, while `set-purpose` next to it has taken `target:"pane"|"tab"` all
along.

`target` defaults to `tab`, which is what this verb has always meant. Two verbs
whose defaults differ is a smaller wrong than a verb that quietly changes its
subject under scripts already written against it. The refusals say `no such pane`
or `no such tab`, so a mistyped target shows up in the reply rather than as a label
that did not move.

The gesture now calls the same function, so the two ways in cannot disagree about
where a pane's name lives -- there is a test that a name set over the socket is the
name the double-click editor offers to edit.

Verified: eight checks in `tests/test_pane_title.py`. A pane named while its
program keeps setting OSC 2 titles keeps the name; clearing it hands the label back
to what the program calls itself *now*; `id: 0` is the focused pane and leaves the
other one alone; `set-name` with no target still renames the tab and leaves the
pane's title alone.
</content>
</entry>
<entry>
<title>sl0ppty: a floor under splitting, and an arrow saying which way</title>
<updated>2026-08-14T12:45:49Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T12:45:49Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=037c3550f075ff78e680e5a1f1508d4c292ffcb1'/>
<id>urn:sha1:037c3550f075ff78e680e5a1f1508d4c292ffcb1</id>
<content type='text'>
Two changes to the same affordance.

A border stops being a button below `min_split cols=32 rows=8`. This is a
different question from min_pane, which is where the layout gives up and
collapses a pane: two twenty-five column panes technically fit and are not
worth having, so the number that decides whether to *offer* a split is larger
than the one that decides whether a pane can survive. It is clamped to never
go below min_pane, because refusing to offer what the layout would collapse
anyway is the floor under the floor.

The test is the layout's own arithmetic rather than an approximation of it:
the same floor, the same gap, and the same child count the split would create
— which is one more sibling when the parent already splits that way and two
when it does not. The guide's standing claim is that drawing and the layout
cannot disagree, and a guide promising a split that immediately collapsed
would be exactly that.

The keyboard obeys the same floor, with a toast. A rule only the mouse
followed would mean the same session splits or refuses depending on how you
asked, and whether there is room cannot depend on that. The control API and
layout files are *not* bound by it: a script asking for a pane is declaring
what it wants rather than being offered something, and D6's responsive
collapse is what catches it there. That distinction was not free — app_split_pane
went through the same helper as the keybinding, so the API was silently bound
too until a test noticed, and the interactive path is now its own function.

The guide also gained an arrow on the dashed line, pointing at the half the
new pane will take. The line already said where the boundary lands and left
which side to be inferred from which border you happened to be touching.

Pointers for left and right rather than the matching triangles: U+25C0 and
U+25B6 carry emoji presentation and are widely rendered double-width, while
screen_text books every chrome glyph as exactly one cell — so one that drew as
two would shift the rest of the row. U+25B2 is already the scroll indicator,
so the vertical pair is known good here.

Raising a floor invalidates every test that quietly relied on the old one, and
nine of them did: sessions sized for a world where a sixty-column terminal
could be split into columns. They are sized for the rule now, and the two that
were really about *layout* collapse rather than the affordance now split over
the API, which is the path that still means "do it anyway".
</content>
</entry>
<entry>
<title>sl0ppty: the pane title is a name — double-click it to rename</title>
<updated>2026-08-14T10:14:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T10:14:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1ceafa4d82bdc37449c174a75fcd38b4be880080'/>
<id>urn:sha1:1ceafa4d82bdc37449c174a75fcd38b4be880080</id>
<content type='text'>
The top row was both the frame's drag handle and its top edge: resting on it
armed the split guide, and a click that never moved split upward. That is
right for a border and wrong for the cells the title sits on, because it left
no gesture free for naming a pane — a double-click there split twice.

So the title text carves its own hit region out of the top row. It is
registered after the row's handle and after the title is painted, which is
what makes it correct: hit_test scans backwards, so the region that wins is
the one whose glyphs the user can actually see, and its width comes from
screen_text's return so a title of wide characters claims the cells it drew.
Resting there arms nothing (draw_split_guide matches "title:" exactly), and a
press takes no side, so the click-splits path — now guarded on a side being
set — does nothing. Dragging still moves the pane: that reads as grabbing it
by its name. Everything else on the top row still splits upward.

Renaming happens in the title cell itself. There is no dialog to place,
nothing to dismiss, and the pane it belongs to cannot be in doubt. It is the
finder's shape: state on app_t, a key handler that owns the keyboard while it
is open, drawn by the pass that draws the frame. Enter keeps the name, Escape
abandons it, clicking away keeps it the way leaving a field does.

A typed name shadows the program's title rather than overwriting it. The
program's OSC 2 keeps arriving underneath, so a shell that rewrites its title
cannot undo a rename, and clearing the name gives the pane back to what the
program calls itself *now* rather than to whatever it happened to say when the
rename began. That is the whole reason pane_t grew a second string instead of
a flag.

Backspace deletes a character rather than a byte — a title is whatever a
program could set, and half a UTF-8 sequence is not a name — and a name too
long for the frame scrolls its head off, because the caret is the one thing
that has to stay visible. double_click_ms (400) joins hover_delay_ms in the
config.

The tests keep a control beside each behaviour, since most of these could pass
by breaking something else: two slow clicks must open no editor, the top edge
must still split, keystrokes must not reach the shell, and a pane closing
mid-rename must give the keyboard back.
</content>
</entry>
</feed>
