<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppij.git/zellij-utils/src/input/theme.rs, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppij.git/atom/zellij-utils/src/input/theme.rs?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppij.git/atom/zellij-utils/src/input/theme.rs?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: 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: optionally render the stacked-pane picker as a grid</title>
<updated>2026-08-11T14:33:46Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-11T14:33:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=4e001669f5a9dcc883f58bf0c3710c7460ae93cc'/>
<id>urn:sha1:4e001669f5a9dcc883f58bf0c3710c7460ae93cc</id>
<content type='text'>
`stack_list_layout "grid"` packs picker entries several per row, which turns
stack_list_max_rows from a count into a space budget: eight panes in a wide
terminal now fit on one row instead of spending three and hiding five.

The entry renderer already drew each entry as a single chunk at its own x/y,
sized by its geom and centring its own label, so a grid needed no rendering
changes at all -- only the loop assigning every entry the same x and the full
width. Column count is derived from the widest label rather than configured, so
it adapts to the titles; one column is the floor and reproduces the previous
full-width rows byte for byte.

The row -&gt; member mapping now has a column axis, and it had already caused one
wrong-pane bug when two consumers disagreed. So the layout is computed once,
by one function, into explicit cells; the renderer, the reserved-row budget and
the mouse hit-test all read those same cells rather than each doing their own
arithmetic. Windowing is by row, so a grid scrolls a whole row at a time and
degrades to the old per-member window when there is one column.

Cells are separated by a two-column gap: without it the focused entry's "&gt; "
marker sits flush against the previous cell and reads as belonging to it.
</content>
</entry>
<entry>
<title>sl0p: cap how many rows the stacked-pane picker takes</title>
<updated>2026-08-11T12:14:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-11T12:14:53Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=9441f1b177c1e08f4ceae76ef70d444103d15efa'/>
<id>urn:sha1:9441f1b177c1e08f4ceae76ef70d444103d15efa</id>
<content type='text'>
A stack reserves one row per member plus a padding row, so a deep stack spends
most of the screen listing itself. `stack_list_max_rows` caps that; the list
windows around the focused member and reports the rest as "+N more".

The indicator goes in the padding row the list already reserves, so it costs
no extra row and never displaces a real entry. Entries keep their real rank
rather than renumbering from 1, otherwise the numbering stops meaning anything
once the window has scrolled. The window slides on ordinary focus movement,
which is also why this needs no new keybinding -- Action is a protobuf enum
and the prost assets cannot be regenerated here (no protoc).

A fixed cap rather than hover-to-expand on purpose: reserved_top_rows feeds
pane geometry, so growing the list on hover would resize the pane's PTY and
reflow whatever is running in it on every mouse move. A real expand/collapse
has to be an overlay that reserves nothing, which is a much larger change to
the render path.

reserved_top_rows and the renderer both go through stack_list_window(); if
they disagreed the list would draw into rows the geometry never reserved, i.e.
over the pane beneath it.
</content>
</entry>
<entry>
<title>sl0p: collapse to a swap layout when the terminal gets narrow</title>
<updated>2026-08-10T22:12:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-10T22:12:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3'/>
<id>urn:sha1:393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3</id>
<content type='text'>
Splitting a window into thirds is great until the window is 70 columns wide
and each pane gets 23. `responsive_min_cols` collapses the tab into the swap
layout named by `responsive_layout` when it gets narrower than that, and
restores the previously active layout when it grows back.

Selects a swap layout BY NAME rather than adding a size-aware
LayoutConstraint: that type crosses both the plugin protobuf API and the
client/server contract, and the committed prost assets cannot be regenerated
here (no protoc).

Restoring uses a 10-column hysteresis band. Without a dead band, dragging a
window edge that lands exactly on the threshold flips the layout on every
motion event, and every flip resizes every PTY in the tab.

"Did the user rearrange panes by hand" is judged from the damaged flag as
sampled BEFORE each resize, not the live one: resize_whole_tab's own
auto-layout pass sets that flag itself and clears it again only if it found a
layout that fits, so on a terminal too narrow for any of them it latches true
although nothing was rearranged. And once collapsed, restore stays reachable
regardless of the flag, so damage occurring while collapsed cannot strand the
tab in the narrow layout.
</content>
</entry>
<entry>
<title>sl0p: optionally hide the 'Pane #N' placeholder title</title>
<updated>2026-08-08T20:47:16Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T20:47:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=9aa323fe6b9d15360ba54a1c73a216228664a289'/>
<id>urn:sha1:9aa323fe6b9d15360ba54a1c73a216228664a289</id>
<content type='text'>
`ui { pane_frames { hide_default_pane_names true } }`, default false.

A pane now shows no title at all until something actually names it: the user
renaming it (including by double-clicking the title row) or the running
program setting a title. Upstream always shows an auto-generated 'Pane #N'.

Implemented by tracking whether pane_title is still the constructor's
placeholder, NOT by matching the string: a program that legitimately titles
itself 'Pane #3' must not be mistaken for an unnamed pane. set_title clears
the flag, since something then chose that title.

current_title reads the option at render time rather than baking an empty
title in at construction, so it hot-reloads and turning the option back off
restores the placeholder on existing panes.

Display precedence is unchanged and now explicit in the code:
    user-given name &gt; title set by the running program &gt; placeholder

render_terminal_title already fell back to an empty string rather than the
placeholder, so the terminal WINDOW title is unaffected.

Empty titles were already handled by the frame renderer
(render_title_left_side returns None for an empty title), including the
sl0p centred/right alignment path.

Baselines held: zellij-server 1436/30, zellij-utils 436/5.
</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: CSS-style 1/2/4-value shorthand for inner padding</title>
<updated>2026-08-08T19:06:40Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T19:06:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=97b744a57e3ba53169781a9357d8dc86b15d387e'/>
<id>urn:sha1:97b744a57e3ba53169781a9357d8dc86b15d387e</id>
<content type='text'>
`padding` now accepts:
  padding 1          -&gt; all four sides
  padding 1 2        -&gt; up/down = 1, left/right = 2
  padding 1 2 3 4    -&gt; up=1 down=2 left=3 right=4

NOTE the 4-value order is UP DOWN LEFT RIGHT as requested, deliberately NOT
CSS's clockwise top-right-bottom-left. Documented on PanePadding and at the
parser, since the similarity to CSS invites the wrong assumption.

3 or 5 values are rejected with a message naming the accepted forms, rather
than being silently reinterpreted. Negative values clamp to 0.

ASPECT: only the 1-value form is aspect-corrected (left/right multiplied by
gap_aspect) so `padding 1` looks square on a non-square cell grid. The 2- and
4-value forms are literal: specifying axes separately means the caller is
already controlling horizontal size, and silently doubling it would surprise.

Style now carries a PanePadding {top,bottom,left,right} instead of an x/y
pair, which also collapses the reconfigure plumbing back to a single field
rather than growing another parameter per side.

to_kdl always emits the explicit 4-value form: emitting the 1-value form
would re-apply gap_aspect on the next read and silently double the padding
on every config round-trip.

Baselines held: zellij-utils 435/5, zellij-server 1421/30.
</content>
</entry>
<entry>
<title>sl0p: uniform pane gap + aspect-corrected horizontal gap</title>
<updated>2026-08-08T18:58:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T18:58:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=c21f7324abdb6d83300a62b95e2af27bfcc75a1e'/>
<id>urn:sha1:c21f7324abdb6d83300a62b95e2af27bfcc75a1e</id>
<content type='text'>
Two changes to how the gap is defined.

1. UNIFORM GAP. Previously every pane inset every side by `gap`, so two
   neighbours each gave up a cell and the space between them was 2*gap while
   the screen edge got only gap. Now a pane insets RIGHT and BOTTOM always,
   and LEFT/TOP only where that edge is the viewport boundary. Two neighbours
   contribute exactly one gap between them, and the outer screen edges get
   the same gap. `gap 1` now means 1 everywhere.

   The per-side offset is computed in tiled_panes, which is the layer that
   knows the viewport, and pushed to panes via Pane::set_gap_offset before the
   content-offset sweep. Floating panes keep a zero gap: they are positioned
   explicitly by the user, so an inter-pane gap does not apply to them (they
   still get padding).

2. ASPECT CORRECTION. Terminal cells are roughly twice as tall as they are
   wide, so an equal number of rows and columns does not look like an equal
   distance. `gap` and `padding` are now expressed in ROWS, and columns are
   multiplied by `gap_aspect` (default 2, minimum 1). Style carries the
   resolved pane_gap_x/y and pane_padding_x/y.

FrameConfig::Default is now hand-written rather than derived: gap_aspect must
default to 2 to match the KDL parser, otherwise a parsed config disagrees with
a default-constructed one and it surfaces as config round-trip diffs (this
broke can_define_ui_configuration_in_configfile and both
bare_config_from_default_assets_to_string tests).

Tests assert the actual invariants: stacked and side-by-side neighbours get
exactly one gap between them, screen edges get the same gap as between panes,
and inset_by refuses to collapse a pane too small to survive it.

Baselines held: zellij-server 1421/30, zellij-utils 433/5.
</content>
</entry>
<entry>
<title>sl0p: configurable pane gap (outer) and padding (inner)</title>
<updated>2026-08-08T17:49:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-08T17:49:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=30782cbb2fab4e01cadb9815927a404591472064'/>
<id>urn:sha1:30782cbb2fab4e01cadb9815927a404591472064</id>
<content type='text'>
Adds `ui { pane_frames { gap N; padding N } }`, both defaulting to 0 so
stock configs are unaffected.

  gap     - cells each pane gives up on every side, leaving real empty
            terminal between neighbouring panes (WM-style outer gaps).
            Two adjacent panes therefore show 2*gap between them.
  padding - cells between a pane's frame and its content; breathing room
            inside the pane without moving the frame.

Design: the pane KEEPS its allocated PaneGeom. Only two things move:

  1. the drawn frame, via Viewport::inset(gap), reusing the existing
     frame_geom_override mechanism already used for stacked panes;
  2. the content, via Offset::expanded_by(gap + padding) inside
     set_content_offset.

Because the allocation is untouched, the kasuari resize solver and mouse
hit-testing need no changes at all, and a click landing in a gap still
resolves to the owning pane. Content position, size and cursor are all
derived from content_offset, so widening it in one place keeps every
consumer consistent.

Viewport::inset refuses to shrink a pane below a frame's minimum viable
extent (two borders + one content cell) rather than emitting a degenerate
or zero-sized frame. Negative KDL values clamp to 0 instead of wrapping.

Tests cover: inset identity/shrink/recentre, the containment invariant
(an inset frame never escapes its allocation), small-pane refusal, and
the key safety property that content inset by gap+border+padding lands
strictly inside the frame drawn at inset(gap).
</content>
</entry>
</feed>
