# TTYCast container format
All integers are unsigned big-endian. Terms are Erlang External Term Format.
## Layout
```text
magic "TTYCAST\0"
version u16
header_len u32
header_etf header_len bytes
chunk*
trailer_magic "TTYCAST_INDEX\0"
trailer_len u64
trailer_etf trailer_len bytes
footer_magic "TTYCAST_FOOTER\0"
trailer_offset u64
```
The footer lets readers open large recordings with one end-of-file read and one trailer `pread`. Writers also maintain `<recording>.live.idx` after each chunk flush. If a process crashes after writing chunks but before writing the trailer/footer, readers can fall back to the live index; `TTYCast.reindex/1` scans intact chunks and writes a fresh trailer/footer.
## Header
The header is an ETF map with dimensions, codec, input policy, metadata, and chunk thresholds.
## Chunk
```text
compressed_len u64
uncompressed_len u64
start_t_us u64
end_t_us u64
event_count u32
payload_gzip compressed_len bytes
```
The uncompressed payload is an ETF map:
```elixir
%{
seq: non_neg_integer(),
start_t_us: non_neg_integer(),
end_t_us: non_neg_integer(),
event_count: non_neg_integer(),
keyframe: %{format: :ghostty_snapshot, t_us: integer(), plain: binary(), vt: binary()},
events: [event]
}
```
Chunks are independently compressed so seeking reads only the nearest keyframe chunk and forward deltas. Keyframes are stored periodically according to `:keyframe_interval_ms`; chunks without a keyframe use `keyframe: nil`.
## Events
Core events:
```elixir
{:output, t_us, bytes}
{:input, t_us, bytes}
{:input_redacted, t_us, byte_count}
{:resize, t_us, cols, rows}
{:marker, t_us, name, metadata}
{:event, t_us, stream, payload}
```
Record raw input only in disposable sessions. The default input policy is `:redacted`; use `:raw` only when explicitly needed, or `:none` to drop input events entirely.