TTYCast container format
Copy MarkdownAll integers are unsigned big-endian. Terms are Erlang External Term Format.
Layout
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 u64The 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
compressed_len u64
uncompressed_len u64
start_t_us u64
end_t_us u64
event_count u32
payload_gzip compressed_len bytesThe uncompressed payload is an ETF map:
%{
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:
{: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.