---
url: /api/terminal.md
description: >-
  API reference for createTerminal -- options, PTY spawning, region selectors,
  screenshots, and the full TestTerminal interface.
---

# Terminal API

## `createTerminal(options)`

Creates a TestTerminal instance wrapping a backend with optional PTY support.

```typescript
import { createTerminal } from "@termless/core"
import type { TestTerminal, TerminalCreateOptions } from "@termless/core"
import { createXtermBackend } from "@termless/xtermjs"

const term = createTerminal({
  backend: createXtermBackend(),
  cols: 80, // default: 80
  rows: 24, // default: 24
  scrollbackLimit: 1000,
})
```

### TerminalCreateOptions

```typescript
interface TerminalCreateOptions {
  backend: TerminalBackend // Required
  cols?: number // Default: 80
  rows?: number // Default: 24
  scrollbackLimit?: number
}
```

| Option            | Type              | Default    | Description                                          |
| ----------------- | ----------------- | ---------- | ---------------------------------------------------- |
| `backend`         | `TerminalBackend` | *required* | Backend instance (e.g., from `createXtermBackend()`) |
| `cols`            | `number`          | `80`       | Terminal width in columns                            |
| `rows`            | `number`          | `24`       | Terminal height in rows                              |
| `scrollbackLimit` | `number`          | --         | Maximum scrollback lines (backend-dependent)         |

## TestTerminal Interface

```typescript
interface TestTerminal extends Terminal {
  // Properties
  readonly cols: number
  readonly rows: number
  readonly backend: TerminalBackend
  readonly alive: boolean // PTY process running?
  readonly exitInfo: string | null // e.g. "exit=0"

  // Region selectors (WHERE) -- getter properties
  readonly screen: Region // visible rows x cols area
  readonly scrollback: Region // history above screen
  readonly buffer: Region // everything (scrollback + screen)
  readonly viewport: Region // current scroll position view

  // Region selectors (WHERE) -- methods
  row(n: number): Row // screen row (negative from bottom)
  cell(row: number, col: number): Cell // single cell
  range(r1: number, c1: number, r2: number, c2: number): Region // rectangular region
  firstRow(): Row // convenience: first screen row
  lastRow(): Row // convenience: last screen row

  // Data feed (no PTY)
  feed(data: Uint8Array | string): void

  // PTY lifecycle
  spawn(command: string[], options?: { env?: Record<string, string>; cwd?: string }): Promise<void>

  // Input (requires PTY)
  press(key: string): void
  type(text: string): void
  click(x: number, y: number, options?: { ctrl?: boolean; shift?: boolean; alt?: boolean }): void
  dblclick(
    x: number,
    y: number,
    options?: { ctrl?: boolean; shift?: boolean; alt?: boolean; delay?: number },
  ): Promise<void>

  // Waiting (deprecated — prefer auto-retry matchers)
  /** @deprecated Use `await expect(term.screen).toContainText(text, { timeout })` instead */
  waitFor(text: string, timeout?: number): Promise<void>
  waitForStable(stableMs?: number, timeout?: number): Promise<void>

  // Search
  findText(text: string): TextMatch | null
  findAllText(pattern: RegExp): TextMatch[]

  // Screenshot
  screenshotSvg(options?: SvgScreenshotOptions): string
  screenshotPng(options?: PngScreenshotOptions): Promise<Uint8Array>
  screenshot(options?: ScreenshotOptions): Promise<Uint8Array> // auto-picker
  screenshotCanvasPng(options?: ScreenshotOptions): Promise<Uint8Array> // explicit native canvas

  // Resize
  resize(cols: number, rows: number): void

  // Cleanup
  close(): Promise<void>
  [Symbol.asyncDispose](): Promise<void>
}
```

## Properties

```typescript
term.cols // number -- current column count
term.rows // number -- current row count
term.backend // TerminalBackend -- the underlying backend
term.alive // boolean -- true if a spawned process is still running
term.exitInfo // string | null -- e.g. "exit=0" after process exits
```

## Region Selectors

Region selectors separate **where** to look from **what** to assert.

All region selector properties (`screen`, `scrollback`, `buffer`, `viewport`) and methods (`row()`, `range()`) return **lazy views** that recompute offsets on every access. This means you can hold a reference to `term.screen` and it always reflects the current terminal state -- no need to re-query after feeding data or resizing.

### Properties (no parentheses)

```typescript
term.screen // Region -- the rows x cols visible area
term.scrollback // Region -- history above screen (empty in alt screen)
term.buffer // Region -- everything (scrollback + screen)
term.viewport // Region -- current scroll position view
```

### Methods

```typescript
term.row(0) // Row -- first screen row
term.row(-1) // Row -- last screen row (negative from bottom)
term.cell(0, 0) // Cell -- single cell at row 0, col 0
term.range(0, 0, 5, 40) // Region -- rectangular region
term.firstRow() // Row -- convenience for first screen row
term.lastRow() // Row -- convenience for last screen row
```

### View Types

```typescript
// Region -- text access for a region
interface Region {
  getText(): string
  getLines(): string[]
  containsText(text: string): boolean
}

// Row -- a row with positional context (extends Region)
interface Row extends Region {
  readonly row: number
  readonly cells: Cell[]
  cellAt(col: number): Cell
}

// Cell -- a single cell with positional context and style
interface Cell {
  readonly text: string
  readonly row: number
  readonly col: number
  readonly fg: Color | null
  readonly bg: Color | null
  readonly bold: boolean
  readonly faint: boolean
  readonly italic: boolean
  readonly underline: UnderlineStyle
  readonly strikethrough: boolean
  readonly inverse: boolean
  readonly wide: boolean
}
```

## Data Feed

### `feed(data)`

Write data directly to the terminal (no PTY). Useful for testing rendering of ANSI output.

```typescript
term.feed("Hello, world!") // string
term.feed("\x1b[1;31mRed bold\x1b[0m") // ANSI escape sequences
term.feed(new Uint8Array([0x48, 0x69])) // raw bytes
```

## PTY Lifecycle

### `spawn(command, options?)`

Spawn a child process with a pseudo-terminal. The process output is fed to the backend automatically.

```typescript
await term.spawn(["ls", "-la"])
await term.spawn(["my-app", "--port", "3000"], {
  env: { NODE_ENV: "test" },
  cwd: "/path/to/project",
})
```

Throws if a process is already spawned or if the terminal is closed.

### SpawnOptions

```typescript
interface SpawnOptions {
  command: string[]
  env?: Record<string, string>
  cwd?: string
}
```

## Input

### `press(key)`

Send a keypress to the spawned process. Parses human-readable key descriptions into ANSI escape sequences.

```typescript
term.press("a") // Single character
term.press("Enter") // Named keys
term.press("ArrowUp") // Arrow keys
term.press("Ctrl+c") // Modifier + key
term.press("Ctrl+Shift+a") // Multiple modifiers
term.press("Alt+x") // Alt modifier
term.press("F5") // Function keys F1-F12
term.press("Shift+Tab") // Reverse tab
```

Supported modifiers: `Ctrl`, `Control`, `Alt`, `Option`, `Shift`, `Meta`, `Cmd`, `Super`.

Supported named keys: `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`, `End`, `PageUp`, `PageDown`, `Enter`, `Tab`, `Backspace`, `Delete`, `Escape`, `Space`, `F1`--`F12`.

### `type(text)`

Send raw text to the spawned process. Unlike `press()`, this does not parse key descriptions -- it writes the string directly to the PTY.

```typescript
term.type("Hello, world!")
term.type("search query\r") // \r for Enter
```

### `click(x, y, options?)`

Send a mouse click at terminal coordinates (0-based column, 0-based row). Requires PTY and mouse reporting to be enabled by the application.

```typescript
term.click(5, 10) // Click at column 5, row 10
term.click(0, 0, { ctrl: true }) // Ctrl+click at top-left
term.click(20, 5, { shift: true }) // Shift+click for selection
term.click(10, 3, { alt: true }) // Alt+click
```

| Option  | Type      | Default | Description         |
| ------- | --------- | ------- | ------------------- |
| `ctrl`  | `boolean` | `false` | Hold Ctrl modifier  |
| `shift` | `boolean` | `false` | Hold Shift modifier |
| `alt`   | `boolean` | `false` | Hold Alt modifier   |

### `dblclick(x, y, options?)`

Send a double-click at terminal coordinates. Returns a promise because it sends two clicks with a configurable delay between them.

```typescript
await term.dblclick(5, 10) // Double-click at column 5, row 10
await term.dblclick(5, 10, { delay: 50 }) // Custom inter-click delay (default: 100ms)
await term.dblclick(5, 10, { ctrl: true }) // Ctrl+double-click
```

| Option  | Type      | Default | Description                         |
| ------- | --------- | ------- | ----------------------------------- |
| `ctrl`  | `boolean` | `false` | Hold Ctrl modifier                  |
| `shift` | `boolean` | `false` | Hold Shift modifier                 |
| `alt`   | `boolean` | `false` | Hold Alt modifier                   |
| `delay` | `number`  | `100`   | Milliseconds between the two clicks |

## Waiting

### `waitFor(text, timeout?)` :badge\[deprecated]{type="warning"}

> **Deprecated.** Use auto-retry matchers instead:
>
> ```typescript
> await expect(term.screen).toContainText("ready>", { timeout: 5000 })
> ```
>
> Auto-retry matchers integrate with Vitest's expect API and provide better error messages on timeout.

Wait for specific text to appear in the terminal buffer. Polls every 50ms.

```typescript
await term.waitFor("ready>") // Default timeout: 5000ms
await term.waitFor("Loading...", 10000) // Custom timeout
```

Throws `Error` if the text doesn't appear within the timeout.

### `waitForStable(stableMs?, timeout?)`

Wait for terminal content to stop changing. Useful after keypresses or when waiting for rendering to complete.

```typescript
await term.waitForStable() // Default: stable for 200ms, timeout 5000ms
await term.waitForStable(100, 3000) // Stable for 100ms, timeout 3000ms
```

## Search

### `findText(text)`

Find the first occurrence of text in the terminal buffer. Returns a match or null.

```typescript
const pos = term.findText("Error")
if (pos) {
  console.log(`Found at row ${pos.row}, col ${pos.col}`)
}
```

Returns: `{ text: string, row: number, col: number, cells: Cell[] } | null`

### `findAllText(pattern)`

Find all regex matches in the terminal buffer.

```typescript
const matches = term.findAllText(/\d+\.\d+/g) // Find all decimal numbers
for (const m of matches) {
  console.log(`${m.text} at (${m.row}, ${m.col})`)
}
```

Returns: `TextMatch[]` -- array of `{ text, row, col, cells }`.

## Reading State

These methods are part of the `Terminal` interface and work with @termless/test matchers.

### `getText()`

Get all terminal text as a single string with newline-separated rows.

### `getTextRange(startRow, startCol, endRow, endCol)`

Get text from a rectangular region of the terminal.

### `getCell(row, col)`

Get a single cell with all attributes. Returns a `Cell` object:

```typescript
const cell = term.getCell(0, 0)
cell.text // string -- character
cell.fg // Color | null -- foreground color
cell.bg // Color | null -- background color
cell.bold // boolean
cell.faint // boolean
cell.italic // boolean
cell.underline // "none" | "single" | "double" | "curly" | "dotted" | "dashed"
cell.strikethrough // boolean
cell.inverse // boolean
cell.wide // boolean -- double-width character
```

### `getRow(row)`

Get all cells in a row as `Cell[]`.

### `getRows()`

Get the entire terminal grid as `Cell[][]`.

### `getCursor()`

Get cursor state: `{ col: number, row: number, visible: boolean, style: "block" | "underline" | "beam" }`.

### `getMode(mode)`

Check if a terminal mode is active. Returns `boolean`.

### `getTitle()`

Get the terminal title (set via OSC 2 escape sequence).

### `getScrollback()`

Get scrollback state: `{ viewportTop: number, totalRows: number, screenRows: number }`.

## Terminal Interface

The read-only subset that terminal matchers accept:

```typescript
interface Terminal {
  getText(): string
  getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string
  getCell(row: number, col: number): Cell
  getRow(row: number): Cell[]
  getRows(): Cell[][]
  getCursor(): Cursor
  getMode(mode: TerminalMode): boolean
  getTitle(): string
  getScrollback(): ScrollbackState
}
```

## TextMatch

```typescript
interface TextMatch {
  text: string
  row: number
  col: number
  cells: Cell[]
}
```

## Screenshots

### `screenshotSvg(options?)`

Capture the terminal as an SVG string. See [Screenshots](/guide/screenshots) for options.

```typescript
const svg = term.screenshotSvg()
const svg = term.screenshotSvg({ theme: { background: "#282a36" } })
```

### `screenshotPng(options?)`

Capture the terminal as a PNG buffer. Requires `@resvg/resvg-js` (`bun add -d @resvg/resvg-js`). See [Screenshots](/guide/screenshots) for options.

```typescript
const png = await term.screenshotPng()
const png = await term.screenshotPng({ scale: 3, theme: { background: "#282a36" } })
```

### `screenshot(options?)` — auto-picker (recommended)

Capture the terminal as a PNG, picking the best renderer available:

1. If the backend has a native `screenshot()` (today: `@termless/ghostty`'s ghostty backend), use it directly.
2. Else if `@termless/ghostty` is installed, serialize the cell grid via `cellsToAnsi` and feed to `renderAnsiPng` (native canvas + ghostty-web WASM).
3. Else fall back to the resvg-based `screenshotPng()` path — cross-platform, lower fidelity.

```typescript
const png = await term.screenshot()
const png = await term.screenshot({ fontPath: "/path/to/iosevka.ttf", theme: { background: "#1a1b26" } })
```

### `screenshotCanvasPng(options?)` — explicit native canvas

Always routes through `@termless/ghostty`'s `renderTerminalPng`, regardless of the backend. Use when the caller needs the ghostty renderer's fidelity specifically and doesn't want the auto-picker silently choosing a different path. Throws if `@termless/ghostty` is not installed.

```typescript
const png = await term.screenshotCanvasPng()
const png = await term.screenshotCanvasPng({ fontSize: 14, fontFamily: "'Fira Code', monospace" })
```

## Resize

### `resize(cols, rows)`

Resize the terminal. Updates both backend and PTY (if spawned).

```typescript
term.resize(120, 40)
```

## Cleanup

### `close()`

Close the terminal, kill any spawned process, and destroy the backend. Safe to call multiple times.

```typescript
await term.close()
```

### `Symbol.asyncDispose`

Supports the `using` declaration for automatic cleanup:

```typescript
await using term = createTerminal({ backend: createXtermBackend() })
// term.close() called automatically when scope exits
```

## Key Utilities

```typescript
import { parseKey, keyToAnsi } from "@termless/core"

// Parse "Ctrl+a" -> { key: "a", ctrl: true }
const desc = parseKey("Ctrl+Shift+ArrowUp")

// Convert to ANSI escape sequence
const ansi = keyToAnsi("Ctrl+c") // "\x03"
const ansi = keyToAnsi({ key: "ArrowUp", ctrl: true }) // "\x1b[1;5A"
```

## hasExtension

Type-safe extension check for backend capabilities:

```typescript
import { hasExtension } from "@termless/core"
import type { MouseEncodingExtension } from "@termless/core"

if (hasExtension<MouseEncodingExtension>(backend, "mouse")) {
  const encoded = backend.encodeMouse({ x: 5, y: 10, button: "left", action: "press" })
}
```
