Commit Graph

4 Commits

Author SHA1 Message Date
Levi Neuwirth 664cc25d0c feat(terminal): profiles, scrollback, and a configurable escape key
Stage 1 of the terminal config/copy-mode arc. The terminal had no
configuration surface at all: the command hardcoded $SHELL, scrollback
was a per-open argument only, and the escape chord was a literal in
Rust. No protocol change.

Profiles are a raw Lua table, pmacs.terminal.profiles, not a registry
setting: ConfigValue is four scalars with no table kind, so profiles
join pmacs.lsp.config and pmacs.pair.sets until table-valued settings
exist. The registry gains three scalars whose defaults reproduce the
previous behavior exactly.

Field resolution is explicit open argument, then profile field, then
scalar setting, then $SHELL. env MERGES, with explicit entries
overriding the profile's, because first-wins there would silently drop
half a user's environment. An explicitly named profile that does not
exist is an error even when terminal.default-profile is valid, so a typo
cannot silently fall back.

The two open-time settings resolve through the GLOBAL chain, because
they are read before the identity buffer exists and no caller could have
pinned a local override on a buffer that does not yet exist. Only
terminal.escape-key resolves per buffer, which makes a per-terminal
escape a supported feature.

The escape key is parsed at most once per (terminal, config epoch), and
the cache lives on TerminalSession so its lifetime is the terminal's,
with no purge hook to forget. The epoch alone is not a sufficient key:
it does not advance when focus moves between two terminals with
different buffer-local values, so an epoch-only cache serves one
terminal's chord to the other. An unparseable value falls back to C-c
and reports once per terminal per effective invalid value through the
status line, because a terminal with no escape chord cannot be escaped
to fix the setting that broke it.

Repeating the escape now sends THAT chord to the child through the
ordinary key encoder, rather than a hardcoded ETX. With an escape of
C-x, the previous code sent Ctrl-C and made literal Ctrl-X unreachable.

C-c t opens a terminal. COHERENCE Priority 1 names a terminal
keybinding, and section 2 step 8 grades the terminal works-but-
undiscoverable; C-c is already a live global prefix, so this is a new
leaf rather than a shadow. It is unreachable from inside a terminal,
where C-c is the escape.

Acceptance is tests/terminal_config_acceptance.rs, deliberately NOT
crdt-gated so CI actually runs it. Four bites, each against a different
plausible wrong implementation: a hardcoded ETX fails acc6/9; an
epoch-only cache key fails acc7; a single last-entry cache fails acc8's
parse count; removing the invalid-value fallback fails acc10.

Two test-instrument notes worth keeping. cat -v is the echo probe
because the screen rejects C0 controls before they reach cells, so a raw
echoed Ctrl-X would be invisible. And the probe counts occurrences
rather than testing presence, because a single-character probe collides
with the child's own banner text.
2026-07-25 18:36:18 -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
Levi Neuwirth 1b32f81033 feat(vterm): establish Stage 2 view authority contracts
Add exact frontend/window/buffer view identities, logical-cell selections,
durable controller ownership, lifecycle cleanup, default-name uniquification,
and a nested authenticated interactive-command origin shared with Lua. These
contracts let the terminal core and TUI lanes proceed without inventing
parallel state.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 18:56:15 -04:00
Levi Neuwirth bbc1f33a7c feat(vterm): add Stage 1 terminal core
Add compatibility-preserving full-screen ANSI operations, the bounded terminal
screen and input encoders, and a transactional TerminalManager owning one
read-only identity buffer, PTY process, and screen per session.

Drain terminal-owned process events before process.after-tick, retain exact
final output and PID/outcome annotations, reap killed buffers and shutdown
children safely, and enforce buffer-owned read-only checks across ordinary,
host, undo/redo, and CRDT mutation paths.

Cover split parser and grapheme boundaries, screen/reflow/history invariants,
device responses, lifecycle cleanup, and a real adversarial alternate-screen
PTY. Record the fully gated Stage 1 delivery and downstream TUI/GPU contracts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 14:20:28 -04:00