AphrodyBun GitHub

Runtime docs · Runtime · Process & System

Terminal APIs

Run interactive processes with Bun.Terminal or parse terminal output with Bun's headless terminal screens.

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.