Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
This commit is contained in:
561
vendor/indicatif/src/draw_target.rs
vendored
Normal file
561
vendor/indicatif/src/draw_target.rs
vendored
Normal file
@ -0,0 +1,561 @@
|
||||
use std::io;
|
||||
use std::sync::{Arc, RwLock, RwLockWriteGuard};
|
||||
use std::thread::panicking;
|
||||
use std::time::Duration;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Instant;
|
||||
|
||||
use console::Term;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use instant::Instant;
|
||||
|
||||
use crate::multi::{MultiProgressAlignment, MultiState};
|
||||
use crate::TermLike;
|
||||
|
||||
/// Target for draw operations
|
||||
///
|
||||
/// This tells a progress bar or a multi progress object where to paint to.
|
||||
/// The draw target is a stateful wrapper over a drawing destination and
|
||||
/// internally optimizes how often the state is painted to the output
|
||||
/// device.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgressDrawTarget {
|
||||
kind: TargetKind,
|
||||
}
|
||||
|
||||
impl ProgressDrawTarget {
|
||||
/// Draw to a buffered stdout terminal at a max of 20 times a second.
|
||||
///
|
||||
/// For more information see [`ProgressDrawTarget::term`].
|
||||
pub fn stdout() -> Self {
|
||||
Self::term(Term::buffered_stdout(), 20)
|
||||
}
|
||||
|
||||
/// Draw to a buffered stderr terminal at a max of 20 times a second.
|
||||
///
|
||||
/// This is the default draw target for progress bars. For more
|
||||
/// information see [`ProgressDrawTarget::term`].
|
||||
pub fn stderr() -> Self {
|
||||
Self::term(Term::buffered_stderr(), 20)
|
||||
}
|
||||
|
||||
/// Draw to a buffered stdout terminal at a max of `refresh_rate` times a second.
|
||||
///
|
||||
/// For more information see [`ProgressDrawTarget::term`].
|
||||
pub fn stdout_with_hz(refresh_rate: u8) -> Self {
|
||||
Self::term(Term::buffered_stdout(), refresh_rate)
|
||||
}
|
||||
|
||||
/// Draw to a buffered stderr terminal at a max of `refresh_rate` times a second.
|
||||
///
|
||||
/// For more information see [`ProgressDrawTarget::term`].
|
||||
pub fn stderr_with_hz(refresh_rate: u8) -> Self {
|
||||
Self::term(Term::buffered_stderr(), refresh_rate)
|
||||
}
|
||||
|
||||
pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
|
||||
Self {
|
||||
kind: TargetKind::Multi { state, idx },
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw to a terminal, with a specific refresh rate.
|
||||
///
|
||||
/// Progress bars are by default drawn to terminals however if the
|
||||
/// terminal is not user attended the entire progress bar will be
|
||||
/// hidden. This is done so that piping to a file will not produce
|
||||
/// useless escape codes in that file.
|
||||
///
|
||||
/// Will panic if refresh_rate is `0`.
|
||||
pub fn term(term: Term, refresh_rate: u8) -> Self {
|
||||
Self {
|
||||
kind: TargetKind::Term {
|
||||
term,
|
||||
last_line_count: 0,
|
||||
rate_limiter: RateLimiter::new(refresh_rate),
|
||||
draw_state: DrawState::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw to a boxed object that implements the [`TermLike`] trait.
|
||||
pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
|
||||
Self {
|
||||
kind: TargetKind::TermLike {
|
||||
inner: term_like,
|
||||
last_line_count: 0,
|
||||
rate_limiter: None,
|
||||
draw_state: DrawState::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw to a boxed object that implements the [`TermLike`] trait,
|
||||
/// with a specific refresh rate.
|
||||
pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
|
||||
Self {
|
||||
kind: TargetKind::TermLike {
|
||||
inner: term_like,
|
||||
last_line_count: 0,
|
||||
rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
|
||||
draw_state: DrawState::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A hidden draw target.
|
||||
///
|
||||
/// This forces a progress bar to be not rendered at all.
|
||||
pub fn hidden() -> Self {
|
||||
Self {
|
||||
kind: TargetKind::Hidden,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the draw target is hidden.
|
||||
///
|
||||
/// This is internally used in progress bars to figure out if overhead
|
||||
/// from drawing can be prevented.
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self.kind {
|
||||
TargetKind::Hidden => true,
|
||||
TargetKind::Term { ref term, .. } => !term.is_term(),
|
||||
TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current width of the draw target.
|
||||
pub(crate) fn width(&self) -> u16 {
|
||||
match self.kind {
|
||||
TargetKind::Term { ref term, .. } => term.size().1,
|
||||
TargetKind::Multi { ref state, .. } => state.read().unwrap().width(),
|
||||
TargetKind::Hidden => 0,
|
||||
TargetKind::TermLike { ref inner, .. } => inner.width(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the backing `MultiProgress` (if applicable) that the associated progress bar should
|
||||
/// be marked a zombie.
|
||||
pub(crate) fn mark_zombie(&self) {
|
||||
if let TargetKind::Multi { idx, state } = &self.kind {
|
||||
state.write().unwrap().mark_zombie(*idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the given draw state (draws it).
|
||||
pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
|
||||
match &mut self.kind {
|
||||
TargetKind::Term {
|
||||
term,
|
||||
last_line_count,
|
||||
rate_limiter,
|
||||
draw_state,
|
||||
} => {
|
||||
if !term.is_term() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match force_draw || rate_limiter.allow(now) {
|
||||
true => Some(Drawable::Term {
|
||||
term,
|
||||
last_line_count,
|
||||
draw_state,
|
||||
}),
|
||||
false => None, // rate limited
|
||||
}
|
||||
}
|
||||
TargetKind::Multi { idx, state, .. } => {
|
||||
let state = state.write().unwrap();
|
||||
Some(Drawable::Multi {
|
||||
idx: *idx,
|
||||
state,
|
||||
force_draw,
|
||||
now,
|
||||
})
|
||||
}
|
||||
TargetKind::TermLike {
|
||||
inner,
|
||||
last_line_count,
|
||||
rate_limiter,
|
||||
draw_state,
|
||||
} => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) {
|
||||
true => Some(Drawable::TermLike {
|
||||
term_like: &**inner,
|
||||
last_line_count,
|
||||
draw_state,
|
||||
}),
|
||||
false => None, // rate limited
|
||||
},
|
||||
// Hidden, finished, or no need to refresh yet
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Properly disconnects from the draw target
|
||||
pub(crate) fn disconnect(&self, now: Instant) {
|
||||
match self.kind {
|
||||
TargetKind::Term { .. } => {}
|
||||
TargetKind::Multi { idx, ref state, .. } => {
|
||||
let state = state.write().unwrap();
|
||||
let _ = Drawable::Multi {
|
||||
state,
|
||||
idx,
|
||||
force_draw: true,
|
||||
now,
|
||||
}
|
||||
.clear();
|
||||
}
|
||||
TargetKind::Hidden => {}
|
||||
TargetKind::TermLike { .. } => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
|
||||
match &self.kind {
|
||||
TargetKind::Multi { state, idx } => Some((state, *idx)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
|
||||
self.kind.adjust_last_line_count(adjust);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TargetKind {
|
||||
Term {
|
||||
term: Term,
|
||||
last_line_count: usize,
|
||||
rate_limiter: RateLimiter,
|
||||
draw_state: DrawState,
|
||||
},
|
||||
Multi {
|
||||
state: Arc<RwLock<MultiState>>,
|
||||
idx: usize,
|
||||
},
|
||||
Hidden,
|
||||
TermLike {
|
||||
inner: Box<dyn TermLike>,
|
||||
last_line_count: usize,
|
||||
rate_limiter: Option<RateLimiter>,
|
||||
draw_state: DrawState,
|
||||
},
|
||||
}
|
||||
|
||||
impl TargetKind {
|
||||
/// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
|
||||
fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
|
||||
let last_line_count: &mut usize = match self {
|
||||
Self::Term {
|
||||
last_line_count, ..
|
||||
} => last_line_count,
|
||||
Self::TermLike {
|
||||
last_line_count, ..
|
||||
} => last_line_count,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match adjust {
|
||||
LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
|
||||
LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Drawable<'a> {
|
||||
Term {
|
||||
term: &'a Term,
|
||||
last_line_count: &'a mut usize,
|
||||
draw_state: &'a mut DrawState,
|
||||
},
|
||||
Multi {
|
||||
state: RwLockWriteGuard<'a, MultiState>,
|
||||
idx: usize,
|
||||
force_draw: bool,
|
||||
now: Instant,
|
||||
},
|
||||
TermLike {
|
||||
term_like: &'a dyn TermLike,
|
||||
last_line_count: &'a mut usize,
|
||||
draw_state: &'a mut DrawState,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Drawable<'a> {
|
||||
/// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
|
||||
pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
|
||||
let last_line_count: &mut usize = match self {
|
||||
Drawable::Term {
|
||||
last_line_count, ..
|
||||
} => last_line_count,
|
||||
Drawable::TermLike {
|
||||
last_line_count, ..
|
||||
} => last_line_count,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match adjust {
|
||||
LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
|
||||
LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
|
||||
let mut state = match self {
|
||||
Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
|
||||
Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
|
||||
Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
|
||||
};
|
||||
|
||||
state.reset();
|
||||
state
|
||||
}
|
||||
|
||||
pub(crate) fn clear(mut self) -> io::Result<()> {
|
||||
let state = self.state();
|
||||
drop(state);
|
||||
self.draw()
|
||||
}
|
||||
|
||||
pub(crate) fn draw(self) -> io::Result<()> {
|
||||
match self {
|
||||
Drawable::Term {
|
||||
term,
|
||||
last_line_count,
|
||||
draw_state,
|
||||
} => draw_state.draw_to_term(term, last_line_count),
|
||||
Drawable::Multi {
|
||||
mut state,
|
||||
force_draw,
|
||||
now,
|
||||
..
|
||||
} => state.draw(force_draw, None, now),
|
||||
Drawable::TermLike {
|
||||
term_like,
|
||||
last_line_count,
|
||||
draw_state,
|
||||
} => draw_state.draw_to_term(term_like, last_line_count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum LineAdjust {
|
||||
/// Adds to `last_line_count` so that the next draw also clears those lines
|
||||
Clear(usize),
|
||||
/// Subtracts from `last_line_count` so that the next draw retains those lines
|
||||
Keep(usize),
|
||||
}
|
||||
|
||||
pub(crate) struct DrawStateWrapper<'a> {
|
||||
state: &'a mut DrawState,
|
||||
orphan_lines: Option<&'a mut Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'a> DrawStateWrapper<'a> {
|
||||
pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
|
||||
Self {
|
||||
state,
|
||||
orphan_lines: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<String>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
orphan_lines: Some(orphan_lines),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DrawStateWrapper<'_> {
|
||||
type Target = DrawState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for DrawStateWrapper<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DrawStateWrapper<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(orphaned) = &mut self.orphan_lines {
|
||||
orphaned.extend(self.state.lines.drain(..self.state.orphan_lines_count));
|
||||
self.state.orphan_lines_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RateLimiter {
|
||||
interval: u16, // in milliseconds
|
||||
capacity: u8,
|
||||
prev: Instant,
|
||||
}
|
||||
|
||||
/// Rate limit but allow occasional bursts above desired rate
|
||||
impl RateLimiter {
|
||||
fn new(rate: u8) -> Self {
|
||||
Self {
|
||||
interval: 1000 / (rate as u16), // between 3 and 1000 milliseconds
|
||||
capacity: MAX_BURST,
|
||||
prev: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn allow(&mut self, now: Instant) -> bool {
|
||||
if now < self.prev {
|
||||
return false;
|
||||
}
|
||||
|
||||
let elapsed = now - self.prev;
|
||||
// If `capacity` is 0 and not enough time (`self.interval` ms) has passed since
|
||||
// `self.prev` to add new capacity, return `false`. The goal of this method is to
|
||||
// make this decision as efficient as possible.
|
||||
if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We now calculate `new`, the number of ms, since we last returned `true`,
|
||||
// and `remainder`, which represents a number of ns less than 1ms which we cannot
|
||||
// convert into capacity now, so we're saving it for later.
|
||||
let (new, remainder) = (
|
||||
elapsed.as_millis() / self.interval as u128,
|
||||
elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
|
||||
);
|
||||
|
||||
// We add `new` to `capacity`, subtract one for returning `true` from here,
|
||||
// then make sure it does not exceed a maximum of `MAX_BURST`, then store it.
|
||||
self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
|
||||
// Store `prev` for the next iteration after subtracting the `remainder`.
|
||||
// Just use `unwrap` here because it shouldn't be possible for this to underflow.
|
||||
self.prev = now
|
||||
.checked_sub(Duration::from_nanos(remainder as u64))
|
||||
.unwrap();
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BURST: u8 = 20;
|
||||
|
||||
/// The drawn state of an element.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct DrawState {
|
||||
/// The lines to print (can contain ANSI codes)
|
||||
pub(crate) lines: Vec<String>,
|
||||
/// The number of lines that shouldn't be reaped by the next tick.
|
||||
pub(crate) orphan_lines_count: usize,
|
||||
/// True if we should move the cursor up when possible instead of clearing lines.
|
||||
pub(crate) move_cursor: bool,
|
||||
/// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
|
||||
pub(crate) alignment: MultiProgressAlignment,
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
fn draw_to_term(
|
||||
&mut self,
|
||||
term: &(impl TermLike + ?Sized),
|
||||
last_line_count: &mut usize,
|
||||
) -> io::Result<()> {
|
||||
if panicking() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.lines.is_empty() && self.move_cursor {
|
||||
term.move_cursor_up(*last_line_count)?;
|
||||
} else {
|
||||
// Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n'
|
||||
let n = *last_line_count;
|
||||
term.move_cursor_up(n.saturating_sub(1))?;
|
||||
for i in 0..n {
|
||||
term.clear_line()?;
|
||||
if i + 1 != n {
|
||||
term.move_cursor_down(1)?;
|
||||
}
|
||||
}
|
||||
term.move_cursor_up(n.saturating_sub(1))?;
|
||||
}
|
||||
|
||||
let shift = match self.alignment {
|
||||
MultiProgressAlignment::Bottom if self.lines.len() < *last_line_count => {
|
||||
let shift = *last_line_count - self.lines.len();
|
||||
for _ in 0..shift {
|
||||
term.write_line("")?;
|
||||
}
|
||||
shift
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let term_height = term.height() as usize;
|
||||
let term_width = term.width() as usize;
|
||||
let len = self.lines.len();
|
||||
let mut real_len = 0;
|
||||
let mut last_line_filler = 0;
|
||||
debug_assert!(self.orphan_lines_count <= self.lines.len());
|
||||
for (idx, line) in self.lines.iter().enumerate() {
|
||||
let line_width = console::measure_text_width(line);
|
||||
let diff = if line.is_empty() {
|
||||
// Empty line are new line
|
||||
1
|
||||
} else {
|
||||
// Calculate real length based on terminal width
|
||||
// This take in account linewrap from terminal
|
||||
let terminal_len = (line_width as f64 / term_width as f64).ceil() as usize;
|
||||
|
||||
// If the line is effectively empty (for example when it consists
|
||||
// solely of ANSI color code sequences, count it the same as a
|
||||
// new line. If the line is measured to be len = 0, we will
|
||||
// subtract with overflow later.
|
||||
usize::max(terminal_len, 1)
|
||||
};
|
||||
// Don't consider orphan lines when comparing to terminal height.
|
||||
debug_assert!(idx <= real_len);
|
||||
if self.orphan_lines_count <= idx
|
||||
&& real_len - self.orphan_lines_count + diff > term_height
|
||||
{
|
||||
break;
|
||||
}
|
||||
real_len += diff;
|
||||
if idx != 0 {
|
||||
term.write_line("")?;
|
||||
}
|
||||
term.write_str(line)?;
|
||||
if idx + 1 == len {
|
||||
// Keep the cursor on the right terminal side
|
||||
// So that next user writes/prints will happen on the next line
|
||||
last_line_filler = term_width.saturating_sub(line_width);
|
||||
}
|
||||
}
|
||||
term.write_str(&" ".repeat(last_line_filler))?;
|
||||
|
||||
term.flush()?;
|
||||
*last_line_count = real_len - self.orphan_lines_count + shift;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.lines.clear();
|
||||
self.orphan_lines_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
|
||||
|
||||
#[test]
|
||||
fn multi_is_hidden() {
|
||||
let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
|
||||
|
||||
let pb = mp.add(ProgressBar::new(100));
|
||||
assert!(mp.is_hidden());
|
||||
assert!(pb.is_hidden());
|
||||
}
|
||||
}
|
337
vendor/indicatif/src/format.rs
vendored
Normal file
337
vendor/indicatif/src/format.rs
vendored
Normal file
@ -0,0 +1,337 @@
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use number_prefix::NumberPrefix;
|
||||
|
||||
const SECOND: Duration = Duration::from_secs(1);
|
||||
const MINUTE: Duration = Duration::from_secs(60);
|
||||
const HOUR: Duration = Duration::from_secs(60 * 60);
|
||||
const DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
|
||||
const YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60);
|
||||
|
||||
/// Wraps an std duration for human basic formatting.
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedDuration(pub Duration);
|
||||
|
||||
/// Wraps an std duration for human readable formatting.
|
||||
#[derive(Debug)]
|
||||
pub struct HumanDuration(pub Duration);
|
||||
|
||||
/// Formats bytes for human readability
|
||||
#[derive(Debug)]
|
||||
pub struct HumanBytes(pub u64);
|
||||
|
||||
/// Formats bytes for human readability using SI prefixes
|
||||
#[derive(Debug)]
|
||||
pub struct DecimalBytes(pub u64);
|
||||
|
||||
/// Formats bytes for human readability using ISO/IEC prefixes
|
||||
#[derive(Debug)]
|
||||
pub struct BinaryBytes(pub u64);
|
||||
|
||||
/// Formats counts for human readability using commas
|
||||
#[derive(Debug)]
|
||||
pub struct HumanCount(pub u64);
|
||||
|
||||
/// Formats counts for human readability using commas for floats
|
||||
#[derive(Debug)]
|
||||
pub struct HumanFloatCount(pub f64);
|
||||
|
||||
impl fmt::Display for FormattedDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut t = self.0.as_secs();
|
||||
let seconds = t % 60;
|
||||
t /= 60;
|
||||
let minutes = t % 60;
|
||||
t /= 60;
|
||||
let hours = t % 24;
|
||||
t /= 24;
|
||||
if t > 0 {
|
||||
let days = t;
|
||||
write!(f, "{days}d {hours:02}:{minutes:02}:{seconds:02}")
|
||||
} else {
|
||||
write!(f, "{hours:02}:{minutes:02}:{seconds:02}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `HumanDuration` should be as intuitively understandable as possible.
|
||||
// So we want to round, not truncate: otherwise 1 hour and 59 minutes
|
||||
// would display an ETA of "1 hour" which underestimates the time
|
||||
// remaining by a factor 2.
|
||||
//
|
||||
// To make the precision more uniform, we avoid displaying "1 unit"
|
||||
// (except for seconds), because it would be displayed for a relatively
|
||||
// long duration compared to the unit itself. Instead, when we arrive
|
||||
// around 1.5 unit, we change from "2 units" to the next smaller unit
|
||||
// (e.g. "89 seconds").
|
||||
//
|
||||
// Formally:
|
||||
// * for n >= 2, we go from "n+1 units" to "n units" exactly at (n + 1/2) units
|
||||
// * we switch from "2 units" to the next smaller unit at (1.5 unit minus half of the next smaller unit)
|
||||
|
||||
impl fmt::Display for HumanDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut idx = 0;
|
||||
for (i, &(cur, _, _)) in UNITS.iter().enumerate() {
|
||||
idx = i;
|
||||
match UNITS.get(i + 1) {
|
||||
Some(&next) if self.0.saturating_add(next.0 / 2) >= cur + cur / 2 => break,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
let (unit, name, alt) = UNITS[idx];
|
||||
// FIXME when `div_duration_f64` is stable
|
||||
let mut t = (self.0.as_secs_f64() / unit.as_secs_f64()).round() as usize;
|
||||
if idx < UNITS.len() - 1 {
|
||||
t = Ord::max(t, 2);
|
||||
}
|
||||
|
||||
match (f.alternate(), t) {
|
||||
(true, _) => write!(f, "{t}{alt}"),
|
||||
(false, 1) => write!(f, "{t} {name}"),
|
||||
(false, _) => write!(f, "{t} {name}s"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UNITS: &[(Duration, &str, &str)] = &[
|
||||
(YEAR, "year", "y"),
|
||||
(WEEK, "week", "w"),
|
||||
(DAY, "day", "d"),
|
||||
(HOUR, "hour", "h"),
|
||||
(MINUTE, "minute", "m"),
|
||||
(SECOND, "second", "s"),
|
||||
];
|
||||
|
||||
impl fmt::Display for HumanBytes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match NumberPrefix::binary(self.0 as f64) {
|
||||
NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
|
||||
NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DecimalBytes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match NumberPrefix::decimal(self.0 as f64) {
|
||||
NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
|
||||
NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BinaryBytes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match NumberPrefix::binary(self.0 as f64) {
|
||||
NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
|
||||
NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HumanCount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
let num = self.0.to_string();
|
||||
let len = num.len();
|
||||
for (idx, c) in num.chars().enumerate() {
|
||||
let pos = len - idx - 1;
|
||||
f.write_char(c)?;
|
||||
if pos > 0 && pos % 3 == 0 {
|
||||
f.write_char(',')?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HumanFloatCount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
let num = format!("{:.4}", self.0);
|
||||
let (int_part, frac_part) = match num.split_once('.') {
|
||||
Some((int_str, fract_str)) => (int_str.to_string(), fract_str),
|
||||
None => (self.0.trunc().to_string(), ""),
|
||||
};
|
||||
let len = int_part.len();
|
||||
for (idx, c) in int_part.chars().enumerate() {
|
||||
let pos = len - idx - 1;
|
||||
f.write_char(c)?;
|
||||
if pos > 0 && pos % 3 == 0 {
|
||||
f.write_char(',')?;
|
||||
}
|
||||
}
|
||||
let frac_trimmed = frac_part.trim_end_matches('0');
|
||||
if !frac_trimmed.is_empty() {
|
||||
f.write_char('.')?;
|
||||
f.write_str(frac_trimmed)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const MILLI: Duration = Duration::from_millis(1);
|
||||
|
||||
#[test]
|
||||
fn human_duration_alternate() {
|
||||
for (unit, _, alt) in UNITS {
|
||||
assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_less_than_one_second() {
|
||||
assert_eq!(
|
||||
"0 seconds",
|
||||
format!("{}", HumanDuration(Duration::from_secs(0)))
|
||||
);
|
||||
assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI)));
|
||||
assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI)));
|
||||
assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI)));
|
||||
assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_less_than_two_seconds() {
|
||||
assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI)));
|
||||
assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI)));
|
||||
assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_one_unit() {
|
||||
assert_eq!("1 second", format!("{}", HumanDuration(SECOND)));
|
||||
assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE)));
|
||||
assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR)));
|
||||
assert_eq!("24 hours", format!("{}", HumanDuration(DAY)));
|
||||
assert_eq!("7 days", format!("{}", HumanDuration(WEEK)));
|
||||
assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_less_than_one_and_a_half_unit() {
|
||||
// this one is actually done at 1.5 unit - half of the next smaller unit - epsilon
|
||||
// and should display the next smaller unit
|
||||
let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI);
|
||||
assert_eq!("89 seconds", format!("{d}"));
|
||||
let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI);
|
||||
assert_eq!("89 minutes", format!("{d}"));
|
||||
let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI);
|
||||
assert_eq!("35 hours", format!("{d}"));
|
||||
let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI);
|
||||
assert_eq!("10 days", format!("{d}"));
|
||||
let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI);
|
||||
assert_eq!("78 weeks", format!("{d}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_one_and_a_half_unit() {
|
||||
// this one is actually done at 1.5 unit - half of the next smaller unit
|
||||
// and should still display "2 units"
|
||||
let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2);
|
||||
assert_eq!("2 minutes", format!("{d}"));
|
||||
let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2);
|
||||
assert_eq!("2 hours", format!("{d}"));
|
||||
let d = HumanDuration(DAY + DAY / 2 - HOUR / 2);
|
||||
assert_eq!("2 days", format!("{d}"));
|
||||
let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2);
|
||||
assert_eq!("2 weeks", format!("{d}"));
|
||||
let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2);
|
||||
assert_eq!("2 years", format!("{d}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_two_units() {
|
||||
assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND)));
|
||||
assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE)));
|
||||
assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR)));
|
||||
assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY)));
|
||||
assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK)));
|
||||
assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_less_than_two_and_a_half_units() {
|
||||
let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI);
|
||||
assert_eq!("2 seconds", format!("{d}"));
|
||||
let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI);
|
||||
assert_eq!("2 minutes", format!("{d}"));
|
||||
let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI);
|
||||
assert_eq!("2 hours", format!("{d}"));
|
||||
let d = HumanDuration(2 * DAY + DAY / 2 - MILLI);
|
||||
assert_eq!("2 days", format!("{d}"));
|
||||
let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI);
|
||||
assert_eq!("2 weeks", format!("{d}"));
|
||||
let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI);
|
||||
assert_eq!("2 years", format!("{d}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_two_and_a_half_units() {
|
||||
let d = HumanDuration(2 * SECOND + SECOND / 2);
|
||||
assert_eq!("3 seconds", format!("{d}"));
|
||||
let d = HumanDuration(2 * MINUTE + MINUTE / 2);
|
||||
assert_eq!("3 minutes", format!("{d}"));
|
||||
let d = HumanDuration(2 * HOUR + HOUR / 2);
|
||||
assert_eq!("3 hours", format!("{d}"));
|
||||
let d = HumanDuration(2 * DAY + DAY / 2);
|
||||
assert_eq!("3 days", format!("{d}"));
|
||||
let d = HumanDuration(2 * WEEK + WEEK / 2);
|
||||
assert_eq!("3 weeks", format!("{d}"));
|
||||
let d = HumanDuration(2 * YEAR + YEAR / 2);
|
||||
assert_eq!("3 years", format!("{d}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_duration_three_units() {
|
||||
assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND)));
|
||||
assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE)));
|
||||
assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR)));
|
||||
assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY)));
|
||||
assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK)));
|
||||
assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_count() {
|
||||
assert_eq!("42", format!("{}", HumanCount(42)));
|
||||
assert_eq!("7,654", format!("{}", HumanCount(7654)));
|
||||
assert_eq!("12,345", format!("{}", HumanCount(12345)));
|
||||
assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_float_count() {
|
||||
assert_eq!("42", format!("{}", HumanFloatCount(42.0)));
|
||||
assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0)));
|
||||
assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0)));
|
||||
assert_eq!(
|
||||
"1,234,567,890",
|
||||
format!("{}", HumanFloatCount(1234567890.0))
|
||||
);
|
||||
assert_eq!("42.5", format!("{}", HumanFloatCount(42.5)));
|
||||
assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345)));
|
||||
assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345)));
|
||||
assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321)));
|
||||
assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456)));
|
||||
assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789)));
|
||||
assert_eq!(
|
||||
"1,234,567,890.1235",
|
||||
format!("{}", HumanFloatCount(1234567890.1234567))
|
||||
);
|
||||
assert_eq!(
|
||||
"1,234,567,890.1234",
|
||||
format!("{}", HumanFloatCount(1234567890.1234321))
|
||||
);
|
||||
}
|
||||
}
|
399
vendor/indicatif/src/in_memory.rs
vendored
Normal file
399
vendor/indicatif/src/in_memory.rs
vendored
Normal file
@ -0,0 +1,399 @@
|
||||
use std::fmt::{Debug, Formatter, Write as _};
|
||||
use std::io::Write as _;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use vt100::Parser;
|
||||
|
||||
use crate::TermLike;
|
||||
|
||||
/// A thin wrapper around [`vt100::Parser`].
|
||||
///
|
||||
/// This is just an [`Arc`] around its internal state, so it can be freely cloned.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "in_memory")))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InMemoryTerm {
|
||||
state: Arc<Mutex<InMemoryTermState>>,
|
||||
}
|
||||
|
||||
impl InMemoryTerm {
|
||||
pub fn new(rows: u16, cols: u16) -> InMemoryTerm {
|
||||
assert!(rows > 0, "rows must be > 0");
|
||||
assert!(cols > 0, "cols must be > 0");
|
||||
InMemoryTerm {
|
||||
state: Arc::new(Mutex::new(InMemoryTermState::new(rows, cols))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
*state = InMemoryTermState::new(state.height, state.width);
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> String {
|
||||
let state = self.state.lock().unwrap();
|
||||
|
||||
// For some reason, the `Screen::contents` method doesn't include newlines in what it
|
||||
// returns, making it useless for our purposes. So we need to manually reconstruct the
|
||||
// contents by iterating over the rows in the terminal buffer.
|
||||
let mut rows = state
|
||||
.parser
|
||||
.screen()
|
||||
.rows(0, state.width)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Reverse the rows and trim empty lines from the end
|
||||
rows = rows
|
||||
.into_iter()
|
||||
.rev()
|
||||
.skip_while(|line| line.is_empty())
|
||||
.map(|line| line.trim_end().to_string())
|
||||
.collect();
|
||||
|
||||
// Un-reverse the rows and join them up with newlines
|
||||
rows.reverse();
|
||||
rows.join("\n")
|
||||
}
|
||||
|
||||
pub fn contents_formatted(&self) -> Vec<u8> {
|
||||
let state = self.state.lock().unwrap();
|
||||
|
||||
// For some reason, the `Screen::contents` method doesn't include newlines in what it
|
||||
// returns, making it useless for our purposes. So we need to manually reconstruct the
|
||||
// contents by iterating over the rows in the terminal buffer.
|
||||
let mut rows = state
|
||||
.parser
|
||||
.screen()
|
||||
.rows_formatted(0, state.width)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Reverse the rows and trim empty lines from the end
|
||||
rows = rows
|
||||
.into_iter()
|
||||
.rev()
|
||||
.skip_while(|line| line.is_empty())
|
||||
.collect();
|
||||
|
||||
// Un-reverse the rows
|
||||
rows.reverse();
|
||||
|
||||
// Calculate buffer size
|
||||
let reset = b"[m";
|
||||
let len = rows.iter().map(|line| line.len() + reset.len() + 1).sum();
|
||||
|
||||
// Join rows up with reset codes and newlines
|
||||
let mut contents = rows.iter().fold(Vec::with_capacity(len), |mut acc, cur| {
|
||||
acc.extend_from_slice(cur);
|
||||
acc.extend_from_slice(reset);
|
||||
acc.push(b'\n');
|
||||
acc
|
||||
});
|
||||
|
||||
// Remove last newline again, but leave the reset code
|
||||
contents.truncate(len.saturating_sub(1));
|
||||
contents
|
||||
}
|
||||
|
||||
pub fn moves_since_last_check(&self) -> String {
|
||||
let mut s = String::new();
|
||||
for line in std::mem::take(&mut self.state.lock().unwrap().history) {
|
||||
writeln!(s, "{line:?}").unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl TermLike for InMemoryTerm {
|
||||
fn width(&self) -> u16 {
|
||||
self.state.lock().unwrap().width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.state.lock().unwrap().height
|
||||
}
|
||||
|
||||
fn move_cursor_up(&self, n: usize) -> std::io::Result<()> {
|
||||
match n {
|
||||
0 => Ok(()),
|
||||
_ => {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Up(n));
|
||||
state.write_str(&format!("\x1b[{n}A"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_down(&self, n: usize) -> std::io::Result<()> {
|
||||
match n {
|
||||
0 => Ok(()),
|
||||
_ => {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Down(n));
|
||||
state.write_str(&format!("\x1b[{n}B"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_right(&self, n: usize) -> std::io::Result<()> {
|
||||
match n {
|
||||
0 => Ok(()),
|
||||
_ => {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Right(n));
|
||||
state.write_str(&format!("\x1b[{n}C"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_left(&self, n: usize) -> std::io::Result<()> {
|
||||
match n {
|
||||
0 => Ok(()),
|
||||
_ => {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Left(n));
|
||||
state.write_str(&format!("\x1b[{n}D"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_line(&self, s: &str) -> std::io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Str(s.into()));
|
||||
state.history.push(Move::NewLine);
|
||||
|
||||
// Don't try to handle writing lines with additional newlines embedded in them - it's not
|
||||
// worth the extra code for something that indicatif doesn't even do. May revisit in future.
|
||||
debug_assert!(
|
||||
s.lines().count() <= 1,
|
||||
"calling write_line with embedded newlines is not allowed"
|
||||
);
|
||||
|
||||
// vte100 needs the full \r\n sequence to jump to the next line and reset the cursor to
|
||||
// the beginning of the line. Be flexible and take either \n or \r\n
|
||||
state.write_str(s)?;
|
||||
state.write_str("\r\n")
|
||||
}
|
||||
|
||||
fn write_str(&self, s: &str) -> std::io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Str(s.into()));
|
||||
state.write_str(s)
|
||||
}
|
||||
|
||||
fn clear_line(&self) -> std::io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Clear);
|
||||
state.write_str("\r\x1b[2K")
|
||||
}
|
||||
|
||||
fn flush(&self) -> std::io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.history.push(Move::Flush);
|
||||
state.parser.flush()
|
||||
}
|
||||
}
|
||||
|
||||
struct InMemoryTermState {
|
||||
width: u16,
|
||||
height: u16,
|
||||
parser: vt100::Parser,
|
||||
history: Vec<Move>,
|
||||
}
|
||||
|
||||
impl InMemoryTermState {
|
||||
pub(crate) fn new(rows: u16, cols: u16) -> InMemoryTermState {
|
||||
InMemoryTermState {
|
||||
width: cols,
|
||||
height: rows,
|
||||
parser: Parser::new(rows, cols, 0),
|
||||
history: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_str(&mut self, s: &str) -> std::io::Result<()> {
|
||||
self.parser.write_all(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for InMemoryTermState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("InMemoryTermState").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum Move {
|
||||
Up(usize),
|
||||
Down(usize),
|
||||
Left(usize),
|
||||
Right(usize),
|
||||
Str(String),
|
||||
NewLine,
|
||||
Clear,
|
||||
Flush,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) {
|
||||
in_mem
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.parser
|
||||
.screen()
|
||||
.cursor_position()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_wrapping() {
|
||||
let in_mem = InMemoryTerm::new(10, 5);
|
||||
assert_eq!(cursor_pos(&in_mem), (0, 0));
|
||||
|
||||
in_mem.write_str("ABCDE").unwrap();
|
||||
assert_eq!(in_mem.contents(), "ABCDE");
|
||||
assert_eq!(cursor_pos(&in_mem), (0, 5));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("ABCDE")
|
||||
"#
|
||||
);
|
||||
|
||||
// Should wrap onto next line
|
||||
in_mem.write_str("FG").unwrap();
|
||||
assert_eq!(in_mem.contents(), "ABCDE\nFG");
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 2));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("FG")
|
||||
"#
|
||||
);
|
||||
|
||||
in_mem.write_str("HIJ").unwrap();
|
||||
assert_eq!(in_mem.contents(), "ABCDE\nFGHIJ");
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 5));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("HIJ")
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_line() {
|
||||
let in_mem = InMemoryTerm::new(10, 5);
|
||||
assert_eq!(cursor_pos(&in_mem), (0, 0));
|
||||
|
||||
in_mem.write_line("A").unwrap();
|
||||
assert_eq!(in_mem.contents(), "A");
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 0));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("A")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
|
||||
in_mem.write_line("B").unwrap();
|
||||
assert_eq!(in_mem.contents(), "A\nB");
|
||||
assert_eq!(cursor_pos(&in_mem), (2, 0));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("B")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
|
||||
in_mem.write_line("Longer than cols").unwrap();
|
||||
assert_eq!(in_mem.contents(), "A\nB\nLonge\nr tha\nn col\ns");
|
||||
assert_eq!(cursor_pos(&in_mem), (6, 0));
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("Longer than cols")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_functionality() {
|
||||
let in_mem = InMemoryTerm::new(10, 80);
|
||||
|
||||
in_mem.write_line("This is a test line").unwrap();
|
||||
assert_eq!(in_mem.contents(), "This is a test line");
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("This is a test line")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
|
||||
in_mem.write_line("And another line!").unwrap();
|
||||
assert_eq!(in_mem.contents(), "This is a test line\nAnd another line!");
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("And another line!")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
|
||||
in_mem.move_cursor_up(1).unwrap();
|
||||
in_mem.write_str("TEST").unwrap();
|
||||
|
||||
assert_eq!(in_mem.contents(), "This is a test line\nTESTanother line!");
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Up(1)
|
||||
Str("TEST")
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newlines() {
|
||||
let in_mem = InMemoryTerm::new(10, 10);
|
||||
in_mem.write_line("LINE ONE").unwrap();
|
||||
in_mem.write_line("LINE TWO").unwrap();
|
||||
in_mem.write_line("").unwrap();
|
||||
in_mem.write_line("LINE FOUR").unwrap();
|
||||
|
||||
assert_eq!(in_mem.contents(), "LINE ONE\nLINE TWO\n\nLINE FOUR");
|
||||
|
||||
assert_eq!(
|
||||
in_mem.moves_since_last_check(),
|
||||
r#"Str("LINE ONE")
|
||||
NewLine
|
||||
Str("LINE TWO")
|
||||
NewLine
|
||||
Str("")
|
||||
NewLine
|
||||
Str("LINE FOUR")
|
||||
NewLine
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_zero_movement() {
|
||||
let in_mem = InMemoryTerm::new(10, 80);
|
||||
in_mem.write_line("LINE ONE").unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 0));
|
||||
|
||||
// Check that moving zero rows/cols does not actually move cursor
|
||||
in_mem.move_cursor_up(0).unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 0));
|
||||
|
||||
in_mem.move_cursor_down(0).unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 0));
|
||||
|
||||
in_mem.move_cursor_right(1).unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 1));
|
||||
|
||||
in_mem.move_cursor_left(0).unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 1));
|
||||
|
||||
in_mem.move_cursor_right(0).unwrap();
|
||||
assert_eq!(cursor_pos(&in_mem), (1, 1));
|
||||
}
|
||||
}
|
355
vendor/indicatif/src/iter.rs
vendored
Normal file
355
vendor/indicatif/src/iter.rs
vendored
Normal file
@ -0,0 +1,355 @@
|
||||
use std::borrow::Cow;
|
||||
use std::io::{self, IoSliceMut};
|
||||
use std::iter::FusedIterator;
|
||||
#[cfg(feature = "tokio")]
|
||||
use std::pin::Pin;
|
||||
#[cfg(feature = "tokio")]
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
use tokio::io::{ReadBuf, SeekFrom};
|
||||
|
||||
use crate::progress_bar::ProgressBar;
|
||||
use crate::state::ProgressFinish;
|
||||
use crate::style::ProgressStyle;
|
||||
|
||||
/// Wraps an iterator to display its progress.
|
||||
pub trait ProgressIterator
|
||||
where
|
||||
Self: Sized + Iterator,
|
||||
{
|
||||
/// Wrap an iterator with default styling. Uses `Iterator::size_hint` to get length.
|
||||
/// Returns `Some(..)` only if `size_hint.1` is `Some`. If you want to create a progress bar
|
||||
/// even if `size_hint.1` returns `None` use `progress_count` or `progress_with` instead.
|
||||
fn try_progress(self) -> Option<ProgressBarIter<Self>> {
|
||||
self.size_hint()
|
||||
.1
|
||||
.map(|len| self.progress_count(u64::try_from(len).unwrap()))
|
||||
}
|
||||
|
||||
/// Wrap an iterator with default styling.
|
||||
fn progress(self) -> ProgressBarIter<Self>
|
||||
where
|
||||
Self: ExactSizeIterator,
|
||||
{
|
||||
let len = u64::try_from(self.len()).unwrap();
|
||||
self.progress_count(len)
|
||||
}
|
||||
|
||||
/// Wrap an iterator with an explicit element count.
|
||||
fn progress_count(self, len: u64) -> ProgressBarIter<Self> {
|
||||
self.progress_with(ProgressBar::new(len))
|
||||
}
|
||||
|
||||
/// Wrap an iterator with a custom progress bar.
|
||||
fn progress_with(self, progress: ProgressBar) -> ProgressBarIter<Self>;
|
||||
|
||||
/// Wrap an iterator with a progress bar and style it.
|
||||
fn progress_with_style(self, style: crate::ProgressStyle) -> ProgressBarIter<Self>
|
||||
where
|
||||
Self: ExactSizeIterator,
|
||||
{
|
||||
let len = u64::try_from(self.len()).unwrap();
|
||||
let bar = ProgressBar::new(len).with_style(style);
|
||||
self.progress_with(bar)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps an iterator to display its progress.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgressBarIter<T> {
|
||||
pub(crate) it: T,
|
||||
pub progress: ProgressBar,
|
||||
}
|
||||
|
||||
impl<T> ProgressBarIter<T> {
|
||||
/// Builder-like function for setting underlying progress bar's style.
|
||||
///
|
||||
/// See [ProgressBar::with_style].
|
||||
pub fn with_style(mut self, style: ProgressStyle) -> Self {
|
||||
self.progress = self.progress.with_style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-like function for setting underlying progress bar's prefix.
|
||||
///
|
||||
/// See [ProgressBar::with_prefix].
|
||||
pub fn with_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.progress = self.progress.with_prefix(prefix);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-like function for setting underlying progress bar's message.
|
||||
///
|
||||
/// See [ProgressBar::with_message].
|
||||
pub fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.progress = self.progress.with_message(message);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-like function for setting underlying progress bar's position.
|
||||
///
|
||||
/// See [ProgressBar::with_position].
|
||||
pub fn with_position(mut self, position: u64) -> Self {
|
||||
self.progress = self.progress.with_position(position);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-like function for setting underlying progress bar's elapsed time.
|
||||
///
|
||||
/// See [ProgressBar::with_elapsed].
|
||||
pub fn with_elapsed(mut self, elapsed: Duration) -> Self {
|
||||
self.progress = self.progress.with_elapsed(elapsed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-like function for setting underlying progress bar's finish behavior.
|
||||
///
|
||||
/// See [ProgressBar::with_finish].
|
||||
pub fn with_finish(mut self, finish: ProgressFinish) -> Self {
|
||||
self.progress = self.progress.with_finish(finish);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T: Iterator<Item = S>> Iterator for ProgressBarIter<T> {
|
||||
type Item = S;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.it.next();
|
||||
|
||||
if item.is_some() {
|
||||
self.progress.inc(1);
|
||||
} else if !self.progress.is_finished() {
|
||||
self.progress.finish_using_style();
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ExactSizeIterator> ExactSizeIterator for ProgressBarIter<T> {
|
||||
fn len(&self) -> usize {
|
||||
self.it.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DoubleEndedIterator> DoubleEndedIterator for ProgressBarIter<T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let item = self.it.next_back();
|
||||
|
||||
if item.is_some() {
|
||||
self.progress.inc(1);
|
||||
} else if !self.progress.is_finished() {
|
||||
self.progress.finish_using_style();
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FusedIterator> FusedIterator for ProgressBarIter<T> {}
|
||||
|
||||
impl<R: io::Read> io::Read for ProgressBarIter<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let inc = self.it.read(buf)?;
|
||||
self.progress.inc(inc as u64);
|
||||
Ok(inc)
|
||||
}
|
||||
|
||||
fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
|
||||
let inc = self.it.read_vectored(bufs)?;
|
||||
self.progress.inc(inc as u64);
|
||||
Ok(inc)
|
||||
}
|
||||
|
||||
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
|
||||
let inc = self.it.read_to_string(buf)?;
|
||||
self.progress.inc(inc as u64);
|
||||
Ok(inc)
|
||||
}
|
||||
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
|
||||
self.it.read_exact(buf)?;
|
||||
self.progress.inc(buf.len() as u64);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: io::BufRead> io::BufRead for ProgressBarIter<R> {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
self.it.fill_buf()
|
||||
}
|
||||
|
||||
fn consume(&mut self, amt: usize) {
|
||||
self.it.consume(amt);
|
||||
self.progress.inc(amt as u64);
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: io::Seek> io::Seek for ProgressBarIter<S> {
|
||||
fn seek(&mut self, f: io::SeekFrom) -> io::Result<u64> {
|
||||
self.it.seek(f).map(|pos| {
|
||||
self.progress.set_position(pos);
|
||||
pos
|
||||
})
|
||||
}
|
||||
// Pass this through to preserve optimizations that the inner I/O object may use here
|
||||
// Also avoid sending a set_position update when the position hasn't changed
|
||||
fn stream_position(&mut self) -> io::Result<u64> {
|
||||
self.it.stream_position()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
impl<W: tokio::io::AsyncWrite + Unpin> tokio::io::AsyncWrite for ProgressBarIter<W> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.it).poll_write(cx, buf).map(|poll| {
|
||||
poll.map(|inc| {
|
||||
self.progress.inc(inc as u64);
|
||||
inc
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.it).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.it).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
impl<W: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for ProgressBarIter<W> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let prev_len = buf.filled().len() as u64;
|
||||
if let Poll::Ready(e) = Pin::new(&mut self.it).poll_read(cx, buf) {
|
||||
self.progress.inc(buf.filled().len() as u64 - prev_len);
|
||||
Poll::Ready(e)
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
impl<W: tokio::io::AsyncSeek + Unpin> tokio::io::AsyncSeek for ProgressBarIter<W> {
|
||||
fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
|
||||
Pin::new(&mut self.it).start_seek(position)
|
||||
}
|
||||
|
||||
fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
|
||||
Pin::new(&mut self.it).poll_complete(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
impl<W: tokio::io::AsyncBufRead + Unpin + tokio::io::AsyncRead> tokio::io::AsyncBufRead
|
||||
for ProgressBarIter<W>
|
||||
{
|
||||
fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
|
||||
let this = self.get_mut();
|
||||
let result = Pin::new(&mut this.it).poll_fill_buf(cx);
|
||||
if let Poll::Ready(Ok(buf)) = &result {
|
||||
this.progress.inc(buf.len() as u64);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn consume(mut self: Pin<&mut Self>, amt: usize) {
|
||||
Pin::new(&mut self.it).consume(amt);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "futures")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures")))]
|
||||
impl<S: futures_core::Stream + Unpin> futures_core::Stream for ProgressBarIter<S> {
|
||||
type Item = S::Item;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let item = std::pin::Pin::new(&mut this.it).poll_next(cx);
|
||||
match &item {
|
||||
std::task::Poll::Ready(Some(_)) => this.progress.inc(1),
|
||||
std::task::Poll::Ready(None) => this.progress.finish_using_style(),
|
||||
std::task::Poll::Pending => {}
|
||||
}
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: io::Write> io::Write for ProgressBarIter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.it.write(buf).map(|inc| {
|
||||
self.progress.inc(inc as u64);
|
||||
inc
|
||||
})
|
||||
}
|
||||
|
||||
fn write_vectored(&mut self, bufs: &[io::IoSlice]) -> io::Result<usize> {
|
||||
self.it.write_vectored(bufs).map(|inc| {
|
||||
self.progress.inc(inc as u64);
|
||||
inc
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.it.flush()
|
||||
}
|
||||
|
||||
// write_fmt can not be captured with reasonable effort.
|
||||
// as it uses write_all internally by default that should not be a problem.
|
||||
// fn write_fmt(&mut self, fmt: fmt::Arguments) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<S, T: Iterator<Item = S>> ProgressIterator for T {
|
||||
fn progress_with(self, progress: ProgressBar) -> ProgressBarIter<Self> {
|
||||
ProgressBarIter { it: self, progress }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::iter::{ProgressBarIter, ProgressIterator};
|
||||
use crate::progress_bar::ProgressBar;
|
||||
use crate::ProgressStyle;
|
||||
|
||||
#[test]
|
||||
fn it_can_wrap_an_iterator() {
|
||||
let v = [1, 2, 3];
|
||||
let wrap = |it: ProgressBarIter<_>| {
|
||||
assert_eq!(it.map(|x| x * 2).collect::<Vec<_>>(), vec![2, 4, 6]);
|
||||
};
|
||||
|
||||
wrap(v.iter().progress());
|
||||
wrap(v.iter().progress_count(3));
|
||||
wrap({
|
||||
let pb = ProgressBar::new(v.len() as u64);
|
||||
v.iter().progress_with(pb)
|
||||
});
|
||||
wrap({
|
||||
let style = ProgressStyle::default_bar()
|
||||
.template("{wide_bar:.red} {percent}/100%")
|
||||
.unwrap();
|
||||
v.iter().progress_with_style(style)
|
||||
});
|
||||
}
|
||||
}
|
247
vendor/indicatif/src/lib.rs
vendored
Normal file
247
vendor/indicatif/src/lib.rs
vendored
Normal file
@ -0,0 +1,247 @@
|
||||
//! indicatif is a library for Rust that helps you build command line
|
||||
//! interfaces that report progress to users. It comes with various
|
||||
//! tools and utilities for formatting anything that indicates progress.
|
||||
//!
|
||||
//! Platform support:
|
||||
//!
|
||||
//! * Linux
|
||||
//! * macOS
|
||||
//! * Windows (colors require Windows 10)
|
||||
//!
|
||||
//! Best paired with other libraries in the family:
|
||||
//!
|
||||
//! * [console](https://docs.rs/console)
|
||||
//! * [dialoguer](https://docs.rs/dialoguer)
|
||||
//!
|
||||
//! # Crate Contents
|
||||
//!
|
||||
//! * **Progress bars**
|
||||
//! * [`ProgressBar`](struct.ProgressBar.html) for bars and spinners
|
||||
//! * [`MultiProgress`](struct.MultiProgress.html) for multiple bars
|
||||
//! * **Data Formatting**
|
||||
//! * [`HumanBytes`](struct.HumanBytes.html) for formatting bytes
|
||||
//! * [`DecimalBytes`](struct.DecimalBytes.html) for formatting bytes using SI prefixes
|
||||
//! * [`BinaryBytes`](struct.BinaryBytes.html) for formatting bytes using ISO/IEC prefixes
|
||||
//! * [`HumanDuration`](struct.HumanDuration.html) for formatting durations
|
||||
//! * [`HumanCount`](struct.HumanCount.html) for formatting large counts
|
||||
//! * [`HumanFloatCount`](struct.HumanFloatCount.html) for formatting large float counts
|
||||
//!
|
||||
//! # Progress Bars and Spinners
|
||||
//!
|
||||
//! indicatif comes with a `ProgressBar` type that supports both bounded
|
||||
//! progress bar uses as well as unbounded "spinner" type progress reports.
|
||||
//! Progress bars are `Sync` and `Send` objects which means that they are
|
||||
//! internally locked and can be passed from thread to thread.
|
||||
//!
|
||||
//! Additionally a `MultiProgress` utility is provided that can manage
|
||||
//! rendering multiple progress bars at once (eg: from multiple threads).
|
||||
//!
|
||||
//! To whet your appetite, this is what this can look like:
|
||||
//!
|
||||
//! <img src="https://github.com/console-rs/indicatif/raw/main/screenshots/yarn.gif?raw=true" width="60%">
|
||||
//!
|
||||
//! Progress bars are manually advanced and by default draw to stderr.
|
||||
//! When you are done, the progress bar can be finished either visibly
|
||||
//! (eg: the progress bar stays on the screen) or cleared (the progress
|
||||
//! bar will be removed).
|
||||
//!
|
||||
//! ```rust
|
||||
//! use indicatif::ProgressBar;
|
||||
//!
|
||||
//! let bar = ProgressBar::new(1000);
|
||||
//! for _ in 0..1000 {
|
||||
//! bar.inc(1);
|
||||
//! // ...
|
||||
//! }
|
||||
//! bar.finish();
|
||||
//! ```
|
||||
//!
|
||||
//! General progress bar behaviors:
|
||||
//!
|
||||
//! * if a non terminal is detected the progress bar will be completely
|
||||
//! hidden. This makes piping programs to logfiles make sense out of
|
||||
//! the box.
|
||||
//! * a progress bar only starts drawing when `set_message`, `inc`, `set_position`
|
||||
//! or `tick` are called. In some situations you might have to call `tick`
|
||||
//! once to draw it.
|
||||
//! * progress bars should be explicitly finished to reset the rendering
|
||||
//! for others. Either by also clearing them or by replacing them with
|
||||
//! a new message / retaining the current message.
|
||||
//! * the default template renders neither message nor prefix.
|
||||
//!
|
||||
//! # Iterators
|
||||
//!
|
||||
//! Similar to [tqdm](https://github.com/tqdm/tqdm), progress bars can be
|
||||
//! associated with an iterator. For example:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use indicatif::ProgressIterator;
|
||||
//!
|
||||
//! for _ in (0..1000).progress() {
|
||||
//! // ...
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! See the [`ProgressIterator`](trait.ProgressIterator.html) trait for more
|
||||
//! methods to configure the number of elements in the iterator or change
|
||||
//! the progress bar style. Indicatif also has optional support for parallel
|
||||
//! iterators with [Rayon](https://github.com/rayon-rs/rayon). In your
|
||||
//! `Cargo.toml`, use the "rayon" feature:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! indicatif = {version = "*", features = ["rayon"]}
|
||||
//! ```
|
||||
//!
|
||||
//! And then use it like this:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # extern crate rayon;
|
||||
//! use indicatif::ParallelProgressIterator;
|
||||
//! use rayon::iter::{ParallelIterator, IntoParallelRefIterator};
|
||||
//!
|
||||
//! let v: Vec<_> = (0..100000).collect();
|
||||
//! let v2: Vec<_> = v.par_iter().progress_count(v.len() as u64).map(|i| i + 1).collect();
|
||||
//! assert_eq!(v2[0], 1);
|
||||
//! ```
|
||||
//!
|
||||
//! Or if you'd like to customize the progress bar:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # extern crate rayon;
|
||||
//! use indicatif::{ProgressBar, ParallelProgressIterator, ProgressStyle};
|
||||
//! use rayon::iter::{ParallelIterator, IntoParallelRefIterator};
|
||||
//!
|
||||
//! // Alternatively, use `ProgressBar::new().with_style()`
|
||||
//! let style = ProgressStyle::default_bar();
|
||||
//! let v: Vec<_> = (0..100000).collect();
|
||||
//! let v2: Vec<_> = v.par_iter().progress_with_style(style).map(|i| i + 1).collect();
|
||||
//! assert_eq!(v2[0], 1);
|
||||
//! ```
|
||||
//!
|
||||
//! # Templates
|
||||
//!
|
||||
//! Progress bars can be styled with simple format strings similar to the
|
||||
//! ones in Rust itself. The format for a placeholder is `{key:options}`
|
||||
//! where the `options` part is optional. If provided the format is this:
|
||||
//!
|
||||
//! ```text
|
||||
//! <^> for an optional alignment specification (left, center and right respectively)
|
||||
//! WIDTH an optional width as positive integer
|
||||
//! ! an optional exclamation mark to enable truncation
|
||||
//! .STYLE an optional dot separated style string
|
||||
//! /STYLE an optional dot separated alternative style string
|
||||
//! ```
|
||||
//!
|
||||
//! For the style component see [`Style::from_dotted_str`](https://docs.rs/console/0.7.5/console/struct.Style.html#method.from_dotted_str)
|
||||
//! for more information. Indicatif uses the `console` base crate for all
|
||||
//! colorization and formatting options.
|
||||
//!
|
||||
//! Some examples for templates:
|
||||
//!
|
||||
//! ```text
|
||||
//! [{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}
|
||||
//! ```
|
||||
//!
|
||||
//! This sets a progress bar that is 40 characters wide and has cyan
|
||||
//! as primary style color and blue as alternative style color.
|
||||
//! Alternative styles are currently only used for progress bars.
|
||||
//!
|
||||
//! Example configuration:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use indicatif::{ProgressBar, ProgressStyle};
|
||||
//! # let bar = ProgressBar::new(0);
|
||||
//! bar.set_style(ProgressStyle::with_template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
|
||||
//! .unwrap()
|
||||
//! .progress_chars("##-"));
|
||||
//! ```
|
||||
//!
|
||||
//! The following keys exist:
|
||||
//!
|
||||
//! * `bar`: renders a progress bar. By default 20 characters wide. The
|
||||
//! style string is used to color the elapsed part, the alternative
|
||||
//! style is used for the bar that is yet to render.
|
||||
//! * `wide_bar`: like `bar` but always fills the remaining space. It should not be used with
|
||||
//! `wide_msg`.
|
||||
//! * `spinner`: renders the spinner (current tick string).
|
||||
//! * `prefix`: renders the prefix set on the progress bar.
|
||||
//! * `msg`: renders the currently set message on the progress bar.
|
||||
//! * `wide_msg`: like `msg` but always fills the remaining space and truncates. It should not be used
|
||||
//! with `wide_bar`.
|
||||
//! * `pos`: renders the current position of the bar as integer
|
||||
//! * `human_pos`: renders the current position of the bar as an integer, with commas as the
|
||||
//! thousands separator.
|
||||
//! * `len`: renders the amount of work to be done as an integer
|
||||
//! * `human_len`: renders the total length of the bar as an integer, with commas as the thousands
|
||||
//! separator.
|
||||
//! * `bytes`: renders the current position of the bar as bytes.
|
||||
//! * `percent`: renders the current position of the bar as a percentage of the total length.
|
||||
//! * `total_bytes`: renders the total length of the bar as bytes.
|
||||
//! * `elapsed_precise`: renders the elapsed time as `HH:MM:SS`.
|
||||
//! * `elapsed`: renders the elapsed time as `42s`, `1m` etc.
|
||||
//! * `per_sec`: renders the speed in steps per second.
|
||||
//! * `bytes_per_sec`: renders the speed in bytes per second.
|
||||
//! * `binary_bytes_per_sec`: renders the speed in bytes per second using
|
||||
//! power-of-two units, i.e. `MiB`, `KiB`, etc.
|
||||
//! * `eta_precise`: the remaining time (like `elapsed_precise`).
|
||||
//! * `eta`: the remaining time (like `elapsed`).
|
||||
//! * `duration_precise`: the extrapolated total duration (like `elapsed_precise`).
|
||||
//! * `duration`: the extrapolated total duration time (like `elapsed`).
|
||||
|
||||
//!
|
||||
//! The design of the progress bar can be altered with the integrated
|
||||
//! template functionality. The template can be set by changing a
|
||||
//! `ProgressStyle` and attaching it to the progress bar.
|
||||
//!
|
||||
//! # Human Readable Formatting
|
||||
//!
|
||||
//! There are some formatting wrappers for showing elapsed time and
|
||||
//! file sizes for human users:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::time::Duration;
|
||||
//! use indicatif::{HumanBytes, HumanCount, HumanDuration, HumanFloatCount};
|
||||
//!
|
||||
//! assert_eq!("3.00 MiB", HumanBytes(3*1024*1024).to_string());
|
||||
//! assert_eq!("8 seconds", HumanDuration(Duration::from_secs(8)).to_string());
|
||||
//! assert_eq!("33,857,009", HumanCount(33857009).to_string());
|
||||
//! assert_eq!("33,857,009.1235", HumanFloatCount(33857009.123456).to_string());
|
||||
//! ```
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! * `rayon`: adds rayon support
|
||||
//! * `improved_unicode`: adds improved unicode support (graphemes, better width calculation)
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![warn(unreachable_pub)]
|
||||
|
||||
mod draw_target;
|
||||
mod format;
|
||||
#[cfg(feature = "in_memory")]
|
||||
mod in_memory;
|
||||
mod iter;
|
||||
mod multi;
|
||||
mod progress_bar;
|
||||
#[cfg(feature = "rayon")]
|
||||
mod rayon;
|
||||
mod state;
|
||||
pub mod style;
|
||||
mod term_like;
|
||||
|
||||
pub use crate::draw_target::ProgressDrawTarget;
|
||||
pub use crate::format::{
|
||||
BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
|
||||
HumanFloatCount,
|
||||
};
|
||||
#[cfg(feature = "in_memory")]
|
||||
pub use crate::in_memory::InMemoryTerm;
|
||||
pub use crate::iter::{ProgressBarIter, ProgressIterator};
|
||||
pub use crate::multi::{MultiProgress, MultiProgressAlignment};
|
||||
pub use crate::progress_bar::{ProgressBar, WeakProgressBar};
|
||||
#[cfg(feature = "rayon")]
|
||||
pub use crate::rayon::ParallelProgressIterator;
|
||||
pub use crate::state::{ProgressFinish, ProgressState};
|
||||
pub use crate::style::ProgressStyle;
|
||||
pub use crate::term_like::TermLike;
|
688
vendor/indicatif/src/multi.rs
vendored
Normal file
688
vendor/indicatif/src/multi.rs
vendored
Normal file
@ -0,0 +1,688 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::io;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::panicking;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::draw_target::{DrawState, DrawStateWrapper, LineAdjust, ProgressDrawTarget};
|
||||
use crate::progress_bar::ProgressBar;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use instant::Instant;
|
||||
|
||||
/// Manages multiple progress bars from different threads
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiProgress {
|
||||
pub(crate) state: Arc<RwLock<MultiState>>,
|
||||
}
|
||||
|
||||
impl Default for MultiProgress {
|
||||
fn default() -> Self {
|
||||
Self::with_draw_target(ProgressDrawTarget::stderr())
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiProgress {
|
||||
/// Creates a new multi progress object.
|
||||
///
|
||||
/// Progress bars added to this object by default draw directly to stderr, and refresh
|
||||
/// a maximum of 15 times a second. To change the refresh rate set the draw target to
|
||||
/// one with a different refresh rate.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Creates a new multi progress object with the given draw target.
|
||||
pub fn with_draw_target(draw_target: ProgressDrawTarget) -> Self {
|
||||
Self {
|
||||
state: Arc::new(RwLock::new(MultiState::new(draw_target))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a different draw target for the multiprogress bar.
|
||||
pub fn set_draw_target(&self, target: ProgressDrawTarget) {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.draw_target.disconnect(Instant::now());
|
||||
state.draw_target = target;
|
||||
}
|
||||
|
||||
/// Set whether we should try to move the cursor when possible instead of clearing lines.
|
||||
///
|
||||
/// This can reduce flickering, but do not enable it if you intend to change the number of
|
||||
/// progress bars.
|
||||
pub fn set_move_cursor(&self, move_cursor: bool) {
|
||||
self.state.write().unwrap().move_cursor = move_cursor;
|
||||
}
|
||||
|
||||
/// Set alignment flag
|
||||
pub fn set_alignment(&self, alignment: MultiProgressAlignment) {
|
||||
self.state.write().unwrap().alignment = alignment;
|
||||
}
|
||||
|
||||
/// Adds a progress bar.
|
||||
///
|
||||
/// The progress bar added will have the draw target changed to a
|
||||
/// remote draw target that is intercepted by the multi progress
|
||||
/// object overriding custom `ProgressDrawTarget` settings.
|
||||
///
|
||||
/// Adding a progress bar that is already a member of the `MultiProgress`
|
||||
/// will have no effect.
|
||||
pub fn add(&self, pb: ProgressBar) -> ProgressBar {
|
||||
self.internalize(InsertLocation::End, pb)
|
||||
}
|
||||
|
||||
/// Inserts a progress bar.
|
||||
///
|
||||
/// The progress bar inserted at position `index` will have the draw
|
||||
/// target changed to a remote draw target that is intercepted by the
|
||||
/// multi progress object overriding custom `ProgressDrawTarget` settings.
|
||||
///
|
||||
/// If `index >= MultiProgressState::objects.len()`, the progress bar
|
||||
/// is added to the end of the list.
|
||||
///
|
||||
/// Inserting a progress bar that is already a member of the `MultiProgress`
|
||||
/// will have no effect.
|
||||
pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar {
|
||||
self.internalize(InsertLocation::Index(index), pb)
|
||||
}
|
||||
|
||||
/// Inserts a progress bar from the back.
|
||||
///
|
||||
/// The progress bar inserted at position `MultiProgressState::objects.len() - index`
|
||||
/// will have the draw target changed to a remote draw target that is
|
||||
/// intercepted by the multi progress object overriding custom
|
||||
/// `ProgressDrawTarget` settings.
|
||||
///
|
||||
/// If `index >= MultiProgressState::objects.len()`, the progress bar
|
||||
/// is added to the start of the list.
|
||||
///
|
||||
/// Inserting a progress bar that is already a member of the `MultiProgress`
|
||||
/// will have no effect.
|
||||
pub fn insert_from_back(&self, index: usize, pb: ProgressBar) -> ProgressBar {
|
||||
self.internalize(InsertLocation::IndexFromBack(index), pb)
|
||||
}
|
||||
|
||||
/// Inserts a progress bar before an existing one.
|
||||
///
|
||||
/// The progress bar added will have the draw target changed to a
|
||||
/// remote draw target that is intercepted by the multi progress
|
||||
/// object overriding custom `ProgressDrawTarget` settings.
|
||||
///
|
||||
/// Inserting a progress bar that is already a member of the `MultiProgress`
|
||||
/// will have no effect.
|
||||
pub fn insert_before(&self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar {
|
||||
self.internalize(InsertLocation::Before(before.index().unwrap()), pb)
|
||||
}
|
||||
|
||||
/// Inserts a progress bar after an existing one.
|
||||
///
|
||||
/// The progress bar added will have the draw target changed to a
|
||||
/// remote draw target that is intercepted by the multi progress
|
||||
/// object overriding custom `ProgressDrawTarget` settings.
|
||||
///
|
||||
/// Inserting a progress bar that is already a member of the `MultiProgress`
|
||||
/// will have no effect.
|
||||
pub fn insert_after(&self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar {
|
||||
self.internalize(InsertLocation::After(after.index().unwrap()), pb)
|
||||
}
|
||||
|
||||
/// Removes a progress bar.
|
||||
///
|
||||
/// The progress bar is removed only if it was previously inserted or added
|
||||
/// by the methods `MultiProgress::insert` or `MultiProgress::add`.
|
||||
/// If the passed progress bar does not satisfy the condition above,
|
||||
/// the `remove` method does nothing.
|
||||
pub fn remove(&self, pb: &ProgressBar) {
|
||||
let mut state = pb.state();
|
||||
let idx = match &state.draw_target.remote() {
|
||||
Some((state, idx)) => {
|
||||
// Check that this progress bar is owned by the current MultiProgress.
|
||||
assert!(Arc::ptr_eq(&self.state, state));
|
||||
*idx
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
state.draw_target = ProgressDrawTarget::hidden();
|
||||
self.state.write().unwrap().remove_idx(idx);
|
||||
}
|
||||
|
||||
fn internalize(&self, location: InsertLocation, pb: ProgressBar) -> ProgressBar {
|
||||
let mut state = self.state.write().unwrap();
|
||||
let idx = state.insert(location);
|
||||
drop(state);
|
||||
|
||||
pb.set_draw_target(ProgressDrawTarget::new_remote(self.state.clone(), idx));
|
||||
pb
|
||||
}
|
||||
|
||||
/// Print a log line above all progress bars in the [`MultiProgress`]
|
||||
///
|
||||
/// If the draw target is hidden (e.g. when standard output is not a terminal), `println()`
|
||||
/// will not do anything.
|
||||
pub fn println<I: AsRef<str>>(&self, msg: I) -> io::Result<()> {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.println(msg, Instant::now())
|
||||
}
|
||||
|
||||
/// Hide all progress bars temporarily, execute `f`, then redraw the [`MultiProgress`]
|
||||
///
|
||||
/// Executes 'f' even if the draw target is hidden.
|
||||
///
|
||||
/// Useful for external code that writes to the standard output.
|
||||
///
|
||||
/// **Note:** The internal lock is held while `f` is executed. Other threads trying to print
|
||||
/// anything on the progress bar will be blocked until `f` finishes.
|
||||
/// Therefore, it is recommended to avoid long-running operations in `f`.
|
||||
pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.suspend(f, Instant::now())
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> io::Result<()> {
|
||||
self.state.write().unwrap().clear(Instant::now())
|
||||
}
|
||||
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.state.read().unwrap().is_hidden()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MultiState {
|
||||
/// The collection of states corresponding to progress bars
|
||||
members: Vec<MultiStateMember>,
|
||||
/// Set of removed bars, should have corresponding members in the `members` vector with a
|
||||
/// `draw_state` of `None`.
|
||||
free_set: Vec<usize>,
|
||||
/// Indices to the `draw_states` to maintain correct visual order
|
||||
ordering: Vec<usize>,
|
||||
/// Target for draw operation for MultiProgress
|
||||
draw_target: ProgressDrawTarget,
|
||||
/// Whether or not to just move cursor instead of clearing lines
|
||||
move_cursor: bool,
|
||||
/// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
|
||||
alignment: MultiProgressAlignment,
|
||||
/// Lines to be drawn above everything else in the MultiProgress. These specifically come from
|
||||
/// calling `ProgressBar::println` on a pb that is connected to a `MultiProgress`.
|
||||
orphan_lines: Vec<String>,
|
||||
/// The count of currently visible zombie lines.
|
||||
zombie_lines_count: usize,
|
||||
}
|
||||
|
||||
impl MultiState {
|
||||
fn new(draw_target: ProgressDrawTarget) -> Self {
|
||||
Self {
|
||||
members: vec![],
|
||||
free_set: vec![],
|
||||
ordering: vec![],
|
||||
draw_target,
|
||||
move_cursor: false,
|
||||
alignment: MultiProgressAlignment::default(),
|
||||
orphan_lines: Vec::new(),
|
||||
zombie_lines_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_zombie(&mut self, index: usize) {
|
||||
let member = &mut self.members[index];
|
||||
|
||||
// If the zombie is the first visual bar then we can reap it right now instead of
|
||||
// deferring it to the next draw.
|
||||
if index != self.ordering.first().copied().unwrap() {
|
||||
member.is_zombie = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let line_count = member
|
||||
.draw_state
|
||||
.as_ref()
|
||||
.map(|d| d.lines.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Track the total number of zombie lines on the screen
|
||||
self.zombie_lines_count = self.zombie_lines_count.saturating_add(line_count);
|
||||
|
||||
// Make `DrawTarget` forget about the zombie lines so that they aren't cleared on next draw.
|
||||
self.draw_target
|
||||
.adjust_last_line_count(LineAdjust::Keep(line_count));
|
||||
|
||||
self.remove_idx(index);
|
||||
}
|
||||
|
||||
pub(crate) fn draw(
|
||||
&mut self,
|
||||
mut force_draw: bool,
|
||||
extra_lines: Option<Vec<String>>,
|
||||
now: Instant,
|
||||
) -> io::Result<()> {
|
||||
if panicking() {
|
||||
return Ok(());
|
||||
}
|
||||
let width = self.width() as f64;
|
||||
// Calculate real length based on terminal width
|
||||
// This take in account linewrap from terminal
|
||||
fn real_len(lines: &[String], width: f64) -> usize {
|
||||
lines.iter().fold(0, |sum, val| {
|
||||
sum + (console::measure_text_width(val) as f64 / width).ceil() as usize
|
||||
})
|
||||
}
|
||||
|
||||
// Assumption: if extra_lines is not None, then it has at least one line
|
||||
debug_assert_eq!(
|
||||
extra_lines.is_some(),
|
||||
extra_lines.as_ref().map(Vec::len).unwrap_or_default() > 0
|
||||
);
|
||||
|
||||
let mut reap_indices = vec![];
|
||||
|
||||
// Reap all consecutive 'zombie' progress bars from head of the list.
|
||||
let mut adjust = 0;
|
||||
for &index in &self.ordering {
|
||||
let member = &self.members[index];
|
||||
if !member.is_zombie {
|
||||
break;
|
||||
}
|
||||
|
||||
let line_count = member
|
||||
.draw_state
|
||||
.as_ref()
|
||||
.map(|d| real_len(&d.lines, width))
|
||||
.unwrap_or_default();
|
||||
// Track the total number of zombie lines on the screen.
|
||||
self.zombie_lines_count += line_count;
|
||||
|
||||
// Track the number of zombie lines that will be drawn by this call to draw.
|
||||
adjust += line_count;
|
||||
|
||||
reap_indices.push(index);
|
||||
}
|
||||
|
||||
// If this draw is due to a `println`, then we need to erase all the zombie lines.
|
||||
// This is because `println` is supposed to appear above all other elements in the
|
||||
// `MultiProgress`.
|
||||
if extra_lines.is_some() {
|
||||
self.draw_target
|
||||
.adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
|
||||
self.zombie_lines_count = 0;
|
||||
}
|
||||
|
||||
let orphan_lines_count = real_len(&self.orphan_lines, width);
|
||||
force_draw |= orphan_lines_count > 0;
|
||||
let mut drawable = match self.draw_target.drawable(force_draw, now) {
|
||||
Some(drawable) => drawable,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut draw_state = drawable.state();
|
||||
draw_state.orphan_lines_count = orphan_lines_count;
|
||||
draw_state.alignment = self.alignment;
|
||||
|
||||
if let Some(extra_lines) = &extra_lines {
|
||||
draw_state.lines.extend_from_slice(extra_lines.as_slice());
|
||||
draw_state.orphan_lines_count += real_len(extra_lines, width);
|
||||
}
|
||||
|
||||
// Add lines from `ProgressBar::println` call.
|
||||
draw_state.lines.append(&mut self.orphan_lines);
|
||||
|
||||
for index in &self.ordering {
|
||||
let member = &self.members[*index];
|
||||
if let Some(state) = &member.draw_state {
|
||||
draw_state.lines.extend_from_slice(&state.lines[..]);
|
||||
}
|
||||
}
|
||||
|
||||
drop(draw_state);
|
||||
let drawable = drawable.draw();
|
||||
|
||||
for index in reap_indices {
|
||||
self.remove_idx(index);
|
||||
}
|
||||
|
||||
// The zombie lines were drawn for the last time, so make `DrawTarget` forget about them
|
||||
// so they aren't cleared on next draw.
|
||||
if extra_lines.is_none() {
|
||||
self.draw_target
|
||||
.adjust_last_line_count(LineAdjust::Keep(adjust));
|
||||
}
|
||||
|
||||
drawable
|
||||
}
|
||||
|
||||
pub(crate) fn println<I: AsRef<str>>(&mut self, msg: I, now: Instant) -> io::Result<()> {
|
||||
let msg = msg.as_ref();
|
||||
|
||||
// If msg is "", make sure a line is still printed
|
||||
let lines: Vec<String> = match msg.is_empty() {
|
||||
false => msg.lines().map(Into::into).collect(),
|
||||
true => vec![String::new()],
|
||||
};
|
||||
|
||||
self.draw(true, Some(lines), now)
|
||||
}
|
||||
|
||||
pub(crate) fn draw_state(&mut self, idx: usize) -> DrawStateWrapper<'_> {
|
||||
let member = self.members.get_mut(idx).unwrap();
|
||||
// alignment is handled by the `MultiProgress`'s underlying draw target, so there is no
|
||||
// point in propagating it here.
|
||||
let state = member.draw_state.get_or_insert(DrawState {
|
||||
move_cursor: self.move_cursor,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
DrawStateWrapper::for_multi(state, &mut self.orphan_lines)
|
||||
}
|
||||
|
||||
pub(crate) fn is_hidden(&self) -> bool {
|
||||
self.draw_target.is_hidden()
|
||||
}
|
||||
|
||||
pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, f: F, now: Instant) -> R {
|
||||
self.clear(now).unwrap();
|
||||
let ret = f();
|
||||
self.draw(true, None, Instant::now()).unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn width(&self) -> u16 {
|
||||
self.draw_target.width()
|
||||
}
|
||||
|
||||
fn insert(&mut self, location: InsertLocation) -> usize {
|
||||
let idx = if let Some(idx) = self.free_set.pop() {
|
||||
self.members[idx] = MultiStateMember::default();
|
||||
idx
|
||||
} else {
|
||||
self.members.push(MultiStateMember::default());
|
||||
self.members.len() - 1
|
||||
};
|
||||
|
||||
match location {
|
||||
InsertLocation::End => self.ordering.push(idx),
|
||||
InsertLocation::Index(pos) => {
|
||||
let pos = Ord::min(pos, self.ordering.len());
|
||||
self.ordering.insert(pos, idx);
|
||||
}
|
||||
InsertLocation::IndexFromBack(pos) => {
|
||||
let pos = self.ordering.len().saturating_sub(pos);
|
||||
self.ordering.insert(pos, idx);
|
||||
}
|
||||
InsertLocation::After(after_idx) => {
|
||||
let pos = self.ordering.iter().position(|i| *i == after_idx).unwrap();
|
||||
self.ordering.insert(pos + 1, idx);
|
||||
}
|
||||
InsertLocation::Before(before_idx) => {
|
||||
let pos = self.ordering.iter().position(|i| *i == before_idx).unwrap();
|
||||
self.ordering.insert(pos, idx);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.len(),
|
||||
self.ordering.len(),
|
||||
"Draw state is inconsistent"
|
||||
);
|
||||
|
||||
idx
|
||||
}
|
||||
|
||||
fn clear(&mut self, now: Instant) -> io::Result<()> {
|
||||
match self.draw_target.drawable(true, now) {
|
||||
Some(mut drawable) => {
|
||||
// Make the clear operation also wipe out zombie lines
|
||||
drawable.adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
|
||||
self.zombie_lines_count = 0;
|
||||
drawable.clear()
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_idx(&mut self, idx: usize) {
|
||||
if self.free_set.contains(&idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.members[idx] = MultiStateMember::default();
|
||||
self.free_set.push(idx);
|
||||
self.ordering.retain(|&x| x != idx);
|
||||
|
||||
assert_eq!(
|
||||
self.len(),
|
||||
self.ordering.len(),
|
||||
"Draw state is inconsistent"
|
||||
);
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.members.len() - self.free_set.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MultiStateMember {
|
||||
/// Draw state will be `None` for members that haven't been drawn before, or for entries that
|
||||
/// correspond to something in the free set.
|
||||
draw_state: Option<DrawState>,
|
||||
/// Whether the corresponding progress bar (more precisely, `BarState`) has been dropped.
|
||||
is_zombie: bool,
|
||||
}
|
||||
|
||||
impl Debug for MultiStateMember {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MultiStateElement")
|
||||
.field("draw_state", &self.draw_state)
|
||||
.field("is_zombie", &self.is_zombie)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Vertical alignment of a multi progress.
|
||||
///
|
||||
/// The alignment controls how the multi progress is aligned if some of its progress bars get removed.
|
||||
/// E.g. `Top` alignment (default), when _progress bar 2_ is removed:
|
||||
/// ```ignore
|
||||
/// [0/100] progress bar 1 [0/100] progress bar 1
|
||||
/// [0/100] progress bar 2 => [0/100] progress bar 3
|
||||
/// [0/100] progress bar 3
|
||||
/// ```
|
||||
///
|
||||
/// `Bottom` alignment
|
||||
/// ```ignore
|
||||
/// [0/100] progress bar 1
|
||||
/// [0/100] progress bar 2 => [0/100] progress bar 1
|
||||
/// [0/100] progress bar 3 [0/100] progress bar 3
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum MultiProgressAlignment {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Default for MultiProgressAlignment {
|
||||
fn default() -> Self {
|
||||
Self::Top
|
||||
}
|
||||
}
|
||||
|
||||
enum InsertLocation {
|
||||
End,
|
||||
Index(usize),
|
||||
IndexFromBack(usize),
|
||||
After(usize),
|
||||
Before(usize),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
|
||||
|
||||
#[test]
|
||||
fn late_pb_drop() {
|
||||
let pb = ProgressBar::new(10);
|
||||
let mpb = MultiProgress::new();
|
||||
// This clone call is required to trigger a now fixed bug.
|
||||
// See <https://github.com/console-rs/indicatif/pull/141> for context
|
||||
#[allow(clippy::redundant_clone)]
|
||||
mpb.add(pb.clone());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_bar_sync_send() {
|
||||
let _: Box<dyn Sync> = Box::new(ProgressBar::new(1));
|
||||
let _: Box<dyn Send> = Box::new(ProgressBar::new(1));
|
||||
let _: Box<dyn Sync> = Box::new(MultiProgress::new());
|
||||
let _: Box<dyn Send> = Box::new(MultiProgress::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_hidden() {
|
||||
let mpb = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
|
||||
let pb = mpb.add(ProgressBar::new(123));
|
||||
pb.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_modifications() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
let p2 = mp.add(ProgressBar::new(1));
|
||||
let p3 = mp.add(ProgressBar::new(1));
|
||||
mp.remove(&p2);
|
||||
mp.remove(&p1);
|
||||
let p4 = mp.insert(1, ProgressBar::new(1));
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
// the removed place for p1 is reused
|
||||
assert_eq!(state.members.len(), 4);
|
||||
assert_eq!(state.len(), 3);
|
||||
|
||||
// free_set may contain 1 or 2
|
||||
match state.free_set.last() {
|
||||
Some(1) => {
|
||||
assert_eq!(state.ordering, vec![0, 2, 3]);
|
||||
assert!(state.members[1].draw_state.is_none());
|
||||
assert_eq!(p4.index().unwrap(), 2);
|
||||
}
|
||||
Some(2) => {
|
||||
assert_eq!(state.ordering, vec![0, 1, 3]);
|
||||
assert!(state.members[2].draw_state.is_none());
|
||||
assert_eq!(p4.index().unwrap(), 1);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
assert_eq!(p0.index().unwrap(), 0);
|
||||
assert_eq!(p1.index(), None);
|
||||
assert_eq!(p2.index(), None);
|
||||
assert_eq!(p3.index().unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_insert_from_back() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
let p2 = mp.add(ProgressBar::new(1));
|
||||
let p3 = mp.insert_from_back(1, ProgressBar::new(1));
|
||||
let p4 = mp.insert_from_back(10, ProgressBar::new(1));
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
assert_eq!(state.ordering, vec![4, 0, 1, 3, 2]);
|
||||
assert_eq!(p0.index().unwrap(), 0);
|
||||
assert_eq!(p1.index().unwrap(), 1);
|
||||
assert_eq!(p2.index().unwrap(), 2);
|
||||
assert_eq!(p3.index().unwrap(), 3);
|
||||
assert_eq!(p4.index().unwrap(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_insert_after() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
let p2 = mp.add(ProgressBar::new(1));
|
||||
let p3 = mp.insert_after(&p2, ProgressBar::new(1));
|
||||
let p4 = mp.insert_after(&p0, ProgressBar::new(1));
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
assert_eq!(state.ordering, vec![0, 4, 1, 2, 3]);
|
||||
assert_eq!(p0.index().unwrap(), 0);
|
||||
assert_eq!(p1.index().unwrap(), 1);
|
||||
assert_eq!(p2.index().unwrap(), 2);
|
||||
assert_eq!(p3.index().unwrap(), 3);
|
||||
assert_eq!(p4.index().unwrap(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_insert_before() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
let p2 = mp.add(ProgressBar::new(1));
|
||||
let p3 = mp.insert_before(&p0, ProgressBar::new(1));
|
||||
let p4 = mp.insert_before(&p2, ProgressBar::new(1));
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
assert_eq!(state.ordering, vec![3, 0, 1, 4, 2]);
|
||||
assert_eq!(p0.index().unwrap(), 0);
|
||||
assert_eq!(p1.index().unwrap(), 1);
|
||||
assert_eq!(p2.index().unwrap(), 2);
|
||||
assert_eq!(p3.index().unwrap(), 3);
|
||||
assert_eq!(p4.index().unwrap(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_insert_before_and_after() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
let p2 = mp.add(ProgressBar::new(1));
|
||||
let p3 = mp.insert_before(&p0, ProgressBar::new(1));
|
||||
let p4 = mp.insert_after(&p3, ProgressBar::new(1));
|
||||
let p5 = mp.insert_after(&p3, ProgressBar::new(1));
|
||||
let p6 = mp.insert_before(&p1, ProgressBar::new(1));
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
assert_eq!(state.ordering, vec![3, 5, 4, 0, 6, 1, 2]);
|
||||
assert_eq!(p0.index().unwrap(), 0);
|
||||
assert_eq!(p1.index().unwrap(), 1);
|
||||
assert_eq!(p2.index().unwrap(), 2);
|
||||
assert_eq!(p3.index().unwrap(), 3);
|
||||
assert_eq!(p4.index().unwrap(), 4);
|
||||
assert_eq!(p5.index().unwrap(), 5);
|
||||
assert_eq!(p6.index().unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_progress_multiple_remove() {
|
||||
let mp = MultiProgress::new();
|
||||
let p0 = mp.add(ProgressBar::new(1));
|
||||
let p1 = mp.add(ProgressBar::new(1));
|
||||
// double remove beyond the first one have no effect
|
||||
mp.remove(&p0);
|
||||
mp.remove(&p0);
|
||||
mp.remove(&p0);
|
||||
|
||||
let state = mp.state.read().unwrap();
|
||||
// the removed place for p1 is reused
|
||||
assert_eq!(state.members.len(), 2);
|
||||
assert_eq!(state.free_set.len(), 1);
|
||||
assert_eq!(state.len(), 1);
|
||||
assert!(state.members[0].draw_state.is_none());
|
||||
assert_eq!(state.free_set.last(), Some(&0));
|
||||
|
||||
assert_eq!(state.ordering, vec![1]);
|
||||
assert_eq!(p0.index(), None);
|
||||
assert_eq!(p1.index().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mp_no_crash_double_add() {
|
||||
let mp = MultiProgress::new();
|
||||
let pb = mp.add(ProgressBar::new(10));
|
||||
mp.add(pb);
|
||||
}
|
||||
}
|
808
vendor/indicatif/src/progress_bar.rs
vendored
Normal file
808
vendor/indicatif/src/progress_bar.rs
vendored
Normal file
@ -0,0 +1,808 @@
|
||||
#[cfg(test)]
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
|
||||
use std::time::Duration;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Instant;
|
||||
use std::{fmt, io, thread};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use instant::Instant;
|
||||
#[cfg(test)]
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::draw_target::ProgressDrawTarget;
|
||||
use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset, TabExpandedString};
|
||||
use crate::style::ProgressStyle;
|
||||
use crate::{ProgressBarIter, ProgressIterator, ProgressState};
|
||||
|
||||
/// A progress bar or spinner
|
||||
///
|
||||
/// The progress bar is an [`Arc`] around its internal state. When the progress bar is cloned it
|
||||
/// just increments the refcount (so the original and its clone share the same state).
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressBar {
|
||||
state: Arc<Mutex<BarState>>,
|
||||
pos: Arc<AtomicPosition>,
|
||||
ticker: Arc<Mutex<Option<Ticker>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ProgressBar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ProgressBar").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
/// Creates a new progress bar with a given length
|
||||
///
|
||||
/// This progress bar by default draws directly to stderr, and refreshes a maximum of 15 times
|
||||
/// a second. To change the refresh rate, set the draw target to one with a different refresh
|
||||
/// rate.
|
||||
pub fn new(len: u64) -> Self {
|
||||
Self::with_draw_target(Some(len), ProgressDrawTarget::stderr())
|
||||
}
|
||||
|
||||
/// Creates a completely hidden progress bar
|
||||
///
|
||||
/// This progress bar still responds to API changes but it does not have a length or render in
|
||||
/// any way.
|
||||
pub fn hidden() -> Self {
|
||||
Self::with_draw_target(None, ProgressDrawTarget::hidden())
|
||||
}
|
||||
|
||||
/// Creates a new progress bar with a given length and draw target
|
||||
pub fn with_draw_target(len: Option<u64>, draw_target: ProgressDrawTarget) -> Self {
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(BarState::new(len, draw_target, pos.clone()))),
|
||||
pos,
|
||||
ticker: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a clone of the current progress bar style.
|
||||
pub fn style(&self) -> ProgressStyle {
|
||||
self.state().style.clone()
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given style
|
||||
pub fn with_style(self, style: ProgressStyle) -> Self {
|
||||
self.set_style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given tab width
|
||||
pub fn with_tab_width(self, tab_width: usize) -> Self {
|
||||
self.state().set_tab_width(tab_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given prefix
|
||||
///
|
||||
/// For the prefix to be visible, the `{prefix}` placeholder must be present in the template
|
||||
/// (see [`ProgressStyle`]).
|
||||
pub fn with_prefix(self, prefix: impl Into<Cow<'static, str>>) -> Self {
|
||||
let mut state = self.state();
|
||||
state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width);
|
||||
drop(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given message
|
||||
///
|
||||
/// For the message to be visible, the `{msg}` placeholder must be present in the template (see
|
||||
/// [`ProgressStyle`]).
|
||||
pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> Self {
|
||||
let mut state = self.state();
|
||||
state.state.message = TabExpandedString::new(message.into(), state.tab_width);
|
||||
drop(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given position
|
||||
pub fn with_position(self, pos: u64) -> Self {
|
||||
self.state().state.set_pos(pos);
|
||||
self
|
||||
}
|
||||
|
||||
/// A convenience builder-like function for a progress bar with a given elapsed time
|
||||
pub fn with_elapsed(self, elapsed: Duration) -> Self {
|
||||
self.state().state.started = Instant::now().checked_sub(elapsed).unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the finish behavior for the progress bar
|
||||
///
|
||||
/// This behavior is invoked when [`ProgressBar`] or
|
||||
/// [`ProgressBarIter`] completes and
|
||||
/// [`ProgressBar::is_finished()`] is false.
|
||||
/// If you don't want the progress bar to be automatically finished then
|
||||
/// call `on_finish(None)`.
|
||||
///
|
||||
/// [`ProgressBar`]: crate::ProgressBar
|
||||
/// [`ProgressBarIter`]: crate::ProgressBarIter
|
||||
/// [`ProgressBar::is_finished()`]: crate::ProgressBar::is_finished
|
||||
pub fn with_finish(self, finish: ProgressFinish) -> Self {
|
||||
self.state().on_finish = finish;
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new spinner
|
||||
///
|
||||
/// This spinner by default draws directly to stderr. This adds the default spinner style to it.
|
||||
pub fn new_spinner() -> Self {
|
||||
let rv = Self::with_draw_target(None, ProgressDrawTarget::stderr());
|
||||
rv.set_style(ProgressStyle::default_spinner());
|
||||
rv
|
||||
}
|
||||
|
||||
/// Overrides the stored style
|
||||
///
|
||||
/// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it.
|
||||
pub fn set_style(&self, style: ProgressStyle) {
|
||||
self.state().set_style(style);
|
||||
}
|
||||
|
||||
/// Sets the tab width (default: 8). All tabs will be expanded to this many spaces.
|
||||
pub fn set_tab_width(&mut self, tab_width: usize) {
|
||||
let mut state = self.state();
|
||||
state.set_tab_width(tab_width);
|
||||
state.draw(true, Instant::now()).unwrap();
|
||||
}
|
||||
|
||||
/// Spawns a background thread to tick the progress bar
|
||||
///
|
||||
/// When this is enabled a background thread will regularly tick the progress bar in the given
|
||||
/// interval. This is useful to advance progress bars that are very slow by themselves.
|
||||
///
|
||||
/// When steady ticks are enabled, calling [`ProgressBar::tick()`] on a progress bar does not
|
||||
/// have any effect.
|
||||
pub fn enable_steady_tick(&self, interval: Duration) {
|
||||
// The way we test for ticker termination is with a single static `AtomicBool`. Since cargo
|
||||
// runs tests concurrently, we have a `TICKER_TEST` lock to make sure tests using ticker
|
||||
// don't step on each other. This check catches attempts to use tickers in tests without
|
||||
// acquiring the lock.
|
||||
#[cfg(test)]
|
||||
{
|
||||
let guard = TICKER_TEST.try_lock();
|
||||
let lock_acquired = guard.is_ok();
|
||||
// Drop the guard before panicking to avoid poisoning the lock (which would cause other
|
||||
// ticker tests to fail)
|
||||
drop(guard);
|
||||
if lock_acquired {
|
||||
panic!("you must acquire the TICKER_TEST lock in your test to use this method");
|
||||
}
|
||||
}
|
||||
|
||||
if interval.is_zero() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stop_and_replace_ticker(Some(interval));
|
||||
}
|
||||
|
||||
/// Undoes [`ProgressBar::enable_steady_tick()`]
|
||||
pub fn disable_steady_tick(&self) {
|
||||
self.stop_and_replace_ticker(None);
|
||||
}
|
||||
|
||||
fn stop_and_replace_ticker(&self, interval: Option<Duration>) {
|
||||
let mut ticker_state = self.ticker.lock().unwrap();
|
||||
if let Some(ticker) = ticker_state.take() {
|
||||
ticker.stop();
|
||||
}
|
||||
|
||||
*ticker_state = interval.map(|interval| Ticker::new(interval, &self.state));
|
||||
}
|
||||
|
||||
/// Manually ticks the spinner or progress bar
|
||||
///
|
||||
/// This automatically happens on any other change to a progress bar.
|
||||
pub fn tick(&self) {
|
||||
self.tick_inner(Instant::now());
|
||||
}
|
||||
|
||||
fn tick_inner(&self, now: Instant) {
|
||||
// Only tick if a `Ticker` isn't installed
|
||||
if self.ticker.lock().unwrap().is_none() {
|
||||
self.state().tick(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the position of the progress bar by `delta`
|
||||
pub fn inc(&self, delta: u64) {
|
||||
self.pos.inc(delta);
|
||||
let now = Instant::now();
|
||||
if self.pos.allow(now) {
|
||||
self.tick_inner(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// A quick convenience check if the progress bar is hidden
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.state().draw_target.is_hidden()
|
||||
}
|
||||
|
||||
/// Indicates that the progress bar finished
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.state().state.is_finished()
|
||||
}
|
||||
|
||||
/// Print a log line above the progress bar
|
||||
///
|
||||
/// If the progress bar is hidden (e.g. when standard output is not a terminal), `println()`
|
||||
/// will not do anything. If you want to write to the standard output in such cases as well, use
|
||||
/// [`suspend`] instead.
|
||||
///
|
||||
/// If the progress bar was added to a [`MultiProgress`], the log line will be
|
||||
/// printed above all other progress bars.
|
||||
///
|
||||
/// [`suspend`]: ProgressBar::suspend
|
||||
/// [`MultiProgress`]: crate::MultiProgress
|
||||
pub fn println<I: AsRef<str>>(&self, msg: I) {
|
||||
self.state().println(Instant::now(), msg.as_ref());
|
||||
}
|
||||
|
||||
/// Update the `ProgressBar`'s inner [`ProgressState`]
|
||||
pub fn update(&self, f: impl FnOnce(&mut ProgressState)) {
|
||||
self.state()
|
||||
.update(Instant::now(), f, self.ticker.lock().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// Sets the position of the progress bar
|
||||
pub fn set_position(&self, pos: u64) {
|
||||
self.pos.set(pos);
|
||||
let now = Instant::now();
|
||||
if self.pos.allow(now) {
|
||||
self.tick_inner(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the length of the progress bar
|
||||
pub fn set_length(&self, len: u64) {
|
||||
self.state().set_length(Instant::now(), len);
|
||||
}
|
||||
|
||||
/// Increase the length of the progress bar
|
||||
pub fn inc_length(&self, delta: u64) {
|
||||
self.state().inc_length(Instant::now(), delta);
|
||||
}
|
||||
|
||||
/// Sets the current prefix of the progress bar
|
||||
///
|
||||
/// For the prefix to be visible, the `{prefix}` placeholder must be present in the template
|
||||
/// (see [`ProgressStyle`]).
|
||||
pub fn set_prefix(&self, prefix: impl Into<Cow<'static, str>>) {
|
||||
let mut state = self.state();
|
||||
state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width);
|
||||
state.update_estimate_and_draw(Instant::now());
|
||||
}
|
||||
|
||||
/// Sets the current message of the progress bar
|
||||
///
|
||||
/// For the message to be visible, the `{msg}` placeholder must be present in the template (see
|
||||
/// [`ProgressStyle`]).
|
||||
pub fn set_message(&self, msg: impl Into<Cow<'static, str>>) {
|
||||
let mut state = self.state();
|
||||
state.state.message = TabExpandedString::new(msg.into(), state.tab_width);
|
||||
state.update_estimate_and_draw(Instant::now());
|
||||
}
|
||||
|
||||
/// Creates a new weak reference to this `ProgressBar`
|
||||
pub fn downgrade(&self) -> WeakProgressBar {
|
||||
WeakProgressBar {
|
||||
state: Arc::downgrade(&self.state),
|
||||
pos: Arc::downgrade(&self.pos),
|
||||
ticker: Arc::downgrade(&self.ticker),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the ETA calculation
|
||||
///
|
||||
/// This can be useful if the progress bars made a large jump or was paused for a prolonged
|
||||
/// time.
|
||||
pub fn reset_eta(&self) {
|
||||
self.state().reset(Instant::now(), Reset::Eta);
|
||||
}
|
||||
|
||||
/// Resets elapsed time and the ETA calculation
|
||||
pub fn reset_elapsed(&self) {
|
||||
self.state().reset(Instant::now(), Reset::Elapsed);
|
||||
}
|
||||
|
||||
/// Resets all of the progress bar state
|
||||
pub fn reset(&self) {
|
||||
self.state().reset(Instant::now(), Reset::All);
|
||||
}
|
||||
|
||||
/// Finishes the progress bar and leaves the current message
|
||||
pub fn finish(&self) {
|
||||
self.state()
|
||||
.finish_using_style(Instant::now(), ProgressFinish::AndLeave);
|
||||
}
|
||||
|
||||
/// Finishes the progress bar and sets a message
|
||||
///
|
||||
/// For the message to be visible, the `{msg}` placeholder must be present in the template (see
|
||||
/// [`ProgressStyle`]).
|
||||
pub fn finish_with_message(&self, msg: impl Into<Cow<'static, str>>) {
|
||||
self.state()
|
||||
.finish_using_style(Instant::now(), ProgressFinish::WithMessage(msg.into()));
|
||||
}
|
||||
|
||||
/// Finishes the progress bar and completely clears it
|
||||
pub fn finish_and_clear(&self) {
|
||||
self.state()
|
||||
.finish_using_style(Instant::now(), ProgressFinish::AndClear);
|
||||
}
|
||||
|
||||
/// Finishes the progress bar and leaves the current message and progress
|
||||
pub fn abandon(&self) {
|
||||
self.state()
|
||||
.finish_using_style(Instant::now(), ProgressFinish::Abandon);
|
||||
}
|
||||
|
||||
/// Finishes the progress bar and sets a message, and leaves the current progress
|
||||
///
|
||||
/// For the message to be visible, the `{msg}` placeholder must be present in the template (see
|
||||
/// [`ProgressStyle`]).
|
||||
pub fn abandon_with_message(&self, msg: impl Into<Cow<'static, str>>) {
|
||||
self.state().finish_using_style(
|
||||
Instant::now(),
|
||||
ProgressFinish::AbandonWithMessage(msg.into()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Finishes the progress bar using the behavior stored in the [`ProgressStyle`]
|
||||
///
|
||||
/// See [`ProgressBar::with_finish()`].
|
||||
pub fn finish_using_style(&self) {
|
||||
let mut state = self.state();
|
||||
let finish = state.on_finish.clone();
|
||||
state.finish_using_style(Instant::now(), finish);
|
||||
}
|
||||
|
||||
/// Sets a different draw target for the progress bar
|
||||
///
|
||||
/// This can be used to draw the progress bar to stderr (this is the default):
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use indicatif::{ProgressBar, ProgressDrawTarget};
|
||||
/// let pb = ProgressBar::new(100);
|
||||
/// pb.set_draw_target(ProgressDrawTarget::stderr());
|
||||
/// ```
|
||||
///
|
||||
/// **Note:** Calling this method on a [`ProgressBar`] linked with a [`MultiProgress`] (after
|
||||
/// running [`MultiProgress::add`]) will unlink this progress bar. If you don't want this
|
||||
/// behavior, call [`MultiProgress::set_draw_target`] instead.
|
||||
///
|
||||
/// [`MultiProgress`]: crate::MultiProgress
|
||||
/// [`MultiProgress::add`]: crate::MultiProgress::add
|
||||
/// [`MultiProgress::set_draw_target`]: crate::MultiProgress::set_draw_target
|
||||
pub fn set_draw_target(&self, target: ProgressDrawTarget) {
|
||||
let mut state = self.state();
|
||||
state.draw_target.disconnect(Instant::now());
|
||||
state.draw_target = target;
|
||||
}
|
||||
|
||||
/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar
|
||||
///
|
||||
/// Useful for external code that writes to the standard output.
|
||||
///
|
||||
/// If the progress bar was added to a MultiProgress, it will suspend the entire MultiProgress
|
||||
///
|
||||
/// **Note:** The internal lock is held while `f` is executed. Other threads trying to print
|
||||
/// anything on the progress bar will be blocked until `f` finishes.
|
||||
/// Therefore, it is recommended to avoid long-running operations in `f`.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// let mut pb = ProgressBar::new(3);
|
||||
/// pb.suspend(|| {
|
||||
/// println!("Log message");
|
||||
/// })
|
||||
/// ```
|
||||
pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
|
||||
self.state().suspend(Instant::now(), f)
|
||||
}
|
||||
|
||||
/// Wraps an [`Iterator`] with the progress bar
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// let v = vec![1, 2, 3];
|
||||
/// let pb = ProgressBar::new(3);
|
||||
/// for item in pb.wrap_iter(v.iter()) {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
pub fn wrap_iter<It: Iterator>(&self, it: It) -> ProgressBarIter<It> {
|
||||
it.progress_with(self.clone())
|
||||
}
|
||||
|
||||
/// Wraps an [`io::Read`] with the progress bar
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::fs::File;
|
||||
/// # use std::io;
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// # fn test () -> io::Result<()> {
|
||||
/// let source = File::open("work.txt")?;
|
||||
/// let mut target = File::create("done.txt")?;
|
||||
/// let pb = ProgressBar::new(source.metadata()?.len());
|
||||
/// io::copy(&mut pb.wrap_read(source), &mut target);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn wrap_read<R: io::Read>(&self, read: R) -> ProgressBarIter<R> {
|
||||
ProgressBarIter {
|
||||
progress: self.clone(),
|
||||
it: read,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps an [`io::Write`] with the progress bar
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::fs::File;
|
||||
/// # use std::io;
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// # fn test () -> io::Result<()> {
|
||||
/// let mut source = File::open("work.txt")?;
|
||||
/// let target = File::create("done.txt")?;
|
||||
/// let pb = ProgressBar::new(source.metadata()?.len());
|
||||
/// io::copy(&mut source, &mut pb.wrap_write(target));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn wrap_write<W: io::Write>(&self, write: W) -> ProgressBarIter<W> {
|
||||
ProgressBarIter {
|
||||
progress: self.clone(),
|
||||
it: write,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
/// Wraps an [`tokio::io::AsyncWrite`] with the progress bar
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use tokio::fs::File;
|
||||
/// # use tokio::io;
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// # async fn test() -> io::Result<()> {
|
||||
/// let mut source = File::open("work.txt").await?;
|
||||
/// let mut target = File::open("done.txt").await?;
|
||||
/// let pb = ProgressBar::new(source.metadata().await?.len());
|
||||
/// io::copy(&mut source, &mut pb.wrap_async_write(target)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn wrap_async_write<W: tokio::io::AsyncWrite + Unpin>(
|
||||
&self,
|
||||
write: W,
|
||||
) -> ProgressBarIter<W> {
|
||||
ProgressBarIter {
|
||||
progress: self.clone(),
|
||||
it: write,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
/// Wraps an [`tokio::io::AsyncRead`] with the progress bar
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use tokio::fs::File;
|
||||
/// # use tokio::io;
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// # async fn test() -> io::Result<()> {
|
||||
/// let mut source = File::open("work.txt").await?;
|
||||
/// let mut target = File::open("done.txt").await?;
|
||||
/// let pb = ProgressBar::new(source.metadata().await?.len());
|
||||
/// io::copy(&mut pb.wrap_async_read(source), &mut target).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn wrap_async_read<R: tokio::io::AsyncRead + Unpin>(&self, read: R) -> ProgressBarIter<R> {
|
||||
ProgressBarIter {
|
||||
progress: self.clone(),
|
||||
it: read,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a [`futures::Stream`](https://docs.rs/futures/0.3/futures/stream/trait.StreamExt.html) with the progress bar
|
||||
///
|
||||
/// ```
|
||||
/// # use indicatif::ProgressBar;
|
||||
/// # futures::executor::block_on(async {
|
||||
/// use futures::stream::{self, StreamExt};
|
||||
/// let pb = ProgressBar::new(10);
|
||||
/// let mut stream = pb.wrap_stream(stream::iter('a'..='z'));
|
||||
///
|
||||
/// assert_eq!(stream.next().await, Some('a'));
|
||||
/// assert_eq!(stream.count().await, 25);
|
||||
/// # }); // block_on
|
||||
/// ```
|
||||
#[cfg(feature = "futures")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures")))]
|
||||
pub fn wrap_stream<S: futures_core::Stream>(&self, stream: S) -> ProgressBarIter<S> {
|
||||
ProgressBarIter {
|
||||
progress: self.clone(),
|
||||
it: stream,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current position
|
||||
pub fn position(&self) -> u64 {
|
||||
self.state().state.pos()
|
||||
}
|
||||
|
||||
/// Returns the current length
|
||||
pub fn length(&self) -> Option<u64> {
|
||||
self.state().state.len()
|
||||
}
|
||||
|
||||
/// Returns the current ETA
|
||||
pub fn eta(&self) -> Duration {
|
||||
self.state().state.eta()
|
||||
}
|
||||
|
||||
/// Returns the current rate of progress
|
||||
pub fn per_sec(&self) -> f64 {
|
||||
self.state().state.per_sec()
|
||||
}
|
||||
|
||||
/// Returns the current expected duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.state().state.duration()
|
||||
}
|
||||
|
||||
/// Returns the current elapsed time
|
||||
pub fn elapsed(&self) -> Duration {
|
||||
self.state().state.elapsed()
|
||||
}
|
||||
|
||||
/// Index in the `MultiState`
|
||||
pub(crate) fn index(&self) -> Option<usize> {
|
||||
self.state().draw_target.remote().map(|(_, idx)| idx)
|
||||
}
|
||||
|
||||
/// Current message
|
||||
pub fn message(&self) -> String {
|
||||
self.state().state.message.expanded().to_string()
|
||||
}
|
||||
|
||||
/// Current prefix
|
||||
pub fn prefix(&self) -> String {
|
||||
self.state().state.prefix.expanded().to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn state(&self) -> MutexGuard<'_, BarState> {
|
||||
self.state.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A weak reference to a `ProgressBar`.
|
||||
///
|
||||
/// Useful for creating custom steady tick implementations
|
||||
#[derive(Clone, Default)]
|
||||
pub struct WeakProgressBar {
|
||||
state: Weak<Mutex<BarState>>,
|
||||
pos: Weak<AtomicPosition>,
|
||||
ticker: Weak<Mutex<Option<Ticker>>>,
|
||||
}
|
||||
|
||||
impl WeakProgressBar {
|
||||
/// Create a new `WeakProgressBar` that returns `None` when [`upgrade`] is called.
|
||||
///
|
||||
/// [`upgrade`]: WeakProgressBar::upgrade
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Attempts to upgrade the Weak pointer to a [`ProgressBar`], delaying dropping of the inner
|
||||
/// value if successful. Returns `None` if the inner value has since been dropped.
|
||||
///
|
||||
/// [`ProgressBar`]: struct.ProgressBar.html
|
||||
pub fn upgrade(&self) -> Option<ProgressBar> {
|
||||
let state = self.state.upgrade()?;
|
||||
let pos = self.pos.upgrade()?;
|
||||
let ticker = self.ticker.upgrade()?;
|
||||
Some(ProgressBar { state, pos, ticker })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Ticker {
|
||||
stopping: Arc<(Mutex<bool>, Condvar)>,
|
||||
join_handle: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for Ticker {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
self.join_handle.take().map(|handle| handle.join());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
static TICKER_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
impl Ticker {
|
||||
pub(crate) fn new(interval: Duration, bar_state: &Arc<Mutex<BarState>>) -> Self {
|
||||
debug_assert!(!interval.is_zero());
|
||||
|
||||
// A `Mutex<bool>` is used as a flag to indicate whether the ticker was requested to stop.
|
||||
// The `Condvar` is used a notification mechanism: when the ticker is dropped, we notify
|
||||
// the thread and interrupt the ticker wait.
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let stopping = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let control = TickerControl {
|
||||
stopping: stopping.clone(),
|
||||
state: Arc::downgrade(bar_state),
|
||||
};
|
||||
|
||||
let join_handle = thread::spawn(move || control.run(interval));
|
||||
Self {
|
||||
stopping,
|
||||
join_handle: Some(join_handle),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stop(&self) {
|
||||
*self.stopping.0.lock().unwrap() = true;
|
||||
self.stopping.1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
struct TickerControl {
|
||||
stopping: Arc<(Mutex<bool>, Condvar)>,
|
||||
state: Weak<Mutex<BarState>>,
|
||||
}
|
||||
|
||||
impl TickerControl {
|
||||
fn run(&self, interval: Duration) {
|
||||
#[cfg(test)]
|
||||
TICKER_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
while let Some(arc) = self.state.upgrade() {
|
||||
let mut state = arc.lock().unwrap();
|
||||
if state.state.is_finished() {
|
||||
break;
|
||||
}
|
||||
|
||||
state.tick(Instant::now());
|
||||
|
||||
drop(state); // Don't forget to drop the lock before sleeping
|
||||
drop(arc); // Also need to drop Arc otherwise BarState won't be dropped
|
||||
|
||||
// Wait for `interval` but return early if we are notified to stop
|
||||
let (_, result) = self
|
||||
.stopping
|
||||
.1
|
||||
.wait_timeout_while(self.stopping.0.lock().unwrap(), interval, |stopped| {
|
||||
!*stopped
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// If the wait didn't time out, it means we were notified to stop
|
||||
if !result.timed_out() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
TICKER_RUNNING.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests using the global TICKER_RUNNING flag need to be serialized
|
||||
#[cfg(test)]
|
||||
pub(crate) static TICKER_TEST: Lazy<Mutex<()>> = Lazy::new(Mutex::default);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[allow(clippy::float_cmp)]
|
||||
#[test]
|
||||
fn test_pbar_zero() {
|
||||
let pb = ProgressBar::new(0);
|
||||
assert_eq!(pb.state().state.fraction(), 1.0);
|
||||
}
|
||||
|
||||
#[allow(clippy::float_cmp)]
|
||||
#[test]
|
||||
fn test_pbar_maxu64() {
|
||||
let pb = ProgressBar::new(!0);
|
||||
assert_eq!(pb.state().state.fraction(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pbar_overflow() {
|
||||
let pb = ProgressBar::new(1);
|
||||
pb.set_draw_target(ProgressDrawTarget::hidden());
|
||||
pb.inc(2);
|
||||
pb.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_position() {
|
||||
let pb = ProgressBar::new(1);
|
||||
pb.set_draw_target(ProgressDrawTarget::hidden());
|
||||
pb.inc(2);
|
||||
let pos = pb.position();
|
||||
assert_eq!(pos, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weak_pb() {
|
||||
let pb = ProgressBar::new(0);
|
||||
let weak = pb.downgrade();
|
||||
assert!(weak.upgrade().is_some());
|
||||
::std::mem::drop(pb);
|
||||
assert!(weak.upgrade().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_wrap_a_reader() {
|
||||
let bytes = &b"I am an implementation of io::Read"[..];
|
||||
let pb = ProgressBar::new(bytes.len() as u64);
|
||||
let mut reader = pb.wrap_read(bytes);
|
||||
let mut writer = Vec::new();
|
||||
io::copy(&mut reader, &mut writer).unwrap();
|
||||
assert_eq!(writer, bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_wrap_a_writer() {
|
||||
let bytes = b"implementation of io::Read";
|
||||
let mut reader = &bytes[..];
|
||||
let pb = ProgressBar::new(bytes.len() as u64);
|
||||
let writer = Vec::new();
|
||||
let mut writer = pb.wrap_write(writer);
|
||||
io::copy(&mut reader, &mut writer).unwrap();
|
||||
assert_eq!(writer.it, bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticker_thread_terminates_on_drop() {
|
||||
let _guard = TICKER_TEST.lock().unwrap();
|
||||
assert!(!TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.enable_steady_tick(Duration::from_millis(50));
|
||||
|
||||
// Give the thread time to start up
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
|
||||
assert!(TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
|
||||
drop(pb);
|
||||
assert!(!TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticker_thread_terminates_on_drop_2() {
|
||||
let _guard = TICKER_TEST.lock().unwrap();
|
||||
assert!(!TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.enable_steady_tick(Duration::from_millis(50));
|
||||
let pb2 = pb.clone();
|
||||
|
||||
// Give the thread time to start up
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
|
||||
assert!(TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
|
||||
drop(pb);
|
||||
assert!(TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
|
||||
drop(pb2);
|
||||
assert!(!TICKER_RUNNING.load(Ordering::SeqCst));
|
||||
}
|
||||
}
|
235
vendor/indicatif/src/rayon.rs
vendored
Normal file
235
vendor/indicatif/src/rayon.rs
vendored
Normal file
@ -0,0 +1,235 @@
|
||||
use rayon::iter::plumbing::{Consumer, Folder, Producer, ProducerCallback, UnindexedConsumer};
|
||||
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
|
||||
|
||||
use crate::{ProgressBar, ProgressBarIter};
|
||||
|
||||
/// Wraps a Rayon parallel iterator.
|
||||
///
|
||||
/// See [`ProgressIterator`](trait.ProgressIterator.html) for method
|
||||
/// documentation.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rayon")))]
|
||||
pub trait ParallelProgressIterator
|
||||
where
|
||||
Self: Sized + ParallelIterator,
|
||||
{
|
||||
/// Wrap an iterator with a custom progress bar.
|
||||
fn progress_with(self, progress: ProgressBar) -> ProgressBarIter<Self>;
|
||||
|
||||
/// Wrap an iterator with an explicit element count.
|
||||
fn progress_count(self, len: u64) -> ProgressBarIter<Self> {
|
||||
self.progress_with(ProgressBar::new(len))
|
||||
}
|
||||
|
||||
fn progress(self) -> ProgressBarIter<Self>
|
||||
where
|
||||
Self: IndexedParallelIterator,
|
||||
{
|
||||
let len = u64::try_from(self.len()).unwrap();
|
||||
self.progress_count(len)
|
||||
}
|
||||
|
||||
/// Wrap an iterator with a progress bar and style it.
|
||||
fn progress_with_style(self, style: crate::ProgressStyle) -> ProgressBarIter<Self>
|
||||
where
|
||||
Self: IndexedParallelIterator,
|
||||
{
|
||||
let len = u64::try_from(self.len()).unwrap();
|
||||
let bar = ProgressBar::new(len).with_style(style);
|
||||
self.progress_with(bar)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send, T: ParallelIterator<Item = S>> ParallelProgressIterator for T {
|
||||
fn progress_with(self, progress: ProgressBar) -> ProgressBarIter<Self> {
|
||||
ProgressBarIter { it: self, progress }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send, T: IndexedParallelIterator<Item = S>> IndexedParallelIterator for ProgressBarIter<T> {
|
||||
fn len(&self) -> usize {
|
||||
self.it.len()
|
||||
}
|
||||
|
||||
fn drive<C: Consumer<Self::Item>>(self, consumer: C) -> <C as Consumer<Self::Item>>::Result {
|
||||
let consumer = ProgressConsumer::new(consumer, self.progress);
|
||||
self.it.drive(consumer)
|
||||
}
|
||||
|
||||
fn with_producer<CB: ProducerCallback<Self::Item>>(
|
||||
self,
|
||||
callback: CB,
|
||||
) -> <CB as ProducerCallback<Self::Item>>::Output {
|
||||
return self.it.with_producer(Callback {
|
||||
callback,
|
||||
progress: self.progress,
|
||||
});
|
||||
|
||||
struct Callback<CB> {
|
||||
callback: CB,
|
||||
progress: ProgressBar,
|
||||
}
|
||||
|
||||
impl<T, CB: ProducerCallback<T>> ProducerCallback<T> for Callback<CB> {
|
||||
type Output = CB::Output;
|
||||
|
||||
fn callback<P>(self, base: P) -> CB::Output
|
||||
where
|
||||
P: Producer<Item = T>,
|
||||
{
|
||||
let producer = ProgressProducer {
|
||||
base,
|
||||
progress: self.progress,
|
||||
};
|
||||
self.callback.callback(producer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressProducer<T> {
|
||||
base: T,
|
||||
progress: ProgressBar,
|
||||
}
|
||||
|
||||
impl<T, P: Producer<Item = T>> Producer for ProgressProducer<P> {
|
||||
type Item = T;
|
||||
type IntoIter = ProgressBarIter<P::IntoIter>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
ProgressBarIter {
|
||||
it: self.base.into_iter(),
|
||||
progress: self.progress,
|
||||
}
|
||||
}
|
||||
|
||||
fn min_len(&self) -> usize {
|
||||
self.base.min_len()
|
||||
}
|
||||
|
||||
fn max_len(&self) -> usize {
|
||||
self.base.max_len()
|
||||
}
|
||||
|
||||
fn split_at(self, index: usize) -> (Self, Self) {
|
||||
let (left, right) = self.base.split_at(index);
|
||||
(
|
||||
ProgressProducer {
|
||||
base: left,
|
||||
progress: self.progress.clone(),
|
||||
},
|
||||
ProgressProducer {
|
||||
base: right,
|
||||
progress: self.progress,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressConsumer<C> {
|
||||
base: C,
|
||||
progress: ProgressBar,
|
||||
}
|
||||
|
||||
impl<C> ProgressConsumer<C> {
|
||||
fn new(base: C, progress: ProgressBar) -> Self {
|
||||
ProgressConsumer { base, progress }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C: Consumer<T>> Consumer<T> for ProgressConsumer<C> {
|
||||
type Folder = ProgressFolder<C::Folder>;
|
||||
type Reducer = C::Reducer;
|
||||
type Result = C::Result;
|
||||
|
||||
fn split_at(self, index: usize) -> (Self, Self, Self::Reducer) {
|
||||
let (left, right, reducer) = self.base.split_at(index);
|
||||
(
|
||||
ProgressConsumer::new(left, self.progress.clone()),
|
||||
ProgressConsumer::new(right, self.progress),
|
||||
reducer,
|
||||
)
|
||||
}
|
||||
|
||||
fn into_folder(self) -> Self::Folder {
|
||||
ProgressFolder {
|
||||
base: self.base.into_folder(),
|
||||
progress: self.progress,
|
||||
}
|
||||
}
|
||||
|
||||
fn full(&self) -> bool {
|
||||
self.base.full()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C: UnindexedConsumer<T>> UnindexedConsumer<T> for ProgressConsumer<C> {
|
||||
fn split_off_left(&self) -> Self {
|
||||
ProgressConsumer::new(self.base.split_off_left(), self.progress.clone())
|
||||
}
|
||||
|
||||
fn to_reducer(&self) -> Self::Reducer {
|
||||
self.base.to_reducer()
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressFolder<C> {
|
||||
base: C,
|
||||
progress: ProgressBar,
|
||||
}
|
||||
|
||||
impl<T, C: Folder<T>> Folder<T> for ProgressFolder<C> {
|
||||
type Result = C::Result;
|
||||
|
||||
fn consume(self, item: T) -> Self {
|
||||
self.progress.inc(1);
|
||||
ProgressFolder {
|
||||
base: self.base.consume(item),
|
||||
progress: self.progress,
|
||||
}
|
||||
}
|
||||
|
||||
fn complete(self) -> C::Result {
|
||||
self.base.complete()
|
||||
}
|
||||
|
||||
fn full(&self) -> bool {
|
||||
self.base.full()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send, T: ParallelIterator<Item = S>> ParallelIterator for ProgressBarIter<T> {
|
||||
type Item = S;
|
||||
|
||||
fn drive_unindexed<C: UnindexedConsumer<Self::Item>>(self, consumer: C) -> C::Result {
|
||||
let consumer1 = ProgressConsumer::new(consumer, self.progress.clone());
|
||||
self.it.drive_unindexed(consumer1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
use crate::{ParallelProgressIterator, ProgressBar, ProgressBarIter, ProgressStyle};
|
||||
|
||||
#[test]
|
||||
fn it_can_wrap_a_parallel_iterator() {
|
||||
let v = vec![1, 2, 3];
|
||||
fn wrap<'a, T: ParallelIterator<Item = &'a i32>>(it: ProgressBarIter<T>) {
|
||||
assert_eq!(it.map(|x| x * 2).collect::<Vec<_>>(), vec![2, 4, 6]);
|
||||
}
|
||||
|
||||
wrap(v.par_iter().progress_count(3));
|
||||
wrap({
|
||||
let pb = ProgressBar::new(v.len() as u64);
|
||||
v.par_iter().progress_with(pb)
|
||||
});
|
||||
|
||||
wrap({
|
||||
let style = ProgressStyle::default_bar()
|
||||
.template("{wide_bar:.red} {percent}/100%")
|
||||
.unwrap();
|
||||
v.par_iter().progress_with_style(style)
|
||||
});
|
||||
}
|
||||
}
|
798
vendor/indicatif/src/state.rs
vendored
Normal file
798
vendor/indicatif/src/state.rs
vendored
Normal file
@ -0,0 +1,798 @@
|
||||
use std::borrow::Cow;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use instant::Instant;
|
||||
use portable_atomic::{AtomicU64, AtomicU8, Ordering};
|
||||
|
||||
use crate::draw_target::ProgressDrawTarget;
|
||||
use crate::style::ProgressStyle;
|
||||
|
||||
pub(crate) struct BarState {
|
||||
pub(crate) draw_target: ProgressDrawTarget,
|
||||
pub(crate) on_finish: ProgressFinish,
|
||||
pub(crate) style: ProgressStyle,
|
||||
pub(crate) state: ProgressState,
|
||||
pub(crate) tab_width: usize,
|
||||
}
|
||||
|
||||
impl BarState {
|
||||
pub(crate) fn new(
|
||||
len: Option<u64>,
|
||||
draw_target: ProgressDrawTarget,
|
||||
pos: Arc<AtomicPosition>,
|
||||
) -> Self {
|
||||
Self {
|
||||
draw_target,
|
||||
on_finish: ProgressFinish::default(),
|
||||
style: ProgressStyle::default_bar(),
|
||||
state: ProgressState::new(len, pos),
|
||||
tab_width: DEFAULT_TAB_WIDTH,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finishes the progress bar using the [`ProgressFinish`] behavior stored
|
||||
/// in the [`ProgressStyle`].
|
||||
pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) {
|
||||
self.state.status = Status::DoneVisible;
|
||||
match finish {
|
||||
ProgressFinish::AndLeave => {
|
||||
if let Some(len) = self.state.len {
|
||||
self.state.pos.set(len);
|
||||
}
|
||||
}
|
||||
ProgressFinish::WithMessage(msg) => {
|
||||
if let Some(len) = self.state.len {
|
||||
self.state.pos.set(len);
|
||||
}
|
||||
self.state.message = TabExpandedString::new(msg, self.tab_width);
|
||||
}
|
||||
ProgressFinish::AndClear => {
|
||||
if let Some(len) = self.state.len {
|
||||
self.state.pos.set(len);
|
||||
}
|
||||
self.state.status = Status::DoneHidden;
|
||||
}
|
||||
ProgressFinish::Abandon => {}
|
||||
ProgressFinish::AbandonWithMessage(msg) => {
|
||||
self.state.message = TabExpandedString::new(msg, self.tab_width);
|
||||
}
|
||||
}
|
||||
|
||||
// There's no need to update the estimate here; once the `status` is no longer
|
||||
// `InProgress`, we will use the length and elapsed time to estimate.
|
||||
let _ = self.draw(true, now);
|
||||
}
|
||||
|
||||
pub(crate) fn reset(&mut self, now: Instant, mode: Reset) {
|
||||
// Always reset the estimator; this is the only reset that will occur if mode is
|
||||
// `Reset::Eta`.
|
||||
self.state.est.reset(now);
|
||||
|
||||
if let Reset::Elapsed | Reset::All = mode {
|
||||
self.state.started = now;
|
||||
}
|
||||
|
||||
if let Reset::All = mode {
|
||||
self.state.pos.reset(now);
|
||||
self.state.status = Status::InProgress;
|
||||
|
||||
for tracker in self.style.format_map.values_mut() {
|
||||
tracker.reset(&self.state, now);
|
||||
}
|
||||
|
||||
let _ = self.draw(false, now);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState), tick: bool) {
|
||||
f(&mut self.state);
|
||||
if tick {
|
||||
self.tick(now);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_length(&mut self, now: Instant, len: u64) {
|
||||
self.state.len = Some(len);
|
||||
self.update_estimate_and_draw(now);
|
||||
}
|
||||
|
||||
pub(crate) fn inc_length(&mut self, now: Instant, delta: u64) {
|
||||
if let Some(len) = self.state.len {
|
||||
self.state.len = Some(len.saturating_add(delta));
|
||||
}
|
||||
self.update_estimate_and_draw(now);
|
||||
}
|
||||
|
||||
pub(crate) fn set_tab_width(&mut self, tab_width: usize) {
|
||||
self.tab_width = tab_width;
|
||||
self.state.message.set_tab_width(tab_width);
|
||||
self.state.prefix.set_tab_width(tab_width);
|
||||
self.style.set_tab_width(tab_width);
|
||||
}
|
||||
|
||||
pub(crate) fn set_style(&mut self, style: ProgressStyle) {
|
||||
self.style = style;
|
||||
self.style.set_tab_width(self.tab_width);
|
||||
}
|
||||
|
||||
pub(crate) fn tick(&mut self, now: Instant) {
|
||||
self.state.tick = self.state.tick.saturating_add(1);
|
||||
self.update_estimate_and_draw(now);
|
||||
}
|
||||
|
||||
pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) {
|
||||
let pos = self.state.pos.pos.load(Ordering::Relaxed);
|
||||
self.state.est.record(pos, now);
|
||||
|
||||
for tracker in self.style.format_map.values_mut() {
|
||||
tracker.tick(&self.state, now);
|
||||
}
|
||||
|
||||
let _ = self.draw(false, now);
|
||||
}
|
||||
|
||||
pub(crate) fn println(&mut self, now: Instant, msg: &str) {
|
||||
let width = self.draw_target.width();
|
||||
let mut drawable = match self.draw_target.drawable(true, now) {
|
||||
Some(drawable) => drawable,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut draw_state = drawable.state();
|
||||
let lines: Vec<String> = msg.lines().map(Into::into).collect();
|
||||
// Empty msg should trigger newline as we are in println
|
||||
if lines.is_empty() {
|
||||
draw_state.lines.push(String::new());
|
||||
} else {
|
||||
draw_state.lines.extend(lines);
|
||||
}
|
||||
draw_state.orphan_lines_count = draw_state.lines.len();
|
||||
if !matches!(self.state.status, Status::DoneHidden) {
|
||||
self.style
|
||||
.format_state(&self.state, &mut draw_state.lines, width);
|
||||
}
|
||||
|
||||
drop(draw_state);
|
||||
let _ = drawable.draw();
|
||||
}
|
||||
|
||||
pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, now: Instant, f: F) -> R {
|
||||
if let Some((state, _)) = self.draw_target.remote() {
|
||||
return state.write().unwrap().suspend(f, now);
|
||||
}
|
||||
|
||||
if let Some(drawable) = self.draw_target.drawable(true, now) {
|
||||
let _ = drawable.clear();
|
||||
}
|
||||
|
||||
let ret = f();
|
||||
let _ = self.draw(true, Instant::now());
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&mut self, mut force_draw: bool, now: Instant) -> io::Result<()> {
|
||||
let width = self.draw_target.width();
|
||||
|
||||
// `|= self.is_finished()` should not be needed here, but we used to always draw for
|
||||
// finished progress bars, so it's kept as to not cause compatibility issues in weird cases.
|
||||
force_draw |= self.state.is_finished();
|
||||
let mut drawable = match self.draw_target.drawable(force_draw, now) {
|
||||
Some(drawable) => drawable,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut draw_state = drawable.state();
|
||||
|
||||
if !matches!(self.state.status, Status::DoneHidden) {
|
||||
self.style
|
||||
.format_state(&self.state, &mut draw_state.lines, width);
|
||||
}
|
||||
|
||||
drop(draw_state);
|
||||
drawable.draw()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BarState {
|
||||
fn drop(&mut self) {
|
||||
// Progress bar is already finished. Do not need to do anything other than notify
|
||||
// the `MultiProgress` that we're now a zombie.
|
||||
if self.state.is_finished() {
|
||||
self.draw_target.mark_zombie();
|
||||
return;
|
||||
}
|
||||
|
||||
self.finish_using_style(Instant::now(), self.on_finish.clone());
|
||||
|
||||
// Notify the `MultiProgress` that we're now a zombie.
|
||||
self.draw_target.mark_zombie();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Reset {
|
||||
Eta,
|
||||
Elapsed,
|
||||
All,
|
||||
}
|
||||
|
||||
/// The state of a progress bar at a moment in time.
|
||||
#[non_exhaustive]
|
||||
pub struct ProgressState {
|
||||
pos: Arc<AtomicPosition>,
|
||||
len: Option<u64>,
|
||||
pub(crate) tick: u64,
|
||||
pub(crate) started: Instant,
|
||||
status: Status,
|
||||
est: Estimator,
|
||||
pub(crate) message: TabExpandedString,
|
||||
pub(crate) prefix: TabExpandedString,
|
||||
}
|
||||
|
||||
impl ProgressState {
|
||||
pub(crate) fn new(len: Option<u64>, pos: Arc<AtomicPosition>) -> Self {
|
||||
let now = Instant::now();
|
||||
Self {
|
||||
pos,
|
||||
len,
|
||||
tick: 0,
|
||||
status: Status::InProgress,
|
||||
started: now,
|
||||
est: Estimator::new(now),
|
||||
message: TabExpandedString::NoTabs("".into()),
|
||||
prefix: TabExpandedString::NoTabs("".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates that the progress bar finished.
|
||||
pub fn is_finished(&self) -> bool {
|
||||
match self.status {
|
||||
Status::InProgress => false,
|
||||
Status::DoneVisible => true,
|
||||
Status::DoneHidden => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the completion as a floating-point number between 0 and 1
|
||||
pub fn fraction(&self) -> f32 {
|
||||
let pos = self.pos.pos.load(Ordering::Relaxed);
|
||||
let pct = match (pos, self.len) {
|
||||
(_, None) => 0.0,
|
||||
(_, Some(0)) => 1.0,
|
||||
(0, _) => 0.0,
|
||||
(pos, Some(len)) => pos as f32 / len as f32,
|
||||
};
|
||||
pct.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// The expected ETA
|
||||
pub fn eta(&self) -> Duration {
|
||||
if self.is_finished() {
|
||||
return Duration::new(0, 0);
|
||||
}
|
||||
|
||||
let len = match self.len {
|
||||
Some(len) => len,
|
||||
None => return Duration::new(0, 0),
|
||||
};
|
||||
|
||||
let pos = self.pos.pos.load(Ordering::Relaxed);
|
||||
|
||||
let sps = self.est.steps_per_second(Instant::now());
|
||||
|
||||
// Infinite duration should only ever happen at the beginning, so in this case it's okay to
|
||||
// just show an ETA of 0 until progress starts to occur.
|
||||
if sps == 0.0 {
|
||||
return Duration::new(0, 0);
|
||||
}
|
||||
|
||||
secs_to_duration(len.saturating_sub(pos) as f64 / sps)
|
||||
}
|
||||
|
||||
/// The expected total duration (that is, elapsed time + expected ETA)
|
||||
pub fn duration(&self) -> Duration {
|
||||
if self.len.is_none() || self.is_finished() {
|
||||
return Duration::new(0, 0);
|
||||
}
|
||||
self.started.elapsed().saturating_add(self.eta())
|
||||
}
|
||||
|
||||
/// The number of steps per second
|
||||
pub fn per_sec(&self) -> f64 {
|
||||
if let Status::InProgress = self.status {
|
||||
self.est.steps_per_second(Instant::now())
|
||||
} else {
|
||||
let len = self.len.unwrap_or_else(|| self.pos());
|
||||
len as f64 / self.started.elapsed().as_secs_f64()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elapsed(&self) -> Duration {
|
||||
self.started.elapsed()
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> u64 {
|
||||
self.pos.pos.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_pos(&mut self, pos: u64) {
|
||||
self.pos.set(pos);
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> Option<u64> {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn set_len(&mut self, len: u64) {
|
||||
self.len = Some(len);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub(crate) enum TabExpandedString {
|
||||
NoTabs(Cow<'static, str>),
|
||||
WithTabs {
|
||||
original: Cow<'static, str>,
|
||||
expanded: String,
|
||||
tab_width: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl TabExpandedString {
|
||||
pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self {
|
||||
let expanded = s.replace('\t', &" ".repeat(tab_width));
|
||||
if s == expanded {
|
||||
Self::NoTabs(s)
|
||||
} else {
|
||||
Self::WithTabs {
|
||||
original: s,
|
||||
expanded,
|
||||
tab_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expanded(&self) -> &str {
|
||||
match &self {
|
||||
Self::NoTabs(s) => {
|
||||
debug_assert!(!s.contains('\t'));
|
||||
s
|
||||
}
|
||||
Self::WithTabs { expanded, .. } => expanded,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
|
||||
if let Self::WithTabs {
|
||||
original,
|
||||
expanded,
|
||||
tab_width,
|
||||
} = self
|
||||
{
|
||||
if *tab_width != new_tab_width {
|
||||
*tab_width = new_tab_width;
|
||||
*expanded = original.replace('\t', &" ".repeat(new_tab_width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-smoothed exponentially weighted estimator
|
||||
///
|
||||
/// This uses an exponentially weighted *time-based* estimator, meaning that it exponentially
|
||||
/// downweights old data based on its age. The rate at which this occurs is currently a constant
|
||||
/// value of 15 seconds for 90% weighting. This means that all data older than 15 seconds has a
|
||||
/// collective weight of 0.1 in the estimate, and all data older than 30 seconds has a collective
|
||||
/// weight of 0.01, and so on.
|
||||
///
|
||||
/// The primary value exposed by `Estimator` is `steps_per_second`. This value is doubly-smoothed,
|
||||
/// meaning that is the result of using an exponentially weighted estimator (as described above) to
|
||||
/// estimate the value of another exponentially weighted estimator, which estimates the value of
|
||||
/// the raw data.
|
||||
///
|
||||
/// The purpose of this extra smoothing step is to reduce instantaneous fluctations in the estimate
|
||||
/// when large updates are received. Without this, estimates might have a large spike followed by a
|
||||
/// slow asymptotic approach to zero (until the next spike).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Estimator {
|
||||
smoothed_steps_per_sec: f64,
|
||||
double_smoothed_steps_per_sec: f64,
|
||||
prev_steps: u64,
|
||||
prev_time: Instant,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Estimator {
|
||||
fn new(now: Instant) -> Self {
|
||||
Self {
|
||||
smoothed_steps_per_sec: 0.0,
|
||||
double_smoothed_steps_per_sec: 0.0,
|
||||
prev_steps: 0,
|
||||
prev_time: now,
|
||||
start_time: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, new_steps: u64, now: Instant) {
|
||||
// sanity check: don't record data if time or steps have not advanced
|
||||
if new_steps <= self.prev_steps || now <= self.prev_time {
|
||||
// Reset on backwards seek to prevent breakage from seeking to the end for length determination
|
||||
// See https://github.com/console-rs/indicatif/issues/480
|
||||
if new_steps < self.prev_steps {
|
||||
self.prev_steps = new_steps;
|
||||
self.reset(now);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let delta_steps = new_steps - self.prev_steps;
|
||||
let delta_t = duration_to_secs(now - self.prev_time);
|
||||
|
||||
// the rate of steps we saw in this update
|
||||
let new_steps_per_second = delta_steps as f64 / delta_t;
|
||||
|
||||
// update the estimate: a weighted average of the old estimate and new data
|
||||
let weight = estimator_weight(delta_t);
|
||||
self.smoothed_steps_per_sec =
|
||||
self.smoothed_steps_per_sec * weight + new_steps_per_second * (1.0 - weight);
|
||||
|
||||
// An iterative estimate like `smoothed_steps_per_sec` is supposed to be an exponentially
|
||||
// weighted average from t=0 back to t=-inf; Since we initialize it to 0, we neglect the
|
||||
// (non-existent) samples in the weighted average prior to the first one, so the resulting
|
||||
// average must be normalized. We normalize the single estimate here in order to use it as
|
||||
// a source for the double smoothed estimate. See comment on normalization in
|
||||
// `steps_per_second` for details.
|
||||
let delta_t_start = duration_to_secs(now - self.start_time);
|
||||
let total_weight = 1.0 - estimator_weight(delta_t_start);
|
||||
let normalized_smoothed_steps_per_sec = self.smoothed_steps_per_sec / total_weight;
|
||||
|
||||
// determine the double smoothed value (EWA smoothing of the single EWA)
|
||||
self.double_smoothed_steps_per_sec = self.double_smoothed_steps_per_sec * weight
|
||||
+ normalized_smoothed_steps_per_sec * (1.0 - weight);
|
||||
|
||||
self.prev_steps = new_steps;
|
||||
self.prev_time = now;
|
||||
}
|
||||
|
||||
/// Reset the state of the estimator. Once reset, estimates will not depend on any data prior
|
||||
/// to `now`. This does not reset the stored position of the progress bar.
|
||||
pub(crate) fn reset(&mut self, now: Instant) {
|
||||
self.smoothed_steps_per_sec = 0.0;
|
||||
self.double_smoothed_steps_per_sec = 0.0;
|
||||
|
||||
// only reset prev_time, not prev_steps
|
||||
self.prev_time = now;
|
||||
self.start_time = now;
|
||||
}
|
||||
|
||||
/// Average time per step in seconds, using double exponential smoothing
|
||||
fn steps_per_second(&self, now: Instant) -> f64 {
|
||||
// Because the value stored in the Estimator is only updated when the Estimator receives an
|
||||
// update, this value will become stuck if progress stalls. To return an accurate estimate,
|
||||
// we determine how much time has passed since the last update, and treat this as a
|
||||
// pseudo-update with 0 steps.
|
||||
let delta_t = duration_to_secs(now - self.prev_time);
|
||||
let reweight = estimator_weight(delta_t);
|
||||
|
||||
// Normalization of estimates:
|
||||
//
|
||||
// The raw estimate is a single value (smoothed_steps_per_second) that is iteratively
|
||||
// updated. At each update, the previous value of the estimate is downweighted according to
|
||||
// its age, receiving the iterative weight W(t) = 0.1 ^ (t/15).
|
||||
//
|
||||
// Since W(Sum(t_n)) = Prod(W(t_n)), the total weight of a sample after a series of
|
||||
// iterative steps is simply W(t_e) - W(t_b), where t_e is the time since the end of the
|
||||
// sample, and t_b is the time since the beginning. The resulting estimate is therefore a
|
||||
// weighted average with sample weights W(t_e) - W(t_b).
|
||||
//
|
||||
// Notice that the weighting function generates sample weights that sum to 1 only when the
|
||||
// sample times span from t=0 to t=inf; but this is not the case. We have a first sample
|
||||
// with finite, positive t_b = t_f. In the raw estimate, we handle times prior to t_f by
|
||||
// setting an initial value of 0, meaning that these (non-existent) samples have no weight.
|
||||
//
|
||||
// Therefore, the raw estimate must be normalized by dividing it by the sum of the weights
|
||||
// in the weighted average. This sum is just W(0) - W(t_f), where t_f is the time since the
|
||||
// first sample, and W(0) = 1.
|
||||
let delta_t_start = duration_to_secs(now - self.start_time);
|
||||
let total_weight = 1.0 - estimator_weight(delta_t_start);
|
||||
|
||||
// Generate updated values for `smoothed_steps_per_sec` and `double_smoothed_steps_per_sec`
|
||||
// (sps and dsps) without storing them. Note that we normalize sps when using it as a
|
||||
// source to update dsps, and then normalize dsps itself before returning it.
|
||||
let sps = self.smoothed_steps_per_sec * reweight / total_weight;
|
||||
let dsps = self.double_smoothed_steps_per_sec * reweight + sps * (1.0 - reweight);
|
||||
dsps / total_weight
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AtomicPosition {
|
||||
pub(crate) pos: AtomicU64,
|
||||
capacity: AtomicU8,
|
||||
prev: AtomicU64,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl AtomicPosition {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
pos: AtomicU64::new(0),
|
||||
capacity: AtomicU8::new(MAX_BURST),
|
||||
prev: AtomicU64::new(0),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn allow(&self, now: Instant) -> bool {
|
||||
if now < self.start {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut capacity = self.capacity.load(Ordering::Acquire);
|
||||
// `prev` is the number of ms after `self.started` we last returned `true`, in ns
|
||||
let prev = self.prev.load(Ordering::Acquire);
|
||||
// `elapsed` is the number of ns since `self.started`
|
||||
let elapsed = (now - self.start).as_nanos() as u64;
|
||||
// `diff` is the number of ns since we last returned `true`
|
||||
let diff = elapsed.saturating_sub(prev);
|
||||
|
||||
// If `capacity` is 0 and not enough time (1ms) has passed since `prev`
|
||||
// to add new capacity, return `false`. The goal of this method is to
|
||||
// make this decision as efficient as possible.
|
||||
if capacity == 0 && diff < INTERVAL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We now calculate `new`, the number of ms, in ns, since we last returned `true`,
|
||||
// and `remainder`, which represents a number of ns less than 1ms which we cannot
|
||||
// convert into capacity now, so we're saving it for later. We do this by
|
||||
// substracting this from `elapsed` before storing it into `self.prev`.
|
||||
let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL));
|
||||
// We add `new` to `capacity`, subtract one for returning `true` from here,
|
||||
// then make sure it does not exceed a maximum of `MAX_BURST`.
|
||||
capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8;
|
||||
|
||||
// Then, we just store `capacity` and `prev` atomically for the next iteration
|
||||
self.capacity.store(capacity, Ordering::Release);
|
||||
self.prev.store(elapsed - remainder, Ordering::Release);
|
||||
true
|
||||
}
|
||||
|
||||
fn reset(&self, now: Instant) {
|
||||
self.set(0);
|
||||
let elapsed = (now.saturating_duration_since(self.start)).as_millis() as u64;
|
||||
self.prev.store(elapsed, Ordering::Release);
|
||||
}
|
||||
|
||||
pub(crate) fn inc(&self, delta: u64) {
|
||||
self.pos.fetch_add(delta, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn set(&self, pos: u64) {
|
||||
self.pos.store(pos, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
const INTERVAL: u64 = 1_000_000;
|
||||
const MAX_BURST: u8 = 10;
|
||||
|
||||
/// Behavior of a progress bar when it is finished
|
||||
///
|
||||
/// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and
|
||||
/// [`ProgressBar::is_finished`] is false.
|
||||
///
|
||||
/// [`ProgressBar`]: crate::ProgressBar
|
||||
/// [`ProgressBarIter`]: crate::ProgressBarIter
|
||||
/// [`ProgressBar::is_finished`]: crate::ProgressBar::is_finished
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ProgressFinish {
|
||||
/// Finishes the progress bar and leaves the current message
|
||||
///
|
||||
/// Same behavior as calling [`ProgressBar::finish()`](crate::ProgressBar::finish).
|
||||
AndLeave,
|
||||
/// Finishes the progress bar and sets a message
|
||||
///
|
||||
/// Same behavior as calling [`ProgressBar::finish_with_message()`](crate::ProgressBar::finish_with_message).
|
||||
WithMessage(Cow<'static, str>),
|
||||
/// Finishes the progress bar and completely clears it (this is the default)
|
||||
///
|
||||
/// Same behavior as calling [`ProgressBar::finish_and_clear()`](crate::ProgressBar::finish_and_clear).
|
||||
AndClear,
|
||||
/// Finishes the progress bar and leaves the current message and progress
|
||||
///
|
||||
/// Same behavior as calling [`ProgressBar::abandon()`](crate::ProgressBar::abandon).
|
||||
Abandon,
|
||||
/// Finishes the progress bar and sets a message, and leaves the current progress
|
||||
///
|
||||
/// Same behavior as calling [`ProgressBar::abandon_with_message()`](crate::ProgressBar::abandon_with_message).
|
||||
AbandonWithMessage(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl Default for ProgressFinish {
|
||||
fn default() -> Self {
|
||||
Self::AndClear
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the appropriate dilution weight for Estimator data given the data's age (in seconds)
|
||||
///
|
||||
/// Whenever an update occurs, we will create a new estimate using a weight `w_i` like so:
|
||||
///
|
||||
/// ```math
|
||||
/// <new estimate> = <previous estimate> * w_i + <new data> * (1 - w_i)
|
||||
/// ```
|
||||
///
|
||||
/// In other words, the new estimate is a weighted average of the previous estimate and the new
|
||||
/// data. We want to choose weights such that for any set of samples where `t_0, t_1, ...` are
|
||||
/// the durations of the samples:
|
||||
///
|
||||
/// ```math
|
||||
/// Sum(t_i) = ews ==> Prod(w_i) = 0.1
|
||||
/// ```
|
||||
///
|
||||
/// With this constraint it is easy to show that
|
||||
///
|
||||
/// ```math
|
||||
/// w_i = 0.1 ^ (t_i / ews)
|
||||
/// ```
|
||||
///
|
||||
/// Notice that the constraint implies that estimates are independent of the durations of the
|
||||
/// samples, a very useful feature.
|
||||
fn estimator_weight(age: f64) -> f64 {
|
||||
const EXPONENTIAL_WEIGHTING_SECONDS: f64 = 15.0;
|
||||
0.1_f64.powf(age / EXPONENTIAL_WEIGHTING_SECONDS)
|
||||
}
|
||||
|
||||
fn duration_to_secs(d: Duration) -> f64 {
|
||||
d.as_secs() as f64 + f64::from(d.subsec_nanos()) / 1_000_000_000f64
|
||||
}
|
||||
|
||||
fn secs_to_duration(s: f64) -> Duration {
|
||||
let secs = s.trunc() as u64;
|
||||
let nanos = (s.fract() * 1_000_000_000f64) as u32;
|
||||
Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Status {
|
||||
InProgress,
|
||||
DoneVisible,
|
||||
DoneHidden,
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ProgressBar;
|
||||
|
||||
// https://github.com/rust-lang/rust-clippy/issues/10281
|
||||
#[allow(clippy::uninlined_format_args)]
|
||||
#[test]
|
||||
fn test_steps_per_second() {
|
||||
let test_rate = |items_per_second| {
|
||||
let mut now = Instant::now();
|
||||
let mut est = Estimator::new(now);
|
||||
let mut pos = 0;
|
||||
|
||||
for _ in 0..20 {
|
||||
pos += items_per_second;
|
||||
now += Duration::from_secs(1);
|
||||
est.record(pos, now);
|
||||
}
|
||||
let avg_steps_per_second = est.steps_per_second(now);
|
||||
|
||||
assert!(avg_steps_per_second > 0.0);
|
||||
assert!(avg_steps_per_second.is_finite());
|
||||
|
||||
let absolute_error = (avg_steps_per_second - items_per_second as f64).abs();
|
||||
let relative_error = absolute_error / items_per_second as f64;
|
||||
assert!(
|
||||
relative_error < 1.0 / 1e9,
|
||||
"Expected rate: {}, actual: {}, relative error: {}",
|
||||
items_per_second,
|
||||
avg_steps_per_second,
|
||||
relative_error
|
||||
);
|
||||
};
|
||||
|
||||
test_rate(1);
|
||||
test_rate(1_000);
|
||||
test_rate(1_000_000);
|
||||
test_rate(1_000_000_000);
|
||||
test_rate(1_000_000_001);
|
||||
test_rate(100_000_000_000);
|
||||
test_rate(1_000_000_000_000);
|
||||
test_rate(100_000_000_000_000);
|
||||
test_rate(1_000_000_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_exponential_ave() {
|
||||
let mut now = Instant::now();
|
||||
let mut est = Estimator::new(now);
|
||||
let mut pos = 0;
|
||||
|
||||
// note: this is the default weight set in the Estimator
|
||||
let weight = 15;
|
||||
|
||||
for _ in 0..weight {
|
||||
pos += 1;
|
||||
now += Duration::from_secs(1);
|
||||
est.record(pos, now);
|
||||
}
|
||||
now += Duration::from_secs(weight);
|
||||
|
||||
// The first level EWA:
|
||||
// -> 90% weight @ 0 eps, 9% weight @ 1 eps, 1% weight @ 0 eps
|
||||
// -> then normalized by deweighting the 1% weight (before -30 seconds)
|
||||
let single_target = 0.09 / 0.99;
|
||||
|
||||
// The second level EWA:
|
||||
// -> same logic as above, but using the first level EWA as the source
|
||||
let double_target = (0.9 * single_target + 0.09) / 0.99;
|
||||
assert_eq!(est.steps_per_second(now), double_target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimator_rewind_position() {
|
||||
let mut now = Instant::now();
|
||||
let mut est = Estimator::new(now);
|
||||
|
||||
now += Duration::from_secs(1);
|
||||
est.record(1, now);
|
||||
|
||||
// should not panic
|
||||
now += Duration::from_secs(1);
|
||||
est.record(0, now);
|
||||
|
||||
// check that reset occurred (estimator at 1 event per sec)
|
||||
now += Duration::from_secs(1);
|
||||
est.record(1, now);
|
||||
assert_eq!(est.steps_per_second(now), 1.0);
|
||||
|
||||
// check that progress bar handles manual seeking
|
||||
let pb = ProgressBar::hidden();
|
||||
pb.set_length(10);
|
||||
pb.set_position(1);
|
||||
pb.tick();
|
||||
// Should not panic.
|
||||
pb.set_position(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_eta() {
|
||||
let mut now = Instant::now();
|
||||
let mut est = Estimator::new(now);
|
||||
|
||||
// two per second, then reset
|
||||
now += Duration::from_secs(1);
|
||||
est.record(2, now);
|
||||
est.reset(now);
|
||||
|
||||
// now one per second, and verify
|
||||
now += Duration::from_secs(1);
|
||||
est.record(3, now);
|
||||
assert_eq!(est.steps_per_second(now), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_stuff() {
|
||||
let duration = Duration::new(42, 100_000_000);
|
||||
let secs = duration_to_secs(duration);
|
||||
assert_eq!(secs_to_duration(secs), duration);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atomic_position_large_time_difference() {
|
||||
let atomic_position = AtomicPosition::new();
|
||||
let later = atomic_position.start + Duration::from_nanos(INTERVAL * u64::from(u8::MAX));
|
||||
// Should not panic.
|
||||
atomic_position.allow(later);
|
||||
}
|
||||
}
|
987
vendor/indicatif/src/style.rs
vendored
Normal file
987
vendor/indicatif/src/style.rs
vendored
Normal file
@ -0,0 +1,987 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Write};
|
||||
use std::mem;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Instant;
|
||||
|
||||
use console::{measure_text_width, Style};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use instant::Instant;
|
||||
#[cfg(feature = "unicode-segmentation")]
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::format::{
|
||||
BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
|
||||
HumanFloatCount,
|
||||
};
|
||||
use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressStyle {
|
||||
tick_strings: Vec<Box<str>>,
|
||||
progress_chars: Vec<Box<str>>,
|
||||
template: Template,
|
||||
// how unicode-big each char in progress_chars is
|
||||
char_width: usize,
|
||||
tab_width: usize,
|
||||
pub(crate) format_map: HashMap<&'static str, Box<dyn ProgressTracker>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "unicode-segmentation")]
|
||||
fn segment(s: &str) -> Vec<Box<str>> {
|
||||
UnicodeSegmentation::graphemes(s, true)
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unicode-segmentation"))]
|
||||
fn segment(s: &str) -> Vec<Box<str>> {
|
||||
s.chars().map(|x| x.to_string().into()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "unicode-width")]
|
||||
fn measure(s: &str) -> usize {
|
||||
unicode_width::UnicodeWidthStr::width(s)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unicode-width"))]
|
||||
fn measure(s: &str) -> usize {
|
||||
s.chars().count()
|
||||
}
|
||||
|
||||
/// finds the unicode-aware width of the passed grapheme cluters
|
||||
/// panics on an empty parameter, or if the characters are not equal-width
|
||||
fn width(c: &[Box<str>]) -> usize {
|
||||
c.iter()
|
||||
.map(|s| measure(s.as_ref()))
|
||||
.fold(None, |acc, new| {
|
||||
match acc {
|
||||
None => return Some(new),
|
||||
Some(old) => assert_eq!(old, new, "got passed un-equal width progress characters"),
|
||||
}
|
||||
acc
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
impl ProgressStyle {
|
||||
/// Returns the default progress bar style for bars
|
||||
pub fn default_bar() -> Self {
|
||||
Self::new(Template::from_str("{wide_bar} {pos}/{len}").unwrap())
|
||||
}
|
||||
|
||||
/// Returns the default progress bar style for spinners
|
||||
pub fn default_spinner() -> Self {
|
||||
Self::new(Template::from_str("{spinner} {msg}").unwrap())
|
||||
}
|
||||
|
||||
/// Sets the template string for the progress bar
|
||||
///
|
||||
/// Review the [list of template keys](../index.html#templates) for more information.
|
||||
pub fn with_template(template: &str) -> Result<Self, TemplateError> {
|
||||
Ok(Self::new(Template::from_str(template)?))
|
||||
}
|
||||
|
||||
pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
|
||||
self.tab_width = new_tab_width;
|
||||
self.template.set_tab_width(new_tab_width);
|
||||
}
|
||||
|
||||
fn new(template: Template) -> Self {
|
||||
let progress_chars = segment("█░");
|
||||
let char_width = width(&progress_chars);
|
||||
Self {
|
||||
tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
|
||||
.chars()
|
||||
.map(|c| c.to_string().into())
|
||||
.collect(),
|
||||
progress_chars,
|
||||
char_width,
|
||||
template,
|
||||
format_map: HashMap::default(),
|
||||
tab_width: DEFAULT_TAB_WIDTH,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the tick character sequence for spinners
|
||||
///
|
||||
/// Note that the last character is used as the [final tick string][Self::get_final_tick_str()].
|
||||
/// At least two characters are required to provide a non-final and final state.
|
||||
pub fn tick_chars(mut self, s: &str) -> Self {
|
||||
self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
|
||||
// Format bar will panic with some potentially confusing message, better to panic here
|
||||
// with a message explicitly informing of the problem
|
||||
assert!(
|
||||
self.tick_strings.len() >= 2,
|
||||
"at least 2 tick chars required"
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the tick string sequence for spinners
|
||||
///
|
||||
/// Note that the last string is used as the [final tick string][Self::get_final_tick_str()].
|
||||
/// At least two strings are required to provide a non-final and final state.
|
||||
pub fn tick_strings(mut self, s: &[&str]) -> Self {
|
||||
self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
|
||||
// Format bar will panic with some potentially confusing message, better to panic here
|
||||
// with a message explicitly informing of the problem
|
||||
assert!(
|
||||
self.progress_chars.len() >= 2,
|
||||
"at least 2 tick strings required"
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the progress characters `(filled, current, to do)`
|
||||
///
|
||||
/// You can pass more than three for a more detailed display.
|
||||
/// All passed grapheme clusters need to be of equal width.
|
||||
pub fn progress_chars(mut self, s: &str) -> Self {
|
||||
self.progress_chars = segment(s);
|
||||
// Format bar will panic with some potentially confusing message, better to panic here
|
||||
// with a message explicitly informing of the problem
|
||||
assert!(
|
||||
self.progress_chars.len() >= 2,
|
||||
"at least 2 progress chars required"
|
||||
);
|
||||
self.char_width = width(&self.progress_chars);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a custom key that owns a [`ProgressTracker`] to the template
|
||||
pub fn with_key<S: ProgressTracker + 'static>(mut self, key: &'static str, f: S) -> Self {
|
||||
self.format_map.insert(key, Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the template string for the progress bar
|
||||
///
|
||||
/// Review the [list of template keys](../index.html#templates) for more information.
|
||||
pub fn template(mut self, s: &str) -> Result<Self, TemplateError> {
|
||||
self.template = Template::from_str(s)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn current_tick_str(&self, state: &ProgressState) -> &str {
|
||||
match state.is_finished() {
|
||||
true => self.get_final_tick_str(),
|
||||
false => self.get_tick_str(state.tick),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tick string for a given number
|
||||
pub fn get_tick_str(&self, idx: u64) -> &str {
|
||||
&self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
|
||||
}
|
||||
|
||||
/// Returns the tick string for the finished state
|
||||
pub fn get_final_tick_str(&self) -> &str {
|
||||
&self.tick_strings[self.tick_strings.len() - 1]
|
||||
}
|
||||
|
||||
fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> BarDisplay<'_> {
|
||||
// The number of clusters from progress_chars to write (rounding down).
|
||||
let width = width / self.char_width;
|
||||
// The number of full clusters (including a fractional component for a partially-full one).
|
||||
let fill = fract * width as f32;
|
||||
// The number of entirely full clusters (by truncating `fill`).
|
||||
let entirely_filled = fill as usize;
|
||||
// 1 if the bar is not entirely empty or full (meaning we need to draw the "current"
|
||||
// character between the filled and "to do" segment), 0 otherwise.
|
||||
let head = usize::from(fill > 0.0 && entirely_filled < width);
|
||||
|
||||
let cur = if head == 1 {
|
||||
// Number of fine-grained progress entries in progress_chars.
|
||||
let n = self.progress_chars.len().saturating_sub(2);
|
||||
let cur_char = if n <= 1 {
|
||||
// No fine-grained entries. 1 is the single "current" entry if we have one, the "to
|
||||
// do" entry if not.
|
||||
1
|
||||
} else {
|
||||
// Pick a fine-grained entry, ranging from the last one (n) if the fractional part
|
||||
// of fill is 0 to the first one (1) if the fractional part of fill is almost 1.
|
||||
n.saturating_sub((fill.fract() * n as f32) as usize)
|
||||
};
|
||||
Some(cur_char)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Number of entirely empty clusters needed to fill the bar up to `width`.
|
||||
let bg = width.saturating_sub(entirely_filled).saturating_sub(head);
|
||||
let rest = RepeatedStringDisplay {
|
||||
str: &self.progress_chars[self.progress_chars.len() - 1],
|
||||
num: bg,
|
||||
};
|
||||
|
||||
BarDisplay {
|
||||
chars: &self.progress_chars,
|
||||
filled: entirely_filled,
|
||||
cur,
|
||||
rest: alt_style.unwrap_or(&Style::new()).apply_to(rest),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_state(
|
||||
&self,
|
||||
state: &ProgressState,
|
||||
lines: &mut Vec<String>,
|
||||
target_width: u16,
|
||||
) {
|
||||
let mut cur = String::new();
|
||||
let mut buf = String::new();
|
||||
let mut wide = None;
|
||||
|
||||
let pos = state.pos();
|
||||
let len = state.len().unwrap_or(pos);
|
||||
for part in &self.template.parts {
|
||||
match part {
|
||||
TemplatePart::Placeholder {
|
||||
key,
|
||||
align,
|
||||
width,
|
||||
truncate,
|
||||
style,
|
||||
alt_style,
|
||||
} => {
|
||||
buf.clear();
|
||||
if let Some(tracker) = self.format_map.get(key.as_str()) {
|
||||
tracker.write(state, &mut TabRewriter(&mut buf, self.tab_width));
|
||||
} else {
|
||||
match key.as_str() {
|
||||
"wide_bar" => {
|
||||
wide = Some(WideElement::Bar { alt_style });
|
||||
buf.push('\x00');
|
||||
}
|
||||
"bar" => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}",
|
||||
self.format_bar(
|
||||
state.fraction(),
|
||||
width.unwrap_or(20) as usize,
|
||||
alt_style.as_ref(),
|
||||
)
|
||||
))
|
||||
.unwrap(),
|
||||
"spinner" => buf.push_str(self.current_tick_str(state)),
|
||||
"wide_msg" => {
|
||||
wide = Some(WideElement::Message { align });
|
||||
buf.push('\x00');
|
||||
}
|
||||
"msg" => buf.push_str(state.message.expanded()),
|
||||
"prefix" => buf.push_str(state.prefix.expanded()),
|
||||
"pos" => buf.write_fmt(format_args!("{pos}")).unwrap(),
|
||||
"human_pos" => {
|
||||
buf.write_fmt(format_args!("{}", HumanCount(pos))).unwrap();
|
||||
}
|
||||
"len" => buf.write_fmt(format_args!("{len}")).unwrap(),
|
||||
"human_len" => {
|
||||
buf.write_fmt(format_args!("{}", HumanCount(len))).unwrap();
|
||||
}
|
||||
"percent" => buf
|
||||
.write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32))
|
||||
.unwrap(),
|
||||
"bytes" => buf.write_fmt(format_args!("{}", HumanBytes(pos))).unwrap(),
|
||||
"total_bytes" => {
|
||||
buf.write_fmt(format_args!("{}", HumanBytes(len))).unwrap();
|
||||
}
|
||||
"decimal_bytes" => buf
|
||||
.write_fmt(format_args!("{}", DecimalBytes(pos)))
|
||||
.unwrap(),
|
||||
"decimal_total_bytes" => buf
|
||||
.write_fmt(format_args!("{}", DecimalBytes(len)))
|
||||
.unwrap(),
|
||||
"binary_bytes" => {
|
||||
buf.write_fmt(format_args!("{}", BinaryBytes(pos))).unwrap();
|
||||
}
|
||||
"binary_total_bytes" => {
|
||||
buf.write_fmt(format_args!("{}", BinaryBytes(len))).unwrap();
|
||||
}
|
||||
"elapsed_precise" => buf
|
||||
.write_fmt(format_args!("{}", FormattedDuration(state.elapsed())))
|
||||
.unwrap(),
|
||||
"elapsed" => buf
|
||||
.write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
|
||||
.unwrap(),
|
||||
"per_sec" => buf
|
||||
.write_fmt(format_args!("{}/s", HumanFloatCount(state.per_sec())))
|
||||
.unwrap(),
|
||||
"bytes_per_sec" => buf
|
||||
.write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
|
||||
.unwrap(),
|
||||
"binary_bytes_per_sec" => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}/s",
|
||||
BinaryBytes(state.per_sec() as u64)
|
||||
))
|
||||
.unwrap(),
|
||||
"eta_precise" => buf
|
||||
.write_fmt(format_args!("{}", FormattedDuration(state.eta())))
|
||||
.unwrap(),
|
||||
"eta" => buf
|
||||
.write_fmt(format_args!("{:#}", HumanDuration(state.eta())))
|
||||
.unwrap(),
|
||||
"duration_precise" => buf
|
||||
.write_fmt(format_args!("{}", FormattedDuration(state.duration())))
|
||||
.unwrap(),
|
||||
"duration" => buf
|
||||
.write_fmt(format_args!("{:#}", HumanDuration(state.duration())))
|
||||
.unwrap(),
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
match width {
|
||||
Some(width) => {
|
||||
let padded = PaddedStringDisplay {
|
||||
str: &buf,
|
||||
width: *width as usize,
|
||||
align: *align,
|
||||
truncate: *truncate,
|
||||
};
|
||||
match style {
|
||||
Some(s) => cur
|
||||
.write_fmt(format_args!("{}", s.apply_to(padded)))
|
||||
.unwrap(),
|
||||
None => cur.write_fmt(format_args!("{padded}")).unwrap(),
|
||||
}
|
||||
}
|
||||
None => match style {
|
||||
Some(s) => cur.write_fmt(format_args!("{}", s.apply_to(&buf))).unwrap(),
|
||||
None => cur.push_str(&buf),
|
||||
},
|
||||
}
|
||||
}
|
||||
TemplatePart::Literal(s) => cur.push_str(s.expanded()),
|
||||
TemplatePart::NewLine => {
|
||||
self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cur.is_empty() {
|
||||
self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_line(
|
||||
&self,
|
||||
lines: &mut Vec<String>,
|
||||
cur: &mut String,
|
||||
state: &ProgressState,
|
||||
buf: &mut String,
|
||||
target_width: u16,
|
||||
wide: &Option<WideElement>,
|
||||
) {
|
||||
let expanded = match wide {
|
||||
Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width),
|
||||
None => mem::take(cur),
|
||||
};
|
||||
|
||||
// If there are newlines, we need to split them up
|
||||
// and add the lines separately so that they're counted
|
||||
// correctly on re-render.
|
||||
for (i, line) in expanded.split('\n').enumerate() {
|
||||
// No newlines found in this case
|
||||
if i == 0 && line.len() == expanded.len() {
|
||||
lines.push(expanded);
|
||||
break;
|
||||
}
|
||||
|
||||
lines.push(line.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize);
|
||||
|
||||
impl Write for TabRewriter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0
|
||||
.write_str(s.replace('\t', &" ".repeat(self.1)).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum WideElement<'a> {
|
||||
Bar { alt_style: &'a Option<Style> },
|
||||
Message { align: &'a Alignment },
|
||||
}
|
||||
|
||||
impl<'a> WideElement<'a> {
|
||||
fn expand(
|
||||
self,
|
||||
cur: String,
|
||||
style: &ProgressStyle,
|
||||
state: &ProgressState,
|
||||
buf: &mut String,
|
||||
width: u16,
|
||||
) -> String {
|
||||
let left = (width as usize).saturating_sub(measure_text_width(&cur.replace('\x00', "")));
|
||||
match self {
|
||||
Self::Bar { alt_style } => cur.replace(
|
||||
'\x00',
|
||||
&format!(
|
||||
"{}",
|
||||
style.format_bar(state.fraction(), left, alt_style.as_ref())
|
||||
),
|
||||
),
|
||||
WideElement::Message { align } => {
|
||||
buf.clear();
|
||||
buf.write_fmt(format_args!(
|
||||
"{}",
|
||||
PaddedStringDisplay {
|
||||
str: state.message.expanded(),
|
||||
width: left,
|
||||
align: *align,
|
||||
truncate: true,
|
||||
}
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let trimmed = match cur.as_bytes().last() == Some(&b'\x00') {
|
||||
true => buf.trim_end(),
|
||||
false => buf,
|
||||
};
|
||||
|
||||
cur.replace('\x00', trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Template {
|
||||
parts: Vec<TemplatePart>,
|
||||
}
|
||||
|
||||
impl Template {
|
||||
fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> {
|
||||
use State::*;
|
||||
let (mut state, mut parts, mut buf) = (Literal, vec![], String::new());
|
||||
for c in s.chars() {
|
||||
let new = match (state, c) {
|
||||
(Literal, '{') => (MaybeOpen, None),
|
||||
(Literal, '\n') => {
|
||||
if !buf.is_empty() {
|
||||
parts.push(TemplatePart::Literal(TabExpandedString::new(
|
||||
mem::take(&mut buf).into(),
|
||||
tab_width,
|
||||
)));
|
||||
}
|
||||
parts.push(TemplatePart::NewLine);
|
||||
(Literal, None)
|
||||
}
|
||||
(Literal, '}') => (DoubleClose, Some('}')),
|
||||
(Literal, c) => (Literal, Some(c)),
|
||||
(DoubleClose, '}') => (Literal, None),
|
||||
(MaybeOpen, '{') => (Literal, Some('{')),
|
||||
(MaybeOpen | Key, c) if c.is_ascii_whitespace() => {
|
||||
// If we find whitespace where the variable key is supposed to go,
|
||||
// backtrack and act as if this was a literal.
|
||||
buf.push(c);
|
||||
let mut new = String::from("{");
|
||||
new.push_str(&buf);
|
||||
buf.clear();
|
||||
parts.push(TemplatePart::Literal(TabExpandedString::new(
|
||||
new.into(),
|
||||
tab_width,
|
||||
)));
|
||||
(Literal, None)
|
||||
}
|
||||
(MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)),
|
||||
(Key, c) if c != '}' && c != ':' => (Key, Some(c)),
|
||||
(Key, ':') => (Align, None),
|
||||
(Key, '}') => (Literal, None),
|
||||
(Key, '!') if !buf.is_empty() => {
|
||||
parts.push(TemplatePart::Placeholder {
|
||||
key: mem::take(&mut buf),
|
||||
align: Alignment::Left,
|
||||
width: None,
|
||||
truncate: true,
|
||||
style: None,
|
||||
alt_style: None,
|
||||
});
|
||||
(Width, None)
|
||||
}
|
||||
(Align, c) if c == '<' || c == '^' || c == '>' => {
|
||||
if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() {
|
||||
match c {
|
||||
'<' => *align = Alignment::Left,
|
||||
'^' => *align = Alignment::Center,
|
||||
'>' => *align = Alignment::Right,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
(Width, None)
|
||||
}
|
||||
(Align, c @ '0'..='9') => (Width, Some(c)),
|
||||
(Align | Width, '!') => {
|
||||
if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() {
|
||||
*truncate = true;
|
||||
}
|
||||
(Width, None)
|
||||
}
|
||||
(Align, '.') => (FirstStyle, None),
|
||||
(Align, '}') => (Literal, None),
|
||||
(Width, c @ '0'..='9') => (Width, Some(c)),
|
||||
(Width, '.') => (FirstStyle, None),
|
||||
(Width, '}') => (Literal, None),
|
||||
(FirstStyle, '/') => (AltStyle, None),
|
||||
(FirstStyle, '}') => (Literal, None),
|
||||
(FirstStyle, c) => (FirstStyle, Some(c)),
|
||||
(AltStyle, '}') => (Literal, None),
|
||||
(AltStyle, c) => (AltStyle, Some(c)),
|
||||
(st, c) => return Err(TemplateError { next: c, state: st }),
|
||||
};
|
||||
|
||||
match (state, new.0) {
|
||||
(MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal(
|
||||
TabExpandedString::new(mem::take(&mut buf).into(), tab_width),
|
||||
)),
|
||||
(Key, Align | Literal) if !buf.is_empty() => {
|
||||
parts.push(TemplatePart::Placeholder {
|
||||
key: mem::take(&mut buf),
|
||||
align: Alignment::Left,
|
||||
width: None,
|
||||
truncate: false,
|
||||
style: None,
|
||||
alt_style: None,
|
||||
});
|
||||
}
|
||||
(Width, FirstStyle | Literal) if !buf.is_empty() => {
|
||||
if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() {
|
||||
*width = Some(buf.parse().unwrap());
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
(FirstStyle, AltStyle | Literal) if !buf.is_empty() => {
|
||||
if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() {
|
||||
*style = Some(Style::from_dotted_str(&buf));
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
(AltStyle, Literal) if !buf.is_empty() => {
|
||||
if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() {
|
||||
*alt_style = Some(Style::from_dotted_str(&buf));
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
(_, _) => (),
|
||||
}
|
||||
|
||||
state = new.0;
|
||||
if let Some(c) = new.1 {
|
||||
buf.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(state, Literal | DoubleClose) && !buf.is_empty() {
|
||||
parts.push(TemplatePart::Literal(TabExpandedString::new(
|
||||
buf.into(),
|
||||
tab_width,
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self { parts })
|
||||
}
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, TemplateError> {
|
||||
Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH)
|
||||
}
|
||||
|
||||
fn set_tab_width(&mut self, new_tab_width: usize) {
|
||||
for part in &mut self.parts {
|
||||
if let TemplatePart::Literal(s) = part {
|
||||
s.set_tab_width(new_tab_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TemplateError {
|
||||
state: State,
|
||||
next: char,
|
||||
}
|
||||
|
||||
impl fmt::Display for TemplateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"TemplateError: unexpected character {:?} in state {:?}",
|
||||
self.next, self.state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TemplateError {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum TemplatePart {
|
||||
Literal(TabExpandedString),
|
||||
Placeholder {
|
||||
key: String,
|
||||
align: Alignment,
|
||||
width: Option<u16>,
|
||||
truncate: bool,
|
||||
style: Option<Style>,
|
||||
alt_style: Option<Style>,
|
||||
},
|
||||
NewLine,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum State {
|
||||
Literal,
|
||||
MaybeOpen,
|
||||
DoubleClose,
|
||||
Key,
|
||||
Align,
|
||||
Width,
|
||||
FirstStyle,
|
||||
AltStyle,
|
||||
}
|
||||
|
||||
struct BarDisplay<'a> {
|
||||
chars: &'a [Box<str>],
|
||||
filled: usize,
|
||||
cur: Option<usize>,
|
||||
rest: console::StyledObject<RepeatedStringDisplay<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BarDisplay<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for _ in 0..self.filled {
|
||||
f.write_str(&self.chars[0])?;
|
||||
}
|
||||
if let Some(cur) = self.cur {
|
||||
f.write_str(&self.chars[cur])?;
|
||||
}
|
||||
self.rest.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct RepeatedStringDisplay<'a> {
|
||||
str: &'a str,
|
||||
num: usize,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for RepeatedStringDisplay<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for _ in 0..self.num {
|
||||
f.write_str(self.str)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct PaddedStringDisplay<'a> {
|
||||
str: &'a str,
|
||||
width: usize,
|
||||
align: Alignment,
|
||||
truncate: bool,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for PaddedStringDisplay<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let cols = measure_text_width(self.str);
|
||||
let excess = cols.saturating_sub(self.width);
|
||||
if excess > 0 && !self.truncate {
|
||||
return f.write_str(self.str);
|
||||
} else if excess > 0 {
|
||||
let (start, end) = match self.align {
|
||||
Alignment::Left => (0, self.str.len() - excess),
|
||||
Alignment::Right => (excess, self.str.len()),
|
||||
Alignment::Center => (
|
||||
excess / 2,
|
||||
self.str.len() - excess.saturating_sub(excess / 2),
|
||||
),
|
||||
};
|
||||
|
||||
return f.write_str(self.str.get(start..end).unwrap_or(self.str));
|
||||
}
|
||||
|
||||
let diff = self.width.saturating_sub(cols);
|
||||
let (left_pad, right_pad) = match self.align {
|
||||
Alignment::Left => (0, diff),
|
||||
Alignment::Right => (diff, 0),
|
||||
Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
|
||||
};
|
||||
|
||||
for _ in 0..left_pad {
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
f.write_str(self.str)?;
|
||||
for _ in 0..right_pad {
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Trait for defining stateful or stateless formatters
|
||||
pub trait ProgressTracker: Send + Sync {
|
||||
/// Creates a new instance of the progress tracker
|
||||
fn clone_box(&self) -> Box<dyn ProgressTracker>;
|
||||
/// Notifies the progress tracker of a tick event
|
||||
fn tick(&mut self, state: &ProgressState, now: Instant);
|
||||
/// Notifies the progress tracker of a reset event
|
||||
fn reset(&mut self, state: &ProgressState, now: Instant);
|
||||
/// Provides access to the progress bar display buffer for custom messages
|
||||
fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write);
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn ProgressTracker> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ProgressTracker for F
|
||||
where
|
||||
F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static,
|
||||
{
|
||||
fn clone_box(&self) -> Box<dyn ProgressTracker> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn tick(&mut self, _: &ProgressState, _: Instant) {}
|
||||
|
||||
fn reset(&mut self, _: &ProgressState, _: Instant) {}
|
||||
|
||||
fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) {
|
||||
(self)(state, w);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::state::{AtomicPosition, ProgressState};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[test]
|
||||
fn test_stateful_tracker() {
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestTracker(Arc<Mutex<String>>);
|
||||
|
||||
impl ProgressTracker for TestTracker {
|
||||
fn clone_box(&self) -> Box<dyn ProgressTracker> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn tick(&mut self, state: &ProgressState, _: Instant) {
|
||||
let mut m = self.0.lock().unwrap();
|
||||
m.clear();
|
||||
m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str());
|
||||
}
|
||||
|
||||
fn reset(&mut self, _state: &ProgressState, _: Instant) {
|
||||
let mut m = self.0.lock().unwrap();
|
||||
m.clear();
|
||||
}
|
||||
|
||||
fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) {
|
||||
w.write_str(self.0.lock().unwrap().as_str()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
use crate::ProgressBar;
|
||||
|
||||
let pb = ProgressBar::new(1);
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template("{{ {foo} }}")
|
||||
.unwrap()
|
||||
.with_key("foo", TestTracker(Arc::new(Mutex::new(String::default()))))
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let style = pb.clone().style();
|
||||
|
||||
style.format_state(&pb.state().state, &mut buf, 16);
|
||||
assert_eq!(&buf[0], "{ }");
|
||||
buf.clear();
|
||||
pb.inc(1);
|
||||
style.format_state(&pb.state().state, &mut buf, 16);
|
||||
assert_eq!(&buf[0], "{ 1 1 }");
|
||||
pb.reset();
|
||||
buf.clear();
|
||||
style.format_state(&pb.state().state, &mut buf, 16);
|
||||
assert_eq!(&buf[0], "{ }");
|
||||
pb.finish_and_clear();
|
||||
}
|
||||
|
||||
use crate::state::TabExpandedString;
|
||||
|
||||
#[test]
|
||||
fn test_expand_template() {
|
||||
const WIDTH: u16 = 80;
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
let state = ProgressState::new(Some(10), pos);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut style = ProgressStyle::default_bar();
|
||||
style.format_map.insert(
|
||||
"foo",
|
||||
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()),
|
||||
);
|
||||
style.format_map.insert(
|
||||
"bar",
|
||||
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()),
|
||||
);
|
||||
|
||||
style.template = Template::from_str("{{ {foo} {bar} }}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "{ FOO BAR }");
|
||||
|
||||
buf.clear();
|
||||
style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_template_flags() {
|
||||
use console::set_colors_enabled;
|
||||
set_colors_enabled(true);
|
||||
|
||||
const WIDTH: u16 = 80;
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
let state = ProgressState::new(Some(10), pos);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut style = ProgressStyle::default_bar();
|
||||
style.format_map.insert(
|
||||
"foo",
|
||||
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()),
|
||||
);
|
||||
|
||||
style.template = Template::from_str("{foo:5}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "XXX ");
|
||||
|
||||
buf.clear();
|
||||
style.template = Template::from_str("{foo:.red.on_blue}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m");
|
||||
|
||||
buf.clear();
|
||||
style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
|
||||
|
||||
buf.clear();
|
||||
style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_truncation() {
|
||||
const WIDTH: u16 = 10;
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
let mut state = ProgressState::new(Some(10), pos);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let style = ProgressStyle::with_template("{wide_msg}").unwrap();
|
||||
state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "abcdefghij");
|
||||
|
||||
buf.clear();
|
||||
let style = ProgressStyle::with_template("{wide_msg:>}").unwrap();
|
||||
state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "klmnopqrst");
|
||||
|
||||
buf.clear();
|
||||
let style = ProgressStyle::with_template("{wide_msg:^}").unwrap();
|
||||
state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "fghijklmno");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_element_style() {
|
||||
const CHARS: &str = "=>-";
|
||||
const WIDTH: u16 = 8;
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
// half finished
|
||||
pos.set(2);
|
||||
let mut state = ProgressState::new(Some(4), pos);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let style = ProgressStyle::with_template("{wide_bar}")
|
||||
.unwrap()
|
||||
.progress_chars(CHARS);
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "====>---");
|
||||
|
||||
buf.clear();
|
||||
let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}")
|
||||
.unwrap()
|
||||
.progress_chars(CHARS);
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(
|
||||
&buf[0],
|
||||
"\u{1b}[31m\u{1b}[44m====>\u{1b}[32m\u{1b}[46m---\u{1b}[0m\u{1b}[0m"
|
||||
);
|
||||
|
||||
buf.clear();
|
||||
let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap();
|
||||
state.message = TabExpandedString::NoTabs("foobar".into());
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m foobar \u{1b}[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_handling() {
|
||||
const WIDTH: u16 = 80;
|
||||
let pos = Arc::new(AtomicPosition::new());
|
||||
let mut state = ProgressState::new(Some(10), pos);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut style = ProgressStyle::default_bar();
|
||||
state.message = TabExpandedString::new("foo\nbar\nbaz".into(), 2);
|
||||
style.template = Template::from_str("{msg}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
|
||||
assert_eq!(buf.len(), 3);
|
||||
assert_eq!(&buf[0], "foo");
|
||||
assert_eq!(&buf[1], "bar");
|
||||
assert_eq!(&buf[2], "baz");
|
||||
|
||||
buf.clear();
|
||||
style.template = Template::from_str("{wide_msg}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
|
||||
assert_eq!(buf.len(), 3);
|
||||
assert_eq!(&buf[0], "foo");
|
||||
assert_eq!(&buf[1], "bar");
|
||||
assert_eq!(&buf[2], "baz");
|
||||
|
||||
buf.clear();
|
||||
state.prefix = TabExpandedString::new("prefix\nprefix".into(), 2);
|
||||
style.template = Template::from_str("{prefix} {wide_msg}").unwrap();
|
||||
style.format_state(&state, &mut buf, WIDTH);
|
||||
|
||||
assert_eq!(buf.len(), 4);
|
||||
assert_eq!(&buf[0], "prefix");
|
||||
assert_eq!(&buf[1], "prefix foo");
|
||||
assert_eq!(&buf[2], "bar");
|
||||
assert_eq!(&buf[3], "baz");
|
||||
}
|
||||
}
|
79
vendor/indicatif/src/term_like.rs
vendored
Normal file
79
vendor/indicatif/src/term_like.rs
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io;
|
||||
|
||||
use console::Term;
|
||||
|
||||
/// A trait for minimal terminal-like behavior.
|
||||
///
|
||||
/// Anything that implements this trait can be used a draw target via [`ProgressDrawTarget::term_like`].
|
||||
///
|
||||
/// [`ProgressDrawTarget::term_like`]: crate::ProgressDrawTarget::term_like
|
||||
pub trait TermLike: Debug + Send + Sync {
|
||||
/// Return the terminal width
|
||||
fn width(&self) -> u16;
|
||||
/// Return the terminal height
|
||||
fn height(&self) -> u16 {
|
||||
// FIXME: remove this default impl in the next major version bump
|
||||
20 // sensible default
|
||||
}
|
||||
|
||||
/// Move the cursor up by `n` lines
|
||||
fn move_cursor_up(&self, n: usize) -> io::Result<()>;
|
||||
/// Move the cursor down by `n` lines
|
||||
fn move_cursor_down(&self, n: usize) -> io::Result<()>;
|
||||
/// Move the cursor right by `n` chars
|
||||
fn move_cursor_right(&self, n: usize) -> io::Result<()>;
|
||||
/// Move the cursor left by `n` chars
|
||||
fn move_cursor_left(&self, n: usize) -> io::Result<()>;
|
||||
|
||||
/// Write a string and add a newline.
|
||||
fn write_line(&self, s: &str) -> io::Result<()>;
|
||||
/// Write a string
|
||||
fn write_str(&self, s: &str) -> io::Result<()>;
|
||||
/// Clear the current line and reset the cursor to beginning of the line
|
||||
fn clear_line(&self) -> io::Result<()>;
|
||||
|
||||
fn flush(&self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl TermLike for Term {
|
||||
fn width(&self) -> u16 {
|
||||
self.size().1
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.size().0
|
||||
}
|
||||
|
||||
fn move_cursor_up(&self, n: usize) -> io::Result<()> {
|
||||
self.move_cursor_up(n)
|
||||
}
|
||||
|
||||
fn move_cursor_down(&self, n: usize) -> io::Result<()> {
|
||||
self.move_cursor_down(n)
|
||||
}
|
||||
|
||||
fn move_cursor_right(&self, n: usize) -> io::Result<()> {
|
||||
self.move_cursor_right(n)
|
||||
}
|
||||
|
||||
fn move_cursor_left(&self, n: usize) -> io::Result<()> {
|
||||
self.move_cursor_left(n)
|
||||
}
|
||||
|
||||
fn write_line(&self, s: &str) -> io::Result<()> {
|
||||
self.write_line(s)
|
||||
}
|
||||
|
||||
fn write_str(&self, s: &str) -> io::Result<()> {
|
||||
self.write_str(s)
|
||||
}
|
||||
|
||||
fn clear_line(&self) -> io::Result<()> {
|
||||
self.clear_line()
|
||||
}
|
||||
|
||||
fn flush(&self) -> io::Result<()> {
|
||||
self.flush()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user