[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

spec: add terminfo & capability layer specification

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.

Nate Moore (Jul 1, 2026, 11:47 PM EDT) a2178c8d 35ce6a0d

+656 -15
+65 -13
specs/input-spec.md
··· 11 11 for decoding raw terminal byte sequences into structured events. 12 12 13 13 Input parsing is architecturally independent from rendering (see 14 - [Renderer Specification](renderer-spec.md), INV-8). The two concerns share a 14 + [Renderer Specification](renderer-spec.md), INV-7). The two concerns share a 15 15 compiled WASM binary for loading efficiency, but neither depends on the other's 16 - state, types, or API surface. 16 + state, types, or API surface. Both consume the shared capability layer defined 17 + in the [Terminfo Specification](terminfo-spec.md); the input parser is 18 + additionally that layer's runtime write path (see Section 6). 17 19 18 - This specification is currently non-normative. The input API has clear design 19 - intent but has undergone more revision than the rendering core and faces known 20 - upcoming forces that will reshape it (Kitty progressive enhancement field 21 - surfacing, terminfo binary parsing). It is written to document the current 22 - surface and guide future stabilization. 20 + This specification is currently non-normative except where noted. The input API 21 + has clear design intent but has undergone more revision than the rendering core 22 + and faces known upcoming forces that will reshape it (Kitty progressive 23 + enhancement field surfacing). It is written to document the current surface and 24 + guide future stabilization. 23 25 24 26 --- 25 27 ··· 31 33 - The scan API and its return type 32 34 - The `InputEvent` discriminated union and its variants 33 35 - The ESC timeout resolution model 36 + - Terminfo integration: key sequence loading and capability query response 37 + recognition (Section 6, normative) 34 38 35 39 ### Out of scope 36 40 ··· 74 78 responsiveness (lower values) and correct disambiguation of ESC-prefixed 75 79 sequences (higher values). 76 80 77 - - **`terminfo`** — A `Uint8Array` of raw terminfo binary. Accepted but C-side 78 - parsing is not yet implemented. 81 + - **`terminfo`** — A `TermInfo` handle from `queryTermInfo()` (see 82 + [Terminfo Specification](terminfo-spec.md) §10). Attaches the parser to the 83 + handle's shared memory and capability struct. Terminal-specific key sequences 84 + from the handle's terminfo bytes are loaded into the parser's sequence trie at 85 + initialization (Section 6.1), and the parser becomes the capability struct's 86 + runtime writer (Section 6.2). When omitted, the parser operates standalone 87 + with xterm default sequences and a private capability struct. 88 + 89 + The previous `Uint8Array` form of this option is replaced by the handle form; 90 + raw bytes are supplied via `queryTermInfo({ terminfo: bytes })`. 79 91 80 92 ### 4.2 Scan 81 93 ··· 135 147 136 148 --- 137 149 138 - ## 6. Deferred / Future Areas 150 + ## 6. Terminfo Integration 151 + 152 + _This section is normative. It defines the input parser's two roles in the 153 + capability layer specified by the [Terminfo Specification](terminfo-spec.md)._ 154 + 155 + ### 6.1 Key sequences from terminfo 156 + 157 + When attached to a `TermInfo` handle whose terminfo bytes are present, the 158 + parser MUST load the terminal's `key_*` string capabilities into its escape 159 + sequence trie at initialization, before any scan. Terminfo-supplied sequences 160 + take precedence over the built-in xterm defaults when they conflict; defaults 161 + remain registered for sequences the terminfo entry does not define. 162 + 163 + The key capabilities consumed are the `key_*` string range mapped to existing 164 + `KEY_*` codes: arrows (`kcuu1`, `kcud1`, `kcub1`, `kcuf1`), function keys 165 + (`kf1`–`kf12`), editing keys (`khome`, `kend`, `kich1`, `kdch1`, `kpp`, `knp`), 166 + and backtab (`kcbt`). Key capabilities with no corresponding `KEY_*` code are 167 + ignored. 168 + 169 + Strings are read directly from the raw terminfo bytes in the shared region; they 170 + are not copied into the capability struct. 171 + 172 + ### 6.2 Query response recognition 173 + 174 + The parser is the runtime write path for the capability struct. During a normal 175 + scan — with responses potentially interleaved with user input — it MUST 176 + recognize and consume the probe responses listed in Terminfo Specification §9.1: 177 + OSC 10/11/12 theme color reports, OSC 21 kitty color reports, OSC 22 pointer 178 + shape reports, XTGETTCAP DCS replies, DECRPM mode-2026 reports, kitty keyboard 179 + flag reports, kitty graphics APC replies, and the DA1 device attributes report. 180 + 181 + For each recognized response the parser updates the corresponding struct fields, 182 + sets the `confirmed` bit, and increments the generation, per Terminfo 183 + Specification §6. Responses are consumed silently: they MUST NOT surface as 184 + `InputEvent`s, and bytes belonging to a recognized response MUST NOT leak into 185 + adjacent events. 186 + 187 + When the parser is standalone (no handle), responses are still recognized and 188 + consumed — writing into the parser's private struct — so stray replies never 189 + corrupt the event stream. 190 + 191 + --- 192 + 193 + ## 7. Deferred / Future Areas 139 194 140 195 _These topics are explicitly excluded from this specification. Their omission is 141 196 intentional, not an oversight._ ··· 143 198 **Full Kitty progressive enhancement event types.** The C-side input parser 144 199 struct has been extended for progressive enhancement fields. The TypeScript 145 200 event types have not been updated to surface them. 146 - 147 - **Terminfo binary parsing.** The input API accepts a `terminfo` option, but 148 - C-side parsing is not implemented. 149 201 150 202 **Whether input parsing should be a separate package.** Architecturally 151 203 independent from the renderer but currently co-located. The distribution
+74 -2
specs/renderer-spec.md
··· 40 40 - The directive model and core helpers 41 41 - Element identity and frame semantics 42 42 - Boundary responsibilities (what Clayterm owns and what it does not) 43 + - Capability-gated emission (Section 7.6; the capability layer itself is 44 + specified in the [Terminfo Specification](terminfo-spec.md)) 43 45 44 46 ### In scope (non-normative, descriptive) 45 47 ··· 58 60 - Demo applications 59 61 - The crankterm project or any specific framework built on Clayterm 60 62 - Input parsing (see [Clayterm Input Specification](input-spec.md)) 63 + - Terminfo parsing and capability probing (see 64 + [Terminfo Specification](terminfo-spec.md)); this specification consumes the 65 + capability struct, it does not define it 61 66 62 67 --- 63 68 ··· 232 237 concern MUST remain independent. Neither MUST depend on the other's state, 233 238 types, or API surface. They MAY share a compiled WASM binary for loading 234 239 efficiency, but this is an implementation convenience, not an architectural 235 - coupling. 240 + coupling. Both MAY consume the shared capability layer defined in the 241 + [Terminfo Specification](terminfo-spec.md); the renderer reads the capability 242 + struct and the input parser writes it, but neither observes the other through it 243 + beyond the capability facts it carries. 236 244 237 245 --- 238 246 ··· 334 342 (see §12.3) before returning, so the caller can detect that some clipping was 335 343 not applied. 336 344 345 + ### 7.6 Capability-gated emission 346 + 347 + A Term instance attached to a `TermInfo` handle (see 348 + [Terminfo Specification](terminfo-spec.md)) reads the capability struct at the 349 + start of each render transaction and gates its output accordingly. This consumes 350 + capabilities; it never writes them (Terminfo Specification TINV-4). 351 + 352 + **Color encoding ladder.** The renderer MUST select its SGR color encoding from 353 + the capability struct: 354 + 355 + - `trueColor` set → 24-bit SGR (`38;2;r;g;b` / `48;2;r;g;b`) 356 + - otherwise `colors` ≥ 256 → 256-color SGR (`38;5;n` / `48;5;n`), mapping RGB to 357 + the nearest entry of the 6×6×6 color cube and 24-step grayscale ramp 358 + - otherwise → 16-color SGR (`30–37`, `90–97` and background equivalents), 359 + mapping RGB to the nearest of the 16 ANSI colors 360 + 361 + The nearest-color quantization method is implementation-defined but MUST be 362 + deterministic: the same RGB input always maps to the same palette entry within a 363 + process. 364 + 365 + **Back-color-erase.** When the `bce` capability is set, the renderer MAY use 366 + erase sequences that rely on the terminal filling cleared cells with the current 367 + background. When it is clear, the renderer MUST NOT depend on that behavior. 368 + 369 + **Synchronized output.** When the `syncOutput` capability is set, the renderer 370 + MUST wrap each non-empty cursor-update-mode frame in the synchronized output 371 + protocol: `CSI ? 2026 h` (begin synchronized update) before the first output 372 + byte and `CSI ? 2026 l` (end synchronized update) after the last, within the 373 + same output buffer. The wrap is frame-scoped: begin and end always appear in the 374 + same render transaction's output, so no terminal state persists between frames 375 + (see §11.2). When the capability is unset, the wrap MUST NOT be emitted. 376 + Line-mode output is never wrapped. 377 + 378 + The wrap complements — never replaces — cell diffing. Emitting only changed 379 + cells (§4.4) remains the primary defense against tearing on terminals without 380 + mode 2026 and the dominant reduction in bytes sent, per the 381 + [guidance modern emulators publish for TUI developers](https://ghostty.org/docs/help/synchronized-output); 382 + the wrap adds atomic frame presentation on terminals that support it. 383 + 384 + **Generation invalidation.** The renderer MUST compare the capability struct's 385 + generation counter on each render transaction. When it differs from the 386 + generation of the previously emitted frame, the renderer MUST invalidate its 387 + diff state and emit the frame as a complete redraw, so that no cell on screen 388 + retains bytes encoded under superseded capabilities. 389 + 390 + A Term with no `TermInfo` handle uses the baseline capabilities (Terminfo 391 + Specification §7.1): 256-color emission. Per the progressive-enhancement 392 + invariant (Terminfo Specification TINV-5), truecolor emission requires positive 393 + evidence — a terminfo entry, environment evidence, or a probe reply — which 394 + supersedes the renderer's historical unconditional truecolor output. 395 + 337 396 --- 338 397 339 398 ## 8. Public Rendering API ··· 344 403 ### 8.1 Term creation 345 404 346 405 ``` 347 - createTerm(options: { width: number; height: number }): Promise<Term> 406 + createTerm(options: { 407 + width: number; 408 + height: number; 409 + terminfo?: TermInfo; 410 + }): Promise<Term> 348 411 ``` 349 412 350 413 Creates a new Term instance bound to the specified terminal dimensions. The 351 414 returned promise resolves when the renderer is ready. The `width` and `height` 352 415 parameters specify the terminal dimensions in character cells. 416 + 417 + The optional `terminfo` handle (from `queryTermInfo()`; see 418 + [Terminfo Specification](terminfo-spec.md) §10) attaches the Term to a shared 419 + capability struct that gates emission per §7.6. When omitted, the Term operates 420 + standalone with default capabilities. 353 421 354 422 ### 8.2 Render invocation 355 423 ··· 614 682 These are the caller's responsibility. The renderer's output contains only the 615 683 escape sequences needed to render the frame content (cursor positioning for cell 616 684 writes, SGR attributes for styling, and UTF-8 text). 685 + 686 + The synchronized-output frame wrap (§7.6) is not terminal-state management in 687 + this sense: mode 2026 is begun and ended within a single frame's output and 688 + never persists across render transactions. 617 689 618 690 ### 11.3 The renderer does not own application lifecycle 619 691
+517
specs/terminfo-spec.md
··· 1 + # Clayterm Terminfo & Capability Specification 2 + 3 + **Version:** 0.1 (draft) **Status:** Proposed. Normative for the shared 4 + capability layer. 5 + 6 + --- 7 + 8 + ## 1. Purpose 9 + 10 + This specification defines Clayterm's terminal capability layer: how static 11 + capability data (compiled terminfo binaries) and runtime capability data 12 + (query/response handshakes such as OSC color queries and DA1) are parsed into a 13 + single shared capability struct, and how the renderer and the input parser 14 + consume it. 15 + 16 + The capability layer exists to answer one question for both consumers: **what 17 + can this terminal do?** The renderer uses the answer to gate what it emits 18 + (color encoding, erase strategy, synchronized-output frame wrapping). The input 19 + parser uses the answer's raw material (terminal-specific key sequences) and is 20 + the write path through which runtime query responses reach the struct. 21 + 22 + --- 23 + 24 + ## 2. Scope 25 + 26 + ### In scope (normative) 27 + 28 + - The `TermInfo` capability struct: its field set, mutation rules, and the 29 + generation counter 30 + - The shared-memory region model and its build constraints 31 + - Compiled terminfo binary parsing (legacy and extended formats) 32 + - The probe model: query batch, completion fence, and sans-IO contract 33 + - The public API: `queryTermInfo()`, the `TermInfo` handle, and how the handle 34 + is passed to `createTerm` and `createInput` 35 + - The baseline capability set and the progressive-enhancement evidence model 36 + 37 + ### Out of scope 38 + 39 + - How the renderer maps capabilities to emitted bytes (see 40 + [Renderer Specification](renderer-spec.md) §7.6) 41 + - How the input parser recognizes query responses byte-by-byte (see 42 + [Input Specification](input-spec.md) §6) 43 + - Caller-layer mode management (alt screen, kitty push/pop, mouse enable) — 44 + capabilities inform these decisions but do not perform them 45 + 46 + --- 47 + 48 + ## 3. Terminology 49 + 50 + **Capability struct (`TermInfo` struct).** A fixed-layout C struct holding the 51 + resolved capability state of one terminal. Lives at a stable pointer for the 52 + lifetime of the handle that owns it. 53 + 54 + **Terminfo region.** A reserved range of linear memory holding the raw compiled 55 + terminfo bytes and the capability struct. 56 + 57 + **Handle (`TermInfo`).** The TypeScript object returned by `queryTermInfo()`. 58 + Owns the shared `WebAssembly.Memory`, the terminfo region, and the pointer to 59 + the capability struct. Passed to `createTerm` and `createInput` to attach them 60 + to the same struct. 61 + 62 + **Probe.** A batch of terminal query sequences emitted as bytes, whose responses 63 + arrive on the input stream and are folded back into the capability struct. 64 + 65 + **Fence.** The final query in a probe batch, chosen because every terminal 66 + answers it. Its response marks the probe as complete. This specification uses 67 + DA1 (`CSI c`) as the fence. 68 + 69 + **Generation.** A monotonic counter on the capability struct, incremented on 70 + every mutation. Consumers compare generations to detect capability change. 71 + 72 + --- 73 + 74 + ## 4. Architectural Model 75 + 76 + ### 4.1 One memory, three tenants 77 + 78 + When a `TermInfo` handle is in use, a single `WebAssembly.Memory` is shared by 79 + up to three tenants: 80 + 81 + 1. The **terminfo region** — raw bytes plus the capability struct, owned by the 82 + handle. 83 + 2. The **renderer instance** — heap and transfer buffers for `createTerm`. 84 + 3. The **input parser instance** — heap and scan buffer for `createInput`. 85 + 86 + The handle owns the memory and allocates disjoint regions to each tenant. Both 87 + WASM instances import the shared memory (`env.memory`) and receive their region 88 + pointers explicitly, as they do today via `init(mem, …)` and 89 + `input_init(mem, …)`. Each additionally receives the capability struct pointer. 90 + 91 + ### 4.2 Data flow 92 + 93 + ``` 94 + terminfo file bytes ──▶ terminfo_parse() ──▶ ┌───────────────┐ 95 + │ TermInfo │ ◀── reads ── renderer 96 + probe bytes ──▶ terminal ──▶ stdin ──▶ │ struct │ 97 + input_scan() ── writes ────▶ │ (+generation) │ ◀── reads ── TS capabilities view 98 + └───────────────┘ 99 + ``` 100 + 101 + - The **terminfo module** (`terminfo.c`) parses the raw bytes into the struct. 102 + It performs no IO. 103 + - The **input parser** is the only runtime writer: when it recognizes a query 104 + response during a normal scan, it updates the struct and bumps the generation. 105 + - The **renderer** only reads the struct, at render-transaction time. 106 + - The **TypeScript layer** reads the struct through the handle's `capabilities` 107 + view. It writes only at handle creation: `terminfo_parse` plus environment 108 + evidence (§7.2). 109 + 110 + The renderer and the input parser never communicate with each other. Both depend 111 + on the terminfo layer, exactly as both depend on shared substrate modules today 112 + (`mem.c`, `utf8.c`). This preserves the independence invariant (Renderer 113 + Specification INV-7). 114 + 115 + ### 4.3 Standalone operation 116 + 117 + `createTerm` and `createInput` remain usable without a handle. When no 118 + `terminfo` option is provided, each factory creates its own private memory (as 119 + today) containing a private capability struct initialized to the §7.1 baseline. 120 + Behavior is identical to a handle with no terminfo bytes, no environment 121 + evidence, and no probe responses. 122 + 123 + --- 124 + 125 + ## 5. Core Invariants 126 + 127 + _This section is normative._ 128 + 129 + **TINV-1. Single source of truth.** All capability state lives in the capability 130 + struct. Neither the renderer nor the input parser may cache capability values 131 + across frames/scans in a way that survives a generation change. 132 + 133 + **TINV-2. Monotonic generation.** Every mutation of the capability struct MUST 134 + increment the generation counter exactly once per logical update. The counter 135 + never decreases. A consumer that observes an unchanged generation MAY assume 136 + every other field is unchanged. 137 + 138 + **TINV-3. Pure parsing.** `terminfo_parse` performs no IO, allocates no memory, 139 + and never traps on malformed input. Input larger than 32768 bytes is rejected at 140 + the TypeScript boundary. Malformed or truncated binaries yield the §7.1 baseline 141 + and a nonzero parse-result code; they MUST NOT partially apply. 142 + 143 + **TINV-4. Single runtime writer.** Capability writes happen at handle creation 144 + (terminfo parse plus environment evidence, §7) and thereafter only through the 145 + input parser recognizing query responses. The renderer MUST NOT write the 146 + struct. The TypeScript layer MUST NOT write it after creation. 147 + 148 + **TINV-5. Progressive enhancement.** The capability layer starts from a 149 + conservative baseline owned by the terminfo module — the built-in equivalent of 150 + `xterm-256color` (§7) — and raises a capability only on positive evidence. 151 + Evidence sources, in increasing precedence: the baseline, the terminfo entry, 152 + environment evidence collected at handle creation (e.g. `COLORTERM`), and probe 153 + responses. A capability bit no evidence supports stays unset; a 154 + higher-precedence denial clears a lower-precedence grant. Consumers MUST NOT 155 + assume capabilities beyond what the struct states. 156 + 157 + **TINV-6. Sans-IO probe.** The probe core produces bytes (`probe()`) and 158 + consumes bytes (via the input parser's scan path). It never touches a stream. 159 + The convenience wrapper (`queryTermInfo`) performs IO but MUST resolve — never 160 + reject — on timeout, non-TTY streams, missing terminfo files, or abort. 161 + 162 + **TINV-7. Disjoint static footprints.** Any two WASM modules that import the 163 + same memory MUST be linked with disjoint static data/heap base ranges (e.g. 164 + coordinated `--global-base`) so that instantiating one cannot clobber the 165 + other's data segments. Under the current single-module build this is trivially 166 + satisfied; a split-module build (layout/input) MUST enforce it in the Makefile. 167 + 168 + --- 169 + 170 + ## 6. The Capability Struct 171 + 172 + _This section is normative for the field set and semantics. Exact byte offsets 173 + are defined by `terminfo.h` and mirrored in TypeScript via `typedef.ts`; they 174 + are implementation surface, not contract._ 175 + 176 + | Field | Type | Meaning | 177 + | -------------- | ------ | -------------------------------------------------------------- | 178 + | `generation` | uint32 | Mutation counter (TINV-2). Starts at 1 after initialization. | 179 + | `colors` | uint32 | `max_colors` from terminfo; 0 when unknown. | 180 + | `flags` | uint32 | Bitfield of `TERMINFO_*` capability bits (below). | 181 + | `confirmed` | uint32 | Subset of `flags` bits confirmed or denied by probe responses. | 182 + | `theme_fg` | uint32 | Theme foreground as `0x00RRGGBB`; valid bit in `flags`. | 183 + | `theme_bg` | uint32 | Theme background as `0x00RRGGBB`; valid bit in `flags`. | 184 + | `theme_cursor` | uint32 | Theme cursor color as `0x00RRGGBB`; valid bit in `flags`. | 185 + 186 + The three `theme_*` fields form the **theme group**, surfaced in TypeScript as a 187 + single `theme` object (§10.2). 188 + 189 + Flag bits: 190 + 191 + | Bit | Source (static) | Source (probe) | 192 + | --------------------------- | ----------------------------------------------------------------------- | --------------------------- | 193 + | `TERMINFO_TRUECOLOR` | `RGB`/`Tc` extended caps, or `colors` ≥ 1<<24; `COLORTERM` env evidence | XTGETTCAP `RGB`/`Tc` reply | 194 + | `TERMINFO_BCE` | `bce` boolean | — | 195 + | `TERMINFO_AM` | `am` boolean | — | 196 + | `TERMINFO_XENL` | `xenl` boolean | — | 197 + | `TERMINFO_ALTSCREEN` | `smcup` string present | — | 198 + | `TERMINFO_STYLED_UNDERLINE` | `Su` boolean / `Smulx` string | — | 199 + | `TERMINFO_SYNC` | — | DECRPM reply for mode 2026 | 200 + | `TERMINFO_KITTY_KEYBOARD` | — | `CSI ? flags u` reply | 201 + | `TERMINFO_KITTY_GRAPHICS` | — | APC `_G…` reply | 202 + | `TERMINFO_KITTY_COLOR` | — | OSC 21 reply | 203 + | `TERMINFO_HYPERLINKS` | reserved (Open Decision 4) | reserved (Open Decision 4) | 204 + | `TERMINFO_POINTER_SHAPE` | — | OSC 22 `?__current__` reply | 205 + | `TERMINFO_THEME_FG` | — | OSC 10 or OSC 21 reply | 206 + | `TERMINFO_THEME_BG` | — | OSC 11 or OSC 21 reply | 207 + | `TERMINFO_THEME_CURSOR` | — | OSC 12 or OSC 21 reply | 208 + 209 + Evidence precedence follows TINV-5: when a bit is set in `confirmed`, the 210 + corresponding `flags` bit reflects the terminal's answer, not the terminfo file 211 + or environment. A denial (e.g. XTGETTCAP invalid-capability reply) clears a 212 + statically-set bit. 213 + 214 + `TERMINFO_POINTER_SHAPE` (OSC 22) is detected via the 215 + [kitty pointer shape protocol](https://sw.kovidgoyal.net/kitty/pointer-shapes/) 216 + query `OSC 22 ; ?__current__ ST`: supporting terminals reply with the current 217 + shape name; non-supporting terminals stay silent and the DA1 fence closes the 218 + question. Note the flag records _protocol_ support only — shape-name 219 + vocabularies vary by terminal (kitty and Ghostty use CSS names, xterm uses X11 220 + names); per-shape support can be refined later via the protocol's `?name,name,…` 221 + query form (deferred). 222 + 223 + `TERMINFO_HYPERLINKS` (OSC 8) is allocated because a consumer is planned (#67), 224 + but the 225 + [OSC 8 specification](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) 226 + is explicit that no detection mechanism exists — see Open Decision 4. Until a 227 + source is settled the bit stays unset. OSC 8 is ignore-safe by ECMA-48 parsing 228 + rules: unsupported terminals render the link text without artifacts, so 229 + consumers MAY emit it without capability confirmation. 230 + 231 + Key sequence strings (`key_*` capabilities) are **not** stored in the struct. 232 + The input parser reads them directly from the raw terminfo bytes in the terminfo 233 + region at initialization time (see Input Specification §6.1). The struct carries 234 + resolved _facts_; the raw region carries _material_. 235 + 236 + --- 237 + 238 + ## 7. Baseline and Environment Evidence 239 + 240 + _This section is normative._ 241 + 242 + ### 7.1 Baseline 243 + 244 + The terminfo module owns the baseline: the built-in equivalent of the 245 + `xterm-256color` terminfo entry. With no terminfo bytes, no environment 246 + evidence, and no probe responses, the struct is initialized to: 247 + 248 + | Field | Default | 249 + | ----------- | -------------------------------- | 250 + | `colors` | 256 | 251 + | `flags` | `AM \| XENL \| ALTSCREEN \| BCE` | 252 + | `confirmed` | 0 | 253 + | `theme_*` | 0 (invalid; theme unknown) | 254 + 255 + Truecolor is **not** assumed at baseline. Per TINV-5 it is enhanced in by 256 + evidence: `RGB`/`Tc` in the terminfo entry, `COLORTERM` in the environment, or 257 + an XTGETTCAP probe reply. This supersedes the renderer's historical 258 + unconditional truecolor emission — a Term with no capability evidence emits 259 + 256-color SGR (Renderer Specification §7.6). 260 + 261 + ### 7.2 Environment evidence 262 + 263 + At handle creation, `queryTermInfo` applies evidence from the (injectable) 264 + environment after parsing the terminfo entry and before the probe: 265 + 266 + - `COLORTERM` equal to `truecolor` or `24bit` sets `TERMINFO_TRUECOLOR`. 267 + 268 + Environment evidence outranks the terminfo entry and is outranked by probe 269 + responses (TINV-5). 270 + 271 + --- 272 + 273 + ## 8. Terminfo Binary Parsing 274 + 275 + _This section is normative._ 276 + 277 + `terminfo_parse(bytes, len, out)` accepts a compiled terminfo entry and 278 + populates the capability struct. 279 + 280 + - Both storage formats MUST be supported: legacy (magic `0432`, 16-bit numbers) 281 + and extended number format (magic `01036`, 32-bit numbers). 282 + - The extended-capability string table (the ncurses extension block after the 283 + standard sections) MUST be parsed for the user-defined capabilities `RGB`, 284 + `Tc`, `Su`, and `Smulx`. 285 + - Parsing is bounds-checked against `len` everywhere. Out-of-range string 286 + offsets, truncated sections, and odd-length string tables yield the parse 287 + failure path (TINV-3), never a trap or partial state. 288 + - The maximum accepted size is 32768 bytes (`MAX_TERMINFO`), the extended 289 + ncurses format limit. The TypeScript boundary enforces this before the bytes 290 + reach linear memory. 291 + 292 + The standard capability indices consumed are: booleans `am` (1), `xenl` (4), 293 + `bce` (28); number `max_colors` (13); strings `smcup` (28) and the `key_*` range 294 + (see Input Specification §6.1 for the key set). 295 + 296 + --- 297 + 298 + ## 9. The Probe 299 + 300 + ### 9.1 Query batch 301 + 302 + _This section is normative._ 303 + 304 + `terminfo.probe()` returns the following queries as one `Uint8Array`, in order: 305 + 306 + | # | Query | Bytes | Answered by | 307 + | -- | ------------------- | ---------------------------------------------------- | --------------------- | 308 + | 1 | Foreground color | `OSC 10 ; ? BEL` | OSC 10 reply | 309 + | 2 | Background color | `OSC 11 ; ? BEL` | OSC 11 reply | 310 + | 3 | Cursor color | `OSC 12 ; ? BEL` | OSC 12 reply | 311 + | 4 | Kitty color | `OSC 21 ; foreground=? ; background=? ; cursor=? ST` | OSC 21 reply | 312 + | 5 | Pointer shape | `OSC 22 ; ?__current__ ST` | OSC 22 reply | 313 + | 6 | Truecolor caps | `DCS + q 524742 ; 5463 ST` (XTGETTCAP `RGB;Tc`) | DCS `1 + r` / `0 + r` | 314 + | 7 | Synchronized output | `CSI ? 2026 $ p` (DECRQM) | `CSI ? 2026 ; Ps $ y` | 315 + | 8 | Kitty keyboard | `CSI ? u` | `CSI ? flags u` | 316 + | 9 | Kitty graphics | `APC _G i=31,s=1,v=1,a=q,t=d,f=24 ; AAAA ST` | APC `_Gi=31;…` reply | 317 + | 10 | **Fence:** DA1 | `CSI c` | `CSI ? … c` | 318 + 319 + Terminals answer queries in order and ignore queries they do not understand. DA1 320 + is answered by every terminal, so its response marks the probe complete: any of 321 + queries 1–9 not yet answered when the DA1 reply arrives will never be answered, 322 + and their capabilities keep their static/default values. 323 + 324 + An [OSC 21](https://sw.kovidgoyal.net/kitty/color-stack/) reply echoes the 325 + queried keys with `?` replaced by the encoded color (or empty when undefined); 326 + it sets `TERMINFO_KITTY_COLOR` and MAY fill any theme fields it carries. The OSC 327 + 10/11/12 replies remain the portable theme source. Color values in OSC replies 328 + MUST be recognized in at least the `rgb:RR/GG/BB` (1–4 hex digits per channel) 329 + and `#`-hash forms; other encodings (`rgbi:`, named colors, `@alpha` suffixes) 330 + MAY be ignored. An [OSC 22](https://sw.kovidgoyal.net/kitty/pointer-shapes/) 331 + reply carries the current shape name; the reply's arrival sets 332 + `TERMINFO_POINTER_SHAPE` and the name itself is discarded in v1. 333 + 334 + The batch is safe to emit unconditionally: every query is either answered or 335 + ignored; none changes terminal state. 336 + 337 + ### 9.2 Response path 338 + 339 + Probe responses arrive on the terminal's input stream, potentially interleaved 340 + with user input. They are recognized and consumed by the input parser during its 341 + normal `scan()` (see Input Specification §6.2). Each recognized response updates 342 + the capability struct, sets the relevant `confirmed` bit, and bumps the 343 + generation. Responses are consumed silently; they do not surface as 344 + `InputEvent`s in v1. 345 + 346 + A render-only consumer (no `createInput`) that wants probe results MUST route 347 + its input stream through the handle's input parser during the probe window; 348 + `queryTermInfo` does exactly this internally. 349 + 350 + ### 9.3 Capability change over time 351 + 352 + Capabilities may change after first use — a probe response can arrive after a 353 + frame has already rendered. The generation counter is the mechanism: the 354 + renderer compares the struct generation on each render transaction and 355 + invalidates its diff state when it changed (Renderer Specification §7.6). No 356 + consumer ceremony is required. 357 + 358 + --- 359 + 360 + ## 10. Public API 361 + 362 + _This section is normative for the shapes shown. Option names follow the 363 + existing codebase conventions._ 364 + 365 + ### 10.1 queryTermInfo 366 + 367 + ``` 368 + queryTermInfo(options?: QueryTermInfoOptions): Promise<TermInfo> 369 + ``` 370 + 371 + The single blessed entry point. It: 372 + 373 + 1. Locates and reads the compiled terminfo entry for the terminal (unless raw 374 + bytes are provided), following the ncurses search path: `$TERMINFO`, 375 + `$HOME/.terminfo`, `$TERMINFO_DIRS` (empty entry = compiled-in defaults), 376 + then `/usr/share/terminfo`, `/etc/terminfo`, `/lib/terminfo`, 377 + `/usr/lib/terminfo`. Both directory layouts are probed: first-letter (Linux) 378 + and two-hex-digit (macOS). Names containing path separators, NUL, or a 379 + leading `.` are rejected. Files are validated by magic number. 380 + 2. Creates the shared memory, terminfo region, and capability struct; parses the 381 + bytes. 382 + 3. Applies environment evidence (§7.2) from the injectable `env`. 383 + 4. When `input` and `output` are TTYs: writes the probe batch to `output` and 384 + feeds `input` through the handle's parser until the DA1 fence or timeout. Raw 385 + mode is enabled for the probe window and restored afterward. 386 + 5. Resolves the handle. 387 + 388 + ``` 389 + interface QueryTermInfoOptions { 390 + term?: string; // terminal name; default env.TERM 391 + env?: Record<string, string | undefined>; // default process.env 392 + terminfo?: Uint8Array; // raw bytes; skips filesystem lookup 393 + input?: ReadStream; // default process.stdin 394 + output?: WriteStream; // default process.stdout 395 + timeout?: number; // ms until probe abandonment; default 100 396 + signal?: AbortSignal; 397 + } 398 + ``` 399 + 400 + Every environmental dependency is injectable (`env`, `terminfo`, `input`, 401 + `output`), making the function fully mockable without a PTY. Per TINV-6 it 402 + resolves — never rejects — when the terminfo file is missing, the streams are 403 + not TTYs, the probe times out, or the signal aborts; the handle then carries 404 + whatever subset of capabilities was resolved. 405 + 406 + ### 10.2 The TermInfo handle 407 + 408 + ``` 409 + interface TermInfo { 410 + readonly capabilities: Capabilities; // decoded live view of the struct 411 + probe(): Uint8Array; // the §9.1 query batch (sans-IO) 412 + } 413 + ``` 414 + 415 + `capabilities` decodes the struct on read (cheap; a handful of field reads) so 416 + it always reflects the current generation: 417 + 418 + ``` 419 + interface Rgb { 420 + r: number; 421 + g: number; 422 + b: number; 423 + } 424 + 425 + interface Capabilities { 426 + generation: number; 427 + colors: number; 428 + trueColor: boolean; 429 + bce: boolean; 430 + autoMargin: boolean; 431 + altScreen: boolean; 432 + styledUnderline: boolean; 433 + syncOutput: boolean; 434 + kittyKeyboard: boolean; 435 + kittyGraphics: boolean; 436 + kittyColor: boolean; 437 + hyperlinks: boolean; // reserved; no detection source exists (Open Decision 4) 438 + pointerShape: boolean; // kitty OSC 22 protocol support 439 + theme: { 440 + foreground?: Rgb; // OSC 10 441 + background?: Rgb; // OSC 11 442 + cursor?: Rgb; // OSC 12 443 + }; 444 + } 445 + ``` 446 + 447 + The handle also carries the shared memory and region pointers as internal 448 + (non-normative) surface consumed by `createTerm` and `createInput`. 449 + 450 + ### 10.3 Attachment 451 + 452 + ``` 453 + createTerm({ width, height, terminfo?: TermInfo }): Promise<Term> 454 + createInput({ escLatency?, terminfo?: TermInfo }): Promise<Input> 455 + ``` 456 + 457 + Passing the same handle to both attaches them to the same memory and struct. A 458 + handle MAY be attached to at most one `Term` and one `Input` at a time; 459 + attaching a second is an error. Factories called without `terminfo` operate 460 + standalone (§4.3). 461 + 462 + The previous `terminfo?: Uint8Array` option on `createInput` is replaced by the 463 + handle form. Raw bytes are provided via `queryTermInfo({ terminfo: bytes })`. 464 + 465 + --- 466 + 467 + ## 11. Deferred / Future Areas 468 + 469 + _Non-normative. Intentional omissions._ 470 + 471 + **OSC 4 palette queries.** The 256-entry palette is not probed; the theme group 472 + covers foreground, background, and cursor (OSC 10/11/12) only. 473 + 474 + **Theme-change notification (mode 2031).** The probe captures a snapshot; live 475 + dark/light switching is not tracked. 476 + 477 + **XTVERSION / DA2 / DA3 identity parsing.** The DA1 reply is used purely as a 478 + fence; terminal identification is not extracted. 479 + 480 + **Surfacing capability changes as events.** Probe responses are consumed 481 + silently. A `capabilitychange` input event or handle callback may be added once 482 + a consumer needs reactivity beyond the generation counter. 483 + 484 + **Pixel mouse (1016) and in-band resize (2048) probing.** Candidates for the 485 + batch once consumers exist. 486 + 487 + **terminfo string emission (`sgr`, `cup` from terminfo).** The renderer 488 + continues to emit hardcoded ANSI; terminfo strings inform input parsing only. 489 + 490 + --- 491 + 492 + ## Open Decisions 493 + 494 + 1. **Should `capabilities` be an event emitter?** v1 is poll-only via 495 + `generation`. Reactive consumers may justify a subscription API. 496 + 497 + 2. **Where does the split-module `--global-base` coordination live?** TINV-7 498 + states the constraint; the mechanism (Makefile flags vs. a linker script) is 499 + a build-system decision for the PR that splits the modules. 500 + 501 + 3. **Should the probe be re-runnable?** `probe()` may be called any number of 502 + times, but `queryTermInfo` runs the managed probe exactly once. Re-probing 503 + after suspend/resume (terminal may have changed) is unaddressed. 504 + 505 + 4. **What detects OSC 8 hyperlinks?** The 506 + [OSC 8 specification](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) 507 + states no detection mechanism exists, and the sequence degrades gracefully on 508 + unsupported terminals. Candidate sources: an extended user capability 509 + convention (none has settled), terminal identity heuristics from 510 + DA2/XTVERSION (currently unparsed, see Deferred), or treating OSC 8 as 511 + permanently ignore-safe and dropping the flag. The struct reserves the bit so 512 + consumers have a stable place to look once a source is chosen. 513 + 514 + 5. **Should per-shape pointer support be probed?** The kitty pointer shape 515 + protocol's `?name,name,…` query reports support for individual shape names. 516 + v1 records protocol support only; a shape-vocabulary field would let the 517 + renderer pick portable shape names.