Raxol.Recording.Asciicast (Raxol v2.6.1)

View Source

Serializes and deserializes asciinema v2 .cast files.

The asciicast v2 format is:

  • Line 1: JSON header with version, width, height, timestamp, env
  • Remaining lines: [elapsed_seconds, "o", "output_data"] (newline-delimited JSON)

See: https://docs.asciinema.org/manual/asciicast/v2/

Concurrency

append!/2 assumes a single writer per file. It opens the file several times (validate header, check/fix the trailing newline, append the body) with no cross-process locking, so two concurrent appenders to the same path can interleave their bodies or both add a trailing newline. The intended usage (e.g. Evidence.Capture owning one .cast per run) already serializes writes through a single process; keep that contract. Do not append to one file from multiple processes concurrently.

Summary

Functions

Appends events to an existing .cast file without rewriting its header.

Decodes an asciicast v2 format string into a session.

Encodes a session to asciicast v2 format string.

Reads a .cast file into a session. Returns {:ok, session} or {:error, reason}.

Reads a .cast file into a session. Raises on failure.

Writes a session to a .cast file.

Functions

append!(events, path)

Appends events to an existing .cast file without rewriting its header.

Validates the existing v2 header, then writes the new event lines to the end of the file. Events carry elapsed_us relative to the session start (the same time base the header's timestamp anchors), so their timestamps are preserved verbatim. A defensive trailing newline is added first so a header-only file (or one whose final byte is not a newline) never fuses the appended line onto the previous one.

Accepts either a list of Session.event() tuples or a Session (its events are appended). Raises if the file is missing or its header is not a valid asciicast v2 header. Appending an empty event list is a no-op: the file is left byte-for-byte untouched (no trailing-newline fixup, no header validation).

Single-writer contract: this is not safe against concurrent appenders to the same path (see the module's Concurrency note). Call it from a single owning process per file.

decode(content)

@spec decode(String.t()) :: Raxol.Recording.Session.t()

Decodes an asciicast v2 format string into a session.

Torn-tail tolerant: if the file was truncated mid-write (process killed) so the final line has no trailing newline, that torn line is dropped silently and every complete event preceding it is recovered. A line that was fully flushed (newline-terminated) but is still unparseable -- whether interior or the last event line -- is committed corruption: parsing stops there, returns the events collected so far, and logs a warning rather than raising. A valid cast round-trips unchanged.

The header line must be present and valid JSON (it is written in full before any event, so a truncated recording still has an intact header). A missing or corrupt header raises; call read/1 for the non-raising {:ok, _} | {:error, _} variant.

encode(session)

@spec encode(Raxol.Recording.Session.t()) :: String.t()

Encodes a session to asciicast v2 format string.

read(path)

@spec read(Path.t()) :: {:ok, Raxol.Recording.Session.t()} | {:error, term()}

Reads a .cast file into a session. Returns {:ok, session} or {:error, reason}.

read!(path)

@spec read!(Path.t()) :: Raxol.Recording.Session.t()

Reads a .cast file into a session. Raises on failure.

write!(session, path)

@spec write!(Raxol.Recording.Session.t(), Path.t()) :: :ok

Writes a session to a .cast file.