This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
VcrSharp (the CLI is vcr, styled VCR#) is a .NET terminal recorder that turns .tape scripts into GIFs, MP4/WebM videos, PNGs, and (animated or static) SVGs. It is inspired by VHS but adds first-class Windows support, the ability to run real commands with Exec, and an interactive record mode that works in any shell.
The big architectural fact: VCR# is browserless. There is no ttyd, no Playwright, no Chromium, no xterm.js, no WebSocket. VCR# launches a real shell over an in-process pseudo-terminal (ConPTY on Windows, a Unix PTY on Linux/macOS), parses the shell's output with a from-scratch VT500 terminal engine (VtScreen), and rasterizes the resulting cell grid directly to frames. The only external runtime dependency is FFmpeg, and only when encoding GIF/MP4/WebM. SVG and PNG output need no external binaries at all. (Raster output additionally needs a monospace font installed on the system; SVG does not.)
This was a ground-up rewrite (the total-rewrite branch / commit 0cf0885 made the native path the only backend). If you find code, docs, or comments referencing ttyd, Playwright, Chromium, browsers, or xterm.js as a live path, they are stale — flag and fix them.
Status: alpha. The API and tape grammar still change.
dotnet build VcrSharp.sln# Record a tape file to its declared Output target(s)
dotnet run --project src/VcrSharp.Cli -- demo.tape
# Validate a tape file (parse + resolve presets) without recording
dotnet run --project src/VcrSharp.Cli -- validate demo.tape
# List the built-in themes
dotnet run --project src/VcrSharp.Cli -- themes
# Interactively record a live shell session into a .tape file
dotnet run --project src/VcrSharp.Cli -- record demo.tape
# One-shot static SVG screenshot of a command's final output (no tape file)
dotnet run --project src/VcrSharp.Cli -- snap "ls -la" -o ls.svg
# One-shot animated SVG of a command start-to-finish (no tape file)
dotnet run --project src/VcrSharp.Cli -- capture "dotnet --version" -o ver.svg
# Mine a directory of tapes into a shared vcr.toml preset (dry run by default)
dotnet run --project src/VcrSharp.Cli -- migrate samples/ --write# Run all tests
dotnet test
# Run a single test project
dotnet test tests/VcrSharp.Core.Tests
dotnet test tests/VcrSharp.Terminal.Tests
# Run a single test by name
dotnet test --filter "FullyQualifiedName~TapeParserTests"There is no Playwright/browser install step anymore. The old playwright.ps1 install chromium instruction is gone. The terminal-engine tests run a vendored libvterm conformance corpus in-process with no external setup.
The docs live in docs/VcrSharp.Docs, a Pennington DocSite (Blazor-rendered, MonorailCSS, no Node). Content is Markdown under Content/, organized into four Diátaxis areas (tutorials, how--to, explanation, reference).
cd docs/VcrSharp.Docs
dotnet run # dev-serve with hot reload
dotnet run -- build # static build; exits non-zero on any error/broken link
dotnet run -- diag warnings # report broken links / unresolved xrefs-
VcrSharp.Core — domain logic, parsing, and the cell-grid contract. No external-process or platform code.
.tapeparsing with Superpower combinators (Parsing/TapeTokenizer.cs,TapeParser.cs,TapeToken.cs)- AST command nodes (
Parsing/Ast/*.cs), all implementingICommand - Session config and runtime state (
Session/SessionOptions.cs,SessionState.cs), shell resolution (Session/ShellConfiguration.cs) - Terminal-grid DTOs the renderers consume (
Rendering/TerminalCell.cs,TerminalContent.cs,ContentExtent.cs) - The config layer:
vcr.tomlmodel + reader + preset resolver + tape migrator (Config/*.cs) - Themes (
Settings/BuiltinThemes.cs,Theme.cs), deprecation table (Settings/SettingDeprecations.cs) - Interactive-recording conversion (
Recording/InputEvent.cs,InputToTapeConverter.cs)
-
VcrSharp.Terminal — the from-scratch terminal engine. A standalone library whose single public type,
VtScreen, is a VT500-series emulator (canonical Paul Williams state machine). It feeds on a byte/char stream and maintains aCols×Rowsgrid of styled cells;ToTerminalContent()snapshots that grid into the Core DTOs. It is conformance-tested against a vendored libvterm corpus. Depends only on Core (for theTerminalContent/TerminalCelltypes). -
VcrSharp.Infrastructure — platform integration and rendering.
- PTY backends behind a neutral seam:
Terminal/IPtyProcess.cs(factoryPtyProcess.Start),Terminal/ConPtyProcess.cs(Windows ConPTY),Terminal/UnixPtyProcess.cs(posix_openpt+posix_spawn) - The terminal surface:
Terminal/TerminalPage.cs(ITerminalPageover PTY+grid),Terminal/FrameCapture.cs(IFrameCapture),Terminal/KeyMap.cs(key → raw terminal bytes),Terminal/InteractiveRecorder.cs(vcr record),Terminal/TerminalRenderer.cs(standalone run-and-snapshot helpers) - The orchestrator:
Session/RecordingSession.cs - Rendering:
Rendering/RasterRenderer.cs(ImageSharp/SixLabors.Fonts → frames),Rendering/SvgRenderer.cs(text SVG, static + animated) withRendering/CustomGlyphRenderer.cs,Rendering/VideoWriter.cs(FFmpeg via FFMpegCore),Rendering/SvgWriter.cs Processes/DependencyValidator.cs(checks only FFmpeg, only when needed)
- PTY backends behind a neutral seam:
-
VcrSharp.Cli — the
vcrCLI, built on Spectre.Console.Cli (Program.cs+Commands/*.cs).
- Parse —
TapeParserreads the.tapeinto a list ofICommandAST nodes. - Resolve config —
Config/PresetResolver.ResolveWithDiscoverywalks up from the tape's directory to find avcr.toml, then expandsUsepresets, theExec name argmacro form, andRunsugar, and derivesOutputfrom a preset'soutDir/output. CLI--set/--outputoverrides are then applied. - Launch —
RecordingSessiondecides the launch shape fromExecpresence (ShouldUseBareRepl= noExec):- No
Exec→ launch a bare interactive REPL; the tapeTypes its own visible command line. - Has
Exec→ join theExeccommand(s) and run them as the shell's hidden foreground process (the launch line is never echoed); laterType/Keyflow over the PTY to that app. It opens the PTY (PtyProcess.Start), creates aVtScreen, and starts a background drain thread reading PTY output →screen.Feed(...).
- No
- Capture — capture is event-driven: after every output chunk the drain thread snapshots the grid and appends it if it differs from the last frame (de-duped by a content fingerprint). This is the "never miss a frame" design — periodic sampling would drop transient TUI states. Key presses are paced ~24 ms (
KeyPaceMs) so a TUI has time to redraw between keys. - Execute — the command loop runs the action commands (
Type,Key,Modifier,Wait,Hide/Show,Sleep,Screenshot) against anExecutionContext.Set/Output/Env/Use/Execare no-ops at this stage — their effect was realized before/around the loop. - Settle — after the loop, wait for output to stabilize and force-append a final frame so the recording always ends on finished output.
- Encode — per
Outputpath, by extension:.svg→SvgWriter(animated) orRenderStaticAsync(whenMode static);.gif/.mp4/.webm→VideoWriter(rasterize frames + FFmpeg);.png→ single final raster frame; extension-less path → a PNG frame sequence + concat manifest.
InteractiveRecorder.RecordAsync authors a .tape from a live session instead of playing one back:
- Launch the user's shell interactively over a PTY (no
Exec). - Put the host console in raw/VT pass-through mode (
ConsoleRawMode: console-mode flags on Windows;stty raw -echo < /dev/ttyon Unix). Falls back to cooked mode if raw can't be set. - Run two pump threads: output (PTY → host stdout, so the user sees the live shell) and input (host stdin → PTY, recording every read chunk as a timestamped
InputEvent). Because a multi-byte keystroke (an arrow'sESC [ A,Alt+char) arrives in one read, escape sequences stay intact. - The session ends when the shell exits (
exit/ Ctrl+D). InputToTapeConverter.Convertturns the captured input byte-stream into tape text — coalescingType, mapping keys/modifiers, insertingSleepfrom real pauses, stripping the trailingexit. This is shell-agnostic because it operates on input, not output. (This converter is unchanged from before; only the capture mechanism — console pumps over a PTY — replaced the old browser hook.)
All tape commands implement ICommand:
Task ExecuteAsync(ExecutionContext context, CancellationToken cancellationToken);ExecutionContext exposes ITerminalPage (the PTY-backed TerminalPage), IFrameCapture (FrameCapture), SessionOptions, and SessionState.
Command nodes in Parsing/Ast/:
- Config (no-op at runtime, applied before/around the loop):
SetCommand,OutputCommand,EnvCommand,UseCommand,ExecCommand - Input (real actions):
TypeCommand,KeyCommand,ModifierCommand - Control (real actions):
SleepCommand,WaitCommand,HideCommand,ShowCommand - Utility (real action):
ScreenshotCommand(PNG and SVG) - Sugar (desugared by
PresetResolverbefore the loop):RunCommand(→Type+Enter+Wait)
Exec's effect happens outside the loop: literal Exec text is baked into the shell launch line; the Exec name arg macro form is expanded to a literal Exec by PresetResolver against a [macro] template in vcr.toml.
Removed commands:
Copy,Paste,Require, andSourcewere deleted from the grammar. They are parse errors now — do not reference them.
-
VtScreen(VcrSharp.Terminal) — the in-process VT500 engine. Implements the full SGR attribute model (bold/dim/italic/underline incl. styled + colored, reverse/conceal/strike/overline/blink, 16/256/truecolor), DEC private modes, alternate screen, scroll regions, line editing, tab stops, cursor save/restore, content-preserving resize, UTF-8 + grapheme/emoji width, and DEC line-drawing. Crash-proof on pathological input (a hard fuzz gate). True reflow-on-resize is deliberately deferred — the largest remaining conformance gap. -
RecordingSession(Infrastructure) — the orchestrator: chooses the launch shape, runs the PTY drain + event-driven capture, runs the command loop, settles, and routes eachOutputto the right writer. -
TerminalPage(Infrastructure) —ITerminalPageover the PTY + grid.Type/Key/Modifierwrite bytes to PTY stdin (the shell echoes them back through the parser);Waitpolls the grid text against a regex (and treats a non-interactiveExecshell exiting as "Wait satisfied");Hide/Showtoggle capture and cursor visibility. -
PtyProcess/ConPtyProcess/UnixPtyProcess(Infrastructure) — the only platform-specific code. Everything above theIPtyProcessseam (engine, grid, renderers, commands, capture) is platform-neutral. -
PresetResolver(Core) — expands the whole config layer (Use,Run,Execmacros, derivedOutput) into primitive commands before recording. The engine never sees presets/macros. Fast-paths (returns input unchanged) when a tape uses none of them. -
TapeMigrator/MigrateCommand— mine a directory of legacy tapes for shared "house style"Setlines, emit avcr.tomlpreset, and rewrite each tape toUseit. Every rewrite is equivalence-checked (identical realized config + action sequence) so a migration provably never changes behavior; it also rewrites deprecatedStaticOutput/FitToContentintoMode/Size. -
RasterRenderer(Infrastructure) — renders aTerminalContentgrid to a raster image with SixLabors.ImageSharp + SixLabors.Fonts (CPU). Resolves a real monospace system font (Cascadia Mono/Code, Consolas, Courier New, DejaVu Sans Mono, …) and throws an actionable error if none exists. Used for GIF/MP4/WebM/PNG. -
SvgRenderer(Infrastructure) — renders the grid to text-based SVG (static viaRenderStaticAsync, animated viaRenderAnimatedAsync). Box-drawing/block/powerline glyphs are emitted as SVG paths (CustomGlyphRenderer), so SVG needs no installed font. Embedding options (all SVG-only):Set Loop false/Set LoopCount N(finite reveal that holds),Set Size fit(crop to content extent),Set SvgIntrinsicSize/Set SvgMetadata(explicitwidth/height+data-*, both default on). SVG honorsPaddingonly; it ignoresMargin/MarginFill/WindowBarSize/BorderRadius. -
VideoWriter(Infrastructure) — rasterizes captured frames and drives FFmpeg (via FFMpegCore) for GIF (palettegen/paletteuse), MP4 (libx264), and WebM (libvpx-vp9, alpha). Also writes the extension-less frame directory (PNG sequence + concat manifest). -
DependencyValidator(Infrastructure) — checks only for FFmpeg, andRecordCommandonly calls it when anOutputis.gif/.mp4/.webm. SVG/PNG/frames-only tapes need no external binaries.
| Command | Purpose | Key options |
|---|---|---|
vcr <tape> (default) |
Record/play a tape to its Output target(s) |
-v/--verbose, --set KEY=VALUE (repeatable), -o/--output FILE (repeatable) |
vcr validate <tape> |
Parse + resolve presets and report command counts; no recording | — |
vcr themes |
List the built-in themes (name/bg/fg) | — |
vcr migrate <dir> |
Extract shared Set lines into vcr.toml, rewrite tapes to Use it (dry run by default) |
--write, --preset NAME (def doc), --threshold FRACTION (def 0.6), -r/--recursive |
vcr snap <command> |
Static SVG screenshot of a command's final output (forces .svg, fit-to-content) |
-o/--output, --theme, --cols, --rows, --font-size, --disable-cursor, --transparent-background, --end-buffer, -v |
vcr capture <command> |
Animated SVG recording of a command's output (forces .svg) |
same as snap |
vcr record [tape] |
Interactively record keystrokes in a real shell → .tape (default recording.tape) |
--shell, --theme, --cols, --rows, --font-size, -v |
- .NET 10 runtime (the tool is a
dotnet tool, commandvcr). - FFmpeg on PATH — only for GIF/MP4/WebM output. SVG/PNG/frames need it not.
- A monospace font installed — only for raster output (GIF/MP4/WebM/PNG). Usually already present (Windows: Consolas/Cascadia; Linux: DejaVu Sans Mono). SVG output needs none.
ttyd,Playwright/Chromium— removed. Not dependencies.
- Superpower (parser combinators) — Core
- SixLabors.ImageSharp + SixLabors.ImageSharp.Drawing (raster/font rendering) — Core
- FFMpegCore (FFmpeg wrapper, the only external-binary-backed package) — Infrastructure
- Spectre.Console / Spectre.Console.Cli + Errata (CLI UI) — Cli
- Serilog (logging) — Core/Cli
There is no Microsoft.Playwright reference.
src/
├── VcrSharp.Core/ # Parsing, AST, SessionOptions, config (vcr.toml), themes, grid DTOs
├── VcrSharp.Terminal/ # VtScreen — the from-scratch VT500 engine
├── VcrSharp.Infrastructure/ # PTY backends, RecordingSession, renderers, FFmpeg writer
└── VcrSharp.Cli/ # `vcr` CLI commands (Spectre.Console.Cli)
tests/
├── VcrSharp.Core.Tests/ # Parser, settings, config/preset/migrate, native-launch tests
└── VcrSharp.Terminal.Tests/ # VtScreen unit tests + vendored libvterm conformance corpus
docs/
├── VcrSharp.Docs/ # Pennington DocSite (Content/ = tutorials/how--to/explanation/reference)
└── vt-conformance-scoreboard.md # auto-generated by the conformance tests
samples/ # Example .tape files (and samples/docs/* feed the docs SVGs)
- Settings:
Set <Name> <value>(value = quoted string, duration, number, bool, or bare word). Must appear before any action command; no duplicates; unknown name is a parse error with a "Did you mean…?" suggestion. - Output:
Output <path>— format by extension (.svg/.gif/.mp4/.webm/.png, or a directory for raw frames). - Input:
Type[@speed] "text"; special keys (Enter,Tab,Backspace,Delete,Escape, arrows,Home,End,PageUp/Down,Insert,Space) with optional@speedand a trailing repeat count (e.g.Backspace 5,Down@100ms 3); modifier chords (Ctrl+C,Alt+Enter,Ctrl+Alt+Shift+Tab). - Control:
Sleep <duration>;Wait[+Buffer|+Line|+Screen][@timeout] [/regex/];Hide/Show(stop/resume frame capture — commands keep executing while hidden). - Execution:
Exec "cmd"(run a real command) andExec name arg(expand a[macro]fromvcr.toml). - Environment:
Env KEY "value". - Config sugar:
Use <preset>(apply avcr.tomlpreset);Run "cmd"(=Type+Enter+Wait).
Mode=animated(default) |static.staticrunsExec, lets output settle, and emits one static frame perOutput(.svg/.pngonly). Supersedes the deprecatedSet StaticOutput true.Size=grid(default) |fit.fitcrops SVG to measured content extent. Supersedes the deprecatedSet FitToContent true.HoldDurationis an alias forEndBuffer.- Both deprecated booleans (
StaticOutput,FitToContent) still parse (with a warning) sovcr migratecan read legacy tapes. - Do not document or use
Set CssVariables— despite a lingeringSessionOptions.CssVariablesproperty, it is not inValidSettingNames, soSet CssVariables …is a parse error.
Discovered by walking up from the tape's directory. A deliberate TOML subset with two section kinds:
[preset.doc] # pulled in by `Use doc`
theme = "one dark"
cols = 82
transparentBackground = true
endBuffer = 5s
outDir = "assets" # reserved: derive Output = assets/<tape>.svg
[preset.landing]
inherits = "doc" # reserved: inherit base-first
mode = static # migrator emits Mode/Size, not StaticOutput/FitToContent
output = "assets/{name}.png" # reserved: explicit template, {name} = tape basename
[macro]
showcase = "dotnet run --no-build showcase {0}" # Exec showcase <arg>Reserved keys: inherits, outDir, output. Every other key is a setting (validated against the tape Set allow-list at resolve time). Presets/macros/Use/Run never reach the engine — PresetResolver expands them into Set/Output/Type/Key/Wait/literal Exec.
Wait+Buffer(default): search output accumulated since the lastWait.Wait+Line: current line only.Wait+Screen: the entire visible grid.
Type: simulates character-by-character keyboard input (the typed text is part of the demo).Exec: runs a real command as the shell's hidden foreground process. The launch shape keys offExecpresence, not Type/Key. A tape withExecnever echoes the launch line; a tape withoutExectypes its own visible command line into a bare REPL.
Screenshot <path> captures a single frame mid-recording. .png → RasterRenderer; .svg → SvgRenderer. With Set ScreenshotWaitForInactivity true, capture waits for output to settle first (ScreenshotInactivityTimeout).
Built-in themes in Settings/BuiltinThemes.cs define foreground/background/ANSI palettes. Applied by the native renderers (raster + SVG) — there is no CSS injection anymore. vcr themes lists them; unknown names fall back to Default.
- Create a class in
VcrSharp.Core/Parsing/Ast/implementingICommand. - Add a parser rule in
Parsing/TapeParser.cs(and a token inTapeTokenizer.csif it needs a new keyword). - Implement
ExecuteAsyncagainstITerminalPage/IFrameCapture(or make it a no-op if its effect is realized before the loop, likeSet/Output/Exec). - If it should be expanded before recording (like
Run), handle it inConfig/PresetResolver.cs. - Add tests in
tests/VcrSharp.Core.Tests/Parsing/.
- Add a property (with default) to
Session/SessionOptions.cs. - Add the name to
ValidSettingNamesinParsing/TapeParser.cs. - Add an apply case in
SessionOptions.ApplySetting. - Add validation in
SessionOptions.Validateif needed. - Add parser tests.
Note: setting names may begin with a command keyword (e.g. WaitTimeout, ScreenshotWaitForInactivity). TapeTokenizer.Keyword() guards the right boundary so such names tokenize as a single identifier.
VtScreen is conformance-driven. The vendored libvterm corpus lives in tests/VcrSharp.Terminal.Tests/Conformance/LibVterm/; the harness (LibVtermHarness.cs) decodes the Perl byte literals and asserts against the grid snapshot. EngineDoesNotCrash + FuzzTests are hard robustness gates. The Scoreboard test regenerates docs/vt-conformance-scoreboard.md. When adding VT behavior, add or enable the relevant corpus assertions and keep the crash gate green.
- Windows: primary platform. PTY = ConPTY (
CreatePseudoConsole, Windows 10 1809+). Default shellpwsh→powershell→cmd. - Linux/macOS: PTY =
posix_openpt+posix_spawn(managedfork/execis unsafe in a multi-threaded CLR). Default shellbash. - Everything above the
IPtyProcessseam is identical across platforms; only PTY launch and raw-console plumbing differ.Set Shellis resolved throughSession/ShellConfiguration.cs(bash/zsh/fish/sh/cmd/pwsh/powershell), which also supplies a clean>prompt and profile/rc suppression.