controlroom
API reference

controlroom

Every export of controlroom, with its signature and doc comment: status, distribute, layout, hints, initial, reduce and 8 more, plus 17 types.

controlroom — full-screen, keyboard-driven terminal screens, each with a static projection for pipes, CI, screen readers and --json, and a drop-in path for ink.

This entry is the native API: open() and the runtime it takes, the layout arithmetic, the tab, focus, collapse and scroll reducer with the hint line generated from a keymap, and the compositor. It never loads React. ink's API is controlroom/ink, and plugins register through controlroom/plugin.

The approved intent and spec it is built to are .sdlc/intents/controlroom/intent.md and spec.md in https://github.com/ofri-peretz/burgee (D-158, amended by D-168).

import { status, distribute, layout, … } from 'controlroom';

Functions

collapse

The tree with every collapsed pane given no rows or columns.

function collapse(tree: Layout, collapsed: ReadonlySet<string>): Layout;
ParameterType
treeLayout
collapsedReadonlySet<string>

Returns Layout

compose

The frame. Live, each pane shows from its scroll entry in state (pinned to its bottom when it has none), and the focused pane, when lines are below it, says how many in its last row. Not live, scroll state never reaches it and every line of every pane is printed (R6).

function compose(tree: Layout, panes: Panes, size: Frame, state?: Pick<ScreenState, 'panes' | 'focused' | 'scroll'>): string[];
ParameterType
treeLayout
panesPanes
sizeFrame
state (optional)Pick<ScreenState, 'panes' | 'focused' | 'scroll'>

Returns string[]

distribute

How many cells each part of one axis gets, given total. Exported for the arithmetic's own tests.

function distribute(total: number, sizes: readonly (number | {
    fr: number;
    min?: number;
})[]): number[];
ParameterType
totalnumber
sizesreadonly (number | { fr: number; min?: number; })[]

Returns number[]

extents

controlroom-scroll-pane R2 — each laid-out pane's content and rectangle height, for reduce.

function extents(tree: Layout, panes: Panes, size: Frame): Map<string, Extent>;
ParameterType
treeLayout
panesPanes
sizeFrame

Returns Map<string, Extent>

fit

lines cut to the rectangle: no more rows than it has, every row exactly its width. at is the first row shown (controlroom-scroll-pane R3), and 'end' shows the last rows. An offset past the last page is read as the last page, so the view never sits past the content.

function fit(lines: readonly string[], rect: Pick<Rect, 'width' | 'height'>, at?: number | 'end'): string[];
ParameterType
linesreadonly string[]
rectPick<Rect, 'width' | 'height'>
at (optional)number | 'end'

Returns string[]

hints

The hint line for keymap: one entry per label, its keys run together in keymap order — ←→ switch tab s toggle status. An action with no label is bound but not advertised. Arrows read best run together (←→); any other set of keys is joined by a slash (q/^C).

function hints(keymap: Keymap, labels: Readonly<Record<string, string>>): string;
ParameterType
keymapKeymap
labelsReadonly<Record<string, string>>

Returns string

initial

function initial(tabs?: readonly string[], panes?: readonly string[]): ScreenState;
ParameterType
tabs (optional)readonly string[]
panes (optional)readonly string[]

Returns ScreenState

layout

The rectangle of every pane in tree, inside area. Panes are keyed by name; a name used twice is a mistake in the tree and throws rather than silently drawing one pane twice.

function layout(tree: Layout, area: Rect, contents?: Contents): Map<string, Rect>;
ParameterType
treeLayout
areaRect
contents (optional)Contents

Returns Map<string, Rect>

open

function open(rt: Runtime, given: ScreenOptions): Screen;
ParameterType
rtRuntime
givenScreenOptions

Returns Screen

reduce

The next state. extents is every laid-out pane's height, which compose measures (extents()): scrolling needs it, and without it a scroll action changes nothing.

function reduce(state: ScreenState, action: string, extents?: ReadonlyMap<string, Extent>): ScreenState;
ParameterType
stateScreenState
actionstring
extents (optional)ReadonlyMap<string, Extent>

Returns ScreenState

render

A pane's text at time t: its animated frame when it has one and live is set, its static projection otherwise.

function render(pane: Pane, t: number, live: boolean): string[];
ParameterType
panePane
tnumber
liveboolean

Returns string[]

Classes

ScreenError

A screen asked for something nobody registered: what is missing, and what to do about it.

class ScreenError extends TypeError {
    readonly fix: string;
    constructor(message: string, fix: string);
}

Constants

processRuntime

The real process, narrowed. isTTY is a getter so the mode is decided when a screen opens, not when this module loads; the clock is wall time through the timers Node keeps.

const processRuntime: () => Runtime;

status

The reserved release's one export, kept so a program that read it still loads.

const status: "reserved";

Interfaces

Extent

A laid-out pane's height: how many lines it holds, and how many rows its rectangle has.

interface Extent {
    readonly content: number;
    readonly rows: number;
}

Frame

The whole frame, rows lines of columns cells. A row a rectangle does not cover is spaces, so the frame always replaces whatever was under it.

interface Frame {
    columns: number;
    rows: number;
    /** Milliseconds since the screen opened, for animated panes. */
    t: number;
    /** Whether animated panes draw their `frame`; a final, settled frame uses each static projection. */
    live: boolean;
}

Pane

A pane: a flagstaff component, the state it is drawn with, and the label a static projection prints.

interface Pane<S = unknown> {
    component: Component<S>;
    state: S;
    label?: string;
    /**
     * Drawn on a live screen only, and left out of the static projection: a hint line or a tab
     * bar means nothing to a pipe, a screen reader or an agent, which get every pane instead (R6).
     */
    liveOnly?: boolean;
}

Part

interface Part {
    /** Defaults to `{ fr: 1 }`. */
    size?: Size;
    content: Layout;
}

Rect

interface Rect {
    x: number;
    y: number;
    width: number;
    height: number;
}

Runtime

interface Runtime extends LoopRuntime {
    readonly stdout: Writer & OutputStream & {
        /** Rows of the terminal, read at every paint so a resize is honoured. */
        readonly rows?: number | undefined;
        on?(event: 'resize', listener: () => void): unknown;
        off?(event: 'resize', listener: () => void): unknown;
    };
    readonly stdin: KeyInput;
}

Screen

interface Screen {
    /** What `open()` decided, from roundel's policy. */
    readonly mode: OutputMode;
    /** Whether keys are being read: a `tty` whose stdin can go raw, and nothing else. */
    readonly interactive: boolean;
    readonly state: ScreenState;
    update(pane: string, state: unknown): void;
    /** Text above the live region, written once and never repainted (Ink's `<Static>`). */
    commit(text: string): void;
    /** Run an action as if its key had been pressed: what a static session has instead of keys. */
    dispatch(action: string): void;
    close(): void;
}

ScreenOptions

interface ScreenOptions {
    /** `'inline'` (the default) keeps the main screen; `'alternate'` takes the whole terminal. */
    screen?: 'inline' | 'alternate';
    /** The panes' arrangement, or a function of the screen state when tabs change what is shown. */
    layout: Layout | ((state: ScreenState) => Layout);
    /** Each pane is a component with its state, or a registered pane (R10) by name with its state. */
    panes: Readonly<Record<string, Pane | {
        pane: string;
        state: unknown;
    }>>;
    /**
     * Key spec → action, as data (caique R2), or the name of a registered keymap (R10). Defaults
     * to the registered `default`. `ctrl+c` is `quit` unless the keymap binds it.
     */
    keymap?: Keymap | string;
    tabs?: readonly string[];
    /** Panes that take focus, in order. */
    focus?: readonly string[];
    /** Whether this run was asked for `--json`. */
    json?: boolean;
    /** Every action, after the screen's own state has taken it: the program's half of the keymap. */
    onAction?(action: string, screen: Screen): void;
    /** R20 — an input line, drawn in one pane of the layout. */
    input?: InputOptions;
}

ScreenState

interface ScreenState {
    readonly tabs: readonly string[];
    readonly active: number;
    /** Panes that take focus, in focus order. */
    readonly panes: readonly string[];
    readonly focused: number;
    readonly collapsed: ReadonlySet<string>;
    /**
     * controlroom-scroll-pane R1 — each pane's first visible row. A pane with no entry, or with
     * `'end'`, is pinned to its bottom and follows new lines, as a log tail does.
     */
    readonly scroll: ReadonlyMap<string, number | 'end'>;
}

Split

interface Split {
    /** `row` places parts left to right; `column` places them top to bottom. */
    direction: 'row' | 'column';
    parts: Part[];
}

Types

Action

The actions the reducer knows. Anything else is the program's own, and leaves the state alone.

type Action = 'tab.next' | 'tab.prev' | 'focus.next' | 'focus.prev' | `tab:${string}` | `toggle:${string}` | 'scroll.up' | 'scroll.down' | 'scroll.pageUp' | 'scroll.pageDown' | 'scroll.top' | 'scroll.bottom' | 'resize';

Contents

What a pane holds, for 'fit'. Lines are measured with linegauge, so ANSI and wide characters count right.

type Contents = Readonly<Record<string, readonly string[]>>;

Keymap

Key spec → action name, e.g. { left: 'tab.prev', s: 'toggle:status' }.

type Keymap = Readonly<Record<string, string>>;

Layout

A pane by name, or a split of parts.

type Layout = string | Split;

Panes

type Panes = Readonly<Record<string, Pane>>;

Size

How much of its parent's main axis a part takes.

  • a number: exactly that many cells;
  • { fr, min }: a share of what the fixed and fitted parts leave, never below min;
  • 'fit': as many cells as the pane's content needs along that axis — its widest line in a row, its line count in a column. Only a pane can fit; a split has no content of its own.
type Size = number | {
    fr: number;
    min?: number;
} | 'fit';

Status

The only value {@link status} has.

type Status = typeof status;

On this page