Commit Graph

3 Commits

Author SHA1 Message Date
Levi Neuwirth d2c3e6099e feat(bottom-panel): paint and drive the GPU panel band
Bottom-panel Stage 2B-3, part 3 of 3: the band a user can see.

Declaration. `FrontendCellGeometry` ships on attach — deliberately without
a side window, because the daemon needs columns before it can paint a
first frame and gating on panel presence would deadlock the first open —
and again on resize and on every font/scale transaction. The two triggers
differ in exactly one way: `Surface` dedups an identical `CellSize`,
`Metrics` never does, because the cells can be identical while the pixels
behind them are not. That is the case daemon-side value dedup cannot see,
and it is why the epoch is frontend-owned.

Columns come from the stable normal-face probe, never `mono_advance`'s
document-glyph fallback: that fallback would make the panel's width
depend on the first glyph of whatever file is open, so two frontends with
identical metrics and different documents would derive different totals.
A probe that returns no width declares zero usable geometry rather than
reaching for a document sample.

Receipt. `Absent` is authoritative and clears; silence retains. A frame
is validated before any state is touched, so rejection is atomic and the
previous valid frame survives it. A duplicate does no work at all — no
plan rebuild, no reshape, no redraw. `Absent` does NOT discard the
geometry declaration: the frame capacity is unchanged by a panel closing.

Paint. The divider strip and the band's cells ride the existing quad and
glyph layers, in BOTH document and terminal modes — gating the band on
`terminal_mode` would make it vanish exactly when it is hosting the
output the user asked for. The strip's painted rect IS its hover/drag hit
rect, so the icon cannot advertise a target the press would miss.

Input. The band claims gestures before either document path: divider
press starts a drag, motion sends `PanelResizeRows` only when the
requested ROW count changes, bare motion inside the band is a `Move` that
neither focuses nor claims. A drag whose epochs no longer match the
panel on screen is dropped rather than applied to its successor — the
same rule the daemon enforces on receipt, checked on both sides because
neither may depend on the other having done it.

Outbox. Four more tail-only coalescing tags. Geometry is latest-wins
because epochs need only increase, not be consecutive. Resize coalesces
over the complete event including its epochs. Panel `Down`/`Up`/wheel/
context stay lossless and ordered: repeated left `Down`s are what the
daemon reads as a multi-click.

Exhaustion latches, and the latch is load-bearing beyond dropping the
frame: an old `Present` whose epoch still matched would otherwise
resurrect a band under geometry this frontend has disowned.

One pre-existing gap found and left alone: the terminal glyph layer
paints every run in one fixed color, dropping `TextRun::color`. The
panel layer resolves its runs properly rather than mirroring that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 18:40:46 -04:00
Levi Neuwirth 431d844322 feat(bottom-panel): split the GPU document bottom into three boundaries
Bottom-panel Stage 2B-3, part 2 of 3: the pixel substrate for the band.

`text_area_bottom` was three boundaries wearing one name — its own doc
comment called it "the single source for every bottom-of-text
computation" — and once a band can be installed they must diverge:

  status_band_top          = max(0, height - status_band_height)
  geometry_capacity_bottom = max(0, status_band_top - divider_height)
  document_text_bottom     = max(0, status_band_top - installed_band)

The census is 29 matches: 20 production call sites, 1 definition, 8 test
sites. All 20 were read in their enclosing function and classified
individually — 8 status-owned, 12 document-owned. A blanket rewrite that
subtracted the band from all of them would move the status chrome with
the document and pass an "everything moved" assertion, which is why the
classification is per site and the criterion asserts both directions.

The three easiest to get wrong keep their named symptoms: document
completion placement is document-owned (status-owned would overlap the
band), minibuffer candidate clipping is status-owned (the minibuffer is
global bufferless chrome anchored to the band, and clipping it at the
document boundary would cut it off), and edge scrolling is document-owned
(left on the old bottom it would auto-scroll from inside the panel).

`geometry_capacity_bottom` reserves the divider even while the panel is
absent. That asymmetry is what breaks the first-open cycle: the daemon
sizes a panel from the capacity it was told about, so a capacity that
ignored the divider would grant a first panel that does not fit once the
divider appears beside it. The document loses no pixels until a `Present`
frame is really on screen.

`PanelBandInset` is a newtype, not an `f32`, because three boundaries here
take a pixel height and only one takes this one.

Alongside it, the band's own machinery: `PanelBand` with ONE derivation of
"is a panel on screen" (`presented()` — retained valid frame, matching
geometry epoch, latch clear), the frontend-owned epoch state machine with
its fail-closed exhaustion latch, the `Absent`-is-authoritative receipt
path, `panel_cell_capacity` (no per-axis cap — a panel may legitimately be
wider than a PTY — plus the daemon's virtual status row), the stable
normal-face probe for column count, and the divider strip whose paint rect
IS its hit rect.

`TerminalPaintPlan::build_grid` factors the shared cell planner so a panel
and a terminal cannot disagree about a wide-continuation pair; terminal
selection spans stay outside it rather than being faked as empty inside.

`PANEL_MIN_VERSION` moves into `pmacs-protocol` so the GPU frontend aliases
one definition instead of restating 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 18:30:57 -04:00
Levi Neuwirth bdf2b6e4b4 feat(vterm): protocol v19 terminal frames and a native GPU terminal
Vterm Stage 3 — the final vterm stage. A semantic frontend can now host a
terminal: the daemon ships complete validated cell grids, and pmacs-gpu
renders them with fixed-cell geometry, its own input path, and no document
projection at all.

Protocol v19 appends three variants after their enums' final v18 members:
InstanceMessage::TerminalFrame (daemon-gated), and FrontendEvent::
TerminalResize / TerminalPointer (frontend-gated). It is the first bump to
gate in both directions, so criterion 28 pins each filter independently and
byte pins on StatuslineSegments and MenuPointer guard the placements.

pmacs-protocol gains src/terminal.rs: the shared row/column/visible-cell/
grapheme/metadata bounds, TerminalProcessState, TerminalSelectionSpan, and
TerminalFrame::validate — the ONE structural policy the daemon runs before
emission and the frontend runs after decode. src/terminal/* re-exports them
so no duplicate type exists, and unicode-width becomes a workspace dependency
so the screen and the validator measure glyph columns with one table. A new
8 MiB aggregate glyph bound keeps the largest legal frame (measured:
13,437,863 bytes) under the unchanged 16 MiB transport cap rather than
widening every connection's allocation ceiling.

The semantic producer suppresses the whole document family for a terminal
buffer while keeping the status band, theme, font, statusline, menu, and
minibuffer, and compares the complete ordered payload rather than
screen_generation — scroll, selection, and process state all change without
advancing it.

Two things the framing did not spell out, both found by the real-daemon
acceptance:

The Viewport gate keys on the authenticated source's ACTIVE buffer, not the
buffer the message names. Viewport also aligns the window to what it
declares, so a stale document viewport in flight when a command opened a
terminal dragged the frontend straight back off it: the window oscillated,
every terminal declaration was refused, and no frame ever arrived, with
nothing logged anywhere.

The producer clears terminal mode on every exit path. The daemon uses that
flag to suppress CursorByte and the presence sweep, so an early return that
left it set kept both suppressed after the frontend returned to a document.

pmacs-gpu/src/terminal.rs is a pure cell-space paint planner, unit-testable
without a GPU. The renderer builds one shaped buffer per text run, so a wide
or cluster glyph's advance can never choose the next column's origin.

Criterion 37 needed a seam rather than a fixture: pmacs-gpu depends only on
pmacs-protocol, so attach::connect's reader sink was generalized and a
--headless-probe mode added. The acceptance drives a real daemon, a real
/bin/sh child, the real attach client, and real composited pixels in one
path — which is how both defects above were found.

Gates: fmt; strict workspace clippy; 1,757 default + 1,933 CRDT library
tests; vterm Stage 1 9/10, Stage 2 4/4, Stage 3 4/5 acceptance
(default/CRDT); statusline 7/8; M4 120; required GPU 127; workspace sweep
2,919 across 83 suites; diff check clean.
2026-07-22 13:28:35 -04:00