Skip to main content

Terminal

O tty​

Low-level terminal control. Writes go directly to the attached PTY and are mirrored to the daemon event stream.

PTY required

tty is undefined when no PTY is attached — for example when stdout is piped or when running under a CI runner. Scripts that need to support both modes should check at startup:

if (typeof tty === 'undefined') {
// no PTY attached — fall back to console.log
}

F tty.write​

write(...args: any[]): void

Writes raw data to the TTY. Arguments are joined with spaces.

tty.write("hello world\n");

F tty.size​

size(): { rows: number; cols: number }

Safe to call without a PTY — defaults to { rows: 24, cols: 80 }.

const { rows, cols } = tty.size();

F tty.clear​

clear(): void

Clears the screen and moves the cursor to the top-left.

F tty.move​

move(row: number, col: number): void

Moves the cursor to the given position (0-based).

F tty.hideCursor​

hideCursor(): void

Hides the terminal cursor.

F tty.showCursor​

showCursor(): void

Shows the terminal cursor.

F tty.alt​

alt(): void

Switches to the alternate screen buffer.

F tty.main​

main(): void

Switches back to the main screen buffer.

F tty.eraseLine​

eraseLine(): void

Erases the current line and returns the cursor to the start of it.

F tty.title​

title(str: string): void

Sets the terminal window title.

F tty.frame​

frame(callback: () => void): void
beginFrame(): void
endFrame(): void

Buffers all tty.* writes and flushes them in a single atomic write, wrapped in Synchronized Output Mode escapes. Capable terminals defer repaint until the end, so the user never sees a half-drawn frame.

function render() {
const { rows, cols } = tty.size();
tty.frame(() => {
tty.move(0, 0);
tty.write(style.bold("DASHBOARD"));
// ...all your draw calls...
});
}
  • Frames nest — only the outermost frame emits the sync begin/end and flushes.
  • Exception-safe — if the callback throws, the buffer is still flushed.
  • Not a compositor — frames coalesce bytes, not cells. The runtime has no virtual screen and won't elide redundant moves or overdraws.

tty.beginFrame() / tty.endFrame() are available if you need to span a frame across an async boundary.

O tty.clipboard​

Namespace mirroring navigator.clipboard.writeText. Used to copy text from a script into the host machine's system clipboard, which works even when the script is running inside a VM or over SSH because the user's terminal emulator interprets the OSC 52 escape and writes to the system clipboard directly.

F tty.clipboard.writeText​

writeText(...args: any[]): void

Writes text to the host system clipboard via the OSC 52 selection-set escape sequence. Arguments are joined with spaces (same shape as tty.write). Fire-and-forget: the JS call returns immediately and the OSC 52 bytes flow back through the PTY to the host terminal, which decodes the base64 payload and writes to the clipboard.

tty.clipboard.writeText("hello world");
tty.clipboard.writeText(JSON.stringify(response));
Silent failure on terminals without OSC 52

The function returns nothing and never throws. If the host terminal does not support OSC 52 (older Terminal.app, gnome-terminal/VTE without it enabled, raw Linux console, tmux/screen without set-clipboard on), the escape bytes are silently dropped and the clipboard is not updated. There is no in-script way to detect this; if it matters, fall back to printing the value to the screen so the user can copy it manually.

Terminals known to support OSC 52: kitty, iTerm2, WezTerm, foot, Ghostty, modern Terminal.app, modern Windows Terminal, xterm, alacritty, tmux/screen with set-clipboard on set in the user's config.

Input events​

tty is an EventEmitter for keyboard and mouse input arriving from the attached terminal. Subscribe with tty.on(event, listener), unsubscribe with tty.off, or use tty.once for one-shot delivery. While at least one listener is registered, the isolate stays alive waiting for events — a script that only subscribes to input does not have to manage its own idle loop.

The event names and payload field names mirror the browser's KeyboardEvent and MouseEvent so existing web-development reflexes carry over.

tty.on("keydown", (e) => {
if (e.code === "Escape") yeet.exit();
if (e.ctrlKey && e.code === "c") yeet.exit();
});

Keyboard events are reported by default. Mouse and the kitty progressive keyboard protocol are opt-in — call tty.enableMouse() / tty.enableKittyKeyboard() once at startup. The corresponding disable* calls undo them; the runtime also restores both on isolate teardown so a crash can't leave the host terminal in an exotic mode.

Naming divergence on character codes

For named keys (ArrowUp, PageUp, F5, ContextMenu, etc.) event.code exactly matches the browser. For character keys it does not: terminals only deliver the resolved character, not the physical key position, so event.code carries the character itself ("a", "+") rather than the browser's layout-specific identifier ("KeyA", "Equal"). Modifier keys also never fire standalone events — pressing Shift produces nothing until the next keystroke arrives with shiftKey: true.

F tty.on("keydown" | "keyup")​

on(event: "keydown" | "keyup", listener: (e: KeyEvent) => void): tty

interface KeyEvent {
code: string; // base key identity, stable across press/release
key: string | null; // produced character (e.g. "A" with shift), null on keyup
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean; // currently always false
repeat: boolean; // true on auto-repeat keydown
}

keyup and repeat are only delivered when the kitty progressive keyboard protocol is enabled. Legacy terminals report every key as a press-only keydown, with repeat: false.

tty.enableKittyKeyboard();
tty.on("keydown", (e) => {
if (e.repeat) return; // ignore auto-repeats
if (e.code === "ArrowUp") moveCursor(-1);
if (e.code === "ArrowDown") moveCursor(+1);
});
tty.on("keyup", (e) => {
if (e.code === " ") releaseFire();
});

F tty.on("mousedown" | "mouseup")​

on(event: "mousedown" | "mouseup", listener: (e: MouseButtonEvent) => void): tty

interface MouseButtonEvent {
button: number; // 0=left, 1=middle, 2=right, 3=back, 4=forward
clientX: number; // 0-based column
clientY: number; // 0-based row
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean; // currently always false
}

Fires only after tty.enableMouse().

F tty.on("mousemove")​

on(event: "mousemove", listener: (e: MouseMoveEvent) => void): tty

interface MouseMoveEvent {
buttons: number; // bitmask: 1=left, 2=right, 4=middle, 8=back, 16=forward
clientX: number;
clientY: number;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}

Reports both pure motion (buttons: 0) and drags. The bitmask matches the browser's MouseEvent.buttons.

F tty.on("wheel")​

on(event: "wheel", listener: (e: WheelEvent) => void): tty

interface WheelEvent {
deltaX: number; // ±120 per notch (horizontal scroll)
deltaY: number; // ±120 per notch (positive = down)
clientX: number;
clientY: number;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}

The ±120 delta mirrors the browser convention (Chromium reports 120 per line on a notched wheel). Direction follows the browser too: positive deltaY is down, positive deltaX is right.

F tty.enableMouse / tty.disableMouse​

enableMouse(): void
disableMouse(): void

Enables/disables SGR mouse reporting from the terminal. Both calls write the appropriate enable/disable escape sequence. The runtime tracks the enabled state and restores disable on isolate teardown.

F tty.enableKittyKeyboard / tty.disableKittyKeyboard​

enableKittyKeyboard(): void
disableKittyKeyboard(): void

Enables/disables the kitty keyboard protocol (disambiguate + report event types + report all keys as escape codes + report associated text). Required for keyup events, repeat: true, and a stable code across press and release. Falls back to legacy press-only reporting on terminals that ignore it. Restored on isolate teardown.


Input events​

tty is an event emitter. Subscribe with tty.on(event, handler) and unsubscribe with tty.off(event, handler). Registering at least one listener keeps the isolate alive until the listener is removed — you don't need a timer to prevent the script from exiting.

tty.on('keydown', e => {
if (e.code === 'Escape') yeet.exit();
});

Enabling input modes​

Key and mouse events are off by default. Call the corresponding enable method before subscribing:

F tty.enableMouse / tty.disableMouse​

enableMouse(): void
disableMouse(): void

Enables or disables SGR mouse button reporting (button presses, releases, motion, and scroll). Emits mousedown, mouseup, mousemove, and wheel events on tty. The runtime automatically disables mouse reporting when the isolate exits.

F tty.enableKittyKeyboard / tty.disableKittyKeyboard​

enableKittyKeyboard(): void
disableKittyKeyboard(): void

Enables or disables the Kitty keyboard protocol. Without it, only keydown is reported and repeat is always false. With it, auto-repeats are delivered as separate keydown events (with repeat: true) and key releases fire keyup. The runtime pops the protocol stack entry on isolate exit.

F tty.on / tty.off / tty.once​

on(event: string, handler: (...args: any[]) => void): tty
off(event: string, handler: (...args: any[]) => void): tty
once(event: string, handler: (...args: any[]) => void): tty

Standard event emitter API. once removes the listener after the first delivery. Both on and off return tty for chaining.


Keyboard events​

Fired on tty after tty.enableKittyKeyboard() (or for basic presses, after any key input arrives).

keydown​

Fires on an initial key press and, under the Kitty keyboard protocol, on every auto-repeat tick.

tty.on('keydown', (e: KeyboardEvent) => void)

keyup​

Fires when a key is released. Only available under the Kitty keyboard protocol — never fires on terminals that don't support it.

tty.on('keyup', (e: KeyboardEvent) => void)

KeyboardEvent payload​

FieldTypeDescription
codestringLayout-independent key identity — mirrors KeyboardEvent.code in browsers. See Key codes.
keystring | nullThe character the key produced ("a", "+") or null when no character was produced (e.g. arrow keys, releases).
ctrlKeybooleanCtrl held.
altKeybooleanAlt held.
shiftKeybooleanShift held.
metaKeybooleanAlways false (super/hyper are not surfaced yet).
repeatbooleantrue on an auto-repeat keydown; requires the Kitty keyboard protocol.

Key codes​

code mirrors the browser's KeyboardEvent.code naming. Letter and symbol keys use their character ("a", "+"); named keys use browser PascalCase:

Tab Enter Escape Backspace ArrowUp ArrowDown ArrowLeft ArrowRight Home End PageUp PageDown Insert Delete F1–F35 CapsLock ScrollLock NumLock PrintScreen Pause ContextMenu

tty.enableKittyKeyboard();

tty.on('keydown', e => {
if (e.ctrlKey && e.code === 'c') yeet.exit();
if (e.code === 'ArrowUp') moveUp();
if (e.code === 'F5') refresh();
});

tty.on('keyup', e => {
// e.key is null on release — use e.code for identity
console.log(`released: ${e.code}`);
});

Mouse events​

Fired on tty after tty.enableMouse(). Coordinates are zero-based and match tty.size().

mousedown / mouseup​

tty.on('mousedown', (e: MouseDownEvent) => void)
tty.on('mouseup', (e: MouseDownEvent) => void)
FieldTypeDescription
buttonnumberWhich button: 0 left, 1 middle, 2 right, 3 back, 4 forward.
clientX / clientYnumberZero-based column and row of the click.
ctrlKey / altKey / shiftKey / metaKeybooleanModifier state (metaKey is always false).

mousemove​

Fired for motion (move or drag). Distinguishable by buttons: 0 is an unpressed move; a non-zero bitmask means a button is held (a drag).

tty.on('mousemove', (e: MouseMoveEvent) => void)
FieldTypeDescription
buttonsnumberBitmask of held buttons: 1 left, 2 right, 4 middle, 8 back, 16 forward. Mirrors MouseEvent.buttons.
clientX / clientYnumberZero-based column and row.
ctrlKey / altKey / shiftKey / metaKeybooleanModifier state.

wheel​

tty.on('wheel', (e: WheelEvent) => void)
FieldTypeDescription
deltaXnumberHorizontal scroll: −120 left, +120 right per notch.
deltaYnumberVertical scroll: −120 up, +120 down per notch.
clientX / clientYnumberZero-based column and row of the pointer.
ctrlKey / altKey / shiftKey / metaKeybooleanModifier state.
tty.enableMouse();

tty.on('mousedown', e => {
console.log(`click at col=${e.clientX} row=${e.clientY} button=${e.button}`);
});

tty.on('mousemove', e => {
if (e.buttons & 1) console.log(`dragging left button`);
});

tty.on('wheel', e => {
if (e.deltaY < 0) scrollUp();
else scrollDown();
});

O style​

(text: string) => string        // all color and formatting helpers
style.fg(text: string, r: number, g: number, b: number) => string
style.bg(text: string, r: number, g: number, b: number) => string

Pure-string ANSI styling utilities. Functions can be nested.

console.log(style.bold(style.green("success")));

Foreground colors​

F style.black

black(text: string): string

F style.red

red(text: string): string

F style.green

green(text: string): string

F style.yellow

yellow(text: string): string

F style.blue

blue(text: string): string

F style.magenta

magenta(text: string): string

F style.purple

purple(text: string): string

F style.cyan

cyan(text: string): string

F style.white

white(text: string): string

F style.brightBlack

brightBlack(text: string): string

F style.brightRed

brightRed(text: string): string

F style.brightGreen

brightGreen(text: string): string

F style.brightYellow

brightYellow(text: string): string

F style.brightBlue

brightBlue(text: string): string

F style.brightMagenta

brightMagenta(text: string): string

F style.brightCyan

brightCyan(text: string): string

F style.brightWhite

brightWhite(text: string): string

Background colors​

F style.bgBlack

bgBlack(text: string): string

F style.bgRed

bgRed(text: string): string

F style.bgGreen

bgGreen(text: string): string

F style.bgYellow

bgYellow(text: string): string

F style.bgBlue

bgBlue(text: string): string

F style.bgMagenta

bgMagenta(text: string): string

F style.bgCyan

bgCyan(text: string): string

F style.bgWhite

bgWhite(text: string): string

F style.bgBrightBlack

bgBrightBlack(text: string): string

F style.bgBrightRed

bgBrightRed(text: string): string

F style.bgBrightGreen

bgBrightGreen(text: string): string

F style.bgBrightYellow

bgBrightYellow(text: string): string

F style.bgBrightBlue

bgBrightBlue(text: string): string

F style.bgBrightMagenta

bgBrightMagenta(text: string): string

F style.bgBrightCyan

bgBrightCyan(text: string): string

F style.bgBrightWhite

bgBrightWhite(text: string): string

F style.bold​

bold(text: string): string

Bold text.

F style.dim​

dim(text: string): string

Dimmed text.

F style.italic​

italic(text: string): string

Italic text.

F style.underline​

underline(text: string): string

Underlined text.

blink(text: string): string

Blinking text. Terminal support varies.

F style.reversed​

reversed(text: string): string

Swaps foreground and background colors.

F style.hidden​

hidden(text: string): string

Hidden text.

F style.strikethrough​

strikethrough(text: string): string

Strikethrough text.

F style.reset​

reset(text: string): string

Strips all styling.

F style.fg​

fg(text: string, r: number, g: number, b: number): string

Applies a foreground color using an RGB triple (0–255 each).

Quantized to 16-color ANSI

These helpers do not emit 24-bit truecolor escapes. Each RGB triple is quantized to the closest ANSI 16-color code. Smooth gradients will collapse to a handful of distinct colors. Probe with JSON.stringify(style.fg('x', r, g, b)) to see the actual escape sequence.

F style.bg​

bg(text: string, r: number, g: number, b: number): string

Applies a background color using an RGB triple (0–255 each).