Skip to content

Term

The term module creates a WASM-backed renderer that converts UI ops into ANSI escape sequences.

Creates a renderer instance. Both dimensions are required.

import {
function createTerm(options: TermOptions): Promise<Term>
createTerm
} from "@bomb.sh/tty";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
: 80,
TermOptions.height: number
height
: 24 });
}
OptionTypeDescription
widthnumberTerminal width in columns
heightnumberTerminal height in rows

Returns a Term with a render() method and an update() method.

Renders a frame and returns the result.

import {
function close(): CloseElement
close
,
function createTerm(options: TermOptions): Promise<Term>
createTerm
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
,
const grow: (min?: number, max?: number) => SizingAxis
grow
} from "@bomb.sh/tty";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
: 80,
TermOptions.height: number
height
: 24 });
let {
let output: Uint8Array<ArrayBufferLike>
output
,
let events: PointerEvent[]
events
,
let info: RenderInfo
info
,
let errors: ClayError[]
errors
,
let animating: boolean
animating
} =
let term: Term
term
.
Term.render(ops: Op[], options?: RenderOptions): RenderResult
render
([
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
() } }),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Hello"),
function close(): CloseElement
close
(),
]);
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
Socket.write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean (+1 overload)

Sends data on the socket. The second parameter specifies the encoding in the case of a string. It defaults to UTF8 encoding.

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory.'drain' will be emitted when the buffer is again free.

The optional callback parameter will be executed when the data is finally written out, which may not be immediately.

See Writable stream write() method for more information.

@since ― v0.1.90

@param ― encoding Only used when data is string.

write
(
let output: Uint8Array<ArrayBufferLike>
output
);
}
OptionTypeDescription
pointer{ x, y, down }Pointer position and button state for hit testing
deltaTimenumberSeconds since last frame (for transitions). Auto-computed if omitted
mode"line"Render into a line region instead of full screen
rownumberStarting row for line mode (1-based, DSR format)

Pass pointer state to get hit-testing events alongside output:

import {
function close(): CloseElement
close
,
function createTerm(options: TermOptions): Promise<Term>
createTerm
,
const fixed: (value: number) => SizingAxis
fixed
,
const grow: (min?: number, max?: number) => SizingAxis
grow
,
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";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
: 80,
TermOptions.height: number
height
: 24 });
let {
let output: Uint8Array<ArrayBufferLike>
output
,
let events: PointerEvent[]
events
} =
let term: Term
term
.
Term.render(ops: Op[], options?: RenderOptions): RenderResult
render
(
[
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?: "ltr" | "ttb"
direction
: "ltr" },
}),
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
:
function fixed(value: number): SizingAxis
fixed
(20),
height?: SizingAxis
height
:
function grow(min?: number, max?: number): SizingAxis
grow
() },
}),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Sidebar"),
function close(): CloseElement
close
(),
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
("main", {
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
() } }),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Main content"),
function close(): CloseElement
close
(),
function close(): CloseElement
close
(),
],
{
RenderOptions.pointer?: {
x: number;
y: number;
down: boolean;
}
pointer
: {
x: number
x
: 5,
y: number
y
: 2,
down: boolean
down
: false } },
);
for (let
let event: PointerEvent
event
of
let events: PointerEvent[]
events
) {
// { type: "pointerenter", id: "sidebar" }
// { type: "pointerleave", id: "sidebar" }
// { type: "pointerclick", id: "main" }
}
}
PropertyTypeDescription
outputUint8ArrayANSI bytes to write to stdout
eventsPointerEvent[]Pointer enter/leave/click events
infoRenderInfoElement bounds lookup
errorsClayError[]Layout errors from Clay
animatingbooleantrue when transitions are still running

Returns element bounds for a given id, or undefined if not found:

import {
function close(): CloseElement
close
,
function createTerm(options: TermOptions): Promise<Term>
createTerm
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
,
const grow: (min?: number, max?: number) => SizingAxis
grow
} from "@bomb.sh/tty";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
: 80,
TermOptions.height: number
height
: 24 });
let {
let info: RenderInfo
info
} =
let term: Term
term
.
Term.render(ops: Op[], options?: RenderOptions): RenderResult
render
([
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
() } }),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Hello"),
function close(): CloseElement
close
(),
]);
let
let bounds: BoundingBox | undefined
bounds
=
let info: RenderInfo
info
.
RenderInfo.get(id: string): ElementInfo | undefined
get
("root")?.
ElementInfo.bounds: BoundingBox | undefined
bounds
;
}
TypeShape
pointerenter{ type: "pointerenter", id: string }
pointerleave{ type: "pointerleave", id: string }
pointerclick{ type: "pointerclick", id: string }

Layout errors include a type and message. Known error types:

  • TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED
  • ARENA_CAPACITY_EXCEEDED
  • ELEMENTS_CAPACITY_EXCEEDED
  • TEXT_MEASUREMENT_CAPACITY_EXCEEDED
  • DUPLICATE_ID
  • FLOATING_CONTAINER_PARENT_NOT_FOUND
  • PERCENTAGE_OVER_1
  • INTERNAL_ERROR
  • UNBALANCED_OPEN_CLOSE
  • CLIP_DEPTH_EXCEEDED

Resizes the renderer in place. Call it when the terminal changes size instead of creating a new Term, which would load the WebAssembly module again:

import
var process: NodeJS.Process
process
from "node:process";
import {
function close(): CloseElement
close
,
function createTerm(options: TermOptions): Promise<Term>
createTerm
,
const grow: (min?: number, max?: number) => SizingAxis
grow
,
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";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
:
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
WriteStream.columns: number

A number specifying the number of columns the TTY currently has. This property is updated whenever the 'resize' event is emitted.

@since ― v0.7.7

columns
,
TermOptions.height: number
height
:
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
WriteStream.rows: number

A number specifying the number of rows the TTY currently has. This property is updated whenever the 'resize' event is emitted.

@since ― v0.7.7

rows
,
});
function
function (local function) frame(): void
frame
() {
let {
let output: Uint8Array<ArrayBufferLike>
output
} =
let term: Term
term
.
Term.render(ops: Op[], options?: RenderOptions): RenderResult
render
([
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
() } }),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Hello"),
function close(): CloseElement
close
(),
]);
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
Socket.write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean (+1 overload)

Sends data on the socket. The second parameter specifies the encoding in the case of a string. It defaults to UTF8 encoding.

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory.'drain' will be emitted when the buffer is again free.

The optional callback parameter will be executed when the data is finally written out, which may not be immediately.

See Writable stream write() method for more information.

@since ― v0.1.90

@param ― encoding Only used when data is string.

write
(
let output: Uint8Array<ArrayBufferLike>
output
);
}
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
WriteStream.on(event: "resize", listener: () => void): NodeJS.WriteStream & {
fd: 1;
} (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

on
("resize", () => {
let term: Term
term
.
Term.update(options: UpdateOptions): void

Change dimensions in place (renderer-spec 7.7). Synchronous. The next render() after a non-no-op update emits a complete redraw, and output views from prior renders become invalid.

update
({
width: number
width
:
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
WriteStream.columns: number

A number specifying the number of columns the TTY currently has. This property is updated whenever the 'resize' event is emitted.

@since ― v0.7.7

columns
,
height: number
height
:
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
WriteStream.rows: number

A number specifying the number of rows the TTY currently has. This property is updated whenever the 'resize' event is emitted.

@since ― v0.7.7

rows
,
});
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
Socket.write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean (+1 overload)

Sends data on the socket. The second parameter specifies the encoding in the case of a string. It defaults to UTF8 encoding.

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory.'drain' will be emitted when the buffer is again free.

The optional callback parameter will be executed when the data is finally written out, which may not be immediately.

See Writable stream write() method for more information.

@since ― v0.1.90

@param ― encoding Only used when data is string.

write
("\x1b[2J");
function (local function) frame(): void
frame
();
});
function (local function) frame(): void
frame
();
}

update() takes one of two forms:

OptionTypeDescription
{ width, height }number, numberThe new size in columns and rows
{ events }Array<{ type: string }>A list of events. Only { type: "resize", width, height } events are read, and the last one wins

update() is synchronous. After a real size change, the next render() redraws every cell. An update to the same size changes nothing.

update() does not clear the screen. When the terminal shrinks, cells outside the new size stay visible, so clear the screen before the next frame, as the example does with \x1b[2J.

update() also empties the output of earlier renders. Write each frame’s output before you call update(), or copy it with output.slice().

When using transitions, gate your render loop on animating:

import {
function close(): CloseElement
close
,
function createTerm(options: TermOptions): Promise<Term>
createTerm
,
function open(id: string, props?: Omit<OpenElement, "directive" | "id">): OpenElement
open
,
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
,
const grow: (min?: number, max?: number) => SizingAxis
grow
} from "@bomb.sh/tty";
async function
function main(): Promise<void>
main
() {
let
let term: Term
term
= await
function createTerm(options: TermOptions): Promise<Term>
createTerm
({
TermOptions.width: number
width
: 80,
TermOptions.height: number
height
: 24 });
function
function (local function) frame(): void
frame
() {
let {
let output: Uint8Array<ArrayBufferLike>
output
,
let animating: boolean
animating
} =
let term: Term
term
.
Term.render(ops: Op[], options?: RenderOptions): RenderResult
render
([
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
() } }),
function text(content: string, props?: Omit<Text, "directive" | "content">): Text
text
("Animating..."),
function close(): CloseElement
close
(),
]);
var process: NodeJS.Process
process
.
NodeJS.Process.stdout: NodeJS.WriteStream & {
fd: 1;
}

The process.stdout property returns a stream connected tostdout (fd 1). It is a net.Socket (which is a Duplex stream) unless fd 1 refers to a file, in which case it is a Writable stream.

For example, to copy process.stdin to process.stdout:

import { stdin, stdout } from 'node:process';
stdin.pipe(stdout);

process.stdout differs from other Node.js streams in important ways. See note on process I/O for more information.

stdout
.
Socket.write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean (+1 overload)

Sends data on the socket. The second parameter specifies the encoding in the case of a string. It defaults to UTF8 encoding.

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory.'drain' will be emitted when the buffer is again free.

The optional callback parameter will be executed when the data is finally written out, which may not be immediately.

See Writable stream write() method for more information.

@since ― v0.1.90

@param ― encoding Only used when data is string.

write
(
let output: Uint8Array<ArrayBufferLike>
output
);
if (
let animating: boolean
animating
)
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)

Schedules execution of a one-time callback after delay milliseconds.

The callback will likely not be invoked in precisely delay milliseconds. Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified.

When delay is larger than 2147483647 or less than 1 or NaN, the delay will be set to 1. Non-integer delays are truncated to an integer.

If callback is not a function, a TypeError will be thrown.

This method has a custom variant for promises that is available using timersPromises.setTimeout().

@since ― v0.0.1

@param ― callback The function to call when the timer elapses.

@param ― delay The number of milliseconds to wait before calling the callback. Default: 1.

@param ― args Optional arguments to pass when the callback is called.

@returns ― for use with clearTimeout()

setTimeout
(
function (local function) frame(): void
frame
, 16);
}
function (local function) frame(): void
frame
();
}

The module exports a type for each shape on this page:

TypeDescribes
TermWhat createTerm() returns
TermOptionsThe options of createTerm(), width and height
UpdateOptionsThe options of update(), in either form
TermResizeEventA { type: "resize", width, height } event that update() reads
RenderOptions, RenderResultThe options and the result of render()
RenderInfoThe info lookup in a render result
ElementInfoWhat info.get(id) returns, { bounds }
BoundingBoxAn element’s bounds, with x, y, width, and height
PointerEvent, ClayErrorThe entries of events and errors