mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-15 19:28:00 +01:00
Support for opening v1 asciicasts
This commit is contained in:
166
src/asciicast.rs
166
src/asciicast.rs
@@ -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()),
|
||||
|
||||
@@ -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
26
tests/casts/full.json
Normal 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
11
tests/casts/minimal.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"width": 100,
|
||||
"height": 50,
|
||||
"stdout": [
|
||||
[
|
||||
1.230000,
|
||||
"hello"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user