<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/src/app_draw.c, branch docs-new</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/src/app_draw.c?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/src/app_draw.c?h=docs-new'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-29T19:31:41Z</updated>
<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>compact: shared borders instead of gaps (D23)</title>
<updated>2026-08-25T12:23:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-25T12:23:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b744a90859bf248bfbea97f990668301b5fa9537'/>
<id>urn:sha1:b744a90859bf248bfbea97f990668301b5fa9537</id>
<content type='text'>
One knob, `compact true`: panes pack flush against 1-cell divider lines
that meet in real junctions, one frame rings the tab, and a pane's title
rides the line above it -- a boundary costs one cell where the default
look spends four. The lines are strokes: each write says which edges of
its cell it needs, unions with what is there, and never clobbers text,
so ├ ┬ ┼ fall out of paint order and the focus ring recolours without
inventing arms. The divider *is* the gap (eff_gap, one answer for the
layout, split_fits and the resize clamp), so gap hits, hover hints and
corner drags work unchanged. Interior edges give up click-to-split --
one cell cannot hold two verbs -- and a pane's status row overlays its
last content row. Floats, zoom and the D6 flatten keep classic frames
through place_hidden: zero new tree cases.

Underneath it, mode or no mode: a border drag now moves the boundary on
that side, converted on the first motion so the press that never moves
keeps its old meaning -- the handle's click still splits, the rim's is
still deliberately nothing (drag.rim). Which is also the fix for gap 0,
where flush panes leave no gap cell to grab and the resize gesture had
silently vanished with it.
</content>
</entry>
<entry>
<title>slosh: paint the padding ring, so a float cannot be seen through</title>
<updated>2026-08-22T16:30:18Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-22T16:30:18Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=f5ce221ee853d430113063cb85c43bbea078818e'/>
<id>urn:sha1:f5ce221ee853d430113063cb85c43bbea078818e</id>
<content type='text'>
A tiled pane sits on a cleared screen, so unpainted padding looked fine by
accident; a float sits on whatever was composited under it, and every
unpainted cell let that show through. draw_frame now fills the ring between
border and contents with exactly what screen_clear leaves, and a suspended
pane's never-composed content rect gets the same treatment.
</content>
</entry>
<entry>
<title>slosh: M10c, the memory -- floats survive being written down</title>
<updated>2026-08-20T21:53:43Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-20T21:53:43Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=64db2051192a6a6f5a9333f503d14b1ccccd699d'/>
<id>urn:sha1:64db2051192a6a6f5a9333f503d14b1ccccd699d</id>
<content type='text'>
A dumped session quietly re-tiled its floats, which was the one data-loss
gap left in D22. dump-layout now records `floating=true x= y= w= h=` -- the
*wanted* rect, because intent is what survives a session and the next
screen's layout clamps it like any frame -- layout files declare the same
(linted: floating is a bool, x/y/w/h mean nothing without it), and a file
whose every pane floats self-heals through ensure_tiled: the top float
lands, the same rule closing the last tiled pane follows, no lint required.
save-workspace inherits all of it from the shared dumper.

`float` over the API takes x/y/w/h and then *places* rather than toggles --
a placement is a statement about where, not about whether -- floating the
pane first when it is tiled, re-placing when it already floats, with
omitted fields meaning "keep". The intent is stored exactly as asked: the
write-back rule is for hands, whose drags need the clamp to agree with the
eye; a script means what it typed.

And kitty graphics respect the stack. The cell compositor gets occlusion
free from paint order, but placements are sent after the diff and would
paint an image straight over a float covering it. Each pane's placements
are now clipped by the float rects above it -- every float for a tiled
pane, the higher-raised for a float -- cropping what one clean edge can
express (the pane-edge source-rectangle arithmetic aimed at another
clipper, including from the top and left, where the source origin moves and
the sub-cell offset is consumed) and suppressing what it cannot: a strip
across the middle or a corner overlap leaves a shape one placement cannot
say, and the image returns whole when the float moves. No z= needed --
clipping is the z-order, since nothing is ever allowed to overlap.

D22's "still owed" paragraph is paid and rewritten; M10 is fully checked.
</content>
</entry>
<entry>
<title>slosh: floating panes and the drop grammar</title>
<updated>2026-08-20T21:22:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-20T21:22:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b33e8c13ff7981effc7ec2735e6aeaaaf1705af7'/>
<id>urn:sha1:b33e8c13ff7981effc7ec2735e6aeaaaf1705af7</id>
<content type='text'>
A pane can float: lifted out of the layout, drawn over the tiled panes and
under the modals, freely moved and shaped. The model is a flag and a rect on
the leaf, exactly like `minimized` -- the seat in the tree is kept, layout
skips it, the siblings absorb its share, and un-floating returns it home.
The drawn rect is the wanted one clamped to the tab every frame, never
written back, so a terminal squeeze round-trips exactly; an explicit move or
resize *does* write back, because the hand is the author of the intent.
Floating pops the pane to the centre: the jump is what says it happened.
Painted after everything tiled, so the hit list -- searched backwards --
resolves an overlap to the float you can see: the z-order and the click
order are the same list. It casts a shadow (float_shadow, one number), and
`floating` joins the states table above `unfocused`, so a float is never
dimmed as merely-unfocused.

The hands: the title moves it, any other border resizes it (a bottom corner
on both axes; the grabbed edge pins at walls and floors, never sliding the
window), H J K L move a focused float, the =/+ key grows it about its
centre, `-` shrinks it (its split was a refusal anyway), and hovering shows
which edges a grab would take, from the same derivation the press uses.
Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`,
where it reads as search); `C-a F` opens a new floating shell -- the
throwaway terminal; the function row f1..f12 became nameable in configs
along the way. Guards throughout: the last tiled pane cannot float or
minimize away, closing it lands the top float, splits are refused with
their own words, floats are not swap targets.

Dropping got a grammar: while a pane is carried, the pane under the pointer
subdivides into a swap centre and four edge bands meaning "insert me beside
this pane, on this side" -- detach_leaf + place_beside, the surgery
cross-tab moves already had. Bands are thirds (the first map of quarters
showed the swap owning half of each axis), the hovered band fills the half
the drop would hand over, the release resolves against the painted frame's
hits so it cannot disagree with the fill, and a drop between siblings along
the axis is a reorder that always fits -- refusing it was refusing the most
ordinary drag there is.

Also: rename-pane/rename-tab as palette actions (renaming was a
double-click and nothing else, the one verb the palette could not reach),
and C-u clears the rename editor like it clears the picker's query.

Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
</content>
</entry>
<entry>
<title>slosh: the logo flies into place -- a particle engine, statelessly</title>
<updated>2026-08-19T12:30:28Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T12:30:28Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=20e1e104f2254c914b464a147aa0368491cdeb0c'/>
<id>urn:sha1:20e1e104f2254c914b464a147aa0368491cdeb0c</id>
<content type='text'>
Every non-space glyph of the logo is a particle that flies to its cell
over the first two fifths of splash_ms, then *is* the logo for the
rest. There is deliberately no particle state anywhere: a position is
a pure function of (target, elapsed, seed), recomputed every frame
like the layout and the pane states and for the same reason -- physics
that is stored can drift, physics that is derived cannot. The seed is
the splash timestamp, so every attach flies differently and one splash
is coherent frame to frame; a per-particle hash staggers departures,
and the delay rescaling lands every straggler exactly at the end,
because an almost-assembled logo reads as a bug.

Five motions, picked by the clock and pinned by {"cmd":"splash",
"motion":N}: rain (fall in, column by column), scatter (converge from
everywhere), slide (rows enter from alternating sides), vortex (spiral
in around the centre, orbit shrinking and unwinding), shuffle (the
glyphs trade places, then sort themselves). The colour chains keep
playing over the box throughout, so particles catch their hue as they
cross into it.

expr_sin() is the expression language's own sine exported for C-side
animation: a motion sketched in the repl and one compiled into the
binary cannot disagree about what sin(90) is.

The tests learned two things the flights taught: the logo's opening
run appears three times per line and assembly is staggered, so a
middle occurrence can land first -- "assembled" is the row whose first
block glyph is where the match begins -- and windows need margins that
survive a parallel suite on a busy box.
</content>
</entry>
<entry>
<title>slosh: the splash colours glyphs, never backgrounds</title>
<updated>2026-08-19T12:18:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T12:18:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=258a0e037a676489bfbcfaf08ef2be0efc2b3a4d'/>
<id>urn:sha1:258a0e037a676489bfbcfaf08ef2be0efc2b3a4d</id>
<content type='text'>
The logo is half-block art: its visible pixels *are* the foreground
and the notches are the terminal's own background. A pass that painted
backgrounds therefore turned the art into a slab with a logo-shaped
hole -- so every splash pass is channel fg now, the effect lands on
exactly the art's pixels, and the backdrop stays dark for free (a
blank cell has no glyph for a colour to land on).
</content>
</entry>
<entry>
<title>slosh: the splash learns colour</title>
<updated>2026-08-19T12:16:16Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T12:16:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=4b0c912f6ece642b80014014687a4afa1b8f39e1'/>
<id>urn:sha1:4b0c912f6ece642b80014014687a4afa1b8f39e1</id>
<content type='text'>
One tint is one colour, and every effect wore the theme accent -- six
effects that were all the same dull blue. An effect is a *chain* now:
phase-shifted tints in loud literal colours, mixing per cell, which is
how a fixed-colour pass machinery does rainbows. Passes may light the
backdrop's background too, so the box is a scene rather than tinted
text, and the logo is drawn white -- the canvas that takes every hue at
full saturation, where tinting blue towards pink lands on mud.

The six: spectrum (rainbow bands), plasma (two interference patterns in
clashing hues), fire (heat up the glyphs, embers in the box), rings
(rainbow ripples), glitch (a reveal under two colours of static),
aurora (green and violet curtains over cyan).

The splash is deliberately the one place that does not defer to the
theme: it is branding wearing a demo, not chrome.

{"cmd":"splash","fx":N} picks an effect by index -- a test needs a
deterministic one, and "show me the aurora again" deserved an answer.
The new test counts dominant-channel hue families on the logo rows,
because two hundred shades of the same blue is exactly the failure it
guards against.
</content>
</entry>
<entry>
<title>slosh: greet an attach with the logo, wearing a random shader</title>
<updated>2026-08-19T12:10:52Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T12:10:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1f5b55bc323e89399ec82eca392612ce2c0167a4'/>
<id>urn:sha1:1f5b55bc323e89399ec82eca392612ce2c0167a4</id>
<content type='text'>
logo.txt is the source of truth; the Makefile bakes it into
build/logo.h the way version.h is baked. When a client attaches
(MSG_HELLO turning a connection into the display), the logo appears
centered over everything for splash_ms (900, 0 for never), on a
cleared backdrop, in the accent -- and one of six effects runs over
its rect through the ordinary shader pass: a diagonal sheen, plasma,
rings, sparkle, a left-to-right reveal, a breathe. Which one is picked
by the splash's own timestamp, so every attach looks a little
different without anybody keeping a counter.

Every effect reads the clock, so the frame clock runs only while the
splash is up (note_animation, same as any animated chain), and `since`
in a splash context is milliseconds since it appeared, so one-shot
effects sweep exactly once. Any key or click ends it and still lands
where it was aimed: a greeting is not a modal. A screen the box does
not fit on gets no greeting rather than a cropped one.

Headless and scripted sessions never attach, so the whole fast suite
never sees it; {"cmd":"splash"} triggers it on demand, which is what
makes it testable from a scripted session -- and lets you replay the
greeting whenever you like.
</content>
</entry>
<entry>
<title>slosh: split app.c into five units, mechanically</title>
<updated>2026-08-19T11:47:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-19T11:47:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ffaf9ff9db77573612317882b96158b5c7ba9655'/>
<id>urn:sha1:ffaf9ff9db77573612317882b96158b5c7ba9655</id>
<content type='text'>
app.c was 7k lines wearing section markers; now the markers are files,
cut exactly along them, no behaviour change:

  app.c          the session core: config in force, pane and tab
                 lifecycle, walking, input routing, introspection
  app_layout.c   layout, tree edits, resizing, equalising, rotating,
                 focus
  app_session.c  layout files, checking, workspaces, dumping a session
  app_draw.c     drawing, the shader passes, kitty graphics
  app_ui.c       the finder, modals, the palette, the project picker,
                 renaming

include/app_internal.h carries what the five share: the tree/tab/app
structs, CFG and the palette macros, and the 50-odd formerly static
functions the cut made cross-unit, declared where they are defined.
They keep their names; only the five units see the header.

One test moved with the code: test_portable_layouts' staleness check
greps the source for the layouts section, which now lives in
app_session.c.

Mechanical, so blame for anything in the four new files continues in
app.c before this commit.
</content>
</entry>
</feed>
