mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-14 18:57:59 +01:00
Make Encoder return bytes instead of directly writing to io::Write impl
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
142
src/asciicast.rs
142
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);
|
||||
|
||||
@@ -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<W: Write> {
|
||||
writer: io::LineWriter<W>,
|
||||
pub struct Encoder {
|
||||
time_offset: u64,
|
||||
}
|
||||
|
||||
impl<W> Writer<W>
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Box<dyn encoder::Encoder>> {
|
||||
let file = self.open_file()?;
|
||||
|
||||
fn get_encoder(&self, header: &Header) -> Box<dyn encoder::Encoder> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<tty::Theme>,
|
||||
config: &Config,
|
||||
) -> encoder::Metadata {
|
||||
fn build_asciicast_metadata(&self, theme: Option<tty::Theme>, 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: 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> {
|
||||
let mut keys = KeyBindings::default();
|
||||
|
||||
|
||||
@@ -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<W: Write> {
|
||||
writer: Writer<W>,
|
||||
pub struct AsciicastEncoder {
|
||||
inner: Encoder,
|
||||
append: bool,
|
||||
metadata: Metadata,
|
||||
}
|
||||
@@ -17,13 +16,12 @@ pub struct Metadata {
|
||||
pub theme: Option<tty::Theme>,
|
||||
}
|
||||
|
||||
impl<W> AsciicastEncoder<W>
|
||||
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<W> super::Encoder for AsciicastEncoder<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn start(&mut self, timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> {
|
||||
impl super::Encoder for AsciicastEncoder {
|
||||
fn start(&mut self, timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
|
||||
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<u8> {
|
||||
self.inner.event(&event)
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Vec<u8> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<u64>, 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<u64>, tty_size: tty::TtySize) -> Vec<u8>;
|
||||
fn event(&mut self, event: Event) -> Vec<u8>;
|
||||
fn finish(&mut self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
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 {
|
||||
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<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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
use crate::asciicast::{Event, EventData};
|
||||
use crate::tty;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct RawEncoder<W> {
|
||||
writer: W,
|
||||
pub struct RawEncoder {
|
||||
append: bool,
|
||||
}
|
||||
|
||||
impl<W> RawEncoder<W> {
|
||||
pub fn new(writer: W, append: bool) -> Self {
|
||||
RawEncoder { writer, append }
|
||||
impl RawEncoder {
|
||||
pub fn new(append: bool) -> Self {
|
||||
RawEncoder { append }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> super::Encoder for RawEncoder<W> {
|
||||
fn start(&mut self, _timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> {
|
||||
impl super::Encoder for RawEncoder {
|
||||
fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
|
||||
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<u8> {
|
||||
if let EventData::Output(data) = event.data {
|
||||
data.into_bytes()
|
||||
} else {
|
||||
Ok(())
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Vec<u8> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -39,20 +41,27 @@ mod tests {
|
||||
use crate::tty::TtySize;
|
||||
|
||||
#[test]
|
||||
fn encoder_impl() -> anyhow::Result<()> {
|
||||
let mut out: Vec<u8> = 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<W: Write> {
|
||||
writer: Option<W>,
|
||||
collector: Option<TextCollector<TextWriter<W>>>,
|
||||
pub struct TextEncoder {
|
||||
collector: Option<TextCollector>,
|
||||
}
|
||||
|
||||
impl<W: Write> TextEncoder<W> {
|
||||
pub fn new(writer: W) -> Self {
|
||||
TextEncoder {
|
||||
writer: Some(writer),
|
||||
collector: None,
|
||||
}
|
||||
impl TextEncoder {
|
||||
pub fn new() -> Self {
|
||||
TextEncoder { collector: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> super::Encoder for TextEncoder<W> {
|
||||
fn start(&mut self, _timestamp: Option<u64>, tty_size: &tty::TtySize) -> io::Result<()> {
|
||||
impl super::Encoder for TextEncoder {
|
||||
fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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> {
|
||||
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<u8> = 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
|
||||
type ExtraEnv = HashMap<String, String>;
|
||||
|
||||
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<S: AsRef<str>, T: Tty + ?Sized, H: Handler>(
|
||||
) -> Result<i32> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Box<dyn Output + Send>>,
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user