Skip to content

Recording

A Recording is a captured terminal session: the captured counterpart of a live Terminal. Where a Terminal is a session happening now, a Recording is that session frozen onto a timeline you can replay, scrub, animate, and diff.

A Recording is one concept — not five. The file formats it serializes to (.tape, .cast, .tty/.ttyz) are encodings, not separate things. Naming the concept after a serialization would be the JPEG-vs-Photo error: the photo is the thing; JPEG is one way to store it.

The three tracks

A Recording is a timeline carrying up to three tracks. Two are sources — the session itself — and one is a projection — a derived view of it.

TrackTierHolds
commandssource — intenttimed high-level instructions: key presses, Type, Sleep, Resize
iosource — observedtimed raw byte events, direction-tagged in / out
framesprojection — derivedrendered visual states + capture metadata (dirty regions, ANSI, PNG)

commands and io are causal — they are the session. commands is the intent (what you asked the terminal to do); io is the observed truth (the exact bytes that flowed). frames is an effect — a materialized view of io × Renderer. A frame can be regenerated; mutating one does not change the session.

A Recording is valid with any non-empty subset of tracks: a hand-authored .tape carries commands only; a live record carries commands + io; a frame trace adds the frames projection.

Track authority

When more than one track is present:

  • io is the authoritative observationplay uses it for byte-exact reproduction.
  • commands is the authoritative intentplay defaults to it because it is editable.

All tracks share one monotonic clock in integer microseconds. Float timestamps (as .cast uses) drift, so termless normalizes them on import.

Visual traces are a Recording projection

A visual trace — the frame tracer's on-disk directory (index.jsonl + NNNNN.png, one TraceFrame row per line) — is not a parallel format sitting beside a Recording. It is a Recording whose frames projection is populated. TraceFrame, the on-disk row, is the serialization of one frame plus its render artifacts: the wall-clock capture instant and the per-frame render cost (render_ms). Those two facts are the only things a rendered frame carries that the timeline cannot derive, so the projection carries them in a small artifacts bag on each Frame — and with them, the frames projection is the lossless carrier of a visual trace.

TraceFrame (on-disk row)  ⇄  Frame (projection entry) + RenderArtifacts

The conversion is one symmetric codec pair — traceToRecording (rows → Recording) and recordingToTraceFrames (Recording → rows). Every consumer that needs the on-disk shape routes through that one pair: the .tty/.ttyz writer, the writeVisualTraceFromRecording disk writer, and the browser viewer. There is no second Frame → row projection. Because the artifacts survive the round trip, recordingToTraceFrames(traceToRecording(rows)) reproduces rows byte-for-byte, so a trace can be written straight from the canonical Recording noun and the bytes are identical to what the raw-TraceFrame[] path would write. (The one exception: a frame annotated with a silvery render-join event keeps only the dependency-free subset of that event on the projection's signal field, so a silvery-annotated trace is not byte-lossless on that one field. Traces recorded without a silvery sidecar — the default — round-trip exactly.)

The naming follows the program's rule that visual/recording things are frames (not "messages", which are wire things): Frame, TraceFrame, RenderArtifacts.

The verbs

Everything you do with a Recording is one of the four verbs:

  • record captures a Terminal into a Recording. record --frames opts into populating the frames projection.
  • view presents a Recording — scrub it in a browser, animate it to a GIF/APNG/SVG, or embed it with the web player.
  • play re-executes a Recording into a Terminal. Its automatic source prefers a non-empty commands track (editable), then discloses a fallback to io (byte-exact). For native .tty and .ttyz inputs, --source=commands or --source=io forces one track and refuses when that track is absent or empty; other input formats reject --source.
  • compare diffs one Recording across N backends.

Formats

A Recording serializes to and from three on-disk formats. Formats are encodings — see the Formats reference for full specs.

FormatRole
.tapecharm/VHS interop. A compiler input — .tape → commands track. Round-trip is lossy.
.castasciinema interop. A symmetric codec.cast ⇄ io track, lossless.
.tty/.ttyztermless's own native format — a live bundle directory sealed into a single-file archive, carrying every track losslessly.

.tape is special: it is a scenario compiler, not a symmetric codec. Type "hi"expands into key events with timing; Sleep is a player directive. Going back out (Recording → .tape) is best-effort. .cast is a true codec — the io track round-trips losslessly. .tty/.ttyz is termless's lossless canonical form.

For how-to material see the Recording Sessions and Tracing Visual Bugs guides.