Skip to content

Ops

The ops module provides builders for describing terminal UI as a flat array of directives. Each frame is a complete snapshot. Build ops, pass them to term.render(), and write the output bytes.

import {
function close(): CloseElement
close
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function rgba(r: number, g: number, b: number, a?: number): number
rgba
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
,
const grow: (min?: number, max?: number) => SizingAxis
grow
} from "@bomb.sh/tty";
let
let ops: (CloseElement | OpenElement | Text)[]
ops
= [
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("root", {
layout?: {
width?: SizingAxis;
height?: SizingAxis;
padding?: {
left?: number;
right?: number;
top?: number;
bottom?: number;
};
gap?: number;
direction?: "ltr" | "ttb";
alignX?: "left" | "center" | "right";
alignY?: "top" | "center" | "bottom";
}
layout
: {
width?: SizingAxis
width
:
function grow(min?: number, max?: number): SizingAxis
grow
(),
height?: SizingAxis
height
:
function grow(min?: number, max?: number): SizingAxis
grow
(),
direction?: "ttb" | "ltr"
direction
: "ttb" },
}),
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("panel", {
border?: {
color: number;
bg?: number;
left?: BorderSide;
right?: BorderSide;
top?: BorderSide;
bottom?: BorderSide;
}
border
: {
color: number
color
:
function rgba(r: number, g: number, b: number, a?: number): number
rgba
(0, 255, 0),
left?: BorderSide
left
: 1,
right?: BorderSide
right
: 1,
top?: BorderSide
top
: 1,
bottom?: BorderSide
bottom
: 1,
},
cornerRadius?: {
tl?: number;
tr?: number;
bl?: number;
br?: number;
}
cornerRadius
: {
tl?: number
tl
: 1,
tr?: number
tr
: 1,
bl?: number
bl
: 1,
br?: number
br
: 1 },
}),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Hello, World!"),
function close(): CloseElement
close
(),
function close(): CloseElement
close
(),
];

Opens a layout element. Every element needs a unique string id for pointer hit-testing and layout queries.

PropertyTypeDescription
widthSizingAxisHorizontal sizing (fit, grow, percent, fixed)
heightSizingAxisVertical sizing
padding{ left?, right?, top?, bottom? }Inner padding in cells
gapnumberGap between children
direction"ltr" | "ttb"Child layout direction
alignX"left" | "center" | "right"Horizontal alignment
alignY"top" | "center" | "bottom"Vertical alignment
PropertyTypeDescription
bgnumberBackground color (packed RGBA via rgba())
cornerRadius{ tl?, tr?, bl?, br? }Rounded corners
border{ color, bg?, left?, right?, top?, bottom? }Border sides (number or { width, color?, bg? }). Each side reserves its width inside the element, in addition to padding
clip{ horizontal?, vertical? }Clip overflowing content
PropertyTypeDescription
floating.x, floating.ynumberPosition offset
floating.expand{ width?, height? }Expand floating bounds
floating.attachTo"none" | "parent" | "element" | "root"Attachment target
floating.attachPoints{ element?, parent? }Which point of the element pins to which point of its target. Both default to "left-top". See Attach points
floating.pointerCaptureMode"capture" | "passthrough"Pointer event capture
floating.clipTo"none" | "attached-parent"Clip to attached parent
floating.zIndexnumberStacking order

An attach point names a horizontal side, then a vertical side. There are nine:

TopCenterBottom
Left"left-top""left-center""left-bottom"
Center"center-top""center-center""center-bottom"
Right"right-top""right-center""right-bottom"

The renderer places the element so that its element point lands on the target’s parent point, then shifts it by floating.x and floating.y. Use the same point on both to pin the element inside a corner or an edge. This badge sits in the panel’s top-right corner:

import {
function close(): CloseElement
close
,
const fixed: (value: number) => SizingAxis
fixed
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
} from "@bomb.sh/tty";
let
let ops: (CloseElement | OpenElement | Text)[]
ops
= [
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("panel", {
layout?: {
width?: SizingAxis;
height?: SizingAxis;
padding?: {
left?: number;
right?: number;
top?: number;
bottom?: number;
};
gap?: number;
direction?: "ltr" | "ttb";
alignX?: "left" | "center" | "right";
alignY?: "top" | "center" | "bottom";
}
layout
: {
width?: SizingAxis
width
:
function fixed(value: number): SizingAxis
fixed
(20),
height?: SizingAxis
height
:
function fixed(value: number): SizingAxis
fixed
(6) } }),
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("badge", {
floating?: {
x?: number;
y?: number;
expand?: {
width?: number;
height?: number;
};
parent?: number;
attachTo?: AttachTo;
attachPoints?: {
element?: AttachPoint;
parent?: AttachPoint;
};
pointerCaptureMode?: PointerCaptureMode;
clipTo?: ClipTo;
zIndex?: number;
}
floating
: {
attachTo?: AttachTo
attachTo
: "parent",
attachPoints?: {
element?: AttachPoint;
parent?: AttachPoint;
}
attachPoints
: {
element?: AttachPoint
element
: "right-top",
parent?: AttachPoint
parent
: "right-top" },
},
}),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("NEW"),
function close(): CloseElement
close
(),
function close(): CloseElement
close
(),
];

Pairing different points places the element outside its target. With element: "left-top" and parent: "right-bottom", the element starts just past the target’s bottom-right corner.

import {
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function rgba(r: number, g: number, b: number, a?: number): number
rgba
} from "@bomb.sh/tty";
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("sidebar", {
layout?: {
width?: SizingAxis;
height?: SizingAxis;
padding?: {
left?: number;
right?: number;
top?: number;
bottom?: number;
};
gap?: number;
direction?: "ltr" | "ttb";
alignX?: "left" | "center" | "right";
alignY?: "top" | "center" | "bottom";
}
layout
: {
width?: SizingAxis
width
: {
type: "fixed"
type
: "fixed",
value: number
value
: 20 } },
bg?: number
bg
:
function rgba(r: number, g: number, b: number, a?: number): number
rgba
(30, 30, 40),
transition?: Transition
transition
: {
Transition.duration: number
duration
: 0.3,
Transition.easing?: Easing
easing
: "easeInOut",
Transition.properties: TransitionProperty[]
properties
: ["width", "bg"],
},
});
PropertyTypeDescription
durationnumberDuration in seconds
easing"linear" | "easeIn" | "easeOut" | "easeInOut"Easing function
propertiesTransitionProperty[]Properties to animate
interactivebooleanAllow interaction during transition

Transition properties: "x", "y", "position", "width", "height", "size", "bg", "overlay", "borderColor", "borderWidth", "all".

Renders a text node inside the current open element.

PropertyTypeDescription
colornumberForeground color
bgnumberBackground color
fontSizenumberFont size
fontIdnumberFont identifier
wrapnumberWrap width
attrsnumberText attributes (bold, italic, etc.)
caretnumberPlaces the terminal cursor at this offset in the text. See Caret

caret gives a text input a real cursor. The renderer moves the terminal cursor to that offset in the text and shows it:

import {
function close(): CloseElement
close
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function rgba(r: number, g: number, b: number, a?: number): number
rgba
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
} from "@bomb.sh/tty";
let
let value: string
value
= "hello";
let
let position: number
position
= 2;
let
let ops: (CloseElement | OpenElement | Text)[]
ops
= [
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("input", {
border?: {
color: number;
bg?: number;
left?: BorderSide;
right?: BorderSide;
top?: BorderSide;
bottom?: BorderSide;
}
border
: {
color: number
color
:
function rgba(r: number, g: number, b: number, a?: number): number
rgba
(0, 255, 0),
left?: BorderSide
left
: 1,
right?: BorderSide
right
: 1,
top?: BorderSide
top
: 1,
bottom?: BorderSide
bottom
: 1,
},
}),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
(
let value: string
value
, {
caret?: number
caret
:
let position: number
position
}),
function close(): CloseElement
close
(),
];

Here the cursor lands between he and llo, inside the border.

  • The offset counts code points, and follows wrapping and wide characters. In 日本語 and 👍a, a caret of 1 lands in the third column.
  • An offset at or past the end of the text places the cursor one cell after the last character, where the next typed character goes.
  • If more than one text node sets a caret, the first one wins.
  • When you stop setting a caret, the next render hides the cursor. Frames that never set a caret leave the cursor alone.
  • Moving only the caret is cheap. The render writes just the cursor move.

Closes the current open element. Ops must be properly nested. Every open needs a matching close.

Pre-packs an op array into a reusable snapshot op. Useful when part of the UI is static across frames.

import {
function close(): CloseElement
close
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function snapshot(ops: Op[]): Op
snapshot
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
,
const grow: (min?: number, max?: number) => SizingAxis
grow
} from "@bomb.sh/tty";
let
let chrome: Op
chrome
=
function snapshot(ops: Op[]): Op
snapshot
([
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("header", {
layout?: {
width?: SizingAxis;
height?: SizingAxis;
padding?: {
left?: number;
right?: number;
top?: number;
bottom?: number;
};
gap?: number;
direction?: "ltr" | "ttb";
alignX?: "left" | "center" | "right";
alignY?: "top" | "center" | "bottom";
}
layout
: {
width?: SizingAxis
width
:
function grow(min?: number, max?: number): SizingAxis
grow
(),
height?: SizingAxis
height
: {
type: "fixed"
type
: "fixed",
value: number
value
: 1 } },
}),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("My App"),
function close(): CloseElement
close
(),
]);
// Each frame: [chrome, ...dynamicOps]

Packs an RGBA color into a 32-bit integer. Alpha defaults to 255.

import {
function rgba(r: number, g: number, b: number, a?: number): number
rgba
} from "@bomb.sh/tty";
function rgba(r: number, g: number, b: number, a?: number): number
rgba
(255, 0, 0); // red
function rgba(r: number, g: number, b: number, a?: number): number
rgba
(0, 255, 0, 128); // semi-transparent green
import {
const fit: (min?: number, max?: number) => SizingAxis
fit
,
const grow: (min?: number, max?: number) => SizingAxis
grow
,
const percent: (value: number) => SizingAxis
percent
,
const fixed: (value: number) => SizingAxis
fixed
} from "@bomb.sh/tty";
function fit(min?: number, max?: number): SizingAxis
fit
(); // shrink to content
function fit(min?: number, max?: number): SizingAxis
fit
(10, 40); // fit with min/max
function grow(min?: number, max?: number): SizingAxis
grow
(); // expand to fill available space
function grow(min?: number, max?: number): SizingAxis
grow
(5, 50); // grow with min/max
function percent(value: number): SizingAxis
percent
(0.5); // 50% of parent
function fixed(value: number): SizingAxis
fixed
(20); // exactly 20 cells
TypeDescription
OpUnion of all directive types
OpenElementElement opened by open()
TextText node
CloseElementElement closed by close()
SizingAxis{ type: "fit" | "grow" | "percent" | "fixed", ... }
TransitionTransition configuration
BorderSidenumber or { width, color?, bg? }

The module exports a type for each kind of op and prop:

TypeDescribes
OpAny op you pass to term.render()
OpenElement, CloseElement, TextThe ops open(), close(), and text() return
SizingAxisA width or height value, such as grow() or fixed(20)
BorderSideOne side of a border
Transition, TransitionProperty, EasingThe transition prop and its values
AttachPoint, AttachTo, PointerCaptureMode, ClipToThe values of the floating props
  • Term API (rendering ops to the terminal)
  • Examples (transitions and layout demos)