termcut 0.2.0 · Bun ≥ 1.4 · MIT
Terminal videos,
written in TypeScript.
Script a session. Record it once. Render it anywhere — MP4, GIF, WebM, SVG, HTML — byte-for-byte the same every time.
bunx termcut init demorecord ≠ render
One recording. Every frame decided by a clock, not by how fast your laptop is.
tcut writes a standard asciicast while it drives the shell, then replays it on a virtual clock: frame N is the screen at N ÷ fps. Hidden setup is cut out of the timeline, unchanged frames are reused, and the same cast re-renders to any format or theme without running a single command again.
00:00
00:01
00:02
00:03
00:04
00:05
00:06
00:07the whole script
This is everything that made the demo above.
import { defineVideo } from "tcut";
export default defineVideo(
{
output: ["docs/demo.gif", "docs/demo.svg", "docs/demo.png"],
theme: "catppuccin-mocha",
cols: 76,
rows: 16,
fps: 30,
typingSpeed: "35ms",
typingJitter: 0.35,
windowBar: "colorful",
title: "tcut",
padding: 18,
margin: 28,
borderRadius: 12,
marginFill: "#11111b",
font: { size: 18 },
},
async (t) => {
await t.hide(async () => {
await t.run("cd $(mktemp -d) && printf '# tcut\\n\\nTerminal videos as code.\\n' > README.md");
await t.clear();
});
await t.run("echo 'Hello from tcut 👋'");
await t.expect(/Hello from tcut/);
await t.sleep("700ms");
await t.type("cat README.md");
await t.sleep("300ms");
await t.enter();
await t.wait();
await t.sleep("900ms");
await t.run("bun --version");
await t.sleep("2s");
},
);what the calls do
- t.run(cmd)
- types it, presses Enter, and returns when the prompt is back — it watches the actual screen, not a timer.
- t.expect(/re/)
- asserts against the screen. Fails loudly with a screen dump, so the demo is also an integration test.
- t.hide(fn)
- runs setup that keeps its effects but never appears in the video.
- typingJitter + seed
- human-looking typing from a seeded generator — reproducible, like everything else.

examples
Interactive tools and AI agents, not just echo.
Because tcut waits on the rendered screen, it can drive anything a human can: menus, prompts, agents that take ten seconds to think. Both recordings below are in the repo underpackages/tcut/examples.


outputs
Pick by extension. Mix them in one run.
- .mp4 .webmneeds ffmpegH.264 / VP9, even dimensions, faststart
- .gif .webpneeds ffmpegpalette-optimised GIF, animated WebP
- .svgneeds nothinganimated vector, built from the cell grid — crisp at any size, tiny
- .htmlneeds nothingsingle-file player with play / pause / loop
- .png .jpgneeds WebViewthe final frame; screenshot() for any moment
- frames/needs WebViewone PNG per frame
- .castneeds alwaysasciicast v2 — plays in asciinema, re-renders forever
output: ["demo.mp4", "demo.gif", "demo.svg", "demo.html"] — or tcut render demo.cast -o demo.svg --theme dracula
tcut test
Your demos are already integration tests.
tcut test examples/ runs every script in fast mode — no typing delay, no sleeps — with no video and no browser. Each expect() becomes an assertion against the real screen; the exit code tells CI whether your README still works.
$ tcut test examples/
TAP version 14
1..2
ok 1 - examples/demo.ts (412ms)
not ok 2 - examples/release.ts (1803ms)
---
Expected /v0\.2\.0/ on screen to match.
--- screen ---
> bun --version
1.4.0
>
...
# 1 passed, 1 failedcompared with VHS
Same idea. Different answers.
VHS showed that terminal GIFs should be code. tcut keeps that and changes what the code is, what it waits on, and what it produces.
| VHS | tcut | |
|---|---|---|
| Script format | a .tape DSL | TypeScript — loops, imports, shared scenes, assertions |
| Waiting for output | regex on raw bytes | run() / wait() / expect() read the rendered screen of a headless Ghostty terminal |
| Determinism | live screenshots; depends on machine speed | record once to .cast, render on a virtual clock — identical frames anywhere |
| Re-theme a demo | run everything again | tcut render demo.cast --theme dracula — no shell is spawned |
| Outputs | mp4 · gif · webm · png frames | + animated SVG · HTML player · PNG/JPG stills; SVG and HTML need no ffmpeg or browser |
| As tests | — | tcut test runs scripts fast; exit code follows expect() |
| Under the hood | ttyd + Chrome + ffmpeg | Bun.Terminal + Ghostty (WASM) + Bun.WebView; ffmpeg only for video containers |
what your machine needs
Only as much as the output you ask for.
- run tcut
- Bun ≥ 1.4 (bun add -g termcut), or the standalone binary — Bun is embedded
- record · tcut test
- a shell (bash, zsh, fish or sh) and the tools your script runs. No browser, no ffmpeg
- render .svg · .html
- nothing else
- render .png · .jpg · frames/
- a WebView — macOS: built in. Linux / Windows: Chrome, Chromium, Edge or Brave installed
- render .mp4 · .gif · .webm
- the WebView above + ffmpeg
- render .webp
- an ffmpeg with libwebp — Homebrew: brew install ffmpeg-full (found automatically)
Verified on macOS. Linux and Windows binaries are cross-compiled and not yet exercised in CI.
install
Three ways in.
With Bun — the package is termcut, the command is tcut.
bun add -g termcutWithout installing anything.
bunx termcut init demoWithout Bun — a single binary for macOS, Linux or Windows from Releases.
curl -fsSL https://github.com/AmanVarshney01/tcut/releases/latest/download/tcut-0.2.0-darwin-arm64 -o tcut && chmod +x tcutThen tcut demo.video.ts. The full API — every option of defineVideo and the t object — is in the README.