diff --git a/examples/download.rs b/examples/download.rs index beb629ce..85ca6884 100644 --- a/examples/download.rs +++ b/examples/download.rs @@ -11,6 +11,7 @@ fn main() { let pb = ProgressBar::new(total_size); pb.set_style(ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .with_key("eta", |state| format!("{:.1}s", state.eta().as_secs_f64())) .progress_chars("#>-")); while downloaded < total_size { diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 933de01a..34e82ff3 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -187,7 +187,9 @@ impl ProgressBar { pub fn set_draw_rate(&self, n: u64) { let mut state = self.state.lock().unwrap(); state.draw_rate = n; - state.draw_next = state.pos.saturating_add(state.per_sec() / n); + state.draw_next = state + .pos + .saturating_add((state.per_sec() / n as f64) as u64); } /// Manually ticks the spinner or progress bar @@ -477,7 +479,7 @@ impl ProgressBar { } /// Returns the current rate of progress - pub fn per_sec(&self) -> u64 { + pub fn per_sec(&self) -> f64 { self.state.lock().unwrap().per_sec() } diff --git a/src/state.rs b/src/state.rs index 01518b43..cc547c24 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,10 +8,10 @@ use crate::draw_target::{ProgressDrawState, ProgressDrawTarget}; use crate::style::{ProgressFinish, ProgressStyle}; /// The state of a progress bar at a moment in time. -pub(crate) struct ProgressState { +pub struct ProgressState { pub(crate) style: ProgressStyle, - pub(crate) pos: u64, - pub(crate) len: u64, + pub pos: u64, + pub len: u64, pub(crate) tick: u64, pub(crate) started: Instant, pub(crate) draw_target: ProgressDrawTarget, @@ -82,11 +82,6 @@ impl ProgressState { pct.max(0.0).min(1.0) } - /// Returns the position of the status bar as `(pos, len)` tuple. - pub fn position(&self) -> (u64, u64) { - (self.pos, self.len) - } - /// Returns the current message of the progress bar. pub fn message(&self) -> &str { &self.message @@ -120,12 +115,12 @@ impl ProgressState { } /// The number of steps per second - pub fn per_sec(&self) -> u64 { - let avg_time = self.est.seconds_per_step(); - if avg_time == 0.0 { - 0 + pub fn per_sec(&self) -> f64 { + let per_sec = 1.0 / self.est.seconds_per_step(); + if per_sec.is_nan() { + 0.0 } else { - (1.0 / avg_time) as u64 + per_sec } } @@ -157,7 +152,7 @@ impl ProgressState { } if new_pos >= self.draw_next { self.draw_next = new_pos.saturating_add(if self.draw_rate != 0 { - self.per_sec() / self.draw_rate + (self.per_sec() / self.draw_rate as f64) as u64 } else { self.draw_delta }); diff --git a/src/style.rs b/src/style.rs index e3db4983..d8f41c64 100644 --- a/src/style.rs +++ b/src/style.rs @@ -4,12 +4,24 @@ use crate::format::{BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, Hu use crate::state::ProgressState; use crate::utils::{expand_template, pad_str}; use std::borrow::Cow; +use std::collections::HashMap; #[cfg(feature = "improved_unicode")] use unicode_segmentation::UnicodeSegmentation; +pub type Format = fn(&ProgressState) -> String; + +#[derive(Clone)] +pub(crate) struct FormatMap(HashMap<&'static str, Format>); + +impl Default for FormatMap { + fn default() -> Self { + FormatMap(HashMap::new()) + } +} + /// Controls the rendering style of progress bars -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ProgressStyle { tick_strings: Vec>, progress_chars: Vec>, @@ -17,6 +29,7 @@ pub struct ProgressStyle { on_finish: ProgressFinish, // how unicode-big each char in progress_chars is char_width: usize, + format_map: FormatMap, } #[cfg(feature = "improved_unicode")] @@ -70,6 +83,7 @@ impl ProgressStyle { char_width, template: "{wide_bar} {pos}/{len}".into(), on_finish: ProgressFinish::default(), + format_map: FormatMap::default(), } } @@ -86,6 +100,7 @@ impl ProgressStyle { char_width, template: "{spinner} {msg}".into(), on_finish: ProgressFinish::default(), + format_map: FormatMap::default(), } } @@ -129,6 +144,12 @@ impl ProgressStyle { self } + /// Adds a custom key that references a `&ProgressState` to the template + pub fn with_key(mut self, key: &'static str, f: Format) -> ProgressStyle { + self.format_map.0.insert(key, f); + self + } + /// Sets the template string for the progress bar /// /// Review the [list of template keys](./index.html#templates) for more information. @@ -226,48 +247,57 @@ impl ProgressStyle { } pub(crate) fn format_state(&self, state: &ProgressState) -> Vec { - let (pos, len) = state.position(); let mut rv = vec![]; for line in self.template.lines() { let mut wide_element = None; - let s = expand_template(line, |var| match var.key { - "wide_bar" => { - wide_element = Some(var.duplicate_for_key("bar")); - "\x00".into() - } - "bar" => self.format_bar( - state.fraction(), - var.width.unwrap_or(20), - var.alt_style.as_ref(), - ), - "spinner" => state.current_tick_str().to_string(), - "wide_msg" => { - wide_element = Some(var.duplicate_for_key("msg")); - "\x00".into() + let s = expand_template(line, |var| { + if let Some(formatter) = self.format_map.0.get(var.key) { + formatter(state) + } else { + match var.key { + "wide_bar" => { + wide_element = Some(var.duplicate_for_key("bar")); + "\x00".into() + } + "bar" => self.format_bar( + state.fraction(), + var.width.unwrap_or(20), + var.alt_style.as_ref(), + ), + "spinner" => state.current_tick_str().to_string(), + "wide_msg" => { + wide_element = Some(var.duplicate_for_key("msg")); + "\x00".into() + } + "msg" => state.message().to_string(), + "prefix" => state.prefix().to_string(), + "pos" => state.pos.to_string(), + "len" => state.len.to_string(), + "percent" => format!("{:.*}", 0, state.fraction() * 100f32), + "bytes" => format!("{}", HumanBytes(state.pos)), + "total_bytes" => format!("{}", HumanBytes(state.len)), + "decimal_bytes" => format!("{}", DecimalBytes(state.pos)), + "decimal_total_bytes" => format!("{}", DecimalBytes(state.len)), + "binary_bytes" => format!("{}", BinaryBytes(state.pos)), + "binary_total_bytes" => format!("{}", BinaryBytes(state.len)), + "elapsed_precise" => { + format!("{}", FormattedDuration(state.started.elapsed())) + } + "elapsed" => format!("{:#}", HumanDuration(state.started.elapsed())), + "per_sec" => format!("{}/s", state.per_sec()), + "bytes_per_sec" => format!("{}/s", HumanBytes(state.per_sec() as u64)), + "binary_bytes_per_sec" => { + format!("{}/s", BinaryBytes(state.per_sec() as u64)) + } + "eta_precise" => format!("{}", FormattedDuration(state.eta())), + "eta" => format!("{:#}", HumanDuration(state.eta())), + "duration_precise" => format!("{}", FormattedDuration(state.duration())), + "duration" => format!("{:#}", HumanDuration(state.duration())), + _ => "".into(), + } } - "msg" => state.message().to_string(), - "prefix" => state.prefix().to_string(), - "pos" => pos.to_string(), - "len" => len.to_string(), - "percent" => format!("{:.*}", 0, state.fraction() * 100f32), - "bytes" => format!("{}", HumanBytes(state.pos)), - "total_bytes" => format!("{}", HumanBytes(state.len)), - "decimal_bytes" => format!("{}", DecimalBytes(state.pos)), - "decimal_total_bytes" => format!("{}", DecimalBytes(state.len)), - "binary_bytes" => format!("{}", BinaryBytes(state.pos)), - "binary_total_bytes" => format!("{}", BinaryBytes(state.len)), - "elapsed_precise" => format!("{}", FormattedDuration(state.started.elapsed())), - "elapsed" => format!("{:#}", HumanDuration(state.started.elapsed())), - "per_sec" => format!("{}/s", state.per_sec()), - "bytes_per_sec" => format!("{}/s", HumanBytes(state.per_sec())), - "binary_bytes_per_sec" => format!("{}/s", BinaryBytes(state.per_sec())), - "eta_precise" => format!("{}", FormattedDuration(state.eta())), - "eta" => format!("{:#}", HumanDuration(state.eta())), - "duration_precise" => format!("{}", FormattedDuration(state.duration())), - "duration" => format!("{:#}", HumanDuration(state.duration())), - _ => "".into(), }); rv.push(if let Some(ref var) = wide_element {