Skip to content

feat: in-place resize via term.update() - #113

Open
natemoo-re wants to merge 3 commits into
mainfrom
nm/feat/resize
Open

feat: in-place resize via term.update()#113
natemoo-re wants to merge 3 commits into
mainfrom
nm/feat/resize

Conversation

@natemoo-re

@natemoo-re natemoo-re commented Aug 22, 2026

Copy link
Copy Markdown
Member

previously, handling a terminal resize meant throwing away the Term and building a new one at the new dimensions — a fresh wasm instance every time, with per-frame diff state lost in the swap.

now, term.update() resizes a Term in place. it takes either explicit { width, height } or { events }, so the resize events from input.scan() can be passed straight through (last resize wins; non-resize events are ignored).

details: no new wasm module or instance is ever created — the existing instance re-lays-out its renderer state and grows its linear memory only as needed. memory only grows, keeping the high-water mark until the Term is discarded. a real resize discards diff and pointer state so the next render() is a complete redraw; a same-size update is a no-op, and invalid dimensions throw.

this also flips the original stacking: update() lands first as the primitive, and terminfo (#106) can then evolve to pass its capability data through the same call.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Size Reduced — -15.6 KB

102.7 KB unpacked

@pkg-pr-new

pkg-pr-new Bot commented Aug 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@bomb.sh/tty@113

commit: 3d1937f

@cowboyd cowboyd mentioned this pull request Aug 26, 2026
12 tasks
Replaces the create-a-new-Term resize story (old 7.4) with a synchronous
update transaction: reallocate renderer state in place within the existing
wasm instance and linear memory, so a resized Term stays valid and keeps
rendering at the new dimensions.

Accepts a discriminated options bag: explicit dimensions or an event array
(resize events coalesce last-wins; non-resize events ignored). The event
shape is structural, deliberately assignable from input's ResizeEvent
without introducing a renderer->input dependency (11.4).

After a non-no-op update the next render is a complete redraw. Growth-only
memory (high-water mark on downsize) is documented as accepted behavior.
Output views are invalidated by update() since memory.grow detaches
buffers; 7.3's validity window is widened accordingly.
term.update() changes a Term's dimensions in place: it re-inits renderer
state in the existing wasm instance and linear memory rather than building a
new Term. statePtr and opsBuf move as the state region is re-laid-out, so
they become getters; the standalone [heap: state][opsBuf] allocation is
factored into a reusable layout() that both the constructor and update() call.

The options bag is a discriminated union of explicit dimensions or an event
array (last resize wins; non-resize ignored). Invalid dimensions throw; a
same-size update is a no-op. A real resize discards JS-side pointer and
timing state and the wasm diff buffers, so the next render is a full redraw.
Covers redraw-after-update, same-size no-op, invalid-dimension throws,
event-array coalescing (last resize wins, non-resize ignored, empty = no-op),
pointer-state reset, layout at the new dimensions, and downsize-then-upsize
past the original size (growth-only memory).
@natemoo-re
natemoo-re changed the base branch from feat/terminfo to main September 1, 2026 00:47
@natemoo-re natemoo-re closed this Sep 1, 2026
@natemoo-re natemoo-re reopened this Sep 1, 2026
@natemoo-re
natemoo-re marked this pull request as ready for review September 1, 2026 12:56
@natemoo-re
natemoo-re requested a review from cowboyd September 1, 2026 12:56
@cowboyd

cowboyd commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

@natemoo-re There's a bit of a dance at the beginning of initialization where we ask clay how much memory it needs, then we allocate it, and then we hand it the memory. This is what allows Clay to not need an allocator and not need libc at all!

Do we need to re-allocate the clay arena as well?

@natemoo-re

Copy link
Copy Markdown
Member Author

@cowboyd great call out! we might? I'll have to double check.

I do have a local branch that does some optimization to clamp the clay arena to the worst case scenario for the given terminal size, which layers nicely on top of this. I can push up the draft.

natemoo-re added a commit that referenced this pull request Sep 1, 2026
Clay preallocates its arena from a fixed default of 8192 max elements,
so an 80x24 terminal reserved 4.85 MB of linear memory before rendering
anything. Element capacity drives nearly all of Clay_MinMemorySize
(~600 B/element on wasm32); the measure-text word cache is second-order.

Derive the cap from the grid instead: 2x cells, clamped to [2048, 8192].
Leaf elements occupy at least one cell and internal nodes with branching
>= 2 can at most double the leaf count, so 2x cells bounds realistic
trees while leaving headroom for floating elements and wrapper chains.
The ceiling keeps large grids at exactly today's capacity; the floor
protects tiny panes.

Arena for 80x24 drops 4.85 MB -> 2.39 MB (-51%); 20x5 bottoms out at
1.23 MB; 200x50 is unchanged. Render benches are flat.

The capacity setters run at the top of both clayterm_size() and init()
so the size the host queries always matches what init consumes, for the
first init and for in-place re-init alike: Clay_MinMemorySize prefers
the live context's caps and Clay_Initialize inherits them from the old
context, so the update() resize flow (#113) sizes correctly in both
directions. Verified against a local merge of nm/feat/resize: full
suite passes, and renders stay error-free across 80x24 -> 200x50 ->
20x5 -> 132x43 in-place resizes.

Exceeding the cap degrades the same way it always has, just at the new
boundary: Clay stops opening elements and the frame surfaces an error
through RenderResult.errors.
natemoo-re added a commit that referenced this pull request Sep 1, 2026
* ref(opt): scale Clay element capacity with the terminal grid

Clay preallocates its arena from a fixed default of 8192 max elements,
so an 80x24 terminal reserved 4.85 MB of linear memory before rendering
anything. Element capacity drives nearly all of Clay_MinMemorySize
(~600 B/element on wasm32); the measure-text word cache is second-order.

Derive the cap from the grid instead: 2x cells, clamped to [2048, 8192].
Leaf elements occupy at least one cell and internal nodes with branching
>= 2 can at most double the leaf count, so 2x cells bounds realistic
trees while leaving headroom for floating elements and wrapper chains.
The ceiling keeps large grids at exactly today's capacity; the floor
protects tiny panes.

Arena for 80x24 drops 4.85 MB -> 2.39 MB (-51%); 20x5 bottoms out at
1.23 MB; 200x50 is unchanged. Render benches are flat.

The capacity setters run at the top of both clayterm_size() and init()
so the size the host queries always matches what init consumes, for the
first init and for in-place re-init alike: Clay_MinMemorySize prefers
the live context's caps and Clay_Initialize inherits them from the old
context, so the update() resize flow (#113) sizes correctly in both
directions. Verified against a local merge of nm/feat/resize: full
suite passes, and renders stay error-free across 80x24 -> 200x50 ->
20x5 -> 132x43 in-place resizes.

Exceeding the cap degrades the same way it always has, just at the new
boundary: Clay stops opening elements and the frame surfaces an error
through RenderResult.errors.

* chore: add changeset for grid-scaled arena
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants