TTYCast (ttycast v0.1.0)

Copy Markdown

Seekable, compressed terminal recordings for BEAM applications.

TTYCast records terminal output, input metadata, resize events, and custom semantic events into a self-contained .ttycast file. Recordings are split into independently compressed chunks with Ghostty terminal keyframes so readers can seek by timestamp without replaying the whole file.

Writing

TTYCast.write("demo.ttycast", [width: 120, height: 40], fn writer ->
  TTYCast.Writer.write(writer, "hello\r\n")
end)

Reading

cast = TTYCast.open!("demo.ttycast")
TTYCast.info(cast)
TTYCast.snapshot!(cast, time_ms: 1_000)
TTYCast.stream(cast) |> Enum.to_list()

Recording commands

TTYCast.record(["sh", "-lc", "echo hello"], path: "demo.ttycast")

See FORMAT.md for the binary container layout.

Summary

Functions

Returns decoded events as a list. Prefer stream/2 for large recordings.

Exports a recording to a supported format.

Exports terminal input/output streams as asciinema v2 JSON lines.

Finds times where plain terminal snapshots contain a string or regex match.

Imports a recording from a supported format.

Imports asciinema v2 JSON lines into a ttycast file.

Returns a compact summary map for a recording.

Returns a collectable sink that writes iodata into the recording.

Opens a recording without decoding chunks eagerly.

Opens a recording or raises.

Reads and decodes one compressed chunk by index entry.

Reads one compressed chunk or raises.

Records a command under a real pseudo-terminal.

Records a command interactively, forwarding the current terminal to the child PTY.

Rebuilds the trailer/footer index from intact chunks.

Returns a Ghostty-rendered terminal snapshot at a timestamp.

Returns a terminal snapshot or raises.

Starts a recording writer.

Streams decoded events lazily from chunks matching the optional time range.

Opens a writer, calls fun, and closes the writer afterwards.

Types

event()

@type event() :: TTYCast.Writer.event()

Functions

events(path_or_cast, opts \\ [])

@spec events(
  Path.t() | TTYCast.Cast.t(),
  keyword()
) :: [event()]

Returns decoded events as a list. Prefer stream/2 for large recordings.

export(path_or_cast, atom, output_path)

@spec export(Path.t() | TTYCast.Cast.t(), :asciinema, Path.t()) ::
  :ok | {:error, term()}

Exports a recording to a supported format.

export_asciinema(path_or_cast, output_path)

@spec export_asciinema(Path.t() | TTYCast.Cast.t(), Path.t()) ::
  :ok | {:error, term()}

Exports terminal input/output streams as asciinema v2 JSON lines.

find(path_or_cast, pattern, opts \\ [])

@spec find(Path.t() | TTYCast.Cast.t(), binary() | Regex.t(), keyword()) :: [
  %{time_ms: non_neg_integer(), match: binary()}
]

Finds times where plain terminal snapshots contain a string or regex match.

import(input_path, atom, output_path, opts \\ [])

@spec import(Path.t(), :asciinema, Path.t(), keyword()) ::
  {:ok, TTYCast.Cast.t()} | {:error, term()}

Imports a recording from a supported format.

import_asciinema(input_path, output_path, opts \\ [])

@spec import_asciinema(Path.t(), Path.t(), keyword()) ::
  {:ok, TTYCast.Cast.t()} | {:error, term()}

Imports asciinema v2 JSON lines into a ttycast file.

info(path_or_cast)

@spec info(Path.t() | TTYCast.Cast.t()) :: map()

Returns a compact summary map for a recording.

into(writer)

@spec into(pid()) :: TTYCast.Sink.t()

Returns a collectable sink that writes iodata into the recording.

open(cast)

@spec open(Path.t() | TTYCast.Cast.t()) :: {:ok, TTYCast.Cast.t()} | {:error, term()}

Opens a recording without decoding chunks eagerly.

open!(cast)

@spec open!(Path.t() | TTYCast.Cast.t()) :: TTYCast.Cast.t()

Opens a recording or raises.

read_chunk(path_or_cast, chunk_index)

@spec read_chunk(Path.t() | TTYCast.Cast.t(), map()) ::
  {:ok, map()} | {:error, term()}

Reads and decodes one compressed chunk by index entry.

read_chunk!(path_or_cast, chunk_index)

@spec read_chunk!(Path.t() | TTYCast.Cast.t(), map()) :: map()

Reads one compressed chunk or raises.

record(command, opts)

@spec record(
  [String.t()] | {String.t(), [String.t()]},
  keyword()
) :: {:ok, TTYCast.Recorder.result()} | {:error, term()}

Records a command under a real pseudo-terminal.

record(cmd, args, opts)

@spec record(String.t(), [String.t()], keyword()) ::
  {:ok, TTYCast.Recorder.result()} | {:error, term()}

record_interactive(command, opts)

@spec record_interactive(
  [String.t()],
  keyword()
) :: {:ok, map()} | {:error, term()}

Records a command interactively, forwarding the current terminal to the child PTY.

reindex(path)

@spec reindex(Path.t()) :: {:ok, map()} | {:error, term()}

Rebuilds the trailer/footer index from intact chunks.

snapshot(path_or_cast, opts \\ [])

@spec snapshot(
  Path.t() | TTYCast.Cast.t(),
  keyword()
) :: {:ok, binary() | map()} | {:error, term()}

Returns a Ghostty-rendered terminal snapshot at a timestamp.

snapshot!(path_or_cast, opts \\ [])

@spec snapshot!(
  Path.t() | TTYCast.Cast.t(),
  keyword()
) :: binary() | map()

Returns a terminal snapshot or raises.

start_writer(opts)

@spec start_writer(keyword()) :: GenServer.on_start()

Starts a recording writer.

stream(path_or_cast, opts \\ [])

@spec stream(
  Path.t() | TTYCast.Cast.t(),
  keyword()
) :: Enumerable.t()

Streams decoded events lazily from chunks matching the optional time range.

write(path, opts \\ [], fun)

@spec write(Path.t(), keyword(), (pid() -> term())) ::
  {:ok, term()} | {:error, term()}

Opens a writer, calls fun, and closes the writer afterwards.