Skip to content
docsv0.3.0

RetainedTuiLoop

The retained-mode loop: renders the tree into a buffer, diffs it against the previous one, and writes only the rows that changed. Owns focus, key dispatch, session values and the input buffer for the run.

RetainedTuiLoop::__construct()

public function __construct(Milpa\Live\Tui\RetainedTuiRenderer $renderer, callable $rootFactory, array $focusOrder, string $initialFocus, int $width, int $height, bool $ansi = true, ?callable $handleKey = null, ?callable $tick = null, ?Milpa\Live\Tui\TuiAnsiPainter $painter = null, ?Milpa\Live\Tui\SynchronizedOutput $sync = null, ?Milpa\Live\Tui\BracketedPaste $paste = null):

Parameters

Parameters of __construct()
NameTypeDescription
$rendererMilpa\Live\Tui\RetainedTuiRenderer
$rootFactorycallable(self): TuiNode
$focusOrderarray<int, string>
$initialFocusstring
$widthint
$heightint
$ansibool
$handleKey(null | callable(string, self): bool)
$tick(null | callable(self): void)
$painter?Milpa\Live\Tui\TuiAnsiPainter
$sync(null | SynchronizedOutput)Synchronized-output wrapper for atomic writes; defaults to enabled.
$paste(null | BracketedPaste)Bracketed-paste detector; defaults to null (no paste detection).

RetainedTuiLoop::focusedId()

public function focusedId(): string

The id currently holding focus, falling back to the initial one.

RetainedTuiLoop::focus()

public function focus(string $id): void

Moves focus to the given id.

Parameters

Parameters of focus()
NameTypeDescription
$idstring

RetainedTuiLoop::setFocusOrder()

public function setFocusOrder(array $focusOrder): void

Replaces the focus order, keeping the currently focused id if it survives.

Parameters

Parameters of setFocusOrder()
NameTypeDescription
$focusOrderarray<int, string>

RetainedTuiLoop::focusNext()

public function focusNext(): void

Moves focus to the next id in order.

RetainedTuiLoop::focusPrevious()

public function focusPrevious(): void

Moves focus to the previous id in order.

RetainedTuiLoop::value()

public function value(string $key, ?mixed $default = null): ?mixed

A value from the loop's session state, or the default when it is not set.

Parameters

Parameters of value()
NameTypeDescription
$keystring
$default?mixed

RetainedTuiLoop::set()

public function set(string $key, ?mixed $value): void

Stores a value in the loop's session state.

Parameters

Parameters of set()
NameTypeDescription
$keystring
$value?mixed

RetainedTuiLoop::renderScreen()

public function renderScreen(): string

The full screen as a string. Bypasses the diff, so this repaints everything — it is for the first frame and for a forced redraw, not the steady state.

RetainedTuiLoop::dispatchKey()

public function dispatchKey(string $key): bool

Routes a key through the shortcut registry and the key handler, returning whether it was consumed.

Parameters

Parameters of dispatchKey()
NameTypeDescription
$keystring

RetainedTuiLoop::runOn()

public function runOn(Milpa\Live\Contracts\Tui\TerminalInterface $terminal, int $idleMicroseconds = 100000, ?int $maxTicks = null, int $escapeTimeoutMicroseconds = 50000): void

Runs the loop against a {@see TerminalInterface} instead of raw streams. Same body as {@see self::run()} — tick, paint the diff, read a key, dispatch — with every terminal effect expressed through the contract: no `stty`, no `stream_isatty` gate, no escape sequences written by hand. That is what makes the interactive path drivable by a fake terminal, and therefore testable without one.

Parameters

Parameters of runOn()
NameTypeDescription
$terminalMilpa\Live\Contracts\Tui\TerminalInterface
$idleMicrosecondsintHow long to idle when a tick produced no key. Zero keeps a scripted run instantaneous.
$maxTicks(int | null)Stop after this many ticks. Null runs until the loop is asked to stop, which is what a session wants; a bounded run is what a test wants, because a loop that never sees its quit key hangs the suite instead of failing it.
$escapeTimeoutMicrosecondsintHow long a partial sequence may sit before it is emitted as-is. A terminal delivers the rest of a CSI in microseconds; a person leaves an Escape pending for as long as they like. Flushing sooner destroys fragmented sequences; never flushing turns Escape into alt+<key>.

RetainedTuiLoop::run()

public function run(?mixed $input = null, ?mixed $output = null): void

Runs the loop against the given input and output streams, writing only the rows each frame's diff reports as changed.

Parameters

Parameters of run()
NameTypeDescription
$input(resource | null)
$output(resource | null)

RetainedTuiLoop::resizeTo()

public function resizeTo(int $width, int $height): void

Adopts a new terminal size. Dropping the previous buffer is not housekeeping, it is the point: the diff walks the CURRENT buffer's rows, so after a shrink the rows that no longer exist are never visited and their old contents stay on screen. Forgetting the previous frame turns the next one into a full repaint, which is the only correct answer to a resize.

Parameters

Parameters of resizeTo()
NameTypeDescription
$widthint
$heightint

RetainedTuiLoop::paintFrame()

public function paintFrame(?mixed $output): void

Writes one frame to $output. The first call after a (re)start does a full clear-and-repaint (there is no previous frame to diff against); every call after that computes {@see VirtualTerminalBuffer::diff()} against the last painted frame and writes only the changed rows via cursor-positioned ANSI patches -- if nothing changed, nothing is written at all. This is what makes the "flicker-resistant redraws" capability an actual runtime behavior instead of dead code. Public (not just used internally by {@see self::run()}) so the diff-repaint behavior can be driven and observed directly -- e.g. by a test harness scripting a state change over a non-TTY stream, where {@see self::run()}'s own TTY detection would otherwise short-circuit to a single one-shot render and never reach the interactive loop body at all. This is the exact per-tick call {@see self::run()} makes; nothing about it is TTY-specific.

Parameters

Parameters of paintFrame()
NameTypeDescription
$outputresource

RetainedTuiLoop::nextFrameBytes()

public function nextFrameBytes(): string

Advances one frame and returns the bytes the terminal should receive — empty when nothing changed. This is where the retained property lives: the first frame is a full paint, an identical frame costs nothing, and any later frame carries only the rows the diff reported. Shared by {@see self::paintFrame()} (stream) and {@see self::runOn()} (terminal contract) so both paint from exactly the same computation.