Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 33 additions & 0 deletions .changeset/terminfo-capability-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
Comment thread
bombshell-cooper[bot] marked this conversation as resolved.
'@bomb.sh/tty': minor
---

Adds `detectTerminal()`, `TerminalInfo`, `Capabilities`, `DetectOptions`, `KeyTable`, and `MAX_TERMINFO_ENTRY` to the public API, and a `terminfo` option to `createTerm`.

`detectTerminal()` reads the compiled terminfo entry for the current terminal (from the ncurses search path, or from bytes passed as `entry`), applies environment evidence (`COLORTERM`), and resolves a frozen `TerminalInfo` carrying static `capabilities`, a `probe` query batch to write to stdout, and opaque `keys` for the input parser.

Pass the returned `TerminalInfo` as `terminfo` to `createTerm` and `createInput`. `term.capabilities` exposes the renderer's current capability snapshot, seeded from `terminfo.capabilities` (or the 256-color baseline when omitted).

Changes `term.update()` to accept an array of `InputEvent` values instead of `{ events }` or `{ width, height }`. Resizes are now `ResizeEvent`s tagged `type: "resize"`; capability events from `scan()` are folded into `term.capabilities`; all other input events are no-ops, so the full `events` array from `scan()` can be passed straight through. `update()` now returns a `Uint8Array` of bytes to write immediately (empty when there are none).

#### Migration

```diff
-term.update({ events });
+const out = term.update(events);
+if (out.length) process.stdout.write(out);
```

```diff
-term.update({ width, height });
+term.update([{ type: "resize", width, height }]);
```

To opt in to terminfo-based capability detection:

```diff
+const terminfo = await detectTerminal({ env: process.env });
const term = await createTerm({ width, height, terminfo });
const input = await createInput({ terminfo });
+process.stdout.write(terminfo.probe);
```
65 changes: 34 additions & 31 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,22 +418,22 @@ The update transaction changes a Term instance's dimensions or capability state
in place. Like the render transaction (§7.2), it is synchronous: it MUST NOT
yield, suspend, or require callbacks during execution.

**Inputs.** The update transaction accepts one `Update` or an ordered array of
`Update` values. `Update` is a discriminated union:
**Inputs.** The update transaction accepts an ordered array of `InputEvent`
values (see [Input Specification](input-spec.md) §5), discriminated by `type`:

- `{ width: number; height: number }` — a resize to the given character-cell
dimensions. Both MUST be positive integers; the transaction MUST throw
otherwise.
- A `ResizeEvent` (`{ type: "resize"; width: number; height: number }`) — a
resize to the given character-cell dimensions. Both MUST be positive integers;
the transaction MUST throw otherwise.
- A `CapabilityEvent` (see [Terminfo Specification](terminfo-spec.md) §6.3) — a
capability value delivered by the input parser from a probe response.
- Any other `InputEvent` (see [Input Specification](input-spec.md) §5) — a no-op
step. It changes no state and contributes no bytes.
- Any other `InputEvent` — a no-op step. It changes no state and contributes no
bytes.

When a batch is provided, the Term folds each `Update` in order. The returned
bytes are the concatenation of each fold's output.
The Term folds each event in array order. The returned bytes are the
concatenation of each fold's output. An empty array is a no-op.

A resize `Update` whose target dimensions equal the Term's current dimensions
MUST be a no-op for that step.
A resize event whose target dimensions equal the Term's current dimensions MUST
be a no-op for that step.

**Resize semantics.** A non-no-op resize step:

Expand Down Expand Up @@ -672,39 +672,34 @@ wherever the directive model expects a color.
### 8.6 Term update

```
term.update(change: Update | readonly Update[]): Uint8Array

type Update =
| { width: number; height: number }
| InputEvent
term.update(events: readonly InputEvent[]): Uint8Array
```

Performs an update transaction as defined in §7.7. `update()` is the universal
sink for both resize and capability change.

**`Update` shapes.** A resize step is `{ width, height }`. A capability step is
any `CapabilityEvent` value (see [Terminfo Specification](terminfo-spec.md)
§6.3). The two shapes are structurally distinct and MUST NOT be combined in a
single object. Every other `InputEvent` is accepted and ignored, so the full
`events` array from `input.scan()` can be passed without filtering. Pass an
array to apply multiple updates in one call; they are folded in order.
**Events.** A resize step is a `ResizeEvent`
(`{ type: "resize", width, height }`). A capability step is any
`CapabilityEvent` value (see [Terminfo Specification](terminfo-spec.md) §6.3).
Every other `InputEvent` is accepted and ignored, so the full `events` array
from `input.scan()` can be passed without filtering. Events are folded in array
order.

**Return value.** `update()` always returns a `Uint8Array`. Write it to the
terminal immediately when non-empty. Do not wait for the next `render()`. An
empty array means the update changed no rendered state.

**Resize shape.** The `{ width, height }` shape is defined structurally by this
specification. It is intentionally assignable from the input specification's
`ResizeEvent`, so events from `input.scan()` pass through directly:
**Host usage.** Events from `input.scan()` pass through directly. Resizes
observed outside the input stream (e.g. `SIGWINCH`) are passed as a constructed
`ResizeEvent`:

```
const { events } = input.scan(bytes);
for (const event of events) {
if (event.type === "resize" || event.type === "capability") {
const out = term.update(event);
if (out.length) stdout.write(out);
}
}
const out = term.update(events);
if (out.length) stdout.write(out);

// on SIGWINCH
term.update([{ type: "resize", width: cols(), height: rows() }]);
```

A resize to the Term's current dimensions is a no-op for that step.
Expand Down Expand Up @@ -1144,6 +1139,14 @@ generated module and instantiated per Term or Input with fresh memory.
renderer state struct and the transfer buffer are allocated in WASM linear
memory. The specific layout is an implementation detail.

**Capability state.** `RuntimeCapabilities` is currently held in TypeScript and
folded by `update()`; the WASM renderer state carries no capability fields
because no renderer output depends on them yet (§7.8). When a
capability-consuming feature lands, the authoritative state is expected to move
into WASM linear memory alongside the renderer state, so that `render()` reads
it without per-frame transfer; `term.capabilities` remains a frozen snapshot
decoded on access.

**Layout engine.** The underlying layout engine is Clay, included as a
dependency. Clay provides flexbox-like layout computation with support for
fixed, grow, and fit sizing; padding; alignment; direction; gap; floating
Expand Down
29 changes: 8 additions & 21 deletions specs/terminfo-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,19 +511,15 @@ When omitted, the parser uses built-in xterm key sequences.
```ts
interface Term {
render(ops: Op[], options?: RenderOptions): RenderResult;
update(change: Update | readonly Update[]): Uint8Array;
update(events: readonly InputEvent[]): Uint8Array;
readonly capabilities: RuntimeCapabilities;
}

type Update =
| { width: number; height: number }
| InputEvent;
```

`update()` accepts one change or a batch. `InputEvent` values other than
`update()` accepts an array of `InputEvent` values. Events other than
`ResizeEvent` and `CapabilityEvent` are no-op steps, so the full `events` array
from `scan()` can be passed without filtering. A batch is folded in order: each
`Update` produces the next `RuntimeCapabilities` and any bytes defined by the
from `scan()` can be passed without filtering. Events are folded in order: each
event produces the next `RuntimeCapabilities` and any bytes defined by the
consuming feature, and the returned bytes are concatenated. The foundation
defines no such bytes. The return value is always a `Uint8Array`; callers write
it to their output stream when non-empty (TINV-5).
Expand All @@ -545,22 +541,13 @@ process.stdout.write(terminfo.probe);

process.stdin.on("data", (bytes: Uint8Array) => {
const { events } = input.scan(bytes);
for (const event of events) {
switch (event.type) {
case "capability":
case "resize": {
const out = term.update(event);
if (out.length) process.stdout.write(out);
break;
}
default:
dispatch(event);
}
}
const out = term.update(events);
if (out.length) process.stdout.write(out);
for (const event of events) dispatch(event);
});

process.on("SIGWINCH", () => {
const out = term.update({ width: cols(), height: rows() });
const out = term.update([{ type: "resize", width: cols(), height: rows() }]);
if (out.length) process.stdout.write(out);
});
```
Expand Down
2 changes: 1 addition & 1 deletion src/terminfo.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* terminfo.c — shared terminal capability layer */
/* terminfo.c — terminal capability layer */

#include "terminfo.h"

Expand Down
12 changes: 6 additions & 6 deletions src/terminfo.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* terminfo.h — shared terminal capability layer
/* terminfo.h — terminal capability layer
*
* Implements the capability struct and terminfo binary parsing defined
* by specs/terminfo-spec.md. The renderer reads the struct; the input
* parser writes probe responses into it; this module owns the baseline
* and the parse path.
* by specs/terminfo-spec.md. detectTerminal() uses the struct to resolve
* static capabilities; the renderer and the input parser share no
* memory through it (terminfo-spec 4.2, TINV-6). This module owns the
* baseline and the parse path.
*/

#ifndef TERMINFO_H
Expand All @@ -29,8 +30,7 @@
#define TERMINFO_THEME_CURSOR (1u << 14)

/* Probe-fence marker: set in `confirmed` (never in `flags`) when a DA1
* device attributes report is recognized. The queryTermInfo probe
* window uses it to detect completion. */
* device attributes report is recognized. */
#define TERMINFO_DA1 (1u << 31)

struct TermInfo {
Expand Down
Loading
Loading