loaders 0.0.0

A fully-featured, customisable progress bar and loading indicator library for Rust CLI and terminal applications
Documentation
//! Thread-safe draw targets for terminal rendering.

use crate::terminal::detect::{supports_interactive_output, terminal_width_for_fd};
use std::fmt;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};

/// A destination for rendered progress output.
///
/// Use `stdout` or `stderr` for terminal output, `hidden` for tests and silent
/// runs, and `to_writer` to capture output in a custom writer.
pub enum DrawTarget {
    /// Standard output.
    Stdout,
    /// Standard error.
    Stderr,
    /// Suppresses all rendered output.
    Hidden,
    /// A caller-provided writer protected by a mutex.
    CustomWriter(Arc<Mutex<dyn Write + Send>>),
}

impl DrawTarget {
    /// Creates a standard-output draw target.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let target = loaders::DrawTarget::stdout();
    /// assert!(!target.is_hidden_for_tests());
    /// ```
    pub fn stdout() -> Self {
        Self::Stdout
    }

    /// Creates a standard-error draw target.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let target = loaders::DrawTarget::stderr();
    /// assert!(!target.is_hidden_for_tests());
    /// ```
    pub fn stderr() -> Self {
        Self::Stderr
    }

    /// Creates a target that discards all output.
    ///
    /// This is useful for tests, benchmarks, and non-interactive paths.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let target = loaders::DrawTarget::hidden();
    /// assert!(target.is_hidden_for_tests());
    /// ```
    pub fn hidden() -> Self {
        Self::Hidden
    }

    /// Creates a draw target from a custom writer.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let target = loaders::DrawTarget::to_writer(Vec::<u8>::new());
    /// assert!(!target.is_hidden_for_tests());
    /// ```
    pub fn to_writer(w: impl Write + Send + 'static) -> Self {
        Self::CustomWriter(Arc::new(Mutex::new(w)))
    }

    /// Returns whether this target discards output.
    ///
    /// This public helper is intended for examples and tests.
    ///
    /// # Examples
    ///
    /// ```rust
    /// assert!(loaders::DrawTarget::hidden().is_hidden_for_tests());
    /// ```
    pub fn is_hidden_for_tests(&self) -> bool {
        self.is_hidden()
    }

    pub(crate) fn is_hidden(&self) -> bool {
        matches!(self, Self::Hidden)
    }

    pub(crate) fn is_tty(&self) -> bool {
        match self {
            Self::Stdout => supports_interactive_output(1),
            Self::Stderr => supports_interactive_output(2),
            Self::Hidden | Self::CustomWriter(_) => false,
        }
    }

    pub(crate) fn width(&self) -> usize {
        match self {
            Self::Stdout => terminal_width_for_fd(1),
            Self::Stderr => terminal_width_for_fd(2),
            Self::Hidden | Self::CustomWriter(_) => terminal_width_for_fd(1),
        }
    }

    pub(crate) fn write_str(&self, s: &str) {
        match self {
            Self::Stdout => {
                let mut out = io::stdout().lock();
                let _ = out.write_all(s.as_bytes());
            }
            Self::Stderr => {
                let mut out = io::stderr().lock();
                let _ = out.write_all(s.as_bytes());
            }
            Self::Hidden => {}
            Self::CustomWriter(writer) => {
                if let Ok(mut guard) = writer.lock() {
                    let _ = guard.write_all(s.as_bytes());
                }
            }
        }
    }

    pub(crate) fn flush(&self) {
        match self {
            Self::Stdout => {
                let _ = io::stdout().lock().flush();
            }
            Self::Stderr => {
                let _ = io::stderr().lock().flush();
            }
            Self::Hidden => {}
            Self::CustomWriter(writer) => {
                if let Ok(mut guard) = writer.lock() {
                    let _ = guard.flush();
                }
            }
        }
    }
}

impl Clone for DrawTarget {
    fn clone(&self) -> Self {
        match self {
            Self::Stdout => Self::Stdout,
            Self::Stderr => Self::Stderr,
            Self::Hidden => Self::Hidden,
            Self::CustomWriter(writer) => Self::CustomWriter(Arc::clone(writer)),
        }
    }
}

impl fmt::Debug for DrawTarget {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Stdout => f.write_str("DrawTarget::Stdout"),
            Self::Stderr => f.write_str("DrawTarget::Stderr"),
            Self::Hidden => f.write_str("DrawTarget::Hidden"),
            Self::CustomWriter(_) => f.write_str("DrawTarget::CustomWriter(..)"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Arc, Mutex};

    #[derive(Clone, Default)]
    struct SharedBuffer(Arc<Mutex<Vec<u8>>>);

    impl Write for SharedBuffer {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            match self.0.lock() {
                Ok(mut inner) => inner.write(buf),
                Err(poisoned) => poisoned.into_inner().write(buf),
            }
        }

        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }

    #[test]
    fn test_hidden_target_is_hidden() {
        assert!(DrawTarget::hidden().is_hidden());
    }

    #[test]
    fn test_stdout_not_hidden() {
        assert!(!DrawTarget::stdout().is_hidden());
    }

    #[test]
    fn test_custom_writer_captures_output() {
        let buffer = SharedBuffer::default();
        let mirror = buffer.clone();
        let target = DrawTarget::to_writer(buffer);
        target.write_str("hello");
        let bytes = match mirror.0.lock() {
            Ok(inner) => inner.clone(),
            Err(poisoned) => poisoned.into_inner().clone(),
        };
        assert_eq!(bytes, b"hello");
    }

    #[test]
    fn test_draw_target_clone() {
        let first = DrawTarget::hidden();
        let second = first.clone();
        assert!(second.is_hidden());
    }
}