<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/tests?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-10-09T10:31:53Z</updated>
<entry>
<title>tests: clang-format and ruff catch up on two files</title>
<updated>2026-10-09T10:31:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:24:53Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=d04228e9124eca3117843dfb5dfcf531d2107d07'/>
<id>urn:sha1:d04228e9124eca3117843dfb5dfcf531d2107d07</id>
<content type='text'>
No behaviour: argument wrapping in path_test, and one check() call in
test_mouse_pixels that was over the line length. Both are what the
pre-commit hook would have done anyway.
</content>
</entry>
<entry>
<title>osc 7501: say what you are doing in somebody else's protocol</title>
<updated>2026-10-09T10:31:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:24:48Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=fcf305085343bcb5c5240ed642468461f247f1ed'/>
<id>urn:sha1:fcf305085343bcb5c5240ed642468461f247f1ed</id>
<content type='text'>
OSC 5577 only works for programs that know slosh exists. The program
status protocol is the same idea written down for everybody, by ghostty's
author and implemented by ghostty and Rex, so a program that already
speaks it needs to know nothing about us:

  printf '\033]7501;state=working:app=cargo:msg=YnVpbGRpbmc=\033\\'

Implemented *as well as* 5577, not instead of it. 5577 is a two-way
control channel -- buttons answer on the program's stdin, shaders are
refused with a reason -- and 7501 deliberately is not, so neither absorbs
the other. What 7501 buys is the programs we do not control.

The scanner becomes one pass for four things: 5577, 7501, OSC 133 ; A and
RIS. The last two are here because 7501 ties record lifetime to them -- a
new prompt or a full reset is how a stale `working` dies without a
heartbeat -- and because a pane should pay for that scan once. osc5577_fn
gives way to osc_scan_cbs_t.

Reports are all-or-nothing, which is the spec's rule: no state, an unknown
state, a broken limit, base64 that will not decode, a control character in
decoded text or a malformed id discards the whole report rather than
writing a partial one. A malformed *pair* is skipped and the rest parsed.

A pane keeps up to PANE_PS_MAX records with LRU eviction, and composes the
one line it has from them by precedence -- blocked, error, done, working,
idle -- resolving `app` from the nearest ancestor that has one, the way
the hierarchy is allowed to mean something. It lands in the slot 5577's
`status` already had, because every reader of a pane wants "what is this
doing", not "which protocol said so"; `panes` reports program_state,
program_kind, program_app and progress beside it, with -1 for a progress
nobody reported, since a bar drawn at 0% says something false.

The feature-detection reply is rate-limited here. Note for the next
re-vendor: upstream lib-vt implements 7501 too, newer than our pin, and
answers the support query itself through write_pty without rate-limiting
it. See "What upstream did" under D1 in DESIGN.md before swapping this
half of the file for GHOSTTY_TERMINAL_OPT_PROGRAM_STATUS.
</content>
</entry>
<entry>
<title>tab bar: a sidebar that fits the terminal, and gives up the edge last</title>
<updated>2026-10-09T10:31:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:24:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=3a0825c6890eff566307c0eaad809cdbcf854efd'/>
<id>urn:sha1:3a0825c6890eff566307c0eaad809cdbcf854efd</id>
<content type='text'>
`tab_bar_width` was a reservation: the configured columns came off the
screen whatever the screen was, and the only fallback was all the way back
to the top row. On a narrow terminal that is chrome displacing the thing it
is chrome for, and the jump to the top moves every target you were aiming
at.

It is a ceiling now. app_tab_bar_cols takes the app, because the answer
depends on the terminal and not only on the config: held to the columns
left after a pane's collapse width, and to a third of the screen, and
narrowed rather than dropped. TAB_BAR_FIT (14) is the narrowest width this
code will invent on its own -- a config that asks for less is taken at its
word -- and 0 means there is no room for a sidebar at all, which is the one
case that still falls back on top.

Height counts too, which it did not before: a sidebar spends a row per tab,
so a wide but short terminal runs out of list before it runs out of screen,
and a tab with no row is not clipped, it is unreachable. tab_bar_min_rows
is that floor. Not when the top strip could not seat them either -- moving
them would gain nothing, so the side you configured stands.

All of it is re-derived per frame, like the layout, so a resize moves the
bar and nothing remembers that it did.

test_tab_bar also stops depending on where the suite is run from. An
unnamed tab borrows the basename of its focused pane's cwd, so eight checks
read `tests` under `make test` (which cds there) and `slosh` from the
repository root, where they failed. They are about padding, alignment and
the index, not about that derivation, so they name the tab first -- by id,
since `set-name` takes 0 for the focused *pane* only. 147 checks, green
from either directory.
</content>
</entry>
<entry>
<title>panes: $SLOSH_PANE, so a program knows which pane it is in</title>
<updated>2026-09-29T12:26:06Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-29T12:26:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=e779c1fafa556330752b2c5f43c56415f655ff24'/>
<id>urn:sha1:e779c1fafa556330752b2c5f43c56415f655ff24</id>
<content type='text'>
A program in a pane could name every pane but its own. `panes` says which
one is *focused*, and focus belongs to the human, so anything that inferred
"where do I live" from it did its work in whichever tab somebody else
happened to be looking at -- and lost a race with anybody else asking the
same question.

So export the id into the child, the way tmux has $TMUX_PANE and zellij
$ZELLIJ_PANE_ID. The id is taken before the pane is made, because pane_new
spawns the program and assigning it afterwards is one instruction too late;
it lives on the pane rather than the layout, so a rerun keeps it -- an
identity that changed under a rerun would be no identity at all. Unset, not
zero, when there is none: a stale value inherited from an outer session
would read as this one's.

`panes` also reports each pane's pid now, which is the same question
answered for anything that cannot read its own environment: walk your
parents up until one of them is on that list.
</content>
</entry>
<entry>
<title>mouse: encode pixel reports against the cell size we advertise</title>
<updated>2026-09-29T12:25:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-29T12:25:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=3760c4e99aff06d09edb0575d7596d0607298e71'/>
<id>urn:sha1:3760c4e99aff06d09edb0575d7596d0607298e71</id>
<content type='text'>
A pane is told how big a cell is twice -- in mode 2048's resize report and
in the XTWINOPS answers -- and the mouse encoder was told something else: a
1x1 cell, so that cell coordinates and "pixels" were the same number.

That held until a program turned on SGR-pixel mouse (mode 1016) by itself.
Textual does, the moment in-band resize works, and then divides every
coordinate it is sent by the pixels-per-cell we advertised: a click 30 cells
across and 9 down arrived as (3, 0), so every click in a Textual app landed
in the top-left corner.

Hand the encoder the cell size the pane already has, and put the pointer in
the middle of its cell. A size that contradicts what the same pane was told
about itself is the lie, not the precision.
</content>
</entry>
<entry>
<title>theme: the busy spinner is its own colour (tab_status_spinner)</title>
<updated>2026-09-22T13:38:08Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:38:08Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1cbfb4474ca31c61254be8d5f3ec046967bd1ff3'/>
<id>urn:sha1:1cbfb4474ca31c61254be8d5f3ec046967bd1ff3</id>
<content type='text'>
It was drawn in tab_status_busy, the colour of the words it sits beside, which
made one decision out of two. The words have to stay readable at a glance and
a mark does not: a theme may well want the glyph loud and the text calm -- or
the text in the accent and the mark a quiet dot -- and with one entry neither
is available without moving both.

Unset it follows tab_status_busy, derived after it and from whatever that
resolved to, named or derived, so nothing changes for anyone who does not ask
and no theme in contrib needs a line. `--dump-theme` writes both, like every
other colour the compositor draws.
</content>
</entry>
<entry>
<title>sidebar: room for the spinner, by default (tab_bar_padding 0 1 0 2)</title>
<updated>2026-09-22T13:24:16Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:24:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ecdc8d0c335c35a74b46ddb22773e8ef4b222891'/>
<id>urn:sha1:ecdc8d0c335c35a74b46ddb22773e8ef4b222891</id>
<content type='text'>
`│⠋bash: cd` -- the mark landed in the one column of left padding there was,
so it ended up welded to the first letter of the status. The rule was "the
mark takes a column the padding was already holding blank", which is right
about not reflowing and wrong about there being enough of it.

So the mark takes the *first* column of the indent and whatever is left over
is the air after it, and the default indent is two columns instead of one. One
column is still available -- `tab_bar_padding 0 1` -- and now means the
squashed look on purpose rather than by default; three gives two spaces; zero
still puts everything against the frame. Nothing reflows when a program starts
or stops working, which was the point of taking a column that was already
blank.

The labels and the tab numbers move with it, because the indent is the strip's
and not the status's -- a status still starts in the column its tab's name
starts in, which is the rule the leading space existed for in the first place.
</content>
</entry>
<entry>
<title>osc 5577: `busy`, and a sidebar that shows it (busy_mark)</title>
<updated>2026-09-22T13:12:02Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:12:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=720aa48e249c0c962e8f73372ceded6092414a6e'/>
<id>urn:sha1:720aa48e249c0c962e8f73372ceded6092414a6e</id>
<content type='text'>
A status is a line of text, and a line of text cannot say whether it is still
true. "building 3/7" reads exactly the same while it builds and ten minutes
after it stopped, which is the one thing somebody reading it from another tab
wants to know -- and the only workaround available to a program was to write
the answer into the words and keep rewriting them.

So `busy`:

    printf '\033]5577;1;status;make test\033\\'
    printf '\033]5577;1;busy;1\033\\'
    ...
    printf '\033]5577;1;busy;0\033\\'

A flag of its own rather than a field of `status`, because a status is the
*whole* payload after the verb -- `status;a;b;c` has always been the text
"a;b;c" -- so anything added to that line would change what every existing
sender means, including the five pi extensions D1 promises byte compatibility
to. Orthogonal, so a program can leave one line up and toggle the other;
anything but 0/false/no/off is on; `clear` takes both; and a pane whose program
has gone is never busy, whatever it last claimed, because a spinner turning
under a dead tab is a lie you keep looking at.

In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived
like the other status colours, so no theme needs a line -- and takes a spinner
in the column `tab_bar_padding` was already holding blank, so nothing reflows
when a program starts or stops working. `busy_mark` is the frames, separated by
spaces: one frame is a static mark and costs nothing, several make it turn, and
only then does the session keep a frame clock -- via the same `animating` flag
an animated shader uses, so the cost lands only while such a row is actually
composed, and `""` drops the mark entirely and leaves the colour to say it.
The frame is a pure function of the clock, like every other pulse here, so two
clients attached to one session show the same one.

`panes` now reports `status` and `busy`, which is the scriptable half of the
same fact: what a pane says about itself, without reading its screen.
</content>
</entry>
<entry>
<title>sidebar: air inside it, the way panes have padding (tab_bar_padding)</title>
<updated>2026-09-22T13:01:07Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:01:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=43ab0d0c0dc0c79f04c7e70f5ab3b22b3e36e09a'/>
<id>urn:sha1:43ab0d0c0dc0c79f04c7e70f5ab3b22b3e36e09a</id>
<content type='text'>
`padding` is a pane's: the ring between a frame and the text it holds, in rows
times gap_aspect so a `1` looks square around a block of prose. Handing the
same knob to the sidebar looked tempting and is wrong twice over. The sidebar
is chrome, not a pane, and its width is fixed by tab_bar_width -- so `padding
1` would quietly eat four of a sixteen-column strip (two cells a side, aspect
included) and shorten every tab name, because somebody wanted their terminal
text to breathe. And its two axes are not one idea: the columns are an indent
for labels, the rows are air above a list.

So tab_bar_padding, its own knob in the family the rest of the sidebar's air
already lives in (tab_bar_pad, tab_gap, newtab_pad), taking the same 1/2/4
CSS-order values as `padding` -- one reader for both, so they cannot drift
into meaning different things -- but counted in cells.

The default `0 1` is what the sidebar already drew: that one column is where
every label's leading space has been hard-coded since the sidebar existed, and
moving it into a setting is what makes `tab_bar_padding 0` possible at all --
names flush against the frame, which was not previously a thing anyone could
ask for. The statuses, the `+ new tab` row and the pane count follow the same
indent, since they were each carrying their own copy of that space.

It is air *inside* the plate: a tab's fill still spans the whole row and the
whole row is still the hit, because the row is what you click and a highlight
that stopped short of the frame would read as a highlighted word rather than a
chosen row. The bottom rows are given up when the strip is short of them, like
every other piece of air in here.

tab_bar_pad is the older name for the top component alone. It still works,
still means exactly that, and wins where a config writes both -- it is the
more specific of the two. The dump writes tab_bar_padding and mentions it.
</content>
</entry>
<entry>
<title>sidebar: align the numbers by default</title>
<updated>2026-09-22T12:49:40Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T12:49:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=9f751ab432ae74d4462fb4c3fefbd85415f47c86'/>
<id>urn:sha1:9f751ab432ae74d4462fb4c3fefbd85415f47c86</id>
<content type='text'>
The knob landed defaulting to "prefix" on the grounds that it is what the
strip has always drawn -- but the strip is not what this is about. A sidebar
row is wider than the name on it, and `1:api` spends the first two or three
cells of every row (three or four once you have ten tabs) on a number nobody
reads until they reach for C-a 2, pushing the names in by a distance that
depends on how many tabs are open. Right-aligned, the names start where the
statuses under them start and the numbers make a column of their own.

`tab_bar_index "prefix"` is still there and unchanged, and the top strip is
unaffected either way -- it ignores the setting, having no far end to align
against.

The tests that used `1:` as the anchor for "where a tab's name starts" now
anchor on the name, which is what they were about; two new ones cover the
default and the prefix form explicitly, so neither mode is tested only by
being the default.
</content>
</entry>
</feed>
