use std::time::Instant;
use bon::Builder;
use crate::{other::Timelike, print_rolling};
#[derive(Builder, Clone, Debug)]
pub struct ProgressBar {
total: usize,
#[builder(default = 40)]
width: usize,
#[builder(default = '█')]
fill: char,
#[builder(default = '░')]
empty: char,
#[builder(default)]
prefix: String,
#[builder(skip)]
started: Option<Instant>,
}
impl ProgressBar {
pub fn new(total: usize) -> Self {
Self::builder().total(total).build()
}
pub fn init(&mut self) {
self.started.get_or_insert_with(Instant::now);
self.render(0);
}
pub fn progress(&mut self, i: usize) {
self.started.get_or_insert_with(Instant::now);
self.render(i);
if i >= self.total {
eprintln!();
}
}
pub fn finish(&mut self) {
let started = *self.started.get_or_insert_with(Instant::now);
let elapsed = started.elapsed().as_secs_f64();
let prefix = if self.prefix.is_empty() { String::new() } else { format!("{} ", self.prefix) };
print_rolling!(
"{prefix}▕{}▏ 100% finished in {}",
str::repeat(&self.fill.to_string(), self.width),
Timelike(elapsed.ceil() as u32)
);
eprintln!();
}
fn render(&self, i: usize) {
let started = self.started.expect("render called before started was set");
let finished = i >= self.total;
let ratio = if self.total == 0 { 1.0 } else { (i as f64 / self.total as f64).min(1.0) };
let filled = (ratio * self.width as f64) as usize;
let empty = self.width - filled;
let pct = (ratio * 100.0) as u32;
let eta = if finished {
let elapsed = started.elapsed().as_secs_f64();
format!(" finished in {}", Timelike(elapsed.ceil() as u32))
} else if i > 0 {
let elapsed = started.elapsed().as_secs_f64();
let remaining = elapsed * (self.total.saturating_sub(i)) as f64 / i as f64;
format!(" ETA {}", Timelike(remaining.ceil() as u32))
} else {
" ETA ?".to_string()
};
let prefix = if self.prefix.is_empty() { String::new() } else { format!("{} ", self.prefix) };
print_rolling!(
"{prefix}▕{}{}▏ {pct}%{eta}",
str::repeat(&self.fill.to_string(), filled),
str::repeat(&self.empty.to_string(), empty)
);
}
}