From 4ad1c22a297623a18915bead3c5b48107ad2d6bf Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Thu, 17 Oct 2024 17:31:59 +0200 Subject: [PATCH] Make Encoder return bytes instead of directly writing to io::Write impl --- Cargo.lock | 5 +- Cargo.toml | 2 +- src/asciicast.rs | 142 +++++++++++++++++---------------------- src/asciicast/v2.rs | 30 ++++----- src/cmd/cat.rs | 8 ++- src/cmd/convert.rs | 25 +++---- src/cmd/rec.rs | 43 +++++++----- src/encoder/asciicast.rs | 39 +++++------ src/encoder/mod.rs | 59 ++++------------ src/encoder/raw.rs | 61 ++++++++++------- src/encoder/txt.rs | 78 ++++++++++----------- src/pty.rs | 8 +-- src/recorder.rs | 29 ++++---- src/streamer/mod.rs | 3 +- 14 files changed, 239 insertions(+), 293 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48918d6..08c3c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,12 +179,11 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "avt" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb3c38905502a1f46c37ae2d3373c36a3aaf3cd9f7fde871984ef870f039bd6" +checksum = "b485f400d02970694eed10e7080f994ad82eaf56a867d6671af5d5e184ed8ee6" dependencies = [ "rgb", - "serde", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 0f57172..f28b5a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ config = { version = "0.14.0", default-features = false, features = ["toml", "in which = "6.0.0" tempfile = "3.9.0" scraper = { version = "0.19.0", default-features = false } -avt = "0.11.0" +avt = "0.14.0" axum = { version = "0.7.4", default-features = false, features = ["http1", "ws"] } tokio = { version = "1.35.1", features = ["full"] } futures-util = "0.3.30" diff --git a/src/asciicast.rs b/src/asciicast.rs index 5ccc6c2..d12a686 100644 --- a/src/asciicast.rs +++ b/src/asciicast.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::fs; use std::io::{self, BufRead}; use std::path::Path; -pub use v2::Writer; +pub use v2::Encoder; pub struct Asciicast<'a> { pub header: Header, @@ -138,12 +138,11 @@ pub fn accelerate( #[cfg(test)] mod tests { - use super::{Asciicast, Event, EventData, Header, Writer}; + use super::{Asciicast, Encoder, Event, EventData, Header}; use crate::tty; use anyhow::Result; use rgb::RGB8; use std::collections::HashMap; - use std::io; #[test] fn open_v1_minimal() { @@ -221,44 +220,30 @@ mod tests { } #[test] - fn writer() { + fn encoder() { let mut data = Vec::new(); - { - let mut fw = Writer::new(&mut data, 0); + let header = Header { + version: 2, + cols: 80, + rows: 24, + timestamp: None, + idle_time_limit: None, + command: None, + title: None, + env: Default::default(), + theme: None, + }; - let header = Header { - version: 2, - cols: 80, - rows: 24, - timestamp: None, - idle_time_limit: None, - command: None, - title: None, - env: Default::default(), - theme: None, - }; + let mut enc = Encoder::new(0); + data.extend(enc.header(&header)); + data.extend(enc.event(&Event::output(1000001, "hello\r\n".to_owned()))); - fw.write_header(&header).unwrap(); - - fw.write_event(&Event::output(1000001, "hello\r\n".to_owned())) - .unwrap(); - } - - { - let mut fw = Writer::new(&mut data, 1000001); - - fw.write_event(&Event::output(1000001, "world".to_owned())) - .unwrap(); - - fw.write_event(&Event::input(2000002, " ".to_owned())) - .unwrap(); - - fw.write_event(&Event::resize(3000003, (100, 40))).unwrap(); - - fw.write_event(&Event::output(4000004, "żółć".to_owned())) - .unwrap(); - } + let mut enc = Encoder::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 lines = parse(data); @@ -284,53 +269,48 @@ mod tests { } #[test] - fn write_header() { - let mut data = Vec::new(); + fn header_encoding() { + let mut enc = Encoder::new(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()); - { - let mut fw = Writer::new(io::Cursor::new(&mut data), 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()); + let theme = tty::Theme { + fg: RGB8::new(0, 1, 2), + bg: RGB8::new(0, 100, 200), + palette: vec![ + RGB8::new(0, 0, 0), + RGB8::new(10, 11, 12), + RGB8::new(20, 21, 22), + RGB8::new(30, 31, 32), + RGB8::new(40, 41, 42), + RGB8::new(50, 51, 52), + RGB8::new(60, 61, 62), + RGB8::new(70, 71, 72), + RGB8::new(80, 81, 82), + RGB8::new(90, 91, 92), + RGB8::new(100, 101, 102), + RGB8::new(110, 111, 112), + RGB8::new(120, 121, 122), + RGB8::new(130, 131, 132), + RGB8::new(140, 141, 142), + RGB8::new(150, 151, 152), + ], + }; - let theme = tty::Theme { - fg: RGB8::new(0, 1, 2), - bg: RGB8::new(0, 100, 200), - palette: vec![ - RGB8::new(0, 0, 0), - RGB8::new(10, 11, 12), - RGB8::new(20, 21, 22), - RGB8::new(30, 31, 32), - RGB8::new(40, 41, 42), - RGB8::new(50, 51, 52), - RGB8::new(60, 61, 62), - RGB8::new(70, 71, 72), - RGB8::new(80, 81, 82), - RGB8::new(90, 91, 92), - RGB8::new(100, 101, 102), - RGB8::new(110, 111, 112), - RGB8::new(120, 121, 122), - RGB8::new(130, 131, 132), - RGB8::new(140, 141, 142), - RGB8::new(150, 151, 152), - ], - }; - - let header = Header { - version: 2, - cols: 80, - rows: 24, - timestamp: Some(1704719152), - idle_time_limit: Some(1.5), - command: Some("/bin/bash".to_owned()), - title: Some("Demo".to_owned()), - env: Some(env), - theme: Some(theme), - }; - - fw.write_header(&header).unwrap(); - } + let header = Header { + version: 2, + cols: 80, + rows: 24, + timestamp: Some(1704719152), + idle_time_limit: Some(1.5), + command: Some("/bin/bash".to_owned()), + title: Some("Demo".to_owned()), + env: Some(env), + theme: Some(theme), + }; + let data = enc.header(&header); let lines = parse(data); assert_eq!(lines[0]["version"], 2); diff --git a/src/asciicast/v2.rs b/src/asciicast/v2.rs index 5a4628c..4475f1d 100644 --- a/src/asciicast/v2.rs +++ b/src/asciicast/v2.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; use std::fmt; -use std::io::{self, Write}; +use std::io; #[derive(Deserialize)] struct V2Header { @@ -156,30 +156,28 @@ where } } -pub struct Writer { - writer: io::LineWriter, +pub struct Encoder { time_offset: u64, } -impl Writer -where - W: Write, -{ - pub fn new(writer: W, time_offset: u64) -> Self { - Self { - writer: io::LineWriter::new(writer), - time_offset, - } +impl Encoder { + pub fn new(time_offset: u64) -> Self { + Self { time_offset } } - pub fn write_header(&mut self, header: &Header) -> io::Result<()> { + pub fn header(&mut self, header: &Header) -> Vec { let header: V2Header = header.into(); + let mut data = serde_json::to_string(&header).unwrap().into_bytes(); + data.push(b'\n'); - writeln!(self.writer, "{}", serde_json::to_string(&header)?) + data } - pub fn write_event(&mut self, event: &Event) -> io::Result<()> { - writeln!(self.writer, "{}", self.serialize_event(event)?) + pub fn event(&mut self, event: &Event) -> Vec { + let mut data = self.serialize_event(event).unwrap().into_bytes(); + data.push(b'\n'); + + data } fn serialize_event(&self, event: &Event) -> Result { diff --git a/src/cmd/cat.rs b/src/cmd/cat.rs index d13c9ce..8ae6a09 100644 --- a/src/cmd/cat.rs +++ b/src/cmd/cat.rs @@ -4,10 +4,12 @@ use crate::cli; use crate::config::Config; use anyhow::Result; use std::io; +use std::io::Write; impl Command for cli::Cat { fn run(self, _config: &Config) -> Result<()> { - let mut writer = asciicast::Writer::new(io::stdout(), 0); + let mut encoder = asciicast::Encoder::new(0); + let mut stdout = io::stdout(); let mut time_offset: u64 = 0; let mut first = true; @@ -16,7 +18,7 @@ impl Command for cli::Cat { let mut time = time_offset; if first { - writer.write_header(&recording.header)?; + stdout.write_all(&encoder.header(&recording.header))?; first = false; } @@ -24,7 +26,7 @@ impl Command for cli::Cat { let mut event = event?; time = time_offset + event.time; event.time = time; - writer.write_event(&event)?; + stdout.write_all(&encoder.event(&event))?; } time_offset = time; diff --git a/src/cmd/convert.rs b/src/cmd/convert.rs index b0038b3..bbf9856 100644 --- a/src/cmd/convert.rs +++ b/src/cmd/convert.rs @@ -2,7 +2,7 @@ use super::Command; use crate::asciicast::{self, Header}; use crate::cli::{self, Format}; use crate::config::Config; -use crate::encoder::{self, EncoderExt}; +use crate::encoder::{self, AsciicastEncoder, EncoderExt, RawEncoder, TextEncoder}; use crate::util; use anyhow::{bail, Result}; use std::fs; @@ -11,17 +11,16 @@ use std::path::Path; impl Command for cli::Convert { fn run(self, _config: &Config) -> Result<()> { let path = util::get_local_path(&self.input_filename)?; - let input = asciicast::open_from_path(&*path)?; - let mut output = self.get_output(&input.header)?; + let cast = asciicast::open_from_path(&*path)?; + let mut encoder = self.get_encoder(&cast.header); + let mut file = self.open_file()?; - output.encode(input) + encoder.encode_to_file(cast, &mut file) } } impl cli::Convert { - fn get_output(&self, header: &Header) -> Result> { - let file = self.open_file()?; - + fn get_encoder(&self, header: &Header) -> Box { let format = self.format.unwrap_or_else(|| { if self.output_filename.to_lowercase().ends_with(".txt") { Format::Txt @@ -31,15 +30,9 @@ impl cli::Convert { }); match format { - Format::Asciicast => Ok(Box::new(encoder::AsciicastEncoder::new( - file, - false, - 0, - header.into(), - ))), - - Format::Raw => Ok(Box::new(encoder::RawEncoder::new(file, false))), - Format::Txt => Ok(Box::new(encoder::TextEncoder::new(file))), + Format::Asciicast => Box::new(AsciicastEncoder::new(false, 0, header.into())), + Format::Raw => Box::new(RawEncoder::new(false)), + Format::Txt => Box::new(TextEncoder::new()), } } diff --git a/src/cmd/rec.rs b/src/cmd/rec.rs index 2ca622b..91884b3 100644 --- a/src/cmd/rec.rs +++ b/src/cmd/rec.rs @@ -2,10 +2,11 @@ use super::Command; use crate::asciicast; use crate::cli; use crate::config::Config; -use crate::encoder; +use crate::encoder::{AsciicastEncoder, Encoder, Metadata, RawEncoder, TextEncoder}; use crate::locale; use crate::logger; use crate::pty; +use crate::recorder::Output; use crate::recorder::{self, KeyBindings}; use crate::tty::{self, FixedSizeTty, Tty}; use anyhow::{bail, Result}; @@ -13,8 +14,10 @@ use cli::Format; use std::collections::{HashMap, HashSet}; use std::env; use std::fs; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; +use std::time::{SystemTime, UNIX_EPOCH}; impl Command for cli::Record { fn run(mut self, config: &Config) -> Result<()> { @@ -173,17 +176,14 @@ impl cli::Record { match format { Format::Asciicast => { let metadata = self.build_asciicast_metadata(theme, config); + let file = io::LineWriter::new(file); + let encoder = AsciicastEncoder::new(append, time_offset, metadata); - Box::new(encoder::AsciicastEncoder::new( - file, - append, - time_offset, - metadata, - )) + Box::new(FileOutput(file, encoder)) } - Format::Raw => Box::new(encoder::RawEncoder::new(file, append)), - Format::Txt => Box::new(encoder::TextEncoder::new(file)), + Format::Raw => Box::new(FileOutput(file, RawEncoder::new(append))), + Format::Txt => Box::new(FileOutput(file, TextEncoder::new())), } } @@ -191,11 +191,7 @@ impl cli::Record { self.command.as_ref().cloned().or(config.cmd_rec_command()) } - fn build_asciicast_metadata( - &self, - theme: Option, - config: &Config, - ) -> encoder::Metadata { + fn build_asciicast_metadata(&self, theme: Option, config: &Config) -> Metadata { let idle_time_limit = self.idle_time_limit.or(config.cmd_rec_idle_time_limit()); let command = self.get_command(config); @@ -206,7 +202,7 @@ impl cli::Record { .or(config.cmd_rec_env()) .unwrap_or(String::from("TERM,SHELL")); - encoder::Metadata { + Metadata { idle_time_limit, command, title: self.title.clone(), @@ -216,6 +212,23 @@ impl cli::Record { } } +struct FileOutput(W, E); + +impl Output for FileOutput { + fn header(&mut self, time: SystemTime, tty_size: tty::TtySize) -> io::Result<()> { + let timestamp = time.duration_since(UNIX_EPOCH).unwrap().as_secs(); + self.0.write_all(&self.1.start(Some(timestamp), tty_size)) + } + + fn event(&mut self, event: asciicast::Event) -> io::Result<()> { + self.0.write_all(&self.1.event(event)) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.write_all(&self.1.finish()) + } +} + fn get_key_bindings(config: &Config) -> Result { let mut keys = KeyBindings::default(); diff --git a/src/encoder/asciicast.rs b/src/encoder/asciicast.rs index 01cef57..8f655c9 100644 --- a/src/encoder/asciicast.rs +++ b/src/encoder/asciicast.rs @@ -1,10 +1,9 @@ -use crate::asciicast::{Event, Header, Writer}; +use crate::asciicast::{Encoder, Event, Header}; use crate::tty; use std::collections::HashMap; -use std::io::{self, Write}; -pub struct AsciicastEncoder { - writer: Writer, +pub struct AsciicastEncoder { + inner: Encoder, append: bool, metadata: Metadata, } @@ -17,13 +16,12 @@ pub struct Metadata { pub theme: Option, } -impl AsciicastEncoder -where - W: Write, -{ - pub fn new(writer: W, append: bool, time_offset: u64, metadata: Metadata) -> Self { +impl AsciicastEncoder { + pub fn new(append: bool, time_offset: u64, metadata: Metadata) -> Self { + let inner = Encoder::new(time_offset); + Self { - writer: Writer::new(writer, time_offset), + inner, append, metadata, } @@ -44,22 +42,21 @@ where } } -impl super::Encoder for AsciicastEncoder -where - W: Write, -{ - fn start(&mut self, timestamp: Option, tty_size: &tty::TtySize) -> io::Result<()> { +impl super::Encoder for AsciicastEncoder { + fn start(&mut self, timestamp: Option, tty_size: tty::TtySize) -> Vec { if self.append { - Ok(()) + Vec::new() } else { - let header = self.build_header(timestamp, tty_size); - - self.writer.write_header(&header) + self.inner.header(&self.build_header(timestamp, &tty_size)) } } - fn event(&mut self, event: &Event) -> io::Result<()> { - self.writer.write_event(event) + fn event(&mut self, event: Event) -> Vec { + self.inner.event(&event) + } + + fn finish(&mut self) -> Vec { + Vec::new() } } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 627d541..71ca22f 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -8,67 +8,32 @@ pub use raw::RawEncoder; pub use txt::TextEncoder; use crate::asciicast::Event; -use crate::recorder; use crate::tty; use anyhow::Result; -use std::io; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::fs::File; +use std::io::Write; pub trait Encoder { - fn start(&mut self, timestamp: Option, tty_size: &tty::TtySize) -> io::Result<()>; - fn event(&mut self, event: &Event) -> io::Result<()>; - - fn finish(&mut self) -> io::Result<()> { - Ok(()) - } + fn start(&mut self, timestamp: Option, tty_size: tty::TtySize) -> Vec; + fn event(&mut self, event: Event) -> Vec; + fn finish(&mut self) -> Vec; } pub trait EncoderExt { - fn encode(&mut self, recording: crate::asciicast::Asciicast) -> Result<()>; + fn encode_to_file(&mut self, cast: crate::asciicast::Asciicast, file: &mut File) -> Result<()>; } impl EncoderExt for E { - fn encode(&mut self, recording: crate::asciicast::Asciicast) -> Result<()> { - let tty_size = tty::TtySize(recording.header.cols, recording.header.rows); - self.start(recording.header.timestamp, &tty_size)?; + fn encode_to_file(&mut self, cast: crate::asciicast::Asciicast, file: &mut File) -> Result<()> { + let tty_size = tty::TtySize(cast.header.cols, cast.header.rows); + file.write_all(&self.start(cast.header.timestamp, tty_size))?; - for event in recording.events { - self.event(&event?)?; + for event in cast.events { + file.write_all(&self.event(event?))?; } - self.finish()?; + file.write_all(&self.finish())?; Ok(()) } } - -impl recorder::Output for E { - fn start(&mut self, tty_size: &tty::TtySize) -> io::Result<()> { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - self.start(Some(timestamp), tty_size) - } - - fn output(&mut self, time: u64, text: String) -> io::Result<()> { - self.event(&Event::output(time, text)) - } - - fn input(&mut self, time: u64, text: String) -> io::Result<()> { - self.event(&Event::input(time, text)) - } - - fn resize(&mut self, time: u64, size: (u16, u16)) -> io::Result<()> { - self.event(&Event::resize(time, size)) - } - - fn marker(&mut self, time: u64) -> io::Result<()> { - self.event(&Event::marker(time, "".to_owned())) - } - - fn finish(&mut self) -> io::Result<()> { - self.finish() - } -} diff --git a/src/encoder/raw.rs b/src/encoder/raw.rs index 9867967..54d01e9 100644 --- a/src/encoder/raw.rs +++ b/src/encoder/raw.rs @@ -1,34 +1,36 @@ use crate::asciicast::{Event, EventData}; use crate::tty; -use std::io::{self, Write}; -pub struct RawEncoder { - writer: W, +pub struct RawEncoder { append: bool, } -impl RawEncoder { - pub fn new(writer: W, append: bool) -> Self { - RawEncoder { writer, append } +impl RawEncoder { + pub fn new(append: bool) -> Self { + RawEncoder { append } } } -impl super::Encoder for RawEncoder { - fn start(&mut self, _timestamp: Option, tty_size: &tty::TtySize) -> io::Result<()> { +impl super::Encoder for RawEncoder { + fn start(&mut self, _timestamp: Option, tty_size: tty::TtySize) -> Vec { if self.append { - Ok(()) + Vec::new() } else { - write!(self.writer, "\x1b[8;{};{}t", tty_size.1, tty_size.0) + format!("\x1b[8;{};{}t", tty_size.1, tty_size.0).into_bytes() } } - fn event(&mut self, event: &Event) -> io::Result<()> { - if let EventData::Output(data) = &event.data { - self.writer.write_all(data.as_bytes()) + fn event(&mut self, event: Event) -> Vec { + if let EventData::Output(data) = event.data { + data.into_bytes() } else { - Ok(()) + Vec::new() } } + + fn finish(&mut self) -> Vec { + Vec::new() + } } #[cfg(test)] @@ -39,20 +41,27 @@ mod tests { use crate::tty::TtySize; #[test] - fn encoder_impl() -> anyhow::Result<()> { - let mut out: Vec = Vec::new(); - let mut enc = RawEncoder::new(&mut out, false); + fn encoder() { + let mut enc = RawEncoder::new(false); - enc.start(None, &TtySize(100, 50))?; - enc.event(&Event::output(0, "he\x1b[1mllo\r\n".to_owned()))?; - enc.event(&Event::output(1, "world\r\n".to_owned()))?; - enc.event(&Event::input(2, ".".to_owned()))?; - enc.event(&Event::resize(3, (80, 24)))?; - enc.event(&Event::marker(4, ".".to_owned()))?; - enc.finish()?; + assert_eq!( + enc.start(None, TtySize(100, 50)), + "\x1b[8;50;100t".as_bytes() + ); - assert_eq!(out, b"\x1b[8;50;100the\x1b[1mllo\r\nworld\r\n"); + assert_eq!( + enc.event(Event::output(0, "he\x1b[1mllo\r\n".to_owned())), + "he\x1b[1mllo\r\n".as_bytes() + ); - Ok(()) + assert_eq!( + enc.event(Event::output(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.finish().is_empty()); } } diff --git a/src/encoder/txt.rs b/src/encoder/txt.rs index 6990ccf..eca7a41 100644 --- a/src/encoder/txt.rs +++ b/src/encoder/txt.rs @@ -1,62 +1,56 @@ use crate::asciicast::{Event, EventData}; use crate::tty; -use avt::util::{TextCollector, TextCollectorOutput}; -use std::io::{self, Write}; +use avt::util::TextCollector; -pub struct TextEncoder { - writer: Option, - collector: Option>>, +pub struct TextEncoder { + collector: Option, } -impl TextEncoder { - pub fn new(writer: W) -> Self { - TextEncoder { - writer: Some(writer), - collector: None, - } +impl TextEncoder { + pub fn new() -> Self { + TextEncoder { collector: None } } } -impl super::Encoder for TextEncoder { - fn start(&mut self, _timestamp: Option, tty_size: &tty::TtySize) -> io::Result<()> { +impl super::Encoder for TextEncoder { + fn start(&mut self, _timestamp: Option, tty_size: tty::TtySize) -> Vec { let vt = avt::Vt::builder() .size(tty_size.0 as usize, tty_size.1 as usize) .resizable(true) .scrollback_limit(100) .build(); - self.collector = Some(TextCollector::new( - vt, - TextWriter(self.writer.take().unwrap()), - )); + self.collector = Some(TextCollector::new(vt)); - Ok(()) + Vec::new() } - fn event(&mut self, event: &Event) -> io::Result<()> { + fn event(&mut self, event: Event) -> Vec { use EventData::*; match &event.data { - Output(data) => self.collector.as_mut().unwrap().feed_str(data), - Resize(cols, rows) => self.collector.as_mut().unwrap().resize(*cols, *rows), - _ => Ok(()), + Output(data) => text_lines_to_bytes(self.collector.as_mut().unwrap().feed_str(data)), + + Resize(cols, rows) => { + text_lines_to_bytes(self.collector.as_mut().unwrap().resize(*cols, *rows)) + } + + _ => Vec::new(), } } - fn finish(&mut self) -> io::Result<()> { - self.collector.as_mut().unwrap().flush() + fn finish(&mut self) -> Vec { + text_lines_to_bytes(self.collector.take().unwrap().flush().iter()) } } -struct TextWriter(W); +fn text_lines_to_bytes>(lines: impl Iterator) -> Vec { + lines.fold(Vec::new(), |mut bytes, line| { + bytes.extend_from_slice(line.as_ref().as_bytes()); + bytes.push(b'\n'); -impl TextCollectorOutput for TextWriter { - type Error = io::Error; - - fn push(&mut self, line: String) -> Result<(), Self::Error> { - self.0.write_all(line.as_bytes())?; - self.0.write_all(b"\n") - } + bytes + }) } #[cfg(test)] @@ -67,17 +61,19 @@ mod tests { use crate::tty::TtySize; #[test] - fn encoder_impl() -> anyhow::Result<()> { - let mut out: Vec = Vec::new(); - let mut enc = TextEncoder::new(&mut out); + fn encoder() { + let mut enc = TextEncoder::new(); - enc.start(None, &TtySize(3, 1))?; - enc.event(&Event::output(0, "he\x1b[1mllo\r\n".to_owned()))?; - enc.event(&Event::output(1, "world\r\n".to_owned()))?; - enc.finish()?; + assert!(enc.start(None, TtySize(3, 1)).is_empty()); - assert_eq!(out, b"hello\nworld\n"); + assert!(enc + .event(Event::output(0, "he\x1b[1mllo\r\n".to_owned())) + .is_empty()); - Ok(()) + assert!(enc + .event(Event::output(1, "world\r\n".to_owned())) + .is_empty()); + + assert_eq!(enc.finish(), "hello\nworld\n".as_bytes()); } } diff --git a/src/pty.rs b/src/pty.rs index b9dcbba..ae7b0dd 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -23,7 +23,7 @@ use std::time::{Duration, Instant}; type ExtraEnv = HashMap; pub trait Handler { - fn start(&mut self, epoch: Instant, tty_size: TtySize); + fn start(&mut self, tty_size: TtySize); fn output(&mut self, time: Duration, data: &[u8]) -> bool; fn input(&mut self, time: Duration, data: &[u8]) -> bool; fn resize(&mut self, time: Duration, tty_size: TtySize) -> bool; @@ -37,7 +37,7 @@ pub fn exec, T: Tty + ?Sized, H: Handler>( ) -> Result { let winsize = tty.get_size(); let epoch = Instant::now(); - handler.start(epoch, winsize.into()); + handler.start(winsize.into()); let result = unsafe { pty::forkpty(Some(&winsize), None) }?; match result.fork_result { @@ -378,7 +378,7 @@ mod tests { use super::Handler; use crate::pty::ExtraEnv; use crate::tty::{FixedSizeTty, NullTty, TtySize}; - use std::time::{Duration, Instant}; + use std::time::Duration; #[derive(Default)] struct TestHandler { @@ -387,7 +387,7 @@ mod tests { } impl Handler for TestHandler { - fn start(&mut self, _epoch: Instant, tty_size: TtySize) { + fn start(&mut self, tty_size: TtySize) { self.tty_size = Some(tty_size); } diff --git a/src/recorder.rs b/src/recorder.rs index 8c14eac..3c20211 100644 --- a/src/recorder.rs +++ b/src/recorder.rs @@ -1,3 +1,4 @@ +use crate::asciicast::Event; use crate::config::Key; use crate::notifier::Notifier; use crate::pty; @@ -6,7 +7,7 @@ use crate::util; use std::io; use std::sync::mpsc; use std::thread; -use std::time::{Duration, Instant}; +use std::time::{Duration, SystemTime}; pub struct Recorder { output: Option>, @@ -22,15 +23,9 @@ pub struct Recorder { } pub trait Output { - fn start(&mut self, tty_size: &tty::TtySize) -> io::Result<()>; - fn output(&mut self, time: u64, text: String) -> io::Result<()>; - fn input(&mut self, time: u64, text: String) -> io::Result<()>; - fn resize(&mut self, time: u64, size: (u16, u16)) -> io::Result<()>; - fn marker(&mut self, time: u64) -> io::Result<()>; - - fn finish(&mut self) -> io::Result<()> { - Ok(()) - } + fn header(&mut self, time: SystemTime, tty_size: tty::TtySize) -> io::Result<()>; + fn event(&mut self, event: Event) -> io::Result<()>; + fn flush(&mut self) -> io::Result<()>; } enum Message { @@ -82,9 +77,9 @@ impl Recorder { } impl pty::Handler for Recorder { - fn start(&mut self, _epoch: Instant, tty_size: tty::TtySize) { + fn start(&mut self, tty_size: tty::TtySize) { let mut output = self.output.take().unwrap(); - let _ = output.start(&tty_size); + let _ = output.header(SystemTime::now(), tty_size); let receiver = self.receiver.take().unwrap(); let mut notifier = self.notifier.take().unwrap(); @@ -100,7 +95,7 @@ impl pty::Handler for Recorder { let text = output_decoder.feed(&data); if !text.is_empty() { - let _ = output.output(time, text); + let _ = output.event(Event::output(time, text)); } } @@ -108,19 +103,19 @@ impl pty::Handler for Recorder { let text = input_decoder.feed(&data); if !text.is_empty() { - let _ = output.input(time, text); + let _ = output.event(Event::input(time, text)); } } Resize(time, new_tty_size) => { if new_tty_size != last_tty_size { - let _ = output.resize(time, new_tty_size.into()); + let _ = output.event(Event::resize(time, new_tty_size.into())); last_tty_size = new_tty_size; } } Marker(time) => { - let _ = output.marker(time); + let _ = output.event(Event::marker(time, String::new())); } Notification(text) => { @@ -129,7 +124,7 @@ impl pty::Handler for Recorder { } } - let _ = output.finish(); + let _ = output.flush(); }); self.handle = Some(util::JoinHandle::new(handle)); diff --git a/src/streamer/mod.rs b/src/streamer/mod.rs index 88cbf6a..bc3824c 100644 --- a/src/streamer/mod.rs +++ b/src/streamer/mod.rs @@ -10,7 +10,6 @@ use crate::util; use std::net; use std::thread; use std::time::Duration; -use std::time::Instant; use tokio::sync::mpsc; use tracing::info; @@ -83,7 +82,7 @@ impl Streamer { } impl pty::Handler for Streamer { - fn start(&mut self, _epoch: Instant, tty_size: tty::TtySize) { + fn start(&mut self, tty_size: tty::TtySize) { let pty_rx = self.pty_rx.take().unwrap(); let (clients_tx, mut clients_rx) = mpsc::channel(1); let shutdown_token = tokio_util::sync::CancellationToken::new();