use std::io::{IsTerminal, Write};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::Mutex;
#[derive(Clone)]
pub struct ProgressConfig {
pub total: usize,
pub tty_update_frequency: usize,
pub non_tty_update_frequency: usize,
pub show_elapsed: bool,
pub show_eta: bool,
pub show_rate: bool,
pub prefix: String,
pub show_details: bool,
}
impl Default for ProgressConfig {
fn default() -> Self {
Self {
total: 0,
tty_update_frequency: 5,
non_tty_update_frequency: 1,
show_elapsed: true,
show_eta: true,
show_rate: true,
prefix: String::new(),
show_details: true,
}
}
}
pub struct ProgressTracker {
config: ProgressConfig,
progress: Arc<Mutex<usize>>,
start_time: Instant,
is_tty: bool,
}
impl ProgressTracker {
pub fn new(total: usize) -> Self {
Self::with_config(ProgressConfig {
total,
..Default::default()
})
}
pub fn with_config(config: ProgressConfig) -> Self {
Self {
config,
progress: Arc::new(Mutex::new(0)),
start_time: Instant::now(),
is_tty: std::io::stderr().is_terminal(),
}
}
pub fn handle(&self) -> ProgressHandle {
ProgressHandle {
config: self.config.clone(),
progress: Arc::clone(&self.progress),
start_time: self.start_time,
is_tty: self.is_tty,
}
}
pub async fn increment(&self, message: Option<&str>) {
let mut p = self.progress.lock().await;
*p += 1;
let current = *p;
drop(p);
self.display_progress(current, message).await;
}
pub async fn set(&self, value: usize, message: Option<&str>) {
let mut p = self.progress.lock().await;
*p = value;
drop(p);
self.display_progress(value, message).await;
}
pub async fn current(&self) -> usize {
*self.progress.lock().await
}
async fn display_progress(&self, current: usize, message: Option<&str>) {
let update_freq = if self.is_tty {
self.config.tty_update_frequency
} else {
self.config.non_tty_update_frequency
};
let is_multiple = update_freq != 0 && current % update_freq == 0;
if !is_multiple && current != self.config.total {
return;
}
let elapsed = self.start_time.elapsed().as_secs();
let percentage = if self.config.total > 0 {
(current as f64 / self.config.total as f64) * 100.0
} else {
0.0
};
if self.is_tty {
let mut output = format!(
"\r{}{}/{} ({:.1}%)",
self.config.prefix, current, self.config.total, percentage
);
if self.config.show_rate && elapsed > 0 {
let rate = current as f64 / elapsed as f64;
output.push_str(&format!(" | {:.1}/s", rate));
}
if self.config.show_eta && elapsed > 0 && current > 0 {
let rate = current as f64 / elapsed as f64;
let remaining = self.config.total.saturating_sub(current);
let eta_secs = (remaining as f64 / rate) as u64;
let eta_mins = eta_secs / 60;
output.push_str(&format!(" | ETA: {}m{}s", eta_mins, eta_secs % 60));
}
if self.config.show_elapsed {
output.push_str(&format!(" | Elapsed: {}s", elapsed));
}
if self.config.show_details {
if let Some(msg) = message {
output.push_str(&format!(" | {}", msg));
}
}
output.push_str(" ");
eprint!("{}", output);
std::io::stderr().flush().ok();
} else {
let mut output = format!("[{}/{}] ({:.1}%)", current, self.config.total, percentage);
if !self.config.prefix.is_empty() {
output = format!("{} {}", self.config.prefix, output);
}
if self.config.show_elapsed {
output.push_str(&format!(" | Elapsed: {}s", elapsed));
}
if self.config.show_details {
if let Some(msg) = message {
output.push_str(&format!(" | {}", msg));
}
}
eprintln!("{}", output);
}
}
pub async fn finish(&self) {
if self.is_tty {
eprintln!(); }
let elapsed = self.start_time.elapsed();
eprintln!(
"✅ Completed {} items in {:.2}s",
self.config.total,
elapsed.as_secs_f64()
);
}
}
#[derive(Clone)]
pub struct ProgressHandle {
config: ProgressConfig,
progress: Arc<Mutex<usize>>,
start_time: Instant,
is_tty: bool,
}
impl ProgressHandle {
pub async fn increment(&self, message: Option<&str>) {
let mut p = self.progress.lock().await;
*p += 1;
let current = *p;
drop(p);
self.display_progress(current, message).await;
}
pub async fn current(&self) -> usize {
*self.progress.lock().await
}
async fn display_progress(&self, current: usize, message: Option<&str>) {
let update_freq = if self.is_tty {
self.config.tty_update_frequency
} else {
self.config.non_tty_update_frequency
};
let is_multiple = update_freq != 0 && current % update_freq == 0;
if !is_multiple && current != self.config.total {
return;
}
let elapsed = self.start_time.elapsed().as_secs();
let percentage = if self.config.total > 0 {
(current as f64 / self.config.total as f64) * 100.0
} else {
0.0
};
if self.is_tty {
let mut output = format!(
"\r{}{}/{} ({:.1}%)",
self.config.prefix, current, self.config.total, percentage
);
if self.config.show_rate && elapsed > 0 {
let rate = current as f64 / elapsed as f64;
output.push_str(&format!(" | {:.1}/s", rate));
}
if self.config.show_eta && elapsed > 0 && current > 0 {
let rate = current as f64 / elapsed as f64;
let remaining = self.config.total.saturating_sub(current);
let eta_secs = (remaining as f64 / rate) as u64;
let eta_mins = eta_secs / 60;
output.push_str(&format!(" | ETA: {}m{}s", eta_mins, eta_secs % 60));
}
if self.config.show_elapsed {
output.push_str(&format!(" | Elapsed: {}s", elapsed));
}
if self.config.show_details {
if let Some(msg) = message {
output.push_str(&format!(" | {}", msg));
}
}
output.push_str(" ");
eprint!("{}", output);
std::io::stderr().flush().ok();
} else {
let mut output = format!("[{}/{}] ({:.1}%)", current, self.config.total, percentage);
if !self.config.prefix.is_empty() {
output = format!("{} {}", self.config.prefix, output);
}
if self.config.show_elapsed {
output.push_str(&format!(" | Elapsed: {}s", elapsed));
}
if self.config.show_details {
if let Some(msg) = message {
output.push_str(&format!(" | {}", msg));
}
}
eprintln!("{}", output);
}
}
}