Make Encoder return bytes instead of directly writing to io::Write impl

This commit is contained in:
Marcin Kulik
2024-10-17 17:31:59 +02:00
parent 75e275082a
commit 4ad1c22a29
14 changed files with 239 additions and 293 deletions

5
Cargo.lock generated
View File

@@ -179,12 +179,11 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "avt" name = "avt"
version = "0.11.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cb3c38905502a1f46c37ae2d3373c36a3aaf3cd9f7fde871984ef870f039bd6" checksum = "b485f400d02970694eed10e7080f994ad82eaf56a867d6671af5d5e184ed8ee6"
dependencies = [ dependencies = [
"rgb", "rgb",
"serde",
"unicode-width", "unicode-width",
] ]

View File

@@ -28,7 +28,7 @@ config = { version = "0.14.0", default-features = false, features = ["toml", "in
which = "6.0.0" which = "6.0.0"
tempfile = "3.9.0" tempfile = "3.9.0"
scraper = { version = "0.19.0", default-features = false } 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"] } axum = { version = "0.7.4", default-features = false, features = ["http1", "ws"] }
tokio = { version = "1.35.1", features = ["full"] } tokio = { version = "1.35.1", features = ["full"] }
futures-util = "0.3.30" futures-util = "0.3.30"

View File

@@ -7,7 +7,7 @@ use std::collections::HashMap;
use std::fs; use std::fs;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::path::Path; use std::path::Path;
pub use v2::Writer; pub use v2::Encoder;
pub struct Asciicast<'a> { pub struct Asciicast<'a> {
pub header: Header, pub header: Header,
@@ -138,12 +138,11 @@ pub fn accelerate(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Asciicast, Event, EventData, Header, Writer}; use super::{Asciicast, Encoder, Event, EventData, Header};
use crate::tty; use crate::tty;
use anyhow::Result; use anyhow::Result;
use rgb::RGB8; use rgb::RGB8;
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
#[test] #[test]
fn open_v1_minimal() { fn open_v1_minimal() {
@@ -221,44 +220,30 @@ mod tests {
} }
#[test] #[test]
fn writer() { fn encoder() {
let mut data = Vec::new(); let mut data = Vec::new();
{ let header = Header {
let mut fw = Writer::new(&mut data, 0); version: 2,
cols: 80,
rows: 24,
timestamp: None,
idle_time_limit: None,
command: None,
title: None,
env: Default::default(),
theme: None,
};
let header = Header { let mut enc = Encoder::new(0);
version: 2, data.extend(enc.header(&header));
cols: 80, data.extend(enc.event(&Event::output(1000001, "hello\r\n".to_owned())));
rows: 24,
timestamp: None,
idle_time_limit: None,
command: None,
title: None,
env: Default::default(),
theme: None,
};
fw.write_header(&header).unwrap(); let mut enc = Encoder::new(1000001);
data.extend(enc.event(&Event::output(1000001, "world".to_owned())));
fw.write_event(&Event::output(1000001, "hello\r\n".to_owned())) data.extend(enc.event(&Event::input(2000002, " ".to_owned())));
.unwrap(); data.extend(enc.event(&Event::resize(3000003, (100, 40))));
} data.extend(enc.event(&Event::output(4000004, "żółć".to_owned())));
{
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 lines = parse(data); let lines = parse(data);
@@ -284,53 +269,48 @@ mod tests {
} }
#[test] #[test]
fn write_header() { fn header_encoding() {
let mut data = Vec::new(); 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 theme = tty::Theme {
let mut fw = Writer::new(io::Cursor::new(&mut data), 0); fg: RGB8::new(0, 1, 2),
let mut env = HashMap::new(); bg: RGB8::new(0, 100, 200),
env.insert("SHELL".to_owned(), "/usr/bin/fish".to_owned()); palette: vec![
env.insert("TERM".to_owned(), "xterm256-color".to_owned()); 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 { let header = Header {
fg: RGB8::new(0, 1, 2), version: 2,
bg: RGB8::new(0, 100, 200), cols: 80,
palette: vec![ rows: 24,
RGB8::new(0, 0, 0), timestamp: Some(1704719152),
RGB8::new(10, 11, 12), idle_time_limit: Some(1.5),
RGB8::new(20, 21, 22), command: Some("/bin/bash".to_owned()),
RGB8::new(30, 31, 32), title: Some("Demo".to_owned()),
RGB8::new(40, 41, 42), env: Some(env),
RGB8::new(50, 51, 52), theme: Some(theme),
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 data = enc.header(&header);
let lines = parse(data); let lines = parse(data);
assert_eq!(lines[0]["version"], 2); assert_eq!(lines[0]["version"], 2);

View File

@@ -4,7 +4,7 @@ use anyhow::{anyhow, bail, Result};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::io::{self, Write}; use std::io;
#[derive(Deserialize)] #[derive(Deserialize)]
struct V2Header { struct V2Header {
@@ -156,30 +156,28 @@ where
} }
} }
pub struct Writer<W: Write> { pub struct Encoder {
writer: io::LineWriter<W>,
time_offset: u64, time_offset: u64,
} }
impl<W> Writer<W> impl Encoder {
where pub fn new(time_offset: u64) -> Self {
W: Write, Self { time_offset }
{
pub fn new(writer: W, time_offset: u64) -> Self {
Self {
writer: io::LineWriter::new(writer),
time_offset,
}
} }
pub fn write_header(&mut self, header: &Header) -> io::Result<()> { pub fn header(&mut self, header: &Header) -> Vec<u8> {
let header: V2Header = header.into(); 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<()> { pub fn event(&mut self, event: &Event) -> Vec<u8> {
writeln!(self.writer, "{}", self.serialize_event(event)?) let mut data = self.serialize_event(event).unwrap().into_bytes();
data.push(b'\n');
data
} }
fn serialize_event(&self, event: &Event) -> Result<String, serde_json::Error> { fn serialize_event(&self, event: &Event) -> Result<String, serde_json::Error> {

View File

@@ -4,10 +4,12 @@ use crate::cli;
use crate::config::Config; use crate::config::Config;
use anyhow::Result; use anyhow::Result;
use std::io; use std::io;
use std::io::Write;
impl Command for cli::Cat { impl Command for cli::Cat {
fn run(self, _config: &Config) -> Result<()> { 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 time_offset: u64 = 0;
let mut first = true; let mut first = true;
@@ -16,7 +18,7 @@ impl Command for cli::Cat {
let mut time = time_offset; let mut time = time_offset;
if first { if first {
writer.write_header(&recording.header)?; stdout.write_all(&encoder.header(&recording.header))?;
first = false; first = false;
} }
@@ -24,7 +26,7 @@ impl Command for cli::Cat {
let mut event = event?; let mut event = event?;
time = time_offset + event.time; time = time_offset + event.time;
event.time = time; event.time = time;
writer.write_event(&event)?; stdout.write_all(&encoder.event(&event))?;
} }
time_offset = time; time_offset = time;

View File

@@ -2,7 +2,7 @@ use super::Command;
use crate::asciicast::{self, Header}; use crate::asciicast::{self, Header};
use crate::cli::{self, Format}; use crate::cli::{self, Format};
use crate::config::Config; use crate::config::Config;
use crate::encoder::{self, EncoderExt}; use crate::encoder::{self, AsciicastEncoder, EncoderExt, RawEncoder, TextEncoder};
use crate::util; use crate::util;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::fs; use std::fs;
@@ -11,17 +11,16 @@ use std::path::Path;
impl Command for cli::Convert { impl Command for cli::Convert {
fn run(self, _config: &Config) -> Result<()> { fn run(self, _config: &Config) -> Result<()> {
let path = util::get_local_path(&self.input_filename)?; let path = util::get_local_path(&self.input_filename)?;
let input = asciicast::open_from_path(&*path)?; let cast = asciicast::open_from_path(&*path)?;
let mut output = self.get_output(&input.header)?; 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 { impl cli::Convert {
fn get_output(&self, header: &Header) -> Result<Box<dyn encoder::Encoder>> { fn get_encoder(&self, header: &Header) -> Box<dyn encoder::Encoder> {
let file = self.open_file()?;
let format = self.format.unwrap_or_else(|| { let format = self.format.unwrap_or_else(|| {
if self.output_filename.to_lowercase().ends_with(".txt") { if self.output_filename.to_lowercase().ends_with(".txt") {
Format::Txt Format::Txt
@@ -31,15 +30,9 @@ impl cli::Convert {
}); });
match format { match format {
Format::Asciicast => Ok(Box::new(encoder::AsciicastEncoder::new( Format::Asciicast => Box::new(AsciicastEncoder::new(false, 0, header.into())),
file, Format::Raw => Box::new(RawEncoder::new(false)),
false, Format::Txt => Box::new(TextEncoder::new()),
0,
header.into(),
))),
Format::Raw => Ok(Box::new(encoder::RawEncoder::new(file, false))),
Format::Txt => Ok(Box::new(encoder::TextEncoder::new(file))),
} }
} }

View File

@@ -2,10 +2,11 @@ use super::Command;
use crate::asciicast; use crate::asciicast;
use crate::cli; use crate::cli;
use crate::config::Config; use crate::config::Config;
use crate::encoder; use crate::encoder::{AsciicastEncoder, Encoder, Metadata, RawEncoder, TextEncoder};
use crate::locale; use crate::locale;
use crate::logger; use crate::logger;
use crate::pty; use crate::pty;
use crate::recorder::Output;
use crate::recorder::{self, KeyBindings}; use crate::recorder::{self, KeyBindings};
use crate::tty::{self, FixedSizeTty, Tty}; use crate::tty::{self, FixedSizeTty, Tty};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -13,8 +14,10 @@ use cli::Format;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::fs; use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
impl Command for cli::Record { impl Command for cli::Record {
fn run(mut self, config: &Config) -> Result<()> { fn run(mut self, config: &Config) -> Result<()> {
@@ -173,17 +176,14 @@ impl cli::Record {
match format { match format {
Format::Asciicast => { Format::Asciicast => {
let metadata = self.build_asciicast_metadata(theme, config); 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( Box::new(FileOutput(file, encoder))
file,
append,
time_offset,
metadata,
))
} }
Format::Raw => Box::new(encoder::RawEncoder::new(file, append)), Format::Raw => Box::new(FileOutput(file, RawEncoder::new(append))),
Format::Txt => Box::new(encoder::TextEncoder::new(file)), 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()) self.command.as_ref().cloned().or(config.cmd_rec_command())
} }
fn build_asciicast_metadata( fn build_asciicast_metadata(&self, theme: Option<tty::Theme>, config: &Config) -> Metadata {
&self,
theme: Option<tty::Theme>,
config: &Config,
) -> encoder::Metadata {
let idle_time_limit = self.idle_time_limit.or(config.cmd_rec_idle_time_limit()); let idle_time_limit = self.idle_time_limit.or(config.cmd_rec_idle_time_limit());
let command = self.get_command(config); let command = self.get_command(config);
@@ -206,7 +202,7 @@ impl cli::Record {
.or(config.cmd_rec_env()) .or(config.cmd_rec_env())
.unwrap_or(String::from("TERM,SHELL")); .unwrap_or(String::from("TERM,SHELL"));
encoder::Metadata { Metadata {
idle_time_limit, idle_time_limit,
command, command,
title: self.title.clone(), title: self.title.clone(),
@@ -216,6 +212,23 @@ impl cli::Record {
} }
} }
struct FileOutput<W: Write, E: Encoder>(W, E);
impl<W: Write, E: Encoder> Output for FileOutput<W, E> {
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<KeyBindings> { fn get_key_bindings(config: &Config) -> Result<KeyBindings> {
let mut keys = KeyBindings::default(); let mut keys = KeyBindings::default();

View File

@@ -1,10 +1,9 @@
use crate::asciicast::{Event, Header, Writer}; use crate::asciicast::{Encoder, Event, Header};
use crate::tty; use crate::tty;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{self, Write};
pub struct AsciicastEncoder<W: Write> { pub struct AsciicastEncoder {
writer: Writer<W>, inner: Encoder,
append: bool, append: bool,
metadata: Metadata, metadata: Metadata,
} }
@@ -17,13 +16,12 @@ pub struct Metadata {
pub theme: Option<tty::Theme>, pub theme: Option<tty::Theme>,
} }
impl<W> AsciicastEncoder<W> impl AsciicastEncoder {
where pub fn new(append: bool, time_offset: u64, metadata: Metadata) -> Self {
W: Write, let inner = Encoder::new(time_offset);
{
pub fn new(writer: W, append: bool, time_offset: u64, metadata: Metadata) -> Self {
Self { Self {
writer: Writer::new(writer, time_offset), inner,
append, append,
metadata, metadata,
} }
@@ -44,22 +42,21 @@ where
} }
} }
impl<W> super::Encoder for AsciicastEncoder<W> impl super::Encoder for AsciicastEncoder {
where fn start(&mut self, timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
W: Write,
{
fn start(&mut self, timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> {
if self.append { if self.append {
Ok(()) Vec::new()
} else { } else {
let header = self.build_header(timestamp, tty_size); self.inner.header(&self.build_header(timestamp, &tty_size))
self.writer.write_header(&header)
} }
} }
fn event(&mut self, event: &Event) -> io::Result<()> { fn event(&mut self, event: Event) -> Vec<u8> {
self.writer.write_event(event) self.inner.event(&event)
}
fn finish(&mut self) -> Vec<u8> {
Vec::new()
} }
} }

View File

@@ -8,67 +8,32 @@ pub use raw::RawEncoder;
pub use txt::TextEncoder; pub use txt::TextEncoder;
use crate::asciicast::Event; use crate::asciicast::Event;
use crate::recorder;
use crate::tty; use crate::tty;
use anyhow::Result; use anyhow::Result;
use std::io; use std::fs::File;
use std::time::{SystemTime, UNIX_EPOCH}; use std::io::Write;
pub trait Encoder { pub trait Encoder {
fn start(&mut self, timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()>; fn start(&mut self, timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8>;
fn event(&mut self, event: &Event) -> io::Result<()>; fn event(&mut self, event: Event) -> Vec<u8>;
fn finish(&mut self) -> Vec<u8>;
fn finish(&mut self) -> io::Result<()> {
Ok(())
}
} }
pub trait EncoderExt { 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<E: Encoder + ?Sized> EncoderExt for E { impl<E: Encoder + ?Sized> EncoderExt for E {
fn encode(&mut self, recording: crate::asciicast::Asciicast) -> Result<()> { fn encode_to_file(&mut self, cast: crate::asciicast::Asciicast, file: &mut File) -> Result<()> {
let tty_size = tty::TtySize(recording.header.cols, recording.header.rows); let tty_size = tty::TtySize(cast.header.cols, cast.header.rows);
self.start(recording.header.timestamp, &tty_size)?; file.write_all(&self.start(cast.header.timestamp, tty_size))?;
for event in recording.events { for event in cast.events {
self.event(&event?)?; file.write_all(&self.event(event?))?;
} }
self.finish()?; file.write_all(&self.finish())?;
Ok(()) Ok(())
} }
} }
impl<E: Encoder> 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()
}
}

View File

@@ -1,34 +1,36 @@
use crate::asciicast::{Event, EventData}; use crate::asciicast::{Event, EventData};
use crate::tty; use crate::tty;
use std::io::{self, Write};
pub struct RawEncoder<W> { pub struct RawEncoder {
writer: W,
append: bool, append: bool,
} }
impl<W> RawEncoder<W> { impl RawEncoder {
pub fn new(writer: W, append: bool) -> Self { pub fn new(append: bool) -> Self {
RawEncoder { writer, append } RawEncoder { append }
} }
} }
impl<W: Write> super::Encoder for RawEncoder<W> { impl super::Encoder for RawEncoder {
fn start(&mut self, _timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> { fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
if self.append { if self.append {
Ok(()) Vec::new()
} else { } 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<()> { fn event(&mut self, event: Event) -> Vec<u8> {
if let EventData::Output(data) = &event.data { if let EventData::Output(data) = event.data {
self.writer.write_all(data.as_bytes()) data.into_bytes()
} else { } else {
Ok(()) Vec::new()
} }
} }
fn finish(&mut self) -> Vec<u8> {
Vec::new()
}
} }
#[cfg(test)] #[cfg(test)]
@@ -39,20 +41,27 @@ mod tests {
use crate::tty::TtySize; use crate::tty::TtySize;
#[test] #[test]
fn encoder_impl() -> anyhow::Result<()> { fn encoder() {
let mut out: Vec<u8> = Vec::new(); let mut enc = RawEncoder::new(false);
let mut enc = RawEncoder::new(&mut out, false);
enc.start(None, &TtySize(100, 50))?; assert_eq!(
enc.event(&Event::output(0, "he\x1b[1mllo\r\n".to_owned()))?; enc.start(None, TtySize(100, 50)),
enc.event(&Event::output(1, "world\r\n".to_owned()))?; "\x1b[8;50;100t".as_bytes()
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!(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());
} }
} }

View File

@@ -1,62 +1,56 @@
use crate::asciicast::{Event, EventData}; use crate::asciicast::{Event, EventData};
use crate::tty; use crate::tty;
use avt::util::{TextCollector, TextCollectorOutput}; use avt::util::TextCollector;
use std::io::{self, Write};
pub struct TextEncoder<W: Write> { pub struct TextEncoder {
writer: Option<W>, collector: Option<TextCollector>,
collector: Option<TextCollector<TextWriter<W>>>,
} }
impl<W: Write> TextEncoder<W> { impl TextEncoder {
pub fn new(writer: W) -> Self { pub fn new() -> Self {
TextEncoder { TextEncoder { collector: None }
writer: Some(writer),
collector: None,
}
} }
} }
impl<W: Write> super::Encoder for TextEncoder<W> { impl super::Encoder for TextEncoder {
fn start(&mut self, _timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> { fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
let vt = avt::Vt::builder() let vt = avt::Vt::builder()
.size(tty_size.0 as usize, tty_size.1 as usize) .size(tty_size.0 as usize, tty_size.1 as usize)
.resizable(true) .resizable(true)
.scrollback_limit(100) .scrollback_limit(100)
.build(); .build();
self.collector = Some(TextCollector::new( self.collector = Some(TextCollector::new(vt));
vt,
TextWriter(self.writer.take().unwrap()),
));
Ok(()) Vec::new()
} }
fn event(&mut self, event: &Event) -> io::Result<()> { fn event(&mut self, event: Event) -> Vec<u8> {
use EventData::*; use EventData::*;
match &event.data { match &event.data {
Output(data) => self.collector.as_mut().unwrap().feed_str(data), Output(data) => text_lines_to_bytes(self.collector.as_mut().unwrap().feed_str(data)),
Resize(cols, rows) => self.collector.as_mut().unwrap().resize(*cols, *rows),
_ => Ok(()), Resize(cols, rows) => {
text_lines_to_bytes(self.collector.as_mut().unwrap().resize(*cols, *rows))
}
_ => Vec::new(),
} }
} }
fn finish(&mut self) -> io::Result<()> { fn finish(&mut self) -> Vec<u8> {
self.collector.as_mut().unwrap().flush() text_lines_to_bytes(self.collector.take().unwrap().flush().iter())
} }
} }
struct TextWriter<W: Write>(W); fn text_lines_to_bytes<S: AsRef<str>>(lines: impl Iterator<Item = S>) -> Vec<u8> {
lines.fold(Vec::new(), |mut bytes, line| {
bytes.extend_from_slice(line.as_ref().as_bytes());
bytes.push(b'\n');
impl<W: Write> TextCollectorOutput for TextWriter<W> { bytes
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")
}
} }
#[cfg(test)] #[cfg(test)]
@@ -67,17 +61,19 @@ mod tests {
use crate::tty::TtySize; use crate::tty::TtySize;
#[test] #[test]
fn encoder_impl() -> anyhow::Result<()> { fn encoder() {
let mut out: Vec<u8> = Vec::new(); let mut enc = TextEncoder::new();
let mut enc = TextEncoder::new(&mut out);
enc.start(None, &TtySize(3, 1))?; assert!(enc.start(None, TtySize(3, 1)).is_empty());
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_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());
} }
} }

View File

@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
type ExtraEnv = HashMap<String, String>; type ExtraEnv = HashMap<String, String>;
pub trait Handler { 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 output(&mut self, time: Duration, data: &[u8]) -> bool;
fn input(&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; fn resize(&mut self, time: Duration, tty_size: TtySize) -> bool;
@@ -37,7 +37,7 @@ pub fn exec<S: AsRef<str>, T: Tty + ?Sized, H: Handler>(
) -> Result<i32> { ) -> Result<i32> {
let winsize = tty.get_size(); let winsize = tty.get_size();
let epoch = Instant::now(); let epoch = Instant::now();
handler.start(epoch, winsize.into()); handler.start(winsize.into());
let result = unsafe { pty::forkpty(Some(&winsize), None) }?; let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
match result.fork_result { match result.fork_result {
@@ -378,7 +378,7 @@ mod tests {
use super::Handler; use super::Handler;
use crate::pty::ExtraEnv; use crate::pty::ExtraEnv;
use crate::tty::{FixedSizeTty, NullTty, TtySize}; use crate::tty::{FixedSizeTty, NullTty, TtySize};
use std::time::{Duration, Instant}; use std::time::Duration;
#[derive(Default)] #[derive(Default)]
struct TestHandler { struct TestHandler {
@@ -387,7 +387,7 @@ mod tests {
} }
impl Handler for TestHandler { 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); self.tty_size = Some(tty_size);
} }

View File

@@ -1,3 +1,4 @@
use crate::asciicast::Event;
use crate::config::Key; use crate::config::Key;
use crate::notifier::Notifier; use crate::notifier::Notifier;
use crate::pty; use crate::pty;
@@ -6,7 +7,7 @@ use crate::util;
use std::io; use std::io;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, SystemTime};
pub struct Recorder { pub struct Recorder {
output: Option<Box<dyn Output + Send>>, output: Option<Box<dyn Output + Send>>,
@@ -22,15 +23,9 @@ pub struct Recorder {
} }
pub trait Output { pub trait Output {
fn start(&mut self, tty_size: &tty::TtySize) -> io::Result<()>; fn header(&mut self, time: SystemTime, tty_size: tty::TtySize) -> io::Result<()>;
fn output(&mut self, time: u64, text: String) -> io::Result<()>; fn event(&mut self, event: Event) -> io::Result<()>;
fn input(&mut self, time: u64, text: String) -> io::Result<()>; fn flush(&mut self) -> 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 { enum Message {
@@ -82,9 +77,9 @@ impl Recorder {
} }
impl pty::Handler for 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 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 receiver = self.receiver.take().unwrap();
let mut notifier = self.notifier.take().unwrap(); let mut notifier = self.notifier.take().unwrap();
@@ -100,7 +95,7 @@ impl pty::Handler for Recorder {
let text = output_decoder.feed(&data); let text = output_decoder.feed(&data);
if !text.is_empty() { 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); let text = input_decoder.feed(&data);
if !text.is_empty() { if !text.is_empty() {
let _ = output.input(time, text); let _ = output.event(Event::input(time, text));
} }
} }
Resize(time, new_tty_size) => { Resize(time, new_tty_size) => {
if new_tty_size != last_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; last_tty_size = new_tty_size;
} }
} }
Marker(time) => { Marker(time) => {
let _ = output.marker(time); let _ = output.event(Event::marker(time, String::new()));
} }
Notification(text) => { Notification(text) => {
@@ -129,7 +124,7 @@ impl pty::Handler for Recorder {
} }
} }
let _ = output.finish(); let _ = output.flush();
}); });
self.handle = Some(util::JoinHandle::new(handle)); self.handle = Some(util::JoinHandle::new(handle));

View File

@@ -10,7 +10,6 @@ use crate::util;
use std::net; use std::net;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::time::Instant;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::info; use tracing::info;
@@ -83,7 +82,7 @@ impl Streamer {
} }
impl pty::Handler for 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 pty_rx = self.pty_rx.take().unwrap();
let (clients_tx, mut clients_rx) = mpsc::channel(1); let (clients_tx, mut clients_rx) = mpsc::channel(1);
let shutdown_token = tokio_util::sync::CancellationToken::new(); let shutdown_token = tokio_util::sync::CancellationToken::new();