Skip to main content

ralph_workflow/logger/
progress.rs

1//! Progress bar display utilities.
2//!
3//! Provides visual progress feedback for long-running operations.
4
5use super::Colors;
6use crate::logger::stdout_writer::stdout_write_line;
7
8/// Print a progress bar with percentage and counts.
9///
10/// Displays a visual progress bar like: `[████████░░░░░░░░] 50% (5/10)`
11///
12/// # Arguments
13///
14/// * `current` - Current progress value
15/// * `total` - Total value for 100% completion
16/// * `label` - Label to display before the progress bar
17pub fn print_progress(current: u32, total: u32, label: &str) {
18    let c = Colors::new();
19
20    if total == 0 {
21        let line = format!(
22            "{}{}:{} {}[no progress data]{}",
23            c.dim(),
24            label,
25            c.reset(),
26            c.yellow(),
27            c.reset()
28        );
29        let _ = stdout_write_line(&line);
30        return;
31    }
32
33    let bar_width: usize = 20;
34    // Safe: result is bounded to 0..=100 by .min(100)
35    let pct = u32::try_from(
36        (u64::from(current))
37            .saturating_mul(100)
38            .saturating_div(u64::from(total))
39            .min(100),
40    )
41    .unwrap_or(0);
42    // Safe: result is bounded to 0..=bar_width by .min(bar_width)
43    let filled = usize::try_from(
44        (u64::from(current))
45            .saturating_mul(bar_width as u64)
46            .saturating_div(u64::from(total))
47            .min(bar_width as u64),
48    )
49    .unwrap_or(0);
50    let empty = bar_width - filled;
51
52    let bar: String = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
53
54    let line = format!(
55        "{}{}:{} {}[{}]{} {}{}%{} ({}/{})",
56        c.dim(),
57        label,
58        c.reset(),
59        c.cyan(),
60        bar,
61        c.reset(),
62        c.bold(),
63        pct,
64        c.reset(),
65        current,
66        total
67    );
68    let _ = stdout_write_line(&line);
69}
70
71#[cfg(test)]
72mod tests {
73    /// Helper function for testing progress bar generation logic
74    fn generate_progress_bar(current: u32, total: u32) -> (u32, String) {
75        if total == 0 {
76            return (0, String::new());
77        }
78        let bar_width: usize = 20;
79        let pct = u32::try_from(
80            (u64::from(current))
81                .saturating_mul(100)
82                .saturating_div(u64::from(total))
83                .min(100),
84        )
85        .unwrap_or(0);
86        let filled = usize::try_from(
87            (u64::from(current))
88                .saturating_mul(bar_width as u64)
89                .saturating_div(u64::from(total))
90                .min(bar_width as u64),
91        )
92        .unwrap_or(0);
93        let empty = bar_width - filled;
94        let bar: String = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
95        (pct, bar)
96    }
97
98    #[test]
99    fn test_progress_bar_50_percent() {
100        let (pct, bar) = generate_progress_bar(5, 10);
101        assert_eq!(pct, 50);
102        assert_eq!(bar, "██████████░░░░░░░░░░");
103    }
104
105    #[test]
106    fn test_progress_bar_100_percent() {
107        let (pct, bar) = generate_progress_bar(10, 10);
108        assert_eq!(pct, 100);
109        assert_eq!(bar, "████████████████████");
110    }
111
112    #[test]
113    fn test_progress_bar_0_percent() {
114        let (pct, bar) = generate_progress_bar(0, 10);
115        assert_eq!(pct, 0);
116        assert_eq!(bar, "░░░░░░░░░░░░░░░░░░░░");
117    }
118
119    #[test]
120    fn test_progress_bar_zero_total() {
121        let (pct, bar) = generate_progress_bar(0, 0);
122        assert_eq!(pct, 0);
123        assert_eq!(bar, "");
124    }
125}