Commits
Fix two blocking issues before merge:
1. When maxLines=1 and isAtBottom=false, evicting the anchor line left
the anchor pointing at a gone lineIndex. resolveViewport() returned
no entries from a non-empty buffer. Removed the `lineCount > 1` guard
so the anchor always clamps to `evictedLineIndex + 1`. Added
regression test R.EVICT.maxLines1-not-at-bottom.
2. Resolved the contradiction between O-9 ("every slice fits within
columns") and the columns=1 + width-2 CJK glyph behavior. Documented
columns ≥ max glyph width as a precondition on VirtualizerOptions,
updated O-9 test name, wrap-golden test labels, and the spec.
1. resize() now clamps anchorSubRow using exact wrap count instead of
ceil(displayWidth/columns) estimate, which undercounts when boundary
waste from wide characters increases actual sub-row count.
2. _recomputeCurrentEstimate() clamps currentEstimatedVisualRow to
[0, totalEstimatedVisualRows-1] so the scrollbar invariant holds
even when exact wrap counts exceed the estimate.
3. computeWrapPoints() no longer emits wrap point at index 0 when the
first visible character is wider than columns — the character
overflows its row instead of creating an empty first sub-row.
Adds 4 regression tests covering all three fixes.
Property tests cover identity monotonicity, eviction stability,
estimation constraints, and viewport invariants under random ops.
Benchmarks report informational gate results for all 7 perf gates.
Implement Virtualizer.resize(): clear wrap cache on column change,
recompute estimated visual rows, clamp anchor sub-row. Row-only
changes update rows without invalidation.
Add 17 new tests: C.RESIZE.* (8), G.RESIZE.* (2), deferred
C.APPEND.caches-displayWidth (1), exactness structural tests (6).
109 virtualizer tests total, 249 repo-wide.
Implement the virtualizer package for viewport virtualization over
large terminal text output. This covers PRs 1–3 of the implementation
plan:
- PR 1: Export createDisplayWidth from renderer (WASM per-codepoint
wcwidth, R.WIDTH.* tests)
- PR 2: Virtualizer core — appendLine, resolveViewport, ring buffer,
ANSI scanner, wrap walker, getLineDisplayWidth (~66 tests)
- PR 3: scrollBy + scrollToFraction with all deferred scroll-dependent
tests (~92 tests total)
resize() still throws "not implemented" — deferred to PR 4.
Adds the v0.1 draft specification covering the architectural model,
core invariants, public rendering API, descriptor model, and boundary
responsibilities. Non-normative sections document settling surfaces
(transfer encoding, input parsing, pointer events) separately from
the stable rendering core.
Add line rendering mode, termcodes, and inline region demo
Rename csi.ts to termcodes.ts and add ESC(), SHOWCURSOR(),
HIDECURSOR(), ALTSCREEN(), and MAINSCREEN() helpers. Make
CursorEvent.row/column 1-based to match DSR native format.
Replace all raw escape sequences in the demo with termcodes.
Use DECSC/DECRC (ESC 7/8) for cursor save/restore instead of
SCO (CSI s/u).
Move the row offset from a constructor parameter to a render-time
option. Row is now 1-based (matching ECMA-48 DSR/CPR format) so
callers can pass the queried cursor position directly without
conversion. Remove line mode from the inline region demo in favor
of raw newline allocation followed by CUP rendering for all frames.
This forces a render of all cells on the very first frame, which is
perfectly acceptable.
Add { mode: "line" } option to render() that emits newline-separated
rows instead of CUP positioning sequences. Line mode writes every cell
and primes the front buffer, so subsequent diff renders work without
a full redraw.
Inline WASM binary into generated TypeScript module
Replace runtime filesystem read of clayterm.wasm with a build step
that base64-encodes the binary into wasm.ts. This removes the
node:fs/promises dependency and --allow-read requirement for the
WASM file.
Accept signed int32 colors in validator
The rgba() function returns signed int32 when alpha >= 128 (bit 31
set). The validator's u32 type rejected these negative values. Add
a color type that accepts the full signed-to-unsigned 32-bit range.
The rgba() function returns signed int32 when alpha >= 128 (bit 31
set). The validator's u32 type rejected these negative values. Add
a color type that accepts the full signed-to-unsigned 32-bit range.
Add CursorEvent with 0-based top/left coords by parsing the terminal's
DSR response (\x1b[row;colR). Rename createTerm's row option to top
so cursor.top feeds directly into createTerm({ top: cursor.top }).
Add a `row` option to TermOptions that offsets all CUP sequences by
the given number of rows, enabling rendering into a region below
existing terminal content rather than always starting at row 1.
Introduce a Setting type that pairs apply/revert escape sequences,
ensuring terminal state is always cleanly restored. Refactor the
keyboard demo to use settings() for alternate buffer, cursor, mouse
tracking, and progressive input.
Clay's pointer hit-testing is now wired through render() via RenderOptions,
returning pointerenter/pointerleave/pointerclick events by diffing
Clay_GetPointerOverIds() across frames. The demo keyboard shows hover
highlights on keys, semantic element IDs, and configurable event log filters.
Correct modifier key codepoint mapping to match the spec (all-lefts-first
57441–57446, then all-rights 57447–57452). Add parser for Kitty-enhanced
legacy CSI sequences (arrows, function keys, tilde-style) that include
`:action` sub-parameters.
Replace CharEvent/KeyEvent with keydown/keyrepeat/keyup discriminated
union. Rewrite the CSI u parser to extract sub-fields: action type
(press/repeat/release), alternate keys (shifted/base), and associated
text codepoints. Legacy terminals emit keydown only; enhanced terminals
add keyrepeat and keyup. Text is populated for printable keys at all
enhancement levels.
Add typedef.ts (ported from clayscript) for declarative C struct
layout computation. Replace hand-written offset constants in
input-native.ts with offsets derived from the struct definition,
eliminating manual offset math while keeping direct DataView
access for V8 performance.
Parse CSI codepoint;modifiers u sequences from terminals that support
the Kitty keyboard protocol (Kitty, Ghostty, WezTerm, iTerm2, etc).
Maps special key codepoints (Tab, Enter, Escape, Backspace, F1-F12,
arrows, etc.) to existing KEY_* constants, and decodes modifier
bitfields to MOD_* flags.
Protocol overview: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
Key encoding: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-event-encoding
Functional keys: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
Modifier encoding: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
In setcell(), skip overwriting bg when the incoming value is
ATTR_DEFAULT so that backgrounds painted by render_rect() flow
through to text cells.
Fixes #1
Size memory dynamically based on clayterm_size() instead of hardcoding
256 pages, which was too small for large terminals.
Prevent silent memory corruption with a bounds check in pack() to make
sure we do not pack more operations that can fit in the buffer.
1MB should fit thousands of elements.
Add a complete VT/ANSI terminal input parser that converts VT200/SGR/urxvt
mouse protocols, UTF-8 decoding, and escape sequence into input events
Introduce a `validate` module that uses @sinclair/typebox to
enforce correct op structure and numeric ranges (u8/u16/u32)
before rendering. Exports `validate`, `assert`, and a `validated`
Term wrapper. Adds the `./validate` subpath export.
Use explicit byte-loop implementations instead of __builtin_mem*
to avoid infinite recursion when clang lowers builtins back to
libc calls on some versions.
- Change Term.render() to return Uint8Array instead of string, avoiding
unnecessary TextDecoder overhead on every frame
- Fix Clay element ID collisions by adding a monotonic index to the hash
- Remove cursor hide/show from the C render path (caller responsibility)
- Update README example and tests to match new return type
WASM-based terminal renderer powered by Clay UI layout engine.
Includes command buffer protocol for TS→C communication,
double-buffered ANSI output, and Deno test harness.
Fix two blocking issues before merge:
1. When maxLines=1 and isAtBottom=false, evicting the anchor line left
the anchor pointing at a gone lineIndex. resolveViewport() returned
no entries from a non-empty buffer. Removed the `lineCount > 1` guard
so the anchor always clamps to `evictedLineIndex + 1`. Added
regression test R.EVICT.maxLines1-not-at-bottom.
2. Resolved the contradiction between O-9 ("every slice fits within
columns") and the columns=1 + width-2 CJK glyph behavior. Documented
columns ≥ max glyph width as a precondition on VirtualizerOptions,
updated O-9 test name, wrap-golden test labels, and the spec.
1. resize() now clamps anchorSubRow using exact wrap count instead of
ceil(displayWidth/columns) estimate, which undercounts when boundary
waste from wide characters increases actual sub-row count.
2. _recomputeCurrentEstimate() clamps currentEstimatedVisualRow to
[0, totalEstimatedVisualRows-1] so the scrollbar invariant holds
even when exact wrap counts exceed the estimate.
3. computeWrapPoints() no longer emits wrap point at index 0 when the
first visible character is wider than columns — the character
overflows its row instead of creating an empty first sub-row.
Adds 4 regression tests covering all three fixes.
Implement Virtualizer.resize(): clear wrap cache on column change,
recompute estimated visual rows, clamp anchor sub-row. Row-only
changes update rows without invalidation.
Add 17 new tests: C.RESIZE.* (8), G.RESIZE.* (2), deferred
C.APPEND.caches-displayWidth (1), exactness structural tests (6).
109 virtualizer tests total, 249 repo-wide.
Implement the virtualizer package for viewport virtualization over
large terminal text output. This covers PRs 1–3 of the implementation
plan:
- PR 1: Export createDisplayWidth from renderer (WASM per-codepoint
wcwidth, R.WIDTH.* tests)
- PR 2: Virtualizer core — appendLine, resolveViewport, ring buffer,
ANSI scanner, wrap walker, getLineDisplayWidth (~66 tests)
- PR 3: scrollBy + scrollToFraction with all deferred scroll-dependent
tests (~92 tests total)
resize() still throws "not implemented" — deferred to PR 4.
Rename csi.ts to termcodes.ts and add ESC(), SHOWCURSOR(),
HIDECURSOR(), ALTSCREEN(), and MAINSCREEN() helpers. Make
CursorEvent.row/column 1-based to match DSR native format.
Replace all raw escape sequences in the demo with termcodes.
Use DECSC/DECRC (ESC 7/8) for cursor save/restore instead of
SCO (CSI s/u).
Move the row offset from a constructor parameter to a render-time
option. Row is now 1-based (matching ECMA-48 DSR/CPR format) so
callers can pass the queried cursor position directly without
conversion. Remove line mode from the inline region demo in favor
of raw newline allocation followed by CUP rendering for all frames.
The rgba() function returns signed int32 when alpha >= 128 (bit 31
set). The validator's u32 type rejected these negative values. Add
a color type that accepts the full signed-to-unsigned 32-bit range.
Replace CharEvent/KeyEvent with keydown/keyrepeat/keyup discriminated
union. Rewrite the CSI u parser to extract sub-fields: action type
(press/repeat/release), alternate keys (shifted/base), and associated
text codepoints. Legacy terminals emit keydown only; enhanced terminals
add keyrepeat and keyup. Text is populated for printable keys at all
enhancement levels.
Parse CSI codepoint;modifiers u sequences from terminals that support
the Kitty keyboard protocol (Kitty, Ghostty, WezTerm, iTerm2, etc).
Maps special key codepoints (Tab, Enter, Escape, Backspace, F1-F12,
arrows, etc.) to existing KEY_* constants, and decodes modifier
bitfields to MOD_* flags.
Protocol overview: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
Key encoding: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-event-encoding
Functional keys: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
Modifier encoding: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
Add a complete VT/ANSI terminal input parser that converts VT200/SGR/urxvt
mouse protocols, UTF-8 decoding, and escape sequence into input events
- Change Term.render() to return Uint8Array instead of string, avoiding
unnecessary TextDecoder overhead on every frame
- Fix Clay element ID collisions by adding a monotonic index to the hash
- Remove cursor hide/show from the C render path (caller responsibility)
- Update README example and tests to match new return type