loaders 0.0.0

A fully-featured, customisable progress bar and loading indicator library for Rust CLI and terminal applications
Documentation
//! Management of multiple progress bars in one terminal block.

use crate::bar::progress::ProgressBar;
use crate::bar::render::Renderer;
use crate::terminal::writer::DrawTarget;
use std::fmt;
use std::sync::{Arc, Mutex, MutexGuard};
use std::thread;
use std::time::Duration;

struct MultiState {
    bars: Vec<ProgressBar>,
    renderer: Renderer,
    move_cursor: bool,
}

/// A manager for rendering multiple progress bars together.
///
/// Bars added to a `MultiProgress` are redirected into the shared renderer, and
/// `join` keeps the display refreshed until every bar finishes or is abandoned.
#[derive(Clone)]
pub struct MultiProgress {
    inner: Arc<Mutex<MultiState>>,
}

impl MultiProgress {
    /// Creates a multi-progress renderer on standard error.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::new();
    /// multi.clear();
    /// ```
    pub fn new() -> Self {
        Self::with_draw_target(DrawTarget::stderr())
    }

    /// Creates a multi-progress renderer with a custom target.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// multi.clear();
    /// ```
    pub fn with_draw_target(target: DrawTarget) -> Self {
        Self {
            inner: Arc::new(Mutex::new(MultiState {
                bars: Vec::new(),
                renderer: Renderer::new(target),
                move_cursor: true,
            })),
        }
    }

    /// Adds a progress bar at the end of the block.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let pb = multi.add(loaders::ProgressBar::hidden());
    /// pb.finish();
    /// ```
    pub fn add(&self, pb: ProgressBar) -> ProgressBar {
        self.insert(usize::MAX, pb)
    }

    /// Inserts a progress bar at an index.
    ///
    /// Out-of-range indices append to the end.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let pb = multi.insert(0, loaders::ProgressBar::hidden());
    /// pb.finish();
    /// ```
    pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar {
        pb.redirect_target(DrawTarget::hidden());
        let mut inner = self.lock_inner();
        let idx = index.min(inner.bars.len());
        inner.bars.insert(idx, pb.clone());
        Self::render_locked(&mut inner);
        pb
    }

    /// Inserts a progress bar before another bar.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let first = multi.add(loaders::ProgressBar::hidden());
    /// let second = multi.insert_before(&first, loaders::ProgressBar::hidden());
    /// second.finish();
    /// first.finish();
    /// ```
    pub fn insert_before(&self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar {
        let index = self
            .lock_inner()
            .bars
            .iter()
            .position(|bar| bar.same_bar(before))
            .unwrap_or(0);
        self.insert(index, pb)
    }

    /// Inserts a progress bar after another bar.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let first = multi.add(loaders::ProgressBar::hidden());
    /// let second = multi.insert_after(&first, loaders::ProgressBar::hidden());
    /// second.finish();
    /// first.finish();
    /// ```
    pub fn insert_after(&self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar {
        let index = self
            .lock_inner()
            .bars
            .iter()
            .position(|bar| bar.same_bar(after))
            .map_or(usize::MAX, |idx| idx + 1);
        self.insert(index, pb)
    }

    /// Removes a progress bar from the block.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let pb = multi.add(loaders::ProgressBar::hidden());
    /// multi.remove(&pb);
    /// ```
    pub fn remove(&self, pb: &ProgressBar) {
        let mut inner = self.lock_inner();
        inner.bars.retain(|bar| !bar.same_bar(pb));
        Self::render_locked(&mut inner);
    }

    /// Prints a message above the multi-progress block.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// multi.println("hello");
    /// ```
    pub fn println(&self, msg: &str) {
        self.lock_inner().renderer.println(msg);
    }

    /// Clears the multi-progress block.
    ///
    /// # Examples
    ///
    /// ```rust
    /// loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden()).clear();
    /// ```
    pub fn clear(&self) {
        self.lock_inner().renderer.clear();
    }

    /// Blocks until all managed bars are finished or abandoned.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::with_draw_target(loaders::DrawTarget::hidden());
    /// let pb = multi.add(loaders::ProgressBar::hidden());
    /// pb.finish();
    /// multi.join();
    /// ```
    pub fn join(&self) {
        loop {
            let done = {
                let mut inner = self.lock_inner();
                Self::render_locked(&mut inner);
                inner.bars.iter().all(ProgressBar::is_complete_for_join)
            };
            if done {
                break;
            }
            thread::sleep(Duration::from_millis(50));
        }
    }

    /// Configures whether cursor movement should be preferred by the renderer.
    ///
    /// This setting is retained for API compatibility; non-TTY streams still use
    /// newline snapshots.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let multi = loaders::MultiProgress::new();
    /// multi.set_move_cursor(false);
    /// ```
    pub fn set_move_cursor(&self, val: bool) {
        self.lock_inner().move_cursor = val;
    }

    fn render_locked(inner: &mut MultiState) {
        let width = inner.renderer.target.width();
        let lines: Vec<String> = inner
            .bars
            .iter()
            .map(|bar| bar.render_line_for_width(width))
            .collect();
        if lines.is_empty() {
            inner.renderer.clear();
        } else {
            inner.renderer.draw(&lines.join("\n"));
        }
    }

    fn lock_inner(&self) -> MutexGuard<'_, MultiState> {
        match self.inner.lock() {
            Ok(guard) => guard,
            Err(poisoned) => poisoned.into_inner(),
        }
    }
}

impl Default for MultiProgress {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Debug for MultiProgress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let inner = self.lock_inner();
        f.debug_struct("MultiProgress")
            .field("bars", &inner.bars.len())
            .field("move_cursor", &inner.move_cursor)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hidden_multi() -> MultiProgress {
        MultiProgress::with_draw_target(DrawTarget::hidden())
    }

    #[test]
    fn test_add_bar_to_multi() {
        let multi = hidden_multi();
        let pb = multi.add(ProgressBar::hidden());
        assert!(pb.is_hidden());
        assert_eq!(multi.lock_inner().bars.len(), 1);
    }

    #[test]
    fn test_remove_bar() {
        let multi = hidden_multi();
        let pb = multi.add(ProgressBar::hidden());
        multi.remove(&pb);
        assert_eq!(multi.lock_inner().bars.len(), 0);
    }

    #[test]
    fn test_insert_before() {
        let multi = hidden_multi();
        let first = multi.add(ProgressBar::hidden());
        let second = multi.insert_before(&first, ProgressBar::hidden());
        let inner = multi.lock_inner();
        assert!(inner.bars[0].same_bar(&second));
    }

    #[test]
    fn test_insert_after() {
        let multi = hidden_multi();
        let first = multi.add(ProgressBar::hidden());
        let second = multi.insert_after(&first, ProgressBar::hidden());
        let inner = multi.lock_inner();
        assert!(inner.bars[1].same_bar(&second));
    }

    #[test]
    fn test_join_exits_when_all_finish() {
        let multi = hidden_multi();
        let pb = multi.add(ProgressBar::hidden());
        pb.finish();
        multi.join();
        assert!(pb.is_finished());
    }

    #[test]
    fn test_multi_println() {
        let multi = hidden_multi();
        multi.println("hello");
        assert_eq!(multi.lock_inner().bars.len(), 0);
    }
}