---
sidebar_position: 3
title: Terminal (tty & style)
---

# Terminal

## <Obj /> tty {#tty}

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

:::warning 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:

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

### <Fn /> tty.write {#tty-write}

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

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

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

### <Fn /> tty.size {#tty-size}

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

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

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

### <Fn /> tty.clear {#tty-clear}

```ts
clear(): void
```

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

### <Fn /> tty.move {#tty-move}

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

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

### <Fn /> tty.hideCursor {#tty-hide-cursor}

```ts
hideCursor(): void
```

Hides the terminal cursor.

### <Fn /> tty.showCursor {#tty-show-cursor}

```ts
showCursor(): void
```

Shows the terminal cursor.

### <Fn /> tty.alt {#tty-alt}

```ts
alt(): void
```

Switches to the alternate screen buffer.

### <Fn /> tty.main {#tty-main}

```ts
main(): void
```

Switches back to the main screen buffer.

### <Fn /> tty.eraseLine {#tty-erase-line}

```ts
eraseLine(): void
```

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

### <Fn /> tty.title {#tty-title}

```ts
title(str: string): void
```

Sets the terminal window title.

### <Fn /> tty.frame {#tty-frame}

```ts
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.

```js
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.

### <Obj /> tty.clipboard {#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.

#### <Fn /> tty.clipboard.writeText {#tty-clipboard-write-text}

```ts
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.

```js
tty.clipboard.writeText("hello world");
tty.clipboard.writeText(JSON.stringify(response));
```

:::warning 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 {#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.

```js
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.

:::note 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`.
:::

#### <Fn /> tty.on("keydown" | "keyup") {#tty-on-key}

```ts
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`.

```js
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();
});
```

#### <Fn /> tty.on("mousedown" | "mouseup") {#tty-on-mouse-button}

```ts
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()`.

#### <Fn /> tty.on("mousemove") {#tty-on-mousemove}

```ts
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`.

#### <Fn /> tty.on("wheel") {#tty-on-wheel}

```ts
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.

#### <Fn /> tty.enableMouse / tty.disableMouse {#tty-mouse}

```ts
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.

#### <Fn /> tty.enableKittyKeyboard / tty.disableKittyKeyboard {#tty-kitty-keyboard}

```ts
enableKittyKeyboard(): void
disableKittyKeyboard(): void
```

Enables/disables the [kitty keyboard protocol](https://sw.kovidgoyal.net/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 {#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.

```js
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:

### <Fn /> tty.enableMouse / tty.disableMouse {#tty-enable-mouse}

```ts
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.

### <Fn /> tty.enableKittyKeyboard / tty.disableKittyKeyboard {#tty-enable-kitty-keyboard}

```ts
enableKittyKeyboard(): void
disableKittyKeyboard(): void
```

Enables or disables the [Kitty keyboard protocol](https://sw.kovidgoyal.net/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.

### <Fn /> tty.on / tty.off / tty.once {#tty-on-off-once}

```ts
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 {#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.

```ts
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.

```ts
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-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 {#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`

```js
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 {#mouse-events}

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

#### `mousedown` / `mouseup`

```ts
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).

```ts
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`

```ts
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. |

```js
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();
});
```

---

## <Obj /> style {#style}

```ts
(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.

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

### Foreground colors

<ForegroundColors />

### Background colors

<BackgroundColors />

### <Fn /> style.bold {#style-bold}

```ts
bold(text: string): string
```

Bold text.

### <Fn /> style.dim {#style-dim}

```ts
dim(text: string): string
```

Dimmed text.

### <Fn /> style.italic {#style-italic}

```ts
italic(text: string): string
```

Italic text.

### <Fn /> style.underline {#style-underline}

```ts
underline(text: string): string
```

Underlined text.

### <Fn /> style.blink {#style-blink}

```ts
blink(text: string): string
```

Blinking text. Terminal support varies.

### <Fn /> style.reversed {#style-reversed}

```ts
reversed(text: string): string
```

Swaps foreground and background colors.

### <Fn /> style.hidden {#style-hidden}

```ts
hidden(text: string): string
```

Hidden text.

### <Fn /> style.strikethrough {#style-strikethrough}

```ts
strikethrough(text: string): string
```

Strikethrough text.

### <Fn /> style.reset {#style-reset}

```ts
reset(text: string): string
```

Strips all styling.

### <Fn /> style.fg {#style-fg}

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

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

:::note 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.
:::

### <Fn /> style.bg {#style-bg}

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

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