use std::{
cmp::Ordering,
fmt::Debug,
io::{self, StdoutLock, Write},
time::{Duration, Instant},
};
use num_traits::AsPrimitive;
use crate::{fmt, math};
const DEFAULT_WIDTH: u64 = 70;
const DEFAULT_FILLED_CHARACTER: char = '=';
const DEFAULT_CURRENT_CHARACTER: char = '>';
const DEFAULT_REMAINING_CHARACTER: char = ' ';
const PULSE_INTERVAL: Duration = Duration::from_secs(1);
pub struct Progress {
width: u64,
filled_character: char,
current_character: char,
remaining_character: char,
total: u64,
current: u64,
stopped: bool,
tags: Vec<Tag>,
rate_count: u64,
previous_rate: u64,
instants: [Option<Instant>; 101],
pulse_instant: Instant,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Tag {
Tps,
Eta,
Time,
}
impl Progress {
#[must_use]
pub fn new(total: impl AsPrimitive<u64>) -> Self {
let total = total.as_();
assert_ne!(total, 0, "Total cannot be zero.");
let now = Instant::now();
let mut instants = [None; 101];
instants[0] = Some(now);
let progress = Progress {
width: DEFAULT_WIDTH,
filled_character: DEFAULT_FILLED_CHARACTER,
current_character: DEFAULT_CURRENT_CHARACTER,
remaining_character: DEFAULT_REMAINING_CHARACTER,
total,
current: 0,
stopped: false,
tags: Vec::new(),
rate_count: 0,
previous_rate: 0,
instants,
pulse_instant: now,
};
progress.draw(0, 0, None, Duration::ZERO);
progress
}
#[inline]
pub fn set_width(&mut self, width: impl AsPrimitive<u64>) {
let width = width.as_();
assert_ne!(width, 0, "Width cannot be zero.");
self.width = width;
}
#[inline]
#[must_use]
pub fn with_width(mut self, width: impl AsPrimitive<u64>) -> Self {
self.set_width(width);
self
}
#[inline]
pub fn set_filled_character(&mut self, filled_character: char) {
self.filled_character = filled_character;
}
#[inline]
#[must_use]
pub fn with_filled_character(mut self, filled_character: char) -> Self {
self.set_filled_character(filled_character);
self
}
#[inline]
pub fn set_current_character(&mut self, current_character: char) {
self.current_character = current_character;
}
#[inline]
#[must_use]
pub fn with_current_character(mut self, current_character: char) -> Self {
self.set_current_character(current_character);
self
}
#[inline]
pub fn set_remaining_character(&mut self, remaining_character: char) {
self.remaining_character = remaining_character;
}
#[inline]
#[must_use]
pub fn with_remaining_character(mut self, remaining_character: char) -> Self {
self.set_remaining_character(remaining_character);
self
}
#[inline]
pub fn set_tag(&mut self, tag: Tag) {
assert!(
!self.tags.contains(&tag),
"Progress tag {tag:?} is already enabled.",
);
self.tags.push(tag);
}
#[inline]
#[must_use]
pub fn with_tag(mut self, tag: Tag) -> Self {
self.set_tag(tag);
self
}
#[inline]
#[must_use]
pub fn is_complete(&self) -> bool {
self.current == self.total
}
#[inline]
pub fn tick(&mut self, value: impl AsPrimitive<u64>) {
self.set(self.current + value.as_());
}
fn set(&mut self, value: u64) {
assert!(!self.stopped, "Progress bar has been stopped.");
assert!(
value <= self.total,
"Progress value ({value}) larger than total ({}).",
self.total,
);
let previous = self.current;
self.current = value;
let amount = self.get_progress_amount(self.current) as u8;
let previous_amount = self.get_progress_amount(previous) as u8;
let now = Instant::now();
let pulse_duration = self.pulse(&now);
let rate = self.get_rate(pulse_duration);
if amount == previous_amount && amount != 100 && pulse_duration.is_none() {
return;
}
for index in (previous_amount + 1)..=amount {
self.instants[index as usize] = Some(now);
}
self.draw(
amount,
rate,
self.get_eta(&now),
now - self.instants[0].unwrap(),
);
self.stopped = amount == 100;
}
#[inline]
pub fn stop(&mut self) {
if self.stopped {
return;
}
self.stopped = true;
let now = Instant::now();
let amount = self.get_progress_amount(self.current) as u8;
self.draw_final(amount, now - self.instants[0].unwrap());
}
#[must_use]
fn pulse(&mut self, now: &Instant) -> Option<Duration> {
let duration = now.duration_since(self.pulse_instant);
if duration >= PULSE_INTERVAL {
self.pulse_instant = *now;
return Some(duration);
}
None
}
#[must_use]
fn get_progress_amount(&self, current: u64) -> f64 {
100.0 * current as f64 / self.total as f64
}
#[must_use]
fn get_progress_position(&self, amount: u8) -> u64 {
(self.width as f64 * amount as f64 / 100.0) as u64
}
#[must_use]
fn get_rate(&mut self, pulse_duration: Option<Duration>) -> u64 {
self.rate_count += 1;
if let Some(pulse_duration) = pulse_duration {
let ms = pulse_duration.as_millis() as f64;
let rate = self.rate_count as f64 / (ms / 1000.0);
self.previous_rate = rate as u64;
self.rate_count = 0;
return rate.round() as u64;
}
self.previous_rate
}
#[must_use]
fn get_eta(&self, now: &Instant) -> Option<Duration> {
let amount = self.get_progress_amount(self.current);
let elapsed = now.duration_since(self.instants[0].unwrap());
if amount as u8 == 100 || elapsed.is_zero() {
return None;
}
let x = amount * 2.0 - 100.0;
let x1 = *math::min(&[x, 98.0]).unwrap() as i64;
if x1 <= 0 || self.instants[x1 as usize].is_none() {
let rate = self.current as f64 / elapsed.as_millis() as f64;
if rate == 0.0 {
return None;
}
let duration_ms = ((self.total - self.current) as f64 / rate) as u64;
let duration = Duration::from_millis(duration_ms);
return Some(duration);
}
let x2 = x1 as usize + 1;
let y1 = self.instants[x1 as usize].unwrap();
let y2 = self.instants[x2].unwrap();
let m = y2 - y1;
let b = y1 - m * x1 as u32;
Some(*now - (b + Duration::from_millis((m.as_millis() as f64 * x) as u64)))
}
fn draw(&self, amount: u8, rate: u64, eta: Option<Duration>, elapsed: Duration) {
if amount == 100 {
return self.draw_final(amount, elapsed);
}
let mut lock = io::stdout().lock();
let position = self.get_progress_position(amount);
write!(lock, "\x1B[2K\r[").unwrap();
for i in 0..self.width {
let character = match i.cmp(&position) {
Ordering::Less => self.filled_character,
Ordering::Greater => self.remaining_character,
Ordering::Equal => self.current_character,
};
write!(lock, "\x1B[33m{character}\x1B[0m").unwrap();
}
write!(lock, "] \x1B[33m{amount} %\x1B[0m").unwrap();
for tag in &self.tags {
match tag {
Tag::Tps => {
if rate > 0 {
print_rate(&mut lock, rate);
}
},
Tag::Eta => {
if eta.is_some_and(|eta| !eta.is_zero()) {
print_eta(&mut lock, eta.unwrap());
}
},
Tag::Time => {
if !elapsed.is_zero() {
print_time(&mut lock, elapsed);
}
},
}
}
write!(lock, "\r").unwrap();
lock.flush().unwrap();
}
fn draw_final(&self, amount: u8, elapsed: Duration) {
let mut lock = io::stdout().lock();
let position = self.get_progress_position(amount);
write!(lock, "\x1B[2K[").unwrap();
for i in 0..self.width {
let character = match i.cmp(&position) {
Ordering::Less => self.filled_character,
Ordering::Greater => self.remaining_character,
Ordering::Equal => self.current_character,
};
if amount < 100 {
write!(lock, "\x1B[31m{character}\x1B[0m").unwrap();
} else {
write!(lock, "\x1B[32m{character}\x1B[0m").unwrap();
}
}
if amount < 100 {
write!(lock, "] \x1B[31m{amount} %\x1B[0m").unwrap();
} else {
write!(lock, "] \x1B[32m{amount} %\x1B[0m").unwrap();
}
if self.tags.contains(&Tag::Time) {
print_time(&mut lock, elapsed);
}
writeln!(lock).unwrap();
lock.flush().unwrap();
}
}
fn print_rate(lock: &mut StdoutLock, rate: u64) {
write!(lock, " ({} tps)", fmt::number(rate),).unwrap();
}
fn print_eta(lock: &mut StdoutLock, eta: Duration) {
write!(lock, " (eta {})", fmt::timespan(eta.as_millis()),).unwrap();
}
fn print_time(lock: &mut StdoutLock, elapsed: Duration) {
write!(lock, " (time {})", fmt::timespan(elapsed.as_millis()),).unwrap();
}