Support for opening v1 asciicasts

This commit is contained in:
Marcin Kulik
2024-01-23 14:30:05 +01:00
parent 68bc7266f9
commit c0ec1ac758
6 changed files with 192 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::{self, Display};
@@ -17,8 +17,37 @@ pub struct Writer<W: Write> {
time_offset: u64,
}
#[derive(Deserialize)]
pub struct Header {
pub version: u8,
pub cols: u16,
pub rows: u16,
pub timestamp: Option<u64>,
pub idle_time_limit: Option<f64>,
pub command: Option<String>,
pub title: Option<String>,
pub env: Option<HashMap<String, String>>,
}
#[derive(Debug, Deserialize)]
struct V1 {
version: u8,
width: u16,
height: u16,
command: Option<String>,
title: Option<String>,
env: Option<HashMap<String, String>>,
stdout: Vec<V1Stdout>,
}
#[derive(Debug, Deserialize)]
struct V1Stdout {
#[serde(deserialize_with = "deserialize_time")]
time: u64,
data: String,
}
#[derive(Deserialize)]
pub struct V2Header {
pub version: u8,
pub width: u16,
pub height: u16,
@@ -59,7 +88,8 @@ where
}
pub fn write_header(&mut self, header: &Header) -> io::Result<()> {
writeln!(self.writer, "{}", serde_json::to_string(header)?)
let header: V2Header = header.into();
writeln!(self.writer, "{}", serde_json::to_string(&header)?)
}
pub fn write_event(&mut self, mut event: Event) -> io::Result<()> {
@@ -87,10 +117,38 @@ pub fn open_from_path<S: AsRef<Path>>(path: S) -> Result<Reader<'static>> {
pub fn open<'a, R: BufRead + 'a>(reader: R) -> Result<Reader<'a>> {
let mut lines = reader.lines();
let first_line = lines.next().ok_or(anyhow!("empty file"))??;
let header: Header = serde_json::from_str(&first_line)?;
let events = Box::new(lines.filter_map(parse_event));
Ok(Reader { header, events })
if let Ok(header) = serde_json::from_str::<V2Header>(&first_line) {
if header.version != 2 {
bail!("unsupported asciicast version")
}
let header: Header = header.into();
let events = Box::new(lines.filter_map(parse_event));
Ok(Reader { header, events })
} else {
let json = std::iter::once(Ok(first_line))
.chain(lines)
.collect::<io::Result<String>>()?;
let asciicast: V1 = serde_json::from_str(&json)?;
if asciicast.version != 1 {
bail!("unsupported asciicast version")
}
let header: Header = (&asciicast).into();
let events = Box::new(
asciicast
.stdout
.into_iter()
.map(|e| Ok(Event::output(e.time, e.data.as_bytes()))),
);
Ok(Reader { header, events })
}
}
fn parse_event(line: io::Result<String>) -> Option<Result<Event>> {
@@ -200,7 +258,7 @@ impl Display for EventCode {
}
}
impl serde::Serialize for Header {
impl serde::Serialize for V2Header {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
@@ -253,6 +311,51 @@ impl serde::Serialize for Header {
}
}
impl From<&Header> for V2Header {
fn from(header: &Header) -> Self {
V2Header {
version: 2,
width: header.cols,
height: header.rows,
timestamp: header.timestamp,
idle_time_limit: header.idle_time_limit,
command: header.command.clone(),
title: header.title.clone(),
env: header.env.clone(),
}
}
}
impl From<V2Header> for Header {
fn from(header: V2Header) -> Self {
Header {
version: 2,
cols: header.width,
rows: header.height,
timestamp: None,
idle_time_limit: None,
command: header.command,
title: header.title,
env: header.env,
}
}
}
impl From<&V1> for Header {
fn from(header: &V1) -> Self {
Header {
version: 1,
cols: header.width,
rows: header.height,
timestamp: None,
idle_time_limit: None,
command: header.command.clone(),
title: header.title.clone(),
env: header.env.clone(),
}
}
}
fn serialize_event(event: &Event) -> Result<String, serde_json::Error> {
Ok(format!(
"[{}, {}, {}]",
@@ -312,13 +415,48 @@ mod tests {
use std::io;
#[test]
fn open() {
let file = File::open("tests/demo.cast").unwrap();
fn open_v1_minimal() {
let file = File::open("tests/casts/minimal.json").unwrap();
let Reader { header, events } = super::open(io::BufReader::new(file)).unwrap();
let events = events.collect::<Result<Vec<Event>>>().unwrap();
assert_eq!(header.version, 1);
assert_eq!((header.cols, header.rows), (100, 50));
assert_eq!(events[0].time, 1230000);
assert_eq!(events[0].code, EventCode::Output);
assert_eq!(events[0].data, "hello");
}
#[test]
fn open_v1_full() {
let file = File::open("tests/casts/full.json").unwrap();
let Reader { header, events } = super::open(io::BufReader::new(file)).unwrap();
let events = events.collect::<Result<Vec<Event>>>().unwrap();
assert_eq!(header.version, 1);
assert_eq!((header.cols, header.rows), (100, 50));
assert_eq!(events[0].time, 1);
assert_eq!(events[0].code, EventCode::Output);
assert_eq!(events[0].data, "ż");
assert_eq!(events[1].time, 100000);
assert_eq!(events[1].code, EventCode::Output);
assert_eq!(events[1].data, "ółć");
assert_eq!(events[2].time, 10500000);
assert_eq!(events[2].code, EventCode::Output);
assert_eq!(events[2].data, "\r\n");
}
#[test]
fn open_v2() {
let file = File::open("tests/casts/demo.cast").unwrap();
let Reader { header, events } = super::open(io::BufReader::new(file)).unwrap();
let events = events.take(7).collect::<Result<Vec<Event>>>().unwrap();
assert_eq!((header.width, header.height), (75, 18));
assert_eq!((header.cols, header.rows), (75, 18));
assert_eq!(events[1].time, 100989);
assert_eq!(events[1].code, EventCode::Output);
@@ -342,8 +480,8 @@ mod tests {
let header = Header {
version: 2,
width: 80,
height: 24,
cols: 80,
rows: 24,
timestamp: None,
idle_time_limit: None,
command: None,
@@ -403,8 +541,8 @@ mod tests {
let header = Header {
version: 2,
width: 80,
height: 24,
cols: 80,
rows: 24,
timestamp: Some(1704719152),
idle_time_limit: Some(1.5),
command: Some("/bin/bash".to_owned()),

View File

@@ -30,12 +30,12 @@ where
}
fn build_header(&self, timestamp: u64, tty_size: &tty::TtySize) -> Header {
let (width, height) = (*tty_size).into();
let (cols, rows) = (*tty_size).into();
Header {
version: 2,
width,
height,
cols,
rows,
timestamp: Some(timestamp),
idle_time_limit: self.metadata.idle_time_limit,
command: self.metadata.command.clone(),

26
tests/casts/full.json Normal file
View File

@@ -0,0 +1,26 @@
{
"version": 1,
"width": 100,
"height": 50,
"duration": 10.5,
"command": "/bin/bash",
"title": null,
"env": {
"TERM": "xterm-256color",
"SHELL": "/bin/bash"
},
"stdout": [
[
0.000001,
"ż"
],
[
0.100000,
"ółć"
],
[
10.500000,
"\r\n"
]
]
}

11
tests/casts/minimal.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": 1,
"width": 100,
"height": 50,
"stdout": [
[
1.230000,
"hello"
]
]
}

View File

@@ -1,40 +0,0 @@
{"env": {"TERM": "xterm-256color", "SHELL": "/usr/local/bin/fish"}, "width": 75, "height": 18, "timestamp": 1509091818, "version": 2, "idle_time_limit": 2.0}
[0.089436, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
[0.100989, "o", "\u001b[?2004h"]
[0.164215, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
[0.164513, "o", "\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K"]
[0.164709, "o", "\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
[1.511526, "i", "v"]
[1.511937, "o", "v"]
[1.512148, "o", "\b\u001b[38;2;0;95;215mv\u001b[30m\u001b(B\u001b[m"]
[1.514564, "o", "\u001b[38;2;85;85;85mim tests/vim.cast \u001b[18D\u001b[30m\u001b(B\u001b[m"]
[1.615727, "i", "i"]
[1.616261, "o", "\u001b[38;2;0;95;215mi\u001b[38;2;85;85;85mm tests/vim.cast \u001b[17D\u001b[30m\u001b(B\u001b[m"]
[1.694908, "i", "m"]
[1.695262, "o", "\u001b[38;2;0;95;215mm\u001b[38;2;85;85;85m tests/vim.cast \u001b[16D\u001b[30m\u001b(B\u001b[m"]
[2.751713, "i", "\r"]
[2.752186, "o", "\u001b[K\r\n\u001b[30m"]
[2.752381, "o", "\u001b(B\u001b[m\u001b[?2004l"]
[2.752718, "o", "\u001b]0;vim /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m\r"]
[2.86619, "o", "\u001b[?1000h\u001b[?2004h\u001b[?1049h\u001b[?1h\u001b=\u001b[?2004h"]
[2.867669, "o", "\u001b[1;18r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[H\u001b[2J\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[1;1H\u001b[>c"]
[2.868169, "i", "\u001b[2;2R\u001b[>0;95;0c"]
[2.869918, "o", "\u001b[?1000l\u001b[?1002h\u001b[?12$p"]
[2.870136, "o", "\u001b[?25l\u001b[1;1H\u001b[93m1 \u001b[m\u001b[38;5;231m\u001b[48;5;235m\r\n\u001b[38;5;59m\u001b[48;5;236m~ \u001b[3;1H~ \u001b[4;1H~ \u001b[5;1H~ \u001b[6;1H~ \u001b[7;1H~ \u001b[8;1H~ \u001b[9;1H~ \u001b[10;1H~ \u001b[11;1H~ \u001b[12;1H~ \u001b[13;1H~ "]
[2.870245, "o", " \u001b[14;1H~ \u001b[15;1H~ \u001b[16;1H~ \u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[17;1H\u001b[1m\u001b[38;5;231m\u001b[48;5;236m[No Name] (unix/utf-8/) (line 0/1, col 000)\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[3;30HVIM - Vi IMproved\u001b[5;30Hversion 8.0.1171\u001b[6;26Hby Bram Moolenaar et al.\u001b[7;17HVim is open source and freely distributable\u001b[9;24HBecome a registered Vim user!\u001b[10;15Htype :help register\u001b[38;5;59m\u001b[48;5;236m<Enter>\u001b[m\u001b[38;5;231m\u001b[48;5;235m for information \u001b[12;15Htype :q\u001b[38;5;59m\u001b[48;5;236m<Enter>\u001b[m\u001b[38;5;231m\u001b[48;5;235m to exit \u001b[13;15Htype :help\u001b[38;5;59m\u001b[48;5;236m<Enter>\u001b[m\u001b[38;5;231m\u001b[48;5;235m or \u001b[38;5;59m\u001b[48;5;236m<F1>\u001b[m\u001b[38;5;231m\u001b[48;5;235m for on-line help\u001b[14;15Htype :help version8\u001b[38;5;59m\u001b[48;5;236m<Enter>\u001b[m\u001b[38;5;231m\u001b[48;5;235m for version"]
[2.870302, "o", " info\u001b[1;5H\u001b[?25h"]
[5.63147, "i", ":"]
[5.631755, "o", "\u001b[?25l\u001b[18;65H:\u001b[1;5H"]
[5.631934, "o", "\u001b[18;65H\u001b[K\u001b[18;1H:\u001b[?2004l\u001b[?2004h\u001b[?25h"]
[6.16692, "i", "q"]
[6.167137, "o", "q\u001b[?25l\u001b[?25h"]
[7.463349, "i", "\r"]
[7.463561, "o", "\r"]
[7.498922, "o", "\u001b[?25l\u001b[?1002l\u001b[?2004l"]
[7.604236, "o", "\u001b[18;1H\u001b[K\u001b[18;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l"]
[7.612576, "o", "\u001b[?2004h"]
[7.655999, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
[7.656239, "o", "\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
[11.891762, "i", "\u0004"]
[11.893297, "o", "\r\n\u001b[30m\u001b(B\u001b[m\u001b[30m\u001b(B\u001b[m"]
[11.89348, "o", "\u001b[?2004l"]