Skip to content

feat: add Subtitle command for text overlays on recordings#726

Draft
gtmax wants to merge 1 commit intocharmbracelet:mainfrom
gtmax:feat/subtitle-command
Draft

feat: add Subtitle command for text overlays on recordings#726
gtmax wants to merge 1 commit intocharmbracelet:mainfrom
gtmax:feat/subtitle-command

Conversation

@gtmax
Copy link
Copy Markdown

@gtmax gtmax commented Mar 22, 2026

Discussion: #727

Summary

Adds a Subtitle command that renders text overlays on terminal recordings — useful for demos, tutorials, and walkthroughs where you want to explain what's happening on screen.

Related: #173 (Progress bars & Overlays)
Related: #164 (display key names — the overlay infrastructure added here could be extended to support keystroke display)

Tape Syntax

# Show a subtitle
Subtitle "This is what's happening"

# Change the subtitle
Subtitle "Now something else"

# Hide the subtitle
Subtitle ""

Settings

Set SubtitleFontSize 28
Set SubtitleFontFamily "system-ui"
Set SubtitleColor "#ffffff"
Set SubtitleBackground "rgba(0,0,0,0.75)"
Set SubtitlePosition "bottom"       # top, center, bottom
Set SubtitlePadding 12
Set SubtitleBorderRadius 8

The subtitle DSL is deliberately kept simple for now — settings cover the common styling needs, and the implementation can be swapped or extended later without changing the tape syntax.

Example

Here's a real-world demo GIF using subtitles to narrate a terminal workflow (wotr):

wotr demo with subtitles

Implementation: Canvas Overlay Stream

The subtitle is rendered onto a separate <canvas> element and captured as a third frame stream alongside the existing text and cursor streams. ffmpeg composites it on top of the fully styled terminal frame (after padding, window bar, border radius, and margins).

Why Canvas and not HTML/CSS?

HTML/CSS would give richer styling, but Element.Screenshot() and Page.Screenshot() in Chrome DevTools Protocol do not preserve transparency — they always composite against the page background. Since VHS needs transparent PNGs for its ffmpeg compositing pipeline, only Canvas.toDataURL() (via CanvasToImage) works, as it returns canvas content with alpha channel intact.

Why not ASS subtitles (burned in via ffmpeg)?

ffmpeg's ass/subtitles filter requires libass, which is not included in standard Homebrew ffmpeg builds. Requiring users to compile ffmpeg with --enable-libass would break the install experience. The canvas approach requires no additional ffmpeg dependencies beyond what VHS already uses.

Performance

When no Subtitle commands are present in the tape, the overlay stream is not created and there is zero overhead. The tape is pre-scanned for Subtitle commands before recording begins.

Changes

  • token/token.go — Add SUBTITLE command token and subtitle setting tokens
  • parser/parser.go — Add parseSubtitle() parser and register in command dispatcher
  • command.go — Add ExecuteSubtitle and subtitle setting executors
  • vhs.go — Add SubtitleOptions, overlay canvas setup, renderOverlay() method
  • evaluator.go — Pre-scan for Subtitle commands to enable overlay stream
  • video.go — Add overlay frame format, third input stream, HasOverlay flag
  • ffmpeg.go — Add WithOverlay filter builder, overlayStream on StreamBuilder
  • command_test.go — Update command count
  • parser/parser_test.go — Add subtitle parser tests

Adds a Subtitle command that renders text overlays on terminal recordings.
Subtitles are drawn onto a separate overlay canvas, captured as a third
frame stream, and composited in ffmpeg after all terminal styling (padding,
window bar, margins) is applied.

Tape syntax:

  Subtitle "text to display"
  Subtitle ""                  # hide

Settings:

  Set SubtitleFontSize 28
  Set SubtitleFontFamily "system-ui"
  Set SubtitleColor "#ffffff"
  Set SubtitleBackground "rgba(0,0,0,0.75)"
  Set SubtitlePosition "bottom"   # top, center, bottom
  Set SubtitlePadding 12
  Set SubtitleBorderRadius 8

## Implementation

Uses a canvas-based overlay approach:

1. A separate <canvas> element is created, sized to the full output
   dimensions (matching padding, margins, window bar).
2. On each frame capture tick, the subtitle text is drawn onto this
   canvas using Canvas 2D API (fillRect for background pill,
   fillText for the text).
3. The overlay canvas is captured as a third PNG frame stream
   alongside the existing text and cursor streams.
4. ffmpeg composites the overlay on top of the fully styled terminal
   frame (after padding, window bar, border radius, and margins).

### Why not HTML/CSS overlay?

DOM elements can be styled with CSS but Element.Screenshot() and
Page.Screenshot() in Chrome DevTools Protocol do not preserve
transparency — they always composite against the page background.
Since VHS needs transparent PNGs for its ffmpeg compositing pipeline,
only Canvas.toDataURL() (via CanvasToImage) works, as it returns the
canvas content with alpha channel intact.

### Why not ASS subtitles?

ffmpeg's ass/subtitles filter requires libass, which is not included
in standard Homebrew ffmpeg builds. Requiring users to compile ffmpeg
with --enable-libass would break the install experience. The canvas
approach requires no additional ffmpeg dependencies.

### Performance

When no Subtitle commands are present in the tape, the overlay stream
is not created and there is zero overhead. The tape is pre-scanned
for Subtitle commands before recording begins.

Closes charmbracelet#173
Closes charmbracelet#164
@gtmax gtmax requested a review from a team as a code owner March 22, 2026 16:32
@gtmax gtmax requested review from meowgorithm and raphamorim and removed request for a team March 22, 2026 16:32
@gtmax gtmax marked this pull request as draft March 22, 2026 16:50
@gtmax
Copy link
Copy Markdown
Author

gtmax commented Mar 22, 2026

Per the contributing guidelines, opened a Discussion for this feature: #727

Converting to draft until the team has a chance to review the approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant