Skip to Content
steep

Sessions and commands

Open a terminal, run a command in it, send input, wait for the screen, and replay recordings.

A reel.Session owns a terminal emulator, and logs everything done through it.

s, err := reel.Open(ctx, 80, 24, reel.WithTitle("demo"))
if err != nil {
	return err
}
defer s.Close()

A program writes output with s.Write and reads input with s.Read, which makes a session an io.ReadWriter you can hand to a program, such as a tea.Program with tea.WithInput and tea.WithOutput. Or use a driver: Exec for a command, and Attach for a recording.

Input

MethodSends
Type(text, opts...)Text, as a key press per character on a US layout
Key(name, opts...)One key, such as enter, ctrl+c, shift+tab, or f5
Paste(text)A paste, bracketed if the program enabled it
Click(button, x, y, opts...)A press and release at a cell
Drag(button, from, to, opts...)A press, motion through each cell, and a release
Mouse(action, button, x, y)A single mouse event, if the program asked for them

Keys are encoded for the keyboard mode the program set. reel.Delay pauses after each key press, and reel.Repeat repeats it.

The terminal

MethodDoes
Resize(cols, rows)Resizes the terminal, and an Exec process's pseudo-terminal
Focus(on)Sets focus, and tells a program that asked
ScrollUp, ScrollDown, ScrollToTop, ScrollToBottomMoves the viewport, without telling the program
SetScrollbackSize(lines), ClearScrollback()Manages history
SetForeground, SetBackground, SetCursorColorSets colors, and their SetDefault* forms set the fallbacks
Mark(label), Annotate(a)Adds to the timeline

Waiting

Wait blocks until a condition holds, checking whenever the screen changes. Pass a context with a deadline to time out:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

res, err := s.Wait(ctx, reel.Contains("ready"))

The conditions are Contains, NotContains, Matches, NotMatches, ScreenFunc, and OnCursorLine. A failed wait returns a *WaitError, which has the screen it last saw.

MethodWaits until
Wait(ctx, cond)A condition holds
WaitSettle(ctx, quiet)The screen's text hasn't changed for quiet
WaitIdle(ctx, since, quiet, limit)The program has responded to something and gone quiet

Reading

MethodReturns
Screen()The screen, as a *reel.Screen, which marshals to the JSON snapshots use
ScreenText()The text as Wait checks it
State()The terminal's state: title, cursor, progress, and more
Terminal()The vt.Terminal, for reading. Changes made through it aren't recorded.
Timeline()The timeline

Options

OptionDoes
WithTitle(title)Names the recording
WithTypingDelay(d)Pauses after each key press, so a recording shows typing
WithSkipPristine()Skips the bare terminal before the first paint and after the last, for programs that write queries first or clear the screen as they exit, as Bubble Tea does
WithScrollback(lines)Sets how much history is kept
WithScrollOnInput(enabled)Sets whether input scrolls a scrolled viewport back down
WithSource(enabled), WithSourceSkip(pkgs...)Records where in Go source each mark and annotation came from
WithoutQueries()Leaves out query annotations
WithCellSize(w, h)Sets the pixel size of a cell that programs read
WithKittyFiles(), WithKittyStorage(n)Controls Kitty graphics
WithRuntime(rt)Opens the terminal from your own pool
WithUnknownSequence(fn)Calls fn with sequences the terminal drops

Sessions are safe for concurrent use. Close returns the terminal to its pool, and the timeline stays readable afterward.

Writing without a program

A session doesn't need a program. You can write to it directly, which suits generating a demo or reproducing a bug from escape sequences:

s.Write([]byte("\x1b[1mBold\x1b[0m, and a \x1b]8;;https://getsteep.dev\x07link\x1b]8;;\x07\r\n"))

See _examples/reel-export.

Running commands

reel.Exec runs a command in a session, on a pseudo-terminal, and records when it exits. The command can be anything that runs in a terminal.

cmd := exec.CommandContext(ctx, "bash", "--norc")
proc, err := reel.Exec(ctx, s, cmd)
if err != nil {
	return err
}

s.Wait(ctx, reel.OnCursorLine(reel.Contains("$ ")))
s.Type("ls\n")
s.Wait(ctx, reel.OnCursorLine(reel.Contains("$ ")))
s.Type("exit\n")

status, err := proc.Wait()

Process.Wait returns the exit status. On Unix, a command killed by a signal has status 128 plus the signal, as shells report it.

Resizing the session resizes the pseudo-terminal, and the command gets a SIGWINCH.

What a recording can expose

A command gets cmd.Env, or your whole environment if that's nil, and in CI or your shell that can include tokens. reel doesn't write the environment into a recording, but it records everything the command prints and every key typed. A .cast file keeps all of it as plain text. So a shell running env, a program printing its config, or a debug log can put a secret in the file.

Give the command only what it needs:

cmd := exec.CommandContext(ctx, "bash", "--norc")
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + os.Getenv("HOME")}

Redact hides a value from images and videos. It can't help a .cast file, so a profile with Redact can't write one. If a recording might hold a secret, don't share its .cast.

Bubble Tea, in your process

To record a tea.Program without building a binary, connect it to the session directly:

s, err := reel.Open(ctx, 80, 24, reel.WithSkipPristine())
// ...
p := tea.NewProgram(model, tea.WithInput(s), tea.WithOutput(s), tea.WithWindowSize(80, 24))

Session.Write acts like a terminal in its default, cooked mode: each LF is written as CR LF, as a pseudo-terminal's ONLCR does, so a program starts a new line with LF. On Windows, Bubble Tea writes LF to mean only "down a row", so open the session with reel.WithoutONLCR() there. Use WithoutONLCR too when you forward output that already went through a pseudo-terminal, such as an SSH session's. reel.Exec and reel.Attach always write their output as is.

WithSkipPristine leaves out the blank terminal before the first paint, since Bubble Tea writes capability queries first, and the one it leaves after leaving the alternate screen. A blank terminal between two paints, such as a program exiting before the next command runs, is kept.

To do this in a test, steep is built for it.

Replaying recordings

reel can export what was recorded elsewhere.

asciicast

reel.ReadTimeline reads a v2 or v3 file, such as one from asciinema, which exports the same way as reel's own.

script(1)

reel.Attach replays output recorded elsewhere into a session. Pass the timing file with WithTiming, for the output's times. Without one, the output plays at once.

script --log-out typescript --log-timing timing --command htop
s, err := reel.Open(ctx, 80, 24)
// ...
typescript, err := os.Open("typescript")
// ...
timing, err := os.Open("timing")
// ...
if err := reel.Attach(ctx, s, typescript, reel.WithTiming(timing)); err != nil {
	return err
}
err = reel.Export(ctx, s.Timeline(), reel.Profile{Outputs: reel.MustFiles("htop.gif")})

The session's size has to match the terminal the recording was made in, since the typescript doesn't say. See _examples/reel-attach, and reel attach for the same from the CLI.

Last updated on

On this page