Skip to content

feat: terminfo#106

Draft
natemoo-re wants to merge 5 commits into
mainfrom
feat/terminfo
Draft

feat: terminfo#106
natemoo-re wants to merge 5 commits into
mainfrom
feat/terminfo

Conversation

@natemoo-re

Copy link
Copy Markdown
Member

What does this PR do?

Adds a terminal capability layer that answers one question for both the renderer and the input parser: what can this terminal do?

  • new queryTermInfo() reads the terminal's terminfo file, checks COLORTERM, and asks the terminal directly (theme colors, truecolor, kitty keyboard/graphics/pointer-shape, synchronized output) — all folded into one capability struct in shared wasm memory
  • createTerm({ terminfo }) and createInput({ terminfo }) attach to the same handle, so both sides agree on what the terminal supports
  • the renderer now emits the best color format the terminal is known to support: truecolor → 256-color → 16-color. With no evidence, we now emit 256-color by default, truecolor is gated on detection
  • frames are wrapped in synchronized output (mode 2026) when the terminal confirms support, so they paint atomically without tearing
  • if a capability changes mid-session (a query answer arrives after the first frame), the next frame is a full redraw so nothing stale stays on screen
  • the input parser loads terminal-specific key sequences from terminfo and silently consumes query replies so they never leak into key events
  • fully mockable: injectable env, terminfo bytes, and stdin/stdout streams — tests run without a PTY, and the probe never rejects (missing terminfo, non-TTY, or timeout just means fewer confirmed capabilities)

specs/terminfo-spec.md is new and normative; the input and renderer specs gained matching sections. Commit bodies carry the deeper mechanism (probe fence, shared-memory instancing, fixture generation).

Closes #60

Type of change

  • Bug fix
  • Feature
  • Refactor (no behavior change)
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • All tests pass
  • Files are formatted
  • I have added/updated tests for my changes
  • I have added a changes

AI-generated code disclos

  • This PR includes AI-ge

Adds specs/terminfo-spec.md defining the shared capability layer:

- One shared WebAssembly.Memory with three tenants (terminfo region,
  renderer heap, input heap); both instances receive explicit region
  pointers plus a capability struct pointer. Split-module builds must
  link with disjoint static footprints (TINV-7).
- A fixed-layout TermInfo struct: generation counter, max_colors,
  capability flag bits, probe-confirmation bits, and a theme group
  (OSC 10/11/12 foreground/background/cursor).
- Progressive enhancement (TINV-5): a conservative xterm-256color
  baseline owned by the terminfo module, raised only by positive
  evidence with precedence baseline < terminfo entry < environment
  (COLORTERM) < probe response. This deliberately supersedes the
  renderer's historical unconditional truecolor emission.
- Sans-IO probe (TINV-6): a query batch (OSC 10/11/12, kitty OSC 21
  color, kitty OSC 22 pointer shape, XTGETTCAP RGB;Tc, DECRQM 2026,
  kitty keyboard, kitty graphics) fenced by DA1, with responses
  recognized by the input parser during normal scans. queryTermInfo()
  is the single blessed entry point; every environmental dependency
  (env, terminfo bytes, streams, timeout) is injectable.

Updates input-spec.md: the terminfo option becomes the TermInfo handle;
new normative section 6 covers key_* trie loading and query-response
recognition (responses are consumed silently and never leak into
events); terminfo parsing leaves the deferred list.

Updates renderer-spec.md: new section 7.6 (capability-gated emission)
specifies the color-encoding ladder (truecolor / 256 / 16), bce-gated
erase, the mode-2026 synchronized-output frame wrap, and
generation-triggered full redraw; INV-7 and section 11.2 are amended so
the shared capability layer and the frame-scoped sync wrap cannot be
read as violating renderer/input independence or the no-terminal-state
boundary.

References: OSC 8 spec (egmontkob gist), kitty color-stack and
pointer-shapes protocols, Ghostty synchronized-output guidance.
Implements the terminfo-spec capability core:

- src/terminfo.{c,h}: the TermInfo capability struct (generation,
  colors, flags, confirmed, theme group), the xterm-256color baseline
  init, and terminfo_parse for both storage formats (legacy 0432 and
  extended-number 01036) including the extended capability table
  (RGB/Tc/Su/Smulx). Parsing is bounds-checked everywhere and
  all-or-nothing: malformed input returns a nonzero code without
  touching the struct (TINV-3). A successful parse replaces the
  standard capabilities the entry owns — absent booleans clear, colors
  becomes the entry's max_colors — matching ncurses semantics where an
  entry fully describes its terminal. terminfo_grant applies
  creation-time evidence (COLORTERM) and bumps the generation only on
  actual change.

- terminfo.ts: queryTermInfo() as the single blessed entry point.
  Locates entries via the ncurses search path (TERMINFO, ~/.terminfo,
  TERMINFO_DIRS, compiled-in defaults; letter and hex directory
  layouts; traversal-safe, magic-validated — ported from the
  bombshell-dev/ui feat/terminfo spike), parses into a fresh shared
  WebAssembly.Memory with a bump allocator (the renderer and input
  parser will attach to the same memory in follow-ups), applies
  COLORTERM evidence, and runs the sans-IO probe batch (OSC 10/11/12,
  kitty OSC 21/22, XTGETTCAP RGB;Tc, DECRQM 2026, kitty keyboard and
  graphics, DA1 fence) over injectable streams. It never rejects on
  environmental grounds: missing entries, non-TTY streams, timeouts,
  and aborts all resolve with whatever evidence was gathered. Raw mode
  is saved and restored around the probe window.

- test/terminfo.test.ts + embedded fixtures (tasks/gen-fixtures.ts):
  real xterm-256color, tic-compiled extended-caps entry, hand-built
  01036 entry (macOS ships ncurses 6.0, which predates that format),
  16-color downgrade entry. The extended-block layout (name offsets
  relative to the names sub-table) was verified against tic output
  byte-by-byte.

The probe window currently closes on timeout only; DA1 fence
recognition lands with the input parser integration.
Implements input-spec section 6, the input parser's two roles in the
capability layer:

Key sequences (6.1): input_init gains terminfo bytes and a TermInfo
pointer. key_* string capabilities (arrows, kf1-kf12, khome/kend/
kich1/kdch1/kpp/knp, kcbt — indices verified empirically against tic
output, including the alphabetical kf10=67 quirk) seed the sequence
trie before the xterm defaults; trie_add is first-writer-wins, so
terminfo sequences take precedence while defaults stay registered for
anything the entry omits.

Query responses (6.2): a new parse_response path in the scan loop
recognizes and silently consumes capability replies, writing them into
the shared struct with one generation bump per logical update:

- OSC 10/11/12 theme color reports (rgb:/# forms, 1-4 hex digits per
  channel, BEL or ST terminated)
- OSC 21 kitty color reports (key=value; fills theme, sets kittyColor)
- OSC 22 kitty pointer shape reports
- XTGETTCAP DCS replies (1+r naming RGB/Tc confirms truecolor; 0+r
  denies it — probe evidence outranks the terminfo entry)
- DECRPM reports (mode 2026 grants/denies syncOutput; other modes are
  consumed silently)
- kitty keyboard flag reports (CSI ? u)
- kitty graphics APC replies (;OK grants, error payloads deny)
- DA1 device attributes reports, which also set the TERMINFO_DA1
  fence marker in confirmed for the queryTermInfo probe window

Responses are recognized only behind tight prefixes (OSC number in
{10,11,12,21,22}, DCS [01]+r, APC G, CSI ?) so Alt+]/Alt+P/Alt+_
keystrokes keep their existing behavior, and unterminated responses
are abandoned after 1KB. Responses interleave with user input and
buffer across scan calls without leaking bytes into adjacent events.
A parser with no handle consumes responses into a private struct so
stray replies never corrupt the event stream.

createInput({ terminfo }) now takes the TermInfo handle (the raw
Uint8Array form moved to queryTermInfo({ terminfo: bytes })); the
parser instantiates against the handle's shared memory and allocates
its arena from the handle's bump allocator. A handle can be attached
to at most one Input (terminfo-spec 10.3).
Gates the renderer's output on the shared capability struct:

- Color encoding ladder: emit_attr routes colors through emit_color,
  which picks truecolor (38;2), 256-color (38;5, nearest 6x6x6 cube or
  24-step grayscale ramp, tmux/xterm colour_find_rgb approach), or
  16-color (nearest of the xterm default ANSI palette, 30-37/90-97)
  from the frame's capabilities. With no evidence the baseline emits
  256-color SGR — this deliberately supersedes the historical
  unconditional truecolor output per the progressive-enhancement
  invariant (TINV-5); truecolor now requires terminfo, COLORTERM, or a
  probe reply.

- Generation invalidation: reduce() compares the struct generation
  against the last emitted frame and invalidates the front buffer on
  change, so a mid-session capability upgrade (e.g. an XTGETTCAP reply
  confirming truecolor) produces a complete redraw with no stale cells.

- Synchronized output: when mode 2026 is probe-confirmed, non-empty
  cursor-update frames are wrapped in CSI ?2026h/l within the same
  output buffer. Empty frames drop the wrap entirely and line-mode
  output is never wrapped. The output buffer gets explicit headroom so
  a saturated frame cannot truncate the closing wrap.

- init() takes a TermInfo pointer (NULL = private baseline struct);
  createTerm({ terminfo }) attaches to a handle's shared memory, with
  at most one Term per handle (terminfo-spec 10.3).

Attached consumers now reuse the handle's WASM instance instead of
re-instantiating the module over the shared memory: a fresh
instantiation rewrites the module's data segments, which clobbered
Clay's already-initialized static context (caught by the
generation-redraw test as an indirect call through zeroed state).

Existing tests that asserted truecolor SGR against handle-less terms
now attach COLORTERM evidence via test/caps.ts helpers, and the
baseline expectation is covered by new 256-color tests.
Completes the queryTermInfo probe window: response bytes are fed
through a probe-private input parser over the handle's shared memory,
so replies fold into the capability struct as they arrive and the DA1
device-attributes report — which every terminal answers — resolves the
probe immediately instead of waiting out the timeout. The parser is
abandoned after the window; the consumer's createInput({ terminfo })
parser takes over for the session.

Adds terminfo-spec 10.3 attachment tests (at most one Input and one
Term per handle) and exports the public capability surface from mod.ts
(queryTermInfo, TermInfo, Capabilities, Rgb, probe stream types,
MAX_TERMINFO), deliberately omitting the internals() attachment
helper.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Size Increased — +22.1 KB

146.4 KB unpacked

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@bomb.sh/tty@106

commit: 3e8056c

@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 23.18%

❌ 3 regressed benchmarks
✅ 7 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime createInput 278.8 ms 370.1 ms -24.67%
WallTime createTerm 284.6 ms 375.5 ms -24.21%
WallTime time to first render 299 ms 376.5 ms -20.6%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/terminfo (3e8056c) with main (35ce6a0)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💡 Color-encoding modes (16-color / 256-color) derived from terminfo

1 participant