loaders 0.0.0

A fully-featured, customisable progress bar and loading indicator library for Rust CLI and terminal applications
Documentation
//! Internal state tracked by progress bars and spinners.

use crate::style::style::ProgressStyle;
use std::time::{Duration, Instant};

/// Mutable progress state used while rendering bars and custom template keys.
///
/// The fields are public so custom template callbacks can inspect state without
/// allocation or locking. Mutating these fields directly is only supported
/// inside the crate.
#[derive(Clone, Debug)]
pub struct BarState {
    /// Current position.
    pub pos: u64,
    /// Optional total length.
    pub len: Option<u64>,
    /// Message rendered by `{msg}`.
    pub message: String,
    /// Prefix rendered by `{prefix}`.
    pub prefix: String,
    /// Postfix rendered by `{postfix}`.
    pub postfix: String,
    /// Start time used for elapsed, ETA, and rate calculations.
    pub started_at: Instant,
    /// Position at the last draw.
    pub last_draw_pos: u64,
    /// Time of the last draw.
    pub last_draw_time: Instant,
    /// Minimum position delta before redrawing.
    pub draw_delta: u64,
    /// Maximum redraws per second; `0` means unlimited.
    pub draw_rate: u64,
    /// Whether the bar completed successfully.
    pub finished: bool,
    /// Whether the bar was abandoned.
    pub abandoned: bool,
    /// Whether the bar should suppress output.
    pub hidden: bool,
    /// Style used to render this state.
    pub style: ProgressStyle,
    /// Optional interval for automatic spinner ticks.
    pub steady_tick_interval: Option<Duration>,
    /// Current spinner frame index.
    pub spinner_frame_index: usize,
}

impl BarState {
    /// Creates a new state value.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let state = loaders::bar::state::BarState::new(
    ///     Some(10),
    ///     loaders::ProgressStyle::default_bar(),
    /// );
    /// assert_eq!(state.pos, 0);
    /// ```
    pub fn new(len: Option<u64>, style: ProgressStyle) -> BarState {
        let now = Instant::now();
        BarState {
            pos: 0,
            len,
            message: String::new(),
            prefix: String::new(),
            postfix: String::new(),
            started_at: now,
            last_draw_pos: 0,
            last_draw_time: now,
            draw_delta: 1,
            draw_rate: 15,
            finished: false,
            abandoned: false,
            hidden: false,
            style,
            steady_tick_interval: None,
            spinner_frame_index: 0,
        }
    }

    /// Returns true when position or time throttling allows another redraw.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let mut state = loaders::bar::state::BarState::new(None, loaders::ProgressStyle::default_spinner());
    /// state.draw_rate = 0;
    /// assert!(state.should_redraw());
    /// ```
    pub fn should_redraw(&self) -> bool {
        if self.hidden {
            return false;
        }

        let moved = self.pos.saturating_sub(self.last_draw_pos);
        if self.draw_delta == 0 || moved >= self.draw_delta {
            return true;
        }

        if self.draw_rate == 0 {
            return true;
        }

        let min_interval = Duration::from_secs_f64(1.0 / self.draw_rate as f64);
        self.last_draw_time.elapsed() >= min_interval
    }

    /// Marks the current state as drawn.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let mut state = loaders::bar::state::BarState::new(None, loaders::ProgressStyle::default_spinner());
    /// state.pos = 3;
    /// state.update_draw_time();
    /// assert_eq!(state.last_draw_pos, 3);
    /// ```
    pub fn update_draw_time(&mut self) {
        self.last_draw_pos = self.pos;
        self.last_draw_time = Instant::now();
    }

    /// Returns the completed fraction clamped to `0.0..=1.0`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let mut state = loaders::bar::state::BarState::new(Some(10), loaders::ProgressStyle::default_bar());
    /// state.pos = 5;
    /// assert_eq!(state.fraction(), 0.5);
    /// ```
    pub fn fraction(&self) -> f64 {
        match self.len {
            Some(0) | None => 0.0,
            Some(len) => (self.pos as f64 / len as f64).clamp(0.0, 1.0),
        }
    }

    /// Estimates remaining time from average throughput.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let state = loaders::bar::state::BarState::new(Some(10), loaders::ProgressStyle::default_bar());
    /// assert_eq!(state.eta(), std::time::Duration::ZERO);
    /// ```
    pub fn eta(&self) -> Duration {
        let Some(len) = self.len else {
            return Duration::ZERO;
        };
        if self.pos == 0 || self.pos >= len {
            return Duration::ZERO;
        }
        let rate = self.per_sec();
        if rate <= 0.0 {
            Duration::ZERO
        } else {
            Duration::from_secs_f64((len - self.pos) as f64 / rate)
        }
    }

    /// Returns average units per second since the bar started.
    ///
    /// # Examples
    ///
    /// ```rust
    /// let state = loaders::bar::state::BarState::new(None, loaders::ProgressStyle::default_spinner());
    /// assert_eq!(state.per_sec(), 0.0);
    /// ```
    pub fn per_sec(&self) -> f64 {
        let elapsed = self.started_at.elapsed().as_secs_f64();
        if elapsed <= 0.0 {
            0.0
        } else {
            self.pos as f64 / elapsed
        }
    }
}

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

    #[test]
    fn test_fraction_zero_len() {
        let state = BarState::new(Some(0), ProgressStyle::default_bar());
        assert_eq!(state.fraction(), 0.0);
    }

    #[test]
    fn test_fraction_halfway() {
        let mut state = BarState::new(Some(10), ProgressStyle::default_bar());
        state.pos = 5;
        assert_eq!(state.fraction(), 0.5);
    }

    #[test]
    fn test_fraction_over_clamps() {
        let mut state = BarState::new(Some(10), ProgressStyle::default_bar());
        state.pos = 20;
        assert_eq!(state.fraction(), 1.0);
    }

    #[test]
    fn test_should_redraw_by_delta() {
        let mut state = BarState::new(Some(10), ProgressStyle::default_bar());
        state.draw_delta = 3;
        state.pos = 3;
        assert!(state.should_redraw());
    }

    #[test]
    fn test_eta_estimate() {
        let mut state = BarState::new(Some(20), ProgressStyle::default_bar());
        state.pos = 10;
        state.started_at = Instant::now() - Duration::from_secs(10);
        let eta = state.eta();
        assert!(eta.as_secs() >= 9 && eta.as_secs() <= 11);
    }
}