From cd3a22971cecd24960ea73d42d20c9ce00cdf2d6 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Wed, 6 Nov 2019 14:32:17 -0500 Subject: implement seeking in playing ttyrecs --- Cargo.lock | 109 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/cmd/play.rs | 142 +++++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 223 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb4de01..ab699d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "term_size", "textwrap", "unicode-width", @@ -401,6 +401,41 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.6", + "quote 1.0.2", + "strsim 0.9.2", + "syn 1.0.5", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote 1.0.2", + "syn 1.0.5", +] + [[package]] name = "digest" version = "0.7.6" @@ -459,6 +494,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumset" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b811aef4ff1cc938f13bbec348f0ecbfc2bb565b7ab90161c9f0b2805edc8a" +dependencies = [ + "enumset_derive", + "num-traits", +] + +[[package]] +name = "enumset_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b184c2d0714bbeeb6440481a19c78530aa210654d99529f13d2f860a1b447598" +dependencies = [ + "darling", + "proc-macro2 1.0.6", + "quote 1.0.2", + "syn 1.0.5", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -725,6 +782,12 @@ dependencies = [ "tokio-io", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.1.5" @@ -949,6 +1012,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db1b4163932b207be6e3a06412aed4d84cca40dc087419f231b3a38cba2ca8e9" +[[package]] +name = "num-traits" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.10.1" @@ -1587,6 +1659,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "032c03039aae92b350aad2e3779c352e104d919cb192ba2fabbd7b831ce4f0f6" + [[package]] name = "syn" version = "0.15.44" @@ -1655,6 +1733,7 @@ dependencies = [ "url 2.1.0", "users", "uuid 0.8.1", + "vt100", ] [[package]] @@ -2118,6 +2197,12 @@ dependencies = [ "libc", ] +[[package]] +name = "utf8parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" + [[package]] name = "uuid" version = "0.7.4" @@ -2154,6 +2239,28 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +[[package]] +name = "vt100" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95036fec2baf2d86abb01a81f6eb1ad3050d6a53026af54f5c386a1147f3125c" +dependencies = [ + "enumset", + "log", + "unicode-normalization", + "unicode-width", + "vte", +] + +[[package]] +name = "vte" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f42f536e22f7fcbb407639765c8fd78707a33109301f834a594758bedd6e8cf" +dependencies = [ + "utf8parse", +] + [[package]] name = "want" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index bb1b081..75d4ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ twoway = "0.2" url = "2" users = "0.9" uuid = { version = "0.8", features = ["v4"] } +vt100 = "0.3" [[bin]] name = "tt" diff --git a/src/cmd/play.rs b/src/cmd/play.rs index acc0673..a3986ea 100644 --- a/src/cmd/play.rs +++ b/src/cmd/play.rs @@ -55,7 +55,8 @@ pub fn config( struct Frame { dur: std::time::Duration, - data: Vec, + full: Vec, + diff: Vec, } impl Frame { @@ -96,6 +97,7 @@ struct Player { timer: Option, base_time: std::time::Instant, played_amount: std::time::Duration, + paused: Option, } impl Player { @@ -111,9 +113,14 @@ impl Player { timer: None, base_time: std::time::Instant::now(), played_amount: std::time::Duration::default(), + paused: None, } } + fn current_frame(&self) -> Option<&Frame> { + self.ttyrec.frame(self.idx) + } + fn base_time_incr(&mut self, incr: std::time::Duration) { self.base_time += incr; self.set_timer(); @@ -141,6 +148,46 @@ impl Player { self.set_timer(); } + fn back(&mut self) { + self.idx = self.idx.saturating_sub(1); + self.recalculate_times(); + self.set_timer(); + } + + fn forward(&mut self) { + self.idx = self.idx.saturating_add(1); + self.recalculate_times(); + self.set_timer(); + } + + fn toggle_pause(&mut self) { + let now = std::time::Instant::now(); + if let Some(time) = self.paused.take() { + self.base_time_incr(now - time); + } else { + self.paused = Some(now); + } + } + + fn paused(&self) -> bool { + self.paused.is_some() + } + + fn recalculate_times(&mut self) { + let now = std::time::Instant::now(); + self.played_amount = self + .ttyrec + .frames + .iter() + .map(|f| f.dur) + .take(self.idx) + .sum(); + self.base_time = now - self.played_amount; + if let Some(paused) = &mut self.paused { + *paused = now; + } + } + fn set_timer(&mut self) { if let Some(frame) = self.ttyrec.frame(self.idx) { self.timer = Some(tokio::timer::Delay::new( @@ -169,7 +216,7 @@ impl Player { }; futures::try_ready!(timer.poll().context(crate::error::Sleep)); - let ret = frame.data.clone(); + let ret = frame.diff.clone(); self.idx += 1; self.played_amount += @@ -191,6 +238,7 @@ enum FileState { }, Open { reader: ttyrec::Reader, + parser: vt100::Parser, }, Eof, } @@ -199,9 +247,10 @@ struct PlaySession { file: FileState, player: Player, raw_screen: Option, + alternate_screen: Option, key_reader: crate::key_reader::KeyReader, last_frame_time: std::time::Duration, - paused: Option, + last_frame_screen: Option, } impl PlaySession { @@ -216,9 +265,10 @@ impl PlaySession { }, player: Player::new(playback_ratio, max_frame_length), raw_screen: None, + alternate_screen: None, key_reader: crate::key_reader::KeyReader::new(), last_frame_time: std::time::Duration::default(), - paused: None, + last_frame_screen: None, } } @@ -230,12 +280,7 @@ impl PlaySession { crossterm::InputEvent::Keyboard(crossterm::KeyEvent::Char( ' ', )) => { - if let Some(time) = self.paused.take() { - self.player - .base_time_incr(std::time::Instant::now() - time); - } else { - self.paused = Some(std::time::Instant::now()); - } + self.player.toggle_pause(); } crossterm::InputEvent::Keyboard(crossterm::KeyEvent::Char( '+', @@ -252,10 +297,41 @@ impl PlaySession { )) => { self.player.playback_ratio_reset(); } + crossterm::InputEvent::Keyboard(crossterm::KeyEvent::Char( + '<', + )) => { + self.player.back(); + self.redraw()?; + } + crossterm::InputEvent::Keyboard(crossterm::KeyEvent::Char( + '>', + )) => { + self.player.forward(); + self.redraw()?; + } _ => {} } Ok(false) } + + fn redraw(&self) -> Result<()> { + let frame = if let Some(frame) = self.player.current_frame() { + frame + } else { + return Ok(()); + }; + self.write(&frame.full)?; + Ok(()) + } + + fn write(&self, data: &[u8]) -> Result<()> { + // TODO async + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + stdout.write(data).context(crate::error::WriteTerminal)?; + stdout.flush().context(crate::error::FlushTerminal)?; + Ok(()) + } } impl PlaySession { @@ -290,8 +366,10 @@ impl PlaySession { filename: filename.to_string(), } })); + let size = crate::term::Size::get()?; let reader = ttyrec::Reader::new(file); - self.file = FileState::Open { reader }; + let parser = vt100::Parser::new(size.rows, size.cols); + self.file = FileState::Open { reader, parser }; Ok(component_future::Async::DidWork) } _ => Ok(component_future::Async::NothingToDo), @@ -299,17 +377,31 @@ impl PlaySession { } fn poll_read_file(&mut self) -> component_future::Poll<(), Error> { - if let FileState::Open { reader } = &mut self.file { + if let FileState::Open { reader, parser } = &mut self.file { if let Some(frame) = component_future::try_ready!(reader .poll_read() .context(crate::error::ReadTtyrec)) { + parser.process(&frame.data); + let frame_time = frame.time - reader.offset().unwrap(); let frame_dur = frame_time - self.last_frame_time; self.last_frame_time = frame_time; + + let full = parser.screen().contents_formatted(); + let diff = if let Some(last_frame_screen) = + &self.last_frame_screen + { + parser.screen().contents_diff(last_frame_screen) + } else { + full.clone() + }; + + self.last_frame_screen = Some(parser.screen().clone()); self.player.add_frame(Frame { dur: frame_dur, - data: frame.data, + full, + diff, }); } else { self.file = FileState::Eof; @@ -327,19 +419,17 @@ impl PlaySession { .context(crate::error::ToRawMode)?, ); } + if self.alternate_screen.is_none() { + self.alternate_screen = Some( + crossterm::AlternateScreen::to_alternate(false) + .context(crate::error::ToAlternateScreen)?, + ); + } let e = component_future::try_ready!(self.key_reader.poll()).unwrap(); let quit = self.keypress(&e)?; if quit { - self.raw_screen = None; - - // TODO async - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - stdout - .write(b"\x1bc") - .context(crate::error::WriteTerminal)?; - stdout.flush().context(crate::error::FlushTerminal)?; + self.write(b"\x1b[?25h")?; Ok(component_future::Async::Ready(())) } else { Ok(component_future::Async::DidWork) @@ -347,16 +437,12 @@ impl PlaySession { } fn poll_write_terminal(&mut self) -> component_future::Poll<(), Error> { - if self.paused.is_some() { + if self.player.paused() { return Ok(component_future::Async::NothingToDo); } if let Some(data) = component_future::try_ready!(self.player.poll()) { - // TODO async - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - stdout.write(&data).context(crate::error::WriteTerminal)?; - stdout.flush().context(crate::error::FlushTerminal)?; + self.write(&data)?; Ok(component_future::Async::DidWork) } else if let FileState::Eof = self.file { Ok(component_future::Async::Ready(())) -- cgit v1.2.3-54-g00ecf