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
impl FrameWriter
Sourcepub fn write_frame(&mut self, params: &FrameParams<'_>) -> Vec<u8> ⓘ
pub fn write_frame(&mut self, params: &FrameParams<'_>) -> Vec<u8> ⓘ
Emit the bytes for one interactive frame and advance internal state.
Mirrors renderInteractiveFrame exactly:
- debug: write
full_static_output + outputraw — no diff, clear, or BSU/ESU ever (ink.tsx550-560). - Compute
is_fullscreen = is_tty && output_height >= viewport_rowsandoutput_to_render = is_fullscreen ? output : output + "\n". - Accumulate
static_outputintofull_static_output. - clear branch (
should_clear_terminal_for_frame): writeclearTerminal + full_static_output + output(rawoutput, notoutput_to_render—ink.tsx:1066), thenLineDiff::syncthe baseline tooutput_to_render. Always writes, so wrap in BSU/ESU when synchronizing. - static branch (
static_output != ""):LineDiff::clearerase +static_output+LineDiff::diff(output_to_render)(a fresh bootstrap). Always writes, so wrap when synchronizing. - steady branch (
output != last_output || cursor_dirty):LineDiff::diff(output_to_render), wrapped in BSU/ESU only when the diff is non-empty (thewillRenderrule: a no-op diff — including one whose gate was opened solely bycursor_dirty— emits nothing, so no empty BSU/ESU pair). - else emit nothing.
Every branch records last_output = output and
last_output_height = output_height.
Sourcepub fn last_changed_lines(&self) -> u32
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.
Sourcepub fn reset_diff_state(&mut self)
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 zeroingpreviousOutput/previousLines. It is the visible erase.render.reset()(log-update.ts:337-342) emits nothing — it only zeroespreviousOutput,previousLines,previousCursorPosition, andcursorWasShown.
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.
Sourcepub fn clear(&mut self) -> Vec<u8> ⓘ
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.
Sourcepub fn sync_baseline(&mut self)
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.
Sourcepub fn restore_last_output(&mut self) -> Vec<u8> ⓘ
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.
Sourcepub fn forget_last_output(&mut self)
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.
Sourcepub fn reset_static_output(&mut self)
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.
Sourcepub fn compose_console_write(&mut self, data: &[u8], sync: bool) -> Vec<u8> ⓘ
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.
Sourcepub fn compose_console_prefix(&mut self, sync: bool) -> Vec<u8> ⓘ
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.
Sourcepub fn compose_console_suffix(&mut self, sync: bool) -> Vec<u8> ⓘ
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
impl Clone for FrameWriter
Source§fn clone(&self) -> FrameWriter
fn clone(&self) -> FrameWriter
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for FrameWriter
impl Debug for FrameWriter
Source§impl Default for FrameWriter
impl Default for FrameWriter
Source§fn default() -> FrameWriter
fn default() -> FrameWriter
Source§impl PartialEq for FrameWriter
impl PartialEq for FrameWriter
Source§fn eq(&self, other: &FrameWriter) -> bool
fn eq(&self, other: &FrameWriter) -> bool
self and other values to be equal, and is used by ==.