/** * tmux capture-pane snapshot. * * Returns a plain-text snapshot of a pane's visible content. * Used by the snapshot route (T-1.5) and the /thumbnail endpoint (T-1.6). * * Owner: T-1.1 */ import { execFile } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); export interface SnapshotOptions { /** tmux session name */ session: string; /** pane index within session (default "0.0") */ pane?: string; /** capture width (default: actual pane width) */ width?: number; /** capture height (default: actual pane height) */ height?: number; /** include escape sequences for colour/style (default: false = plain text) */ escapes?: boolean; } /** * Capture a plain-text (or escape-annotated) snapshot of a tmux pane. * Returns raw text as a string. */ export async function capturePane(opts: SnapshotOptions): Promise { const { session, pane, escapes = false } = opts; // Target just the session when no pane specified — avoids base-index issues. const target = pane ? `${session}:${pane}` : session; const args = ["capture-pane", "-t", target, "-p"]; if (escapes) args.push("-e"); // include escape sequences // Note: -S/-E (start/end line) omitted — captures current visible content const { stdout } = await execFileAsync("tmux", args); return stdout; } /** * Capture a thumbnail-sized snapshot (40×12) for the REST thumbnail endpoint. * Returns plain text, trimmed. */ export async function captureThumbnail( session: string, pane?: string, ): Promise { // tmux can't resize the capture directly via capture-pane flags, so we // capture full content and truncate to 40-char wide × 12 lines. const raw = await capturePane({ session, pane, escapes: false }); const lines = raw.split("\n").slice(0, 12); return lines.map((l) => l.slice(0, 40)).join("\n"); }