Skip to content

Commit 2c6215c

Browse files
authored
feat: term.update() folds CapabilityEvents into runtime capabilities (#133)
1 parent d8f2a24 commit 2c6215c

8 files changed

Lines changed: 300 additions & 152 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@bomb.sh/tty': minor
3+
---
4+
5+
Adds `detectTerminal()`, `TerminalInfo`, `Capabilities`, `DetectOptions`, `KeyTable`, and `MAX_TERMINFO_ENTRY` to the public API, and a `terminfo` option to `createTerm`.
6+
7+
`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.
8+
9+
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).
10+
11+
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).
12+
13+
#### Migration
14+
15+
```diff
16+
-term.update({ events });
17+
+const out = term.update(events);
18+
+if (out.length) process.stdout.write(out);
19+
```
20+
21+
```diff
22+
-term.update({ width, height });
23+
+term.update([{ type: "resize", width, height }]);
24+
```
25+
26+
To opt in to terminfo-based capability detection:
27+
28+
```diff
29+
+const terminfo = await detectTerminal({ env: process.env });
30+
const term = await createTerm({ width, height, terminfo });
31+
const input = await createInput({ terminfo });
32+
+process.stdout.write(terminfo.probe);
33+
```

‎specs/renderer-spec.md‎

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -418,22 +418,22 @@ The update transaction changes a Term instance's dimensions or capability state
418418
in place. Like the render transaction (§7.2), it is synchronous: it MUST NOT
419419
yield, suspend, or require callbacks during execution.
420420

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

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

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

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

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

@@ -672,39 +672,34 @@ wherever the directive model expects a color.
672672
### 8.6 Term update
673673

674674
```
675-
term.update(change: Update | readonly Update[]): Uint8Array
676-
677-
type Update =
678-
| { width: number; height: number }
679-
| InputEvent
675+
term.update(events: readonly InputEvent[]): Uint8Array
680676
```
681677

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

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

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

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

700696
```
701697
const { events } = input.scan(bytes);
702-
for (const event of events) {
703-
if (event.type === "resize" || event.type === "capability") {
704-
const out = term.update(event);
705-
if (out.length) stdout.write(out);
706-
}
707-
}
698+
const out = term.update(events);
699+
if (out.length) stdout.write(out);
700+
701+
// on SIGWINCH
702+
term.update([{ type: "resize", width: cols(), height: rows() }]);
708703
```
709704

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

1142+
**Capability state.** `RuntimeCapabilities` is currently held in TypeScript and
1143+
folded by `update()`; the WASM renderer state carries no capability fields
1144+
because no renderer output depends on them yet (§7.8). When a
1145+
capability-consuming feature lands, the authoritative state is expected to move
1146+
into WASM linear memory alongside the renderer state, so that `render()` reads
1147+
it without per-frame transfer; `term.capabilities` remains a frozen snapshot
1148+
decoded on access.
1149+
11471150
**Layout engine.** The underlying layout engine is Clay, included as a
11481151
dependency. Clay provides flexbox-like layout computation with support for
11491152
fixed, grow, and fit sizing; padding; alignment; direction; gap; floating

‎specs/terminfo-spec.md‎

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -511,19 +511,15 @@ When omitted, the parser uses built-in xterm key sequences.
511511
```ts
512512
interface Term {
513513
render(ops: Op[], options?: RenderOptions): RenderResult;
514-
update(change: Update | readonly Update[]): Uint8Array;
514+
update(events: readonly InputEvent[]): Uint8Array;
515515
readonly capabilities: RuntimeCapabilities;
516516
}
517-
518-
type Update =
519-
| { width: number; height: number }
520-
| InputEvent;
521517
```
522518

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

546542
process.stdin.on("data", (bytes: Uint8Array) => {
547543
const { events } = input.scan(bytes);
548-
for (const event of events) {
549-
switch (event.type) {
550-
case "capability":
551-
case "resize": {
552-
const out = term.update(event);
553-
if (out.length) process.stdout.write(out);
554-
break;
555-
}
556-
default:
557-
dispatch(event);
558-
}
559-
}
544+
const out = term.update(events);
545+
if (out.length) process.stdout.write(out);
546+
for (const event of events) dispatch(event);
560547
});
561548

562549
process.on("SIGWINCH", () => {
563-
const out = term.update({ width: cols(), height: rows() });
550+
const out = term.update([{ type: "resize", width: cols(), height: rows() }]);
564551
if (out.length) process.stdout.write(out);
565552
});
566553
```

‎src/terminfo.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* terminfo.c — shared terminal capability layer */
1+
/* terminfo.c — terminal capability layer */
22

33
#include "terminfo.h"
44

‎src/terminfo.h‎

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

910
#ifndef TERMINFO_H
@@ -29,8 +30,7 @@
2930
#define TERMINFO_THEME_CURSOR (1u << 14)
3031

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

3636
struct TermInfo {

0 commit comments

Comments
 (0)