<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/include, 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/include?h=v0.1.7</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/include?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>graphics: place what a float leaves of an image, not nothing</title>
<updated>2026-09-06T17:59:13Z</updated>
<author>
<name>Ingmar Steen</name>
<email>iksteen@gmail.com</email>
</author>
<published>2026-09-06T17:59:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=af82aff3cac5370cc08b2e0d7358097bfb2d9e45'/>
<id>urn:sha1:af82aff3cac5370cc08b2e0d7358097bfb2d9e45</id>
<content type='text'>
Occlusion for placements is clipping: the cell compositor gets it free
from paint order, but images are sent after the diff and drawn over it,
so what a float covers has to be taken out of the placement itself. A
clean edge came out as a smaller rectangle; anything else -- a float
across the middle, a float on a corner, a float wholly inside -- was
refused, on the grounds that the remainder is an L and a placement is a
rectangle. The image vanished, all of it, the moment a floating pane
crossed it, on the terminal and in the mirror alike, and came back when
the float moved off.

The remainder is not one rectangle, but it is always a few: the strips
above and below what is covered, and what is left beside it between
them. So rect_subtract() cuts the visible remainder into rectangles and
each is placed in its own right, with the source rectangle the same
arithmetic the pane edge already used -- generalised to a cut on any
side, and to a first piece that keeps the sub-cell offset while the
others start on a cell boundary. Eight pieces is the ceiling (two
floats' worth); past it the old refusal stands.

The pieces of one placement need an id each, and the program's own
placement id is one number that cannot be several, so the model now
hands out the placement ids the client is told and keeps the program's
as the identity it was. `graphics` reports the piece alongside it.

Checked against a float parked over an image: the picture is cut around
the float, the strips tile exactly the image minus what the float
covers, and their source rectangles land where their cells do (a float
inside a 40x10-cell natural-size image gives 320x32 above, 320x32
below, 56x96 left and 72x96 right).

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>graphics: place an image once, not once per frame</title>
<updated>2026-09-06T17:41:18Z</updated>
<author>
<name>Ingmar Steen</name>
<email>iksteen@gmail.com</email>
</author>
<published>2026-09-06T17:41:18Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=06b22770e0ff2d03dffafed223c63d7d4608020c'/>
<id>urn:sha1:06b22770e0ff2d03dffafed223c63d7d4608020c</id>
<content type='text'>
Every frame re-emitted `a=p` for every visible placement. On a terminal
that is invisible: a kitty placement is a standing instruction, not a
per-frame one ("the other commands to erase text must have no effect on
graphics"), so a repaint of the cells under a picture does not disturb
it and saying it again says nothing new.

It is not free anywhere else. A mirror cannot know that the identical
placement is identical: shellglass re-decodes and re-encodes the image
on every one, holds the placement back while that runs, and -- because
an image change rides only in a full frame -- republishes the whole
screen each time. Measured on one image and a pane printing text for
twenty seconds, the mirror published 430 full frames and re-sent the
image list 215 times; 215 of those fulls carried no image at all,
because the placement was still mid-decode. That alternation is what
was visible in the browser: a picture that flickered while anything on
screen moved and vanished when it stopped. The same twenty seconds now
costs one full frame, 412 diffs and one image list.

So a placement is emitted only when it is new or has actually changed,
and the client's state records what it is holding. Three things have to
be right for that to be safe: a dropped frame un-marks the placements it
carried (they are owed again, exactly as transmissions and deletions
already were); a retransmit of the pixels invalidates the placement,
because kitty deletes an image's placements when its data is replaced;
and a full repaint clears the screen, which takes the images with it, so
every placement is owed again -- gfx_repaint(), not gfx_reset(), because
the pixels the terminal was sent survive a clear and a deletion still
owed from a dropped frame is still owed.

The live check drives a picture that does not move while the screen
around it repaints: one placement across 121 frames, where the old
behaviour emitted 120.

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>pane: drop the render snapshot of panes on hidden tabs -- 154K -&gt; 66K each</title>
<updated>2026-09-03T14:26:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-03T14:26:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=e3059676a6b342ea9ff07753d5b976e854d4b111'/>
<id>urn:sha1:e3059676a6b342ea9ff07753d5b976e854d4b111</id>
<content type='text'>
An updated GhosttyRenderState retains a full copy of the pane's viewport
(rows, cells, per-row arenas, styles) so renders can read it without the
terminal lock. Measured with bpftrace and a lib-vt micro-benchmark, that
copy is ~140KB for an 80x24 pane -- more than twice the terminal it
mirrors (~57KB) -- and every pane kept one forever, composed or not.

Only the current tab is ever composed, so app_compose now drops the
snapshot of every pane on the other tabs (a pointer test once dropped),
and pane_new no longer creates one at all: pane_compose and
pane_select_word build it on first use. Both already begin with
ghostty_render_state_update(), so a dropped snapshot is rebuilt, never
missed, and the tab switch already forces a full repaint.

Server memory per empty 80x24 terminal drops from 154K to 66K anon
(Superlogical reports 68K on macOS; tmux 4K, zellij 1.2-6.2M). Fifty
background tabs: 8.2M -&gt; 3.8M, reclaimed on close, and cycling through
tabs does not ratchet. What remains is the lib-vt terminal itself.
</content>
</entry>
<entry>
<title>pty: install the terminfo entry implicitly when it is missing everywhere</title>
<updated>2026-09-02T14:24:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-02T14:24:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c58a875ef3bcb464cdbea72bdbd0a874b6f93729'/>
<id>urn:sha1:c58a875ef3bcb464cdbea72bdbd0a874b6f93729</id>
<content type='text'>
The lookup miss now writes the embedded entry to ~/.terminfo and
uses it, so a bare box gets the full TERM with no step at all. An
entry that exists anywhere is respected -- possibly newer, certainly
somebody's -- and a HOME that cannot be written leaves the
xterm-256color fallback standing. --install-terminfo stays as the
by-hand spelling; the writer moved to pty.c where the panes are.
</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>
</feed>
