pub struct InkRoot { /* private fields */ }Expand description
The real InkRoot: owns the persistent DOM arena, the per-frame transport
writer, the JS transform dispatcher, and the cursor-dirty gate.
Implementations§
Source§impl InkRoot
impl InkRoot
Sourcepub fn new(
transform_dispatcher: Function<'_, FnArgs<(u32, String, u32)>, String>,
) -> Result<Self>
pub fn new( transform_dispatcher: Function<'_, FnArgs<(u32, String, u32)>, String>, ) -> Result<Self>
new(transform_dispatcher) — immediately create_ref() the supplied
Function and store the resulting FunctionRef. The Function handle
itself does not outlive this call; the FunctionRef does. The arena and
frame writer start empty; the JS reconciler builds the tree via commit.
Sourcepub fn commit(&mut self, ops: Buffer) -> Result<()>
pub fn commit(&mut self, ops: Buffer) -> Result<()>
commit(ops) — apply a batch of DOM mutations transactionally.
Decodes the ENTIRE op buffer first (M3-C decode_ops). Because the
decoder is all-or-nothing, a malformed buffer returns a typed napi error
with ZERO arena mutation — prior DOM state is preserved. Only a fully
decoded Vec<Op> is handed to dom::apply. The id-magnitude bound runs
before apply to keep the arena’s resize_with out of OOM-abort range.
Sourcepub fn render_frame(
&mut self,
env: Env,
mode: RenderMode,
opts: RenderOpts,
) -> Result<Option<FrameResult>>
pub fn render_frame( &mut self, env: Env, mode: RenderMode, opts: RenderOpts, ) -> Result<Option<FrameResult>>
render_frame(env, mode, opts) -> Option<FrameResult> — the production
render path (M3-E keystone).
Pipeline: style the arena through the core entry (render_styled, M3-B),
dispatching each has_transform node’s transform to the stored JS
dispatcher mid-walk; render the <Static> subtree once via render_static
(renderer.ts’s second pass — "" when the tree has no static node); then
wire the live (string, height) plus the static string into
FrameWriter::write_frame and return its diffed bytes.
Ordering (the M3-0 throw-discard template): render_styled builds the
whole styled string into LOCALS, firing every dispatcher call during its
walk — BEFORE any FrameWriter mutation. A dispatcher throw is captured in
err_cell and surfaces as this method’s Err before write_frame runs,
so a failed render leaves the writer’s diff baseline untouched.
Returns None when write_frame produced no bytes (a no-change frame:
FrameWriter returns an empty Vec for an unchanged output), mirroring
rt’s “willRender” no-op semantics; Some(FrameResult) otherwise.
Sourcepub fn render_to_string(
&mut self,
env: Env,
width: u16,
color_level: u8,
) -> Result<String>
pub fn render_to_string( &mut self, env: Env, width: u16, color_level: u8, ) -> Result<String>
render_to_string(width, colorLevel) — one-shot debug render: a plain full
frame of the committed arena at width, with NO diff-state mutation (it
routes through render_frame’s Debug mode + throwaway writer). This is
the surface the npm renderToString (M3-L) wires; it returns the visible
string, so it MUST honor the detected color level just like render_frame.
color_level is the JS-detected chalk.level (0–3, the SAME value the npm
renderToString threads into render_frame for the static-capture pass),
so the returned border/Box-bg SGR matches ink: none at level 0, downgraded
at 1/2, truecolor at 3.
Returns the plain styled output string (mode-independent: the same string
render_styled produces), which is what renderToString returns. The
transport bytes are irrelevant to a string query, so they are discarded.
Sourcepub fn free(&mut self, id: u32) -> Result<()>
pub fn free(&mut self, id: u32) -> Result<()>
free(id) — JS-driven lifecycle without a Rust finalizer.
JS owns the u32 id space and calls this on detachDeletedInstance (M3-G).
It applies a single Free op to the arena (drop one slot, no cascade —
dom::apply semantics).
Reentrancy-guarded like commit/render_frame: free is a third
&mut self.arena mutation, so a transform dispatcher re-entering it
mid-walk would take &mut self.arena while the render holds &self.arena
(aliasing UB) AND drop a node out of the very tree being walked. It reads
rendering FIRST and rejects a reentrant call with the same catchable
typed error. Returning Result<()> is what surfaces that throw to JS — the
reconciler’s detachDeletedInstance must handle the (vanishingly rare)
reentry rejection.
Sourcepub fn set_cursor(&mut self, pos: Option<CursorPos>) -> Result<()>
pub fn set_cursor(&mut self, pos: Option<CursorPos>) -> Result<()>
set_cursor(pos) — store the cursor position and flip the cursor_dirty
gate.
Mirrors ink’s render.setCursorPosition (log-update.ts:163-166)
EXACTLY: cursorPosition = position; cursorDirty = true. inkferro now
STORES pos in cursor_position (mapping the
napi u32 CursorPos onto rt’s usize RtCursorPos) AND sets the
dirty gate. The next render_frame resolves the ACTIVE cursor as
active = cursor_dirty ? cursor_position : None (getActiveCursor,
log-update.ts:43), passes it into FrameParams.cursor, and the rt
composes the ink-faithful cursor escape bytes — so a cursor-only change on
unchanged content now produces a frame (the useCursor/IME behavior).
None mirrors setCursorPosition(undefined): it CLEARS the stored
position and (still) sets dirty, so the next render’s active cursor is
None and the rt emits the hide sequence if a cursor was shown.
Reentrancy-guarded like commit: it writes &mut self.cursor_dirty/
&mut self.cursor_position, which a transform dispatcher re-entering
mid-render would alias against the render’s read of the same fields. Reads
rendering FIRST and rejects a reentrant call with the same catchable
typed error, touching nothing else.
Sourcepub fn measure(&self, id: u32) -> Rect
pub fn measure(&self, id: u32) -> Rect
measure(id) -> Rect — read a node’s computed layout (M3-F).
Mirrors ink’s measureElement (dom.ts): returns a node’s computed
{width, height, left, top}. inkferro has no stored engine (ADR-3 rebuilds
per frame), so measure rebuilds the layout engine from the persistent
arena at the last-rendered WIDTH and reads computed(id).
SEMANTICS — current-DOM-at-last-width, NOT a frozen last-layout snapshot.
The rebuild reads the arena as it stands NOW, laid out at the last render’s
width. In ink’s reconciler every commit is followed by a render in
resetAfterCommit, so the arena and the last layout never diverge at a
measure call (the M3-J call pattern is render→measure). Reading the live
arena is also what makes the trust cases fall out for free: a node freed
after the last render rebuilds WITHOUT it, so computed(id) is None →
zero Rect — exactly the freed-id contract below. A cached last-layout rect
would instead return STALE non-zero geometry for that freed id, which is
why this rebuilds rather than caches. The only divergence from ink is the
pathological commit-without-render-then-measure, which the reconciler flow
does not produce.
TRUST BOUNDARY: id is an arbitrary u32 from JS. An unknown, freed,
out-of-range, or never-laid-out id (including the no-render-yet case, where
last_render_width is None) returns a ZERO Rect — NEVER a panic, NEVER
an Err. This matches ink’s measureElement on an unmounted ref (yields
zeros). measure is therefore infallible by contract.
Reentrancy: it takes &self, but a &self read aliased against
render_frame’s &mut self is STILL UB under napi-rs v3. So it is guarded
too — but, because measure must stay infallible for bad input, a reentry
returns the SAME safe zero-Rect sentinel instead of erroring.
Sourcepub fn measure_absolute(&self, id: u32) -> Rect
pub fn measure_absolute(&self, id: u32) -> Rect
measureAbsolute(id) -> Rect — like InkRoot::measure, but left/
top are ABSOLUTE (root-relative) coordinates: the sum of the rounded
parent-relative offsets down the ancestor chain, i.e. the cell where the
renderer paints the node (#124). width/height are identical to
measure’s.
Mirrors the jacob314/ink fork’s getBoundingBox accumulation
(measure-element.ts: summing getComputedLeft/Top up parentNode),
which compat6’s getBoundingBox shim is built on. ADDITIVE: measure’s
parent-relative contract is untouched (wire/API covenant).
Same semantics, trust boundary, and reentrancy contract as measure:
rebuilds the layout at the last-rendered width; an unknown, freed,
out-of-range, never-laid-out, or no-render-yet id returns the ZERO Rect
— never a panic, never an Err; a reentrant call returns the same zero
sentinel.
Sourcepub fn clear(&mut self) -> Result<Buffer>
pub fn clear(&mut self) -> Result<Buffer>
clear() -> Buffer — ink’s log.clear() (M3-K3): erase the live frame and
zero the diff baseline, returning the erase bytes for the JS Ink (the sole
stream writer) to emit.
Thin bridge over FrameWriter::clear: returns eraseLines(prevHeight) and
zeroes the LineDiff baseline so the next repaint is full,
while PRESERVING last_output* (so restore_last_output/sync_baseline can
repaint / re-pin afterwards). The one shared erase-emitting gesture the K3
orchestration composes: interactive writeToStdout, instance.clear(), and
resize-shrink all start here.
Reentrancy-guarded like commit/render_frame: it mutates
&mut self.frame_writer, which a transform dispatcher re-entering mid-render
would alias against the render’s own &mut frame_writer (the napi-v3
segfault-on-reentry path). Reads rendering FIRST and rejects a reentrant
call with the same catchable typed error, touching nothing else.
Sourcepub fn sync_baseline(&mut self) -> Result<()>
pub fn sync_baseline(&mut self) -> Result<()>
sync_baseline() — ink’s log.sync(lastOutputToRender || lastOutput + '\n')
(M3-K3): re-pin the diff baseline to the current on-screen frame WITHOUT
emitting any bytes.
Thin bridge over FrameWriter::sync_baseline. Returns nothing (it writes
no bytes — the pure-path LineDiff::sync is byte-free): instance.clear()
composes write(clear()); sync_baseline() so a subsequent unchanged
re-render diffs to a no-op (ink’s “unmount’s final onRender sees it as
unchanged and log-update skips it”).
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn restore_last_output(&mut self) -> Result<Buffer>
pub fn restore_last_output(&mut self) -> Result<Buffer>
restore_last_output() -> Buffer — ink’s restoreLastOutput() (M3-K3):
repaint the last frame from the cleared baseline, returning the repaint bytes
for the JS Ink (the sole stream writer) to emit.
Thin bridge over FrameWriter::restore_last_output: after a clear()
zeroed the baseline, this diff is a bootstrap that re-emits the FULL last
frame. The interactive writeToStdout composes clear() -> data -> restore_last_output() so an app console.log is sandwiched between an erase
of the live region and its repaint, BSU/ESU-wrapped by the JS caller.
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn compose_console_write(
&mut self,
data: Buffer,
sync: bool,
) -> Result<Buffer>
pub fn compose_console_write( &mut self, data: Buffer, sync: bool, ) -> Result<Buffer>
composeConsoleWrite(data, sync) -> Buffer — the FUSED interactive
writeToStdout console-interleave (P1.2 / #1): one buffer carrying
bsu? + clear() + data + restoreLastOutput() + esu?, the exact
concatenation of the five writes the JS multi-write path used to make
(ink ink.tsx:687-698). sync is the JS shouldSync() result
(stdout.isTTY && interactive) — resolved JS-side because the InkRoot
holds no stream/TTY state. The JS Ink class stays the SOLE stream
writer: this returns bytes for ONE stdout.write.
Thin bridge over FrameWriter::compose_console_write, which composes
the SAME clear/restore_last_output primitives (and their state
transitions) the old path triggered — byte-identical in both the
rendered and the nothing-rendered-yet (empty erase/repaint) states.
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn compose_console_prefix(&mut self, sync: bool) -> Result<Buffer>
pub fn compose_console_prefix(&mut self, sync: bool) -> Result<Buffer>
composeConsolePrefix(sync) -> Buffer — the stdout-side OPENING half of
the interactive writeToStderr interleave: bsu? + clear(). Paired
with compose_console_suffix so the JS
path is exactly 3 writes: prefix->stdout, data->stderr, suffix->stdout —
concatenating (per stream) to the same bytes as the old 5-write shape
(ink ink.tsx:719-727).
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn compose_console_suffix(&mut self, sync: bool) -> Result<Buffer>
pub fn compose_console_suffix(&mut self, sync: bool) -> Result<Buffer>
composeConsoleSuffix(sync) -> Buffer — the stdout-side CLOSING half of
the interactive writeToStderr interleave: restoreLastOutput() + esu?. MUST follow the matching
compose_console_prefix (whose
clear() zeroed the diff baseline) so the restore is the full-frame
bootstrap.
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn forget_last_output(&mut self) -> Result<()>
pub fn forget_last_output(&mut self) -> Result<()>
forget_last_output() — ink’s resized() lastOutput = ''; lastOutputToRender = ''; (ink.tsx:466-467): zero the writer’s
last_output/last_output_to_render so the post-clear re-render of the
reflowed (possibly byte-IDENTICAL) frame is FORCED to repaint.
Thin bridge over FrameWriter::forget_last_output. Emits no bytes. The
resize-shrink path composes write(clear()); forget_last_output(); <re-render>: clear() erases + zeroes the diff baseline, this zeroes
last_output so the steady gate (output != last_output) re-opens even on
an unchanged reflow. This REPLACES the pre-#41 setCursor(undefined) hack
(which opened the gate via cursor_dirty) now that the cursor gate keys on
POSITION change, not cursor_dirty.
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn reset_static_output(&mut self) -> Result<()>
pub fn reset_static_output(&mut self) -> Result<()>
reset_static_output() — ink’s handleStaticChange (ink.tsx:522-525),
the Rust half (#118): zero ONLY the writer’s accumulated
full_static_output when the <Static> node’s IDENTITY changes, so the
clear-branch replay (clearTerminal + fullStaticOutput + output,
ink ink.tsx:1066) never re-emits a dead <Static> instance’s items.
The JS Ink.handleStaticChange calls this alongside its own debug-side
fullStaticOutput = '' reset; everything else in the writer
(last_output*, diff baseline, cursor state) survives.
Thin bridge over FrameWriter::reset_static_output. Emits no bytes.
ADDITIVE method (wire-format covenant: no existing surface changed).
Reentrancy-guarded like clear: it mutates &mut self.frame_writer.
Sourcepub fn push_input(&mut self, bytes: Buffer) -> Result<Vec<InputEvent>>
pub fn push_input(&mut self, bytes: Buffer) -> Result<Vec<InputEvent>>
push_input(bytes) -> Vec<InputEvent> — feed the persistent input parser
(M3-F).
Wraps core’s Parser::feed (the
ported input-parser.ts + parse-keypress.ts + kitty pipeline). The
Parser is owned by InkRoot (the input_parser field) and NEVER
recreated per call: its kitty/legacy segmenter is a state machine that
buffers partial sequences across chunks, so a CSI sequence split across two
push_input calls must resume from the carried state.
Each core CoreInputEvent is mirrored into a #[napi(object)]
InputEvent. For a key event the JS-facing Key is derived EXACTLY as
ink’s useInput handleData derives it from parseKeypress’s output
(use-input.ts:179-242): the camelCase boolean fields (upArrow, …,
return, escape, tab, backspace, delete) plus the raw modifiers
and kitty fields, AND the resolved input string. This collapses ink’s
in-hook parseKeypress into the FFI, so the JS useInput (M3-I) becomes a
thin subscriber.
Reentrancy-guarded: feed mutates &mut self.input_parser, which a
dispatcher re-entering mid-render would alias. (Inputs are dispatched on
the JS main thread between renders, so a real reentry is pathological, but
the guard keeps the memory-safety invariant total.) Reads rendering FIRST
and rejects with the same catchable typed error.
Sourcepub fn has_pending_escape(&self) -> bool
pub fn has_pending_escape(&self) -> bool
has_pending_escape() -> bool — whether a bare/partial escape is buffered
in the parser awaiting the host’s escape-flush timer (M3-I2 / task #52).
Thin read over core’s
Parser::has_pending_escape
(input/mod.rs → segmenter.rs): true only while a lone \x1b (or a
short incomplete escape) is buffered, and explicitly false while
assembling a paste-start marker (ESC [ 2 0 0) or any other partial CSI —
so an Esc-PREFIXED sequence (e.g. \x1b[A → upArrow) never reads as a bare
Esc. The JS App polls this after draining a readable burst and, when set,
arms the 20ms schedulePendingInputFlush timer that surfaces the buffered
Esc via flush_pending_escape.
Reentrancy: it takes &self, but — exactly like measure — a &self read
aliased against render_frame’s &mut self is STILL UB under napi-rs v3.
Because this method must stay infallible (it has no error channel a JS
poll could branch on), a reentry returns the SAME safe false sentinel
measure mirrors with its zero-Rect: under render there is, by definition,
no host escape-flush timer to arm, so reporting “no pending escape” is the
correct conservative answer.
Sourcepub fn flush_pending_escape(&mut self) -> Result<Vec<InputEvent>>
pub fn flush_pending_escape(&mut self) -> Result<Vec<InputEvent>>
flush_pending_escape() -> Vec<InputEvent> — take any buffered escape
bytes as literal input, decoded to the SAME InputEvent shape push_input
returns (M3-I2 / task #52).
Wraps core’s
Parser::flush_pending_escape,
which mem::takes the buffered bytes. A lone \x1b here decodes — via the
SAME parse_keypress + InputEvent::from_core mapping push_input uses
— to a normal key event {kind:'Key', input:'', key:{escape:true, …}}, so
JS receives a parsed Esc keypress, NOT raw bytes. None (nothing pending)
yields an empty Vec.
CRITICAL — the flushed bytes MUST be decoded with parse_keypress
directly, NOT re-fed through self.input_parser.feed. The segmenter treats
a lone \x1b as an INCOMPLETE sequence and would re-buffer it (returning
[] and re-arming has_pending_escape), so a feed-based flush would
never surface the Esc. Decoding the taken bytes directly is what makes the
post-flush has_pending_escape() go false — the discriminating invariant.
Reentrancy-guarded like push_input: flush_pending_escape mutates
&mut self.input_parser, which a dispatcher re-entering mid-render would
alias. Reads rendering FIRST and rejects with the same catchable typed
error.