aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-13 18:46:58 +0200
committeruser <user@clank>2026-08-13 18:46:58 +0200
commit2f0750285b1b174a552113c4aed9288f44985ae3 (patch)
treee8033ba0b3d3ac04e4e49fb4fa53f7049ac21499 /docs
parentdocs: handover (diff)
downloadsl0ppi-2f0750285b1b174a552113c4aed9288f44985ae3.tar.gz
sl0ppi-2f0750285b1b174a552113c4aed9288f44985ae3.tar.xz
sl0ppi-2f0750285b1b174a552113c4aed9288f44985ae3.zip
sl0p: make the split button visible, and clickable where it is drawn
The `+` shipped in 0.1.7 and 0.1.8 without ever appearing on a live session, while rendering in 265 snapshots. Four faults, each enough on its own: - title_line_without_middle computed the right side only inside the left side's and_then, so a pane with no title (hide_default_pane_names, the live default) got none at all; - title_line_with_middle discarded the right side whenever the left was absent -- that is every frame in a session with a second client, which the live box always had, and "MY FOCUS AND:" on every frame was saying so; - the title was budgeted before the button, so a title longer than the frame (any shell showing a path) consumed its columns. The button is anchored and hit-tested by pure geometry, so it cannot be the part that loses; - the click hit-tested position_and_size() while the frame is drawn inset by the pane gap, putting the clickable columns two right of the glyph and a row above it at the top of the screen. Hence "the click works" and "nothing is drawn" both being true, and that click test proving nothing. Adds Pane::frame_rect(), the rect the frame is really drawn in, which the status bar buttons had already had to work around by hand. Also fixes render_my_focus building its short form from separators that Vec::append had already drained: 4 cells wide while reporting 6, a title line two cells short of its pane. Found by asserting the width in the new test. Verification, since unit tests have now been insufficient here four times: split_button_is_drawn_where_it_is_hit_tested covers the combinations the live config produces, and packaging/checks/split-button-live.py (make check-live) reads the glyph off a real session, attaches a second client to force the middle builder, and clicks the columns it found the glyph in. Both confirmed to fail on the parent commit.
Diffstat (limited to 'docs')
-rw-r--r--docs/HANDOVER.md101
-rw-r--r--docs/roadmap.md31
2 files changed, 91 insertions, 41 deletions
diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md
index 467c29b..6f7f89b 100644
--- a/docs/HANDOVER.md
+++ b/docs/HANDOVER.md
@@ -2,54 +2,64 @@
Written at the end of a long session. Start here.
-## Pick this up first: the `+` split button does not render
+## Done: the `+` split button renders (and clicks where it is drawn)
**Feature:** a `+` at the right end of every pane's title line; clicking it
-splits that pane. Shipped in 0.1.7 (hover-gated) and 0.1.8 (always drawn).
+splits that pane. Shipped invisible in 0.1.7 and 0.1.8; fixed after both.
-**Status: the click works, the button is invisible on a live session.**
+It was never one bug. The title line has three builders, and the button was
+falling out of two of them, while the click was hit-testing a rect the frame is
+not drawn in:
-- clicking those 3 columns splits the pane — verified by injecting real SGR
- mouse events into an attached client's pty (`1 -> 2 panes`; clicking the title
- or mid-frame correctly does nothing)
-- the button renders in **265 unit snapshots**
-- it has never once been seen on a real session
+1. **No title, no right side.** `title_line_without_middle` only computed the
+ right side *inside* the left side's `and_then`, so a pane with an empty title
+ got none. Live sets `hide_default_pane_names true`, so unnamed panes have
+ exactly that.
+2. **A "MY FOCUS" middle dropped the right side entirely.**
+ `title_line_with_middle` fell through to `middle_only_title_line` whenever
+ the left side was absent. Any second client in the session puts a middle
+ there — including the phantom client of open item 1 below, which is why the
+ live box *always* took this path (`MY FOCUS AND:` on every frame was the
+ clue, and it was in the bug report all along).
+3. **A long title ate the button.** The title was budgeted first and the right
+ side got the remainder; shell titles are long paths. The anchored button is
+ now reserved before the title, since its hit-test is pure geometry and cannot
+ ask the renderer what fitted.
+4. **The click hit-tested `position_and_size()`**, the pane's allocation, while
+ the frame is drawn inset by the pane gap (`gap 1` → 2 columns, 1 row). The
+ clickable columns sat out in the blank gap, two right of the drawn `+`. This
+ is why "clicking works" and "nothing is drawn" were both true, and why that
+ click test proved nothing. There is now `Pane::frame_rect()` — the rect the
+ frame is actually drawn in — and both the button and (already) the status bar
+ buttons derive from it.
-Two wrong diagnoses so far, both mine:
+The centre-aligned path, the strong lead in the previous handover, was fine.
-1. "It's hover-gated and hover is broken" → removed the gate in 0.1.8, still
- invisible. (`mouse_is_hovering_over_pane` does appear to be false live, but
- that was not the only problem.)
-2. "The click test proves the feature" → it does not. The hit-test is purely
- geometric and never consults the renderer, so it kept working while nothing
- was drawn.
+**What was missing was the reproduction, not the analysis.** Two things now
+exist so this cannot come back:
-**The strong lead, untested:** every snapshot uses zellij's DEFAULT frame
-config. The live config does not:
+- `split_button_is_drawn_where_it_is_hit_tested` — asserts the button occupies
+ exactly `split_button_columns()` across alignments × rounded × titled/untitled
+ × one/two clients × 4 widths. Positional, not `contains("+")`: the short
+ multi-client indication is itself a `+`.
+- `packaging/checks/split-button-live.py` (`make check-live`) — spawns a real
+ session at a pty, reads the glyph off the screen, attaches a second client to
+ force the middle builder, then clicks the columns it *found the glyph in* and
+ confirms the split. Verified to fail on the pre-fix commit for both reasons
+ (button absent with 2 clients; click in the gap does nothing).
-```kdl
-pane_frames { rounded_corners true; title_alignment "center"; gap 1; padding 0 0 0 0 }
-```
-
-`render_title_right_side` is called from **three** different title builders
-(`zellij-server/src/ui/pane_boundaries_frame.rs`, lines ~889, ~915, ~983) with
-different `max_length` budgets. The snapshots only ever exercised the
-left-aligned path; `title_alignment "center"` takes another one.
-
-**Ten-second check before writing any code:** set `title_alignment "left"` in
-`~/.config/sl0ppij/config.kdl` on the test box, re-attach, look for the `+`. If
-it appears, that is the bug confirmed.
-
-**Then:** add a frame unit test using the live config's alignment/rounded/gap
-settings — the reproduction that was missing all along — and fix whichever
-builder drops the button.
+One more bug fell out of the width assertion: `render_my_focus` built its full
+and short forms from the same separator vectors, and `Vec::append` drains — so
+the short "ME" form rendered 4 cells wide while reporting 6, two cells short of
+the pane. Fixed.
Code involved:
| what | where |
|---|---|
-| render + geometry | `ui/pane_boundaries_frame.rs`: `render_split_button`, `split_button_columns`, `SPLIT_BUTTON_*` |
+| render + geometry | `ui/pane_boundaries_frame.rs`: `render_split_button`, `split_button_columns`, `anchored_right_side_len`, `SPLIT_BUTTON_*` |
| click | `tab/mouse_handler.rs`: `intercept_split_button_click` |
+| where the frame really is | `tab/mod.rs`: `Pane::frame_rect` (+ the two impls) |
Not drawn on floating panes on purpose: the PIN control owns that corner and is
hit-tested by its drawn position, so anything to its right moves it out from
@@ -75,8 +85,7 @@ URL afterwards.
## Open, in the order I would do them
-1. **The `+` rendering** (above).
-2. **Phantom client on headless `sl0ppi up`.** `attach --create-background`
+1. **Phantom client on headless `sl0ppi up`.** `attach --create-background`
leaves one client in the session forever, so every pane frame shows
`MY FOCUS AND:`. Worked around by creating the session *by attaching* when
there is a tty; the headless path still leaks. Ruled out: the CLI-action
@@ -86,14 +95,15 @@ URL afterwards.
`ScreenInstruction::RemoveClient` on a debug build to find where it is lost.
Cheap alternative: give the headless path its own pty (`script -qec`) and
kill the client afterwards — killed clients demonstrably do not leak.
-3. **bun is the AVX2 build.** Caused `Illegal instruction` on a plain KVM guest.
+2. **bun is the AVX2 build.** Caused `Illegal instruction` on a plain KVM guest.
One line in `packaging/fetch-vendor.sh`: `bun-linux-x64-baseline.zip`.
-4. **`pir` needs python3 + fzf**, neither shipped. The fix is folding it into
+3. **`pir` needs python3 + fzf**, neither shipped. The fix is folding it into
`sl0ppi agent --resume` (already designed in `docs/cli-design.md`).
-5. **Tab overflow.** `sl0ppi open <name>` wants to be a fuzzy picker once a root
+4. **Tab overflow.** `sl0ppi open <name>` wants to be a fuzzy picker once a root
has a dozen projects.
-6. Button polish: shift/right-click for the other split direction; reuse
- `status_bar_button_style` so it looks pressable.
+5. Button polish: shift/right-click for the other split direction; reuse
+ `status_bar_button_style` so it looks pressable. Extend `make check-live`
+ rather than trusting a snapshot.
Fixed this session, for context: client ids were recycled (silently ate ~25% of
`zellij action` output), suspended panes spammed resize errors, responsive
@@ -123,6 +133,15 @@ bar. See `docs/roadmap.md` for each with its measurements.
template into each tab instead of emitting `default_tab_template`.
- **The tab test harness does no rendering**, so `panes_to_hide` is always empty
and stack-related bugs cannot be reproduced in it. Use a scripted pty.
+- **A hit-test that never consults the renderer is not evidence of anything.**
+ The split button "worked" for two releases while being invisible. If an
+ affordance is drawn, the check has to *find the glyph* and click where it
+ found it — `packaging/checks/split-button-live.py` is the pattern.
+- **`os.read` on a settled pty blocks forever.** `os.set_blocking(fd, False)`
+ and catch `BlockingIOError`, or a live check hangs instead of failing.
+- **The pane's geometry is not where its frame is drawn.** With a gap, the frame
+ is inset from the allocation: use `Pane::frame_rect()` for anything the user
+ can see, `position_and_size()` only for the layout solver.
- **Clean up test sessions.** A background-created session I left behind is what
produced a "MY FOCUS" bug report from the user.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index bc8aa6f..98c8949 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -215,6 +215,37 @@ Two panes sharing one rect means a stack; distinct rects mean it really
expanded. Check both directions (create narrow then widen, create wide then
narrow) and add panes afterwards.
+## Fixed: the `+` split button was drawn nowhere and clicked in the gap
+
+Shipped in 0.1.7 and 0.1.8 without ever being visible on a live session, while
+rendering in 265 unit snapshots. Four separate faults, each individually enough
+to hide it:
+
+- `title_line_without_middle` computed the right side only if the pane had a
+ title -- and `hide_default_pane_names true` means unnamed panes have none;
+- `title_line_with_middle` dropped the right side whenever the left was absent,
+ which is every frame in a session with a second client (`MY FOCUS AND:`);
+- the title was budgeted before the button, so a title longer than the frame
+ (i.e. any shell showing a path) consumed its columns;
+- the click hit-tested the pane's allocation while the frame is drawn inset by
+ the pane gap, putting the clickable columns two to the right of the glyph.
+
+The first three are fixed in `ui/pane_boundaries_frame.rs` by reserving the
+anchored button before anything else and never discarding the right side; the
+fourth by `Pane::frame_rect()`, the rect the frame is actually drawn in.
+
+**How to re-run the check** -- unit tests were necessary and insufficient here
+for the fourth time, so this one reads the screen:
+
+```bash
+make check-live # packaging/checks/split-button-live.py
+```
+
+It starts a session at a pty with the live frame config, finds the `+` on the
+frame, attaches a second client (the middle builder), and clicks the columns the
+glyph was found in, expecting `2 -> 3` panes. Confirmed to fail on the commit
+before the fix, for both the render and the click.
+
## Standing risks
**`sl0ppij/target` is a symlink to `~/lol/zellij/target`.** Introduced when the