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 F1F35 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).