An open API service indexing awesome lists of open source software.

https://github.com/nlopes/asciicast-rs

An asciicast file format parser
https://github.com/nlopes/asciicast-rs

asciicast asciinema parser

Last synced: 18 days ago
JSON representation

An asciicast file format parser

Awesome Lists containing this project

README

          

# asciicast-rs

A library to parse [`asciicast` file
format](https://docs.asciinema.org/manual/asciicast/v3/) files across all `asciicast`
versions.

## Versions supported

- [`asciicast` v1 format](https://docs.asciinema.org/manual/asciicast/v1/)
- [`asciicast` v2 format](https://docs.asciinema.org/manual/asciicast/v2/)
- [`asciicast` v3 format](https://docs.asciinema.org/manual/asciicast/v3/)

### Why support all 3?

I wanted to be able to parse old files as well for another project I'm working on called
[acdc](https://github.com/nlopes/acdc).

## Installation

```sh
cargo add asciicast-rs
```

You can parse from:

- a byte slice using `from_slice`
- a `BufRead` using `from_reader`
- a file using `from_path`

They all return `Result<_, asciicast_rs::Error>`.

### Parsing a known version

The version is part of the type system, so you can parse directly into `Asciicast`.

```rust
use asciicast_rs::{Asciicast, V2};

let recording = b"{\"version\":2,\"width\":80,\"height\":24}\n[0.5,\"o\",\"hello\"]\n";
let cast = Asciicast::::from_slice(recording).expect("valid v2 recording");

println!("{}x{}", cast.header.width, cast.header.height);
for event in &cast.events {
if let Some(text) = event.as_output() {
print!("{text}");
}
}
```

To read from a file instead, use `Asciicast::::from_path("recording.cast")`.

### Auto-detecting the version

When the version is not known ahead of time, use `AsciicastVersioned`, which detects it
from the content and yields the matching variant, each wrapping a fully typed
`Asciicast`.

```rust
use asciicast_rs::AsciicastVersioned;

let recording = b"{\"version\":2,\"width\":80,\"height\":24}\n";
match AsciicastVersioned::from_slice(recording).expect("valid recording") {
AsciicastVersioned::V1(cast) => println!("v1, {} frames", cast.events.len()),
AsciicastVersioned::V2(cast) => println!("v2, {} events", cast.events.len()),
AsciicastVersioned::V3(cast) => println!("v3, {} events", cast.events.len()),
}
```

### Streaming events (v2 / v3)

`from_slice`/`from_path`/`from_reader` eagerly collect every event into a `Vec`. For long
recordings, you can instead use a `Reader` (or wrappers like `v2::stream` and
`v3::stream`) which yields a `Result` per line as an `Iterator`, so the whole
recording is never held in memory at once. You can construct one with `Reader::::open` or one of the wrappers mentioned.

> [!NOTE]
> Only the newline-delimited versions (v2, v3) implement `Streamable`, so `Reader` is a compile error (v1 is a single JSON document and is always parsed eagerly).

```rust
use asciicast_rs::v2;

let recording: &[u8] = b"{\"version\":2,\"width\":80,\"height\":24}\n[0.5,\"o\",\"hi\"]\n";
let reader = v2::stream(recording).expect("valid v2 header");

println!("{}x{}", reader.header().width, reader.header().height);
for event in reader {
let event = event.expect("valid event");
if let Some(text) = event.as_output() {
print!("{text}");
}
}

// If you have the stream and want to materialise the eager `Asciicast` you can do:
let eager = v2::stream(recording)
.and_then(|reader| reader.into_recording())
.expect("valid v2 recording");
assert_eq!(eager.events.len(), 1);
```

### Working with the parsed data

```rust
use asciicast_rs::{Asciicast, V3};

let recording = b"{\"version\":3,\"term\":{\"cols\":80,\"rows\":24}}\n";
let cast = Asciicast::::from_slice(recording).expect("valid v3 recording");
assert_eq!(cast.header.term.cols, 80);
```

### Absolute timestamps

Timing differs by version (see [Data model](#data-model)), so `absolute_times` normalises
it for you: it pairs each event with its absolute time in seconds since the start,
accumulating relative entries (v1, v3) and passing v2's already-absolute times through.

```rust
use asciicast_rs::{Asciicast, V3};

let recording = b"{\"version\":3,\"term\":{\"cols\":80,\"rows\":24}}\n[0.1,\"o\",\"a\"]\n[0.2,\"o\",\"b\"]\n";
let cast = Asciicast::::from_slice(recording).expect("valid v3 recording");

for (at, event) in cast.absolute_times() {
if let Some(text) = event.as_output() {
println!("{at:.3}s: {text}");
}
}
```

`Reader::absolute_times` is the streaming counterpart, yielding `Result<(f64, Event), _>`
so you get absolute timestamps without buffering the recording.

## Data model

- `Asciicast` is `{ header, events }`, parameterised by a version marker (`V1`, `V2`,
`V3`).
- Each version has its own `Header` and event type under `asciicast_rs::{v1, v2, v3}`.
Events expose a typed payload plus accessors (`as_output`, `as_input`, `as_marker`,
`as_resize`, and also `as_exit` for v3).
- Shared types live in `asciicast_rs::common` (`Theme`, `Rgb`, `Resize`, `ExitStatus`,
`Env`, and the colour error types).
- Timing semantics follow the spec: v2 event `time` is absolute (seconds since start),
while v1 frame `delay` and v3 event `interval` are relative to the previous entry. Use
`Asciicast::absolute_times` to iterate events with a uniform absolute timestamp.

> [!NOTE]
> In v1, the nomenclature used is attributes and frames instead of header and events (_roughly_). I thought that keeping to header and events across the versions was fine but isn't strictly accurate.

## Feature flags

- `chrono` *(off by default)* — adds a `timestamp_datetime()` accessor to the v2 and v3
headers, returning the recording's start time as a `chrono::DateTime`:

```sh
cargo add asciicast-rs --features chrono
```

## What this crate is not

A way to serialize `asciicast` format to files. Reason being that I wanted this crate to
start with the smallest possible "features", whilst being complete in terms of parsing, in
case one day the `asciinema` project decides to extract their serialization and parsing
into its own library and crate.

## Motivation

I needed a parser for `asciicast` files but realized there wasn't one (that I could easily
find) that was obvious I should use. More notes on what I found:

- `asciinema` is [built in rust](https://github.com/asciinema/asciinema) but unfortunately
it doesn't expose the `asciicast` format publicly as a library.

- There was also a library named [`asciicast`](https://crates.io/crates/asciicast) but
unfortunately doesn't seem to get any more updates.

Therefore I decided to create this crate to try to become the canonical rust library for
parsing `asciicast` format. If one day `asciinema` decides to provide a public crate then
I'd be happy to stop work here.