Skip to main content

asciicast_rs/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4
5use std::{
6    fs::File,
7    io::{BufRead, BufReader, Cursor, Read},
8    path::Path,
9};
10
11use serde::Deserialize;
12
13mod error;
14mod reader;
15mod versions;
16
17pub use error::Error;
18pub use reader::Reader;
19pub use versions::{Streamable, V1, V2, V3, Version, common, v1, v2, v3};
20
21/// A parsed `asciicast` recording of a known version `V`.
22#[derive(Debug, Clone, PartialEq)]
23pub struct Asciicast<V: Version> {
24    /// The recording's header metadata.
25    pub header: V::Header,
26    /// The recording's events, in order.
27    pub events: Vec<V::Event>,
28}
29
30impl<V: Version> Asciicast<V> {
31    /// Parse a recording from a buffered reader.
32    ///
33    /// # Errors
34    ///
35    /// Returns an [`Error`] if reading fails, the input is not valid JSON for
36    /// version `V`, the declared version does not match, or an event payload is
37    /// malformed.
38    pub fn from_reader<R: BufRead>(reader: R) -> Result<Self, Error> {
39        V::parse(reader)
40    }
41
42    /// Parse a recording from a byte slice.
43    ///
44    /// # Errors
45    ///
46    /// See [`Asciicast::from_reader`].
47    pub fn from_slice(bytes: &[u8]) -> Result<Self, Error> {
48        V::parse(bytes)
49    }
50
51    /// Parse a recording from a file path.
52    ///
53    /// # Errors
54    ///
55    /// Returns an [`Error`] if the file cannot be opened, or for any reason
56    /// described by [`Asciicast::from_reader`].
57    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
58        V::parse(BufReader::new(File::open(path)?))
59    }
60
61    /// Iterate over the events paired with their absolute time, in seconds since
62    /// the start of the recording.
63    ///
64    /// Each item is `(absolute_seconds, event)`. This normalises the differing
65    /// per-version timing: v1 frame delays and v3 event intervals (relative to
66    /// the previous entry) are accumulated, while v2 event times (already
67    /// absolute) are passed through.
68    pub fn absolute_times(&self) -> impl Iterator<Item = (f64, &V::Event)> {
69        self.events.iter().scan(0.0_f64, |elapsed, event| {
70            let raw = V::event_time(event);
71            let absolute = if V::RELATIVE_TIMING {
72                *elapsed += raw;
73                *elapsed
74            } else {
75                raw
76            };
77            Some((absolute, event))
78        })
79    }
80}
81
82/// A parsed `asciicast` recording whose version was detected at runtime.
83///
84/// Returned by the auto-detecting constructors; `match` on it to recover the
85/// fully typed [`Asciicast<V>`].
86#[derive(Debug, Clone, PartialEq)]
87pub enum AsciicastVersioned {
88    /// A v1 recording.
89    V1(Asciicast<V1>),
90    /// A v2 recording.
91    V2(Asciicast<V2>),
92    /// A v3 recording.
93    V3(Asciicast<V3>),
94}
95
96impl AsciicastVersioned {
97    /// Parse a recording from a buffered reader, detecting its version from the
98    /// content.
99    ///
100    /// The version is read from the `version` field of the first line. A v1
101    /// recording may be pretty-printed across multiple lines, in which case the
102    /// first line is not a complete JSON object; that case is treated as v1.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`Error::UnknownVersion`] if the declared version is not 1, 2, or
107    /// 3, or any error from the matching [`Asciicast::from_reader`].
108    pub fn from_reader<R: BufRead>(mut reader: R) -> Result<Self, Error> {
109        #[derive(Deserialize)]
110        struct VersionProbe {
111            version: u8,
112        }
113
114        let mut first_line = String::new();
115        reader.read_line(&mut first_line)?;
116
117        // A complete first line carries the version; a parse failure means a
118        // pretty-printed (multi-line) v1 document whose first line is just `{`.
119        let version = serde_json::from_str::<VersionProbe>(&first_line)
120            .ok()
121            .map(|probe| probe.version);
122
123        // Re-feed the consumed first line ahead of the rest of the reader.
124        let combined = BufReader::new(Cursor::new(first_line.into_bytes()).chain(reader));
125
126        match version {
127            None | Some(1) => Asciicast::<V1>::from_reader(combined).map(Self::V1),
128            Some(2) => Asciicast::<V2>::from_reader(combined).map(Self::V2),
129            Some(3) => Asciicast::<V3>::from_reader(combined).map(Self::V3),
130            Some(other) => Err(Error::UnknownVersion(other)),
131        }
132    }
133
134    /// Parse a recording from a byte slice, detecting its version.
135    ///
136    /// # Errors
137    ///
138    /// See [`AsciicastVersioned::from_reader`].
139    pub fn from_slice(bytes: &[u8]) -> Result<Self, Error> {
140        Self::from_reader(bytes)
141    }
142
143    /// Parse a recording from a file path, detecting its version.
144    ///
145    /// # Errors
146    ///
147    /// Returns an [`Error`] if the file cannot be opened, or for any reason
148    /// described by [`AsciicastVersioned::from_reader`].
149    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
150        Self::from_reader(BufReader::new(File::open(path)?))
151    }
152}
153
154/// Commonly used types, re-exported for glob import.
155pub mod prelude {
156    pub use crate::versions::{V1, V2, V3, Version};
157    pub use crate::{Asciicast, AsciicastVersioned, Error};
158}