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
@spec append!( [Raxol.Recording.Session.event()] | Raxol.Recording.Session.t(), Path.t() ) :: :ok
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.
@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.
@spec encode(Raxol.Recording.Session.t()) :: String.t()
Encodes a session to asciicast v2 format string.
@spec read(Path.t()) :: {:ok, Raxol.Recording.Session.t()} | {:error, term()}
Reads a .cast file into a session. Returns {:ok, session} or {:error, reason}.
@spec read!(Path.t()) :: Raxol.Recording.Session.t()
Reads a .cast file into a session. Raises on failure.
@spec write!(Raxol.Recording.Session.t(), Path.t()) :: :ok
Writes a session to a .cast file.