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.