vault_audit_tools/utils/
progress.rs

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