From 0e29511db3a0ccc1108fda2efeae165b26ecd3b5 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Fri, 17 Oct 2025 11:38:57 +0200 Subject: [PATCH] Use Duration instead of u64 for event timestamps --- src/alis.rs | 82 ++++++++++++---------- src/asciicast.rs | 143 +++++++++++++++++++++++++-------------- src/asciicast/util.rs | 8 ++- src/asciicast/v1.rs | 16 +++-- src/asciicast/v2.rs | 26 ++++--- src/asciicast/v3.rs | 32 ++++++--- src/cmd/cat.rs | 3 +- src/cmd/convert.rs | 5 +- src/cmd/session.rs | 2 +- src/encoder/asciicast.rs | 10 ++- src/encoder/raw.rs | 24 +++++-- src/encoder/txt.rs | 12 +++- src/player.rs | 11 +-- src/session.rs | 22 +++--- src/stream.rs | 16 ++--- 15 files changed, 260 insertions(+), 152 deletions(-) diff --git a/src/alis.rs b/src/alis.rs index 8631765..c9e4bc5 100644 --- a/src/alis.rs +++ b/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>>( stream: S, ) -> impl Stream, 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 { diff --git a/src/asciicast.rs b/src/asciicast.rs index b535387..6ffc347 100644 --- a/src/asciicast.rs +++ b/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> { } } -pub fn get_duration>(path: S) -> Result { +pub fn get_duration>(path: S) -> Result { 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>, limit: f64, ) -> impl Iterator> { - 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> { 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> { 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>) -> Vec<(u64, String)> { + fn output(events: impl Iterator>) -> Vec<(Duration, String)> { events .filter_map(|r| { if let Ok(Event { diff --git a/src/asciicast/util.rs b/src/asciicast/util.rs index f1706ce..f85f951 100644 --- a/src/asciicast/util.rs +++ b/src/asciicast/util.rs @@ -1,7 +1,9 @@ +use std::time::Duration; + use anyhow::Result; use serde::{Deserialize, Deserializer}; -pub fn deserialize_time<'de, D>(deserializer: D) -> Result +pub fn deserialize_time<'de, D>(deserializer: D) -> Result 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}"))), diff --git a/src/asciicast/v1.rs b/src/asciicast/v1.rs index e4ae75a..e2b6124 100644 --- a/src/asciicast/v1.rs +++ b/src/asciicast/v1.rs @@ -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> { env: asciicast.env.clone(), }; - let events = Box::new(asciicast.stdout.into_iter().scan(0, |prev_time, event| { - let time = *prev_time + event.time; - *prev_time = time; + 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))) - })); + Some(Ok(Event::output(time, event.data))) + }, + )); Ok(Asciicast { version: Version::One, diff --git a/src/asciicast/v2.rs b/src/asciicast/v2.rs index f0a1c68..848f304 100644 --- a/src/asciicast/v2.rs +++ b/src/asciicast/v2.rs @@ -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); #[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" + ); } } diff --git a/src/asciicast/v3.rs b/src/asciicast/v3.rs index 56e1ad1..8a8adfb 100644 --- a/src/asciicast/v3.rs +++ b/src/asciicast/v3.rs @@ -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); #[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 { @@ -78,7 +79,7 @@ pub fn open(header_line: &str) -> Result { 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 { @@ -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" + ); } } diff --git a/src/cmd/cat.rs b/src/cmd/cat.rs index 19e627e..b140ad5 100644 --- a/src/cmd/cat.rs +++ b/src/cmd/cat.rs @@ -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; diff --git a/src/cmd/convert.rs b/src/cmd/convert.rs index 10692f6..ea6544a 100644 --- a/src/cmd/convert.rs +++ b/src/cmd/convert.rs @@ -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()), } diff --git a/src/cmd/session.rs b/src/cmd/session.rs index 3b8ab6f..93dc248 100644 --- a/src/cmd/session.rs +++ b/src/cmd/session.rs @@ -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))) diff --git a/src/encoder/asciicast.rs b/src/encoder/asciicast.rs index 2692f3b..affa3f8 100644 --- a/src/encoder/asciicast.rs +++ b/src/encoder/asciicast.rs @@ -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 { 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 { 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) } diff --git a/src/encoder/raw.rs b/src/encoder/raw.rs index 3102d4a..f0864e9 100644 --- a/src/encoder/raw.rs +++ b/src/encoder/raw.rs @@ -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()); } } diff --git a/src/encoder/txt.rs b/src/encoder/txt.rs index 5c7b568..a2fedbe 100644 --- a/src/encoder/txt.rs +++ b/src/encoder/txt.rs @@ -54,6 +54,8 @@ fn text_lines_to_bytes>(lines: impl Iterator) -> Vec #[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()); diff --git a/src/player.rs b/src/player.rs index 3efe61a..8159166 100644 --- a/src/player.rs +++ b/src/player.rs @@ -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; } diff --git a/src/session.rs b/src/session.rs index 58990a8..287e088 100644 --- a/src/session.rs +++ b/src/session.rs @@ -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 { keys: KeyBindings, notifier: N, output_decoder: Utf8Decoder, - pause_time: Option, + pause_time: Option, prefix_mode: bool, record_input: bool, - time_offset: u64, + time_offset: Duration, tty_size: TtySize, } @@ -93,7 +93,7 @@ pub async fn run, 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 Session { 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 } } diff --git a/src/stream.rs b/src/stream.rs index e09f2c2..eb419e7 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -27,12 +27,12 @@ struct Subscription { #[derive(Clone)] pub enum Event { - Init(u64, u64, TtySize, Option, 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, 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,