use crate::error::UnsafeInterval;
use crate::iter::IteratorExt;
use crate::{File, color, util::Colorized};
use clap::Parser;
use regex::Regex;
use std::error::Error;
use std::ffi::OsString;
use std::fmt::Display;
use std::io::{self, BufRead, Write, stdout};
use std::thread::sleep;
use std::time::Duration;
#[derive(Debug, Parser)]
#[command(version)]
struct Cli {
file: Vec<OsString>,
#[arg(short = 'l', long = "line-speed")]
lines_per_second: Option<f32>,
#[arg(short = 'c', long = "char-speed")]
chars_per_second: Option<f32>,
#[arg(short = 'r', long = "replace", num_args = 2, value_names = ["REGEXP", "REPLACE"])]
replace: Vec<String>,
#[arg(long = "fg", value_name = "NAME_OR_HEX")]
foreground: Option<color::Color>,
#[arg(long = "bg", value_name = "NAME_OR_HEX")]
background: Option<color::Color>,
}
pub fn run() -> Result<(), Box<dyn Error>> {
Cli::parse().run()
}
impl Cli {
#[inline]
#[must_use = "CLI run result should be handled"]
pub fn run(mut self) -> Result<(), Box<dyn Error>> {
if self.file.is_empty() {
self.file.push("-".into())
}
let mut stdout = stdout();
let replacers = self.replacers()?;
let line_wait = self
.lines_per_second
.map(|per_second| Self::safe_duration(per_second, "lines"))
.transpose()?
.unwrap_or(Duration::ZERO);
let char_wait = self
.chars_per_second
.map(|per_second| Self::safe_duration(per_second, "characters"))
.transpose()?
.unwrap_or(Duration::ZERO);
let line_iterators = self
.file
.iter()
.map(File::from)
.map(|f| f.into_reader())
.map_ok(|reader| reader.into_buf_reader())
.map_ok(|reader| reader.lines());
for lines in line_iterators {
for line in lines? {
let line = line.map(|s| Self::find_and_replace(s, &replacers))?;
if char_wait == Duration::ZERO {
self.wait_write(&mut stdout, &line, line_wait)?;
} else {
for c in line.chars() {
self.wait_write(&mut stdout, c, char_wait)?;
}
}
writeln!(stdout)?;
}
}
if line_wait == Duration::ZERO && char_wait == Duration::ZERO {
stdout.flush()?;
}
Ok(())
}
fn safe_duration(per_second: f32, unit: &'static str) -> Result<Duration, UnsafeInterval<f32>> {
if per_second <= 0f32 {
Err(UnsafeInterval::new(unit, per_second))
} else {
Ok(Duration::from_secs(1).div_f32(per_second))
}
}
fn wait_write<W, D>(&self, mut out: W, content: D, wait: Duration) -> io::Result<()>
where
W: Write,
D: Display,
{
let colorized = Colorized::new(content, &self.foreground, &self.background);
if wait >= Duration::ZERO {
sleep(wait);
write!(out, "{colorized}").and_then(|_| out.flush())
} else {
write!(out, "{colorized}")
}
}
fn replacers(&self) -> Result<Vec<FindAndReplace>, Box<dyn Error>> {
let results = self
.replace
.chunks_exact(2)
.map(|chunk| (Regex::new(&chunk[0]), chunk[1].clone()))
.map(|(result, string)| result.map(|re| (re, string)))
.collect::<Result<Vec<_>, _>>()?;
Ok(results)
}
fn find_and_replace<S>(text: S, replacers: &[FindAndReplace]) -> String
where
S: Into<String>,
{
replacers.iter().fold(text.into(), |text, (find, replace)| {
find.replace_all(&text, replace).to_string()
})
}
}
type FindAndReplace = (Regex, String);