Record TUI — VHS Terminal Recording
Overview
Charmbracelet VHS renders a .tape script to GIF / MP4 / WebM / ASCII. Write the demo as code rather than capturing a screen, because a .tape re-renders unattended when the app changes and runs the same way in CI. For a throwaway recording nobody will re-run, asciinema is lighter.
Setup
# macOS
brew install charmbracelet/tap/vhs ffmpeg ttyd
# Debian/Ubuntu
sudo apt install ffmpeg && sudo snap install ttyd --classic && go install github.com/charmbracelet/vhs@latest
vhs --version && ffmpeg -version && ttyd --version # all three are required
Tape file structure
Every Set goes above the first interaction command. A Set placed lower is silently discarded — see Gotchas.
Output demo.gif # 1. output (gif/mp4/webm/ascii)
Require my-app # 2. fail fast if the app is missing
Set Shell "bash" # 3. settings, all grouped before any command
Set FontSize 20
Set Width 1200
Set Height 600
Set Theme "Catppuccin Frappe"
Set WindowBar Colorful
Set Padding 20
Set TypingSpeed 75ms
Hide # 4. hidden setup — Ctrl+L before Show
Type "export TERM=xterm-256color"
Enter
Sleep 500ms
Ctrl+L
Show
Type "my-app --demo" # 5. visible interactions, with deliberate pauses
Sleep 500ms
Enter
Sleep 2s
Down 3 # 6. app interaction
Enter
Sleep 3s # generous final frame before the loop restarts
Quick reference
| Command | Purpose |
|---|---|
Output file.gif |
Output file + format (gif/mp4/webm/ascii); repeat for several formats |
Require app |
Fail if the program is not on PATH |
Set Key Value |
Terminal settings (FontSize, Width, Height, Theme, Framerate, …) |
Type "text" / Type@100ms "text" |
Emulate typing (optionally at a per-line speed) |
Enter / Tab / Space / Escape |
Key presses |
Up/Down/Left/Right (Down 3) |
Navigation with optional repeat count |
Ctrl+key, Alt+key, Shift+key |
Modifier combos |
Sleep 2s |
Fixed pause (ms or s) |
Wait+Screen@30s /regex/ |
Block until screen content matches, with a timeout |
Hide / Show |
Control recording visibility (execution continues either way) |
Screenshot file.png |
Capture the current frame |
Env VAR "val" / Source other.tape |
Set an env var / include another tape |
vhs validate demo.tape # parse-check, no rendering
vhs demo.tape # record
vhs themes # list themes
vhs new demo.tape # scaffold from template
Full command + settings detail: reference/COMMAND-REFERENCE.md.
| Use case | Width | Height | FontSize |
|---|---|---|---|
| README GIF | 1200 | 600 | 20 |
| Docs/tutorial | 1400 | 800 | 18 |
| Social media | 1200 | 630 | 22 |
| Full TUI app | 1600 | 900 | 16 |
| Compact CLI | 800 | 400 | 20 |
Recording a tape for an app
Read the app's --help output or its source to learn how it launches, which keys it responds to, and which states are worth showing. Draft a tape covering those states, then vhs validate demo.tape → vhs demo.tape → watch the rendered file → adjust. Watching is the step that matters: validation parses syntax and cannot tell you the demo outruns the viewer.
Bound the demo before drafting: a README GIF shows one workflow in 10–30 seconds — three to five states, each held long enough to read (Sleep 2s–3s). Past 30 seconds, cut states or split into a second tape rather than adding scenes; a demo that shows everything shows nothing, and the GIF size grows with it.
Copy-paste starting points (Basic CLI, Interactive TUI, Build-and-Run, Multi-Panel, CI golden file, composable Source tapes): reference/TEMPLATES.md.
Examples
✅ Output first, settings grouped above the commands, Require guarding the binary, a pause after typing and a long final frame:
Output demo.gif
Require my-app
Set Shell "bash"
Set FontSize 20
Set Width 1200
Set Height 600
Type "my-app status"
Sleep 500ms
Enter
Sleep 3s
❌ Same tape, three defects — Set Width lands after Type and is dropped, nothing separates typing from execution, and a missing binary produces a confusing empty GIF instead of an error:
Output demo.gif
Type "my-app status"
Enter
Set Width 1200
✅ Pacing a TUI whose startup time varies — wait on a screen match instead of guessing a Sleep:
Type "my-tui"
Enter
Wait+Screen@30s /Ready/
Down 3
Sleep 500ms
Enter
Sleep 3s
Gotchas
- A
Setbelow the first command is silently dropped, andvhs validatestill exits 0. Verified on vhs 0.11.0:Set FontSize 40after aTyperendered at the default size, and the same holds forSet TypingSpeed— there is no per-setting exception. Keep everySetabove the firstType/key command and confirm by watching the output, because nothing in the toolchain reports this. To vary typing speed mid-tape, use the per-lineType@100ms "…"form. vhs validateonly parses. It never launches the app, so a missing binary, a wrong keybinding, or a demo that races ahead of the app all pass. AddRequire <app>so a missing dependency fails loudly rather than rendering an empty terminal.Type "cmd"does not press Enter. Follow it with an explicitEnter, and putSleep 500msbetween them so the viewer can read the command before it runs.Hidehides output, not execution. Commands inside aHideblock still run. End the block withCtrl+LbeforeShowso leftover setup output doesn't open the recording.Waittimes out. Raise it per command (Wait+Screen@30s /ready/) or globally (Set WaitTimeout 30s) for a slow-booting TUI, rather than padding with a longerSleepthat will still be wrong on a slower machine.- VHS records in a fresh shell, so your prompt customizations are absent. Pin
Set Shell "bash"to get the same prompt on your machine and in CI. - A TUI that renders with wrong colors or wrapping is usually a terminal-setup problem. Pin it in the
Hideblock:Env TERM "xterm-256color"plusType "stty rows 50 cols 120". - GIFs cross 5 MB quickly. GitHub renders them inline, but the README crawls. Try
Set Framerate 15first — it is the cheapest win and costs little perceived smoothness — thengifsicle -O3 --lossy=80, and switch to MP4 before shrinking dimensions past readability.
Optimize & CI
Size reduction, playback tuning, and the GIF-vs-MP4-vs-WebM decision tree: reference/OPTIMIZATION.md. Recording in GitHub Actions, plus ASCII output as a golden file for UI regression tests: reference/CI-INTEGRATION.md.
Integration
Pairs with build-tui — record the TUI right after building it, and commit the .tape next to the source so the GIF regenerates with the app. Use screenshot-local instead when the target is a web UI on localhost.
External: VHS · VHS Action · examples · themes · gifsicle.