loaders 0.0.0

A fully-featured, customisable progress bar and loading indicator library for Rust CLI and terminal applications
Documentation
//! Ergonomic spinner wrapper built on `ProgressBar`.

use crate::bar::progress::ProgressBar;
use crate::style::style::ProgressStyle;
use std::fmt;
use std::time::Duration;

/// A convenience wrapper for indeterminate loading indicators.
///
/// `Spinner` delegates rendering and thread safety to `ProgressBar` while
/// keeping spinner-specific names close at hand.
#[derive(Clone)]
pub struct Spinner {
    bar: ProgressBar,
    interval: Duration,
}

impl Spinner {
    /// Creates a spinner with custom frames.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// assert!(!spinner.is_finished());
    /// ```
    pub fn new(frames: &'static [&'static str]) -> Self {
        Self::new_with_interval(frames, Duration::from_millis(80))
    }

    /// Creates a spinner with custom frames and tick interval.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use std::time::Duration;
    /// let spinner = loaders::Spinner::new_with_interval(&loaders::spinner::frames::DOTS, Duration::from_millis(50));
    /// assert!(!spinner.is_finished());
    /// ```
    pub fn new_with_interval(frames: &'static [&'static str], interval: Duration) -> Self {
        let style = ProgressStyle::default_spinner().tick_strings(frames);
        let bar = ProgressBar::builder().style(style).build();
        Self { bar, interval }
    }

    /// Creates a spinner and sets its message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::with_message(&loaders::spinner::frames::DOTS, "loading");
    /// assert_eq!(spinner.bar().message(), "loading");
    /// ```
    pub fn with_message(frames: &'static [&'static str], message: impl Into<String>) -> Self {
        let spinner = Self::new(frames);
        spinner.set_message(message);
        spinner
    }

    /// Starts automatic ticking.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.start();
    /// spinner.stop();
    /// ```
    pub fn start(&self) {
        self.bar.enable_steady_tick(self.interval);
    }

    /// Starts automatic ticking with a message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.start_with_message("working");
    /// spinner.stop();
    /// ```
    pub fn start_with_message(&self, msg: impl Into<String>) {
        self.set_message(msg);
        self.start();
    }

    /// Stops and finishes the spinner.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.stop();
    /// assert!(spinner.is_finished());
    /// ```
    pub fn stop(&self) {
        self.bar.disable_steady_tick();
        self.bar.finish();
    }

    /// Stops the spinner with a final message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.stop_with_message("done");
    /// assert_eq!(spinner.bar().message(), "done");
    /// ```
    pub fn stop_with_message(&self, msg: impl Into<String>) {
        self.bar.disable_steady_tick();
        self.bar.finish_with_message(msg);
    }

    /// Stops the spinner and replaces the frame with a fixed symbol.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.stop_with_symbol("ok", "done");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn stop_with_symbol(&self, symbol: &str, msg: impl Into<String>) {
        self.bar.disable_steady_tick();
        let style = ProgressStyle::default_spinner().tick_strings(&[symbol]);
        self.bar.set_style(style);
        self.bar.finish_with_message(msg);
    }

    /// Stops the spinner with a success marker.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.success("done");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn success(&self, msg: impl Into<String>) {
        self.stop_with_symbol("ok", msg);
    }

    /// Stops the spinner with a failure marker.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.failure("failed");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn failure(&self, msg: impl Into<String>) {
        self.stop_with_symbol("x", msg);
    }

    /// Stops the spinner with a warning marker.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.warning("skipped");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn warning(&self, msg: impl Into<String>) {
        self.stop_with_symbol("!", msg);
    }

    /// Stops the spinner with an informational marker.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.info("noted");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn info(&self, msg: impl Into<String>) {
        self.stop_with_symbol("i", msg);
    }

    /// Stops the spinner and leaves the final symbol visible.
    ///
    /// This is an alias for `stop_with_symbol`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.stop_and_persist("ok", "done");
    /// assert!(spinner.is_finished());
    /// ```
    pub fn stop_and_persist(&self, symbol: &str, msg: impl Into<String>) {
        self.stop_with_symbol(symbol, msg);
    }

    /// Sets the spinner message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.set_message("working");
    /// assert_eq!(spinner.bar().message(), "working");
    /// ```
    pub fn set_message(&self, msg: impl Into<String>) {
        self.bar.set_message(msg);
    }

    /// Sets the spinner prefix.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.set_prefix("job");
    /// assert_eq!(spinner.bar().prefix(), "job");
    /// ```
    pub fn set_prefix(&self, prefix: impl Into<String>) {
        self.bar.set_prefix(prefix);
    }

    /// Replaces the underlying spinner style.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.set_style(loaders::ProgressStyle::default_spinner());
    /// ```
    pub fn set_style(&self, style: ProgressStyle) {
        self.bar.set_style(style);
    }

    /// Returns the configured automatic tick interval.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// assert!(spinner.interval().as_millis() > 0);
    /// ```
    pub fn interval(&self) -> Duration {
        self.interval
    }

    /// Advances the spinner by one frame.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// spinner.tick();
    /// ```
    pub fn tick(&self) {
        self.bar.tick();
    }

    /// Returns whether the spinner is finished.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// assert!(!spinner.is_finished());
    /// ```
    pub fn is_finished(&self) -> bool {
        self.bar.is_finished()
    }

    /// Returns the underlying progress bar.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let spinner = loaders::Spinner::new(&loaders::spinner::frames::LINE);
    /// assert_eq!(spinner.bar().length(), None);
    /// ```
    pub fn bar(&self) -> &ProgressBar {
        &self.bar
    }
}

impl fmt::Debug for Spinner {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Spinner")
            .field("bar", &self.bar)
            .field("interval", &self.interval)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::terminal::writer::DrawTarget;

    fn hidden_spinner() -> Spinner {
        let bar = ProgressBar::builder()
            .target(DrawTarget::hidden())
            .style(ProgressStyle::default_spinner())
            .build();
        Spinner {
            bar,
            interval: Duration::from_millis(1),
        }
    }

    #[test]
    fn test_spinner_starts_and_stops() {
        let spinner = hidden_spinner();
        spinner.start();
        spinner.stop();
        assert!(spinner.is_finished());
    }

    #[test]
    fn test_spinner_message_update() {
        let spinner = hidden_spinner();
        spinner.set_message("hello");
        assert_eq!(spinner.bar().message(), "hello");
    }

    #[test]
    fn test_spinner_stop_with_symbol() {
        let spinner = hidden_spinner();
        spinner.stop_with_symbol("ok", "done");
        assert_eq!(spinner.bar().message(), "done");
    }

    #[test]
    fn test_spinner_status_helpers_finish() {
        let success = hidden_spinner();
        success.success("done");
        assert!(success.is_finished());

        let failure = hidden_spinner();
        failure.failure("failed");
        assert!(failure.is_finished());

        let warning = hidden_spinner();
        warning.warning("skipped");
        assert!(warning.is_finished());

        let info = hidden_spinner();
        info.info("noted");
        assert!(info.is_finished());
    }

    #[test]
    fn test_spinner_interval_and_set_style() {
        let spinner = hidden_spinner();
        spinner.set_style(ProgressStyle::default_spinner().tick_strings(&["a", "b"]));
        assert_eq!(spinner.interval(), Duration::from_millis(1));
    }

    #[test]
    fn test_spinner_is_finished_after_stop() {
        let spinner = hidden_spinner();
        spinner.stop();
        assert!(spinner.is_finished());
    }
}