Skip to content

ADR-003: PTY Terminal Rendering and Screen Model

DateStatusDecidersRelatedConfidence
2026-04-20Accepted, amendedFizeau maintainersADR-002, ADR-004, SPIKE-001, CONTRACT-003Medium

Context

ADR-002 selects direct PTY ownership and versioned PTY cassettes. ADR-004 constrains that decision with a build-vs-buy boundary: Fizeau must adopt or wrap an existing terminal emulator rather than becoming a terminal emulator project. That still leaves a hard implementation question: how does Fizeau turn raw ANSI PTY output from real TUIs into stable screen frames for assertions, replay, and inspection?

top was spiked through a direct PTY in SPIKE-001. The spike successfully started top, sent input, resized the PTY, captured raw bytes, and rendered useful frames with a VT emulator. It also showed that raw output contains dense ANSI mode changes, cursor motion, screen clears, SGR styling, and volatile terminal content. Regex stripping is not a viable screen model.

Decision

Fizeau will implement internal/pty/terminal as a wrapper around a real VT/ANSI terminal emulator library. The project will not hand-roll ANSI parsing or rely on regex stripping for TUI assertions. The implementation bead is blocked on the ADR-004 build-vs-buy evaluation before choosing the concrete backend.

internal/pty/session owns the PTY process and raw byte stream. internal/pty/terminal consumes raw bytes and produces normalized screen snapshots, frame diffs, cursor state, terminal size, and semantic extraction helpers. internal/pty/cassette stores both the raw evidence stream and the derived frame stream.

The emulator backend is intentionally hidden behind an internal interface so it can be replaced if conformance tests expose gaps.

Key Points: real terminal emulator | raw bytes preserved | frames derived | backend replaceable

Terminal Model Contract

The terminal layer must expose:

  • byte ingestion that preserves order from the PTY reader;
  • current screen snapshot as cells or lines;
  • frame snapshots or diffs with monotonic t_ms;
  • cursor position and visibility;
  • terminal size and resize handling;
  • style metadata policy: either preserve color/style in cells or explicitly document what is dropped;
  • semantic text extraction for harness probes;
  • normalization hooks for volatile screen facts such as clocks, PIDs, elapsed durations, animation counters, and process ordering.

The terminal layer must not:

  • spawn processes;
  • write cassettes directly;
  • know Claude, Codex, quota, model, reasoning, or token-usage semantics;
  • import internal/harnesses.

Library Selection

The first implementation bead must evaluate terminal emulator candidates before locking one in. The spike proves github.com/hinshun/vt10x can render top well enough for a first pass, but candidate evaluation should also consider maintainability, Unicode/wide-character support, alternate screen behavior, resize behavior, OSC/title handling, color/style support, API fit, and test coverage.

The selected emulator backend and version must be recorded in manifest.terminal.emulator for every cassette whose frames were derived through that backend. Frame assertions either re-derive frames from raw output with the manifest-pinned emulator or fail with a clear emulator mismatch.

Candidate families include:

  • github.com/hinshun/vt10x
  • terminal model pieces used by go-expect
  • Charmbracelet/x ANSI tooling
  • other small maintained VT parser/emulator libraries with a compatible API

Conformance Tests

The PTY terminal model is not complete until tests prove behavior against real terminal programs. These tests must be fully automated through the cassette assertion framework defined in ADR-002: replay runs in collapsed time for fast CI, record mode is scripted when enabled, and no support claim depends on manual screen inspection.

TargetRequired Evidence
topCapture multiple rendered frames from one run, including initial paint, refresh, input-driven state change, and resize-driven layout change. Assertions check semantic screen facts, not full raw byte equality.
PagerA less-style flow proves scroll, quit, and alternate-screen or raw-mode behavior where available.
Full-screen TUIAn editor/curses-style flow such as vim, nano, or dialog proves cursor movement, screen redraw, and key handling.
Synthetic fixturesDeterministic ANSI fixtures cover Unicode/wide characters, style policy, cursor movement, clear-screen, scroll regions, resize races, and malformed/partial escape sequences.

Linux and macOS host smoke tests are required before promoting primary PTY support. Docker Linux conformance is useful but cannot prove host-specific PTY semantics. Windows remains out of scope until a Windows PTY adapter and fixtures are designed.

Consequences

TypeImpact
PositiveHarness probes can assert against rendered screens instead of brittle raw ANSI output.
PositiveCassettes preserve raw evidence while also carrying human-reviewable frames.
PositiveThe emulator backend can be swapped without rewriting harness adapters.
NegativeFizeau inherits terminal-emulator edge cases and must maintain a conformance suite.
NegativeTerminal rendering is more work than PTY process control alone.

Risks

RiskProbImpactMitigation
Emulator library mishandles a real harness TUIMHKeep backend behind internal/pty/terminal; require real TUI conformance fixtures before support claims
Tests become flaky due to volatile TUI contentHMSeparate semantic normalization from secret scrubbing and assert stable screen facts
Unicode or style handling loses meaningful UI stateMMAdd synthetic wide-character/style fixtures and document style preservation policy
Raw and rendered evidence divergeMMStore output.raw as authoritative evidence and derive frames through deterministic replay tests

Validation

Success MetricReview Trigger
top spike behavior is reproduced in automated conformance teststop can only be inspected manually
Terminal model handles raw byte streams, resize, input-driven redraw, and volatile normalizationHarness probes parse regex-stripped ANSI text
output.raw and frames.jsonl are both generated from the same PTY streamCassette contains frames without raw evidence
Terminal backend can be replaced behind one interfaceHarness adapters import a concrete emulator package
Cassettes record the emulator name/version used for frame derivationFrame assertions pass or fail differently after an emulator upgrade with no manifest mismatch

References

Review Checklist

  • Context names a specific problem
  • Decision statement is actionable
  • Alternatives are represented by library-selection criteria
  • Consequences include positive and negative impacts
  • Risks have mitigations
  • Validation section defines review triggers