use std::{
io::{stdout, Write},
time::{Duration, Instant},
};
pub struct Progress {
start_time: Instant,
end_time: Instant,
last_tick_time: Instant,
pub total: usize,
current: usize,
interval: ProgressInterval,
prefix: String,
autoprint: bool,
finished: bool,
same_line: bool,
}
pub enum ProgressInterval {
Time(Duration),
Count(usize),
Percent(usize),
}
impl ProgressInterval {
fn should_update(&self, progress: &Progress) -> bool {
match self {
ProgressInterval::Time(interval) => {
let now = Instant::now();
&now.duration_since(progress.last_tick_time) > interval
}
ProgressInterval::Count(interval) => progress.current % interval == 0,
ProgressInterval::Percent(interval) => {
let current_percent = (100 * progress.current) / progress.total;
current_percent % interval == 0
}
}
}
}
impl Progress {
pub fn new(total: usize, interval: ProgressInterval) -> Progress {
let now = Instant::now();
Progress {
start_time: now,
end_time: now, total,
current: 0,
interval,
prefix: "".to_string(),
finished: false,
last_tick_time: now,
autoprint: true,
same_line: true,
}
}
pub fn set_prefix(&mut self, msg: String) {
self.prefix = msg;
}
pub fn set_autoprint(&mut self, enabled: bool) {
self.autoprint = enabled;
}
pub fn set_same_line(&mut self, same_line: bool) {
self.same_line = same_line;
}
pub fn set(&mut self, new_current: usize) -> bool {
let mut should_print = false;
if !self.finished {
let prev = self.current;
self.current = new_current;
should_print = self.post_update(prev);
}
should_print
}
pub fn inc(&mut self) -> bool {
self.set(self.current + 1)
}
fn post_update(&mut self, prev: usize) -> bool {
let mut should_print = false;
if self.should_update(prev) {
self.last_tick_time = Instant::now();
should_print = true;
if self.autoprint {
self.print();
}
}
if self.current >= self.total {
self.finished = true;
self.end_time = Instant::now();
if self.autoprint {
println!("{}", self.finish_msg());
}
}
should_print
}
pub fn current_count(&self) -> usize {
self.current
}
fn should_update(&self, prev_count: usize) -> bool {
let still_running = !self.finished;
let just_finished = self.current >= self.total;
let on_interval_boundary = self.interval.should_update(self);
let just_started = prev_count == 0;
still_running && (just_started || just_finished || on_interval_boundary)
}
fn print(&self) {
if self.same_line {
print!("{}\r{}", termion::clear::CurrentLine, self.msg());
stdout().flush().expect("stdout flush failed");
} else {
println!("{}", self.msg());
}
}
pub fn msg(&self) -> String {
let complete_char = "\u{2588}";
let incomplete_char = "\u{2591}";
let percent = (100.0 * self.current as f64 / self.total as f64) as usize;
let scaled_percent = percent / 4;
let bar = format!(
"{}{}",
complete_char.repeat(scaled_percent),
incomplete_char.repeat(25 - scaled_percent)
);
format!(
"{msg}{current_pad}{current}/{total}: {bar} {percent_pad}{percent}% ({elapsed} elapsed, {eta} remaining, {hz:.2}/s)",
msg = self.prefix,
current_pad = if self.current > 0 {
" ".repeat((self.total.ilog10() - self.current.ilog10()) as usize)
} else { "".to_string() },
current = self.current,
total = self.total,
percent = percent,
percent_pad = if percent > 0 {
" ".repeat(2 - percent.ilog10() as usize)
} else { "".to_string() },
eta = humantime::format_duration(self.time_estimate()),
elapsed = humantime::format_duration(Duration::from_secs(
self.start_time.elapsed().as_secs()
)),
hz = self.current as f32 / self.start_time.elapsed().as_secs_f32()
)
}
pub fn finish_msg(&self) -> String {
format!(
"{}{msg}finished in {dur}",
if self.same_line { "\n" } else { "" },
msg = self.prefix,
dur = humantime::format_duration(Duration::from_secs(
self.end_time.duration_since(self.start_time).as_secs()
)),
)
}
fn time_estimate(&self) -> Duration {
let remaining_count = self.total - self.current;
let elapsed_secs = self.start_time.elapsed().as_secs_f64();
Duration::from_secs((remaining_count as f64 * elapsed_secs / self.current as f64) as u64)
}
}
#[cfg(test)]
mod tests {
use std::thread;
use super::*;
#[test]
fn over_increment() {
let mut pb = Progress::new(4, ProgressInterval::Count(2));
for _ in 0..100 {
pb.inc();
}
}
#[test]
fn count_interval_test() {
let mut pb = Progress::new(10, ProgressInterval::Count(2));
let mut msgs = Vec::new();
for _ in 0..10 {
if pb.inc() {
msgs.push(pb.msg());
}
}
assert_eq!(msgs.len(), 6);
}
#[test]
fn percent_interval_test() {
let mut pb = Progress::new(10, ProgressInterval::Percent(50));
let mut msgs = Vec::new();
for _ in 0..10 {
if pb.inc() {
msgs.push(pb.msg());
}
}
assert_eq!(msgs.len(), 3);
}
#[test]
fn time_interval_test() {
let mut pb = Progress::new(8, ProgressInterval::Time(Duration::from_millis(50)));
let mut msgs = Vec::new();
for _ in 0..8 {
thread::sleep(Duration::from_millis(30));
if pb.inc() {
msgs.push(pb.msg());
}
}
assert_eq!(msgs.len(), 5);
}
}