gespenst API - v0.1.0
    Preparing search index...

    Headless Runtime

    Use Ghostty VT parsing, input encoding, events, snapshots, and resolved viewport cells without a DOM or browser renderer. The headless runtime is the terminal state machine: it does not create a shell, open a PTY, draw pixels, or measure fonts.

    The headless export is included in @gespenst/core:

    pnpm add @gespenst/core
    
    const runtime = await createCoreRuntime();
    const terminal = runtime.createTerminal({ cols: 100, rows: 30, scrollbackLines: 2_000 });

    terminal.write('\x1b[1;34mParsed by Ghostty VT\x1b[0m\r\n');
    const frame = terminal.viewport();
    const text = frame.viewportRows.map((row) => row.text).join('\n');

    terminal.key({
    code: 'KeyC',
    text: 'c',
    modifiers: KeyModifiers.control,
    });

    console.log(text);
    runtime.dispose();

    One CoreRuntime owns the Ghostty WASM instance and can create multiple CoreTerminal instances. Reuse a runtime when processing many independent sessions. Disposing the runtime disposes all of its terminals and prevents further creation.

    Headless terminals default to 80 columns, 24 rows, 9 by 18 device-pixel cells, and 10,000 scrollback lines. Set explicit cell dimensions when downstream code uses pixel geometry. There is no font measurement because the runtime does not own a browser canvas.

    write() accepts text or bytes and updates terminal state. Call render() afterward to read an incremental frame containing changed rows, or call viewport() to read a complete resolved viewport. Use changed rows for incremental consumers and full snapshots only where required.

    bufferState() returns authoritative active-screen, scrollback, viewport, and cursor coordinates. readBuffer() reads a half-open, clamped range from the complete retained Ghostty grid and defaults to the visible viewport:

    const state = terminal.bufferState();
    const recentHistory = terminal.readBuffer({
    start: Math.max(0, state.totalRows - 200),
    end: state.totalRows,
    });

    Rows include stable retained-row identities, grapheme-aware cells, width roles, resolved styles, wrapping, and semantic content. Page large histories instead of reading the entire scrollback on every update.

    Terminal input methods return encoded PTY bytes and emit an input event. Output may also cause Ghostty to emit replies or metadata events. Subscribe to the events needed by your integration.

    Assert the resolved screen rather than comparing raw escape sequences. This works well for CLI snapshot tests, prompts, progress output, cursor behavior, and ANSI styling tests.

    export async function assertCliOutput(): Promise<void> {
    const runtime = await createCoreRuntime();
    try {
    const terminal = runtime.createTerminal({ cols: 80, rows: 24 });
    terminal.write('\x1b[32mBuild succeeded\x1b[0m\r\n');

    const screen = terminal
    .viewport()
    .viewportRows.map((row) => row.text)
    .join('\n');

    if (!screen.includes('Build succeeded')) {
    throw new Error('Expected the success message in the visible terminal');
    }
    } finally {
    runtime.dispose();
    }
    }

    Pair the runtime with node-pty or another PTY implementation. The adapter below deliberately does not depend on a specific PTY package. PTY output is parsed into changed rows for the client, while all input events, including terminal replies, are sent back to the PTY.

    interface PtyAdapter {
    onData(listener: (data: string) => void): Disposable;
    write(data: string): void;
    }

    export async function attachPty(
    pty: PtyAdapter,
    sendToClient: (message: unknown) => void
    ): Promise<Disposable> {
    const runtime = await createCoreRuntime();
    const terminal = runtime.createTerminal({ cols: 120, rows: 30 });
    const decoder = new TextDecoder();

    const output = pty.onData((data) => {
    terminal.write(data);
    const frame = terminal.render();

    if (frame.dirty !== 'clean') {
    sendToClient({ rows: frame.changedRows, cursor: frame.cursor });
    }
    });

    const input = terminal.on('input', ({ data }) => {
    pty.write(decoder.decode(data, { stream: true }));
    });

    return {
    dispose() {
    output.dispose();
    input.dispose();
    runtime.dispose();
    },
    };
    }

    This cell-diff protocol can reduce client work and bandwidth for specialized applications. For a conventional browser terminal, forwarding the raw PTY byte stream is simpler and moves parsing to the client.

    A renderer does not need to parse ANSI or VT sequences. Consume the resolved changed rows and cursor state with WebGPU, WebGL, Canvas, OffscreenCanvas, or a native rendering surface.

    interface IncrementalRenderer {
    updateRow(row: RenderRow): void;
    updateCursor(cursor: RenderCursor): void;
    present(): void;
    }

    export function renderOutput(
    terminal: CoreTerminal,
    renderer: IncrementalRenderer,
    data: string | Uint8Array
    ): void {
    terminal.write(data);
    const frame = terminal.render();

    for (const row of frame.changedRows) renderer.updateRow(row);
    renderer.updateCursor(frame.cursor);
    renderer.present();
    }

    Use render().changedRows on the hot path. Reserve viewport() for initialization, recovery, or occasional inspection to avoid rebuilding every visible row unnecessarily.

    Extract logical terminal content after cursor movement, wrapping, and screen updates have been applied. This is useful for searchable CI logs, session summaries, accessibility views, and AI context extraction.

    interface TextIndex {
    add(text: string): Promise<void>;
    }

    export async function indexTerminalBuffer(terminal: CoreTerminal, index: TextIndex): Promise<void> {
    terminal.selectAll();
    try {
    const transcript = terminal.getSelection({
    format: 'plain',
    unwrap: true,
    trim: true,
    });
    await index.add(transcript);
    } finally {
    terminal.clearSelection();
    }
    }

    The result represents terminal buffer state, not an immutable raw log. Keep the original PTY stream as well when auditing or exact playback matters.

    Store timestamped PTY chunks and replay them through the same parser used for live sessions. A player can update only the rows changed by each chunk.

    interface RecordingEntry {
    readonly delayMs: number;
    readonly data: string | Uint8Array;
    }

    export async function replay(
    terminal: CoreTerminal,
    renderer: IncrementalRenderer,
    recording: readonly RecordingEntry[]
    ): Promise<void> {
    for (const entry of recording) {
    await new Promise((resolve) => setTimeout(resolve, entry.delayMs));
    renderOutput(terminal, renderer, entry.data);
    }
    }

    This pattern supports terminal demos, CI failure replays, debugging tools, thumbnails, and scrubbable timelines.

    Snapshots serialize Ghostty terminal state and geometry. Use them for checkpoints, refresh recovery, worker migration, and fast reconnection without replaying an entire recording.

    interface SnapshotStorage {
    save(id: string, snapshot: Uint8Array): Promise<void>;
    load(id: string): Promise<Uint8Array>;
    }

    export async function checkpointTerminal(
    terminal: CoreTerminal,
    storage: SnapshotStorage
    ): Promise<void> {
    await storage.save('session-123', terminal.snapshot());
    }

    export async function restoreTerminal(
    storage: SnapshotStorage
    ): Promise<{ readonly terminal: CoreTerminal; dispose(): void }> {
    const runtime = await createCoreRuntime();
    const terminal = runtime.createTerminal();
    terminal.restore(await storage.load('session-123'));
    return {
    terminal,
    dispose() {
    runtime.dispose();
    },
    };
    }

    A snapshot does not preserve the shell process or PTY. Persist and reconnect those resources separately.

    The runtime tracks active terminal modes, so keys, paste, focus, and pointer input can be encoded for the current application state. For example, paste() honors bracketed-paste mode.

    export function connectInput(
    terminal: CoreTerminal,
    sendToPty: (data: Uint8Array) => void
    ): Disposable {
    const input = terminal.on('input', ({ data }) => sendToPty(data));

    terminal.key({ code: 'ArrowUp' });
    terminal.paste('hello\nworld');
    terminal.focus(true);

    return input;
    }

    An input method both returns its encoded bytes and emits an input event. Choose one forwarding path; forwarding both would send the same input twice.

    Titles, working directories, progress reports, notifications, bells, clipboard requests, and errors are available independently of rendering. This supports dashboards, session managers, IDEs, and background-task notifications.

    interface SessionMetadata {
    title: string;
    cwd: string;
    progress: number | null;
    }

    export function observeSession(
    terminal: CoreTerminal,
    metadata: SessionMetadata,
    notify: (title: string, body: string) => void
    ): Disposable {
    const subscriptions = [
    terminal.on('title', (title) => {
    metadata.title = title;
    }),
    terminal.on('cwd', (cwd) => {
    metadata.cwd = cwd;
    }),
    terminal.on('progress', ({ progress }) => {
    metadata.progress = progress;
    }),
    terminal.on('notification', ({ title, body }) => notify(title, body)),
    ];

    return {
    dispose() {
    for (const subscription of subscriptions) subscription.dispose();
    },
    };
    }

    Create one CoreTerminal for each independent session while sharing a single compiled Ghostty WASM runtime. This fits browser IDEs, multi-pane terminals, CI dashboards, and server-side session processors.

    export async function createSessionPool(): Promise<Disposable> {
    const runtime = await createCoreRuntime();
    const sessions = new Map([
    ['build', runtime.createTerminal()],
    ['server', runtime.createTerminal()],
    ['tests', runtime.createTerminal()],
    ]);

    sessions.get('build')?.write('Compiling...\r\n');
    sessions.get('tests')?.write('14 tests passed\r\n');

    return runtime;
    }

    Dispose individual terminals as sessions close. Disposing the runtime releases every terminal that is still active.

    createCoreRuntime() accepts URLs, strings, byte arrays, responses, or compiled WebAssembly.Module objects. The loader caches compilation by URL and by reusable ArrayBuffer identity. Preload with preloadGhostty() when startup timing needs an explicit boundary.

    The callback bridge must match the Ghostty artifact. Deploy and version both assets together.

    The runtime is useful for snapshot generation, terminal-aware indexing, protocol tests, server-side VT parsing, and deterministic render-state inspection. It is not a PTY implementation. Use the operating system or a PTY package to create a shell process and pass bytes between it and the core.