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}