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;| Parameter | Type |
|---|---|
tree | Layout |
collapsed | ReadonlySet<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[];| Parameter | Type |
|---|---|
tree | Layout |
panes | Panes |
size | Frame |
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[];| Parameter | Type |
|---|---|
total | number |
sizes | readonly (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>;| Parameter | Type |
|---|---|
tree | Layout |
panes | Panes |
size | Frame |
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[];| Parameter | Type |
|---|---|
lines | readonly string[] |
rect | Pick<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;| Parameter | Type |
|---|---|
keymap | Keymap |
labels | Readonly<Record<string, string>> |
Returns string
initial
function initial(tabs?: readonly string[], panes?: readonly string[]): ScreenState;| Parameter | Type |
|---|---|
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>;| Parameter | Type |
|---|---|
tree | Layout |
area | Rect |
contents (optional) | Contents |
Returns Map<string, Rect>
open
function open(rt: Runtime, given: ScreenOptions): Screen;| Parameter | Type |
|---|---|
rt | Runtime |
given | ScreenOptions |
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;| Parameter | Type |
|---|---|
state | ScreenState |
action | string |
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[];| Parameter | Type |
|---|---|
pane | Pane |
t | number |
live | boolean |
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 belowmin;'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;A chat CLI
A chat CLI in the shape of Claude Code on the native API: an inline screen whose transcript flows into scrollback, a streamed markdown reply, slash commands, a permission prompt, Esc to interrupt — and one prompt per stdin line in a pipe.
controlroom/ink
Every export of controlroom/ink, with its signature and doc comment: Box, Newline, Spacer, Static, Text, Transform and 18 more, plus 25 types.