use std::sync::Arc;
use parking_lot::RwLock;
use crossterm::{style::Colorize};
use crossterm::tty::IsTty;
use std::time::{Duration, SystemTime};
use crate::logging::{Logger, LoggerRefreshItemKind, LoggerTextItem};
#[derive(Clone, Copy, PartialEq)]
pub enum ProgressBarStyle {
Download,
Action,
}
#[derive(Clone)]
pub struct ProgressBar {
id: usize,
start_time: SystemTime,
progress_bars: ProgressBars,
message: String,
size: usize,
style: ProgressBarStyle,
pos: Arc<RwLock<usize>>,
}
impl ProgressBar {
pub fn set_position(&self, new_pos: usize) {
let mut pos = self.pos.write();
*pos = new_pos;
}
pub fn finish(&self) {
self.progress_bars.finish_progress(self.id);
}
}
#[derive(Clone)]
pub struct ProgressBars {
logger: Logger,
state: Arc<RwLock<InternalState>>,
}
struct InternalState {
drawer_id: usize,
progress_bar_counter: usize,
progress_bars: Vec<ProgressBar>,
}
impl ProgressBars {
pub fn are_supported() -> bool {
std::io::stderr().is_tty() && crate::terminal::get_terminal_width().is_some()
}
pub fn new(logger: &Logger) -> Option<Self> {
if ProgressBars::are_supported() {
Some(ProgressBars {
logger: logger.clone(),
state: Arc::new(RwLock::new(InternalState {
drawer_id: 0,
progress_bar_counter: 0,
progress_bars: Vec::new(),
})),
})
} else {
None
}
}
pub fn add_progress(&self, message: String, style: ProgressBarStyle, total_size: usize) -> ProgressBar {
let mut internal_state = self.state.write();
let id = internal_state.progress_bar_counter;
let pb = ProgressBar {
id,
progress_bars: self.clone(),
start_time: SystemTime::now(),
message,
size: total_size,
style,
pos: Arc::new(RwLock::new(0))
};
internal_state.progress_bars.push(pb.clone());
internal_state.progress_bar_counter += 1;
if internal_state.progress_bars.len() == 1 {
self.start_draw_thread(&mut internal_state);
}
pb
}
fn finish_progress(&self, progress_bar_id: usize) {
let mut internal_state = self.state.write();
if let Some(index) = internal_state.progress_bars.iter().position(|p| p.id == progress_bar_id) {
internal_state.progress_bars.remove(index);
}
if internal_state.progress_bars.is_empty() {
self.logger.remove_refresh_item(LoggerRefreshItemKind::ProgressBars)
}
}
fn start_draw_thread(&self, internal_state: &mut InternalState) {
internal_state.drawer_id += 1;
let drawer_id = internal_state.drawer_id;
let internal_state = self.state.clone();
let logger = self.logger.clone();
std::thread::spawn(move || {
loop {
{
let internal_state = internal_state.read();
if internal_state.drawer_id != drawer_id || internal_state.progress_bars.is_empty() {
break;
}
let terminal_width = crate::terminal::get_terminal_width().unwrap();
let mut text = String::new();
for (i, progress_bar) in internal_state.progress_bars.iter().enumerate() {
if i > 0 { text.push_str("\n"); }
text.push_str(&progress_bar.message);
text.push_str("\n");
text.push_str(&get_progress_bar_text(
terminal_width,
*progress_bar.pos.read(),
progress_bar.size,
progress_bar.style,
progress_bar.start_time.elapsed().unwrap()
));
}
logger.set_refresh_item(LoggerRefreshItemKind::ProgressBars, vec![LoggerTextItem::Text(text)]);
}
std::thread::sleep(Duration::from_millis(100));
}
});
}
}
fn get_progress_bar_text(terminal_width: u16, pos: usize, total: usize, pb_style: ProgressBarStyle, duration: Duration) -> String {
let total = std::cmp::max(pos, total);
let bytes_text = if pb_style == ProgressBarStyle::Download {
format!(" {}/{}", get_bytes_text(pos, total), get_bytes_text(total, total))
} else {
String::new()
};
let elapsed_text = get_elapsed_text(duration);
let mut text = String::new();
text.push_str(&elapsed_text);
let percent = pos as f32 / total as f32;
let total_bars = (std::cmp::min(50, terminal_width - 15) as usize) - elapsed_text.len() - 1 - 2;
let completed_bars = (total_bars as f32 * percent).floor() as usize;
text.push_str(" [");
if completed_bars != total_bars {
if completed_bars > 0 {
text.push_str(&format!("{}", format!("{}{}", "#".repeat(completed_bars - 1), ">").cyan()))
}
text.push_str(&format!("{}", "-".repeat(total_bars - completed_bars).blue()))
} else {
text.push_str(&format!("{}", "#".repeat(completed_bars).cyan()))
}
text.push_str("]");
text.push_str(&bytes_text);
text
}
fn get_bytes_text(byte_count: usize, total_bytes: usize) -> String {
let bytes_to_kb = 1_000;
let bytes_to_mb = 1_000_000;
return if total_bytes < bytes_to_mb {
get_in_format(byte_count, bytes_to_kb, "KB")
} else {
get_in_format(byte_count, bytes_to_mb, "MB")
};
fn get_in_format(byte_count: usize, conversion: usize, suffix: &str) -> String {
let converted_value = byte_count / conversion;
let decimal = (byte_count % conversion) * 100 / conversion;
format!("{}.{:0>2}{}", converted_value, decimal, suffix)
}
}
fn get_elapsed_text(elapsed: Duration) -> String {
let elapsed_secs = elapsed.as_secs();
let seconds = elapsed_secs % 60;
let minutes = (elapsed_secs / 60) % 60;
let hours = (elapsed_secs / 60) / 60;
format!("[{:0>2}:{:0>2}:{:0>2}]", hours, minutes, seconds)
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::*;
#[test]
fn it_should_get_bytes_text() {
assert_eq!(get_bytes_text(9, 999), "0.00KB");
assert_eq!(get_bytes_text(10, 999), "0.01KB");
assert_eq!(get_bytes_text(100, 999), "0.10KB");
assert_eq!(get_bytes_text(200, 999), "0.20KB");
assert_eq!(get_bytes_text(520, 999), "0.52KB");
assert_eq!(get_bytes_text(1000, 10_000), "1.00KB");
assert_eq!(get_bytes_text(10_000, 10_000), "10.00KB");
assert_eq!(get_bytes_text(999_999, 990_999), "999.99KB");
assert_eq!(get_bytes_text(1_000_000, 1_000_000), "1.00MB");
assert_eq!(get_bytes_text(9_524_102, 10_000_000), "9.52MB");
}
#[test]
fn it_should_get_elapsed_text() {
assert_eq!(get_elapsed_text(Duration::from_secs(1)), "[00:00:01]");
assert_eq!(get_elapsed_text(Duration::from_secs(20)), "[00:00:20]");
assert_eq!(get_elapsed_text(Duration::from_secs(59)), "[00:00:59]");
assert_eq!(get_elapsed_text(Duration::from_secs(60)), "[00:01:00]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 5 + 23)), "[00:05:23]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 59 + 59)), "[00:59:59]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60)), "[01:00:00]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60 * 3 + 20 * 60 + 2)), "[03:20:02]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60 * 99)), "[99:00:00]");
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60 * 120)), "[120:00:00]");
}
}