Implement --append and --overwrite for rec

This commit is contained in:
Marcin Kulik
2023-10-27 22:25:28 +02:00
parent 0a3e6ecd8b
commit d0aa467d81
9 changed files with 164 additions and 224 deletions

View File

@@ -3,7 +3,8 @@
## 3.0.0 (wip)
* Full rewrite in Rust
* Use of `--append` and `--overwrite` together gives returns error now
* rec: `--append` can be used with `--raw` now
* rec: use of `--append` and `--overwrite` together returns error now
## 2.4.0 (2023-10-23)

62
Cargo.lock generated
View File

@@ -66,7 +66,6 @@ dependencies = [
"nix",
"serde",
"serde_json",
"tempfile",
"termion",
]
@@ -134,22 +133,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "errno"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "heck"
version = "0.4.1"
@@ -168,12 +151,6 @@ version = "0.2.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
[[package]]
name = "log"
version = "0.4.20"
@@ -236,35 +213,13 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall 0.2.16",
]
[[package]]
name = "rustix"
version = "0.38.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"redox_syscall",
]
[[package]]
@@ -321,19 +276,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix",
"windows-sys",
]
[[package]]
name = "termion"
version = "2.0.1"
@@ -342,7 +284,7 @@ checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
dependencies = [
"libc",
"numtoa",
"redox_syscall 0.2.16",
"redox_syscall",
"redox_termios",
]

View File

@@ -18,6 +18,3 @@ termion = "2.0.1"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
clap = { version = "4.4.7", features = ["derive"] }
[dev-dependencies]
tempfile = "3.8.0"

View File

@@ -7,8 +7,8 @@ use std::io::BufRead;
use std::io::Write;
use std::path::Path;
pub struct Writer {
file: fs::File,
pub struct Writer<W: Write> {
writer: W,
time_offset: f64,
}
@@ -99,46 +99,25 @@ pub fn get_duration<S: AsRef<Path>>(path: S) -> anyhow::Result<f64> {
Ok(time)
}
impl Writer {
pub fn new<S: AsRef<Path>>(path: S, append: bool) -> anyhow::Result<Self> {
if append {
Self::append(path)
} else {
Self::create(path)
impl<W> Writer<W>
where
W: Write,
{
pub fn new(writer: W, time_offset: f64) -> Self {
Self {
writer,
time_offset,
}
}
pub fn create<S: AsRef<Path>>(path: S) -> anyhow::Result<Self> {
let file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path)?;
Ok(Self {
file,
time_offset: 0.0,
})
}
pub fn append<S: AsRef<Path>>(path: S) -> anyhow::Result<Self> {
let time_offset = get_duration(&path)?;
let file = fs::OpenOptions::new().append(true).open(path)?;
Ok(Self { file, time_offset })
}
pub fn write_header(&mut self, header: &Header) -> io::Result<()> {
if self.time_offset == 0.0 {
write_header(&mut self.file, header)
} else {
Ok(())
}
write_header(&mut self.writer, header)
}
pub fn write_event(&mut self, mut event: Event) -> io::Result<()> {
event.time += self.time_offset;
write_event(&mut self.file, &event)
write_event(&mut self.writer, &event)
}
}
@@ -215,9 +194,8 @@ impl From<V2Header> for Header {
#[cfg(test)]
mod tests {
use super::{Event, EventCode, Header, Writer};
use std::fs::{self, File};
use std::fs::File;
use std::io;
use tempfile::tempdir;
#[test]
fn open() {
@@ -246,38 +224,39 @@ mod tests {
#[test]
fn writer() {
let tmp_dir = tempdir().unwrap();
let tmp_path = tmp_dir.path().join("test.cast");
let mut data = Vec::new();
{
let header = Header {
terminal_size: (80, 24),
idle_time_limit: None,
};
let cursor = io::Cursor::new(&mut data);
let mut fw = Writer::new(cursor, 0.0);
let mut fw = Writer::create(&tmp_path).unwrap();
let header = Header {
terminal_size: (80, 24),
idle_time_limit: None,
};
fw.write_header(&header).unwrap();
fw.write_header(&header).unwrap();
fw.write_event(Event {
time: 1.0,
code: EventCode::Output,
data: "hello\r\n".to_owned(),
})
.unwrap();
}
fw.write_event(Event {
time: 1.0,
code: EventCode::Output,
data: "hello\r\n".to_owned(),
})
.unwrap();
{
let mut fw = Writer::append(&tmp_path).unwrap();
let data_len = data.len() as u64;
let mut cursor = io::Cursor::new(&mut data);
cursor.set_position(data_len);
let mut fw = Writer::new(cursor, 1.0);
fw.write_event(Event {
time: 1.0,
code: EventCode::Output,
data: "world".to_owned(),
})
.unwrap();
}
fw.write_event(Event {
time: 1.0,
code: EventCode::Output,
data: "world".to_owned(),
})
.unwrap();
assert_eq!(fs::read_to_string(tmp_path).unwrap(), "{\"version\":2,\"width\":80,\"height\":24}\n[1.0,\"o\",\"hello\\r\\n\"]\n[2.0,\"o\",\"world\"]\n");
let asciicast = String::from_utf8(data).unwrap();
assert_eq!(asciicast, "{\"version\":2,\"width\":80,\"height\":24}\n[1.0,\"o\",\"hello\\r\\n\"]\n[2.0,\"o\",\"world\"]\n");
}
}

9
src/format.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod asciicast;
pub mod raw;
use std::io;
pub trait Writer {
fn header(&mut self, size: (u16, u16)) -> io::Result<()>;
fn output(&mut self, time: f64, data: &[u8]) -> io::Result<()>;
fn input(&mut self, time: f64, data: &[u8]) -> io::Result<()>;
}

39
src/format/asciicast.rs Normal file
View File

@@ -0,0 +1,39 @@
use crate::asciicast;
use std::io::{self, Write};
pub struct Writer<W: Write> {
inner: asciicast::Writer<W>,
}
impl<W> Writer<W>
where
W: Write,
{
pub fn new(writer: W, time_offset: f64) -> Self {
Writer {
inner: asciicast::Writer::new(writer, time_offset),
}
}
}
impl<W> super::Writer for Writer<W>
where
W: Write,
{
fn header(&mut self, size: (u16, u16)) -> io::Result<()> {
let header = asciicast::Header {
terminal_size: (size.0 as usize, size.1 as usize),
idle_time_limit: None,
};
self.inner.write_header(&header)
}
fn output(&mut self, time: f64, data: &[u8]) -> io::Result<()> {
self.inner.write_event(asciicast::Event::output(time, data))
}
fn input(&mut self, time: f64, data: &[u8]) -> io::Result<()> {
self.inner.write_event(asciicast::Event::input(time, data))
}
}

25
src/format/raw.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::io::{self, Write};
pub struct Writer<W> {
writer: W,
}
impl<W> Writer<W> {
pub fn new(writer: W) -> Self {
Writer { writer }
}
}
impl<W: Write> super::Writer for Writer<W> {
fn header(&mut self, size: (u16, u16)) -> io::Result<()> {
write!(self.writer, "\x1b[8;{};{}t", size.1, size.0)
}
fn output(&mut self, _time: f64, data: &[u8]) -> io::Result<()> {
self.writer.write_all(data)
}
fn input(&mut self, _time: f64, _data: &[u8]) -> io::Result<()> {
Ok(())
}
}

View File

@@ -1,9 +1,12 @@
mod asciicast;
mod format;
mod pty;
mod recorder;
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::env;
use std::fs;
use std::path;
#[derive(Debug, Parser)]
#[clap(author, version, about)]
@@ -109,7 +112,7 @@ fn main() -> Result<()> {
Commands::Record {
filename,
stdin,
append,
mut append,
raw,
overwrite,
command,
@@ -120,13 +123,33 @@ fn main() -> Result<()> {
rows,
quiet,
} => {
let format = if raw {
recorder::Format::Raw
let exists = path::Path::new(&filename).exists();
append = append && exists;
let mut opts = fs::OpenOptions::new();
opts.write(true)
.append(append)
.create_new(!overwrite && !append)
.truncate(overwrite);
let writer: Box<dyn format::Writer> = if raw {
let file = opts.open(&filename)?;
Box::new(format::raw::Writer::new(file))
} else {
recorder::Format::Asciicast
let writer = if append {
let time_offset = asciicast::get_duration(&filename)?;
let file = opts.open(&filename)?;
format::asciicast::Writer::new(file, time_offset)
} else {
let file = opts.open(&filename)?;
format::asciicast::Writer::new(file, 0.0)
};
Box::new(writer)
};
let mut recorder = recorder::new(filename, format, append, stdin)?;
let mut recorder = recorder::Recorder::new(writer, append, stdin);
let command = if command == "$SHELL" {
env::var("SHELL").ok().unwrap_or("/bin/sh".to_owned())

View File

@@ -1,52 +1,25 @@
use crate::asciicast;
use crate::format;
use crate::pty;
use std::fs::{self, File};
use std::io::{self, Write};
use std::io;
use std::time;
pub struct Recorder {
writer: Box<dyn FileWriter>,
writer: Box<dyn format::Writer>,
append: bool,
record_input: bool,
start_time: time::Instant,
}
trait FileWriter {
fn header(&mut self, size: (u16, u16)) -> io::Result<()>;
fn output(&mut self, time: f64, data: &[u8]) -> io::Result<()>;
fn input(&mut self, time: f64, data: &[u8]) -> io::Result<()>;
}
pub enum Format {
Asciicast,
Raw,
}
struct RawWriter {
file: File,
append: bool,
}
pub fn new<S: Into<String>>(
path: S,
format: Format,
append: bool,
record_input: bool,
) -> anyhow::Result<Recorder> {
let path = path.into();
let writer: Box<dyn FileWriter> = match format {
Format::Asciicast => Box::new(asciicast::Writer::new(path, append)?),
Format::Raw => Box::new(RawWriter::new(path, append)?),
};
Ok(Recorder {
writer,
record_input,
start_time: time::Instant::now(),
})
}
impl Recorder {
pub fn new(writer: Box<dyn format::Writer>, append: bool, record_input: bool) -> Self {
Recorder {
writer,
append,
record_input,
start_time: time::Instant::now(),
}
}
fn elapsed_time(&self) -> f64 {
self.start_time.elapsed().as_secs_f64()
}
@@ -55,7 +28,12 @@ impl Recorder {
impl pty::Recorder for Recorder {
fn start(&mut self, size: (u16, u16)) -> io::Result<()> {
self.start_time = time::Instant::now();
self.writer.header(size)
if !self.append {
self.writer.header(size)
} else {
Ok(())
}
}
fn output(&mut self, data: &[u8]) {
@@ -70,56 +48,3 @@ impl pty::Recorder for Recorder {
}
}
}
impl FileWriter for asciicast::Writer {
fn header(&mut self, size: (u16, u16)) -> io::Result<()> {
let header = asciicast::Header {
terminal_size: (size.0 as usize, size.1 as usize),
idle_time_limit: None,
};
self.write_header(&header)
}
fn output(&mut self, time: f64, data: &[u8]) -> io::Result<()> {
self.write_event(asciicast::Event::output(time, data))
}
fn input(&mut self, time: f64, data: &[u8]) -> io::Result<()> {
self.write_event(asciicast::Event::input(time, data))
}
}
impl RawWriter {
fn new(path: String, append: bool) -> io::Result<Self> {
let mut opts = fs::OpenOptions::new();
if append {
opts.append(true);
} else {
opts.create_new(true).write(true);
}
let file = opts.open(path)?;
Ok(Self { file, append })
}
}
impl FileWriter for RawWriter {
fn header(&mut self, size: (u16, u16)) -> io::Result<()> {
if self.append {
Ok(())
} else {
write!(self.file, "\x1b[8;{};{}t", size.1, size.0)
}
}
fn output(&mut self, _time: f64, data: &[u8]) -> io::Result<()> {
self.file.write_all(data)
}
fn input(&mut self, _time: f64, _data: &[u8]) -> io::Result<()> {
Ok(())
}
}