{"slug": "terminal-control-control-inspect-test-and-capture-terminal-sessions", "title": "Terminal Control: Control, inspect, test, and capture terminal sessions", "summary": "Terminal Control, a new open-source tool by Kit Langton, enables AI agents to control, inspect, test, and capture real terminal sessions via pseudo-terminals. The Rust-based tool supports named sessions, visible-screen reads, exact keyboard input, and video recording, allowing coding agents to interact with TUIs and CLIs without guessing from plain command output. It is designed for agent workflows and requires Rust 1.93 or newer.", "body_md": "Control, inspect, test, and capture real terminal applications for agents and TUI review.\n\nSaved from one live OpenCode session using `start`\n\n, `send`\n\n, and `save`\n\n.\n\nTerminal Control is built for agents first. Install the `termctrl`\n\nbinary, install the skill, then ask your coding agent to operate terminal applications through a real pseudo-terminal instead of guessing from plain command output.\n\nRequires Rust 1.93 or newer. Video export also requires `ffmpeg`\n\n.\n\n```\ncargo install terminal-control\ntermctrl --help\n```\n\nInstall the current repository head instead of the latest crate release:\n\n```\ncargo install --locked --git https://github.com/kitlangton/terminal-control terminal-control\n```\n\nInstall the agent skill from this repository:\n\n```\nnpx skills add kitlangton/terminal-control --skill terminal-control\n```\n\nThen ask your agent for terminal work in ordinary language:\n\n```\nUse terminal-control to open my TUI, press through the setup flow, and save a screenshot of the final screen.\nStart two terminal sessions: one running the dev server and one running the CLI. Drive the CLI until it connects, then show me both screens.\nRecord yourself using the terminal app, mark the important moments, and export a short MP4 demo.\n```\n\nThe skill teaches agents the safe workflow: start named sessions, wait for visible text, send exact input, inspect screens, save artifacts, record timelines, mark important moments, export videos, and stop sessions when finished.\n\n- Real PTY control for TUIs, shells, curses apps, OpenTUI apps, and long-running CLIs.\n- Named background sessions so an agent can keep multiple terminals alive and switch between them.\n- Visible-screen reads through\n`show`\n\n, not brittle scraping of scrollback or logs. - Exact keyboard and text input with\n`send`\n\n, including arrows, tabs, enter, escape, page keys, and`ctrl-a`\n\nthrough`ctrl-z`\n\n. - Explicit waits for rendered text before interacting.\n- Resizing to test responsive terminal layouts.\n- Evidence capture as PNG, SVG, text, JSON, or ANSI when requested.\n- Recording timelines with markers, edited MP4 export, and optional branded footers for demos and bug reports.\n- Local-only owner-protected session sockets and explicit warnings around sensitive terminal artifacts.\n\nRead a one-off terminal screen:\n\n```\ntermctrl show --cols 100 --rows 32 -- my-terminal-app\n```\n\nSave evidence:\n\n```\ntermctrl save --format png --format txt --out captures/home -- my-terminal-app\n```\n\nDrive a persistent TUI session:\n\n```\ntermctrl start demo --host opentui --cols 112 --rows 34 -- opencode\ntermctrl wait demo \"Ask anything\" --timeout 20000\ntermctrl send demo --pace-ms 35 'text:Write a terminal haiku.' enter\ntermctrl show demo\ntermctrl stop demo\n```\n\nRecord and export a video:\n\n```\ntermctrl start demo --host opentui --record captures/demo.termctrl -- opencode\ntermctrl wait demo \"Ask anything\"\ntermctrl mark demo ready\ntermctrl send demo --pace-ms 35 'text:Write a short terminal haiku. End with DONE.' enter\ntermctrl wait demo \"DONE\" --timeout 60000\ntermctrl mark demo after-answer\ntermctrl stop demo\ntermctrl video captures/demo.termctrl --edit captures/demo.json --out captures/demo.mp4\n```\n\nThe sections below explain each workflow in more detail.\n\nRequires Rust 1.93 or newer. Video export also requires `ffmpeg`\n\n.\n\n```\ncargo install terminal-control\ntermctrl --help\n```\n\nInstall the current repository head instead of the latest crate release:\n\n```\ncargo install --locked --git https://github.com/kitlangton/terminal-control terminal-control\n```\n\nRun a program in a PTY and print its visible terminal state:\n\n```\ntermctrl show --cols 100 --rows 32 -- my-terminal-app\n```\n\nShow is the routine observation command: it prints visible text to standard output and creates no files. Request a different stdout-readable representation explicitly:\n\n```\ntermctrl show --format json -- my-terminal-app\ntermctrl show --format svg -- my-terminal-app\n```\n\nWait for an application to mount, then interact before reading its screen:\n\n```\ntermctrl show --cols 100 --rows 32 --wait-for \"Commands\" \\\n  -s ctrl-p text:model enter -- my-terminal-app\n```\n\nOpenTUI applications such as OpenCode require the opt-in host handshake:\n\n```\ntermctrl show --host opentui --cols 112 --rows 34 \\\n  --wait-for \"/connect\" -- opencode\n```\n\nWrite only the artifact formats you request:\n\n```\ntermctrl save --format png --out captures/home.png -- my-terminal-app\ntermctrl save --format png --format txt --out captures/model -- my-terminal-app\n```\n\nThe second command writes `captures/model.png`\n\nand `captures/model.txt`\n\n. ANSI stream artifacts can contain sensitive terminal data and are only produced when explicitly requested with `--format ansi`\n\n.\n\nUse a named session when several observations or interactions should target the same running application:\n\n```\ntermctrl start demo --host opentui --cols 112 --rows 34 -- opencode\ntermctrl status demo\ntermctrl wait demo \"/connect\" --timeout 5000\ntermctrl show demo\ntermctrl send demo text:/connect enter\ntermctrl resize demo --cols 132 --rows 38\ntermctrl wait demo \"Connect a provider\" --timeout 5000\ntermctrl show demo\ntermctrl save demo --format png --out captures/provider.png\ntermctrl stop demo\n```\n\n`status`\n\nreports `running`\n\nor `exited`\n\n, the effective working directory, command, viewport, and recording path. An exited session retains its final screen for `show`\n\nuntil it is stopped. `list`\n\ndistinguishes unavailable stale sockets from incompatible older session protocols.\n\n`send`\n\naccepts `ctrl-a`\n\nthrough `ctrl-z`\n\n, keys such as `enter`\n\n, `escape`\n\n, arrows, `tab`\n\n, `shift-tab`\n\n, `backspace`\n\n, `delete`\n\n, `home`\n\n, `end`\n\n, `page-up`\n\n, and `page-down`\n\n, plus typed input as `text:<value>`\n\n. Use `ctrl-c`\n\nto interrupt work or pipe exact prompt bytes with `--stdin`\n\n:\n\n```\nprintf '%s' 'Summarize the active view.' | termctrl send demo --stdin\n```\n\n`resize`\n\ncontrols the terminal viewport and records geometry changes in `.termctrl`\n\ntimelines when recording is enabled. A session whose retained ANSI transcript has already been truncated cannot be resized because its current screen cannot be replayed at a new size safely.\n\nFor normal-screen tools and long-running log processes, inspect retained scrollback directly:\n\n```\ntermctrl logs demo\ntermctrl logs demo --ansi > captures/demo-output.ansi\n```\n\nFull-screen alternate-screen TUIs do not provide useful terminal logs; read their visible screen with `show`\n\nor retain a recording timeline instead. Status exposes `logs_truncated`\n\nafter raw retained ANSI reaches `--max-bytes`\n\n; the session continues running and retains its most recent transcript bytes.\n\nRestart a single named owner safely when deploying updated code:\n\n```\ntermctrl restart demo\n```\n\n`restart NAME`\n\nreuses the prior command, effective working directory, viewport, host profile, color policy, and recording path by default. Supply options or a replacement command only when deliberately changing the launch.\n\nRecord a session timeline and replay it as an MP4:\n\n```\ntermctrl start demo --record captures/demo.termctrl \\\n  --host opentui --cols 112 --rows 34 -- opencode\ntermctrl wait demo \"Ask anything\"\ntermctrl mark demo before-prompt\ntermctrl send demo --pace-ms 35 'text:Write a short terminal haiku. End with the uppercase form of done.' enter\ntermctrl wait demo \"DONE\" --timeout 60000\ntermctrl mark demo after-answer\ntermctrl save demo --format png --out captures/answer.png\ntermctrl stop demo\n\ntermctrl markers captures/demo.termctrl\ntermctrl show --recording captures/demo.termctrl --at-marker after-answer\ntermctrl video captures/demo.termctrl --edit captures/demo.json --footer --tail-ms 0 --hide-cursor --out captures/demo.mp4\n```\n\nThe marker-based edit plan is explicit and deterministic. `speed`\n\naccelerates or slows the real recorded time inside that clip. `caption`\n\nadds a visible annotation row. `hold_ms`\n\nis optional and creates a deliberate still frame at the end of a clip; omit it when you do not want artificial freezes.\n\n```\n{\n  \"clips\": [\n    {\n      \"from\": \"before-prompt\",\n      \"to\": \"after-answer\",\n      \"speed\": 4,\n      \"caption\": \"The agent answers inside the live terminal UI\"\n    }\n  ]\n}\n```\n\nWithout `--edit`\n\n, video export preserves the observed recording timing. Edit plans are preferable for polished demos because they select intentional marker ranges and can accelerate animated spinner spans without relying on visual-idle heuristics. Keep speeds low enough for important terminal text to remain readable, and add `hold_ms`\n\nor leave a `--tail-ms`\n\nhold when the final screen is the point of the demo. Identical rendered screens are rasterized once and reused during export. Video export trims startup frames before non-whitespace text by default while still preserving recordings that only paint terminal backgrounds; use `--include-startup`\n\nto keep all startup frames. `video`\n\nholds the final frame for one second by default so short recordings do not end abruptly; pass `--tail-ms 0`\n\nfor a strict no-holds cut. Pass `--footer`\n\nto put the clip caption, elapsed timecode, and `TERMINAL CONTROL`\n\nbranding in a bottom footer instead of rendering captions as inline annotation rows.\n\nUse `termctrl markers captures/demo.termctrl`\n\nto audit available marker names and timestamps. Use `termctrl show --recording captures/demo.termctrl --at-marker after-answer`\n\nor `--at-ms 1234`\n\nto inspect exact screens while tuning an edit plan.\n\nRecordings are JSON Lines files containing terminal output, client input, and automatic host input; they can include prompts or secrets. Treat them as sensitive artifacts.\n\nRepeat `--format`\n\nto export only what you need:\n\n```\ntermctrl save --format png --format txt --out captures/home -- my-terminal-app\n```\n\nRead a current visible screen directly for agent inspection, or select JSON/ANSI/SVG explicitly:\n\n```\ntermctrl show demo\ntermctrl show demo --format json\n```\n\nFor commands whose useful output is piped, use `--pipe`\n\n. Pipe reads force color by default; pass `--color never`\n\nfor plain output:\n\n```\ntermctrl save --pipe --format png --format txt --cols 100 --rows 16 \\\n  --out captures/log -- my-command\n```\n\nOne-off `show`\n\nand `save`\n\noperations own disposable command processes: once the visible screen is read or saved, the launched process tree is terminated. Use `start`\n\nfor long-running applications.\n\nRender an existing ANSI/VT terminal stream without launching a process. An `.ansi`\n\nfile is a conventionally named byte stream of terminal output and escape sequences, not a separate container format:\n\n```\nprintf '\\033[44;97m terminal output \\033[0m\\n' | termctrl show --input -\nprintf '\\033[44;97m terminal output \\033[0m\\n' | termctrl save --input - --format png --out captures/stdin.png\n```\n\nThe crate also exposes the shot engine, live sessions, and artifact model to Rust callers. The CLI is built on the same `terminal_control::shot`\n\n, `terminal_control::session`\n\n, `terminal_control::frame`\n\n, `terminal_control::render`\n\n, and `terminal_control::recording`\n\nmodules:\n\n``` js\nlet shot = terminal_control::shot::from_ansi(b\"\\x1b[32mready\\x1b[0m\".to_vec(), 1, 20, 1024)?;\nassert_eq!(shot.frame.text(), \"ready\");\nlet svg = terminal_control::render::svg(&shot.frame, &terminal_control::render::Options::default());\n```\n\nA library session keeps one PTY-backed application in process for fast test interaction without repeatedly invoking the CLI:\n\n``` js\nuse std::time::Duration;\n\nlet mut session = terminal_control::session::Session::start(\n    &[\"my-terminal-app\".to_owned()],\n    None,\n    None,\n    &terminal_control::shot::Options::default(),\n)?;\nsession.wait_for_text(\"Ready\", Duration::from_secs(5))?;\nlet status = session.status()?;\nsession.send(b\"help\\r\")?;\nsession.wait_for_idle(Duration::from_millis(250), Duration::from_secs(5))?;\nlet capture = session.capture(Duration::from_millis(250), Duration::from_secs(5))?;\nlet shot = capture.shot;\nlet exit = session.wait_for_exit(Duration::from_secs(5))?;\nsession.stop()?;\n```\n\nStructured output is versioned for external tools:\n\n- A\n`save --format json`\n\ncapture is a`Frame`\n\nobject with`version: 1`\n\n, described by`schemas/frame-v1.schema.json`\n\n. - A\n`.termctrl`\n\nrecording is JSON Lines: its first line is a versioned header and subsequent lines are timed output, input, or resize entries, each described by`schemas/recording-entry-v1.schema.json`\n\n. - Recording byte arrays contain the original terminal or input bytes as integers from\n`0`\n\nto`255`\n\n; recordings can contain sensitive text or input.\n\n`session::Session`\n\nis the embedded lifecycle interface; the flat named-session CLI commands and the external driver are adapters over the same implementation.\n\nExternal agent tooling can keep multiple embedded sessions alive through a versioned JSON Lines protocol over stdin/stdout:\n\n```\ntermctrl driver\n```\n\nThe driver writes a `hello`\n\nmessage with protocol and Terminal Control versions, then accepts typed operations including `launch`\n\n, `status`\n\n, `send`\n\n, `waitForText`\n\n, `waitForIdle`\n\n, `waitForExit`\n\n, `capture`\n\n, `logs`\n\n, `recording`\n\n, `resize`\n\n, `stop`\n\n, and `shutdown`\n\n. It is intended for clients such as a TypeScript TUI test or agent-control library, while the shell-facing flat commands remain convenient for individual workflows.\n\n```\n{\"type\":\"hello\",\"protocolVersion\":1,\"terminalControlVersion\":\"<installed-version>\"}\n{\"id\":1,\"method\":\"launch\",\"sessionId\":\"app\",\"params\":{\"command\":[\"my-terminal-app\"],\"cols\":100,\"rows\":30,\"inheritEnv\":false,\"env\":{\"TERM\":\"xterm-256color\"}}}\n{\"id\":2,\"method\":\"waitForText\",\"sessionId\":\"app\",\"params\":{\"text\":\"Ready\",\"timeoutMs\":5000}}\n{\"id\":3,\"method\":\"send\",\"sessionId\":\"app\",\"params\":{\"input\":[{\"type\":\"text\",\"value\":\"help\"},{\"type\":\"key\",\"value\":\"enter\"}]}}\n{\"id\":4,\"method\":\"capture\",\"sessionId\":\"app\",\"params\":{\"settleMs\":250,\"deadlineMs\":5000}}\n```\n\nA driver `capture`\n\nresponse contains a structured visible frame, derived `text`\n\n, and a completion `reason`\n\n: `idle`\n\n, `deadline`\n\n, `exited`\n\n, or `outputclosed`\n\n. Raw ANSI is omitted by default; request `includeAnsi: true`\n\nfor retained transcript bytes or `includeSvg: true`\n\nfor rendered visual evidence. A test client should normally require `idle`\n\nor `exited`\n\ninstead of accepting a deadline fallback as a stable snapshot. Driver input is intentionally exact: text, raw bytes, known key values, and single-letter control input are supported without claiming unimplemented key chords.\n\n`@kitlangton/terminal-control`\n\nexposes the driver as isolated typed test sessions. It deliberately separates the visible screen from readable retained logs and the exact ANSI/VT transcript. Its npm distribution includes an optional native package for macOS or GNU/Linux on arm64 or x64, so consumers do not need a Rust toolchain or separate `termctrl`\n\ninstallation.\n\nAfter the initial npm publication:\n\n```\nbun add -d @kitlangton/terminal-control vitest\njs\nimport { TerminalControl } from \"@kitlangton/terminal-control\"\n\nawait using terminal = await TerminalControl.make({\n  artifacts: {\n    directory: \".termctrl-artifacts\",\n    onFailure: true,\n    includeTranscript: false,\n    includeRecording: true,\n  },\n})\nawait using session = await terminal.launch({\n  command: [\"/absolute/path/to/my-terminal-app\"],\n  viewport: { cols: 100, rows: 30 },\n  inheritEnv: false,\n  env: { TERM: \"xterm-256color\", HOME: \"/tmp/test-home\" },\n  record: \"on-failure\",\n})\n\nawait session.screen.waitForText(/Ready/)\nawait session.keyboard.type(\"help\")\nawait session.keyboard.press(\"Enter\")\n\nconst text = await session.screen.text()\nconst frame = await session.screen.frame()\nconst logs = await session.logs.text()\nconst transcript = await session.transcript.ansi()\n\nexpect(text).toMatchSnapshot()\nexpect(frame).toMatchSnapshot()\n\nconst exit = await session.waitForExit({ timeoutMs: 5_000 })\nexpect(exit).toMatchObject({ reason: \"exited\", exit: { code: 0 } })\n```\n\nWhen working directly from this repository before installing the npm artifacts, pass `binaryPath: \"./target/release/termctrl\"`\n\nor set `TERMCTRL_BINARY`\n\n.\n\n`session.screen.text()`\n\nand `session.screen.frame()`\n\nwait for a settled capture and reject deadline or output-closed fallback by default. A test that intentionally needs an intermediate frame can request it explicitly:\n\n``` js\nconst capture = await session.screen.capture({ allowIncomplete: true })\nconsole.log(capture.reason, capture.text, capture.frame)\n```\n\nThis makes ordinary text or frame snapshots stable by default while retaining explicit access to live, incomplete terminal state.\n\nKeyboard presses are typed as the sequences Terminal Control encodes exactly, such as `\"Enter\"`\n\n, `\"ArrowDown\"`\n\n, or `\"Control+C\"`\n\n. Use `session.keyboard.write(bytes)`\n\nwhen a test deliberately needs exact terminal bytes outside that supported key set.\n\nVitest users can add a screen-aware assertion that writes configured artifacts on failure. Standard `toMatchSnapshot()`\n\nand `toMatchInlineSnapshot()`\n\nremain the simplest snapshot format because visible text is reviewable in source control:\n\n``` js\nimport { expect } from \"vitest\"\nimport { extendTerminalControlMatchers } from \"@kitlangton/terminal-control/vitest\"\n\nextendTerminalControlMatchers(expect)\n\nawait expect(session).toHaveScreenText(\"Ready\\n\\nChoose an option\")\nawait expect(session.screen.text()).resolves.toMatchInlineSnapshot()\n```\n\n`session.writeArtifacts(name)`\n\nand failing `toHaveScreenText(...)`\n\nassertions can write `screen.txt`\n\n, `screen.json`\n\n, `screen.svg`\n\n, `logs.txt`\n\n, and `metadata.json`\n\n. Environment variable values are redacted in metadata. `transcript.ansi`\n\nand `recording.termctrl`\n\nare opt-in because terminal streams and typed input may contain secrets. Wrap ordinary snapshot assertions when evidence should be saved on any thrown assertion:\n\n``` js\nawait session.withArtifactsOnFailure(\"settings-snapshot\", async () => {\n  await expect(session.screen.text()).resolves.toMatchSnapshot()\n})\n```\n\nEnable a recording with `record: true`\n\nor `record: \"on-failure\"`\n\n; a test may explicitly save it before disposing the session:\n\n```\nawait session.resize({ cols: 120, rows: 40 })\nawait session.saveRecording(\"artifacts/navigation.termctrl\")\n```\n\nThe npm workspace publishes `@kitlangton/terminal-control`\n\nwith fixed-version platform packages: `@kitlangton/terminal-control-darwin-arm64`\n\n, `@kitlangton/terminal-control-darwin-x64`\n\n, `@kitlangton/terminal-control-linux-arm64-gnu`\n\n, and `@kitlangton/terminal-control-linux-x64-gnu`\n\n. The client is compiled to ESM JavaScript with declarations; each native package receives the release Rust executable during the `npm release`\n\nworkflow.\n\nFor subsequent user-facing npm changes, create a Changeset with `bun run changeset`\n\n, commit the generated release metadata, run `bun run version-packages`\n\n, refresh `bun.lock`\n\n, and commit the versioned package metadata before running the workflow. Run the workflow with `publish: false`\n\nto assemble packages only, or `publish: true`\n\nto publish assembled tarballs after its clean Bun and Node/Vitest consumer validation passes.\n\nThe publish job is prepared for npm trusted publishing through GitHub Actions OIDC. In npm package settings, configure `kitlangton/terminal-control`\n\nand workflow `npm-release.yml`\n\nas the trusted publisher for the client and each platform package before using `publish: true`\n\n.\n\n- Persistent sessions use owner-only local Unix sockets and are supported on macOS and Linux.\n`--host opentui`\n\nanswers startup probes needed by current OpenTUI applications; Kitty graphics are reported unavailable because the current renderer does not decode image payloads.- The current renderer uses a pure-Rust\n`vt100`\n\nterminal backend and exports PNG, SVG, JSON, text, and raw ANSI stream artifacts. - Run\n`termctrl <command> --help`\n\nfor dimensions, timing, color, rendering, and output options.", "url": "https://wpnews.pro/news/terminal-control-control-inspect-test-and-capture-terminal-sessions", "canonical_source": "https://github.com/anomalyco/terminal-control", "published_at": "2026-07-11 02:03:22+00:00", "updated_at": "2026-07-11 02:35:32.349477+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Terminal Control", "Kit Langton", "Rust", "OpenCode", "OpenTUI", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/terminal-control-control-inspect-test-and-capture-terminal-sessions", "markdown": "https://wpnews.pro/news/terminal-control-control-inspect-test-and-capture-terminal-sessions.md", "text": "https://wpnews.pro/news/terminal-control-control-inspect-test-and-capture-terminal-sessions.txt", "jsonld": "https://wpnews.pro/news/terminal-control-control-inspect-test-and-capture-terminal-sessions.jsonld"}}