vault_audit_tools/utils/
progress.rs

1use std::io::{self, Write};
2use std::time::{Duration, Instant};
3
4/// Progress bar for displaying processing status
5pub struct ProgressBar {
6    total: Option<usize>,
7    current: usize,
8    last_update: Instant,
9    update_interval: Duration,
10    label: String,
11    started: Instant,
12    render_count: usize,
13}
14
15impl ProgressBar {
16    /// Create a new progress bar with known total
17    pub fn new(total: usize, label: &str) -> Self {
18        let mut pb = Self {
19            total: Some(total),
20            current: 0,
21            last_update: Instant::now(),
22            update_interval: Duration::from_millis(200), // Update every 200ms
23            label: label.to_string(),
24            started: Instant::now(),
25            render_count: 0,
26        };
27        pb.render(); // Show initial state
28        pb
29    }
30
31    /// Create a new progress bar with unknown total (spinner mode)
32    pub fn new_spinner(label: &str) -> Self {
33        let mut pb = Self {
34            total: None,
35            current: 0,
36            last_update: Instant::now(),
37            update_interval: Duration::from_millis(200),
38            label: label.to_string(),
39            started: Instant::now(),
40            render_count: 0,
41        };
42        pb.render(); // Show initial state
43        pb
44    }
45
46    /// Update progress (only renders if enough time has passed)
47    pub fn update(&mut self, current: usize) {
48        self.current = current;
49
50        if self.last_update.elapsed() >= self.update_interval {
51            self.render();
52            self.last_update = Instant::now();
53        }
54    }
55
56    /// Increment progress by 1
57    #[allow(dead_code)]
58    pub fn inc(&mut self) {
59        self.update(self.current + 1);
60    }
61
62    /// Force render regardless of update interval
63    pub fn render(&mut self) {
64        self.render_count += 1;
65
66        if let Some(total) = self.total {
67            let percentage = if total > 0 {
68                (self.current as f64 / total as f64 * 100.0).min(100.0)
69            } else {
70                0.0
71            };
72
73            let bar_width = 40;
74            let filled = (bar_width as f64 * percentage / 100.0) as usize;
75            let empty = bar_width - filled;
76
77            let bar = format!("[{}{}]", "=".repeat(filled), " ".repeat(empty));
78
79            eprint!(
80                "\r{} {} {:>6.1}% ({}/{})",
81                self.label,
82                bar,
83                percentage,
84                format_number(self.current),
85                format_number(total)
86            );
87        } else {
88            // Spinner mode for unknown total
89            let spinner = ['|', '/', '-', '\\'];
90            let idx = self.render_count % spinner.len();
91
92            eprint!(
93                "\r{} {} {}",
94                self.label,
95                spinner[idx],
96                format_number(self.current)
97            );
98        }
99
100        let _ = io::stderr().flush();
101    }
102
103    /// Finish the progress bar and print final message
104    pub fn finish(&mut self) {
105        self.render();
106        let elapsed = self.started.elapsed();
107        let rate = if elapsed.as_secs() > 0 {
108            format!(
109                "{}/s",
110                format_number(self.current / elapsed.as_secs() as usize)
111            )
112        } else {
113            "".to_string()
114        };
115
116        eprintln!(
117            " Done in {:.1}s {}",
118            elapsed.as_secs_f64(),
119            if rate.is_empty() {
120                "".to_string()
121            } else {
122                format!("({})", rate)
123            }
124        );
125    }
126
127    /// Finish with custom message
128    pub fn finish_with_message(&mut self, message: &str) {
129        self.render();
130        eprintln!(" {}", message);
131    }
132}
133
134/// Format a number with thousand separators
135fn format_number(n: usize) -> String {
136    let s = n.to_string();
137    let mut result = String::new();
138    for (i, c) in s.chars().rev().enumerate() {
139        if i > 0 && i % 3 == 0 {
140            result.push(',');
141        }
142        result.push(c);
143    }
144    result.chars().rev().collect()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_format_number() {
153        assert_eq!(format_number(0), "0");
154        assert_eq!(format_number(999), "999");
155        assert_eq!(format_number(1000), "1,000");
156        assert_eq!(format_number(1234567), "1,234,567");
157    }
158}