<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/src/app.c, branch v0.1.7</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/src/app.c?h=v0.1.7</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/src/app.c?h=v0.1.7'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-09-08T14:58:24Z</updated>
<entry>
<title>search: a scrollback search bar on /, matches highlighted</title>
<updated>2026-09-08T14:58:24Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-08T14:58:24Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=935fb7e93726dcb8ab0529c2dbcb6b5e75656794'/>
<id>urn:sha1:935fb7e93726dcb8ab0529c2dbcb6b5e75656794</id>
<content type='text'>
C-a / opens an inline search bar on the focused pane's status row, on
lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and
alt-screen survival are the library's, the UI and the wiring are ours).
Type to search live; the viewport follows the current match; up/down
(or C-n/C-p) step through them with wraparound; Enter keeps your place,
Escape snaps back. A k-of-n counter reads on the right, or "no matches".

Every match on screen is tinted, and the one the bar is on is tinted
apart -- warm gold by default, painted after the shader pass so the
scrolled wash does not dilute a match you are hunting for. Four themeable
colours (search_fg/bg, search_cur_fg/bg).

Bare / took the key from help, which keeps ?; that needed the legacy
decoder to read the shift out of a punctuation byte the way it already
did for capitals -- without it ? and / decode identically (SLASH, no
mods) and cannot be two keys on a client without the kitty protocol.

tests/test_search.py drives it end to end (39 checks): the bar, the
counter, stepping and wrap, all-match vs current highlighting, Enter/
Escape, new output updating the count, case-insensitivity, the keyboard
belonging to the bar, click-away, and that / and ? no longer collide.
</content>
</entry>
<entry>
<title>app: cut the modals, toasts and splash out of the images under them</title>
<updated>2026-09-06T18:34:26Z</updated>
<author>
<name>Ingmar Steen</name>
<email>iksteen@gmail.com</email>
</author>
<published>2026-09-06T18:34:26Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=79a2812808061129bf1449d9f4c3356b36bf0061'/>
<id>urn:sha1:79a2812808061129bf1449d9f4c3356b36bf0061</id>
<content type='text'>
Overlays are painted over the panes and the compositor needs no more
than that: painted later is on top. A placement is not painted, though
-- the client's terminal draws the image over the cells, in whatever
order they arrived -- so an overlay that is only drawn is an overlay an
image sits on top of. The cheatsheet came up under a picture and was
unreadable through it, in the terminal and in the mirror both.

So a frame now records where its overlays landed, and the graphics pass
takes them out of a placement exactly as it takes out a float: the
modal's box (the cheatsheet and the picker wear the same frame), each
toast, and the splash. The list is rebuilt every compose, so a rect that
is not registered again is one that is no longer on screen.

Only the opaque things are registered. The scrim behind a modal tints
what is behind it rather than hiding it, and an image cannot be tinted
by us at all -- so a picture beside the cheatsheet stays bright while
the text around it is pushed back, which is wrong-looking but a great
deal better than the image disappearing while a modal is up.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
</content>
</entry>
<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>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>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>selection: drag past an edge scrolls the viewport under it</title>
<updated>2026-08-27T13:09:02Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-27T13:09:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=2ee881452aa4b19701a071f0333a15265e3b314e'/>
<id>urn:sha1:2ee881452aa4b19701a071f0333a15265e3b314e</id>
<content type='text'>
Text taller than the viewport was not selectable in one gesture: the
drag stopped dead at the pane's border. Two faults, one visible and one
waiting. The visible one: DRAG_SELECT motion subtracted the content
origin in uint16_t, so a pointer above or left of the pane underflowed,
the grid lookup failed, and the selection froze. The waiting one: the
anchor was a pair of *viewport* coordinates re-resolved on every extend,
so the moment anything scrolled mid-drag the anchor would have drifted
with the viewport, re-anchoring to whatever slid under the press.

The anchor is now a tracked grid ref -- the thing lib-vt documents as
being for 'long-lived anchors such as selections' -- so it follows its
cell across scrolling, pruning and reflow, and only the near end of the
selection moves. If a program flooding output prunes the anchored row
out of scrollback entirely, the selection keeps what it had; there is no
cell left to anchor a new answer to.

Motion clamps into the content rect (a pointer past an edge is pointing
at the nearest cell), and holding past the top or bottom edge scrolls on
a clock rather than on motion, because a resting pointer sends no events
and 'keep going' is exactly what resting it there means. app_tick() does
the step, app_next_deadline_ms() wakes the poll loop for it -- the same
mechanism toasts and hover guides already use -- and both front ends
call the tick, so the behaviour exists under the server and the headless
harness alike. Rows per step is how far past the edge the pointer sits:
one row past creeps, further hurries, so the hand already dragging is
also the throttle. The metronome is select_scroll_ms, a knob so the test
can configure the clock down instead of sleeping.

The test drags to one row above the pane and simply holds: the viewport
walks to the top of scrollback (clamping makes the endpoint exact), the
copy starts at a line that was never on screen with the press, and still
ends at the pressed row -- that last check is the tracked anchor's.
</content>
</entry>
<entry>
<title>macos: the kernel still knows, it just spells /proc differently</title>
<updated>2026-08-26T14:11:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-26T14:11:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3'/>
<id>urn:sha1:087b2bd1d31dac24f8b0ffe79c33f4ebf1f60bd3</id>
<content type='text'>
Portability fixes contributed by our macOS developer, applied with one
hardening tweak (below).

Darwin has no /proc, so everything that read one gets the native answer:
live_cwd via proc_pidinfo(PROC_PIDVNODEPATHINFO), the foreground command
via sysctl(KERN_PROCARGS2) parsed back into the NUL-separated shape
/proc/&lt;pid&gt;/cmdline gives, and SLOSH_BIN via _NSGetExecutablePath plus
realpath. The pty gets its winsize set on the slave after opening it,
because Darwin attaches no line discipline to the master until then and
the earlier TIOCSWINSZ was silently dropped -- a pane born 0x0. The
shader repl binds tab in libedit's own words, since macOS ships Python's
readline as a libedit wrapper that accepts GNU syntax in silence.

Two finds are cross-platform bugs: a toast wider than the screen minus
the gap computed a negative x that uint16_t made ~65533, drawing the
longest messages -- the ones most worth reading -- off screen; now the
middle is elided (a path's tail is the half you needed) and x clamps.
And a pane label was 128 bytes, which a Homebrew interpreter's full path
exceeds before its arguments even start; dump-layout wrote the
truncation down and a restore ran it, so it is 1024 now.

Saving a layout learned cwd_as_named: the kernel answers with the
physical path, and on macOS /tmp is a symlink to /private/tmp, so a
layout that said cwd="/tmp" stopped round-tripping through a save.
If realpath(named) is where the pane still is, the named spelling wins.

Tests follow: realpath'd tmpdirs, the scroll test reads the shell's
first prompt instead of spelling out bash 5.3, and wrapped pane text is
joined before matching a long path.

One tweak on top of the contribution: the elision needs five columns
for a head, the dots and a tail -- below that `avail - 3` goes
negative, and negative as size_t is a pointer into the weeds; a
terminal that narrow gets a plain clip.
</content>
</entry>
</feed>
