Skip to content

Screenshots

Termless generates screenshots from terminal state. SVG is built in with zero dependencies. PNG output flows through Terminal.screenshot() — an auto-picker that prefers the native canvas pipeline (@napi-rs/canvas + ghostty-web WASM, via @termless/ghostty) and falls back to @resvg/resvg-js on hosts where the native canvas isn't available. There's no Chromium dependency anywhere in the pipeline.

For one-off command-output images, use termless record --screenshot. For reproducible docs, demos, and regression fixtures, write a .tape and render it with termless play. Both paths render from parsed terminal state, so ANSI styling, cursor state, and screen geometry stay inspectable before they become an image.

Basic Usage

typescript
import { createTerminal } from "@termless/core"
import { createXtermBackend } from "@termless/xtermjs"

const term = createTerminal({ backend: createXtermBackend(), cols: 80, rows: 24 })
term.feed("\x1b[1;38;2;255;85;85mError:\x1b[0m file not found")

const svg = term.screenshotSvg()
const png = await term.screenshot() // auto-picker: native canvas with resvg fallback
const canvasPng = await term.screenshotCanvasPng() // explicit native canvas (requires @termless/ghostty)
const resvgPng = await term.screenshotPng() // explicit resvg (requires @resvg/resvg-js)

Saving to File

typescript
import { writeFile } from "node:fs/promises"

// SVG
const svg = term.screenshotSvg()
await writeFile("/tmp/terminal.svg", svg, "utf-8")

// PNG via the auto-picker (preferred for routine use)
const png = await term.screenshot()
await writeFile("/tmp/terminal.png", png)

Options

screenshotSvg() accepts an optional SvgScreenshotOptions object:

typescript
const svg = term.screenshotSvg({
  fontFamily: "'JetBrains Mono', monospace",
  fontSize: 16,
  cellWidth: 9.6,
  cellHeight: 20,
  padding: 16,
  borderRadius: 8,
  windowBar: "colorful",
  margin: 24,
  marginFill: "#111827",
  theme: {
    foreground: "#f8f8f2",
    background: "#282a36",
    cursor: "#f8f8f0",
    palette: { 1: "#ff5555", 2: "#50fa7b" },
  },
})
OptionTypeDefaultDescription
fontFamilystring"'Menlo', 'Monaco', 'Courier New', monospace"CSS font-family for text
fontSizenumber16Font size in px
cellWidthnumber9.6Character cell width in px
cellHeightnumber20Character cell height in px
themeSvgTheme(dark theme)Color theme
paddingnumber0Padding between terminal cells and the frame
borderRadiusnumber0Radius for the terminal background rectangle
windowBar"none" | "rings" | "colorful""none"Optional macOS-style window controls
windowBarSizenumber40Height reserved for the window bar
marginnumber0Outer image margin
marginFillstringtransparentFill color behind the terminal frame and margin

Polished Frames

Use visual frame options when the screenshot is meant for docs, blog posts, or release notes:

typescript
const png = await term.screenshotPng({
  theme: { background: "#111827", foreground: "#f9fafb" },
  padding: 18,
  borderRadius: 8,
  windowBar: "colorful",
  margin: 24,
  marginFill: "#0b1020",
})

The same options are available from .tape settings and termless play flags:

tape
Set Theme "github-dark"
Set Padding 18
Set BorderRadius 8
Set WindowBar "colorful"
Set Margin 24
Set MarginFill "#0b1020"

Type "bun test"
Enter
Sleep 1s
Screenshot build.png
bash
termless play demo.tape -o demo.png --window-bar colorful --padding 18 --margin 24 --margin-fill '#0b1020'

Command Output Captures

For a single command, termless record --screenshot is the shortest path:

bash
termless record --cols 100 --rows 30 --screenshot /tmp/listing.png -- ls -la

This runs the command in a PTY, waits for content, captures the terminal state, and writes SVG or PNG based on the filename extension. Add --keys and --wait-for for simple interactive captures:

bash
termless record --wait-for "ready>" --keys j,j,Enter --screenshot /tmp/app.svg -- bun km view /path/to/vault

Use .tape when the capture needs setup, timing, hidden commands, multiple screenshots, or a future GIF/APNG/SVG recording.

SvgTheme

FieldTypeDefaultDescription
foregroundstring"#d4d4d4"Default text color
backgroundstring"#1e1e1e"Background color
cursorstring"#aeafad"Cursor color
paletteRecord<number, string>--Override ANSI palette colors (0-255)

PNG Options

screenshotPng() accepts a PngScreenshotOptions object (extends SvgScreenshotOptions with an additional scale option):

typescript
const png = await term.screenshotPng({
  scale: 2, // default: 2 (retina-quality)
  theme: { background: "#282a36" },
})
OptionTypeDefaultDescription
scalenumber2Render scale factor (1 = actual size)

All SvgScreenshotOptions (fontFamily, fontSize, cellWidth, cellHeight, theme) are also accepted.

Native Canvas Options

screenshot() (auto-picker) and screenshotCanvasPng() accept a ScreenshotOptions object. When the native canvas path is selected (the default whenever @termless/ghostty is installed), bytes go through ghostty-web's CanvasRenderer + @napi-rs/canvas — same renderer ghostty.org's terminal uses, just driven by JS.

Install @termless/ghostty to enable the native canvas path:

bash
bun add -d @termless/ghostty
typescript
const png = await term.screenshot({
  fontSize: 14,
  fontFamily: "'Fira Code', monospace",
  fontPath: "/path/to/iosevka.ttf",
  theme: { background: "#1a1b26", foreground: "#c0caf5" },
  dpr: 2,
})

Use the auto-picker for routine work. Use screenshotCanvasPng() when you specifically need the native canvas path and don't want a silent fallback to resvg (e.g., visual-regression tests that pin the ghostty renderer's output).

OptionTypeDefaultDescription
colsnumberbackend colsOverride grid width
rowsnumberbackend rowsOverride grid height
fontSizenumber16Glyph size in CSS px
fontFamilystring"monospace"CSS font family
fontPathstring--Path to .ttf/.otf bundled as the first family
dprnumber2Device pixel ratio
themeCanvasThemeTokyo Night StormColor palette

Standalone Functions

You can also use the screenshot functions directly on any TerminalReadable:

typescript
import { screenshotSvg, screenshotPng } from "@termless/core"
import { renderTerminalPng } from "@termless/ghostty"

const svg = screenshotSvg(term, { theme: { background: "#000" } })
const resvgPng = await screenshotPng(term, { scale: 3 }) // explicit resvg path
const canvasPng = await renderTerminalPng(term, { fontSize: 14 }) // explicit native canvas path

What Gets Rendered

  • Text content with proper monospace positioning
  • Foreground colors (ANSI 16, 256-color, truecolor)
  • Background colors (merged into rectangles for adjacent same-color cells)
  • Bold (via font-weight="bold")
  • Italic (via font-style="italic")
  • Faint/dim (via opacity="0.5")
  • Underline (via text-decoration="underline")
  • Strikethrough (via text-decoration="line-through")
  • Inverse video (fg/bg swapped)
  • Wide characters (double-width CJK, emoji)
  • Cursor (block, underline, or beam -- with semi-transparent overlay)