fix(tui): the title bar's escape hatch outranks the document name (improvise-t14d)

`fill_line` composed the title bar's two segments by concatenating them and
letting ratatui clip. ratatui clips the RIGHT, and the right segment is
" ?:help  :q quit " -- the only thing on screen telling a stuck user how to get
help or get out. The left segment is decoration whose length is unbounded, so
the affordance was the first thing sacrificed: measured through a TestBackend, a
document named "Quarterly Regional Sales Analysis" stored as
quarterly-regional-sales-2026.improv loses ":q quit" at 100 columns.

Two smaller faults in the same three lines. The padding was `left.len()` --
BYTES -- and the title's own format string spells a "·", two bytes and one cell,
so every document's right segment sat one cell in from the edge it is anchored
to, before any non-ASCII name made it worse. And there was no truncation at all,
so the failure was a mid-token clip of "?:" rather than a graceful drop.

`Keeps` makes the priority a per-call-site decision rather than a global rule,
because it genuinely differs: the title bar keeps the RIGHT (the hatch outranks
the name), while `minibuffer_line` and `draw_status` keep the LEFT, which is what
they already did -- there the left is the user's own typing plus the cursor, or
the status message, and right priority would let a long message eat the text
being edited. Truncation goes through ui::text::truncate, so widths are display
cells throughout and an overflow is an ellipsis.

Five tests, each rendered through a real TestBackend because the loss happens in
ratatui's clip and a helper's return value cannot see it. Three faults injected,
each caught by a distinct test; the reasoning and the injections are in
context/test-evidence/tui.md and context/code/tui.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-22 18:25:01 -07:00
parent 7400421af8
commit 6bd6cb5771
3 changed files with 255 additions and 5 deletions
+29
View File
@@ -154,6 +154,35 @@ composes both: the prompt and buffer on the left, any pending `status_msg`
right-aligned. The alternatives and why they lost are in
`context/test-evidence/tui.md`.
### `fill_line` — which segment yields is a per-call-site decision
Three rows in the TUI are one left segment and one right segment sharing a line:
the title bar, `minibuffer_line` and `draw_status`. `fill_line` composes them,
and it owes two things that it did not originally do — it must measure in
**display cells** (it padded with `left.len()`, bytes, and the title's own `·`
is two of them, so every document's right segment sat one cell in from its edge)
and it must **truncate**, because concatenating and letting ratatui clip clips
the *right*.
What clipping the right costs depends on the row, which is why `Keeps` is a
parameter and not a rule:
- **The title bar keeps the right.** Its left segment is decoration whose length
is unbounded — a document name plus a filename — and its right segment is the
fixed `" ?:help :q quit "`, the only thing on screen telling a stuck user how
to get out. Unconditional left priority made the escape hatch the first thing
sacrificed, at 100 columns with an ordinary document name (improvise-t14d).
- **Both bottom-bar rows keep the left**, which is what they already did. There
the left is the user's own typing plus the cursor (`minibuffer_line`) or the
status message (`draw_status`), and the right is a view badge or the message's
own margin. Right priority there would let a long message eat the text being
edited — the same mistake in the other direction.
So the rule is not "the right wins" but "the affordance wins over the
decoration", and which is which is local. Truncation goes through
`ui::text::truncate` (improvise-8l0c), so an overflow is a `…` rather than a
mid-token clip of `?:`.
### The caches are built here, per frame
`project::formulas_of`, `category_tree_of`, `view_list_of`, `tile_bar_of`,