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