tcut_

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 demo
bun add -g termcutstandalone binariessource
tcut > > e > ec > echo > echo > echo ' > echo 'H > echo 'He > echo 'Hel > echo 'Hell > echo 'Hello > echo 'Hello > echo 'Hello f > echo 'Hello fr > echo 'Hello fro > echo 'Hello from > echo 'Hello from t > echo 'Hello from tc > echo 'Hello from tcu > echo 'Hello from tcut > echo 'Hello from tcut 👋 > echo 'Hello from tcut 👋' > echo 'Hello from tcut 👋'Hello from tcut 👋> > echo 'Hello from tcut 👋'Hello from tcut 👋> c > echo 'Hello from tcut 👋'Hello from tcut 👋> ca > echo 'Hello from tcut 👋'Hello from tcut 👋> cat > echo 'Hello from tcut 👋'Hello from tcut 👋> cat > echo 'Hello from tcut 👋'Hello from tcut 👋> cat R > echo 'Hello from tcut 👋'Hello from tcut 👋> cat RE > echo 'Hello from tcut 👋'Hello from tcut 👋> cat READ > echo 'Hello from tcut 👋'Hello from tcut 👋> cat READM > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README. > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.m > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> b > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bu > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun - > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun -- > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --ve > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --ver > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --versi > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --versio > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --version > echo 'Hello from tcut 👋'Hello from tcut 👋> cat README.md# tcutTerminal videos as code.> bun --version1.4.0>
docs/demo.svg · 19 KB · no JavaScript, no fonts embeddedrendered by tcut from the script below

record ≠ 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.

t.hide()−2.9s
Frame at 0 seconds00:00
Frame at 1 seconds00:01
Frame at 2 seconds00:02
Frame at 3 seconds00:03
Frame at 4 seconds00:04
Frame at 5 seconds00:05
Frame at 6 seconds00:06
Frame at 7 seconds00:07
202 frames at 30 fps47 of them are unique — the rest are reused, so idle seconds cost nothing2.9s of setup commands ran but were cut from the timeline

the 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.
Final frame of the demo as a PNG still
demo.png 31 KBdemo.gif 19 KBdemo.svg 19 KB6.7s

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.

Recording of bun create better-t-stack being answered with arrow keys and Enter
better-t-stack.ts — answers the bun create better-t-stackprompts with down() / enter(), and picks the deployment by reading the screen until the highlighted row says Cloudflare.
Recording of Claude Code answering a question and Codex editing a file
ai-agents.tsclaude -p explains a file,codex exec edits it. The model's words differ every recording; the rendered video never does.

outputs

Pick by extension. Mix them in one run.

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 failed

compared 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.

VHStcut
Script formata .tape DSLTypeScript — loops, imports, shared scenes, assertions
Waiting for outputregex on raw bytesrun() / wait() / expect() read the rendered screen of a headless Ghostty terminal
Determinismlive screenshots; depends on machine speedrecord once to .cast, render on a virtual clock — identical frames anywhere
Re-theme a demorun everything againtcut render demo.cast --theme dracula — no shell is spawned
Outputsmp4 · gif · webm · png frames+ animated SVG · HTML player · PNG/JPG stills; SVG and HTML need no ffmpeg or browser
As teststcut test runs scripts fast; exit code follows expect()
Under the hoodttyd + Chrome + ffmpegBun.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 termcut

Without installing anything.

bunx termcut init demo

Without 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 tcut

Then tcut demo.video.ts. The full API — every option of defineVideo and the t object — is in the README.