Use Duration instead of u64 for event timestamps

This commit is contained in:
Marcin Kulik
2025-10-17 11:38:57 +02:00
parent 686f548ab2
commit 0e29511db3
15 changed files with 260 additions and 152 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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}"))),

View File

@@ -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 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,

View File

@@ -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"
);
}
}

View File

@@ -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"
);
}
}

View File

@@ -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;

View File

@@ -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()),
}

View File

@@ -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)))

View File

@@ -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)
}

View File

@@ -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());
}
}

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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,