Merge pull request #585 from asciinema/rust-play

New impl of `play` command
This commit is contained in:
Marcin Kulik
2024-01-02 20:59:03 +01:00
committed by GitHub
7 changed files with 161 additions and 18 deletions

View File

@@ -1,5 +1,7 @@
use crate::player;
use anyhow::Result;
use clap::Args;
use std::fs;
#[derive(Debug, Args)]
pub struct Cli {
@@ -24,6 +26,17 @@ pub struct Cli {
impl Cli {
pub fn run(self) -> Result<()> {
todo!();
let speed = self.speed.unwrap_or(1.0);
loop {
let file = fs::File::open(&self.filename)?;
player::play(file, speed, self.idle_time_limit)?;
if !self.loop_ {
break;
}
}
Ok(())
}
}

View File

@@ -44,7 +44,7 @@ pub struct Cli {
/// Limit idle time to given number of seconds
#[arg(short, long, value_name = "SECS")]
idle_time_limit: Option<f32>,
idle_time_limit: Option<f64>,
/// Override terminal width (columns) for recorded command
#[arg(long)]

View File

@@ -13,7 +13,7 @@ pub struct Header {
pub cols: u16,
pub rows: u16,
pub timestamp: u64,
pub idle_time_limit: Option<f32>,
pub idle_time_limit: Option<f64>,
pub command: Option<String>,
pub title: Option<String>,
pub env: HashMap<String, String>,

View File

@@ -1,4 +1,4 @@
use serde::de::Error;
use anyhow::Result;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::{self, Display};
@@ -17,13 +17,13 @@ pub struct Header {
width: u16,
height: u16,
timestamp: u64,
idle_time_limit: Option<f32>,
pub idle_time_limit: Option<f64>,
command: Option<String>,
title: Option<String>,
env: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Event {
#[serde(deserialize_with = "deserialize_time")]
pub time: u64,
@@ -32,7 +32,7 @@ pub struct Event {
pub data: String,
}
#[derive(PartialEq, Eq, Debug)]
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum EventCode {
Output,
Input,
@@ -84,7 +84,7 @@ where
}
}
pub fn get_duration<S: AsRef<Path>>(path: S) -> anyhow::Result<u64> {
pub fn get_duration<S: AsRef<Path>>(path: S) -> Result<u64> {
let file = fs::File::open(path)?;
let reader = io::BufReader::new(file);
let (_header, events) = open(reader)?;
@@ -93,9 +93,7 @@ pub fn get_duration<S: AsRef<Path>>(path: S) -> anyhow::Result<u64> {
Ok(time)
}
pub fn open<R: BufRead>(
reader: R,
) -> anyhow::Result<(Header, impl Iterator<Item = anyhow::Result<Event>>)> {
pub fn open<R: BufRead>(reader: R) -> Result<(Header, impl Iterator<Item = Result<Event>>)> {
let mut lines = reader.lines();
let first_line = lines.next().ok_or(anyhow::anyhow!("empty file"))??;
let header: Header = serde_json::from_str(&first_line)?;
@@ -104,7 +102,7 @@ pub fn open<R: BufRead>(
Ok((header, events))
}
fn parse_event(line: io::Result<String>) -> Option<anyhow::Result<Event>> {
fn parse_event(line: io::Result<String>) -> Option<Result<Event>> {
match line {
Ok(line) => {
if line.is_empty() {
@@ -122,6 +120,8 @@ fn deserialize_time<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let value: serde_json::Value = Deserialize::deserialize(deserializer)?;
let string = value.to_string();
let parts: Vec<&str> = string.split('.').collect();
@@ -146,6 +146,7 @@ fn deserialize_code<'de, D>(deserializer: D) -> Result<EventCode, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
use EventCode::*;
let value: &str = Deserialize::deserialize(deserializer)?;
@@ -292,9 +293,62 @@ impl From<&super::Header> for Header {
}
}
pub fn limit_idle_time(
events: impl Iterator<Item = Result<Event>>,
limit: f64,
) -> impl Iterator<Item = Result<Event>> {
let limit = (limit * 1_000_000.0) as u64;
let mut prev_time = 0;
let mut offset = 0;
events.map(move |event| {
event.map(|event| {
let delay = event.time - prev_time;
if delay > limit {
offset += delay - limit;
}
prev_time = event.time;
let time = event.time - offset;
Event { time, ..event }
})
})
}
pub fn accelerate(
events: impl Iterator<Item = Result<Event>>,
speed: f64,
) -> impl Iterator<Item = Result<Event>> {
events.map(move |event| {
event.map(|event| {
let time = ((event.time as f64) / speed) as u64;
Event { time, ..event }
})
})
}
pub fn output(
events: impl Iterator<Item = Result<Event>>,
) -> impl Iterator<Item = Result<(u64, String)>> {
events.filter_map(|e| match e {
Ok(Event {
code: EventCode::Output,
time,
data,
}) => Some(Ok((time, data))),
Ok(_) => None,
Err(e) => Some(Err(e)),
})
}
#[cfg(test)]
mod tests {
use super::{Event, EventCode, Header, Writer};
use anyhow::Result;
use std::collections::HashMap;
use std::fs::File;
use std::io;
@@ -304,10 +358,7 @@ mod tests {
let file = File::open("tests/demo.cast").unwrap();
let (header, events) = super::open(io::BufReader::new(file)).unwrap();
let events = events
.take(7)
.collect::<anyhow::Result<Vec<Event>>>()
.unwrap();
let events = events.take(7).collect::<Result<Vec<Event>>>().unwrap();
assert_eq!((header.width, header.height), (75, 18));
@@ -427,4 +478,48 @@ mod tests {
.collect::<serde_json::Result<Vec<_>>>()
.unwrap()
}
#[test]
fn accelerate() {
let stdout = [(0u64, "foo"), (20, "bar"), (50, "baz")]
.map(|(time, output)| Ok(Event::output(time, output.as_bytes())));
let stdout = super::accelerate(stdout.into_iter(), 2.0)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(stdout[0].time, 0);
assert_eq!(stdout[0].data, "foo");
assert_eq!(stdout[1].time, 10);
assert_eq!(stdout[1].data, "bar");
assert_eq!(stdout[2].time, 25);
assert_eq!(stdout[2].data, "baz");
}
#[test]
fn limit_idle_time() {
let stdout = [
(0, "foo"),
(1_000_000, "bar"),
(3_500_000, "baz"),
(4_000_000, "qux"),
(7_500_000, "quux"),
]
.map(|(time, output)| Ok(Event::output(time, output.as_bytes())));
let stdout = super::limit_idle_time(stdout.into_iter(), 2.0)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(stdout[0].time, 0);
assert_eq!(stdout[0].data, "foo");
assert_eq!(stdout[1].time, 1_000_000);
assert_eq!(stdout[1].data, "bar");
assert_eq!(stdout[2].time, 3_000_000);
assert_eq!(stdout[2].data, "baz");
assert_eq!(stdout[3].time, 3_500_000);
assert_eq!(stdout[3].data, "qux");
assert_eq!(stdout[4].time, 5_500_000);
assert_eq!(stdout[4].data, "quux");
}
}

View File

@@ -2,6 +2,7 @@ mod cmd;
mod config;
mod format;
mod locale;
mod player;
mod pty;
mod recorder;
use crate::config::Config;

34
src/player.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::format::asciicast;
use anyhow::Result;
use std::io::{self, Write};
use std::thread;
use std::time::{Duration, Instant};
pub fn play(input: impl io::Read, speed: f64, idle_time_limit: Option<f64>) -> Result<()> {
let reader = io::BufReader::new(input);
let (header, events) = asciicast::open(reader)?;
let mut stdout = io::stdout();
let idle_time_limit = idle_time_limit
.or(header.idle_time_limit)
.unwrap_or(f64::MAX);
let events = asciicast::limit_idle_time(events, idle_time_limit);
let events = asciicast::accelerate(events, speed);
let output = asciicast::output(events);
let epoch = Instant::now();
for o in output {
let (time, data) = o?;
let diff = time as i64 - epoch.elapsed().as_micros() as i64;
if diff > 0 {
stdout.flush().unwrap();
thread::sleep(Duration::from_micros(diff as u64));
}
stdout.write_all(data.as_bytes())?;
}
Ok(())
}

View File

@@ -11,7 +11,7 @@ pub struct Recorder {
start_time: Instant,
append: bool,
record_input: bool,
idle_time_limit: Option<f32>,
idle_time_limit: Option<f64>,
command: Option<String>,
title: Option<String>,
env: HashMap<String, String>,
@@ -33,7 +33,7 @@ impl Recorder {
writer: Box<dyn format::Writer + Send>,
append: bool,
record_input: bool,
idle_time_limit: Option<f32>,
idle_time_limit: Option<f64>,
command: Option<String>,
title: Option<String>,
env: HashMap<String, String>,