<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests, branch v0.1.3</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/tests?h=v0.1.3</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests?h=v0.1.3'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-09-02T13:09:36Z</updated>
<entry>
<title>attach tag: just the count, in theme colours</title>
<updated>2026-09-02T13:09:36Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-02T13:09:36Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=cafa48e0a7e1eb48a783bfde6862c5fd82c7aede'/>
<id>urn:sha1:cafa48e0a7e1eb48a783bfde6862c5fd82c7aede</id>
<content type='text'>
The +x+y offset goes -- 'N clients' says the thing that matters. The
inverse-video chip goes with it: the tag now draws in attach_fg /
attach_bg from the theme, defaulting to gray ink on the terminal's
own background. Standing information, not an announcement.
</content>
</entry>
<entry>
<title>multi-attach: say who else is watching, and where you are</title>
<updated>2026-09-02T13:03:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-02T13:03:53Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1a3ba183ae8f2613cae847c8f6460782512acc40'/>
<id>urn:sha1:1a3ba183ae8f2613cae847c8f6460782512acc40</id>
<content type='text'>
A small tag in each client's own top-right corner when several
terminals share the session: the viewer count, and on a view that is
a crop being panned by somebody else's cursor, the crop's offset into
the shared screen ("2 clients +12+4") -- which is exactly the moment
a screen moving on its own needs explaining. Stamped per client after
projection, because every terminal has its own count of others and
its own offset; the shared status bar is composed once and can be
cropped out of a panned view anyway. attach_indicator false hides
it.
</content>
</entry>
<entry>
<title>multi-attach: the config knobs</title>
<updated>2026-09-02T12:57:13Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-02T12:57:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=d698946464f25a0eb43730ffad15656f3247f56b'/>
<id>urn:sha1:d698946464f25a0eb43730ffad15656f3247f56b</id>
<content type='text'>
The patch made shared attach unconditional; these make it a policy.
multi_attach false restores the classic displacing attach
(EXIT_REPLACED, which old clients already understand). size_follows
picks whose size the shared screen takes: active (the patch's rule),
smallest (the tmux rule: filler, never a crop), or largest. The size
fold lives in apply_canvas, so any attach, resize or departure can
move the canvas under the folding policies -- including a non-active
client leaving, which under smallest may have been the very client
holding the floor down. A config reload re-applies both knobs to the
running session.
</content>
</entry>
<entry>
<title>Add competitive multiplayer support.</title>
<updated>2026-09-02T12:51:02Z</updated>
<author>
<name>Ingmar Steen</name>
<email>iksteen@gmail.com</email>
</author>
<published>2026-08-31T12:43:32Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=29061a4aca5e3d645c0e23b885342036ac48f97b'/>
<id>urn:sha1:29061a4aca5e3d645c0e23b885342036ac48f97b</id>
<content type='text'>
Last input wins, canvas size is adjusted to active connection.
</content>
</entry>
<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>graphics: a frame the client never got must be said again</title>
<updated>2026-08-29T19:31:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-29T19:31:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=8a8b5e1c7acd269d4a639a40712412a84480ad18'/>
<id>urn:sha1:8a8b5e1c7acd269d4a639a40712412a84480ad18</id>
<content type='text'>
A pane image is re-emitted to the client as decoded RGBA, which for one
screenshot is ~4MB of base64 in a single indivisible message -- and
conn_send silently dropped anything that would overflow the 4MB outbox,
while push_graphics ignored the refusal and the model advanced anyway.
Everything stateful in that dropped frame was lost for good: the image
was marked sent (so its placements drew nothing, forever), and the
deletions rode out with it (so a placement the client had was never
revoked). Reported as screenshots in a recovered pi transcript parked
at a fixed spot on the screen, immune to scrolling and pane swaps --
those were placements whose deletion left with a dropped frame. The
1000x800 one could never be delivered at all: its transmission alone
was bigger than the cap.

So the frame is now transactional. Deletions queue and are repeated
every flush until gfx_commit() confirms the client heard them;
transmissions recorded in a frame are unwound by an undelivered commit
so they are sent again. A flush nobody commits counts as undelivered at
the next gfx_begin(), which is what the `graphics` control command is:
bytes rendered for a CLI, not the client. The server reports the
verdict and keeps the paint pending until it lands.

The outbox cap now bounds the backlog rather than backlog-plus-message,
so one big transmission on a drained queue always fits -- and that
surfaced a second way to lose: a queued image legitimately holds the
backlog over the cap until the client swallows it, and push_frame read
that as a dead client and closed the connection. The session
auto-detached every time a screenshot crossed the wire. A full outbox
is a client that is *behind*, not gone: push_frame now marks the screen
for a full repaint (the dropped diff means the client's cells are
unknown) and retries as POLLOUT drains the queue; a client that truly
left still shows up as a write error there.

The headless suite pins the owed-until-committed contract; the new
tests/live_graphics.py drives the real socket with a
bigger-than-the-cap image and a reader that stalls, and would have
caught both bugs: the flood must not detach, the transmission must
arrive whole, and a placement scrolled away must be deleted -- then
come back on scroll without a second copy of the pixels.
</content>
</entry>
<entry>
<title>path: unit tests for the two spellings</title>
<updated>2026-08-28T13:10:24Z</updated>
<author>
<name>Peter Geissler</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-28T13:10:24Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1e84ca2a1e521ec3ffcb77f1a26e025e55aab90c'/>
<id>urn:sha1:1e84ca2a1e521ec3ffcb77f1a26e025e55aab90c</id>
<content type='text'>
The functions the last two commits fixed are only interesting on the
platform the suite does not run on, which is how they came to be wrong
in the first place. So the Windows cases are asserted under #ifdef and
the POSIX ones assert the opposite: that a drive letter is an ordinary
name here, that a backslash is an ordinary character, that neither rule
leaked across.

That second half is the one worth having. Both fixes are guarded, and a
guard is exactly the kind of thing a later edit removes without meaning
to -- `\` splitting a POSIX path would invent a directory out of a file
called `a\b`, and nothing else in the suite would notice.

Pure functions, so it joins the C unit tests rather than driving a
session. Run on the Windows box as well, cross-compiled: 28 checks
there, 20 here, the difference being the cases each platform has.
</content>
</entry>
<entry>
<title>ctrl_d_exits: Ctrl-D at a prompt, where the tty cannot do it</title>
<updated>2026-08-28T12:41:42Z</updated>
<author>
<name>Peter Geissler</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-28T12:41:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=2fbd36c1cef6ea6c9fcf9c36afb5209089288257'/>
<id>urn:sha1:2fbd36c1cef6ea6c9fcf9c36afb5209089288257</id>
<content type='text'>
Windows has no VEOF. The console driver does not know the character, so
^D arrives at cmd.exe and PowerShell as an ordinary keystroke and they
ignore it -- it even echoes, which is how you find out: the line reads
`echo hello^D`. The one key that closes a pane on every other platform
does nothing at all, and closing a pane becomes a word you have to type.

That you can type `exit` instead is the tell: the intent was never
ambiguous, only unspoken. So slosh speaks it. What goes to the pane is
the word, not a signal and not a close from outside -- the shell runs
its own cleanup, returns its own status, and keep_dead decides what is
left behind, all three of which reaching past it would skip. It is also
what the user would have typed, so there is nothing new to explain.

The knob is what makes it honest, because a wrong guess costs a pane.
It fires only with the shell itself in the foreground (a program that
is running may want the EOF for itself -- `cat` reading to end is the
whole job), only on the primary screen (in an editor or a pager ^D is
half a page down, and the alternate screen is the pane stating that
rather than us matching process names), and only on an empty line
(POSIX sends EOF there; what it does not do is discard what you typed).

The line is an estimate. The shell's editor is on the far side of a
pty, so pane_send_key counts what it forwarded since the last Enter.
Editing it does not model -- a recalled history entry, ^U, completion
-- leaves the count too high, which forgets to exit. That is the
direction to be wrong in; the other one throws away a half-typed
command.

Default true on Windows, false on POSIX, where the line discipline
already does this and does it properly: ^D there is also EOF for `cat`
and every REPL, none of which is ours to intercept. A knob rather than
an #ifdef so the guards are the same code everywhere and can be tested
where the test suite runs -- test_ctrl_d.py turns it on and drives all
four decisions, plus the case that does exit.

Telling trapped from forwarded needs a program that reports what it
received: on POSIX a forwarded ^D also ends an idle shell, and looking
for the word `exit` cannot separate them either, because bash prints
`exit` itself on EOF.

Verified on the Windows box as well, where it is the default: ^D at an
idle cmd.exe prompt takes the pane (and with it the session, having no
panes left), `more` in the foreground keeps its own EOF, and a
half-typed line survives with the line still on screen.
</content>
</entry>
<entry>
<title>pty: open the slave before the fork, so a pane's size reaches it</title>
<updated>2026-08-27T14:31:56Z</updated>
<author>
<name>Peter Geissler</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-27T14:31:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b380d3541d71ee2a94b4bda3b99182eddd153851'/>
<id>urn:sha1:b380d3541d71ee2a94b4bda3b99182eddd153851</id>
<content type='text'>
On Darwin a pty master has no line discipline until its slave has been
opened: every TIOCSWINSZ on the master until then fails with ENOTTY and
the size is dropped without a word. A pane is born 1x1 and told how big
it really is by the layout an instant after the fork -- and with the
child doing the open(), that message raced the child and usually lost.
Nothing ever sent it again, so the program in the pane spent its whole
life believing it had one row and one column. `C-a e` hit it every
time, because the editor's pane is always brand new: nvim drew its
screen into two rows in the corner of a 94x15 pane.

The old code knew half of this -- the child set the winsize on the
slave it had just opened, which is what kept a fresh pane from being
0x0 -- but that only fixed the *spawn* size. Every resize after it
still went to a master that might not have a slave yet.

So open the slave here, before the fork, and let the child inherit it.
The fork copies the fd table, so the slave is open from the child's
first instruction and every resize the parent sends afterwards lands;
BSD's forkpty() does the same thing for the same reason. The winsize is
set on that slave before the fork, which orders it ahead of anything
the parent sends later, and the child no longer sets it at all: by the
time the child ran, the size it was spawned with could already be
stale, and writing it back would undo the very resize this is about.

test_pty_size.py asks the pane's own program what size it thinks it is
-- for the first pane, for a pane from a split, and after a resize --
and compares it to the pane. It printed "1 1" against a 74x18 pane
before this.

live_m0.py asked `tput cols`, which needs a terminfo entry for
xterm-ghostty and therefore fails on most macs for a reason that has
nothing to do with resizing. `stty size` asks the tty itself, which is
the thing under test.
</content>
</entry>
<entry>
<title>slosh(5): the file formats get their manpage; slosh(1) learns shaders</title>
<updated>2026-08-27T13:27:51Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-27T13:27:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b5df6a7b1d73fc9740a077e8eca2d7fc8f6afe60'/>
<id>urn:sha1:b5df6a7b1d73fc9740a077e8eca2d7fc8f6afe60</id>
<content type='text'>
Section 5 is where unix keeps its file formats, and slosh has two
documents sharing one syntax -- the configuration and the layouts -- so
they share a page the way they share a parser. docs/slosh.5 covers the
KDL subset, where the file lives and how reload refuses a broken one,
include and its precedence, every top-level setting grouped the way
config.md groups them, the theme block, the shader vocabulary with the
expression language summarised, the states and their ranking, the keys
block including direct bindings, and the layout schema (tab, pane, and
their properties).

Held to the source like slosh.1 before it: every name in config.c's
KNOWN_TOP -- the list the loader refuses unknown settings against,
itself held to the loader by test_config.py -- must appear in the page,
so a knob cannot be added without this page hearing about it. The pair
must cross-reference each other, and both must be pure ASCII and lint
clean under groff -ww.

slosh(1) grows a SHADERS section -- the one feature a synopsis reader
would never guess a multiplexer had -- and its SEE ALSO now names
slosh(5) and nothing else: the other multiplexers can advertise
themselves.

Install puts the page under share/man/man5, the release tarballs carry
both pages, and the Homebrew formula installs what the commit it is
building has, guarded as before.
</content>
</entry>
</feed>
