Documentation
¶
Overview ¶
Package termsession provides terminal session recording and playback in asciinema v2 format.
This package enables you to record interactive terminal sessions (PTY), save them as .cast files (asciinema v2 format), and play them back with accurate timing. It's ideal for creating terminal demos, testing terminal applications, and building CLI tutorials.
Recording Sessions ¶
Record an interactive PTY session:
session, _ := termsession.NewSession(termsession.SessionOptions{
Command: []string{"bash"},
})
session.Record("session.cast", RecordingOptions{
Compress: true,
Title: "My Demo",
})
session.Wait()
Record directly without a PTY:
recorder, _ := termsession.NewRecorder("output.cast", 80, 24, RecordingOptions{})
recorder.RecordOutput("Hello, World!\n")
recorder.Close()
Playing Back Sessions ¶
Play back a recorded session with timing preserved:
player, _ := termsession.NewPlayer("session.cast", PlayerOptions{
Speed: 2.0, // 2x speed
MaxIdle: 1.0, // Cap idle time at 1 second
})
player.Play() // Blocks until complete
Control playback dynamically:
go player.Play() time.Sleep(time.Second) player.Pause() time.Sleep(time.Second) player.Resume() player.SetSpeed(3.0)
Loading and Analyzing Recordings ¶
Load a .cast file to inspect or process events:
header, events, _ := termsession.LoadCastFile("session.cast")
fmt.Printf("Duration: %.2fs\n", termsession.Duration(events))
fmt.Printf("Terminal size: %dx%d\n", header.Width, header.Height)
Filter and process events:
outputOnly := termsession.OutputEvents(events)
for _, event := range outputOnly {
fmt.Printf("[%.2fs] %s", event.Time, event.Data)
}
Format Details ¶
The package uses asciinema v2 format (.cast files), which is a simple JSON-based format:
- First line: JSON header with metadata (version, dimensions, title, etc.)
- Subsequent lines: JSON arrays [time, type, data] representing events
- Supports gzip compression automatically
- Compatible with asciinema.org and other asciinema tools
Index ¶
- Variables
- func Duration(events []RecordingEvent) float64
- func LoadCast(r io.Reader) (*RecordingHeader, []RecordingEvent, error)
- func LoadCastFile(filename string) (*RecordingHeader, []RecordingEvent, error)
- type Player
- func (p *Player) EventCount() int
- func (p *Player) GetDuration() float64
- func (p *Player) GetHeader() RecordingHeader
- func (p *Player) GetPosition() float64
- func (p *Player) GetProgress() float64
- func (p *Player) IsPaused() bool
- func (p *Player) Pause()
- func (p *Player) Play() error
- func (p *Player) Resume()
- func (p *Player) Seek(seconds float64)
- func (p *Player) SetLoop(loop bool)
- func (p *Player) SetSpeed(speed float64)
- func (p *Player) Speed() float64
- func (p *Player) Stop()
- func (p *Player) TogglePause()
- type PlayerOptions
- type Recorder
- type RecordingEvent
- type RecordingHeader
- type RecordingOptions
- type Session
- func (s *Session) Close() error
- func (s *Session) ExitCode() int
- func (s *Session) IsRecording() bool
- func (s *Session) PauseRecording()
- func (s *Session) Record(filename string, opts RecordingOptions) error
- func (s *Session) Resize(width, height int) error
- func (s *Session) ResumeRecording()
- func (s *Session) Start() error
- func (s *Session) Wait() error
- type SessionOptions
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrPlayerStopped = errors.New("player has been stopped and cannot be restarted")
ErrPlayerStopped is returned when Play() is called on a stopped player.
Functions ¶
func Duration ¶
func Duration(events []RecordingEvent) float64
Duration returns the total duration of a recording in seconds.
The duration is determined by the timestamp of the last event. Returns 0 if there are no events.
Example ¶
ExampleDuration demonstrates calculating recording duration.
events := []RecordingEvent{
{Time: 0.0, Type: "o", Data: "Start"},
{Time: 1.5, Type: "o", Data: "Middle"},
{Time: 3.0, Type: "o", Data: "End"},
}
duration := Duration(events)
fmt.Printf("Duration: %.1f seconds\n", duration)
Output: Duration: 3.0 seconds
func LoadCast ¶
func LoadCast(r io.Reader) (*RecordingHeader, []RecordingEvent, error)
LoadCast loads a .cast recording from an io.Reader.
This function automatically detects and handles gzip compression by checking the magic bytes. It parses the asciinema v2 format: first line is a JSON header, subsequent lines are JSON arrays representing events.
Malformed events are silently skipped to handle recordings with corruption.
Use this when you have a recording in memory or from a network stream. For loading from a file path, use LoadCastFile instead.
func LoadCastFile ¶
func LoadCastFile(filename string) (*RecordingHeader, []RecordingEvent, error)
LoadCastFile loads a .cast file from disk and returns its contents.
The file is automatically decompressed if it's gzip-compressed. This is a convenience wrapper around LoadCast that opens the file for you.
Returns:
- RecordingHeader: The file metadata (dimensions, title, etc.)
- []RecordingEvent: All events in chronological order
- error: Any error encountered while loading
Example:
header, events, err := termsession.LoadCastFile("recording.cast")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording is %.2f seconds long\n", events[len(events)-1].Time)
Example ¶
ExampleLoadCastFile demonstrates loading and inspecting a .cast file.
// Create a sample recording
tmpfile := filepath.Join(os.TempDir(), "load-example.cast")
defer os.Remove(tmpfile)
recorder, _ := NewRecorder(tmpfile, 80, 24, RecordingOptions{
Compress: false,
Title: "Load Example",
})
recorder.RecordOutput("Hello, World!\n")
recorder.Close()
// Load it back
header, events, err := LoadCastFile(tmpfile)
if err != nil {
panic(err)
}
fmt.Printf("Terminal size: %dx%d\n", header.Width, header.Height)
fmt.Printf("Title: %s\n", header.Title)
fmt.Printf("Events: %d\n", len(events))
Output: Terminal size: 80x24 Title: Load Example Events: 1
Types ¶
type Player ¶
type Player struct {
// contains filtered or unexported fields
}
Player plays back recorded terminal sessions with timing preservation.
Player loads asciinema v2 format recordings and plays them back to an io.Writer, preserving the original timing between events. It supports speed adjustment, pause/resume, seeking, and looping.
Playback is performed in a blocking manner by Play(), or you can run it in a goroutine and control it with the various control methods.
All methods are safe for concurrent use.
func NewPlayer ¶
func NewPlayer(filename string, opts PlayerOptions) (*Player, error)
NewPlayer creates a new player from a .cast recording file.
The recording file is loaded completely into memory. Files can be gzip-compressed (detected automatically). Invalid or malformed events are silently skipped during loading.
Example:
player, err := NewPlayer("demo.cast", PlayerOptions{
Speed: 2.0,
MaxIdle: 1.0,
Output: os.Stdout,
})
if err != nil {
log.Fatal(err)
}
player.Play() // Blocks until playback completes
Example ¶
ExampleNewPlayer demonstrates basic playback.
// Create a sample recording
tmpfile := filepath.Join(os.TempDir(), "player-example.cast")
defer os.Remove(tmpfile)
recorder, _ := NewRecorder(tmpfile, 80, 24, RecordingOptions{
Compress: false,
})
recorder.RecordOutput("Hello from recording!\n")
recorder.Close()
// Play it back
var buf bytes.Buffer
player, err := NewPlayer(tmpfile, PlayerOptions{
Output: &buf,
Speed: 1.0,
})
if err != nil {
panic(err)
}
player.Play()
fmt.Print(buf.String())
Output: Hello from recording!
func (*Player) EventCount ¶
EventCount returns the total number of events in the recording.
func (*Player) GetDuration ¶
GetDuration returns the total duration of the recording in seconds.
If MaxIdle was configured, returns the adjusted duration (with idle times capped). Returns 0 if the recording has no events.
func (*Player) GetHeader ¶
func (p *Player) GetHeader() RecordingHeader
GetHeader returns the recording metadata.
This includes terminal dimensions, title, timestamp, and environment variables.
func (*Player) GetPosition ¶
GetPosition returns the current playback position in seconds.
This is the timestamp of the current event being played. If MaxIdle was configured, returns the adjusted position.
func (*Player) GetProgress ¶
GetProgress returns playback progress as a fraction between 0.0 and 1.0.
Returns 0.0 at the start, 1.0 at the end, and values in between during playback.
func (*Player) Pause ¶
func (p *Player) Pause()
Pause pauses playback.
Playback can be resumed with Resume(). While paused, no events are written to the output. The pause time is tracked internally to prevent time jumps when resuming.
func (*Player) Play ¶
Play starts playback of the recording.
This method blocks until playback completes, is stopped via Stop(), or an error occurs. Events are written to the configured output writer with timing preserved according to the speed multiplier.
If Loop is enabled, playback will restart from the beginning when it reaches the end. Call Stop() from another goroutine to end looped playback.
After Stop() is called, the player cannot be restarted. Create a new player if you need to play the recording again.
Example:
// Blocking playback err := player.Play() // Playback in background with controls go player.Play() time.Sleep(5 * time.Second) player.Pause()
func (*Player) Resume ¶
func (p *Player) Resume()
Resume resumes a paused playback.
If playback is not paused, this is a no-op.
func (*Player) Seek ¶
Seek jumps to a specific time offset in the recording.
The time is specified in seconds from the start of the recording. Seeking adjusts the playback position to the event closest to the target time. This can be called during playback.
func (*Player) SetLoop ¶
SetLoop enables or disables looping.
When enabled, playback restarts from the beginning when it reaches the end.
func (*Player) SetSpeed ¶
SetSpeed changes the playback speed multiplier.
The speed is adjusted smoothly to prevent jumps in playback position. Values less than or equal to 0 are ignored. Common values:
- 0.5 = half speed (slower)
- 1.0 = normal speed
- 2.0 = double speed (faster)
Example ¶
ExamplePlayer_SetSpeed demonstrates changing playback speed.
tmpfile := filepath.Join(os.TempDir(), "speed-example.cast")
defer os.Remove(tmpfile)
recorder, _ := NewRecorder(tmpfile, 80, 24, RecordingOptions{
Compress: false,
})
recorder.RecordOutput("Speed test\n")
recorder.Close()
player, _ := NewPlayer(tmpfile, PlayerOptions{
Output: os.Stdout,
})
fmt.Printf("Initial speed: %.1fx\n", player.Speed())
player.SetSpeed(2.0)
fmt.Printf("New speed: %.1fx\n", player.Speed())
Output: Initial speed: 1.0x New speed: 2.0x
func (*Player) Stop ¶
func (p *Player) Stop()
Stop stops playback immediately and permanently.
After calling Stop, the player cannot be restarted. Create a new player if you need to play the recording again.
func (*Player) TogglePause ¶
func (p *Player) TogglePause()
TogglePause toggles between paused and playing states.
type PlayerOptions ¶
type PlayerOptions struct {
Speed float64 // Playback speed multiplier (1.0 = normal speed, 2.0 = 2x, etc.)
Loop bool // Loop playback when finished (restart from beginning)
MaxIdle float64 // Max idle time between events in seconds (0 = preserve original timing)
Output io.Writer // Output destination (default: os.Stdout)
}
PlayerOptions configures playback behavior.
func DefaultPlayerOptions ¶
func DefaultPlayerOptions() PlayerOptions
DefaultPlayerOptions returns sensible defaults for playback.
Returns options with normal speed (1.0), no looping, and output to stdout.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder captures terminal output to an asciinema v2 format file.
Use this when you want to record terminal output directly without a PTY. For recording interactive PTY sessions, see Session.Record instead.
Recorder is safe for concurrent use (all methods are thread-safe).
func NewRecorder ¶
func NewRecorder(filename string, width, height int, opts RecordingOptions) (*Recorder, error)
NewRecorder creates a new recorder that writes to the specified file.
The file is created immediately and the asciinema v2 header is written. The recorder must be closed with Close() when done to ensure all data is flushed.
Parameters:
- filename: Path to the .cast file to create
- width: Terminal width in columns
- height: Terminal height in rows
- opts: Recording options (compression, redaction, metadata)
Example:
recorder, err := NewRecorder("demo.cast", 80, 24, RecordingOptions{
Compress: true,
Title: "My Demo",
})
if err != nil {
log.Fatal(err)
}
defer recorder.Close()
Example ¶
ExampleNewRecorder demonstrates basic recording usage.
// Create a temporary file for the recording
tmpfile := filepath.Join(os.TempDir(), "example.cast")
defer os.Remove(tmpfile)
// Create a recorder
recorder, err := NewRecorder(tmpfile, 80, 24, RecordingOptions{
Compress: false,
Title: "Example Recording",
})
if err != nil {
panic(err)
}
defer recorder.Close()
// Record some output
recorder.RecordOutput("Hello, ")
recorder.RecordOutput("World!\n")
func (*Recorder) Close ¶
Close finalizes the recording and closes the file.
This flushes all buffered data and closes any compression streams. Always call Close when finished recording to ensure data is not lost. It's safe to call Close multiple times.
func (*Recorder) Flush ¶
Flush writes any buffered data to the underlying file.
Events are buffered for performance. Call Flush to ensure all recorded events are written to disk immediately.
func (*Recorder) Pause ¶
func (r *Recorder) Pause()
Pause temporarily suspends recording.
While paused, calls to RecordOutput and RecordInput are ignored. Use Resume to continue recording. This is useful for temporarily stopping recording during sensitive operations.
Example ¶
ExampleRecorder_Pause demonstrates pausing and resuming recording.
tmpfile := filepath.Join(os.TempDir(), "pause-example.cast")
defer os.Remove(tmpfile)
recorder, err := NewRecorder(tmpfile, 80, 24, RecordingOptions{
Compress: false,
})
if err != nil {
panic(err)
}
defer recorder.Close()
recorder.RecordOutput("Before pause\n")
recorder.Pause()
recorder.RecordOutput("During pause (not recorded)\n")
recorder.Resume()
recorder.RecordOutput("After resume\n")
func (*Recorder) RecordInput ¶
RecordInput records user input data.
Input events are recorded but typically not displayed during playback (most players only render output events). They're useful for analysis and understanding user interactions with the terminal.
This method is safe to call from multiple goroutines.
func (*Recorder) RecordOutput ¶
RecordOutput records terminal output data.
The data is timestamped relative to when the recorder was created. If RedactSecrets is enabled, passwords and API keys will be automatically redacted. Recording is skipped if the recorder is paused or if data is empty.
This method is safe to call from multiple goroutines.
func (*Recorder) Resume ¶
func (r *Recorder) Resume()
Resume continues a paused recording.
Recording resumes from the point where Pause was called, preventing a large time gap from appearing in the recording.
func (*Recorder) UpdateSize ¶
UpdateSize updates the recorded terminal dimensions.
This should be called when the terminal is resized to keep the metadata accurate. Note: asciinema v2 doesn't have explicit resize events, so this only updates internal tracking.
type RecordingEvent ¶
type RecordingEvent struct {
Time float64 // Seconds since recording start
Type string // "o" for terminal output, "i" for user input
Data string // The actual terminal content (may contain ANSI codes)
}
RecordingEvent represents a single terminal I/O event in asciinema v2 format.
Events are encoded as JSON arrays: [time, type, data] where time is seconds elapsed, type is "o" (output) or "i" (input), and data is the actual terminal content.
func OutputEvents ¶
func OutputEvents(events []RecordingEvent) []RecordingEvent
OutputEvents filters and returns only output events (type "o").
Input events (type "i") are filtered out. This is useful because most playback scenarios only need to render output, and input events are primarily kept for analysis purposes.
The returned slice contains references to the original events (not copies).
Example ¶
ExampleOutputEvents demonstrates filtering output events.
events := []RecordingEvent{
{Time: 0.0, Type: "o", Data: "Output line 1"},
{Time: 0.5, Type: "i", Data: "User input"},
{Time: 1.0, Type: "o", Data: "Output line 2"},
}
outputOnly := OutputEvents(events)
fmt.Printf("Total events: %d\n", len(events))
fmt.Printf("Output events: %d\n", len(outputOnly))
Output: Total events: 3 Output events: 2
func (RecordingEvent) MarshalJSON ¶
func (e RecordingEvent) MarshalJSON() ([]byte, error)
MarshalJSON implements custom JSON encoding for asciinema array format
type RecordingHeader ¶
type RecordingHeader struct {
Version int `json:"version"` // Always 2 for asciinema v2 format
Width int `json:"width"` // Terminal width in columns
Height int `json:"height"` // Terminal height in rows
Timestamp int64 `json:"timestamp"` // Unix timestamp of recording start
Env map[string]string `json:"env,omitempty"`
Title string `json:"title,omitempty"` // Optional recording title
}
RecordingHeader represents the asciinema v2 header line.
This is the first line of every .cast file, containing metadata about the recording session. All recordings must start with a valid header.
type RecordingOptions ¶
type RecordingOptions struct {
Compress bool // Enable gzip compression to reduce file size
RedactSecrets bool // Automatically redact passwords, API keys, and tokens
Title string // Recording title for metadata (shown in players)
Env map[string]string // Environment variables to include in metadata
IdleTimeLimit float64 // Max idle time between events in seconds (0 = no limit)
}
RecordingOptions configures recording behavior.
These options control how terminal output is captured and stored.
func DefaultRecordingOptions ¶
func DefaultRecordingOptions() RecordingOptions
DefaultRecordingOptions returns sensible defaults for recording.
By default, recordings are compressed and secrets are redacted.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents an interactive PTY (pseudo-terminal) session.
Session manages a command running in a PTY, handles terminal I/O, and optionally records the session to an asciinema v2 format file. It's designed for creating interactive terminal sessions that feel like a real terminal (with proper handling of colors, cursor movement, etc.).
The session handles:
- PTY creation and management
- Terminal size synchronization (including SIGWINCH)
- Raw mode terminal setup
- Optional recording with the Recorder
Use Start() to begin an interactive session, or Record() to start and record simultaneously.
func NewSession ¶
func NewSession(opts SessionOptions) (*Session, error)
NewSession creates a new PTY session with the given options.
The session is not started until Start() or Record() is called. If no command is specified, the user's shell ($SHELL or /bin/sh) is used.
Example:
session, err := NewSession(SessionOptions{
Command: []string{"bash", "-i"},
Dir: "/tmp",
Env: []string{"TERM=xterm-256color"},
})
if err != nil {
log.Fatal(err)
}
defer session.Close()
Example ¶
ExampleNewSession demonstrates creating a PTY session.
// Create a session that will run a command
session, err := NewSession(SessionOptions{
Command: []string{"bash", "-c", "exit 0"},
Dir: "/tmp",
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Session created with command: %v\n", session.command)
fmt.Printf("Working directory: %s\n", session.dir)
Output: Session created with command: [bash -c exit 0] Working directory: /tmp
func (*Session) Close ¶
Close terminates the session and cleans up resources.
This method:
- Restores the terminal to its original state
- Closes and finalizes any recording
- Closes the PTY
It's safe to call Close multiple times or before the session completes.
func (*Session) ExitCode ¶
ExitCode returns the exit code of the command.
This is only valid after Wait() returns. Returns 0 before the session completes.
func (*Session) IsRecording ¶
IsRecording returns true if the session is being recorded.
func (*Session) PauseRecording ¶
func (s *Session) PauseRecording()
PauseRecording temporarily pauses recording.
Terminal I/O continues normally, but events are not recorded while paused. Has no effect if the session is not being recorded.
func (*Session) Record ¶
func (s *Session) Record(filename string, opts RecordingOptions) error
Record starts the PTY session and records it to the specified file.
This is a convenience method that creates a Recorder, attaches it to the session, and calls Start(). The terminal size is detected automatically (or defaults to 80x24 if detection fails).
The recording file is created immediately with the header written. Call Wait() to block until the session ends, then Close() to finalize.
Example:
session, _ := NewSession(SessionOptions{
Command: []string{"bash", "-c", "echo 'Hello, World!'"},
})
err := session.Record("demo.cast", RecordingOptions{
Compress: true,
Title: "Hello Demo",
})
if err != nil {
log.Fatal(err)
}
session.Wait()
session.Close()
func (*Session) Resize ¶
Resize manually sets the terminal size.
This is useful for programmatically resizing the terminal or when automatic resize detection isn't available (non-TTY scenarios). The recorder is also updated if recording is active.
func (*Session) ResumeRecording ¶
func (s *Session) ResumeRecording()
ResumeRecording resumes a paused recording.
Recording continues from where it was paused. Has no effect if the session is not being recorded or is not paused.
func (*Session) Start ¶
Start begins the PTY session without recording.
This method:
- Creates a PTY and starts the command
- Sets the terminal to raw mode (if stdin is a terminal)
- Starts goroutines to handle I/O between stdin/stdout and the PTY
- Sets up terminal resize handling (SIGWINCH)
The session runs in the background. Use Wait() to block until it completes.
Example:
session, _ := NewSession(SessionOptions{
Command: []string{"bash"},
})
err := session.Start()
if err != nil {
log.Fatal(err)
}
// Session is now interactive
session.Wait()
type SessionOptions ¶
type SessionOptions struct {
Command []string // Command to run (default: user's shell from $SHELL)
Dir string // Working directory (default: current directory)
Env []string // Additional environment variables (added to inherited environment)
Input io.Reader // Input source (default: os.Stdin)
Output io.Writer // Output destination (default: os.Stdout)
}
SessionOptions configures a new PTY session.