{"_id":"@akuederle/termdem","_rev":"3-02f85cad3b67a9a8d4eab08464e9257d","name":"@akuederle/termdem","dist-tags":{"latest":"0.5.0"},"versions":{"0.0.1":{"name":"@akuederle/termdem","version":"0.0.1","keywords":["demo","playwright","react","recording","terminal","typescript"],"author":{"name":"Arne Kuederle","email":"a.kuederle@gmail.com"},"license":"MIT","_id":"@akuederle/termdem@0.0.1","maintainers":[{"name":"akuederle","email":"a.kuederle@gmail.com"}],"homepage":"https://github.com/AKuederle/termdem#readme","bugs":{"url":"https://github.com/AKuederle/termdem/issues"},"bin":{"termdem":"dist/cli.mjs"},"dist":{"shasum":"7ce19031fea67e9d9544db0a3aa31067d42c5ffe","tarball":"https://registry.npmjs.org/@akuederle/termdem/-/termdem-0.0.1.tgz","fileCount":19,"integrity":"sha512-YQ7VLTLw4CV/b6ihKJeTw9FmDcuvpXY0z2yARLE6GikTk/KjlxJTloKYL3cbHGmRy9rg67Nm47Wtdtlo4J6OFA==","signatures":[{"sig":"MEYCIQCRez99MyvJicYhpKdsyMCjuHNa7vRqJ/yneQjHyjU8FgIhALagSq772Y/Y/xvxQhF9lhyZ9J3P8Q2HH7gwX3btx4pK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":133379},"type":"module","_from":"file:akuederle-termdem-0.0.1.tgz","types":"./dist/index.d.mts","engines":{"node":">=22.12.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./cli":{"types":"./dist/cli.d.mts","default":"./dist/cli.mjs"},"./protocol":{"types":"./dist/protocol.d.mts","default":"./dist/protocol.mjs"},"./package.json":"./package.json","./preview-client":{"types":"./dist/preview-client.d.mts","default":"./dist/preview-client.mjs"},"./preview-server":{"types":"./dist/preview-server.d.mts","default":"./dist/preview-server.mjs"}},"scripts":{"dev":"vp pack --watch","test":"vp test run --tagsFilter '!smoke'","build":"vp pack","check":"vp check"},"_npmUser":{"name":"akuederle","email":"a.kuederle@gmail.com"},"_resolved":"/tmp/175f2569bc6605ea9bb9cb75ae1b18b4/akuederle-termdem-0.0.1.tgz","_integrity":"sha512-YQ7VLTLw4CV/b6ihKJeTw9FmDcuvpXY0z2yARLE6GikTk/KjlxJTloKYL3cbHGmRy9rg67Nm47Wtdtlo4J6OFA==","repository":{"url":"git+https://github.com/AKuederle/termdem.git","type":"git","directory":"packages/termdem"},"_npmVersion":"11.12.1","description":"Create complex terminal demo videos using JavaScript and TypeScript.","directories":{},"_nodeVersion":"24.15.0","dependencies":{"ws":"^8.20.0","vite":"npm:@voidzero-dev/vite-plus-core@latest","react":"^19.2.5","node-pty":"^1.1.0","react-dom":"^19.2.5","playwright":"^1.59.1","tailwindcss":"^4.2.4","@wterm/react":"^0.1.9","@tailwindcss/vite":"^4.2.4","@vitejs/plugin-react":"^6.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"bumpp":"^11.0.1","@types/ws":"^8.18.1","vite-plus":"^0.1.14","typescript":"^6.0.2","@types/node":"^25.5.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@typescript/native-preview":"7.0.0-dev.20260328.1"},"_npmOperationalInternal":{"tmp":"tmp/termdem_0.0.1_1777391103081_0.30384807334623143","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@akuederle/termdem","version":"0.4.0","keywords":["demo","playwright","react","recording","terminal","typescript"],"author":{"name":"Arne Kuederle","email":"a.kuederle@gmail.com"},"license":"MIT","_id":"@akuederle/termdem@0.4.0","maintainers":[{"name":"akuederle","email":"a.kuederle@gmail.com"}],"homepage":"https://github.com/AKuederle/termdem#readme","bugs":{"url":"https://github.com/AKuederle/termdem/issues"},"bin":{"termdem":"dist/cli.mjs"},"dist":{"shasum":"fb691dbc39c5b4d702fa63cff954dd8df775fbfe","tarball":"https://registry.npmjs.org/@akuederle/termdem/-/termdem-0.4.0.tgz","fileCount":19,"integrity":"sha512-rQvAR0Dm+AnubjBPJ6qu79t5Ty7/suQHFFrjsnMjKQwTlWWenFE+Nw3a0nthnFK9pXGk/3TZ/wmix05BicKqzA==","signatures":[{"sig":"MEYCIQCEAdekzTBjDqkdgKIzyzSL20zUHy9ekE95a40Kg2n3CgIhAME+Fq71mXjkrFiPje6F8GD7YQgYpiYxUvHZuldPtnC8","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@akuederle%2ftermdem@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":158135},"type":"module","_from":"file:/tmp/akuederle-termdem-0.4.0.tgz","types":"./dist/index.d.mts","engines":{"node":">=22.12.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./cli":{"types":"./dist/cli.d.mts","default":"./dist/cli.mjs"},"./protocol":{"types":"./dist/protocol.d.mts","default":"./dist/protocol.mjs"},"./package.json":"./package.json","./preview-client":{"types":"./dist/preview-client.d.mts","default":"./dist/preview-client.mjs"},"./preview-server":{"types":"./dist/preview-server.d.mts","default":"./dist/preview-server.mjs"}},"scripts":{"dev":"vp pack --watch","test":"vp test run --tagsFilter '!smoke'","build":"vp pack","check":"vp check"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10db7e3e-d498-4698-aa7d-8decc982dcd0"}},"_resolved":"/tmp/akuederle-termdem-0.4.0.tgz","_integrity":"sha512-rQvAR0Dm+AnubjBPJ6qu79t5Ty7/suQHFFrjsnMjKQwTlWWenFE+Nw3a0nthnFK9pXGk/3TZ/wmix05BicKqzA==","repository":{"url":"git+https://github.com/AKuederle/termdem.git","type":"git","directory":"packages/termdem"},"_npmVersion":"11.11.0","description":"Create complex terminal demo videos using JavaScript and TypeScript.","directories":{},"_nodeVersion":"24.14.1","dependencies":{"ws":"^8.20.0","vite":"^8.0.9","react":"^19.2.5","node-pty":"^1.1.0","react-dom":"^19.2.5","@wterm/dom":"^0.1.9","playwright":"^1.59.1","tailwindcss":"^4.2.4","@wterm/react":"^0.1.9","@tailwindcss/vite":"^4.2.4","@vitejs/plugin-react":"^6.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"bumpp":"^11.0.1","@types/ws":"^8.18.1","vite-plus":"^0.1.14","typescript":"^6.0.2","@types/node":"^25.5.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@typescript/native-preview":"7.0.0-dev.20260328.1"},"_npmOperationalInternal":{"tmp":"tmp/termdem_0.4.0_1777451255044_0.2152523885311719","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@akuederle/termdem","version":"0.5.0","description":"Create complex terminal demo videos using JavaScript and TypeScript.","keywords":["demo","playwright","react","recording","terminal","typescript"],"homepage":"https://github.com/AKuederle/termdem#readme","bugs":{"url":"https://github.com/AKuederle/termdem/issues"},"license":"MIT","author":{"name":"Arne Kuederle","email":"a.kuederle@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/AKuederle/termdem.git","directory":"packages/termdem"},"bin":{"termdem":"dist/cli.mjs"},"type":"module","types":"./dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./cli":{"types":"./dist/cli.d.mts","default":"./dist/cli.mjs"},"./preview-client":{"types":"./dist/preview-client.d.mts","default":"./dist/preview-client.mjs"},"./preview-server":{"types":"./dist/preview-server.d.mts","default":"./dist/preview-server.mjs"},"./protocol":{"types":"./dist/protocol.d.mts","default":"./dist/protocol.mjs"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"dependencies":{"@tailwindcss/vite":"^4.2.4","@vitejs/plugin-react":"^6.0.1","@wterm/dom":"^0.1.9","@wterm/react":"^0.1.9","node-pty":"^1.1.0","playwright":"^1.59.1","react":"^19.2.5","react-dom":"^19.2.5","tailwindcss":"^4.2.4","vite":"^8.0.9","ws":"^8.20.0"},"devDependencies":{"@types/node":"^25.5.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@types/ws":"^8.18.1","@typescript/native-preview":"7.0.0-dev.20260328.1","bumpp":"^11.0.1","typescript":"^6.0.2","vite-plus":"^0.1.14"},"engines":{"node":">=22.12.0"},"scripts":{"build":"vp pack","dev":"vp pack --watch","test":"vp test run --tagsFilter '!smoke'","check":"vp check"},"_id":"@akuederle/termdem@0.5.0","_integrity":"sha512-/kOOGAobuiACLXhcXvJQb+swO73jJkco/znnhsJCnovu1D0ouU9EiU0RH9EdEybRjIpu/l82+eydIAXGAub0Ng==","_resolved":"/tmp/akuederle-termdem-0.5.0.tgz","_from":"file:/tmp/akuederle-termdem-0.5.0.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-/kOOGAobuiACLXhcXvJQb+swO73jJkco/znnhsJCnovu1D0ouU9EiU0RH9EdEybRjIpu/l82+eydIAXGAub0Ng==","shasum":"5e4521a82a299d707beb3fda21e7d0ae2c9e8216","tarball":"https://registry.npmjs.org/@akuederle/termdem/-/termdem-0.5.0.tgz","fileCount":19,"unpackedSize":161296,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@akuederle%2ftermdem@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDQayiRixdsJxISz3nvn/2AN/QwrzDLvnYJ3ifbBv8TjAIgGxZarMPDA3S0x8PWlnV/UzMmlcBSQcbgW+IHI1uDC8g="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10db7e3e-d498-4698-aa7d-8decc982dcd0"}},"directories":{},"maintainers":[{"name":"akuederle","email":"a.kuederle@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/termdem_0.5.0_1784894066274_0.5802505303957912"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-28T15:45:03.001Z","modified":"2026-07-24T11:54:26.673Z","0.0.1":"2026-04-28T15:45:03.218Z","0.4.0":"2026-04-29T08:27:35.185Z","0.5.0":"2026-07-24T11:54:26.412Z"},"bugs":{"url":"https://github.com/AKuederle/termdem/issues"},"author":{"name":"Arne Kuederle","email":"a.kuederle@gmail.com"},"license":"MIT","homepage":"https://github.com/AKuederle/termdem#readme","keywords":["demo","playwright","react","recording","terminal","typescript"],"repository":{"type":"git","url":"git+https://github.com/AKuederle/termdem.git","directory":"packages/termdem"},"description":"Create complex terminal demo videos using JavaScript and TypeScript.","maintainers":[{"name":"akuederle","email":"a.kuederle@gmail.com"}],"readme":"# Termdem\n\nCreate complex terminal demo videos using JS/TS.\n\n## Features\n\n- Script multiple terminal panes in one recording.\n- Style each pane with regular React and Tailwind.\n- Simulate realistic typing, special keys, and interactive terminal apps.\n- Run hidden setup, teardown, and background checks.\n- Capture command output and use it to drive later steps.\n\n## Install\n\n> [!WARNING]\n> Termdem is currently supported on macOS and Linux only.\n\n```\nnpm install @akuederle/termdem\n```\n\nInstall the Playwright browser binaries before recording demos.\n\n```\nnpx playwright install chromium\n```\n\nWebM recording works out of the box after the Chromium install.\nInstall [ffmpeg](https://ffmpeg.org/) if you want to record to non-WebM formats such as MP4.\n\n## Usage\n\nTermdem works best when the terminal app you want to demo already lives in a JavaScript or TypeScript project.\nCreate one `.tsx` file per demo, export the `demo` returned by `createTerminalDemo`, and export a `render` function that lays out the terminal panes.\n\n1. Add `@akuederle/termdem` to your dev dependencies.\n2. Create a folder for your demos.\n3. Add a `.tsx` demo file.\n4. Configure panes, write the script, and render the scene.\n5. Preview the demo in your browser with `npx termdem preview ./demos/demo.tsx`.\n6. Record the demo to a video file with `npx termdem record ./demos/demo.tsx ./demo.webm`.\n\n> [!WARNING]\n> Demo files are loaded in two places: the preview server runs `setup`, `script`, and `teardown`, while the browser imports the same module to render the panes.\n> Importing backend-only modules such as `node:fs`, `node:path`, or `node:dgram` is fine when they are only used from server-side callbacks.\n> Do not execute backend-only code at module top level, because the browser render path evaluates top-level code too.\n> Keep filesystem, socket, process, and other Node-only work inside `TmpDir` setup, demo `setup`, `script`, `teardown`, or functions called only from those callbacks.\n\n### Create a Scene\n\nPanes define the terminals that the script can control and the render function can display.\nThe render function receives one React component per pane, keyed by pane name.\n[Tailwind v4](https://tailwindcss.com/) is available in demo files, so regular utility classes are enough for most layouts.\n\n```tsx\nimport { TmpDir, createTerminalDemo, type TerminalPaneComponents } from \"@akuederle/termdem\";\n\nconst workspace = new TmpDir();\n\nexport const demo = createTerminalDemo({\n  panes: [\n    { name: \"server\", pwd: workspace },\n    { name: \"client\", pwd: workspace },\n  ],\n  script: async () => {},\n  settings: {\n    size: { width: 1920, height: 1080 },\n  },\n});\n\nexport function render(panes: TerminalPaneComponents<typeof demo>) {\n  return (\n    <main className=\"grid h-full w-full min-h-0 grid-cols-2 grid-rows-1 gap-px bg-[#333] p-px\">\n      <panes.server className=\"min-h-0 min-w-0\" />\n      <panes.client className=\"min-h-0 min-w-0\" />\n    </main>\n  );\n}\n```\n\n### Create a Script\n\nThe script is an async function that receives an `api` object.\nUse `api.pane(name)` to select a pane and then drive it with `exec`, `sendLine`, `type`, and `press`.\nUse normal JavaScript between terminal actions whenever you need to parse output or decide the next command.\n\n```ts\nimport { TmpDir, createTerminalDemo, quoteShellArg, typedString } from \"@akuederle/termdem\";\n\nconst workspace = new TmpDir();\n\nexport const demo = createTerminalDemo({\n  panes: [\n    { name: \"server\", pwd: workspace },\n    { name: \"client\", pwd: workspace },\n  ],\n  script: async (api) => {\n    const server = api.pane(\"server\");\n    const client = api.pane(\"client\");\n\n    const setup = await server.exec(\"node scripts/server.mjs setup\");\n    const url = setup.lines.find((line) => line.startsWith(\"URL=\"))?.slice(\"URL=\".length);\n\n    if (!url) {\n      throw new Error(\"Server did not print a URL\");\n    }\n\n    await server.sendLine([\n      \"node scripts/server.mjs listen \",\n      typedString(quoteShellArg(url), { typeDelayMs: 0 }),\n    ]);\n    await api.waitFor(\"server ready\", async () => {\n      const result = await api.sidecar.exec(\"curl\", [\"-fsS\", url], {\n        reject: false,\n        timeoutMs: 1000,\n      });\n\n      return result.exitCode === 0;\n    });\n\n    await client.exec([\n      \"node scripts/client.mjs \",\n      typedString(quoteShellArg(url), { typeDelayMs: 0 }),\n    ]);\n  },\n});\n```\n\nThe pane API is for visible terminal work.\nUse `exec` for commands that should finish, `sendLine` for long-running processes, `type` for raw text input, and `press` for single keys or key combinations such as `keys.CTRL_C` and `keys.ESC`.\n\nThe top-level API is for orchestration.\nUse `wait` for fixed delays, `waitFor` for readiness checks, and `sidecar.exec` for hidden sidecar subprocesses that should not appear in the terminal.\n\nSee the full [socket CLI example](./examples/socket-cli/demo.tsx) for a multi-pane server/client demo.\nSee the full [Git/Vim example](./examples/git-vim/demo.tsx) for an interactive full-screen terminal app demo.\n\n## Tips\n\n### Highlight the active terminal\n\nEvery pane component receives a `data-termdem-current` attribute while it is the pane currently controlled by the script.\nUse Tailwind's data selector variants to make that pane stand out without adding state to your render function.\n\n```tsx\n<panes.server className=\"transition data-[termdem-current]:ring-2 data-[termdem-current]:ring-cyan-300 data-[termdem-current]:brightness-110\" />\n<panes.client className=\"transition data-[termdem-current]:ring-2 data-[termdem-current]:ring-cyan-300 data-[termdem-current]:brightness-110\" />\n```\n\n### Configure working dirs\n\nUse `Dir` when the demo should run inside an existing directory, and `TmpDir` when the demo should get a fresh disposable workspace.\nMultiple panes can share the same workspace object; for most demos, prefer `TmpDir` and seed it in `setup` so every recording starts from a clean state.\n\n```ts\nconst workspace = new TmpDir({\n  setup: async (path) => {\n    await fs.writeFile(`${path}/README.md`, \"# Demo\\n\");\n  },\n});\n\npanes: [\n  { name: \"server\", pwd: workspace },\n  { name: \"client\", pwd: workspace },\n];\n```\n\n### Configure pane prompts\n\nSet `prompt` independently of a pane's `name` when its title should describe the pane's role while its\nshell prompt identifies an environment, peer, or working directory.\nThe value uses Bash [`PS1` syntax](https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html)\nand is expanded again before every prompt.\n\n```ts\npanes: [\n  { name: \"files\", prompt: \"($USER@\\\\h \\\\w) \\\\$ \", pwd: workspace },\n  { name: \"watch\", prompt: \"(client2) \\\\$ \", pwd: workspace },\n];\n```\n\nPrompt strings support Bash escapes such as `\\u`, `\\h`, `\\w`, `\\W`, and `\\$`, along with parameter\nexpansion (`$NAME` and `${NAME}`), arithmetic expansion (`$((...))`), and command substitution (`$(...)`\nor backticks).\nCommand substitutions execute each time Bash displays the prompt, so only use trusted commands.\n\nThe prompt can read:\n\n- All environment variables inherited from the process running Termdem.\n- Bash variables such as `PWD`, `UID`, `EUID`, `HOSTNAME`, `BASH_VERSION`, and `SHLVL`.\n- Variables set by earlier commands in that pane; for example, `export PEER=client2` updates `$PEER` in\n  subsequent prompts.\n- Termdem's terminal overrides: `TERM=xterm-256color`, `PAGER=cat`, `GIT_PAGER=cat`, `GH_PAGER=cat`,\n  `DELTA_PAGER=cat`, `MANPAGER=cat`, and `LESS=FRX`.\n\nTermdem reserves `PS1` and `PROMPT_COMMAND` for prompt rendering and command-completion detection.\nCustomize the pane through its `prompt` definition instead of assigning either variable from pane commands.\n\n### Run Setup and Teardown\n\nDirectory `setup` and `teardown` prepare files before panes start, while demo-level `setup` and `teardown` use the same API as `script`.\nDemo-level setup runs before recording starts, so any visible setup output is already present in the first recorded frame.\nIt can return data that is passed as the second `script` argument.\nDemo-level setup and teardown pane commands are visible by default and run with instant typing by default.\nUse `pane.hidden.*` for setup or teardown commands that should run in the pane shell without appearing in the terminal.\n\n```ts\nexport const demo = createTerminalDemo({\n  setup: async (api) => {\n    const result = await api.pane(\"main\").hidden.exec(\"node scripts/prepare.mjs\");\n    return { token: result.text };\n  },\n  script: async (api, setupData) => {\n    await api.pane(\"main\").exec(`node cli.mjs login ${quoteShellArg(setupData.token)}`);\n  },\n  teardown: async (api) => {\n    await api.sidecar.exec(\"node\", [\"scripts/cleanup.mjs\"], { reject: false });\n  },\n});\n```\n\n### Full-screen Terminal Apps/Long running processes.\n\nFor editors and other full-screen terminal apps, start the process with `sendLine()` so the script can keep sending keystrokes while the app remains open.\nUse `type()` for raw input, `press(keys.ENTER)` for supported special keys, and short `wait()` calls when the app needs a moment to redraw.\n\n```ts\nawait pane.sendLine(\"vim README.md\");\nawait api.wait(300);\nawait pane.type(\"i# Demo notes\\n\");\nawait pane.type(keys.ESC);\nawait pane.type(\":wq\");\nawait pane.press(keys.ENTER);\n```\n\n### Typing speed\n\nTyping speed is controlled by `typeDelayMs`, either globally in `settings` or per command/input call.\nSetup and teardown default to instant input, but visible interactive apps sometimes need a small delay because terminals can drop or reorder keypresses that arrive too quickly.\nUse `typedString()` when one part of an input should use a different speed, such as typing a command prefix and pasting a generated argument.\nRaw string segments use the command/input `typeDelayMs`, then the demo-level `settings.typeDelayMs`, then the built-in default.\n`typedString()` segments inherit that same delay unless they provide their own `typeDelayMs`.\nSegments are concatenated exactly, so include spaces in the strings where the final terminal input needs spaces.\n\n```ts\nsettings: {\n  typeDelayMs: typingDelays.WPM_120,\n},\n\n...\n// This will be typed at WPM_120 from the global settings\nawait pane.exec(\"ls .\");\n// This will be instant. All characters send as one\nawait pane.exec(\"npm test\", { typeDelayMs: 0 });\n// This will be typed character by character at WPM_60\nawait pane.type(\"iTyped into Vim\\n\", { typeDelayMs: typingDelays.WPM_60 });\n// The first part will be typed at WPM_80 and then the second part instant\nawait pane.exec(\n  [\"node scripts/client.mjs \", typedString(quoteShellArg(url), { typeDelayMs: 0 })],\n  { typeDelayMs: typingDelays.WPM_80 },\n);\n```\n\n### Parsing command outputs\n\n`pane.exec()` waits for the command to finish and returns cleaned output.\nUse `text` for the full stripped output, or `lines` when you need to pick a value for a later command.\n\n```ts\nconst result = await api.pane(\"server\").exec(\"node scripts/server.mjs setup\");\nconst url = result.lines.find((line) => line.startsWith(\"CHAT_URL=\"))?.slice(\"CHAT_URL=\".length);\n\nawait api.pane(\"client\").exec(`node client.mjs ${quoteShellArg(url)}`);\n```\n\n### Reading long-running terminal screens\n\nUse `pane.screen()` after starting a long-running command with `sendLine()` when you need the terminal's current rendered buffer.\nThis works for output that redraws in place, such as `watch`, progress UIs, and dev servers.\n\n```ts\nconst server = api.pane(\"server\");\nawait server.sendLine(\"pnpm dev\");\n\nawait api.waitFor(\"dev server ready\", async () => {\n  const screen = await server.screen();\n  return screen.text.includes(\"Local:\");\n});\n```\n\n### \"Hidden\" Commands\n\nUse `pane.hidden.exec()`, `pane.hidden.sendLine()`, `pane.hidden.type()`, and `pane.hidden.press()` when hidden work must run in the same pane shell and influence later visible pane state such as exports or `cd`.\nUse normal JavaScript inside `script`, `setup`, and `teardown` for values that do not need a shell, and use `api.sidecar.exec()` for hidden subprocesses that do not need to modify pane state.\n`api.sidecar.exec()` can run any executable available to the preview server, but it receives an executable plus an argument array rather than a shell command string.\nUse `sh -c` explicitly if you need shell syntax such as pipes, redirects, or environment-variable expansion.\nUse `pane.getEnv()` when the sidecar process should run with a snapshot of a pane's exported environment and current working directory.\nCapture the snapshot before starting a foreground process such as a dev server or Vim, because the pane shell cannot answer hidden commands while another program owns the terminal.\nThis pairs well with `api.waitFor()` when a visible pane starts a server and the script needs to wait until it is ready.\n\n```ts\nconst server = api.pane(\"server\");\nconst environment = await server.getEnv();\nawait server.sendLine(\"npm run dev\");\n\nawait api.waitFor(\"server ready\", async () => {\n  const result = await api.sidecar.exec(\"curl\", [\"-fsS\", \"http://127.0.0.1:5173\"], {\n    environment,\n    reject: false,\n    timeoutMs: 1000,\n  });\n\n  return result.exitCode === 0;\n});\n```\n\n### Show preview controls\n\nWhen running in preview mode (`termdem preview demo.tsx`), press `Ctrl + .` in the preview window to show or hide the replay controls.\nThis can be used to restart the run or stop it at a certain point to inspect the output.\n\n### Scale terminal content\n\nUse `zoom` to scale terminal font size while keeping `size` as the video output size.\nFor a higher-resolution video with similarly readable text, increase `size` and `zoom` by the\nsame factor. For example, moving from `1280x720` to `2560x1440` with `zoom: 2` keeps the terminal\ncontent visually comparable while producing a larger video.\n\n```ts\nsettings: {\n  size: { width: 1280, height: 720 },\n  zoom: 1.5,\n}\n```\n\n## How it works\n\nA demo file defines a script, which is the sequence of steps to run, and a visual layout made from React components.\nTermdem uses [Vite](https://vite.dev/) to split that file into a server-side execution engine and a client-side rendering bundle.\n\nThe frontend uses [wterm](https://wterm.dev/react) to render a POSIX-compatible terminal in the browser.\nEach rendered terminal connects to a backend PTY over WebSocket.\n\nThe execution engine runs commands in the PTY and echoes terminal codes and text to the terminal rendered in the browser.\n\nFor recording, Termdem uses a headless Chromium instance orchestrated by [Playwright](https://playwright.dev/) and the browser's built-in recording functionality to generate the video.\nFinally, Termdem uses [ffmpeg](https://ffmpeg.org/) to convert the video to its final format when needed.\n\n## Why this exists\n\nI needed to record a terminal based demo that showed two processes communicating with each other over a WebSocket.\nTo make the demo reliable and easy to re-record whenever the code changed, I wanted to script it.\n\nBased on this, I found [VHS](https://github.com/charmbracelet/vhs), which has a very nice API to script and record terminal sessions.\nTo make it possible to show multiple processes (aka multiple terminals), I used tmux to multiplex the terminal session that was recorded.\n\nThis worked great, but _VHS_ is missing one critical feature: parsing the typed outputs from within the script.\n\nThe demo I was preparing demonstrated a secret based connection establishment, and a secret from one process needed to be sent to the second process through a \"side channel\" (aka copy and paste, if I recorded the demo manually).\nUnfortunately, in VHS it is impossible to get the output of previous commands and use it to interactively change subsequent commands.\n\nSo simply speaking, I wanted a way to record terminal demos optimized for multiple panes, with the ability to inspect and parse the output of each command.\n","readmeFilename":"README.md"}