# 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
```elixir
def deps do
[
{:ttycast, "~> 0.1.0"}
]
end
```
## Record a command
```sh
mix ttycast.record --output /tmp/demo.ttycast -- sh -lc 'echo hello'
```
Inspect it:
```sh
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:
```sh
mix ttycast.rec --output /tmp/shell.ttycast -- bash
```
Raw input is not recorded by default. To opt in for disposable/debug sessions:
```sh
mix ttycast.rec --output /tmp/shell.ttycast --input raw -- bash
```
## Use from Elixir
For most applications, use scoped writer lifecycle:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```sh
mix ttycast.reindex /tmp/demo.ttycast
```
or:
```elixir
TTYCast.reindex("/tmp/demo.ttycast")
```
## Benchmarks
Run a local benchmark comparing `.ttycast`, asciinema JSONL, and gzipped asciinema JSONL sizes plus open/seek timings:
```sh
mix ttycast.bench --events 10000
```
## Format
See [`FORMAT.md`](FORMAT.md) for the binary container layout and event schema.