api_scanner/
progress_tracker.rs1use std::io::{IsTerminal, Write};
31use std::sync::Arc;
32use std::time::Instant;
33use tokio::sync::Mutex;
34
35#[derive(Clone)]
37pub struct ProgressConfig {
38 pub total: usize,
40 pub tty_update_frequency: usize,
42 pub non_tty_update_frequency: usize,
44 pub show_elapsed: bool,
46 pub show_eta: bool,
48 pub show_rate: bool,
50 pub prefix: String,
52 pub show_details: bool,
54}
55
56impl Default for ProgressConfig {
57 fn default() -> Self {
58 Self {
59 total: 0,
60 tty_update_frequency: 5,
61 non_tty_update_frequency: 1,
62 show_elapsed: true,
63 show_eta: true,
64 show_rate: true,
65 prefix: String::new(),
66 show_details: true,
67 }
68 }
69}
70
71pub struct ProgressTracker {
73 config: ProgressConfig,
74 progress: Arc<Mutex<usize>>,
75 start_time: Instant,
76 is_tty: bool,
77}
78
79impl ProgressTracker {
80 pub fn new(total: usize) -> Self {
82 Self::with_config(ProgressConfig {
83 total,
84 ..Default::default()
85 })
86 }
87
88 pub fn with_config(config: ProgressConfig) -> Self {
90 Self {
91 config,
92 progress: Arc::new(Mutex::new(0)),
93 start_time: Instant::now(),
94 is_tty: std::io::stderr().is_terminal(),
95 }
96 }
97
98 pub fn handle(&self) -> ProgressHandle {
100 ProgressHandle {
101 config: self.config.clone(),
102 progress: Arc::clone(&self.progress),
103 start_time: self.start_time,
104 is_tty: self.is_tty,
105 }
106 }
107
108 pub async fn increment(&self, message: Option<&str>) {
110 let mut p = self.progress.lock().await;
111 *p += 1;
112 let current = *p;
113 drop(p);
114
115 self.display_progress(current, message).await;
116 }
117
118 pub async fn set(&self, value: usize, message: Option<&str>) {
120 let mut p = self.progress.lock().await;
121 *p = value;
122 drop(p);
123
124 self.display_progress(value, message).await;
125 }
126
127 pub async fn current(&self) -> usize {
129 *self.progress.lock().await
130 }
131
132 async fn display_progress(&self, current: usize, message: Option<&str>) {
134 let update_freq = if self.is_tty {
135 self.config.tty_update_frequency
136 } else {
137 self.config.non_tty_update_frequency
138 };
139
140 if !current.is_multiple_of(update_freq) && current != self.config.total {
142 return;
143 }
144
145 let elapsed = self.start_time.elapsed().as_secs();
146 let percentage = if self.config.total > 0 {
147 (current as f64 / self.config.total as f64) * 100.0
148 } else {
149 0.0
150 };
151
152 if self.is_tty {
153 let mut output = format!(
155 "\r{}{}/{} ({:.1}%)",
156 self.config.prefix, current, self.config.total, percentage
157 );
158
159 if self.config.show_rate && elapsed > 0 {
160 let rate = current as f64 / elapsed as f64;
161 output.push_str(&format!(" | {:.1}/s", rate));
162 }
163
164 if self.config.show_eta && elapsed > 0 && current > 0 {
165 let rate = current as f64 / elapsed as f64;
166 let remaining = self.config.total.saturating_sub(current);
167 let eta_secs = (remaining as f64 / rate) as u64;
168 let eta_mins = eta_secs / 60;
169 output.push_str(&format!(" | ETA: {}m{}s", eta_mins, eta_secs % 60));
170 }
171
172 if self.config.show_elapsed {
173 output.push_str(&format!(" | Elapsed: {}s", elapsed));
174 }
175
176 if self.config.show_details {
177 if let Some(msg) = message {
178 output.push_str(&format!(" | {}", msg));
179 }
180 }
181
182 output.push_str(" "); eprint!("{}", output);
185 std::io::stderr().flush().ok();
186 } else {
187 let mut output = format!("[{}/{}] ({:.1}%)", current, self.config.total, percentage);
189
190 if !self.config.prefix.is_empty() {
191 output = format!("{} {}", self.config.prefix, output);
192 }
193
194 if self.config.show_elapsed {
195 output.push_str(&format!(" | Elapsed: {}s", elapsed));
196 }
197
198 if self.config.show_details {
199 if let Some(msg) = message {
200 output.push_str(&format!(" | {}", msg));
201 }
202 }
203
204 eprintln!("{}", output);
205 }
206 }
207
208 pub async fn finish(&self) {
210 if self.is_tty {
211 eprintln!(); }
213 let elapsed = self.start_time.elapsed();
214 eprintln!(
215 "✅ Completed {} items in {:.2}s",
216 self.config.total,
217 elapsed.as_secs_f64()
218 );
219 }
220}
221
222#[derive(Clone)]
224pub struct ProgressHandle {
225 config: ProgressConfig,
226 progress: Arc<Mutex<usize>>,
227 start_time: Instant,
228 is_tty: bool,
229}
230
231impl ProgressHandle {
232 pub async fn increment(&self, message: Option<&str>) {
234 let mut p = self.progress.lock().await;
235 *p += 1;
236 let current = *p;
237 drop(p);
238
239 self.display_progress(current, message).await;
240 }
241
242 pub async fn current(&self) -> usize {
244 *self.progress.lock().await
245 }
246
247 async fn display_progress(&self, current: usize, message: Option<&str>) {
248 let update_freq = if self.is_tty {
249 self.config.tty_update_frequency
250 } else {
251 self.config.non_tty_update_frequency
252 };
253
254 if !current.is_multiple_of(update_freq) && current != self.config.total {
255 return;
256 }
257
258 let elapsed = self.start_time.elapsed().as_secs();
259 let percentage = if self.config.total > 0 {
260 (current as f64 / self.config.total as f64) * 100.0
261 } else {
262 0.0
263 };
264
265 if self.is_tty {
266 let mut output = format!(
267 "\r{}{}/{} ({:.1}%)",
268 self.config.prefix, current, self.config.total, percentage
269 );
270
271 if self.config.show_rate && elapsed > 0 {
272 let rate = current as f64 / elapsed as f64;
273 output.push_str(&format!(" | {:.1}/s", rate));
274 }
275
276 if self.config.show_eta && elapsed > 0 && current > 0 {
277 let rate = current as f64 / elapsed as f64;
278 let remaining = self.config.total.saturating_sub(current);
279 let eta_secs = (remaining as f64 / rate) as u64;
280 let eta_mins = eta_secs / 60;
281 output.push_str(&format!(" | ETA: {}m{}s", eta_mins, eta_secs % 60));
282 }
283
284 if self.config.show_elapsed {
285 output.push_str(&format!(" | Elapsed: {}s", elapsed));
286 }
287
288 if self.config.show_details {
289 if let Some(msg) = message {
290 output.push_str(&format!(" | {}", msg));
291 }
292 }
293
294 output.push_str(" ");
295
296 eprint!("{}", output);
297 std::io::stderr().flush().ok();
298 } else {
299 let mut output = format!("[{}/{}] ({:.1}%)", current, self.config.total, percentage);
300
301 if !self.config.prefix.is_empty() {
302 output = format!("{} {}", self.config.prefix, output);
303 }
304
305 if self.config.show_elapsed {
306 output.push_str(&format!(" | Elapsed: {}s", elapsed));
307 }
308
309 if self.config.show_details {
310 if let Some(msg) = message {
311 output.push_str(&format!(" | {}", msg));
312 }
313 }
314
315 eprintln!("{}", output);
316 }
317 }
318}