TTYCast container format

Copy Markdown

All 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      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.

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 bytes

The 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.