use std::io::{self, IsTerminal, Write};
use std::time::Duration;
use colored::Colorize;
pub struct Progress {
enabled: bool,
label_width: usize,
}
impl Progress {
pub fn new(commands: &[String]) -> Self {
Self {
enabled: io::stderr().is_terminal(),
label_width: commands
.iter()
.map(|c| c.chars().count())
.max()
.unwrap_or(0)
.min(40),
}
}
pub fn warmup(&self, command: &str, position: (usize, usize), current: u32, total: u32) {
self.render(
&self.prefix(position),
command,
"warmup",
current,
total,
None,
);
}
pub fn run(
&self,
command: &str,
position: (usize, usize),
current: u32,
total: u32,
mean_so_far: Option<Duration>,
) {
self.render(
&self.prefix(position),
command,
"run",
current,
total,
mean_so_far,
);
}
pub fn finish(&self) {
if self.enabled {
eprint!("\r\x1b[2K");
let _ = io::stderr().flush();
}
}
fn prefix(&self, (command_index, total_commands): (usize, usize)) -> String {
if total_commands > 1 {
format!("[{}/{}] ", command_index + 1, total_commands)
} else {
String::new()
}
}
fn render(
&self,
prefix: &str,
command: &str,
phase: &str,
current: u32,
total: u32,
mean_so_far: Option<Duration>,
) {
if !self.enabled {
return;
}
let label = truncate(command, self.label_width);
let eta = mean_so_far
.filter(|_| current < total)
.map(|mean| {
format!(
" (~{:.1}s left)",
mean.as_secs_f64() * (total - current) as f64
)
})
.unwrap_or_default();
eprint!(
"\r\x1b[2K{}{} {} {}/{}{}",
prefix.dimmed(),
label.bold(),
phase.dimmed(),
current,
total,
eta.dimmed()
);
let _ = io::stderr().flush();
}
}
fn truncate(s: &str, max: usize) -> String {
if max == 0 || s.chars().count() <= max {
return s.to_string();
}
let mut truncated: String = s.chars().take(max.saturating_sub(1)).collect();
truncated.push('\u{2026}');
truncated
}