mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 19:58:03 +01:00
Use Duration instead of u64 for event timestamps
This commit is contained in:
82
src/alis.rs
82
src/alis.rs
@@ -5,6 +5,7 @@
|
||||
// See more at: https://docs.asciinema.org/manual/server/streaming/
|
||||
|
||||
use std::future;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::{stream, Stream, StreamExt};
|
||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||
@@ -14,13 +15,14 @@ use crate::stream::Event;
|
||||
|
||||
static MAGIC_STRING: &str = "ALiS\x01";
|
||||
|
||||
struct EventSerializer(u64);
|
||||
#[derive(Default)]
|
||||
struct EventSerializer(Duration);
|
||||
|
||||
pub fn stream<S: Stream<Item = Result<Event, BroadcastStreamRecvError>>>(
|
||||
stream: S,
|
||||
) -> impl Stream<Item = Result<Vec<u8>, BroadcastStreamRecvError>> {
|
||||
let header = stream::once(future::ready(Ok(MAGIC_STRING.into())));
|
||||
let mut serializer = EventSerializer(0);
|
||||
let mut serializer = EventSerializer::default();
|
||||
let events = stream.map(move |event| event.map(|event| serializer.serialize_event(event)));
|
||||
|
||||
header.chain(events)
|
||||
@@ -33,7 +35,7 @@ impl EventSerializer {
|
||||
match event {
|
||||
Init(last_id, time, size, theme, init) => {
|
||||
let last_id_bytes = leb128::encode(last_id);
|
||||
let time_bytes = leb128::encode(time);
|
||||
let time_bytes = leb128::encode(time.as_micros() as u64);
|
||||
let cols_bytes = leb128::encode(size.0);
|
||||
let rows_bytes = leb128::encode(size.1);
|
||||
let init_len = init.len() as u32;
|
||||
@@ -150,12 +152,12 @@ impl EventSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
fn rel_time(&mut self, time: u64) -> u64 {
|
||||
fn rel_time(&mut self, time: Duration) -> u64 {
|
||||
let time = time.max(self.0);
|
||||
let rel_time = time - self.0;
|
||||
self.0 = time;
|
||||
|
||||
rel_time
|
||||
rel_time.as_micros() as u64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +170,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serialize_init_with_theme_and_seed() {
|
||||
let mut serializer = EventSerializer(0);
|
||||
let mut serializer = EventSerializer(Duration::from_millis(0));
|
||||
|
||||
let theme = TtyTheme {
|
||||
fg: rgb(255, 255, 255),
|
||||
@@ -195,7 +197,7 @@ mod tests {
|
||||
|
||||
let event = Event::Init(
|
||||
42,
|
||||
1000,
|
||||
Duration::from_micros(1000),
|
||||
TtySize(180, 24),
|
||||
Some(theme),
|
||||
"terminal seed".to_string(),
|
||||
@@ -238,13 +240,21 @@ mod tests {
|
||||
expected.extend_from_slice(b"terminal seed"); // init string
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 1000);
|
||||
assert_eq!(serializer.0.as_micros(), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_init_without_theme_nor_seed() {
|
||||
let mut serializer = EventSerializer(0);
|
||||
let event = Event::Init(1, 500, TtySize(120, 130), None, "".to_string());
|
||||
let mut serializer = EventSerializer::default();
|
||||
|
||||
let event = Event::Init(
|
||||
1,
|
||||
Duration::from_micros(500),
|
||||
TtySize(120, 130),
|
||||
None,
|
||||
"".to_string(),
|
||||
);
|
||||
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -258,13 +268,13 @@ mod tests {
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 500);
|
||||
assert_eq!(serializer.0.as_micros(), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_output() {
|
||||
let mut serializer = EventSerializer(1000);
|
||||
let event = Event::Output(5, 1200, "Hello 世界 🌍".to_string());
|
||||
let mut serializer = EventSerializer(Duration::from_micros(1000));
|
||||
let event = Event::Output(5, Duration::from_micros(1200), "Hello 世界 🌍".to_string());
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let mut expected = vec![
|
||||
@@ -277,13 +287,13 @@ mod tests {
|
||||
expected.extend_from_slice("Hello 世界 🌍".as_bytes()); // text bytes
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 1200); // Time updated to 1200
|
||||
assert_eq!(serializer.0.as_micros(), 1200); // Time updated to 1200
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_input() {
|
||||
let mut serializer = EventSerializer(500);
|
||||
let event = Event::Input(1000000, 750, "x".to_string());
|
||||
let mut serializer = EventSerializer(Duration::from_micros(500));
|
||||
let event = Event::Input(1000000, Duration::from_micros(750), "x".to_string());
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -295,13 +305,13 @@ mod tests {
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 750);
|
||||
assert_eq!(serializer.0.as_micros(), 750);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_resize() {
|
||||
let mut serializer = EventSerializer(2000);
|
||||
let event = Event::Resize(15, 2100, TtySize(180, 50));
|
||||
let mut serializer = EventSerializer(Duration::from_micros(2000));
|
||||
let event = Event::Resize(15, Duration::from_micros(2100), TtySize(180, 50));
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -313,13 +323,13 @@ mod tests {
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 2100);
|
||||
assert_eq!(serializer.0.as_micros(), 2100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_marker_with_label() {
|
||||
let mut serializer = EventSerializer(3000);
|
||||
let event = Event::Marker(20, 3500, "checkpoint".to_string());
|
||||
let mut serializer = EventSerializer(Duration::from_micros(3000));
|
||||
let event = Event::Marker(20, Duration::from_micros(3500), "checkpoint".to_string());
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -332,13 +342,13 @@ mod tests {
|
||||
expected.extend_from_slice(b"checkpoint"); // label bytes
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 3500);
|
||||
assert_eq!(serializer.0.as_micros(), 3500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_marker_without_label() {
|
||||
let mut serializer = EventSerializer(3000);
|
||||
let event = Event::Marker(2, 3300, "".to_string());
|
||||
let mut serializer = EventSerializer(Duration::from_micros(3000));
|
||||
let event = Event::Marker(2, Duration::from_micros(3300), "".to_string());
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -353,8 +363,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serialize_exit_positive_status() {
|
||||
let mut serializer = EventSerializer(4000);
|
||||
let event = Event::Exit(25, 4200, 0);
|
||||
let mut serializer = EventSerializer(Duration::from_micros(4000));
|
||||
let event = Event::Exit(25, Duration::from_micros(4200), 0);
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -365,13 +375,13 @@ mod tests {
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 4200);
|
||||
assert_eq!(serializer.0.as_micros(), 4200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_exit_negative_status() {
|
||||
let mut serializer = EventSerializer(5000);
|
||||
let event = Event::Exit(30, 5300, -1);
|
||||
let mut serializer = EventSerializer(Duration::from_micros(5000));
|
||||
let event = Event::Exit(30, Duration::from_micros(5300), -1);
|
||||
let bytes = serializer.serialize_event(event);
|
||||
|
||||
let expected = vec![
|
||||
@@ -382,27 +392,27 @@ mod tests {
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
assert_eq!(serializer.0, 5300);
|
||||
assert_eq!(serializer.0.as_micros(), 5300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subsequent_event_lower_time() {
|
||||
let mut serializer = EventSerializer(1000);
|
||||
let mut serializer = EventSerializer(Duration::from_micros(1000));
|
||||
|
||||
// First event at time 1000
|
||||
let event1 = Event::Output(1, 1000, "first".to_string());
|
||||
let event1 = Event::Output(1, Duration::from_micros(1000), "first".to_string());
|
||||
let bytes1 = serializer.serialize_event(event1);
|
||||
|
||||
// Verify first event uses time 0 (1000 - 1000)
|
||||
assert_eq!(bytes1[2], 0x00); // relative time should be 0
|
||||
assert_eq!(serializer.0, 1000);
|
||||
assert_eq!(serializer.0.as_micros(), 1000);
|
||||
|
||||
// Second event with lower timestamp (wraparound risk case)
|
||||
let event2 = Event::Output(2, 500, "second".to_string());
|
||||
let event2 = Event::Output(2, Duration::from_micros(500), "second".to_string());
|
||||
let bytes2 = serializer.serialize_event(event2);
|
||||
|
||||
assert_eq!(bytes2[2], 0x00); // relative time should be 0
|
||||
assert_eq!(serializer.0, 1000); // Time should remain 1000 (not decrease)
|
||||
assert_eq!(serializer.0.as_micros(), 1000); // Time should remain 1000 (not decrease)
|
||||
}
|
||||
|
||||
fn rgb(r: u8, g: u8, b: u8) -> RGB8 {
|
||||
|
||||
143
src/asciicast.rs
143
src/asciicast.rs
@@ -8,6 +8,7 @@ use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::io::{self, BufRead};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
@@ -42,7 +43,7 @@ pub struct Header {
|
||||
}
|
||||
|
||||
pub struct Event {
|
||||
pub time: u64,
|
||||
pub time: Duration,
|
||||
pub data: EventData,
|
||||
}
|
||||
|
||||
@@ -141,43 +142,45 @@ pub fn open<'a, R: BufRead + 'a>(reader: R) -> Result<Asciicast<'a>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_duration<S: AsRef<Path>>(path: S) -> Result<u64> {
|
||||
pub fn get_duration<S: AsRef<Path>>(path: S) -> Result<Duration> {
|
||||
let Asciicast { events, .. } = open_from_path(path)?;
|
||||
let time = events.last().map_or(Ok(0), |e| e.map(|e| e.time))?;
|
||||
let time = events
|
||||
.last()
|
||||
.map_or(Ok(Duration::from_micros(0)), |e| e.map(|e| e.time))?;
|
||||
|
||||
Ok(time)
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn output(time: u64, text: String) -> Self {
|
||||
pub fn output(time: Duration, text: String) -> Self {
|
||||
Event {
|
||||
time,
|
||||
data: EventData::Output(text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input(time: u64, text: String) -> Self {
|
||||
pub fn input(time: Duration, text: String) -> Self {
|
||||
Event {
|
||||
time,
|
||||
data: EventData::Input(text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(time: u64, size: (u16, u16)) -> Self {
|
||||
pub fn resize(time: Duration, size: (u16, u16)) -> Self {
|
||||
Event {
|
||||
time,
|
||||
data: EventData::Resize(size.0, size.1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn marker(time: u64, label: String) -> Self {
|
||||
pub fn marker(time: Duration, label: String) -> Self {
|
||||
Event {
|
||||
time,
|
||||
data: EventData::Marker(label),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(time: u64, status: i32) -> Self {
|
||||
pub fn exit(time: Duration, status: i32) -> Self {
|
||||
Event {
|
||||
time,
|
||||
data: EventData::Exit(status),
|
||||
@@ -189,9 +192,9 @@ 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;
|
||||
let limit = Duration::from_micros((limit * 1_000_000.0) as u64);
|
||||
let mut prev_time = Duration::from_micros(0);
|
||||
let mut offset = Duration::from_micros(0);
|
||||
|
||||
events.map(move |event| {
|
||||
event.map(|event| {
|
||||
@@ -215,7 +218,7 @@ pub fn accelerate(
|
||||
) -> impl Iterator<Item = Result<Event>> {
|
||||
events.map(move |event| {
|
||||
event.map(|event| {
|
||||
let time = ((event.time as f64) / speed) as u64;
|
||||
let time = event.time.div_f64(speed);
|
||||
|
||||
Event { time, ..event }
|
||||
})
|
||||
@@ -225,18 +228,21 @@ 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::Two => Some(Box::new(V2Encoder::new(Duration::from_micros(0)))),
|
||||
Version::Three => Some(Box::new(V3Encoder::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Asciicast, Event, EventData, Header, V2Encoder};
|
||||
use crate::tty::TtyTheme;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use rgb::RGB8;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{Asciicast, Event, EventData, Header, V2Encoder};
|
||||
use crate::tty::TtyTheme;
|
||||
|
||||
#[test]
|
||||
fn open_v1_minimal() {
|
||||
@@ -252,7 +258,7 @@ mod tests {
|
||||
assert_eq!((header.term_cols, header.term_rows), (100, 50));
|
||||
assert!(header.term_theme.is_none());
|
||||
|
||||
assert_eq!(events[0].time, 1230000);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1230000));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "hello"));
|
||||
}
|
||||
|
||||
@@ -268,13 +274,13 @@ mod tests {
|
||||
assert_eq!(version, 1);
|
||||
assert_eq!((header.term_cols, header.term_rows), (100, 50));
|
||||
|
||||
assert_eq!(events[0].time, 1);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "ż"));
|
||||
|
||||
assert_eq!(events[1].time, 10000001);
|
||||
assert_eq!(events[1].time, Duration::from_micros(10000001));
|
||||
assert!(matches!(events[1].data, EventData::Output(ref s) if s == "ółć"));
|
||||
|
||||
assert_eq!(events[2].time, 10500001);
|
||||
assert_eq!(events[2].time, Duration::from_micros(10500001));
|
||||
assert!(matches!(events[2].data, EventData::Output(ref s) if s == "\r\n"));
|
||||
}
|
||||
|
||||
@@ -292,7 +298,7 @@ mod tests {
|
||||
assert_eq!((header.term_cols, header.term_rows), (100, 50));
|
||||
assert!(header.term_theme.is_none());
|
||||
|
||||
assert_eq!(events[0].time, 1230000);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1230000));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "hello"));
|
||||
}
|
||||
|
||||
@@ -313,22 +319,22 @@ mod tests {
|
||||
assert_eq!(theme.bg, RGB8::new(0xff, 0xff, 0xff));
|
||||
assert_eq!(theme.palette[0], RGB8::new(0x24, 0x1f, 0x31));
|
||||
|
||||
assert_eq!(events[0].time, 1);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "ż"));
|
||||
|
||||
assert_eq!(events[1].time, 1_000_000);
|
||||
assert_eq!(events[1].time, Duration::from_micros(1_000_000));
|
||||
assert!(matches!(events[1].data, EventData::Output(ref s) if s == "ółć"));
|
||||
|
||||
assert_eq!(events[2].time, 2_300_000);
|
||||
assert_eq!(events[2].time, Duration::from_micros(2_300_000));
|
||||
assert!(matches!(events[2].data, EventData::Input(ref s) if s == "\n"));
|
||||
|
||||
assert_eq!(events[3].time, 5_600_001);
|
||||
assert_eq!(events[3].time, Duration::from_micros(5_600_001));
|
||||
|
||||
assert!(
|
||||
matches!(events[3].data, EventData::Resize(ref cols, ref rows) if *cols == 80 && *rows == 40)
|
||||
);
|
||||
|
||||
assert_eq!(events[4].time, 10_500_000);
|
||||
assert_eq!(events[4].time, Duration::from_micros(10_500_000));
|
||||
assert!(matches!(events[4].data, EventData::Output(ref s) if s == "\r\n"));
|
||||
}
|
||||
|
||||
@@ -346,7 +352,7 @@ mod tests {
|
||||
assert_eq!((header.term_cols, header.term_rows), (100, 50));
|
||||
assert!(header.term_theme.is_none());
|
||||
|
||||
assert_eq!(events[0].time, 1230000);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1230000));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "hello"));
|
||||
}
|
||||
|
||||
@@ -367,22 +373,22 @@ mod tests {
|
||||
assert_eq!(theme.bg, RGB8::new(0xff, 0xff, 0xff));
|
||||
assert_eq!(theme.palette[0], RGB8::new(0x24, 0x1f, 0x31));
|
||||
|
||||
assert_eq!(events[0].time, 1);
|
||||
assert_eq!(events[0].time, Duration::from_micros(1));
|
||||
assert!(matches!(events[0].data, EventData::Output(ref s) if s == "ż"));
|
||||
|
||||
assert_eq!(events[1].time, 1_000_001);
|
||||
assert_eq!(events[1].time, Duration::from_micros(1_000_001));
|
||||
assert!(matches!(events[1].data, EventData::Output(ref s) if s == "ółć"));
|
||||
|
||||
assert_eq!(events[2].time, 1_300_001);
|
||||
assert_eq!(events[2].time, Duration::from_micros(1_300_001));
|
||||
assert!(matches!(events[2].data, EventData::Input(ref s) if s == "\n"));
|
||||
|
||||
assert_eq!(events[3].time, 2_900_002);
|
||||
assert_eq!(events[3].time, Duration::from_micros(2_900_002));
|
||||
|
||||
assert!(
|
||||
matches!(events[3].data, EventData::Resize(ref cols, ref rows) if *cols == 80 && *rows == 40)
|
||||
);
|
||||
|
||||
assert_eq!(events[4].time, 13_400_002);
|
||||
assert_eq!(events[4].time, Duration::from_micros(13_400_002));
|
||||
assert!(matches!(events[4].data, EventData::Output(ref s) if s == "\r\n"));
|
||||
}
|
||||
|
||||
@@ -390,15 +396,27 @@ mod tests {
|
||||
fn encoder() {
|
||||
let mut data = Vec::new();
|
||||
let header = Header::default();
|
||||
let mut enc = V2Encoder::new(0);
|
||||
let mut enc = V2Encoder::new(Duration::from_micros(0));
|
||||
data.extend(enc.header(&header));
|
||||
data.extend(enc.event(&Event::output(1000000, "hello\r\n".to_owned())));
|
||||
data.extend(enc.event(&Event::output(
|
||||
Duration::from_micros(1000000),
|
||||
"hello\r\n".to_owned(),
|
||||
)));
|
||||
|
||||
let mut enc = V2Encoder::new(1000001);
|
||||
data.extend(enc.event(&Event::output(1000001, "world".to_owned())));
|
||||
data.extend(enc.event(&Event::input(2000002, " ".to_owned())));
|
||||
data.extend(enc.event(&Event::resize(3000003, (100, 40))));
|
||||
data.extend(enc.event(&Event::output(4000004, "żółć".to_owned())));
|
||||
let mut enc = V2Encoder::new(Duration::from_micros(1000001));
|
||||
data.extend(enc.event(&Event::output(
|
||||
Duration::from_micros(1000001),
|
||||
"world".to_owned(),
|
||||
)));
|
||||
data.extend(enc.event(&Event::input(
|
||||
Duration::from_micros(2000002),
|
||||
" ".to_owned(),
|
||||
)));
|
||||
data.extend(enc.event(&Event::resize(Duration::from_micros(3000003), (100, 40))));
|
||||
data.extend(enc.event(&Event::output(
|
||||
Duration::from_micros(4000004),
|
||||
"żółć".to_owned(),
|
||||
)));
|
||||
|
||||
let lines = parse(data);
|
||||
|
||||
@@ -425,7 +443,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn header_encoding() {
|
||||
let mut enc = V2Encoder::new(0);
|
||||
let mut enc = V2Encoder::new(Duration::from_micros(0));
|
||||
let mut env = HashMap::new();
|
||||
env.insert("SHELL".to_owned(), "/usr/bin/fish".to_owned());
|
||||
env.insert("TERM".to_owned(), "xterm256-color".to_owned());
|
||||
@@ -493,14 +511,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn accelerate() {
|
||||
let events = [(0u64, "foo"), (20, "bar"), (50, "baz")]
|
||||
.map(|(time, output)| Ok(Event::output(time, output.to_owned())));
|
||||
let events = [(0u64, "foo"), (20, "bar"), (50, "baz")].map(|(time, output)| {
|
||||
Ok(Event::output(
|
||||
Duration::from_micros(time),
|
||||
output.to_owned(),
|
||||
))
|
||||
});
|
||||
|
||||
let output = output(super::accelerate(events.into_iter(), 2.0));
|
||||
|
||||
assert_eq!(output[0], (0, "foo".to_owned()));
|
||||
assert_eq!(output[1], (10, "bar".to_owned()));
|
||||
assert_eq!(output[2], (25, "baz".to_owned()));
|
||||
assert_eq!(output[0], (Duration::from_micros(0), "foo".to_owned()));
|
||||
assert_eq!(output[1], (Duration::from_micros(10), "bar".to_owned()));
|
||||
assert_eq!(output[2], (Duration::from_micros(25), "baz".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -512,18 +534,35 @@ mod tests {
|
||||
(4_000_000, "qux"),
|
||||
(7_500_000, "quux"),
|
||||
]
|
||||
.map(|(time, output)| Ok(Event::output(time, output.to_owned())));
|
||||
.map(|(time, output)| {
|
||||
Ok(Event::output(
|
||||
Duration::from_micros(time),
|
||||
output.to_owned(),
|
||||
))
|
||||
});
|
||||
|
||||
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()));
|
||||
assert_eq!(events[0], (Duration::from_micros(0), "foo".to_owned()));
|
||||
assert_eq!(
|
||||
events[1],
|
||||
(Duration::from_micros(1_000_000), "bar".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
events[2],
|
||||
(Duration::from_micros(3_000_000), "baz".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
events[3],
|
||||
(Duration::from_micros(3_500_000), "qux".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
events[4],
|
||||
(Duration::from_micros(5_500_000), "quux".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
fn output(events: impl Iterator<Item = Result<Event>>) -> Vec<(u64, String)> {
|
||||
fn output(events: impl Iterator<Item = Result<Event>>) -> Vec<(Duration, String)> {
|
||||
events
|
||||
.filter_map(|r| {
|
||||
if let Ok(Event {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
pub fn deserialize_time<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
pub fn deserialize_time<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
@@ -25,13 +27,13 @@ where
|
||||
.parse()
|
||||
.map_err(Error::custom)?;
|
||||
|
||||
Ok(secs * 1_000_000 + micros)
|
||||
Ok(Duration::from_micros(secs * 1_000_000 + micros))
|
||||
}
|
||||
|
||||
[number] => {
|
||||
let secs: u64 = number.parse().map_err(Error::custom)?;
|
||||
|
||||
Ok(secs * 1_000_000)
|
||||
Ok(Duration::from_micros(secs * 1_000_000))
|
||||
}
|
||||
|
||||
_ => Err(Error::custom(format!("invalid time format: {value}"))),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use serde::Deserialize;
|
||||
@@ -20,7 +21,7 @@ struct V1 {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct V1OutputEvent {
|
||||
#[serde(deserialize_with = "deserialize_time")]
|
||||
time: u64,
|
||||
time: Duration,
|
||||
data: String,
|
||||
}
|
||||
|
||||
@@ -50,12 +51,15 @@ pub fn load(json: String) -> Result<Asciicast<'static>> {
|
||||
env: asciicast.env.clone(),
|
||||
};
|
||||
|
||||
let events = Box::new(asciicast.stdout.into_iter().scan(0, |prev_time, event| {
|
||||
let events = Box::new(asciicast.stdout.into_iter().scan(
|
||||
Duration::from_micros(0),
|
||||
|prev_time, event| {
|
||||
let time = *prev_time + event.time;
|
||||
*prev_time = time;
|
||||
|
||||
Some(Ok(Event::output(time, event.data)))
|
||||
}));
|
||||
},
|
||||
));
|
||||
|
||||
Ok(Asciicast {
|
||||
version: Version::One,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
@@ -40,7 +41,7 @@ struct V2Palette(Vec<RGB8>);
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct V2Event {
|
||||
#[serde(deserialize_with = "util::deserialize_time")]
|
||||
time: u64,
|
||||
time: Duration,
|
||||
#[serde(deserialize_with = "deserialize_code")]
|
||||
code: V2EventCode,
|
||||
data: String,
|
||||
@@ -164,11 +165,11 @@ where
|
||||
}
|
||||
|
||||
pub struct V2Encoder {
|
||||
time_offset: u64,
|
||||
time_offset: Duration,
|
||||
}
|
||||
|
||||
impl V2Encoder {
|
||||
pub fn new(time_offset: u64) -> Self {
|
||||
pub fn new(time_offset: Duration) -> Self {
|
||||
Self { time_offset }
|
||||
}
|
||||
|
||||
@@ -212,7 +213,8 @@ impl V2Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(time: u64) -> String {
|
||||
fn format_time(time: Duration) -> String {
|
||||
let time = time.as_micros();
|
||||
let mut formatted_time = format!("{}.{:0>6}", time / 1_000_000, time % 1_000_000);
|
||||
let dot_idx = formatted_time.find('.').unwrap();
|
||||
|
||||
@@ -405,11 +407,19 @@ impl From<&V2Theme> for TtyTheme {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn format_time() {
|
||||
assert_eq!(super::format_time(0), "0.0");
|
||||
assert_eq!(super::format_time(1000001), "1.000001");
|
||||
assert_eq!(super::format_time(12300000), "12.3");
|
||||
assert_eq!(super::format_time(12000003), "12.000003");
|
||||
assert_eq!(super::format_time(Duration::from_micros(0)), "0.0");
|
||||
assert_eq!(
|
||||
super::format_time(Duration::from_micros(1000001)),
|
||||
"1.000001"
|
||||
);
|
||||
assert_eq!(super::format_time(Duration::from_micros(12300000)), "12.3");
|
||||
assert_eq!(
|
||||
super::format_time(Duration::from_micros(12000003)),
|
||||
"12.000003"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
@@ -48,7 +49,7 @@ struct V3Palette(Vec<RGB8>);
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct V3Event {
|
||||
#[serde(deserialize_with = "util::deserialize_time")]
|
||||
time: u64,
|
||||
time: Duration,
|
||||
#[serde(deserialize_with = "deserialize_code")]
|
||||
code: V3EventCode,
|
||||
data: String,
|
||||
@@ -66,7 +67,7 @@ enum V3EventCode {
|
||||
|
||||
pub struct Parser {
|
||||
header: V3Header,
|
||||
prev_time: u64,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
pub fn open(header_line: &str) -> Result<Parser> {
|
||||
@@ -78,7 +79,7 @@ pub fn open(header_line: &str) -> Result<Parser> {
|
||||
|
||||
Ok(Parser {
|
||||
header,
|
||||
prev_time: 0,
|
||||
prev_time: Duration::from_micros(0),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,12 +184,14 @@ where
|
||||
}
|
||||
|
||||
pub struct V3Encoder {
|
||||
prev_time: u64,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
impl V3Encoder {
|
||||
pub fn new() -> Self {
|
||||
Self { prev_time: 0 }
|
||||
Self {
|
||||
prev_time: Duration::from_micros(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&mut self, header: &Header) -> Vec<u8> {
|
||||
@@ -234,7 +237,8 @@ impl V3Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(time: u64) -> String {
|
||||
fn format_time(time: Duration) -> String {
|
||||
let time = time.as_micros();
|
||||
let mut formatted_time = format!("{}.{:0>6}", time / 1_000_000, time % 1_000_000);
|
||||
let dot_idx = formatted_time.find('.').unwrap();
|
||||
|
||||
@@ -462,11 +466,19 @@ impl From<&V3Theme> for TtyTheme {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn format_time() {
|
||||
assert_eq!(super::format_time(0), "0.0");
|
||||
assert_eq!(super::format_time(1000001), "1.000001");
|
||||
assert_eq!(super::format_time(12300000), "12.3");
|
||||
assert_eq!(super::format_time(12000003), "12.000003");
|
||||
assert_eq!(super::format_time(Duration::from_micros(0)), "0.0");
|
||||
assert_eq!(
|
||||
super::format_time(Duration::from_micros(1000001)),
|
||||
"1.000001"
|
||||
);
|
||||
assert_eq!(super::format_time(Duration::from_micros(12300000)), "12.3");
|
||||
assert_eq!(
|
||||
super::format_time(Duration::from_micros(12000003)),
|
||||
"12.000003"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
@@ -12,7 +13,7 @@ impl cli::Cat {
|
||||
let mut stdout = io::stdout();
|
||||
let casts = self.open_input_files()?;
|
||||
let mut encoder = self.get_encoder(casts[0].version)?;
|
||||
let mut time_offset: u64 = 0;
|
||||
let mut time_offset = Duration::from_micros(0);
|
||||
let mut first = true;
|
||||
let mut cols = 0;
|
||||
let mut rows = 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
@@ -32,7 +33,9 @@ impl cli::Convert {
|
||||
|
||||
match format {
|
||||
Format::AsciicastV3 => Box::new(AsciicastV3Encoder::new(false)),
|
||||
Format::AsciicastV2 => Box::new(AsciicastV2Encoder::new(false, 0)),
|
||||
Format::AsciicastV2 => {
|
||||
Box::new(AsciicastV2Encoder::new(false, Duration::from_micros(0)))
|
||||
}
|
||||
Format::Raw => Box::new(RawEncoder::new()),
|
||||
Format::Txt => Box::new(TextEncoder::new()),
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ impl cli::Session {
|
||||
let time_offset = if append {
|
||||
asciicast::get_duration(path)?
|
||||
} else {
|
||||
0
|
||||
Duration::from_micros(0)
|
||||
};
|
||||
|
||||
Ok(Box::new(AsciicastV2Encoder::new(append, time_offset)))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::asciicast::{Event, Header, V2Encoder, V3Encoder};
|
||||
|
||||
pub struct AsciicastV2Encoder {
|
||||
@@ -6,7 +8,7 @@ pub struct AsciicastV2Encoder {
|
||||
}
|
||||
|
||||
impl AsciicastV2Encoder {
|
||||
pub fn new(append: bool, time_offset: u64) -> Self {
|
||||
pub fn new(append: bool, time_offset: Duration) -> Self {
|
||||
let inner = V2Encoder::new(time_offset);
|
||||
|
||||
Self { inner, append }
|
||||
@@ -17,7 +19,8 @@ impl super::Encoder for AsciicastV2Encoder {
|
||||
fn header(&mut self, header: &Header) -> Vec<u8> {
|
||||
if self.append {
|
||||
let size = (header.term_cols, header.term_rows);
|
||||
self.inner.event(&Event::resize(0, size))
|
||||
self.inner
|
||||
.event(&Event::resize(Duration::from_micros(0), size))
|
||||
} else {
|
||||
self.inner.header(header)
|
||||
}
|
||||
@@ -49,7 +52,8 @@ impl super::Encoder for AsciicastV3Encoder {
|
||||
fn header(&mut self, header: &Header) -> Vec<u8> {
|
||||
if self.append {
|
||||
let size = (header.term_cols, header.term_rows);
|
||||
self.inner.event(&Event::resize(0, size))
|
||||
self.inner
|
||||
.event(&Event::resize(Duration::from_micros(0), size))
|
||||
} else {
|
||||
self.inner.header(header)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ impl super::Encoder for RawEncoder {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::RawEncoder;
|
||||
use crate::asciicast::{Event, Header};
|
||||
use crate::encoder::Encoder;
|
||||
@@ -45,18 +47,30 @@ mod tests {
|
||||
assert_eq!(enc.header(&header), "\x1b[8;50;100t".as_bytes());
|
||||
|
||||
assert_eq!(
|
||||
enc.event(Event::output(0, "he\x1b[1mllo\r\n".to_owned())),
|
||||
enc.event(Event::output(
|
||||
Duration::from_micros(0),
|
||||
"he\x1b[1mllo\r\n".to_owned()
|
||||
)),
|
||||
"he\x1b[1mllo\r\n".as_bytes()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
enc.event(Event::output(1, "world\r\n".to_owned())),
|
||||
enc.event(Event::output(
|
||||
Duration::from_micros(1),
|
||||
"world\r\n".to_owned()
|
||||
)),
|
||||
"world\r\n".as_bytes()
|
||||
);
|
||||
|
||||
assert!(enc.event(Event::input(2, ".".to_owned())).is_empty());
|
||||
assert!(enc.event(Event::resize(3, (80, 24))).is_empty());
|
||||
assert!(enc.event(Event::marker(4, ".".to_owned())).is_empty());
|
||||
assert!(enc
|
||||
.event(Event::input(Duration::from_micros(2), ".".to_owned()))
|
||||
.is_empty());
|
||||
assert!(enc
|
||||
.event(Event::resize(Duration::from_micros(3), (80, 24)))
|
||||
.is_empty());
|
||||
assert!(enc
|
||||
.event(Event::marker(Duration::from_micros(4), ".".to_owned()))
|
||||
.is_empty());
|
||||
assert!(enc.flush().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ fn text_lines_to_bytes<S: AsRef<str>>(lines: impl Iterator<Item = S>) -> Vec<u8>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::TextEncoder;
|
||||
use crate::asciicast::{Event, Header};
|
||||
use crate::encoder::Encoder;
|
||||
@@ -71,11 +73,17 @@ mod tests {
|
||||
assert!(enc.header(&header).is_empty());
|
||||
|
||||
assert!(enc
|
||||
.event(Event::output(0, "he\x1b[1mllo\r\n".to_owned()))
|
||||
.event(Event::output(
|
||||
Duration::from_micros(0),
|
||||
"he\x1b[1mllo\r\n".to_owned()
|
||||
))
|
||||
.is_empty());
|
||||
|
||||
assert!(enc
|
||||
.event(Event::output(1, "world\r\n".to_owned()))
|
||||
.event(Event::output(
|
||||
Duration::from_micros(1),
|
||||
"world\r\n".to_owned()
|
||||
))
|
||||
.is_empty());
|
||||
|
||||
assert_eq!(enc.flush(), "hello\nworld\n".as_bytes());
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn play(
|
||||
epoch = Instant::now() - Duration::from_micros(pet);
|
||||
pause_elapsed_time = None;
|
||||
} else if keys.step.as_ref().is_some_and(|k| k == key) {
|
||||
pause_elapsed_time = Some(*time);
|
||||
pause_elapsed_time = Some(time.as_micros() as u64);
|
||||
|
||||
match data {
|
||||
EventData::Output(data) => {
|
||||
@@ -85,7 +85,7 @@ pub async fn play(
|
||||
}
|
||||
|
||||
EventData::Marker(_) => {
|
||||
pause_elapsed_time = Some(time);
|
||||
pause_elapsed_time = Some(time.as_micros() as u64);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -99,10 +99,11 @@ pub async fn play(
|
||||
}
|
||||
} else {
|
||||
while let Some(Event { time, data }) = &next_event {
|
||||
let delay = *time as i64 - epoch.elapsed().as_micros() as i64;
|
||||
let delay = time.as_micros() as i64 - epoch.elapsed().as_micros() as i64;
|
||||
|
||||
if delay > 0 {
|
||||
let delay = (*time as i64 - epoch.elapsed().as_micros() as i64).max(0) as u64;
|
||||
let delay = (time.as_micros() as i64 - epoch.elapsed().as_micros() as i64)
|
||||
.max(0) as u64;
|
||||
|
||||
if let Ok(result) =
|
||||
time::timeout(Duration::from_micros(delay), tty.read(&mut input)).await
|
||||
@@ -135,7 +136,7 @@ pub async fn play(
|
||||
|
||||
EventData::Marker(_) => {
|
||||
if pause_on_markers {
|
||||
pause_elapsed_time = Some(*time);
|
||||
pause_elapsed_time = Some(time.as_micros() as u64);
|
||||
next_event = events.recv().await.transpose()?;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::{Buf, BytesMut};
|
||||
@@ -23,11 +23,11 @@ const BUF_SIZE: usize = 128 * 1024;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
Output(u64, String),
|
||||
Input(u64, String),
|
||||
Resize(u64, TtySize),
|
||||
Marker(u64, String),
|
||||
Exit(u64, i32),
|
||||
Output(Duration, String),
|
||||
Input(Duration, String),
|
||||
Resize(Duration, TtySize),
|
||||
Marker(Duration, String),
|
||||
Exit(Duration, i32),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -55,10 +55,10 @@ struct Session<N: Notifier> {
|
||||
keys: KeyBindings,
|
||||
notifier: N,
|
||||
output_decoder: Utf8Decoder,
|
||||
pause_time: Option<u64>,
|
||||
pause_time: Option<Duration>,
|
||||
prefix_mode: bool,
|
||||
record_input: bool,
|
||||
time_offset: u64,
|
||||
time_offset: Duration,
|
||||
tty_size: TtySize,
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ pub async fn run<S: AsRef<str>, T: Tty + ?Sized, N: Notifier>(
|
||||
pause_time: None,
|
||||
prefix_mode: false,
|
||||
record_input,
|
||||
time_offset: 0,
|
||||
time_offset: Duration::from_micros(0),
|
||||
tty_size: winsize.into(),
|
||||
};
|
||||
|
||||
@@ -302,11 +302,11 @@ impl<N: Notifier> Session<N> {
|
||||
self.send_session_event(event).await;
|
||||
}
|
||||
|
||||
fn elapsed_time(&self) -> u64 {
|
||||
fn elapsed_time(&self) -> Duration {
|
||||
if let Some(pause_time) = self.pause_time {
|
||||
pause_time
|
||||
} else {
|
||||
self.epoch.elapsed().as_micros() as u64 - self.time_offset
|
||||
self.epoch.elapsed() - self.time_offset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ struct Subscription {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
Init(u64, u64, TtySize, Option<TtyTheme>, String),
|
||||
Output(u64, u64, String),
|
||||
Input(u64, u64, String),
|
||||
Resize(u64, u64, TtySize),
|
||||
Marker(u64, u64, String),
|
||||
Exit(u64, u64, i32),
|
||||
Init(u64, Duration, TtySize, Option<TtyTheme>, String),
|
||||
Output(u64, Duration, String),
|
||||
Input(u64, Duration, String),
|
||||
Resize(u64, Duration, TtySize),
|
||||
Marker(u64, Duration, String),
|
||||
Exit(u64, Duration, i32),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -77,7 +77,7 @@ async fn run(
|
||||
) {
|
||||
let (broadcast_tx, _) = broadcast::channel(1024);
|
||||
let mut vt = build_vt(tty_size);
|
||||
let mut stream_time = 0;
|
||||
let mut stream_time = Duration::from_micros(0);
|
||||
let mut last_event_id = 0;
|
||||
let mut last_event_time = Instant::now();
|
||||
|
||||
@@ -127,7 +127,7 @@ async fn run(
|
||||
match request {
|
||||
Some(request) => {
|
||||
let init = if last_event_id > 0 {
|
||||
let elapsed_time = stream_time + last_event_time.elapsed().as_micros() as u64;
|
||||
let elapsed_time = stream_time + last_event_time.elapsed();
|
||||
|
||||
Event::Init(
|
||||
last_event_id,
|
||||
|
||||
Reference in New Issue
Block a user