diff options
Diffstat (limited to 'vendor/indicatif/src/multi.rs')
-rw-r--r-- | vendor/indicatif/src/multi.rs | 688 |
1 files changed, 688 insertions, 0 deletions
diff --git a/vendor/indicatif/src/multi.rs b/vendor/indicatif/src/multi.rs new file mode 100644 index 0000000..4409309 --- /dev/null +++ b/vendor/indicatif/src/multi.rs @@ -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); + } +} |