use std::io::{self, IsTerminal, Read, Write};
use std::time::Duration;
use clap::{CommandFactory, Parser};
use rusty_pv::{PvBuilder, Reporter, UnitSystem, units};
mod cli;
mod mode;
mod strict;
use cli::{Cli, PvSubcommand};
use mode::CompatibilityMode;
fn main() {
let argv: Vec<String> = std::env::args().collect();
#[cfg(unix)]
{
let _ = unsafe {
signal_hook::low_level::register(signal_hook::consts::SIGPIPE, || {
std::process::exit(141);
})
};
}
let active_mode = mode::resolve(&argv);
let exit_code = match active_mode {
CompatibilityMode::Default => run_default(),
CompatibilityMode::Strict => run_strict(&argv[1..]),
};
std::process::exit(exit_code);
}
struct StderrReporter {
units: UnitSystem,
name: Option<String>,
show: DisplayMask,
numeric_emitted_100: bool,
quiet: bool,
}
#[derive(Default)]
struct DisplayMask {
progress: bool,
timer: bool,
eta: bool,
fineta: bool,
rate: bool,
bytes: bool,
average_rate: bool,
numeric: bool,
}
impl DisplayMask {
fn any(&self) -> bool {
self.progress
|| self.timer
|| self.eta
|| self.fineta
|| self.rate
|| self.bytes
|| self.average_rate
|| self.numeric
}
}
impl Reporter for StderrReporter {
fn report(&mut self, progress: &rusty_pv::Progress) {
if self.quiet {
return;
}
let mut stderr = io::stderr().lock();
if self.show.numeric {
let pct = match progress.bytes_total {
Some(total) if total > 0 => {
((progress.bytes_done as f64 / total as f64) * 100.0).clamp(0.0, 100.0) as u64
}
_ => 0,
};
if pct >= 100 {
if !self.numeric_emitted_100 {
let _ = writeln!(stderr, "100");
self.numeric_emitted_100 = true;
}
} else {
let _ = writeln!(stderr, "{pct}");
}
return;
}
let mut parts: Vec<String> = Vec::new();
if let Some(name) = &self.name {
parts.push(format!("{name}:"));
}
if self.show.progress {
parts.push(format_progress_bar(progress));
}
if self.show.timer {
parts.push(format_duration(progress.elapsed));
}
if self.show.fineta {
parts.push(format_fineta(progress));
} else if self.show.eta {
parts.push(format_eta(progress));
}
if self.show.rate {
parts.push(self.units.format_rate(progress.rate));
}
if self.show.bytes {
parts.push(self.units.format_bytes(progress.bytes_done));
}
if self.show.average_rate {
let avg = if progress.elapsed.as_secs_f64() > 0.0 {
progress.bytes_done as f64 / progress.elapsed.as_secs_f64()
} else {
0.0
};
parts.push(format!("[{}]", self.units.format_rate(avg)));
}
let line = parts.join(" ");
let _ = write!(stderr, "\r\x1B[K{line}");
let _ = stderr.flush();
}
}
fn format_progress_bar(progress: &rusty_pv::Progress) -> String {
match progress.bytes_total {
Some(total) if total > 0 => {
let pct = ((progress.bytes_done as f64 / total as f64) * 100.0).clamp(0.0, 100.0);
let width = 20usize;
let filled = ((pct / 100.0) * width as f64) as usize;
let bar: String = "=".repeat(filled) + &" ".repeat(width - filled);
format!("[{bar}] {pct:.0}%")
}
_ => "[?]".to_string(),
}
}
fn format_duration(d: Duration) -> String {
let secs = d.as_secs();
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
}
}
fn format_eta(progress: &rusty_pv::Progress) -> String {
match progress.eta {
Some(d) => format!("ETA {}", format_duration(d)),
None => "ETA ?".to_string(),
}
}
fn format_fineta(progress: &rusty_pv::Progress) -> String {
match progress.eta {
Some(d) => format!("FIN+{}", format_duration(d)),
None => "FIN ?".to_string(),
}
}
fn build_mask_from_cli(c: &Cli) -> DisplayMask {
let mut m = DisplayMask {
progress: c.progress,
timer: c.timer,
eta: c.eta,
fineta: c.fineta,
rate: c.rate,
bytes: c.bytes,
average_rate: c.average_rate,
numeric: c.numeric,
};
if !m.any() {
m.progress = true;
m.timer = true;
m.eta = true;
m.rate = true;
m.bytes = true;
}
m
}
fn build_mask_from_strict(s: &strict::StrictArgs) -> DisplayMask {
let mut m = DisplayMask {
progress: s.progress,
timer: s.timer,
eta: s.eta,
fineta: s.fineta,
rate: s.rate,
bytes: s.bytes,
average_rate: s.average_rate,
numeric: s.numeric,
};
if !m.any() {
m.progress = true;
m.timer = true;
m.eta = true;
m.rate = true;
m.bytes = true;
}
m
}
fn run_default() -> i32 {
let cli = Cli::parse();
if let Some(PvSubcommand::Completions { shell }) = cli.subcommand {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut io::stdout());
return 0;
}
let units = if cli.si_units {
UnitSystem::Si
} else {
UnitSystem::Iec
};
let tty = io::stderr().is_terminal();
let show_display = (tty || cli.force) && !cli.quiet;
let mask = build_mask_from_cli(&cli);
let total_bytes = match &cli.size {
Some(s) => units::parse_size(s, units),
None => None,
};
let rate_limit = cli
.rate_limit
.as_deref()
.and_then(|s| units::parse_size(s, units));
let buffer_size = cli
.buffer_size
.as_deref()
.and_then(|s| units::parse_size(s, units))
.map(|v| v as usize)
.unwrap_or(1 << 20);
let interval = Duration::from_secs_f64(cli.interval.unwrap_or(1.0).max(0.1));
let mut builder = PvBuilder::new().buffer_size(buffer_size).interval(interval);
if let Some(n) = total_bytes {
builder = builder.total_bytes(n);
}
if let Some(rl) = rate_limit {
builder = builder.rate_limit(rl);
}
if let Some(name) = cli.name.clone() {
builder = builder.name(name);
}
if show_display {
builder = builder.reporter(Box::new(StderrReporter {
units,
name: cli.name.clone(),
show: mask,
numeric_emitted_100: false,
quiet: cli.quiet,
}));
}
let pv = builder.build();
let result = if cli.paths.is_empty() {
let stdin = io::stdin();
let mut reader = stdin.lock();
let stdout = io::stdout();
let mut writer = stdout.lock();
pv.copy(&mut reader, &mut writer)
} else {
run_files(&cli.paths, pv)
};
if show_display && !cli.numeric {
let _ = writeln!(io::stderr());
}
match result {
Ok(_) => 0,
Err(e) => {
eprintln!("rusty-pv: {e}");
1
}
}
}
fn run_files(paths: &[String], pv: rusty_pv::Pv) -> Result<u64, rusty_pv::PvError> {
let first = &paths[0];
let mut reader: Box<dyn Read> =
Box::new(std::fs::File::open(first).map_err(|e| rusty_pv::PvError::Io { source: e })?);
let stdout = io::stdout();
let mut writer = stdout.lock();
pv.copy(&mut reader, &mut writer)
}
fn run_strict(args: &[String]) -> i32 {
let parsed = match strict::parse(args) {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
if parsed.help {
let mut cmd = Cli::command();
let _ = cmd.print_help();
return 0;
}
if parsed.version {
println!("rusty-pv {}", env!("CARGO_PKG_VERSION"));
return 0;
}
let units = if parsed.si_units {
UnitSystem::Si
} else {
UnitSystem::Iec
};
let mask = build_mask_from_strict(&parsed);
let tty = io::stderr().is_terminal();
let show_display = (tty || parsed.force) && !parsed.quiet;
let total_bytes = parsed
.size
.as_deref()
.and_then(|s| units::parse_size(s, units));
let rate_limit = parsed
.rate_limit
.as_deref()
.and_then(|s| units::parse_size(s, units));
let buffer_size = parsed
.buffer_size
.as_deref()
.and_then(|s| units::parse_size(s, units))
.map(|v| v as usize)
.unwrap_or(1 << 20);
let interval = Duration::from_secs_f64(parsed.interval.unwrap_or(1.0).max(0.1));
let mut builder = PvBuilder::new().buffer_size(buffer_size).interval(interval);
if let Some(n) = total_bytes {
builder = builder.total_bytes(n);
}
if let Some(rl) = rate_limit {
builder = builder.rate_limit(rl);
}
if let Some(name) = parsed.name.clone() {
builder = builder.name(name);
}
if show_display {
builder = builder.reporter(Box::new(StderrReporter {
units,
name: parsed.name.clone(),
show: mask,
numeric_emitted_100: false,
quiet: parsed.quiet,
}));
}
let pv = builder.build();
let result = if parsed.paths.is_empty() {
let stdin = io::stdin();
let mut reader = stdin.lock();
let stdout = io::stdout();
let mut writer = stdout.lock();
pv.copy(&mut reader, &mut writer)
} else {
run_files(&parsed.paths, pv)
};
if show_display && !parsed.numeric {
let _ = writeln!(io::stderr());
}
match result {
Ok(_) => 0,
Err(e) => {
eprintln!("rusty-pv: {e}");
1
}
}
}