Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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.
  • Loading branch information
natemoo-re committed Aug 21, 2026
commit 9de9847ce2bcb6abdc549e582e4edfe30aa934f6
78 changes: 65 additions & 13 deletions specs/input-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ This specification describes Clayterm's terminal input parsing surface: the API
for decoding raw terminal byte sequences into structured events.

Input parsing is architecturally independent from rendering (see
[Renderer Specification](renderer-spec.md), INV-8). The two concerns share a
[Renderer Specification](renderer-spec.md), INV-7). The two concerns share a
compiled WASM binary for loading efficiency, but neither depends on the other's
state, types, or API surface.
state, types, or API surface. Both consume the shared capability layer defined
in the [Terminfo Specification](terminfo-spec.md); the input parser is
additionally that layer's runtime write path (see Section 6).

This specification is currently non-normative. The input API has clear design
intent but has undergone more revision than the rendering core and faces known
upcoming forces that will reshape it (Kitty progressive enhancement field
surfacing, terminfo binary parsing). It is written to document the current
surface and guide future stabilization.
This specification is currently non-normative except where noted. The input API
has clear design intent but has undergone more revision than the rendering core
and faces known upcoming forces that will reshape it (Kitty progressive
enhancement field surfacing). It is written to document the current surface and
guide future stabilization.

---

Expand All @@ -31,6 +33,8 @@ surface and guide future stabilization.
- The scan API and its return type
- The `InputEvent` discriminated union and its variants
- The ESC timeout resolution model
- Terminfo integration: key sequence loading and capability query response
recognition (Section 6, normative)

### Out of scope

Expand Down Expand Up @@ -74,8 +78,16 @@ Options:
responsiveness (lower values) and correct disambiguation of ESC-prefixed
sequences (higher values).

- **`terminfo`** — A `Uint8Array` of raw terminfo binary. Accepted but C-side
parsing is not yet implemented.
- **`terminfo`** — A `TermInfo` handle from `queryTermInfo()` (see
[Terminfo Specification](terminfo-spec.md) §10). Attaches the parser to the
handle's shared memory and capability struct. Terminal-specific key sequences
from the handle's terminfo bytes are loaded into the parser's sequence trie at
initialization (Section 6.1), and the parser becomes the capability struct's
runtime writer (Section 6.2). When omitted, the parser operates standalone
with xterm default sequences and a private capability struct.

The previous `Uint8Array` form of this option is replaced by the handle form;
raw bytes are supplied via `queryTermInfo({ terminfo: bytes })`.

### 4.2 Scan

Expand Down Expand Up @@ -135,7 +147,50 @@ has already been extended with fields that are not yet mapped to the TS types).

---

## 6. Deferred / Future Areas
## 6. Terminfo Integration

_This section is normative. It defines the input parser's two roles in the
capability layer specified by the [Terminfo Specification](terminfo-spec.md)._

### 6.1 Key sequences from terminfo

When attached to a `TermInfo` handle whose terminfo bytes are present, the
parser MUST load the terminal's `key_*` string capabilities into its escape
sequence trie at initialization, before any scan. Terminfo-supplied sequences
take precedence over the built-in xterm defaults when they conflict; defaults
remain registered for sequences the terminfo entry does not define.

The key capabilities consumed are the `key_*` string range mapped to existing
`KEY_*` codes: arrows (`kcuu1`, `kcud1`, `kcub1`, `kcuf1`), function keys
(`kf1`–`kf12`), editing keys (`khome`, `kend`, `kich1`, `kdch1`, `kpp`, `knp`),
and backtab (`kcbt`). Key capabilities with no corresponding `KEY_*` code are
ignored.

Strings are read directly from the raw terminfo bytes in the shared region; they
are not copied into the capability struct.

### 6.2 Query response recognition

The parser is the runtime write path for the capability struct. During a normal
scan — with responses potentially interleaved with user input — it MUST
recognize and consume the probe responses listed in Terminfo Specification §9.1:
OSC 10/11/12 theme color reports, OSC 21 kitty color reports, OSC 22 pointer
shape reports, XTGETTCAP DCS replies, DECRPM mode-2026 reports, kitty keyboard
flag reports, kitty graphics APC replies, and the DA1 device attributes report.

For each recognized response the parser updates the corresponding struct fields,
sets the `confirmed` bit, and increments the generation, per Terminfo
Specification §6. Responses are consumed silently: they MUST NOT surface as
`InputEvent`s, and bytes belonging to a recognized response MUST NOT leak into
adjacent events.

When the parser is standalone (no handle), responses are still recognized and
consumed — writing into the parser's private struct — so stray replies never
corrupt the event stream.

---

## 7. Deferred / Future Areas

_These topics are explicitly excluded from this specification. Their omission is
intentional, not an oversight._
Expand All @@ -144,9 +199,6 @@ intentional, not an oversight._
struct has been extended for progressive enhancement fields. The TypeScript
event types have not been updated to surface them.

**Terminfo binary parsing.** The input API accepts a `terminfo` option, but
C-side parsing is not implemented.

**Whether input parsing should be a separate package.** Architecturally
independent from the renderer but currently co-located. The distribution
decision is open.
Expand Down
76 changes: 74 additions & 2 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Transitions are specified separately in the
- The directive model and core helpers
- Element identity and frame semantics
- Boundary responsibilities (what Clayterm owns and what it does not)
- Capability-gated emission (Section 7.6; the capability layer itself is
specified in the [Terminfo Specification](terminfo-spec.md))

### In scope (non-normative, descriptive)

Expand All @@ -58,6 +60,9 @@ Transitions are specified separately in the
- Demo applications
- The crankterm project or any specific framework built on Clayterm
- Input parsing (see [Clayterm Input Specification](input-spec.md))
- Terminfo parsing and capability probing (see
[Terminfo Specification](terminfo-spec.md)); this specification consumes the
capability struct, it does not define it

---

Expand Down Expand Up @@ -232,7 +237,10 @@ function scope.
concern MUST remain independent. Neither MUST depend on the other's state,
types, or API surface. They MAY share a compiled WASM binary for loading
efficiency, but this is an implementation convenience, not an architectural
coupling.
coupling. Both MAY consume the shared capability layer defined in the
[Terminfo Specification](terminfo-spec.md); the renderer reads the capability
struct and the input parser writes it, but neither observes the other through it
beyond the capability facts it carries.

---

Expand Down Expand Up @@ -334,6 +342,57 @@ nests clip regions more deeply than the renderer can track:
(see §12.3) before returning, so the caller can detect that some clipping was
not applied.

### 7.6 Capability-gated emission

A Term instance attached to a `TermInfo` handle (see
[Terminfo Specification](terminfo-spec.md)) reads the capability struct at the
start of each render transaction and gates its output accordingly. This consumes
capabilities; it never writes them (Terminfo Specification TINV-4).

**Color encoding ladder.** The renderer MUST select its SGR color encoding from
the capability struct:

- `trueColor` set → 24-bit SGR (`38;2;r;g;b` / `48;2;r;g;b`)
- otherwise `colors` ≥ 256 → 256-color SGR (`38;5;n` / `48;5;n`), mapping RGB to
the nearest entry of the 6×6×6 color cube and 24-step grayscale ramp
- otherwise → 16-color SGR (`30–37`, `90–97` and background equivalents),
mapping RGB to the nearest of the 16 ANSI colors

The nearest-color quantization method is implementation-defined but MUST be
deterministic: the same RGB input always maps to the same palette entry within a
process.

**Back-color-erase.** When the `bce` capability is set, the renderer MAY use
erase sequences that rely on the terminal filling cleared cells with the current
background. When it is clear, the renderer MUST NOT depend on that behavior.

**Synchronized output.** When the `syncOutput` capability is set, the renderer
MUST wrap each non-empty cursor-update-mode frame in the synchronized output
protocol: `CSI ? 2026 h` (begin synchronized update) before the first output
byte and `CSI ? 2026 l` (end synchronized update) after the last, within the
same output buffer. The wrap is frame-scoped: begin and end always appear in the
same render transaction's output, so no terminal state persists between frames
(see §11.2). When the capability is unset, the wrap MUST NOT be emitted.
Line-mode output is never wrapped.

The wrap complements — never replaces — cell diffing. Emitting only changed
cells (§4.4) remains the primary defense against tearing on terminals without
mode 2026 and the dominant reduction in bytes sent, per the
[guidance modern emulators publish for TUI developers](https://ghostty.org/docs/help/synchronized-output);
the wrap adds atomic frame presentation on terminals that support it.

**Generation invalidation.** The renderer MUST compare the capability struct's
generation counter on each render transaction. When it differs from the
generation of the previously emitted frame, the renderer MUST invalidate its
diff state and emit the frame as a complete redraw, so that no cell on screen
retains bytes encoded under superseded capabilities.

A Term with no `TermInfo` handle uses the baseline capabilities (Terminfo
Specification §7.1): 256-color emission. Per the progressive-enhancement
invariant (Terminfo Specification TINV-5), truecolor emission requires positive
evidence — a terminfo entry, environment evidence, or a probe reply — which
supersedes the renderer's historical unconditional truecolor output.

---

## 8. Public Rendering API
Expand All @@ -344,13 +403,22 @@ included. See Section 5 for what this section does and does not freeze._
### 8.1 Term creation

```
createTerm(options: { width: number; height: number }): Promise<Term>
createTerm(options: {
width: number;
height: number;
terminfo?: TermInfo;
}): Promise<Term>
```

Creates a new Term instance bound to the specified terminal dimensions. The
returned promise resolves when the renderer is ready. The `width` and `height`
parameters specify the terminal dimensions in character cells.

The optional `terminfo` handle (from `queryTermInfo()`; see
[Terminfo Specification](terminfo-spec.md) §10) attaches the Term to a shared
capability struct that gates emission per §7.6. When omitted, the Term operates
standalone with default capabilities.

### 8.2 Render invocation

```
Expand Down Expand Up @@ -615,6 +683,10 @@ These are the caller's responsibility. The renderer's output contains only the
escape sequences needed to render the frame content (cursor positioning for cell
writes, SGR attributes for styling, and UTF-8 text).

The synchronized-output frame wrap (§7.6) is not terminal-state management in
this sense: mode 2026 is begun and ended within a single frame's output and
never persists across render transactions.

### 11.3 The renderer does not own application lifecycle

The renderer MUST NOT maintain a run loop, event loop, timer, or subscription
Expand Down
Loading