Make cat use first file's asciicast version

This commit is contained in:
Marcin Kulik
2025-05-06 17:01:22 +02:00
parent dd6b9a3008
commit 986a64d3cc
6 changed files with 128 additions and 21 deletions

View File

@@ -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<dyn Iterator<Item = Result<Event>> + '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<u8>;
fn event(&mut self, event: &Event) -> Vec<u8>;
}
impl PartialEq<u8> 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<u8> {
self.header(header)
}
fn event(&mut self, event: &Event) -> Vec<u8> {
self.event(event)
}
}
impl Encoder for V3Encoder {
fn header(&mut self, header: &Header) -> Vec<u8> {
self.header(header)
}
fn event(&mut self, event: &Event) -> Vec<u8> {
self.event(event)
}
}
pub fn open_from_path<S: AsRef<Path>>(path: S) -> Result<Asciicast<'static>> {
fs::File::open(path)
.map(io::BufReader::new)
@@ -161,6 +214,14 @@ pub fn accelerate(
})
}
pub fn encoder(version: Version) -> Option<Box<dyn Encoder>> {
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::<Result<Vec<Event>>>().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::<Result<Vec<Event>>>().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::<Result<Vec<Event>>>().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::<Result<Vec<Event>>>().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::<Result<Vec<Event>>>().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::<Result<Vec<Event>>>().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));

View File

@@ -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<Asciicast<'static>> {
Some(Ok(Event::output(time, event.data)))
}));
Ok(Asciicast { header, events })
Ok(Asciicast {
version: Version::One,
header,
events,
})
}

View File

@@ -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,
}
}
}

View File

@@ -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<String>) -> Option<Result<Event>> {

View File

@@ -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::<Result<Vec<_>>>()?;
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;

View File

@@ -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;