2024-01-23 21:43:04 +01:00
|
|
|
mod util;
|
|
|
|
|
mod v1;
|
|
|
|
|
mod v2;
|
|
|
|
|
use anyhow::{anyhow, Result};
|
2023-10-28 13:36:54 +02:00
|
|
|
use std::collections::HashMap;
|
2023-10-28 10:23:15 +02:00
|
|
|
use std::fs;
|
2024-01-23 21:43:04 +01:00
|
|
|
use std::io::{self, BufRead};
|
2023-10-28 10:23:15 +02:00
|
|
|
use std::path::Path;
|
2024-01-23 21:43:04 +01:00
|
|
|
pub use v2::Writer;
|
2023-10-27 22:25:28 +02:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
pub struct Asciicast<'a> {
|
2024-01-20 22:35:20 +01:00
|
|
|
pub header: Header,
|
|
|
|
|
pub events: Box<dyn Iterator<Item = Result<Event>> + 'a>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 11:46:01 +02:00
|
|
|
pub struct Header {
|
2024-01-23 14:30:05 +01:00
|
|
|
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>>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 10:23:15 +02:00
|
|
|
pub struct Event {
|
2024-01-02 12:21:58 +01:00
|
|
|
pub time: u64,
|
2024-01-23 21:43:04 +01:00
|
|
|
pub data: EventData,
|
2023-10-27 22:25:28 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
pub enum EventData {
|
|
|
|
|
Output(String),
|
|
|
|
|
Input(String),
|
|
|
|
|
Resize(u16, u16),
|
|
|
|
|
Marker(String),
|
|
|
|
|
Other(char, String),
|
2024-01-02 12:21:58 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
pub fn open_from_path<S: AsRef<Path>>(path: S) -> Result<Asciicast<'static>> {
|
2024-01-22 12:04:17 +01:00
|
|
|
fs::File::open(path)
|
|
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map_err(|e| anyhow!(e))
|
|
|
|
|
.and_then(open)
|
|
|
|
|
.map_err(|e| anyhow!("can't open asciicast file: {e}"))
|
2024-01-20 22:35:20 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
pub fn open<'a, R: BufRead + 'a>(reader: R) -> Result<Asciicast<'a>> {
|
2023-10-28 10:23:15 +02:00
|
|
|
let mut lines = reader.lines();
|
2024-01-22 10:29:53 +01:00
|
|
|
let first_line = lines.next().ok_or(anyhow!("empty file"))??;
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
if let Ok(parser) = v2::open(&first_line) {
|
|
|
|
|
Ok(parser.parse(lines))
|
2024-01-23 14:30:05 +01:00
|
|
|
} else {
|
|
|
|
|
let json = std::iter::once(Ok(first_line))
|
|
|
|
|
.chain(lines)
|
|
|
|
|
.collect::<io::Result<String>>()?;
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
v1::load(json)
|
2024-01-02 12:21:58 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
pub fn get_duration<S: AsRef<Path>>(path: S) -> Result<u64> {
|
|
|
|
|
let Asciicast { events, .. } = open_from_path(path)?;
|
|
|
|
|
let time = events.last().map_or(Ok(0), |e| e.map(|e| e.time))?;
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
Ok(time)
|
2023-10-28 10:23:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Event {
|
2024-01-02 12:21:58 +01:00
|
|
|
pub fn output(time: u64, data: &[u8]) -> Self {
|
2023-10-28 10:23:15 +02:00
|
|
|
Event {
|
|
|
|
|
time,
|
2024-01-23 21:43:04 +01:00
|
|
|
data: EventData::Output(String::from_utf8_lossy(data).to_string()),
|
2023-10-28 10:23:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-02 12:21:58 +01:00
|
|
|
pub fn input(time: u64, data: &[u8]) -> Self {
|
2023-10-28 10:23:15 +02:00
|
|
|
Event {
|
|
|
|
|
time,
|
2024-01-23 21:43:04 +01:00
|
|
|
data: EventData::Input(String::from_utf8_lossy(data).to_string()),
|
2023-10-28 10:23:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-30 13:40:53 +01:00
|
|
|
|
2024-01-02 12:21:58 +01:00
|
|
|
pub fn resize(time: u64, size: (u16, u16)) -> Self {
|
2023-10-30 13:40:53 +01:00
|
|
|
Event {
|
|
|
|
|
time,
|
2024-01-23 21:43:04 +01:00
|
|
|
data: EventData::Resize(size.0, size.1),
|
2023-10-30 13:40:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-12 14:14:55 +01:00
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
pub fn marker(time: u64, label: String) -> Self {
|
2024-01-12 14:14:55 +01:00
|
|
|
Event {
|
|
|
|
|
time,
|
2024-01-25 16:01:14 +01:00
|
|
|
data: EventData::Marker(label),
|
2024-01-12 14:14:55 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-28 10:23:15 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-02 20:53:43 +01:00
|
|
|
pub fn limit_idle_time(
|
|
|
|
|
events: impl Iterator<Item = Result<Event>>,
|
|
|
|
|
limit: f64,
|
|
|
|
|
) -> impl Iterator<Item = Result<Event>> {
|
|
|
|
|
let limit = (limit * 1_000_000.0) as u64;
|
|
|
|
|
let mut prev_time = 0;
|
|
|
|
|
let mut offset = 0;
|
|
|
|
|
|
|
|
|
|
events.map(move |event| {
|
|
|
|
|
event.map(|event| {
|
|
|
|
|
let delay = event.time - prev_time;
|
|
|
|
|
|
|
|
|
|
if delay > limit {
|
|
|
|
|
offset += delay - limit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prev_time = event.time;
|
|
|
|
|
let time = event.time - offset;
|
|
|
|
|
|
|
|
|
|
Event { time, ..event }
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn accelerate(
|
|
|
|
|
events: impl Iterator<Item = Result<Event>>,
|
|
|
|
|
speed: f64,
|
|
|
|
|
) -> impl Iterator<Item = Result<Event>> {
|
|
|
|
|
events.map(move |event| {
|
|
|
|
|
event.map(|event| {
|
|
|
|
|
let time = ((event.time as f64) / speed) as u64;
|
|
|
|
|
|
|
|
|
|
Event { time, ..event }
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 10:23:15 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2024-01-23 21:43:04 +01:00
|
|
|
use super::{Asciicast, Event, EventData, Header, Writer};
|
2024-01-02 20:53:43 +01:00
|
|
|
use anyhow::Result;
|
2023-10-28 13:36:54 +02:00
|
|
|
use std::collections::HashMap;
|
2023-10-28 10:23:15 +02:00
|
|
|
use std::io;
|
|
|
|
|
|
|
|
|
|
#[test]
|
2024-01-23 14:30:05 +01:00
|
|
|
fn open_v1_minimal() {
|
2024-01-23 21:43:04 +01:00
|
|
|
let Asciicast { header, events } =
|
|
|
|
|
super::open_from_path("tests/casts/minimal.json").unwrap();
|
|
|
|
|
|
2024-01-23 14:30:05 +01:00
|
|
|
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);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "hello"));
|
2024-01-23 14:30:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn open_v1_full() {
|
2024-01-23 21:43:04 +01:00
|
|
|
let Asciicast { header, events } = super::open_from_path("tests/casts/full.json").unwrap();
|
2024-01-23 14:30:05 +01:00
|
|
|
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);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "ż"));
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
assert_eq!(events[1].time, 1000000);
|
|
|
|
|
assert!(matches!(events[1].data, EventData::Output(ref s) if s == "ółć"));
|
2024-01-23 14:30:05 +01:00
|
|
|
|
|
|
|
|
assert_eq!(events[2].time, 10500000);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[2].data, EventData::Output(ref s) if s == "\r\n"));
|
2024-01-23 14:30:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn open_v2() {
|
2024-01-23 21:43:04 +01:00
|
|
|
let Asciicast { header, events } = super::open_from_path("tests/casts/demo.cast").unwrap();
|
2024-01-02 20:53:43 +01:00
|
|
|
let events = events.take(7).collect::<Result<Vec<Event>>>().unwrap();
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-23 14:30:05 +01:00
|
|
|
assert_eq!((header.cols, header.rows), (75, 18));
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(events[1].time, 100989);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[1].data, EventData::Output(ref s) if s == "\u{1b}[?2004h"));
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(events[5].time, 1511526);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[5].data, EventData::Input(ref s) if s == "v"));
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(events[6].time, 1511937);
|
2024-01-23 21:43:04 +01:00
|
|
|
assert!(matches!(events[6].data, EventData::Output(ref s) if s == "v"));
|
2023-10-28 10:23:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn writer() {
|
|
|
|
|
let mut data = Vec::new();
|
|
|
|
|
|
2023-11-03 11:05:19 +01:00
|
|
|
{
|
2024-01-23 11:52:28 +01:00
|
|
|
let mut fw = Writer::new(&mut data, 0);
|
2023-11-03 11:05:19 +01:00
|
|
|
|
|
|
|
|
let header = Header {
|
2024-01-22 10:30:40 +01:00
|
|
|
version: 2,
|
2024-01-23 14:30:05 +01:00
|
|
|
cols: 80,
|
|
|
|
|
rows: 24,
|
2024-01-08 14:18:09 +01:00
|
|
|
timestamp: None,
|
2023-11-03 11:05:19 +01:00
|
|
|
idle_time_limit: None,
|
|
|
|
|
command: None,
|
|
|
|
|
title: None,
|
|
|
|
|
env: Default::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fw.write_header(&header).unwrap();
|
2024-01-25 16:01:14 +01:00
|
|
|
|
|
|
|
|
fw.write_event(&Event::output(1000001, "hello\r\n".as_bytes()))
|
2023-11-03 11:05:19 +01:00
|
|
|
.unwrap();
|
|
|
|
|
}
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2023-11-03 11:05:19 +01:00
|
|
|
{
|
2024-01-23 11:52:28 +01:00
|
|
|
let mut fw = Writer::new(&mut data, 1000001);
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
fw.write_event(&Event::output(1000001, "world".as_bytes()))
|
2024-01-02 12:21:58 +01:00
|
|
|
.unwrap();
|
2024-01-25 16:01:14 +01:00
|
|
|
|
|
|
|
|
fw.write_event(&Event::input(2000002, " ".as_bytes()))
|
2023-11-03 11:05:19 +01:00
|
|
|
.unwrap();
|
2024-01-25 16:01:14 +01:00
|
|
|
|
|
|
|
|
fw.write_event(&Event::resize(3000003, (100, 40))).unwrap();
|
|
|
|
|
|
|
|
|
|
fw.write_event(&Event::output(4000004, "żółć".as_bytes()))
|
2024-01-12 13:43:16 +01:00
|
|
|
.unwrap();
|
2023-11-03 11:05:19 +01:00
|
|
|
}
|
2023-10-28 10:23:15 +02:00
|
|
|
|
2023-10-28 13:36:54 +02:00
|
|
|
let lines = parse(data);
|
|
|
|
|
|
|
|
|
|
assert_eq!(lines[0]["version"], 2);
|
|
|
|
|
assert_eq!(lines[0]["width"], 80);
|
|
|
|
|
assert_eq!(lines[0]["height"], 24);
|
2024-01-08 14:18:09 +01:00
|
|
|
assert!(lines[0]["timestamp"].is_null());
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(lines[1][0], 1.000001);
|
2023-10-28 13:36:54 +02:00
|
|
|
assert_eq!(lines[1][1], "o");
|
|
|
|
|
assert_eq!(lines[1][2], "hello\r\n");
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(lines[2][0], 2.000002);
|
2023-10-28 13:36:54 +02:00
|
|
|
assert_eq!(lines[2][1], "o");
|
|
|
|
|
assert_eq!(lines[2][2], "world");
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(lines[3][0], 3.000003);
|
2023-10-30 13:46:04 +01:00
|
|
|
assert_eq!(lines[3][1], "i");
|
|
|
|
|
assert_eq!(lines[3][2], " ");
|
2024-01-02 12:21:58 +01:00
|
|
|
assert_eq!(lines[4][0], 4.000004);
|
2023-10-30 13:46:04 +01:00
|
|
|
assert_eq!(lines[4][1], "r");
|
|
|
|
|
assert_eq!(lines[4][2], "100x40");
|
2024-01-12 13:43:16 +01:00
|
|
|
assert_eq!(lines[5][0], 5.000005);
|
|
|
|
|
assert_eq!(lines[5][1], "o");
|
|
|
|
|
assert_eq!(lines[5][2], "żółć");
|
2023-10-27 22:25:28 +02:00
|
|
|
}
|
2023-10-28 10:44:12 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn write_header() {
|
|
|
|
|
let mut data = Vec::new();
|
2023-11-03 11:05:19 +01:00
|
|
|
|
|
|
|
|
{
|
2024-01-23 11:52:28 +01:00
|
|
|
let mut fw = Writer::new(io::Cursor::new(&mut data), 0);
|
2023-11-03 11:05:19 +01:00
|
|
|
let mut env = HashMap::new();
|
|
|
|
|
env.insert("SHELL".to_owned(), "/usr/bin/fish".to_owned());
|
|
|
|
|
env.insert("TERM".to_owned(), "xterm256-color".to_owned());
|
|
|
|
|
|
|
|
|
|
let header = Header {
|
2024-01-22 10:30:40 +01:00
|
|
|
version: 2,
|
2024-01-23 14:30:05 +01:00
|
|
|
cols: 80,
|
|
|
|
|
rows: 24,
|
2024-01-08 14:18:09 +01:00
|
|
|
timestamp: Some(1704719152),
|
2023-11-03 11:05:19 +01:00
|
|
|
idle_time_limit: Some(1.5),
|
|
|
|
|
command: Some("/bin/bash".to_owned()),
|
|
|
|
|
title: Some("Demo".to_owned()),
|
2024-01-08 14:18:09 +01:00
|
|
|
env: Some(env),
|
2023-11-03 11:05:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fw.write_header(&header).unwrap();
|
|
|
|
|
}
|
2023-10-28 10:44:12 +02:00
|
|
|
|
2023-10-28 13:36:54 +02:00
|
|
|
let lines = parse(data);
|
|
|
|
|
|
|
|
|
|
assert_eq!(lines[0]["version"], 2);
|
|
|
|
|
assert_eq!(lines[0]["width"], 80);
|
|
|
|
|
assert_eq!(lines[0]["height"], 24);
|
2024-01-08 14:18:09 +01:00
|
|
|
assert_eq!(lines[0]["timestamp"], 1704719152);
|
2023-10-28 13:36:54 +02:00
|
|
|
assert_eq!(lines[0]["idle_time_limit"], 1.5);
|
|
|
|
|
assert_eq!(lines[0]["command"], "/bin/bash");
|
|
|
|
|
assert_eq!(lines[0]["title"], "Demo");
|
|
|
|
|
assert_eq!(lines[0]["env"].as_object().unwrap().len(), 2);
|
|
|
|
|
assert_eq!(lines[0]["env"]["SHELL"], "/usr/bin/fish");
|
|
|
|
|
assert_eq!(lines[0]["env"]["TERM"], "xterm256-color");
|
|
|
|
|
}
|
2023-10-28 10:44:12 +02:00
|
|
|
|
2023-10-28 13:36:54 +02:00
|
|
|
fn parse(json: Vec<u8>) -> Vec<serde_json::Value> {
|
|
|
|
|
String::from_utf8(json)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.split('\n')
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
.map(serde_json::from_str::<serde_json::Value>)
|
|
|
|
|
.collect::<serde_json::Result<Vec<_>>>()
|
|
|
|
|
.unwrap()
|
2023-10-28 10:44:12 +02:00
|
|
|
}
|
2024-01-02 20:53:43 +01:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn accelerate() {
|
2024-01-23 21:43:04 +01:00
|
|
|
let events = [(0u64, "foo"), (20, "bar"), (50, "baz")]
|
2024-01-02 20:53:43 +01:00
|
|
|
.map(|(time, output)| Ok(Event::output(time, output.as_bytes())));
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
let output = output(super::accelerate(events.into_iter(), 2.0));
|
2024-01-02 20:53:43 +01:00
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
assert_eq!(output[0], (0, "foo".to_owned()));
|
|
|
|
|
assert_eq!(output[1], (10, "bar".to_owned()));
|
|
|
|
|
assert_eq!(output[2], (25, "baz".to_owned()));
|
2024-01-02 20:53:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn limit_idle_time() {
|
2024-01-23 21:43:04 +01:00
|
|
|
let events = [
|
2024-01-02 20:53:43 +01:00
|
|
|
(0, "foo"),
|
|
|
|
|
(1_000_000, "bar"),
|
|
|
|
|
(3_500_000, "baz"),
|
|
|
|
|
(4_000_000, "qux"),
|
|
|
|
|
(7_500_000, "quux"),
|
|
|
|
|
]
|
|
|
|
|
.map(|(time, output)| Ok(Event::output(time, output.as_bytes())));
|
|
|
|
|
|
2024-01-23 21:43:04 +01:00
|
|
|
let events = output(super::limit_idle_time(events.into_iter(), 2.0));
|
|
|
|
|
|
|
|
|
|
assert_eq!(events[0], (0, "foo".to_owned()));
|
|
|
|
|
assert_eq!(events[1], (1_000_000, "bar".to_owned()));
|
|
|
|
|
assert_eq!(events[2], (3_000_000, "baz".to_owned()));
|
|
|
|
|
assert_eq!(events[3], (3_500_000, "qux".to_owned()));
|
|
|
|
|
assert_eq!(events[4], (5_500_000, "quux".to_owned()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn output(events: impl Iterator<Item = Result<Event>>) -> Vec<(u64, String)> {
|
|
|
|
|
events
|
|
|
|
|
.filter_map(|r| {
|
|
|
|
|
if let Ok(Event {
|
|
|
|
|
time,
|
|
|
|
|
data: EventData::Output(data),
|
|
|
|
|
}) = r
|
|
|
|
|
{
|
|
|
|
|
Some((time, data))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
2024-01-02 20:53:43 +01:00
|
|
|
}
|
2023-10-27 22:25:28 +02:00
|
|
|
}
|