feat: terminfo#106
Draft
natemoo-re wants to merge 5 commits into
Draft
Conversation
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.
|
Size Increased — +22.1 KB 146.4 KB unpacked |
commit: |
Contributor
Merging this PR will degrade performance by 23.18%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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?
queryTermInfo()reads the terminal's terminfo file, checksCOLORTERM, and asks the terminal directly (theme colors, truecolor, kitty keyboard/graphics/pointer-shape, synchronized output) — all folded into one capability struct in shared wasm memorycreateTerm({ terminfo })andcreateInput({ terminfo })attach to the same handle, so both sides agree on what the terminal supportsspecs/terminfo-spec.mdis 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
Checklist
AI-generated code disclos