Refactor metadata (header) handling

This commit is contained in:
Marcin Kulik
2024-11-12 13:46:38 +01:00
parent 4858c25b67
commit db956df97f
11 changed files with 131 additions and 115 deletions

View File

@@ -38,6 +38,21 @@ pub enum EventData {
Other(char, String),
}
impl Default for Header {
fn default() -> Self {
Self {
cols: 80,
rows: 24,
timestamp: None,
idle_time_limit: None,
command: None,
title: None,
env: None,
theme: None,
}
}
}
pub fn open_from_path<S: AsRef<Path>>(path: S) -> Result<Asciicast<'static>> {
fs::File::open(path)
.map(io::BufReader::new)

View File

@@ -1,5 +1,5 @@
use super::Command;
use crate::asciicast::{self, Header};
use crate::asciicast;
use crate::cli::{self, Format};
use crate::config::Config;
use crate::encoder::{self, AsciicastEncoder, EncoderExt, RawEncoder, TextEncoder};
@@ -12,7 +12,7 @@ impl Command for cli::Convert {
fn run(self, _config: &Config) -> Result<()> {
let path = util::get_local_path(&self.input_filename)?;
let cast = asciicast::open_from_path(&*path)?;
let mut encoder = self.get_encoder(&cast.header);
let mut encoder = self.get_encoder();
let mut file = self.open_file()?;
encoder.encode_to_file(cast, &mut file)
@@ -20,7 +20,7 @@ impl Command for cli::Convert {
}
impl cli::Convert {
fn get_encoder(&self, header: &Header) -> Box<dyn encoder::Encoder> {
fn get_encoder(&self) -> Box<dyn encoder::Encoder> {
let format = self.format.unwrap_or_else(|| {
if self.output_filename.to_lowercase().ends_with(".txt") {
Format::Txt
@@ -30,7 +30,7 @@ impl cli::Convert {
});
match format {
Format::Asciicast => Box::new(AsciicastEncoder::new(false, 0, header.into())),
Format::Asciicast => Box::new(AsciicastEncoder::new(false, 0)),
Format::Raw => Box::new(RawEncoder::new(false)),
Format::Txt => Box::new(TextEncoder::new()),
}

View File

@@ -1,14 +1,15 @@
use super::Command;
use crate::asciicast;
use crate::asciicast::Header;
use crate::cli;
use crate::config::Config;
use crate::encoder::{AsciicastEncoder, Encoder, Metadata, RawEncoder, TextEncoder};
use crate::encoder::{AsciicastEncoder, Encoder, 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 crate::tty::{self, FixedSizeTty};
use anyhow::{bail, Result};
use cli::Format;
use std::collections::{HashMap, HashSet};
@@ -34,6 +35,7 @@ impl Command for cli::Record {
let record_input = self.input || config.cmd_rec_input();
let exec_command = super::build_exec_command(command.as_ref().cloned());
let exec_extra_env = super::build_exec_extra_env(&[]);
let output = self.get_output(file, format, append, time_offset, config);
logger::info!("Recording session started, writing to {}", self.path);
@@ -43,8 +45,6 @@ impl Command for cli::Record {
{
let mut tty = self.get_tty()?;
let theme = tty.get_theme();
let output = self.get_output(file, format, append, time_offset, theme, config);
let mut recorder = recorder::Recorder::new(output, record_input, keys, notifier);
pty::exec(&exec_command, &exec_extra_env, &mut tty, &mut recorder)?;
}
@@ -170,20 +170,33 @@ impl cli::Record {
format: Format,
append: bool,
time_offset: u64,
theme: Option<tty::Theme>,
config: &Config,
) -> Box<dyn recorder::Output + Send> {
let metadata = self.build_asciicast_metadata(config);
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);
let writer = io::LineWriter::new(file);
let encoder = AsciicastEncoder::new(append, time_offset);
Box::new(FileOutput(file, encoder))
Box::new(FileOutput {
writer,
encoder,
metadata,
})
}
Format::Raw => Box::new(FileOutput(file, RawEncoder::new(append))),
Format::Txt => Box::new(FileOutput(file, TextEncoder::new())),
Format::Raw => Box::new(FileOutput {
writer: file,
encoder: RawEncoder::new(append),
metadata,
}),
Format::Txt => Box::new(FileOutput {
writer: file,
encoder: TextEncoder::new(),
metadata,
}),
}
}
@@ -191,7 +204,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) -> Metadata {
fn build_asciicast_metadata(&self, config: &Config) -> Metadata {
let idle_time_limit = self.idle_time_limit.or(config.cmd_rec_idle_time_limit());
let command = self.get_command(config);
@@ -207,25 +220,52 @@ impl cli::Record {
command,
title: self.title.clone(),
env: Some(capture_env(&env)),
theme,
}
}
}
struct FileOutput<W: Write, E: Encoder>(W, E);
struct FileOutput<W: Write, E: Encoder> {
writer: W,
encoder: E,
metadata: Metadata,
}
pub struct Metadata {
pub idle_time_limit: Option<f64>,
pub command: Option<String>,
pub title: Option<String>,
pub env: Option<HashMap<String, String>>,
}
impl<W: Write, E: Encoder> Output for FileOutput<W, E> {
fn header(&mut self, time: SystemTime, tty_size: tty::TtySize) -> io::Result<()> {
fn header(
&mut self,
time: SystemTime,
tty_size: tty::TtySize,
theme: Option<tty::Theme>,
) -> io::Result<()> {
let timestamp = time.duration_since(UNIX_EPOCH).unwrap().as_secs();
self.0.write_all(&self.1.start(Some(timestamp), tty_size))
let header = Header {
cols: tty_size.0,
rows: tty_size.1,
timestamp: Some(timestamp),
theme,
idle_time_limit: self.metadata.idle_time_limit,
command: self.metadata.command.as_ref().cloned(),
title: self.metadata.title.as_ref().cloned(),
env: self.metadata.env.as_ref().cloned(),
};
self.writer.write_all(&self.encoder.header(&header))
}
fn event(&mut self, event: asciicast::Event) -> io::Result<()> {
self.0.write_all(&self.1.event(event))
self.writer.write_all(&self.encoder.event(event))
}
fn flush(&mut self) -> io::Result<()> {
self.0.write_all(&self.1.finish())
self.writer.write_all(&self.encoder.flush())
}
}

View File

@@ -6,7 +6,7 @@ use crate::locale;
use crate::logger;
use crate::pty;
use crate::streamer::{self, KeyBindings};
use crate::tty::{self, FixedSizeTty, Tty};
use crate::tty::{self, FixedSizeTty};
use crate::util;
use anyhow::bail;
use anyhow::{anyhow, Context, Result};
@@ -80,7 +80,6 @@ impl Command for cli::Stream {
record_input,
keys,
notifier,
tty.get_theme(),
);
self.init_logging()?;

View File

@@ -1,52 +1,24 @@
use crate::asciicast::{Encoder, Event, Header};
use crate::tty;
use std::collections::HashMap;
pub struct AsciicastEncoder {
inner: Encoder,
append: bool,
metadata: Metadata,
}
pub struct Metadata {
pub idle_time_limit: Option<f64>,
pub command: Option<String>,
pub title: Option<String>,
pub env: Option<HashMap<String, String>>,
pub theme: Option<tty::Theme>,
}
impl AsciicastEncoder {
pub fn new(append: bool, time_offset: u64, metadata: Metadata) -> Self {
pub fn new(append: bool, time_offset: u64) -> Self {
let inner = Encoder::new(time_offset);
Self {
inner,
append,
metadata,
}
}
fn build_header(&self, timestamp: Option<u64>, tty_size: &tty::TtySize) -> Header {
Header {
cols: tty_size.0,
rows: tty_size.1,
timestamp,
idle_time_limit: self.metadata.idle_time_limit,
command: self.metadata.command.clone(),
title: self.metadata.title.clone(),
env: self.metadata.env.clone(),
theme: self.metadata.theme.clone(),
}
Self { inner, append }
}
}
impl super::Encoder for AsciicastEncoder {
fn start(&mut self, timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
fn header(&mut self, header: &Header) -> Vec<u8> {
if self.append {
Vec::new()
} else {
self.inner.header(&self.build_header(timestamp, &tty_size))
self.inner.header(header)
}
}
@@ -54,19 +26,7 @@ impl super::Encoder for AsciicastEncoder {
self.inner.event(&event)
}
fn finish(&mut self) -> Vec<u8> {
fn flush(&mut self) -> Vec<u8> {
Vec::new()
}
}
impl From<&Header> for Metadata {
fn from(header: &Header) -> Self {
Metadata {
idle_time_limit: header.idle_time_limit.as_ref().cloned(),
command: header.command.as_ref().cloned(),
title: header.title.as_ref().cloned(),
env: header.env.as_ref().cloned(),
theme: header.theme.as_ref().cloned(),
}
}
}

View File

@@ -2,21 +2,19 @@ mod asciicast;
mod raw;
mod txt;
pub use asciicast::AsciicastEncoder;
pub use asciicast::Metadata;
pub use raw::RawEncoder;
pub use txt::TextEncoder;
use crate::asciicast::Event;
use crate::tty;
use crate::asciicast::Header;
use anyhow::Result;
pub use asciicast::AsciicastEncoder;
pub use raw::RawEncoder;
use std::fs::File;
use std::io::Write;
pub use txt::TextEncoder;
pub trait Encoder {
fn start(&mut self, timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8>;
fn header(&mut self, header: &Header) -> Vec<u8>;
fn event(&mut self, event: Event) -> Vec<u8>;
fn finish(&mut self) -> Vec<u8>;
fn flush(&mut self) -> Vec<u8>;
}
pub trait EncoderExt {
@@ -25,14 +23,13 @@ pub trait EncoderExt {
impl<E: Encoder + ?Sized> EncoderExt for E {
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))?;
file.write_all(&self.header(&cast.header))?;
for event in cast.events {
file.write_all(&self.event(event?))?;
}
file.write_all(&self.finish())?;
file.write_all(&self.flush())?;
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::asciicast::{Event, EventData};
use crate::tty;
use crate::asciicast::{Event, EventData, Header};
pub struct RawEncoder {
append: bool,
@@ -12,11 +11,11 @@ impl RawEncoder {
}
impl super::Encoder for RawEncoder {
fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
fn header(&mut self, header: &Header) -> Vec<u8> {
if self.append {
Vec::new()
} else {
format!("\x1b[8;{};{}t", tty_size.1, tty_size.0).into_bytes()
format!("\x1b[8;{};{}t", header.rows, header.cols).into_bytes()
}
}
@@ -28,7 +27,7 @@ impl super::Encoder for RawEncoder {
}
}
fn finish(&mut self) -> Vec<u8> {
fn flush(&mut self) -> Vec<u8> {
Vec::new()
}
}
@@ -36,18 +35,20 @@ impl super::Encoder for RawEncoder {
#[cfg(test)]
mod tests {
use super::RawEncoder;
use crate::asciicast::Event;
use crate::asciicast::{Event, Header};
use crate::encoder::Encoder;
use crate::tty::TtySize;
#[test]
fn encoder() {
let mut enc = RawEncoder::new(false);
assert_eq!(
enc.start(None, TtySize(100, 50)),
"\x1b[8;50;100t".as_bytes()
);
let header = Header {
cols: 100,
rows: 50,
..Default::default()
};
assert_eq!(enc.header(&header), "\x1b[8;50;100t".as_bytes());
assert_eq!(
enc.event(Event::output(0, "he\x1b[1mllo\r\n".to_owned())),
@@ -62,6 +63,6 @@ mod tests {
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());
assert!(enc.flush().is_empty());
}
}

View File

@@ -1,5 +1,4 @@
use crate::asciicast::{Event, EventData};
use crate::tty;
use crate::asciicast::{Event, EventData, Header};
use avt::util::TextCollector;
pub struct TextEncoder {
@@ -13,9 +12,9 @@ impl TextEncoder {
}
impl super::Encoder for TextEncoder {
fn start(&mut self, _timestamp: Option<u64>, tty_size: tty::TtySize) -> Vec<u8> {
fn header(&mut self, header: &Header) -> Vec<u8> {
let vt = avt::Vt::builder()
.size(tty_size.0 as usize, tty_size.1 as usize)
.size(header.cols as usize, header.rows as usize)
.resizable(true)
.scrollback_limit(100)
.build();
@@ -39,7 +38,7 @@ impl super::Encoder for TextEncoder {
}
}
fn finish(&mut self) -> Vec<u8> {
fn flush(&mut self) -> Vec<u8> {
text_lines_to_bytes(self.collector.take().unwrap().flush().iter())
}
}
@@ -56,15 +55,20 @@ fn text_lines_to_bytes<S: AsRef<str>>(lines: impl Iterator<Item = S>) -> Vec<u8>
#[cfg(test)]
mod tests {
use super::TextEncoder;
use crate::asciicast::Event;
use crate::asciicast::{Event, Header};
use crate::encoder::Encoder;
use crate::tty::TtySize;
#[test]
fn encoder() {
let mut enc = TextEncoder::new();
assert!(enc.start(None, TtySize(3, 1)).is_empty());
let header = Header {
cols: 3,
rows: 1,
..Default::default()
};
assert!(enc.header(&header).is_empty());
assert!(enc
.event(Event::output(0, "he\x1b[1mllo\r\n".to_owned()))
@@ -74,6 +78,6 @@ mod tests {
.event(Event::output(1, "world\r\n".to_owned()))
.is_empty());
assert_eq!(enc.finish(), "hello\nworld\n".as_bytes());
assert_eq!(enc.flush(), "hello\nworld\n".as_bytes());
}
}

View File

@@ -1,5 +1,5 @@
use crate::io::set_non_blocking;
use crate::tty::{Tty, TtySize};
use crate::tty::{Theme, Tty, TtySize};
use anyhow::{bail, Result};
use nix::errno::Errno;
use nix::libc::EIO;
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
type ExtraEnv = HashMap<String, String>;
pub trait Handler {
fn start(&mut self, tty_size: TtySize);
fn start(&mut self, tty_size: TtySize, theme: Option<Theme>);
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(winsize.into());
handler.start(winsize.into(), tty.get_theme());
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
match result.fork_result {
@@ -377,7 +377,7 @@ impl Drop for SignalFd {
mod tests {
use super::Handler;
use crate::pty::ExtraEnv;
use crate::tty::{FixedSizeTty, NullTty, TtySize};
use crate::tty::{FixedSizeTty, NullTty, Theme, TtySize};
use std::time::Duration;
#[derive(Default)]
@@ -387,7 +387,7 @@ mod tests {
}
impl Handler for TestHandler {
fn start(&mut self, tty_size: TtySize) {
fn start(&mut self, tty_size: TtySize, _theme: Option<Theme>) {
self.tty_size = Some(tty_size);
}

View File

@@ -23,7 +23,12 @@ pub struct Recorder {
}
pub trait Output {
fn header(&mut self, time: SystemTime, tty_size: tty::TtySize) -> io::Result<()>;
fn header(
&mut self,
time: SystemTime,
tty_size: tty::TtySize,
theme: Option<tty::Theme>,
) -> io::Result<()>;
fn event(&mut self, event: Event) -> io::Result<()>;
fn flush(&mut self) -> io::Result<()>;
}
@@ -77,9 +82,9 @@ impl Recorder {
}
impl pty::Handler for Recorder {
fn start(&mut self, tty_size: tty::TtySize) {
fn start(&mut self, tty_size: tty::TtySize, theme: Option<tty::Theme>) {
let mut output = self.output.take().unwrap();
let _ = output.header(SystemTime::now(), tty_size);
let _ = output.header(SystemTime::now(), tty_size, theme);
let receiver = self.receiver.take().unwrap();
let mut notifier = self.notifier.take().unwrap();

View File

@@ -23,7 +23,6 @@ pub struct Streamer {
prefix_mode: bool,
listener: Option<net::TcpListener>,
forward_url: Option<url::Url>,
theme: Option<tty::Theme>,
// XXX: field (drop) order below is crucial for correct shutdown
pty_tx: mpsc::UnboundedSender<Event>,
notifier_tx: std::sync::mpsc::Sender<String>,
@@ -44,7 +43,6 @@ impl Streamer {
record_input: bool,
keys: KeyBindings,
notifier: Box<dyn Notifier>,
theme: Option<tty::Theme>,
) -> Self {
let (notifier_tx, notifier_rx) = std::sync::mpsc::channel();
let (pty_tx, pty_rx) = mpsc::unbounded_channel();
@@ -63,7 +61,6 @@ impl Streamer {
prefix_mode: false,
listener,
forward_url,
theme,
}
}
@@ -82,7 +79,7 @@ impl Streamer {
}
impl pty::Handler for Streamer {
fn start(&mut self, tty_size: tty::TtySize) {
fn start(&mut self, tty_size: tty::TtySize, theme: Option<tty::Theme>) {
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();
@@ -105,8 +102,6 @@ impl pty::Handler for Streamer {
))
});
let theme = self.theme.take();
self.event_loop_handle = wrap_thread_handle(thread::spawn(move || {
runtime.block_on(async move {
event_loop(pty_rx, &mut clients_rx, tty_size, theme).await;