Skip to content

elixir-vibe/ttycast

Repository files navigation

TTYCast

TTYCast records terminal sessions into seekable, compressed .ttycast files for Elixir and Erlang applications.

It is useful when you need more than a plain terminal log: fast snapshots, timestamp seeking, safe input handling, and application-specific timeline events.

Why TTYCast?

  • Seekable replay — recordings are split into compressed chunks with Ghostty terminal keyframes.
  • Small files — chunks are gzip-compressed and indexed in the same container.
  • Safe by default — input is redacted unless raw input is explicitly enabled.
  • Real terminal behavior — command recording runs under a real PTY via Ghostty.
  • Structured events — host apps can record semantic markers and custom streams next to terminal bytes.
  • Portable export — export terminal I/O to asciinema v2 JSONL when needed.

Install

def deps do
  [
    {:ttycast, "~> 0.1.0"}
  ]
end

Record a command

mix ttycast.record --output /tmp/demo.ttycast -- sh -lc 'echo hello'

Inspect it:

mix ttycast.info /tmp/demo.ttycast
mix ttycast.snapshot /tmp/demo.ttycast
mix ttycast.find /tmp/demo.ttycast hello

Record an interactive command in your current terminal:

mix ttycast.rec --output /tmp/shell.ttycast -- bash

Raw input is not recorded by default. To opt in for disposable/debug sessions:

mix ttycast.rec --output /tmp/shell.ttycast --input raw -- bash

Use from Elixir

For most applications, use scoped writer lifecycle:

TTYCast.write("/tmp/demo.ttycast", [width: 120, height: 40], fn writer ->
  TTYCast.Writer.write(writer, "hello\r\n")
  TTYCast.Writer.marker(writer, :checkpoint, %{label: "first screen"})
end)

Read and seek:

cast = TTYCast.open!("/tmp/demo.ttycast")

TTYCast.info(cast)
TTYCast.snapshot!(cast, time_ms: 1_000)
TTYCast.stream(cast) |> Enum.to_list()
TTYCast.export(cast, :asciinema, "/tmp/demo.cast")

Stream existing IO into a recording:

TTYCast.write("/tmp/log.ttycast", [width: 120, height: 40], fn writer ->
  File.stream!("app.log")
  |> Enum.into(TTYCast.into(writer))
end)

Input policy

TTYCast defaults to redacted input:

TTYCast.Writer.input(writer, "secret")
# records {:input_redacted, t_us, 6}

Available policies:

  • :redacted — store byte counts only. Default.
  • :raw — store raw input bytes.
  • :none — drop input events entirely.

Set policy when starting a writer:

TTYCast.start_writer(path: path, width: 80, height: 24, input_policy: :none)

Recovery

Writers maintain a live sidecar index while recording. If a process crashes before the final trailer/footer is written, TTYCast can still open the file through that live index when available.

If the live index is missing but chunks are intact, rebuild the trailer/footer:

mix ttycast.reindex /tmp/demo.ttycast

or:

TTYCast.reindex("/tmp/demo.ttycast")

Benchmarks

Run a local benchmark comparing .ttycast, asciinema JSONL, and gzipped asciinema JSONL sizes plus open/seek timings:

mix ttycast.bench --events 10000

Format

See FORMAT.md for the binary container layout and event schema.

About

Seekable compressed terminal recordings for BEAM applications

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages