Bun has separate APIs for opening a terminal process and rendering terminal
output. Bun.Terminal creates a PTY or ConPTY for a child process; the headless
screens parse terminal output without starting a process.
Run an interactive process
Use Bun.Terminal with Bun.spawn() when a
child process needs a TTY. It connects the process to a PTY on Linux and macOS,
or ConPTY on Windows.
Parse terminal output
Bun.TerminalScreen and Bun.RioTerminal are headless screens. Create one,
write terminal output to it, and read the rendered text or cell data:
import { Terminal as RioTerminal } from "bun:rio";
const screen = new RioTerminal({ cols: 80, rows: 24, scrollback: 1000 });
screen.write("hello\r\nworld");
console.log(screen.text());
The bun:rio module exports the same constructor as Bun.RioTerminal. It only
parses data supplied to write(); it does not create a PTY, spawn a shell, or
read host files. To display a child's output, pass that output to the screen.
The constructor accepts optional dimensions from 1 to 4096 columns and rows,
and a scrollback limit from 0 to 1,000,000 rows. Defaults are 80 columns, 24
rows, and 1,000 scrollback rows. resize(cols, rows) changes the screen size;
reset() clears its contents and pending terminal replies. text() returns
visible text, while text({ scrollback: true }) includes retained lines.
write() returns the number of input bytes consumed. cells() returns a
Uint32Array in row-major order, with four values per cell: code point,
foreground color, background color, and flags. takeReplies() drains replies
to terminal query sequences. The cols, rows, cursor, title, and
alternateScreen properties report the current screen state.
RioTerminal.cellText(col, row) returns the full grapheme at a visible cell's
zero-based column and row. It preserves combining marks, variation selectors,
and joined emoji sequences; a wide-character spacer cell, including the filler
at the end of a line before a wrapped wide character, returns an empty string.
Both coordinates must be integers within the current visible screen.
In contrast, cells() stores only the grapheme's first code point in its first
word; the spacer cell uses code point 32 and display width 0.
Bun.TerminalScreen remains available for existing applications using Bun's
built-in terminal parser. Use Bun.RioTerminal or bun:rio when you want Rio's
VT parser and grid.
Streaming and snapshots
write() consumes UTF-8 bytes; its return value counts bytes rather than JavaScript
UTF-16 code units. Pass byte chunks directly to preserve characters and escape
sequences split across reads. A typed-array or DataView input consumes only that
view's byte range. A string is encoded as UTF-8 on each call, so splitting a
surrogate pair across separate string writes does not preserve that pair.
import { Terminal } from "bun:rio";
const screen = new Terminal({ cols: 80, rows: 24, scrollback: 1000 });
const bytes = new TextEncoder().encode("café\r\n");
screen.write(bytes.subarray(0, 4));
screen.write(bytes.subarray(4));
console.log(screen.cellText(3, 0)); // é
LF moves down without an implicit carriage return; use CRLF to start a new line
at column zero. text() joins physical screen rows with LF and omits unused
trailing cells. Blank rows remain present; wrapped lines remain separate rows.
text({ scrollback: true }) prepends retained history without consuming it or
changing subsequent visible-screen reads. A zero scrollback limit retains no
history. Alternate-screen content is separate from the normal screen.
cells() returns a fresh Uint32Array of cols * rows * 4 words. Cell (col, row)
starts at 4 * (row * cols + col). Mutating this array does not alter the terminal,
and subsequent writes do not alter an earlier snapshot. The first word stores
the first code point, while cellText(col, row) retains the full grapheme.
An untouched empty cell uses code point zero; a wide-character spacer uses 32.
Wide-character continuation cells have width zero and empty
cellText() output. See the public type declaration for packed colors and flags.
Terminal queries enqueue replies. takeReplies() concatenates and drains them;
the caller decides whether to send those bytes back to its child process. Rio
does not send them to any host process automatically. Kitty image transfers
backed by host files, temporary files or shared memory are rejected with an
unsupported-medium reply before host access. Synchronized updates
(CSI ? 2026 h) buffer input until their matching end sequence (CSI ? 2026 l).
Timeout expiry is processed on a subsequent write() call, including an empty
write; reading a snapshot does not drive an event loop.
reset() discards normal and alternate screen content, scrollback, title,
pending replies and incomplete input. It retains the current dimensions and
configured scrollback limit. resize() validates both dimensions before changing
the grid and lets Rio reflow existing content. Large dimensions and history
limits increase retained memory; Bun accounts for the grids, parser buffers and
queued replies when scheduling garbage collection.