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.
Building a frame
Section titled “Building a frame”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(),];open(id, props?)
Section titled “open(id, props?)”Opens a layout element. Every element needs a unique string id for pointer hit-testing and layout queries.
Layout
Section titled “Layout”| Property | Type | Description |
|---|---|---|
width | SizingAxis | Horizontal sizing (fit, grow, percent, fixed) |
height | SizingAxis | Vertical sizing |
padding | { left?, right?, top?, bottom? } | Inner padding in cells |
gap | number | Gap between children |
direction | "ltr" | "ttb" | Child layout direction |
alignX | "left" | "center" | "right" | Horizontal alignment |
alignY | "top" | "center" | "bottom" | Vertical alignment |
| Property | Type | Description |
|---|---|---|
bg | number | Background 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 |
Floating
Section titled “Floating”| Property | Type | Description |
|---|---|---|
floating.x, floating.y | number | Position 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.zIndex | number | Stacking order |
Attach points
Section titled “Attach points”An attach point names a horizontal side, then a vertical side. There are nine:
| Top | Center | Bottom | |
|---|---|---|---|
| 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.
Transitions
Section titled “Transitions”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"], },});| Property | Type | Description |
|---|---|---|
duration | number | Duration in seconds |
easing | "linear" | "easeIn" | "easeOut" | "easeInOut" | Easing function |
properties | TransitionProperty[] | Properties to animate |
interactive | boolean | Allow interaction during transition |
Transition properties: "x", "y", "position", "width", "height", "size", "bg", "overlay", "borderColor", "borderWidth", "all".
text(content, props?)
Section titled “text(content, props?)”Renders a text node inside the current open element.
| Property | Type | Description |
|---|---|---|
color | number | Foreground color |
bg | number | Background color |
fontSize | number | Font size |
fontId | number | Font identifier |
wrap | number | Wrap width |
attrs | number | Text attributes (bold, italic, etc.) |
caret | number | Places 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 of1lands 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.
close()
Section titled “close()”Closes the current open element. Ops must be properly nested. Every open needs a matching close.
snapshot(ops)
Section titled “snapshot(ops)”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]rgba(r, g, b, a?)
Section titled “rgba(r, g, b, a?)”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); // redfunction rgba(r: number, g: number, b: number, a?: number): number
rgba(0, 255, 0, 128); // semi-transparent greenSizing helpers
Section titled “Sizing helpers”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 contentfunction fit(min?: number, max?: number): SizingAxis
fit(10, 40); // fit with min/maxfunction grow(min?: number, max?: number): SizingAxis
grow(); // expand to fill available spacefunction grow(min?: number, max?: number): SizingAxis
grow(5, 50); // grow with min/maxfunction percent(value: number): SizingAxis
percent(0.5); // 50% of parentfunction fixed(value: number): SizingAxis
fixed(20); // exactly 20 cells| Type | Description |
|---|---|
Op | Union of all directive types |
OpenElement | Element opened by open() |
Text | Text node |
CloseElement | Element closed by close() |
SizingAxis | { type: "fit" | "grow" | "percent" | "fixed", ... } |
Transition | Transition configuration |
BorderSide | number or { width, color?, bg? } |
The module exports a type for each kind of op and prop:
| Type | Describes |
|---|---|
Op | Any op you pass to term.render() |
OpenElement, CloseElement, Text | The ops open(), close(), and text() return |
SizingAxis | A width or height value, such as grow() or fixed(20) |
BorderSide | One side of a border |
Transition, TransitionProperty, Easing | The transition prop and its values |
AttachPoint, AttachTo, PointerCaptureMode, ClipTo | The values of the floating props |