js/tty/frame
js/tty/frame.ts
fino:tty/frame — the styled-cell intermediate representation for terminal output.
A Frame is what a laid-out component tree becomes: rows of styled text
segments, a cursor placement, and hit regions for mouse routing. Segments
hold printable text only — no escape sequences, no newlines — so wrapping,
clipping, and composition can never corrupt ANSI state. Bytes are produced
only at the wire edge by rowToAnsi(), frameToAnsi(), and
frameToScreen().
Rows are ragged: trailing blank cells are not materialized. Whether a row gets padded, and with what style, is a decision for the code that writes it to a terminal — a filled final column is recorded as a soft wrap by real terminals, so writers routinely stop one cell short.
ANSI parsing recognizes the SGR subset of
ECMA-48
emitted by fino:tty/style; other terminal control sequences are discarded.
Cell widths follow the terminal-oriented approximation documented by the
exported width helpers rather than promising font-dependent visual layout.
import { rowToAnsi, textRow } from 'fino:tty/frame';
const row = textRow('ready', { fg: 'green' });
const bytes = rowToAnsi(row, { pad: 10 });Functions
function charWidth(cp: number): 0 | 1 | 2
Return the terminal cell width of the code point cp.
function graphemes(text: string): string[]
Split text into approximate terminal grapheme clusters.
Combining marks, emoji modifiers, regional-indicator pairs, and common ZWJ sequences stay together. This is intentionally not a complete Unicode text segmentation implementation because terminal rendering varies by emulator.
function clusterWidth(cluster: string): 0 | 1 | 2
Return the terminal cell width of the approximate grapheme cluster.
function stringWidth(text: string): number
Return the total terminal cell width of text.
function textRow(text: string, style: Style = EMPTY_STYLE): Row
Build a single-segment row from plain text.
function joinRows(...rows: Row[]): Row
Concatenate rows into one.
function clipRow(row: Row, width: number): Row
Cut a row down to at most width cells. A wide character straddling the
boundary is dropped, leaving the row one cell short rather than corrupting
the grid.
function rowToAnsi(row: Row, options: EncodeRowOptions = {}): string
Encode one row as ANSI bytes, starting from and returning to the default style. The result is a pure function of the row and options, so equal rows encode to equal strings — callers use the encoded form as a diff key.
function frameToAnsi(frame: Frame, options: { pad?: boolean } = {}): string
Encode a whole frame, \n-joined, each row padded to the frame width.
function frameToScreen(
frame: Frame,
previous: Frame | null,
origin: { row: number; column: number } = { row: 1, column: 1 },
): string
Encode an absolute-addressed repaint of frame, touching only rows whose
encoded form differs from previous. Rows the previous frame had beyond
the new height are erased. origin is 1-based screen coordinates.
function hitTest(frame: Frame, x: number, y: number): string | undefined
The id of the topmost hit region containing the cell, if any.
function hitPath(frame: Frame, x: number, y: number): string[]
All hit region ids containing the cell, outermost first.
function parseAnsi(text: string, base: Style = EMPTY_STYLE): Row[]
Parse text that may contain SGR escape sequences into styled rows. \n
splits rows, \r is dropped, and tabs expand to 8-column stops. Non-SGR
escape sequences are discarded — cursor movement embedded in cell data
would corrupt a grid, so producers of control sequences must not route
through this.
function visibleWidth(text: string): number
The cell width of text after stripping escape sequences.
function rowText(row: Row): string
Text content of a row with styling discarded.
Interfaces
interface Segment {
A run of printable text in one style. text contains no escape sequences,
newlines, or tabs; width is its cell width (wide characters count two,
combining marks zero).
Readonly Properties
readonly text: string
readonly width: number
readonly style: Style
interface Row {
One terminal row: styled segments, ragged (no trailing padding).
Readonly Properties
readonly segments: readonly Segment[]
readonly width: number
Sum of segment widths.
interface Rect {
An axis-aligned cell rectangle.
Readonly Properties
readonly x: number
readonly y: number
readonly width: number
readonly height: number
interface CursorPlacement {
Where the terminal cursor should sit after a frame is painted.
Readonly Properties
readonly row: number
Frame-relative row, 0-based.
readonly column: number
Frame-relative column, 0-based.
readonly shape?: 'block' | 'bar' | 'underline'
interface HitRect extends Rect {
The resolved rectangle of a node that carried an id, in paint order.
Readonly Properties
readonly id: string
readonly depth: number
Tree depth; deeper nodes win hit-testing ties.
interface Frame {
A complete laid-out surface. rows has exactly height entries.
Readonly Properties
readonly width: number
readonly height: number
readonly rows: readonly Row[]
readonly cursor: CursorPlacement | null
readonly hits: readonly HitRect[]
interface EncodeRowOptions {
Options for encoding one row as ANSI bytes.
Readonly Properties
readonly pad?: number
Pad with spaces to this cell width.
readonly clip?: number
Clip to at most this many cells first.
readonly padStyle?: Style
Style applied to padding cells (a background fill).
Constants
const EMPTY_ROW: Row
An empty row.