diff --git a/src/asciicast.rs b/src/asciicast.rs index 267fafc..853186e 100644 --- a/src/asciicast.rs +++ b/src/asciicast.rs @@ -4,6 +4,7 @@ mod v2; mod v3; use std::collections::HashMap; +use std::fmt::Display; use std::fs; use std::io::{self, BufRead}; use std::path::Path; @@ -15,10 +16,18 @@ pub use v2::V2Encoder; pub use v3::V3Encoder; pub struct Asciicast<'a> { + pub version: Version, pub header: Header, pub events: Box> + 'a>, } +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Version { + One, + Two, + Three, +} + pub struct Header { pub term_cols: u16, pub term_rows: u16, @@ -45,6 +54,30 @@ pub enum EventData { Other(char, String), } +pub trait Encoder { + fn header(&mut self, header: &Header) -> Vec; + fn event(&mut self, event: &Event) -> Vec; +} + +impl PartialEq for Version { + fn eq(&self, other: &u8) -> bool { + matches!( + (self, other), + (Version::One, 1) | (Version::Two, 2) | (Version::Three, 3) + ) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Version::One => write!(f, "1"), + Version::Two => write!(f, "2"), + Version::Three => write!(f, "3"), + } + } +} + impl Default for Header { fn default() -> Self { Self { @@ -62,6 +95,26 @@ impl Default for Header { } } +impl Encoder for V2Encoder { + fn header(&mut self, header: &Header) -> Vec { + self.header(header) + } + + fn event(&mut self, event: &Event) -> Vec { + self.event(event) + } +} + +impl Encoder for V3Encoder { + fn header(&mut self, header: &Header) -> Vec { + self.header(header) + } + + fn event(&mut self, event: &Event) -> Vec { + self.event(event) + } +} + pub fn open_from_path>(path: S) -> Result> { fs::File::open(path) .map(io::BufReader::new) @@ -161,6 +214,14 @@ pub fn accelerate( }) } +pub fn encoder(version: Version) -> Option> { + match version { + Version::One => None, + Version::Two => Some(Box::new(V2Encoder::new(0))), + Version::Three => Some(Box::new(V3Encoder::new())), + } +} + #[cfg(test)] mod tests { use super::{Asciicast, Event, EventData, Header, V2Encoder}; @@ -171,11 +232,15 @@ mod tests { #[test] fn open_v1_minimal() { - let Asciicast { header, events } = - super::open_from_path("tests/casts/minimal.json").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/minimal.json").unwrap(); let events = events.collect::>>().unwrap(); + assert_eq!(version, 1); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert!(header.term_theme.is_none()); @@ -185,9 +250,14 @@ mod tests { #[test] fn open_v1_full() { - let Asciicast { header, events } = super::open_from_path("tests/casts/full.json").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/full.json").unwrap(); let events = events.collect::>>().unwrap(); + assert_eq!(version, 1); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert_eq!(events[0].time, 1); @@ -202,11 +272,15 @@ mod tests { #[test] fn open_v2_minimal() { - let Asciicast { header, events } = - super::open_from_path("tests/casts/minimal-v2.cast").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/minimal-v2.cast").unwrap(); let events = events.collect::>>().unwrap(); + assert_eq!(version, 2); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert!(header.term_theme.is_none()); @@ -216,11 +290,15 @@ mod tests { #[test] fn open_v2_full() { - let Asciicast { header, events } = - super::open_from_path("tests/casts/full-v2.cast").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/full-v2.cast").unwrap(); let events = events.take(5).collect::>>().unwrap(); let theme = header.term_theme.unwrap(); + assert_eq!(version, 2); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert_eq!(header.timestamp, Some(1509091818)); assert_eq!(theme.fg, RGB8::new(0, 0, 0)); @@ -248,11 +326,15 @@ mod tests { #[test] fn open_v3_minimal() { - let Asciicast { header, events } = - super::open_from_path("tests/casts/minimal-v3.cast").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/minimal-v3.cast").unwrap(); let events = events.collect::>>().unwrap(); + assert_eq!(version, 3); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert!(header.term_theme.is_none()); @@ -262,11 +344,15 @@ mod tests { #[test] fn open_v3_full() { - let Asciicast { header, events } = - super::open_from_path("tests/casts/full-v3.cast").unwrap(); + let Asciicast { + version, + header, + events, + } = super::open_from_path("tests/casts/full-v3.cast").unwrap(); let events = events.take(5).collect::>>().unwrap(); let theme = header.term_theme.unwrap(); + assert_eq!(version, 3); assert_eq!((header.term_cols, header.term_rows), (100, 50)); assert_eq!(header.timestamp, Some(1509091818)); assert_eq!(theme.fg, RGB8::new(0, 0, 0)); diff --git a/src/asciicast/v1.rs b/src/asciicast/v1.rs index b6c46ec..e4ae75a 100644 --- a/src/asciicast/v1.rs +++ b/src/asciicast/v1.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::{bail, Result}; use serde::Deserialize; -use super::{Asciicast, Event, Header}; +use super::{Asciicast, Event, Header, Version}; use crate::asciicast::util::deserialize_time; #[derive(Debug, Deserialize)] @@ -57,5 +57,9 @@ pub fn load(json: String) -> Result> { Some(Ok(Event::output(time, event.data))) })); - Ok(Asciicast { header, events }) + Ok(Asciicast { + version: Version::One, + header, + events, + }) } diff --git a/src/asciicast/v2.rs b/src/asciicast/v2.rs index 9a1f3e4..342615c 100644 --- a/src/asciicast/v2.rs +++ b/src/asciicast/v2.rs @@ -5,7 +5,7 @@ use std::io; use anyhow::{anyhow, bail, Context, Result}; use serde::{Deserialize, Deserializer, Serialize}; -use super::{util, Asciicast, Event, EventData, Header}; +use super::{util, Asciicast, Event, EventData, Header, Version}; use crate::tty::TtyTheme; #[derive(Deserialize)] @@ -87,7 +87,11 @@ impl Parser { let events = Box::new(lines.filter_map(parse_line)); - Asciicast { header, events } + Asciicast { + version: Version::Two, + header, + events, + } } } diff --git a/src/asciicast/v3.rs b/src/asciicast/v3.rs index 4963e0b..6cf652e 100644 --- a/src/asciicast/v3.rs +++ b/src/asciicast/v3.rs @@ -5,7 +5,7 @@ use std::io; use anyhow::{anyhow, bail, Context, Result}; use serde::{Deserialize, Deserializer, Serialize}; -use super::{util, Asciicast, Event, EventData, Header}; +use super::{util, Asciicast, Event, EventData, Header, Version}; use crate::tty::TtyTheme; #[derive(Deserialize)] @@ -103,7 +103,11 @@ impl Parser { let events = Box::new(lines.filter_map(move |line| self.parse_line(line))); - Asciicast { header, events } + Asciicast { + version: Version::Three, + header, + events, + } } fn parse_line(&mut self, line: io::Result) -> Option> { diff --git a/src/cmd/cat.rs b/src/cmd/cat.rs index de4e886..cfa0f9f 100644 --- a/src/cmd/cat.rs +++ b/src/cmd/cat.rs @@ -1,7 +1,7 @@ use std::io; use std::io::Write; -use anyhow::Result; +use anyhow::{anyhow, Result}; use crate::asciicast; use crate::cli; @@ -9,11 +9,21 @@ use crate::config::Config; impl cli::Cat { pub fn run(self, _config: &Config) -> Result<()> { - let mut encoder = asciicast::V2Encoder::new(0); let mut stdout = io::stdout(); let mut time_offset: u64 = 0; let mut first = true; + let casts = self + .filename + .iter() + .map(asciicast::open_from_path) + .collect::>>()?; + + let version = casts[0].version; + + let mut encoder = asciicast::encoder(version) + .ok_or(anyhow!("asciicast v{version} files can't be concatenated"))?; + for path in self.filename.iter() { let recording = asciicast::open_from_path(path)?; let mut time = time_offset; diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 5a2dcd5..ef34205 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -7,8 +7,7 @@ use std::io::Write; use anyhow::Result; -use crate::asciicast::Event; -use crate::asciicast::Header; +use crate::asciicast::{Event, Header}; pub use asciicast::{AsciicastV2Encoder, AsciicastV3Encoder}; pub use raw::RawEncoder; pub use txt::TextEncoder;