Skip to main content

FrameWriter

Struct FrameWriter 

Source
pub struct FrameWriter { /* private fields */ }
Expand description

Stateful frame emitter owning a LineDiff plus the small amount of cross-frame state ink keeps on its Ink instance.

Port of the renderInteractiveFrame decision tree. Returns the bytes ink would have written; performs no IO.

Implementations§

Source§

impl FrameWriter

Source

pub fn new() -> Self

Create an empty writer (nothing rendered yet).

Source

pub fn write_frame(&mut self, params: &FrameParams<'_>) -> Vec<u8>

Emit the bytes for one interactive frame and advance internal state.

Mirrors renderInteractiveFrame exactly:

  1. debug: write full_static_output + output raw — no diff, clear, or BSU/ESU ever (ink.tsx 550-560).
  2. Compute is_fullscreen = is_tty && output_height >= viewport_rows and output_to_render = is_fullscreen ? output : output + "\n".
  3. Accumulate static_output into full_static_output.
  4. clear branch (should_clear_terminal_for_frame): write clearTerminal + full_static_output + output (raw output, not output_to_renderink.tsx:1066), then LineDiff::sync the baseline to output_to_render. Always writes, so wrap in BSU/ESU when synchronizing.
  5. static branch (static_output != ""): LineDiff::clear erase + static_output + LineDiff::diff(output_to_render) (a fresh bootstrap). Always writes, so wrap when synchronizing.
  6. steady branch (output != last_output || cursor_dirty): LineDiff::diff(output_to_render), wrapped in BSU/ESU only when the diff is non-empty (the willRender rule: a no-op diff — including one whose gate was opened solely by cursor_dirty — emits nothing, so no empty BSU/ESU pair).
  7. else emit nothing.

Every branch records last_output = output and last_output_height = output_height.

Source

pub fn last_changed_lines(&self) -> u32

How many visible lines the LAST write_frame rewrote. 0 for a no-op (empty-write) frame; the visible line count for a clear/bootstrap/debug full repaint; the differ’s changed-line count for an incremental diff. Pure additive telemetry — reading it never affects the emitted transport bytes. Intended for downstream pacing (P5.3) to distinguish a real-change frame from a no-op timer fire.

Source

pub fn reset_diff_state(&mut self)

Reset the writer to its as-constructed diff state so the next write_frame emits a full repaint.

§Mirrors render.reset(), NOT render.clear()

ink has two state-clearing entry points (log-update.ts):

  • render.clear() (log-update.ts:312-323) writes bytes — it emits a return-to-bottom prefix + eraseLines(previousLines.length) to the stream before zeroing previousOutput/previousLines. It is the visible erase.
  • render.reset() (log-update.ts:337-342) emits nothing — it only zeroes previousOutput, previousLines, previousCursorPosition, and cursorWasShown.

This accessor mirrors render.reset(). The M3-K3 width-shrink invariant is the reason: M3-K3 calls reset_diff_state FIRST and then re-renders. The full repaint comes from the subsequent write_frame bootstrap branch, so this call must emit no bytes — a clear()-style erase would double-erase (the bootstrap diff already re-homes the cursor and repaints). Hence we delegate to LineDiff::reset (the no-byte reset), never LineDiff::clear (the erase-emitting one).

Crucially, LineDiff::reset alone is insufficient: it only zeroes the LineDiff baseline (previous_output/previous_lines), leaving this writer’s own last_output, last_output_height, and full_static_output stale. A stale last_output_height corrupts the clear decision on the next frame (the width-shrink path), and a stale full_static_output would replay dead static content into the clear branch. So we reset the FULL writer state to as-constructed: *self = Self::default() is provably equal to a freshly constructed FrameWriter and stays correct if a field is added later.

Source

pub fn clear(&mut self) -> Vec<u8>

log.clear() (ink log-update.ts:312-323, the incremental variant).

Returns the erase bytes that wipe the previously rendered frame (eraseLines(previousLines.len), the pure-path collapse of returnPrefix + eraseLines(...)) and ZEROES the LineDiff baseline, so the NEXT diff (via restore_last_output or a fresh [write_frame]) repaints the full frame.

PRESERVES last_output, last_output_to_render, last_output_height, and full_static_output: ink’s log.clear() touches ONLY log-update’s own previousOutput/previousLines (the LineDiff baseline here), NOT Ink.lastOutput*. The Ink class zeroes lastOutput/lastOutputToRender SEPARATELY in resized() (ink.tsx:465-466) when it needs to; the interactive writeToStdout erase+restore and instance.clear() both rely on last_output_to_render SURVIVING this call so they can repaint / re-pin it afterwards. This is the one shared erase-emitting gesture; it is NOT a full state reset (contrast reset_diff_state).

Cursor half (ink log-update.ts:312-323): clear() prepends the buildReturnToBottomPrefix (hide + return-to-bottom) when the last frame had a shown cursor, BEFORE the eraseLines, then zeroes previousCursorPosition/cursorWasShown. On the pure path (no cursor ever shown — cursor_was_shown == false) the prefix is "", so the returned bytes are byte-identical to the bare eraseLines the M2 path emitted, and the cursor-state zeroing is a no-op (already None/false). The LineDiff baseline is still the sole thing the differ-erase touches.

Source

pub fn sync_baseline(&mut self)

log.sync(lastOutputToRender || lastOutput + '\n') (ink ink.tsx:940, log-update.ts:344-364 for the pure path).

Re-pins the LineDiff baseline to the CURRENT on-screen frame (last_output_to_render) WITHOUT emitting any bytes. After an instance.clear() erase, this tells the differ “the screen now shows the last frame again” so a subsequent UNCHANGED re-render diffs to a no-op (ink’s comment: “so that unmount’s final onRender sees it as unchanged and log-update skips it”, ink.tsx:938-939).

inkferro divergence from ink’s literal lastOutputToRender || lastOutput + '\n': last_output_to_render is ALREADY the padded string every non-debug write_frame recorded (it equals output fullscreen / output + "\n" otherwise), so the || fallback is unreachable here — the empty last_output_to_render (no frame written yet) is itself the correct baseline (an empty screen), and syncing to "" is a no-op against the already-empty baseline.

Cursor half of ink’s sync (log-update.ts:349-363): with no active cursor passed in (this re-pin is only used on the instance.clear() / unmount path, which the preceding clear already left with cursor_was_shown == false), !activeCursor && cursorWasShown is false (no hide) and there is no suffix, and the trailing previousCursorPosition = undefined; cursorWasShown = false re-pin is a no-op against the already-zeroed state. So this emits NOTHING, matching the M2 LineDiff::sync contract.

Source

pub fn restore_last_output(&mut self) -> Vec<u8>

restoreLastOutput() (ink ink.tsx:499-508): repaint the last frame from the cleared baseline.

Returns LineDiff::diff(last_output_to_render). After a clear has zeroed the baseline, this diff is a fresh bootstrap (the bootstrap branch: previousOutput.is_empty()), so it re-emits the FULL last frame — exactly ink’s this.log(this.lastOutputToRender || this.lastOutput + '\n') after an interactive console.log erased the live region. The cursor replay ink does first (log.setCursorPosition(this.cursorPosition), ink.tsx:506) collapses to nothing in the pure path here: this primitive takes no active-cursor input, so it re-emits only the frame body (a useCursor consumer re-asserts its position on the NEXT real render via write_frame’s cursor path). So this is the byte-faithful pure-path restore.

last_output_to_render is the padded string, mirroring ink passing lastOutputToRender (the wrapped form) into this.log(...). With an empty last_output_to_render (nothing rendered yet) the diff against the empty baseline is itself empty — nothing to restore.

Source

pub fn forget_last_output(&mut self)

Zero ONLY last_output + last_output_to_render, mirroring ink’s resized() (ink.tsx:466-467): after this.log.clear() it sets this.lastOutput = ''; this.lastOutputToRender = ''; so the post-clear re-render of the reflowed (possibly byte-IDENTICAL) frame is forced to repaint — output != lastOutput opens the steady gate.

This is the oracle-faithful replacement for the pre-#41 setCursor(None) hack the inkferro resize path used to open the gate: with the cursor gate now keyed on POSITION change (not cursor_dirty), a set_cursor(None) no longer forces a repaint, so the resize-shrink path zeroes last_output directly, exactly as ink does.

Deliberately NARROWER than reset_diff_state (*self = Self::default()): ink’s resized() touches ONLY lastOutput/ lastOutputToRender. It must NOT zero full_static_output (would drop <Static> content) or last_output_height (would skew the next frame’s clear decision). The LineDiff baseline is left UNTOUCHED here — the resize path calls clear first, which already zeroed it.

§Precondition

When a cursor may be shown, call clear FIRST — it zeroes previous_cursor_position/cursor_was_shown. Calling this standalone with a live cursor would strand a stale previous_cursor_position against an emptied last_output_to_render, emitting a spurious return-prefix on the next frame. ink’s resized() satisfies this by always calling clear() first.

Emits no bytes.

Source

pub fn reset_static_output(&mut self)

Zero ONLY full_static_output, mirroring ink’s handleStaticChange (ink.tsx:522-525): when the <Static> node’s IDENTITY changes (key remount / replacement, detected in the reconciler’s resetAfterCommit, ink reconciler.ts:167-175), ink sets this.fullStaticOutput = '' so the clear-branch replay (clearTerminal + fullStaticOutput + output, ink.tsx:1066) never re-emits a DEAD <Static> instance’s accumulated items. Without this, the writer’s accumulator — fed by every non-debug write_frame — keeps the dead node’s chunks and replays them on the next overflow/leaving-fullscreen clear frame.

Deliberately NARROWER than reset_diff_state: the identity change happens mid-stream with a live on-screen frame, so last_output*, last_output_height, the LineDiff baseline, and the cursor state must all SURVIVE (ink’s handleStaticChange touches only fullStaticOutput). New <Static> content rendered AFTER the identity change re-accumulates normally.

Emits no bytes.

Source

pub fn compose_console_write(&mut self, data: &[u8], sync: bool) -> Vec<u8>

Fused interactive writeToStdout console-interleave (P1.2 / #1): one buffer carrying bsu? + clear() + data + restoreLastOutput() + esu?.

Byte-identical to the old 5-write path that composed the same clear() / restore_last_output() primitives sequentially. The clear() zeros the diff baseline; the restore_last_output() then bootstraps the full last frame from that cleared baseline — exactly what the JS Ink class used to orchestrate as separate .write() calls.

data is the app text (console.log payload) encoded as UTF-8 bytes. sync gates the BSU/ESU DECSET-2026 synchronized-update wrap (shouldSync() resolved JS-side).

In the nothing-rendered-yet state: clear() and restore_last_output() both return empty (no frame to erase/repaint), so the result is bsu? + data + esu? — byte-identical to what the old path produced.

Source

pub fn compose_console_prefix(&mut self, sync: bool) -> Vec<u8>

The stdout-side OPENING half of the fused interactive writeToStderr interleave: bsu? + clear(). Paired with compose_console_suffix so the JS path is exactly 3 writes: prefix→stdout, data→stderr, suffix→stdout.

Source

pub fn compose_console_suffix(&mut self, sync: bool) -> Vec<u8>

The stdout-side CLOSING half of the fused interactive writeToStderr interleave: restoreLastOutput() + esu?. MUST follow a matching compose_console_prefix (whose clear() zeroed the diff baseline) so the restore is the full-frame bootstrap.

Trait Implementations§

Source§

impl Clone for FrameWriter

Source§

fn clone(&self) -> FrameWriter

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for FrameWriter

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for FrameWriter

Source§

fn default() -> FrameWriter

Returns the “default value” for a type. Read more
Source§

impl PartialEq for FrameWriter

Source§

fn eq(&self, other: &FrameWriter) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl StructuralPartialEq for FrameWriter

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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. 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> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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.