tui
js/tty/tui.ts
fino:tty/tui — terminal render target for fino:ui components.
The terminal is a retained-mode host: the fino:ui reconciler maintains a
node tree here, a flexbox-subset layout engine measures and paints it into
styled-cell frames (fino:tty/frame), and ANSI bytes exist only at the wire
edge. Live rendering diffs frames row-wise, so screen writes stay
proportional to what changed.
Primitives are host-neutral: Box (flexbox layout, padding, margin,
borders), Text (styled runs, wrapping, caret), Spacer, Input,
Button, List, Scroll, Layer (content painted above the normal
flow), and Clickable (focusable pointer/key behavior without visuals).
All accept style props (color, background, bold, …)
resolved against fino:tty/style tokens during layout.
Layout model
Layout uses integer terminal cells rather than browser pixels. It implements row and column flex flow, optional row wrapping, fixed/min/max sizes, weighted growth and shrinkage, margins, padding, gaps, alignment, clipping, and out-of-flow layers. Grapheme clusters are never split across cells, and wide characters occupy two cells. Unlike HTML/CSS there is no cascade, intrinsic font metric, percentage sizing, grid, or automatic scrolling; overflow is clipped unless a box explicitly requests visible overflow, and scroll offsets are controlled by the application.
Live apps route mouse input to the topmost painted node and bubble toward
its ancestors. Keys start at the focused node; Tab and Shift-Tab traverse
enabled Clickable nodes, while active overlays may opt into unfocused key
capture. Input reads preserve escape sequences and UTF-8 code points split
across operating-system reads. Unless dimensions are pinned, render()
reflows the retained tree after terminal resize notifications.
/** @jsxImportSource fino:ui *\/
import { Box, Text, renderFrame } from 'fino:tty/tui';
const frame = renderFrame(
<Box border padding={1}><Text color="cyan">Hello</Text></Box>,
{ width: 20, height: 3 },
);Interfaces
interface ButtonProps extends StyleProps, FlexChildProps, Props {
Props accepted by Button.
Properties
label?: string
Text rendered inside the button brackets.
focused?: boolean
Whether to draw the keyboard-focus marker.
interface ListProps extends StyleProps, FlexChildProps, Props {
Props accepted by List.
Properties
items: string[]
Rows rendered from top to bottom.
selectedIndex?: number
Zero-based row receiving the selection marker; defaults to zero.
interface TuiFocus {
Focus control surface exposed by a live TUI app.
Readonly Properties
readonly focusedId: { get(): string | null }
Signal carrying the focused node's id, for components to render focus.
Methods
next(): boolean
Move focus to the next enabled focusable node.
prev(): boolean
Move focus to the previous enabled focusable node.
focus(id: string): boolean
Focus the enabled node with id, returning whether it was found.
blur(): void
Clear the current focus.
interface RenderFrameOptions {
Options for deterministic terminal snapshot rendering.
Properties
width: number
Output width in terminal cells; negative and fractional values are normalized.
height: number
Output height in terminal rows; negative and fractional values are normalized.
interface TerminalSize {
Current terminal viewport size in character cells.
Properties
width: number
Viewport columns.
height: number
Viewport rows.
interface RenderOptions {
Options for live fullscreen terminal rendering.
Properties
width?: number
Fixed viewport width; defaults to the current terminal width.
height?: number
Fixed viewport height; defaults to the current terminal height.
input?: boolean
Whether to create a raw input reader. An event handler also enables input.
mouse?: boolean
Whether the input reader enables SGR mouse reporting; defaults to true.
onEvent?: (event: TuiEvent, app: TuiApp) => void | Promise<void>
Called sequentially for each decoded input event until the app stops.
interface TuiApp {
Handle returned by render() for updating or stopping a fullscreen app.
Methods
update(element: VNode): void
Replace the current tree and stop any reactive root previously supplied to render().
stop(): void
Stop rendering and restore terminal screen, cursor, mouse, and raw-mode state.
frame(): Frame | null
The most recently painted frame.
Properties
input?: TuiInput
Raw input reader when input was enabled.
focus: TuiFocus
Focus traversal and state for the retained tree.
interface TuiInputOptions {
Options for creating a raw terminal input reader.
Properties
mouse?: boolean
Whether to enable SGR mouse reporting while the reader is open; defaults to true.
Types
type TuiKeyEvent = UiKeyEvent
Keyboard event decoded from terminal input.
type TuiMouseEvent = UiMouseEvent
Mouse event decoded from SGR terminal mouse reporting.
type TuiEvent = TuiKeyEvent | TuiMouseEvent
Terminal input event consumed by TUI applications.
Functions
function Button(props: ButtonProps): VNode
Push button primitive rendered as bracketed terminal text.
function List(props: ListProps): VNode
Vertical list primitive with a selected row marker.
function measure(element: VNode, constraints: Constraints): Measured
Measure a terminal component tree under the supplied cell constraints.
function layout(element: VNode, constraints: Constraints): Frame
Lay out and paint a terminal component tree into a structured frame.
function decodeTuiInput(bytes: Uint8Array): TuiEvent[]
Decode one complete terminal input byte chunk into TUI input events.
The decoder understands printable UTF-8, common control keys, arrow/function
CSI sequences, and SGR mouse reporting (CSI < code ; x ; y M/m). SGR mouse
coordinates are converted to zero-based x/y values. A trailing partial
sequence is treated literally; TuiInput reassembles partial live reads.
function createTuiInput(options: TuiInputOptions = {}): TuiInput
Create a raw terminal input reader for TUI applications.
The reader enables raw mode immediately. Call close() when the application
exits so terminal state is restored.
function getTerminalSize(): TerminalSize
Return the current terminal viewport size.
The TUI host asks the terminal with ioctl(TIOCGWINSZ) when possible and
falls back to environment dimensions in non-interactive contexts.
async function measureTerminalSize(): Promise<TerminalSize>
Measure the visible terminal viewport with an ANSI cursor-position query.
This is slower than getTerminalSize() but handles terminal panes where
ioctl(TIOCGWINSZ) or environment dimensions are stale. The function enters
raw mode briefly, moves the cursor to a very large coordinate, asks the
terminal to report the clamped cursor position, restores the cursor, and
returns the reported row/column. If the terminal does not answer quickly, it
falls back to getTerminalSize().
function layoutFrame(element: VNode, options: RenderFrameOptions): Frame
Lay a tree out into a styled-cell frame without touching a terminal.
This is the one-shot path: the tree is laid out fresh with no retained
state. Pass the result to frameToAnsi()/frameToScreen() from
fino:tty/frame, or use renderFrame() for the padded-string form.
function renderFrame(element: VNode, options: RenderFrameOptions): string
Render an element tree to a deterministic terminal frame.
The returned string contains exactly height lines joined with \n, and
each line is padded or clipped to width cells.
function frameSink(options: RenderFrameOptions): Sink<string>
Sink that turns each committed tree into a terminal frame string.
The frame is a complete screen image rather than a diff, so this is a
snapshot sink like htmlSink(). Pair it with renderStatic() to capture
one frame, or with createRoot() to drive a live screen from signals.
import { createRoot } from 'fino:ui';
import { frameSink } from 'fino:tty/tui';
const root = createRoot(App, frameSink({ width: 80, height: 24 }));function terminalSink(options: RenderFrameOptions): Sink<Frame>
Sink that reconciles each committed tree into a retained terminal node tree
and returns the laid-out Frame.
Unlike frameSink(), consecutive commits reuse the mounted tree, so
unchanged subtrees keep their measurement caches. Use this when the caller
needs the structured frame — cursor placement, hit regions — rather than
encoded text.
function render(element: VNode | (() => VNode), options: RenderOptions = {}): TuiApp
Render a fullscreen terminal app and return a lifecycle handle.
This enters the alternate screen, hides the cursor, and paints frames
through the retained host with row-level diffing — only rows whose encoded
form changed are rewritten. When a frame places a caret (Text caret or a
focused Input), the terminal cursor is shown there.
Passing a function instead of a tree makes the app reactive: every signal
read while rendering becomes a dependency, and the screen repaints when one
changes. update() remains available for callers that drive frames
themselves, and takes over from the reactive root when used.
Tree handlers receive input before onEvent; only unconsumed events reach
the fallback callback. stop() is idempotent and cancels a blocked input
read before restoring raw mode, mouse reporting, cursor state, auto-wrap,
and the primary screen.
import { createSignal } from 'fino:ui';
import { Text, render } from 'fino:tty/tui';
const ticks = createSignal(0);
const app = render(() => Text({ children: [String(ticks.get())] }));
ticks.set(1);
app.stop();Classes
class TuiInput {
Raw terminal input reader for keyboard and mouse events.
One reader owns stdin raw mode and its readability watch at a time. Calls to
read() are sequential: each returns one decoded event, preserving partial
escape sequences and UTF-8 between operating-system reads. close() is
idempotent, cancels a blocked read, disables mouse reporting when enabled,
and restores the terminal mode captured by the constructor.
Constructors
constructor(options: TuiInputOptions = {})
Enter raw mode and optionally enable mouse reporting immediately.
Methods
async read(): Promise<TuiEvent | null>
Read the next decoded keyboard or mouse event from stdin.
close(): void
Restore raw mode and mouse reporting.