Skip to content

HalFrgrd/evp

Repository files navigation

evp

evp — a small Rust CLI that ingests VHS-format scripts and produces GIF, SVG, or JSON outputs using libghostty as the underlying terminal emulator.

evp runs a real shell inside an embedded Ghostty VT, schedules typed input from your .tape script onto an absolute timeline, snapshots the terminal at the configured framerate, then streams frames to one or more renderer threads.

Architecture deep-dive architecture.md
Examples examples/
Docker image ghcr.io/halfrgrd/evp:latest
Pre-rendered example GIFs assets release (linked below)

Examples

These GIFs are produced by the ci workflow on every push to master and uploaded as assets on the rolling assets release.

evp_demo — running evp on the command line

evp_demo.gif

hello — bare-minimum recording

hello.gif

shell-tour — multi-command session paced with Wait

shell-tour.gif

keys — modifiers + line editing

keys.gif

colors — ANSI SGR colour table

colors.gif

progress — in-place line rewrites

progress.gif

Quick start

Grab a prebuilt binary into the current directory — no Rust, no Zig, no Docker:

curl -sSfL https://raw.githubusercontent.com/HalFrgrd/evp/master/install.sh | sh

This drops an evp executable into $PWD. Override the destination with EVP_INSTALL_DIR=~/.local/bin sh or pin a release with EVP_VERSION=v0.2.0 sh. The binary is a single fully static (musl + static-pie) ELF — see System requirements below.

Verify the install with the built-in demo (no script files needed):

./evp --run-test-script
# → writes ./evp-test.gif

Then render your own scripts:

./evp examples/hello.tape -o hello.gif

System requirements

The prebuilt binary is statically linked against musl libc as a static-pie executable, so it has no dynamic library dependencies at all:

$ ldd evp
        statically linked
$ file evp
evp: ELF 64-bit LSB pie executable, x86-64, ..., static-pie linked

It runs unmodified on any x86_64 Linux kernel — Alpine, Debian (any version), Ubuntu, RHEL/CentOS (any version), distroless, scratch — no glibc version requirement.

Not required: fontconfig, freetype, ImageMagick, ffmpeg, a display server, or any installed fonts. JetBrains Mono is embedded into the binary.

Using the Docker image

If you don't want a binary on the host, the published image is fully self-contained:

docker run --rm -v "$PWD:/work" \
    ghcr.io/halfrgrd/evp:latest \
    examples/hello.tape -o hello.gif

Build from source

Build dependencies (only needed at build time — the resulting binary does not need either):

  • a Rust toolchain,
  • Zig 0.15.x on $PATH (libghostty's build system).

Both libghostty-vt and libghostty-vt-sys are pulled directly from the upstream git repo by Cargo — there's no sibling repo to clone.

cargo build --release
./target/release/evp examples/hello.tape -o hello.gif

The resulting binary has no dynamic dependency on libghostty-vt.so and no runtime requirement on Zig:

$ ldd ./target/release/evp | grep ghostty || echo "statically linked"
statically linked

Reproducible static build via Docker

If you'd rather not install Rust or Zig locally, the project ships a docker buildx bake recipe that produces the same fully-static musl binary that the GitHub release uses. One command:

docker buildx bake extract-binary
# → /tmp/evp-build/evp  (≈6 MiB, static-pie, stripped)

The extract-binary target writes the binary directly to /tmp/evp-build on the host via buildx's local output writer — no docker create / docker cp plumbing. Other targets in the same bake file:

docker buildx bake test       # workspace cargo test (CI parity)
docker buildx bake runtime    # builds evp:local container image
docker buildx bake build-env  # Rust+Zig base image
docker buildx bake builder     # intermediate builder image

Restricted-network environments (Copilot cloud agent etc.)

.github/workflows/copilot-setup-steps.yml now does one thing only: install Zig 0.15.2 into the Copilot environment.

For clean builds inside a restricted environment, whitelist these domains:

  • ziglang.org — Zig 0.15.2 toolchain downloads.
  • github.com — Cargo's libghostty-rs git dependency and libghostty-vt-sys cloning the pinned Ghostty commit.
  • deps.files.ghostty.org — most Ghostty Zig package tarballs.
  • gitlab.freedesktop.org — the wayland-protocols Zig dependency.

Those hosts come directly from the pinned dependency sources in crates/libghostty-vt-sys/build.rs and Ghostty's build.zig.zon.

As a library

evp ships both a [lib] and a [[bin]]. The library exposes the full pipeline so other Rust tools can embed it:

let script = evp::parse_script(include_str!("../examples/hello.tape"))?;
let out    = evp::run_and_return_recording(&script)?;
let json   = evp::recording_to_json(&out.recording)?;
evp::render_gif(&out.recording, &evp::RenderOptions {
    font_path: None,
    font_size: 20.0,
    frame_style: out.recording.frame_style,
}, std::path::Path::new("hello.gif"))?;

The integration tests in tests/recording_json.rs use this same API end-to-end.

CLI

evp <script> [-o <output.gif|output.svg|output.json>] [--font <path.ttf>] [--dump-json <path.json>] [--log-level <level>]
evp --run-test-script [-o <output.gif|output.svg|output.json>]
evp themes
evp validate <script>
evp completion <shell>
Flag Meaning
<script> Path to a .tape file. Required unless --run-test-script is set.
--run-test-script Run a small built-in demo tape embedded in the binary. Writes ./evp-test.gif by default. Useful for verifying an install end-to-end with no external files.
-o, --output Override the script's Output directives. Output extension picks the renderer (.gif, .svg, or .json).
--font Path to a TTF/OTF/TTC used by the GIF renderer. Defaults to embedded JetBrains Mono family files in assets/fonts/.
--dump-json Also render the intermediate Recording to JSON for later re-rendering or inspection.
--log-level Explicit level override: error, warn, info, debug, trace.
themes Print the bundled VHS theme preset names supported by Set Theme "<name>".
validate <script> Parse a tape and exit without running the PTY or renderer.
completion <shell> Print a shell completion script generated by clap.
--version Print extended build metadata (git SHA/branch/date/dirty flag, build timestamp, rustc, target triple, opt-level).

--log-level overrides the default info level. If it is not provided, RUST_LOG is still honored.

The embedded font family is distributed under SIL OFL 1.1; see licenses/JETBRAINSMONO-OFL-1.1.txt.

GIF rendering uses style-specific JetBrains Mono faces when available:

  • regular: JetBrainsMono-Regular.ttf
  • bold: JetBrainsMono-Bold.ttf
  • italic: JetBrainsMono-Italic.ttf
  • bold italic: JetBrainsMono-BoldItalic.ttf

Additional JetBrains Mono weights are also embedded under assets/fonts/. If a requested style face is unavailable, rendering logs a warning and falls back to the regular face.

Using evp in GitHub Actions

The recommended path is the bundled composite action, which downloads a prebuilt linux-amd64 binary from the matching GitHub Release and adds it to $PATH. No Docker daemon, no Rust toolchain, no Zig — just one uses: step.

- uses: HalFrgrd/evp@v1
  with:
    script: docs/demo.tape
    output: docs/demo.gif

- uses: stefanzweifel/git-auto-commit-action@v5
  with:
    file_pattern: docs/demo.gif

Pin to a specific release for reproducibility:

- uses: HalFrgrd/evp@v0.2.0
  with:
    script: docs/demo.tape
    output: docs/demo.gif
    version: v0.2.0

Action inputs

Input Default Meaning
script (required) Path to the .tape script.
output from script's Output directive Output file path. Extension picks the renderer (.gif, .svg, or .json).
version latest evp release tag to install.
font embedded JetBrains Mono Optional path to a TTF/OTF font for the GIF renderer.
log-level info One of error, warn, info, debug, trace.
install-dir /usr/local/bin Where to install the evp binary. Added to $GITHUB_PATH.

System requirements

The release tarball is a fully static musl + static-pie x86_64 binary, so it runs on every GitHub-hosted Linux runner (ubuntu-22.04, ubuntu-24.04, etc.) without any glibc version concern.

Required at runtime: nothing — ldd reports statically linked. No fontconfig, no display server, no ImageMagick, no ffmpeg, no Zig. Fonts are embedded into the binary. See the top-level System requirements section for details.

Self-hosted runners

The prebuilt tarball is x86_64-only. On non-amd64 hosts (arm64, macOS, Windows) you have two options:

  1. Use the published Docker image — see Docker fallback below.
  2. Build from source — install Zig 0.15.x and Rust, then cargo install --git https://github.com/HalFrgrd/evp evp.

Docker fallback

A multi-arch image is published on every push to master and on tag pushes:

- name: Render terminal demo
  run: |
    docker run --rm -v "$PWD:/work" \
      ghcr.io/halfrgrd/evp:latest \
      docs/demo.tape --output docs/demo.gif

The image is fully self-contained — libghostty-vt.a is statically linked into the binary — so no extra --volume for fonts or libraries is needed.

Status

Feature State
.tape parsing (Set / Type / Sleep / Wait / Hide / Show / Ctrl+X / Output / Env / Source / Require) working
PTY-backed shell, libghostty VT, diff-encoded Recording working
GIF renderer working
JSON renderer / serialisation of Recording working
Animated SVG renderer working (selectable text, ~10× smaller than GIF)
Screenshot, Copy / Paste, Set Theme, Set Margin*, Set WindowBar, Set BorderRadius, Set CursorBlink working
Multiple Output directives for .gif / .svg / .json working
Set LetterSpacing, Set LoopOffset, .mp4 / .webm / .txt / .ascii / PNG-frames outputs not implemented — tape will fail loudly with a clear error

See architecture.md for the design rationale. See the next section for the full VHS-vs-evp parity matrix.

VHS feature parity

evp consumes the same .tape script format as charmbracelet/vhs, but it is a much smaller project — it only implements the subset of VHS that maps cleanly onto an embedded libghostty + Rust renderer. Anything not in that subset fails loudly at parse or run time rather than silently no-op'ing, so a tape that produces a GIF on evp is guaranteed to have exercised every directive it contains.

Supported (matches VHS semantics)

VHS directive evp
Output <path> (.gif / .svg / .json; multiple allowed)
Set Shell <command...> ✅ accepts a full command line (e.g. bash, /bin/bash, bash --norc, bash --rcfile my.rc)
Set FontFamily <path-or-name> (path to a TTF/OTF/TTC) ⚠️ accepts a font file path; VHS resolves font family names via fontconfig. Pass --font /path/to/font.ttf on the CLI for the same effect.
Set FontSize <n>
Set Width <px> / Set Height <px>
Set Padding <px>
Set Margin <px> / Set MarginFill <color>
Set WindowBar <style> / Set WindowBarSize <px>
Set BorderRadius <px>
Set LineHeight <f>
Set Framerate <n> (also FrameRate, FPS)
Set PlaybackSpeed <f>
Set TypingSpeed <duration>
`Set Theme <name json>`
Set CursorBlink <bool>
Set WaitTimeout <duration>
Set WaitPattern /regex/
Type[@<duration>] "text" ... (single + double quotes + raw backticks)
Sleep <duration>
`Wait[+Screen +Line][@] [/regex/]`
Hide / Show
Backspace, Delete, Insert, Enter, Tab, Space, Escape (with optional @<duration> <count>)
Up / Down / Left / Right / PageUp / PageDown / Home / End
ScrollUp / ScrollDown (modelled as keys; see "Differences" below) ⚠️
Ctrl[+Alt][+Shift]+<char>
Screenshot <path.png>
Copy "..." / Paste ✅ (tape-local clipboard)
Env <KEY> <VALUE>
Require <program> (checked against $PATH; missing programs abort the run)
Source <path> (recursive, cycle-detected)
Comments (# to end-of-line)
evp extras: Set Cols <n> / Set Rows <n> (explicit cell-grid override) ✅ (no VHS equivalent)

Not implemented — tape fails loudly

If a tape uses any of the following, evp aborts with an error pointing back to this section, instead of silently dropping the directive on the floor:

VHS directive evp behaviour
Set LetterSpacing <px> parse-time error
Set LoopOffset <pct> parse-time error
Output out.mp4 / .webm / .txt / .ascii / PNG frames directory parse-time error (only .gif, .svg, and .json are written)

CLI / tooling not implemented

VHS ships several subcommands that evp does not provide:

  • vhs new <file> — tape scaffolder
  • vhs record — interactive ttyd recorder that writes a .tape
  • vhs publish <file> — uploads a GIF to vhs.charm.sh
  • vhs serve — SSH server that renders tapes for remote clients
  • vhs manual — built-in command reference

Differences that aren't bugs

These behaviours match VHS's documented semantics but rely on a different implementation, so the visual or runtime output may not be byte-identical:

  • Renderer. VHS records ttyd in a headless browser and re-encodes with ffmpeg/gifski. evp drives an embedded libghostty VT and rasterises cell-by-cell with ab_glyph + gifski. Antialiasing, glyph metrics, and dithering will differ slightly from a VHS recording of the same tape — the content is the same, the pixels may not be.
  • Font resolution. VHS uses the system font stack via the browser / fontconfig. evp ships JetBrains Mono embedded into the binary and treats Set FontFamily as a file path (or accepts --font path/to/font.ttf on the CLI). Passing a bare family name like "JetBrains Mono" will fall back to the embedded faces with a warning.
  • Default palette. VHS defaults to its built-in dark theme; evp now uses the same default theme and also accepts the full bundled VHS preset list from THEMES.md.
  • ScrollUp / ScrollDown. VHS implements these as smooth multi-frame mouse-wheel scrolls. evp models them as discrete key presses fed to the PTY; programs that respond to mouse wheel events in mouse-tracking mode (e.g. less) won't react to them.
  • GitHub Action. VHS has charmbracelet/vhs-action; evp ships its own composite action — see Using evp in GitHub Actions. Their inputs are not interchangeable.
  • Zero runtime dependencies. VHS requires ttyd and ffmpeg on $PATH. evp's release binary is statically linked and needs neither.

Acknowledgments

VHS

evp is based on the vhs project. They share little code but evp does try use the same .tape file format.

The color themes in assets/vhs-themes.json are taken from the VHS project and are licensed under the MIT License. See licenses/VHS-MIT.txt for the full license text.

Copyright (c) 2022-2023 Charmbracelet, Inc

About

A terminal recoder powered by libghostty and rust.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors