Skip to main content

InkRoot

Struct InkRoot 

Source
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.rssegmenter.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.

Source

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.

Trait Implementations§

Source§

impl FromNapiMutRef for InkRoot

Source§

unsafe fn from_napi_mut_ref( env: napi_env, napi_val: napi_value, ) -> Result<&'static mut Self>

This function called to convert napi values to native rust values Read more
Source§

impl FromNapiRef for InkRoot

Source§

unsafe fn from_napi_ref( env: napi_env, napi_val: napi_value, ) -> Result<&'static Self>

This function called to convert napi values to native rust values Read more
Source§

impl JavaScriptClassExt for InkRoot

Source§

fn into_instance<'scope>( self, env: &'scope Env, ) -> Result<ClassInstance<'scope, Self>>

Source§

fn into_reference(self, env: Env) -> Result<Reference<Self>>

Source§

fn instance_of<'env, V: JsValue<'env>>(env: &Env, value: &V) -> Result<bool>

Source§

impl ObjectFinalize for InkRoot

Source§

fn finalize(self, env: Env) -> Result<(), Error>

Source§

impl ToNapiValue for InkRoot

Source§

unsafe fn to_napi_value(env: napi_env, val: InkRoot) -> Result<napi_value>

This function called to convert rust values to napi values Read more
Source§

fn into_unknown(self, env: &Env) -> Result<Unknown<'_>, Error>

Source§

impl TypeName for InkRoot

Source§

impl TypeName for &InkRoot

Source§

impl TypeName for &mut InkRoot

Source§

impl ValidateNapiValue for &InkRoot

Source§

unsafe fn validate(env: napi_env, napi_val: napi_value) -> Result<napi_value>

This function called to validate whether napi value passed to rust is valid type. Read more
Source§

impl ValidateNapiValue for &mut InkRoot

Source§

unsafe fn validate(env: napi_env, napi_val: napi_value) -> Result<napi_value>

This function called to validate whether napi value passed to rust is valid type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> JsValuesTupleIntoVec for T
where T: ToNapiValue,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.