Skip to Content
steep

How it works

How a session becomes files, the event log behind every recording, and exporting compared with recording live.

reel splits recording from rendering. A session only logs events, and an export turns the log into files later.

ProgramSessionHost: keys, resizes, marksTimelineReplay in a fresh terminalRasterizerEffects, by prioritySinks: GIF, MP4, WebM,WebP, PNG.cast.snap, .json

Recording

A driver connects a program to a session. The program writes to the session and reads from it, and the host types, clicks, resizes, and adds marks. Each of these is appended to a timeline, so recording costs one copy of the bytes.

Replaying

reel.Export replays the timeline into a new terminal, and each visible change becomes a frame: program output, or a change of focus or colors. Nothing is drawn at a fixed rate. Frames stay on screen for as long as they did when recorded, on a grid of delays set by Profile.FPS.

Since this works from the log alone, the output is deterministic. A timeline and its saved .cast export to the same files, apart from invalid UTF-8 output (saved as U+FFFD) and cuts.

Drawing and effects

A Rasterizer draws each frame, by default a renderer built from Profile.Render. Then each frame runs through the effects, lowest priority first. An effect can change a frame, drop it, or add more.

Writing

Frames go to every output's sink. Not every output takes this whole path:

OutputComes fromEffects?
.gif, .mp4, .webm, .webp, .pngThe end of the pipelineYes
.png?marks=...A frame at each markOnly concealers and still effects
.snap, .jsonThe replay's screenNo, so they show what a redaction hides
.castThe timeline itselfNo, they're recorded as annotations instead

All outputs are checked and created before any frame is drawn, and each is written to a temporary file and moved into place when complete. So a missing ffmpeg fails at once, and a failure leaves an existing file as it was.

Timelines and asciicast

A reel.Timeline is an append-only log of what the program wrote, what it was sent, and what the host did to its terminal. Each event has an offset in whole milliseconds. It holds no frames, since those are derived by replaying it.

Events

EventWhat it is
OutputBytes the program wrote
InputBytes the terminal sent the program
Resize, Focus, Scroll, ScrollbackSizeWhat the host did to the terminal
ThemeA color change
MarkA named point
AnnotationA structured note, which players ignore
ExitThe end of a command run with Exec

Read them with iterators:

for ev := range tl.Events() { /* every event */ }
for m := range tl.Marks() { fmt.Println(m.At(), m.Label) }
for in := range tl.Inputs() { /* what was typed */ }
for a := range tl.Annotations() { /* notes */ }

A session's timeline grows as it runs, and iterators see events as of the call. tl.Updated() returns a channel that closes when the next event arrives, to follow one live.

Marks

A mark names a moment. Use the form category:value, so tools can group them, with reel.SplitLabel:

s.Mark("login:done")

Effects and stills address a recording by its marks, and a label doesn't have to be unique.

Cutting

Between returns the part of a timeline between two marks. See Cuts.

Saving and reading

A timeline saves as an asciicast file, which any asciicast player reads:

f, err := os.Create("demo.cast")
// ...
_, err = tl.WriteCast(f, reel.CastV2)

reel.ReadTimeline reads a v2 or v3 file back, whether reel wrote it or asciinema recorded it. Comment and blank lines, unknown header fields, and unknown event codes are skipped.

A cut timeline is saved as just its window, opening on a redraw of the screen at its start. Only the visible screen is redrawn, so a cut that starts on the alternate screen and later leaves it shows a blank primary screen. An export of the cut doesn't have that problem.

Export vs live Record

reel.Export works from a finished timeline. reel.Record writes the same outputs while the session runs, replaying each event as it's appended, so frames are drawn and encoded live.

rec, err := reel.Record(ctx, s, reel.Profile{
	Outputs: reel.MustFiles("demo.gif"),
	Effects: []reel.Effect{effects.Keys(time.Second)},
})
if err != nil {
	return err
}

// ... drive the program ...

if err := rec.Stop(); err != nil {
	return err
}

Stop ends the recording on the events so far and writes its outputs. It's safe to call more than once. Canceling ctx abandons the recording and discards its outputs.

Which to use

Almost always Export. It's simpler, a recording doesn't cost anything while the program runs, and you can export a timeline as many times as you want. Record is for long sessions, where you'd rather encode as you go than hold everything until the end.

How they differ

ExportRecord
Draws framesAfter the factWhile the program runs
{duration} in a pathAvailableAn error, since it isn't known
{timestamp}The timeline's startWhen the timeline started, or now if it's empty
Outputs with match=YesNo, since it starts before the marks exist
A mark that never comesWrites the rest, returns a NoMarkErrorThe same, from Stop
Effects seeA complete timelineA growing one, with Context.Live set
RedactInputsHides secrets from the first characterShows the first characters, until enough are typed
EaseInEases in before a markEases in after it

Last updated on

On this page