diff --git a/Cargo.lock b/Cargo.lock index 9851109..01c0d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,7 @@ name = "asciinema" version = "3.0.0-beta.1" dependencies = [ "anyhow", + "avt", "clap", "config", "nix", @@ -132,6 +133,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "avt" +version = "0.8.3" +source = "git+https://github.com/asciinema/avt?tag=v0.8.3#28aed2b5a65a4ff07344fd6bc51b8ccb8542389c" +dependencies = [ + "rgb", + "serde", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -171,6 +181,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -1255,6 +1271,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.7" diff --git a/Cargo.toml b/Cargo.toml index 66898cb..b15471e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ config = { version = "0.13.4", default-features = false, features = ["toml", "in which = "5.0.0" tempfile = "3.9.0" scraper = { version = "0.15.0", default-features = false } +avt = { git = "https://github.com/asciinema/avt", tag = "v0.8.3" } diff --git a/src/cmd/rec.rs b/src/cmd/rec.rs index a2dce6b..2280d2c 100644 --- a/src/cmd/rec.rs +++ b/src/cmd/rec.rs @@ -68,6 +68,7 @@ pub struct Cli { enum Format { Asciicast, Raw, + Txt, } #[derive(Clone, Debug)] @@ -84,7 +85,6 @@ impl Cli { let keys = get_key_bindings(config)?; let notifier = get_notifier(config); let record_input = self.input || config.cmd_rec_input(); - let mut recorder = recorder::Recorder::new(output, record_input, keys, notifier); let exec_command = build_exec_command(command.as_ref().cloned()); let exec_extra_env = build_exec_extra_env(); let tty_size = self.get_tty_size(); @@ -103,6 +103,8 @@ impl Cli { Box::new(tty::NullTty::open()?) }; + let mut recorder = recorder::Recorder::new(output, record_input, keys, notifier); + pty::exec( &exec_command, &exec_extra_env, @@ -179,6 +181,7 @@ impl Cli { } Format::Raw => Ok(Box::new(output::Raw::new(file, append))), + Format::Txt => Ok(Box::new(output::Txt::new(file))), } } diff --git a/src/output/mod.rs b/src/output/mod.rs index 0a21be3..761f6d7 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,6 +1,8 @@ mod asciicast; mod raw; +mod txt; pub use asciicast::Asciicast; pub use asciicast::Metadata; pub use raw::Raw; +pub use txt::Txt; diff --git a/src/output/txt.rs b/src/output/txt.rs new file mode 100644 index 0000000..7e93d8b --- /dev/null +++ b/src/output/txt.rs @@ -0,0 +1,88 @@ +use crate::recorder; +use crate::tty; +use std::io::{self, Write}; + +pub struct Txt { + writer: W, + vt: Option, +} + +impl Txt { + pub fn new(writer: W) -> Self { + Txt { writer, vt: None } + } +} + +impl recorder::Output for Txt { + fn start(&mut self, _timestamp: u64, tty_size: &tty::TtySize) -> io::Result<()> { + let (cols, rows) = (*tty_size).into(); + + let vt = avt::Vt::builder() + .size(cols as usize, rows as usize) + .resizable(true) + .build(); + + self.vt = Some(vt); + + Ok(()) + } + + fn output(&mut self, _time: u64, data: &[u8]) -> io::Result<()> { + let data = String::from_utf8_lossy(data).to_string(); + self.vt.as_mut().unwrap().feed_str(&data); + + Ok(()) + } + + fn input(&mut self, _time: u64, _data: &[u8]) -> io::Result<()> { + Ok(()) + } + + fn resize(&mut self, _time: u64, (cols, rows): (u16, u16)) -> io::Result<()> { + self.vt + .as_mut() + .unwrap() + .feed_str(&format!("\x1b[8;{rows};{cols}t")); + + Ok(()) + } + + fn marker(&mut self, _time: u64) -> io::Result<()> { + Ok(()) + } + + fn finish(&mut self) -> io::Result<()> { + let mut text = self.vt.as_ref().unwrap().text(); + + while !text.is_empty() && text[text.len() - 1].is_empty() { + text.truncate(text.len() - 1); + } + + for line in text { + self.writer.write_all(line.as_bytes())?; + self.writer.write_all(b"\n")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::Txt; + use crate::recorder::Output; + use crate::tty::TtySize; + + #[test] + fn x() { + let mut output: Vec = Vec::new(); + let mut txt = Txt::new(&mut output); + + txt.start(1706111685, &TtySize(3, 1)).unwrap(); + txt.output(0, b"he\x1b[1mllo\r\n").unwrap(); + txt.output(1, b"world\r\n").unwrap(); + txt.finish().unwrap(); + + assert_eq!(output, b"hello\nworld\n"); + } +} diff --git a/src/recorder.rs b/src/recorder.rs index ad9640c..8e67df7 100644 --- a/src/recorder.rs +++ b/src/recorder.rs @@ -26,6 +26,10 @@ pub trait Output { fn input(&mut self, time: u64, data: &[u8]) -> 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(()) + } } enum Message { @@ -120,6 +124,8 @@ impl pty::Recorder for Recorder { } } } + + let _ = output.finish(); }); self.handle = Some(JoinHandle(Some(handle)));