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"}
]
endRecord 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 helloRecord an interactive command in your current terminal:
mix ttycast.rec --output /tmp/shell.ttycast -- bashRaw input is not recorded by default. To opt in for disposable/debug sessions:
mix ttycast.rec --output /tmp/shell.ttycast --input raw -- bashUse 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.ttycastor:
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 10000Format
See FORMAT.md for the binary container layout and event schema.