Skip to main content

api_scanner/
progress_tracker.rs

1//! Universal Progress Tracker
2//!
3//! A reusable progress tracking utility that automatically detects terminal capabilities
4//! and provides appropriate output formatting for both TTY and non-TTY environments.
5//!
6//! ## Features
7//! - Automatic TTY detection
8//! - Thread-safe progress updates
9//! - Configurable update frequency
10//! - Clean output for both interactive and CI/CD environments
11//!
12//! ## Usage
13//!
14//! ```rust
15//! use api_scanner::progress_tracker::ProgressTracker;
16//!
17//! #[tokio::main]
18//! async fn main() {
19//!     let tracker = ProgressTracker::new(100); // 100 total items
20//!
21//!     for _ in 0..100 {
22//!         // Do work...
23//!         tracker.increment(Some("Processing item")).await;
24//!     }
25//!
26//!     tracker.finish().await;
27//! }
28//! ```
29
30use std::io::{IsTerminal, Write};
31use std::sync::Arc;
32use std::time::Instant;
33use tokio::sync::Mutex;
34
35/// Configuration for progress tracking behavior
36#[derive(Clone)]
37pub struct ProgressConfig {
38    /// Total number of items to process
39    pub total: usize,
40    /// Update frequency (every N items) for TTY mode
41    pub tty_update_frequency: usize,
42    /// Update frequency (every N items) for non-TTY mode
43    pub non_tty_update_frequency: usize,
44    /// Whether to show elapsed time
45    pub show_elapsed: bool,
46    /// Whether to show ETA
47    pub show_eta: bool,
48    /// Whether to show rate
49    pub show_rate: bool,
50    /// Custom prefix for progress messages
51    pub prefix: String,
52    /// Whether to show detailed messages (URLs, findings, etc.)
53    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
71/// Thread-safe progress tracker
72pub struct ProgressTracker {
73    config: ProgressConfig,
74    progress: Arc<Mutex<usize>>,
75    start_time: Instant,
76    is_tty: bool,
77}
78
79impl ProgressTracker {
80    /// Create a new progress tracker with default configuration
81    pub fn new(total: usize) -> Self {
82        Self::with_config(ProgressConfig {
83            total,
84            ..Default::default()
85        })
86    }
87
88    /// Create a new progress tracker with custom configuration
89    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    /// Get a cloneable handle for use in async tasks
99    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    /// Increment progress by 1 and optionally display a message
109    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    /// Set progress to a specific value
119    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    /// Get current progress value
128    pub async fn current(&self) -> usize {
129        *self.progress.lock().await
130    }
131
132    /// Display progress based on TTY detection
133    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        // Only display at specified frequency or when complete
141        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            // TTY mode: use carriage return for same-line updates
154            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("   "); // Clear any leftover characters
183
184            eprint!("{}", output);
185            std::io::stderr().flush().ok();
186        } else {
187            // Non-TTY mode: new line for each update
188            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    /// Finish progress tracking and clear the line (TTY mode)
209    pub async fn finish(&self) {
210        if self.is_tty {
211            eprintln!(); // Move to next line
212        }
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/// Cloneable handle for use in async tasks
223#[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    /// Increment progress by 1
233    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    /// Get current progress value
243    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}