<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/src/client.c, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/src/client.c?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/src/client.c?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-27T22:23:19Z</updated>
<entry>
<title>windows: a native port, on ConPTY and AF_UNIX</title>
<updated>2026-08-27T22:23:19Z</updated>
<author>
<name>Peter Geissler</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-27T22:11:24Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=d1b5a64c3855b07074b3c958739df4e16f6084a5'/>
<id>urn:sha1:d1b5a64c3855b07074b3c958739df4e16f6084a5</id>
<content type='text'>
slosh builds and runs on Windows 10/11, x86-64 and ARM64, as one exe
with no runtime dependency beyond the OS: panes, splits, tabs,
detachable sessions, the config file and its live reload, the control
socket, the mouse.

The port is a compatibility layer rather than a fork. compat/win holds
stub &lt;poll.h&gt;, &lt;sys/socket.h&gt;, &lt;termios.h&gt; and friends that redirect to
include/compat_win.h, which is force-included ahead of every translation
unit -- so the existing sources keep their POSIX includes and their
POSIX call sites, and the platform differences are answered in one
place.

Two decisions carry most of it.

Everything the event loop waits on is a socket. On POSIX a pty master,
an inotify descriptor and a unix socket are all file descriptors, so one
poll() covers them; Windows has no such union, and WSAPoll waits on
SOCKETs and nothing else. Rather than rewrite server.c's loop around
WaitForMultipleObjects -- which caps at 64 objects and cannot express
POLLOUT -- each non-socket source gets a pump thread that copies it into
one end of a loopback socketpair. The loop is then the POSIX one
unchanged, and only what sits behind the descriptor differs.

Socket descriptors are tagged rather than guessed: a wrapped SOCKET
comes back as SL_SOCK_BASE + slot, so read, write, close and poll can
tell a socket from a CRT descriptor with certainty instead of inferring
it from the numeric range. That is why socket() and accept() are shimmed
too -- an untagged descriptor escaping into the loop is the one failure
mode this scheme has, and it is worth closing completely.

Three things bit hard enough to be worth naming.

A pseudoconsole does not stop the child inheriting your stdio.
CreateProcess copies the parent's standard handles into the child when
STARTF_USESTDHANDLES is absent, and those win. With slosh's own stdout
redirected -- a pipe under ssh, a file under --script, a log under the
daemon -- the shell wrote its banner into slosh's output instead of into
the pane, and the pane had a correct title (ConPTY emits that itself)
over an empty body. The three handles are cleared for the duration of
the call, leaving the child nothing to inherit.

A ConPTY never reports EOF. The output pipe belongs to the
pseudoconsole, not to the child, so it stays open after the child exits
and pane_pump()'s read() -- the entire mechanism by which a pane learns
its program is gone -- never returns 0. A pane whose shell had been told
`exit` sat there looking alive forever. So the exit is waited for on the
process handle, and ClosePseudoConsole is what then produces the EOF.
That third thread is also why the per-pty state is reference counted: a
pump blocked on a dead pipe must not be waited for, or pty_close would
hang the session on the one program that misbehaves, so whoever finishes
last frees.

cmd.exe does not use CommandLineToArgvW's quoting. A literal quote is
\" to an ordinary program and a backslash-then-end-of-quoted-section to
cmd, so building `cmd /c notepad "C:\...\config.kdl"` the first way
handed the editor \"C:\...\config.kdl\", backslashes and all, and
notepad answered "Not a valid file name" about a path that existed.
cmd /c now gets the rest of the line verbatim -- the caller composed a
complete command with its own quoting, exactly as it does for sh -c.

Paths are mixed, and only some programs mind. slosh joins with '/' while
the environment supplies '\', so the config landed at
C:\Users\you/.config/slosh/config.kdl. The CRT opens that without
complaint, which is why --check reported it fine, but a GUI file dialog
rejects it outright. Anything handed to another program now goes through
sl_path_native() first; the internal representation is left alone.

The rest is small. Panes run %ComSpec%, sessions live in
%LOCALAPPDATA%\slosh rather than $XDG_RUNTIME_DIR because a session
socket must not follow a roaming profile to another host, there is no
SIGWINCH so a resize is noticed by comparing the console size on a
bounded poll, no fork so the daemon is a fresh detached process that
reconstructs its arguments on the command line, and no process group on
a pty so the foreground program is the youngest descendant of the pane's
shell. C-a e prefers whichever console editor is actually installed --
notepad is a GUI program, so a pane running it draws nothing and looks
broken -- and says so when it has to fall back.

make -f Makefile.windows ARCH=x86_64 (or aarch64) cross-compiles from a
mac or Linux, and builds natively on the box with zig on PATH.
</content>
</entry>
<entry>
<title>slosh: rename the project from sl0ppty</title>
<updated>2026-08-18T19:26:00Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-18T19:26:00Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b9e53cace4eb75263077df8623b011e66f5f2241'/>
<id>urn:sha1:b9e53cace4eb75263077df8623b011e66f5f2241</id>
<content type='text'>
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.

Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-&lt;uid&gt;,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -&gt; src/slosh.h, contrib/sl0ppty-dev -&gt; contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.

The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
</content>
</entry>
<entry>
<title>sl0ppty: clang-format the tree</title>
<updated>2026-08-17T12:22:38Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:22:38Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ec91ccffe631ae34b111bf6161fd0e3c487f684c'/>
<id>urn:sha1:ec91ccffe631ae34b111bf6161fd0e3c487f684c</id>
<content type='text'>
The bumpy one. `make fmt` over every `.c` and `.h` outside `vendor/`: 32 files,
1,843 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.

Two thirds of it is alignment the formatter now owns: trailing comments in a
struct, continuation lines under a signature. The rest is the hand-packed tables
opening up to one entry per line -- `ACTIONS`, `NAMED_KEYS`, `ACTION_HELP`. That
is a real loss of density and a real gain in diffs: adding an action was a reflow
of the block and is now one line. Anything worth keeping packed can be fenced with
`// clang-format off`; nothing was, on the grounds that a table is the thing a
formatter is for.

**One change is not formatting.** `src/config.h` had three trailing comments glued
onto one field:

    uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */
    /* how long an announcement stays up */ /* rows per wheel notch */

-- `toast_ms`'s and `scroll_lines`' comments, landed on their neighbour by an
earlier edit, documenting the wrong things ever since. It is in this commit
because it is what caught it: clang-format could not settle on that line, wrapping
it one way and then re-indenting the wrap on a second pass, so `make fmt` was not
idempotent. The comments are back where they belong and it is.

Verified after the reformat, not before: `make clean &amp;&amp; make all` is
warning-free, `make retest` is 1801 checks green, and `make fmt` twice in a row
changes nothing the second time. A formatter that alters behaviour would be worse
than none, so the gate was the whole suite rather than a reading of the diff.
</content>
</entry>
<entry>
<title>sl0ppty: carry the terminal's cell size, so images can have a size</title>
<updated>2026-08-14T21:13:56Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T21:13:56Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=4b380e9b4cbd24dca99bef4991541aa1aece847f'/>
<id>urn:sha1:4b380e9b4cbd24dca99bef4991541aa1aece847f</id>
<content type='text'>
"Kitty graphics pass through" was true of the tests and not of anything else.
Every test transmitted and placed in one command with explicit c=/r=; real
programs upload once with `a=t` and place per frame with `a=p`, saying
nothing about cells, and that path was broken from end to end.

A placement that does not say how many cells it covers is sized from the
image's pixels and the cell size. We never had a cell size:

- pty_spawn and pty_resize left ws_xpixel/ws_ypixel at zero, so a program
  asking its own tty how big a cell is got nothing and fell back to guessing;
- ghostty_terminal_resize was called with 0, 0 for the cell size, so lib-vt
  computed zero cells for such a placement -- and a placement covering zero
  cells is skipped, which on screen is an image that simply is not there.

So the size now runs the whole way through. The client reads it from
TIOCGWINSZ (re-read on every resize, because a window can move to a monitor
with different metrics), MSG_HELLO and MSG_RESIZE carry it -- a 4-byte
payload still means "I did not say", so an older client still attaches -- the
app hands it to every pane in every tab, and each pane gives it to both its
pty and its terminal.

Where nobody knows, the default is 8x16 rather than 0. That is the whole
lesson of this bug: a plausible cell size means an image appears at roughly
the right size, and a zero means it does not appear at all.

`{"cmd":"resize"}` takes optional cell_w/cell_h, so a script can be a client
that knows its own metrics, and so the tests can be one.

The tests now use the protocol the way programs use it, including a check
that a program can read real pixel dimensions out of its own pty -- the thing
that was silently zero. The remaining half of the fix is one layer down, in
vendor/patches/0001.
</content>
</entry>
<entry>
<title>sl0ppty: rename from sl0ptty</title>
<updated>2026-08-14T10:13:18Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-14T10:13:18Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=20f269a0cdf65f196aea09f0279f5927f26d8192'/>
<id>urn:sha1:20f269a0cdf65f196aea09f0279f5927f26d8192</id>
<content type='text'>
A pty is a pseudo-terminal, and that is what this multiplexes: the extra t
was never standing for anything. Renaming now, while the only users are the
people who typed the old name on purpose, is cheaper than renaming later.

Mechanical, but not only cosmetic. The name is also the runtime contract, so
this moves $SL0PTTY and $SL0PTTY_CONFIG, ~/.config/sl0ptty/config.kdl, the
socket directory ($XDG_RUNTIME_DIR/sl0ptty, /tmp/sl0ptty-&lt;uid&gt;), and the
identity string a program gets back from an OSC 5577 hello. A session left
running under the old binary keeps its old socket directory and is invisible
to the new one; there is no migration and it does not seem worth writing one.

The sl0ppi fork keeps its name: it is a different thing, and D1 still points
at it.
</content>
</entry>
<entry>
<title>sl0ptty: focus follows the mouse, with the guards that make it liveable</title>
<updated>2026-08-13T21:39:20Z</updated>
<author>
<name>sl0p</name>
<email>sl0p@sl0p.foo</email>
</author>
<published>2026-08-13T21:39:20Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=80425ad3683510f016a954315902c78bb1f77379'/>
<id>urn:sha1:80425ad3683510f016a954315902c78bb1f77379</id>
<content type='text'>
Hovering a pane focuses it. The feature is three lines; the reason it is not
obnoxious is the guards, and each one is a test:

  - not while the prefix is held, so a chord cannot be hijacked by a nudge of
    the mouse;
  - not through the finder, whose rows are on top of the hit list anyway;
  - not during a drag, which owns the mouse until the button comes up;
  - not onto a collapsed pane. Only 'pane:' and 'title:' targets count, never
    the frame rect's 'focus:', so the pointer crossing a stack of headers does
    not expand three panes on its way past.

Off with 'focus_follows_mouse false'; clicking still focuses either way.

This needed the client to ask for ?1003 (any motion) rather than ?1002 (motion
while a button is held), which fixes something that was quietly broken: a pane
requesting any-event tracking could never receive hover, because the outer
terminal was never asked to report it. It can now, and a pane that asked only
for clicks still gets none — both tested.

One real bug behind that: our decoder represents 'no button held' as
MBTN_UNKNOWN, and pane_send_mouse was passing it to the encoder as a concrete
button. lib-vt has ghostty_mouse_event_clear_button() for exactly this, and
without it bare motion encoded to nothing at all.

320 checks green.
</content>
</entry>
<entry>
<title>sl0ptty: M2 — server/client split, detach/reattach, control socket</title>
<updated>2026-08-13T20:52:33Z</updated>
<author>
<name>sl0p</name>
<email>sl0p@sl0p.foo</email>
</author>
<published>2026-08-13T20:52:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7'/>
<id>urn:sha1:7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7</id>
<content type='text'>
The session now outlives its client, which is the point: agents keep running
while you are gone. The server owns the panes, the layout and the renderer;
the client is a terminal in raw mode and a socket, forwarding bytes and
painting what it is sent. It does not even decode input, so the decoder can
change without redeploying clients.

  sl0ptty            attach to 'main', creating it if nobody is home
  sl0ptty -s NAME    a named session
  sl0ptty ls         live sessions
  sl0ptty cmd LINE   one control command
  C-a d              detach

Connections are not clients. A connection becomes the display client only by
sending HELLO; control commands and liveness probes are just connections and
never displace anybody. This was found the hard way, one hour after the server
first ran: 'sl0ptty ls' kicked the attached client off, because ls connects to
see whether a socket answers. That is precisely the fault that gave the zellij
fork a 20-25% CLI failure rate, rebuilt and then fixed structurally. The
regression runs 20 x (ls + control) against a live client and demands 0
failures and a client that can still type.

cmd.c now holds the command vocabulary once, shared by the headless driver and
the control socket, so a script written against one works against the other —
'sl0ptty cmd "snapshot text"' against a detached session prints its screen.

tests/test_session.py: 18 checks over create, split, probe-resistance, detach,
survival, drive-while-detached, reattach, displacement and cleanup.
123 checks green overall.
</content>
</entry>
</feed>
