<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppij.git/zellij-server/src/ui, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppij.git/atom/zellij-server/src/ui?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppij.git/atom/zellij-server/src/ui?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/'/>
<updated>2026-08-12T14:15:19Z</updated>
<entry>
<title>sl0p: add an indexed style for status bar buttons</title>
<updated>2026-08-12T14:15:19Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T14:15:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=c3764fca00c1d91c06612a33162b2354e9d42be0'/>
<id>urn:sha1:c3764fca00c1d91c06612a33162b2354e9d42be0</id>
<content type='text'>
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of
`[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds
alt+1..9 to the same actions.

A config option rather than a protocol field: whether the numbers mean anything
depends on what the USER has bound, not on what the program knows. It needs no
wire change and so no version bump, and the click report still carries the id --
never the index -- so a program is unaffected by which style is in force.

The style is threaded into layout_status_bar, the one function the renderer draws
from and the hit-test resolves against. That is what makes this safe: changing a
button's rendered width necessarily changes its clickable span too, verified by a
test asserting span width equals the drawn width and that every drawn column
resolves back, for both styles.

Also fixes the web client's Style construction to read the option from config
rather than defaulting it -- my first pass claimed that code had no pane_frames
config available, which was wrong; it uses it two lines further down.
</content>
</entry>
<entry>
<title>sl0p: highlight the status bar button under the pointer</title>
<updated>2026-08-12T13:50:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:50:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=266147d1d5ecadafda6a4893a60019c80f3c2c85'/>
<id>urn:sha1:266147d1d5ecadafda6a4893a60019c80f3c2c85</id>
<content type='text'>
A click target with no hover feedback gives you no way to tell what you are about
to hit, which matters more here than elsewhere because the buttons sit in a frame
row that is only one cell tall.

The hovered button is inverted rather than merely recoloured: swapping
fore/background reads regardless of which colours a theme uses, and makes "the
pointer is on THIS one" unmistakable next to its unhovered neighbours.

Tracked per client, because hover belongs to a pointer rather than to a pane --
with two users attached, one pointer must not light up the other's button. Stored
on the pane keyed by client id, which the frame renderer already has.

Updated before the action dispatch rather than in the hover action, because
motion over the ACTIVE pane is dispatched as SendToTerminal, not UpdateHover --
and the pane you are about to click a button in is usually the one you are
already in. Every other pane's hover for that client is cleared on each motion,
so moving between panes cannot leave a button lit behind.
</content>
</entry>
<entry>
<title>sl0p: clickable action buttons in the pane status bar</title>
<updated>2026-08-12T13:18:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:18:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=549f052ae5451d57b58f5de22de57b2a586e6d23'/>
<id>urn:sha1:549f052ae5451d57b58f5de22de57b2a586e6d23</id>
<content type='text'>
Second slice of the status/action frame. A program declares buttons, and a click
is reported back to it:

    printf '\033]5577;1;buttons;approve:Approve;reject:Reject\033\\'
    -&gt; click -&gt; program's stdin receives  OSC 5577 ; 1 ; click ; approve  ST

The report is a FIXED format carrying only the id, never bytes the payload chose,
and ids are restricted to [A-Za-z0-9_-]. The sequence that creates a button is
in-band and untrusted -- `cat` of a hostile file emits it as readily as a
cooperating program -- so a click that could send arbitrary bytes would let
viewing a file inject into the stdin of whatever is running, a shell included.
Labels are '%'-escaped since the wire format is ';'-separated, and both the
button count and id length are capped because the list lives as long as the pane.

Positions come from one layout_status_bar function that the renderer draws from
and the hit-test resolves against. Computing that geometry twice is how the
stacked-pane picker once selected the wrong pane; a test asserts every drawn
column resolves back to the button drawn there, across 100 widths.

Two things the drive caught that unit tests could not:

- The first delivery path pushed onto grid.pending_messages_to_pty, which is
  only drained when the PANE produces output -- so a click on an idle program sat
  in the queue forever. The write now happens at the tab level, which owns the
  senders.
- The hit-test derived the frame row from the pane's raw geometry, but this fork
  draws the frame inset by gap_offset, so with `gap 1` every click missed by one
  row. It now derives from the CONTENT rect, which is by definition the inside of
  the drawn frame and therefore the same columns the renderer lays out.

Buttons serialise with the session like the status text, so a resurrected pane
comes back with its buttons intact.
</content>
</entry>
<entry>
<title>sl0p: per-pane status bar via OSC 5577 (experimental)</title>
<updated>2026-08-12T12:39:46Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T12:39:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=ee50f2661a141040f7f8579efe1283a0ea5b644c'/>
<id>urn:sha1:ee50f2661a141040f7f8579efe1283a0ea5b644c</id>
<content type='text'>
First slice of the status/action frame: one row of program-supplied text in a
pane's bottom frame row.

    printf '\033]5577;1;status;building 3/7\033\\'
    printf '\033]5577;1;clear\033\\'

Rendered in the EXISTING frame row rather than in reserved rows. Reserving would
shrink the pane's content area and resize its PTY whenever the bar appeared or
disappeared, firing SIGWINCH at the program that asked for it -- the same churn
that made the stack-list picker use a fixed cap. The frame row is already a text
surface with a precedence chain, so this cost no geometry work at all.

State lives on Grid like the window title, so it survives re-renders, resizes
and detach/reattach with no draining. Session resurrection is NOT covered yet.

Versioned from the start, and unknown versions or verbs are ignored, so the
planned button verbs cannot be half-interpreted by an older build. Text is the
remaining params rejoined with ';', which means a single text field needs no
escaping -- the trick OSC 0/2 already uses for titles.

Treated as untrusted, because it is: an in-band sequence is emitted just as
readily by `cat` of a hostile file as by a cooperating program. Control
characters are stripped -- a raw ESC could otherwise repaint anything on screen,
a newline would corrupt the frame row -- and the length is capped, since the
string is retained for the life of the pane.
</content>
</entry>
<entry>
<title>sl0p: add a minimal style for the stacked-pane picker</title>
<updated>2026-08-11T18:08:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-11T18:08:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=3418e8218d91d916b3b2a45ac81d3032c27575c6'/>
<id>urn:sha1:3418e8218d91d916b3b2a45ac81d3032c27575c6</id>
<content type='text'>
The boxed entries read as clutter: every entry bracketed in vertical bars, and
the active one marked by a "&gt; " that sits outside its own cell.

`stack_list_style "minimal"` drops both. The active entry is shown by
inverting it instead, which needs no glyph and no column, and the padding is
moved inside the styled run so the inverted block covers the whole cell rather
than leaving unstyled gaps at its edges. Four fewer columns of decoration per
entry, so more fit per row in grid mode.

Uses ribbon_selected rather than text_selected. Themes give ribbon_selected
real contrast because it marks the selected item elsewhere in zellij's UI --
gruber yellow here, white-on-blue in the TempleOS theme -- whereas
text_selected's background is 40,40,40 against a 24,24,24 terminal and read as
essentially no highlight when tried.

Decoration width comes from the style itself, so Tab's column arithmetic and
the renderer cannot disagree about how wide an entry is. Default stays boxed,
so nothing changes for anyone who does not ask.
</content>
</entry>
<entry>
<title>sl0p: keep stacked-layout entries identifiable</title>
<updated>2026-08-10T22:12:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-10T22:12:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=26ef6c4e925981c86faed879204056cdc3ccbb93'/>
<id>urn:sha1:26ef6c4e925981c86faed879204056cdc3ccbb93</id>
<content type='text'>
Two problems made the stack list unusable for panes nothing had named yet,
which is the common case right after opening them -- and the responsive
collapse walks straight into it, since collapsing is exactly when the list
becomes the only way to pick a pane.

`hide_default_pane_names` no longer applies to stack list entries. In a frame
an empty title is fine: the pane is still visible, sized and positioned, so
you can tell what it is by looking at it. In the stack list the label is the
ENTIRE affordance, so blanking it leaves rows that cannot be identified or
told apart. Entries fall back to the "Pane #N" placeholder even when frames
are configured to hide it.

Entries are also prefixed with their 1-based position, so they stay countable
and addressable even when several share a title or have none. The prefix is
built by one shared helper because the renderer and Tab's column-width
calculation must agree on its width or the list misaligns.
</content>
</entry>
<entry>
<title>sl0p: grayscale the contents of unfocused panes</title>
<updated>2026-08-08T20:36:30Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T20:36:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=cfae103a7afae1541a46cbfe21b011ffe4d09f2d'/>
<id>urn:sha1:cfae103a7afae1541a46cbfe21b011ffe4d09f2d</id>
<content type='text'>
`ui { pane_frames { inactive_grayscale 0-100 } }`, default 0 (off).

Real colour transformation, not the SGR 'faint' attribute zellij already uses
for frames: faint is terminal-dependent, several terminals only apply it to
the 16 ANSI colours, and it does not remove colour. Each cell's colour is
resolved to RGB and blended toward its own Rec.709 luminance, so an unfocused
pane keeps its brightness but drains of colour.

Colour resolution covers the cases that actually occur: RgbCode directly,
ColorIndex via eightbit_to_rgb, and NamedColor by mapping to its 0-15 index.
Cells with NO explicit colour are deliberately left alone - they render in the
terminal default, which is already unsaturated, and rewriting them would mean
emitting an explicit colour for nearly every cell for no visible gain.

Two non-obvious pieces were needed to make it actually work:

- The output buffer only emits CHANGED cells, so an idle pane would keep its
  old colours indefinitely after losing focus. Pane::refresh_for_focus_change
  forces a full repaint, called from ActivePanes::insert (the single place
  focus changes). It is a no-op when the feature is off, so it costs nothing
  by default.

- Terminal panes render through the SHARED multi-client path. Focus is per
  client, so once unfocused panes are desaturated two clients may need
  different output for the same pane. Both tiled and floating panes now fall
  back to per-client rendering when grayscale is enabled - the reason this is
  opt-in.

Known limitation: sixel/kitty image chunks are not colour-transformed and stay
in colour.

Hot-reloads like the other sl0p options. Tests cover the transform: full
strength yields a true neutral grey, partial sits between original and grey,
indexed and named colours resolve rather than being skipped, luminance is
perceptual (green brighter than blue) not a flat average, already-grey is
unchanged, and non-colour codes are untouched.

Baselines held: zellij-server 1436/30, zellij-utils 436/5.
</content>
</entry>
<entry>
<title>sl0p: blank the padding ring too, fixing remaining resize ghosting</title>
<updated>2026-08-08T18:40:12Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T18:40:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=93b86ce0e0d2cec9750fb27075932f6bacd3055b'/>
<id>urn:sha1:93b86ce0e0d2cec9750fb27075932f6bacd3055b</id>
<content type='text'>
The previous ghosting fix only covered ONE of the two regions the insets
carve out. With `padding N` there is a second unowned region: the cells
between the frame border and the content rect. Nothing wrote them, so when
a pane grew, cells that had previously held another pane's border or title
kept those glyphs - the reported 'old pane title/border repeated in the gap
space'.

Both rings are now blanked at every frame-render site:
  1. allocation   \ drawn frame   (outer gap)
  2. frame inner  \ content rect  (inner padding)

Frameless mode is explicitly excluded from ring 2. With pane frames off the
cells around the content belong to ui/boundaries.rs, which draws the
inter-pane boundaries there, and content_offset deliberately leaves room for
them. Blanking that region erased the boundaries and broke three tests
(render_stacks_without_pane_frames,
swap_tiled_layout_with_stacked_children_and_no_pane_frames,
send_cli_toggle_pane_frames_action). Collapsing the interior onto the
content rect emits no ring at all in that mode.

gap_ring_chunks is now robust to any rect relationship: spans are clamped to
`outer`, so a mis-clamped caller cannot make it write outside the pane's own
allocation. Tested with inner rects that cover, miss, straddle and degenerate
against outer.

Suite back to baseline: 1421 passed / 30 pre-existing failures.
</content>
</entry>
<entry>
<title>sl0p: fix ghosting/artifacts when resizing panes with a gap</title>
<updated>2026-08-08T18:26:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T18:26:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=ccc77067e2ef230f3bc84155644b09476728b4ae'/>
<id>urn:sha1:ccc77067e2ef230f3bc84155644b09476728b4ae</id>
<content type='text'>
With a gap configured, PaneFrame::render() emits CharacterChunks only for
the INSET frame. The gap ring - the cells between a pane's allocated
geometry and the smaller frame drawn inside it - was written by nobody.

On a fresh paint that looks correct, but only because the screen had just
been cleared. As soon as panes are moved or resized, whatever was
previously drawn in those cells survives, which is the ghosting/artifacts
seen while dragging pane borders.

gap_ring_chunks() emits explicit blanks for that ring, restoring the
renderer's existing invariant that every cell of a pane's allocation is
written by exactly one pane on every render. It is appended at all four
frame-render sites (tiled + plugin panes, cached and uncached paths), and
is a no-op when gap is 0 so the default path is untouched.

Tests assert the ring never overlaps itself, never escapes the allocation,
never overdraws the frame, has exactly (outer area - inner area) cells, and
that ring + frame together tile the allocation with no holes - a hole being
exactly what produces the artifacts.
</content>
</entry>
<entry>
<title>sl0p: configurable pane title alignment</title>
<updated>2026-08-08T17:31:55Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T17:31:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=3f767ee91b08eebf10b693c08c173b8329ba2c14'/>
<id>urn:sha1:3f767ee91b08eebf10b693c08c173b8329ba2c14</id>
<content type='text'>
Adds `ui { pane_frames { title_alignment "left"|"center"|"right" } }`.

Full-frame panes previously hardcoded the title flush left, while frameless
panes (pane_frame_style "titles") already centered it via
compose_bracketed_title. This brings full frames up to parity and makes the
position configurable.

Implementation notes:
- The value rides on `Style`, which is already threaded to FrameParams, so no
  new plumbing through Tab/PaneContentsAndUi was needed.
- It is deliberately NOT round-tripped over the plugin protobuf boundary:
  plugins have no use for frame title alignment and adding a proto field
  would force regenerating the committed prost assets in assets/prost/.
- Left keeps the original code path untouched, so default configs render
  exactly as before.
- The geometry loop clamps the title start against the right-side indicators
  and breaks on `&gt;=` rather than `==`, so a mis-clamped segment cannot spin.

Tests: alignment ordering, frame width preservation across widths, and
narrow-pane safety (cols 1..24 must not panic or corrupt width).
</content>
</entry>
</feed>
