Terminal
O tty
Low-level terminal control. Writes go directly to the attached PTY and are mirrored to the daemon event stream.
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));
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.
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
| Field | Type | Description |
|---|---|---|
code | string | Layout-independent key identity — mirrors KeyboardEvent.code in browsers. See Key codes. |
key | string | null | The character the key produced ("a", "+") or null when no character was produced (e.g. arrow keys, releases). |
ctrlKey | boolean | Ctrl held. |
altKey | boolean | Alt held. |
shiftKey | boolean | Shift held. |
metaKey | boolean | Always false (super/hyper are not surfaced yet). |
repeat | boolean | true 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)
| Field | Type | Description |
|---|---|---|
button | number | Which button: 0 left, 1 middle, 2 right, 3 back, 4 forward. |
clientX / clientY | number | Zero-based column and row of the click. |
ctrlKey / altKey / shiftKey / metaKey | boolean | Modifier 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)
| Field | Type | Description |
|---|---|---|
buttons | number | Bitmask of held buttons: 1 left, 2 right, 4 middle, 8 back, 16 forward. Mirrors MouseEvent.buttons. |
clientX / clientY | number | Zero-based column and row. |
ctrlKey / altKey / shiftKey / metaKey | boolean | Modifier state. |
wheel
tty.on('wheel', (e: WheelEvent) => void)
| Field | Type | Description |
|---|---|---|
deltaX | number | Horizontal scroll: −120 left, +120 right per notch. |
deltaY | number | Vertical scroll: −120 up, +120 down per notch. |
clientX / clientY | number | Zero-based column and row of the pointer. |
ctrlKey / altKey / shiftKey / metaKey | boolean | Modifier 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.
F style.blink
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).
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).