Skip to main content

ebook/
progress.rs

1//! Progress reporting utilities for long-running operations
2
3use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
4
5/// A simple progress reporter for tracking operation progress
6#[derive(Clone)]
7pub struct Progress {
8    current: Arc<AtomicUsize>,
9    total: usize,
10    name: String,
11}
12
13impl Progress {
14    /// Create a new progress indicator
15    pub fn new(name: String, total: usize) -> Self {
16        Self {
17            current: Arc::new(AtomicUsize::new(0)),
18            total,
19            name,
20        }
21    }
22
23    /// Increment the progress counter
24    pub fn increment(&self, amount: usize) {
25        self.current.fetch_add(amount, Ordering::Relaxed);
26    }
27
28    /// Set the current progress value
29    pub fn set(&self, value: usize) {
30        self.current.store(value, Ordering::Relaxed);
31    }
32
33    /// Get the current progress value
34    pub fn current(&self) -> usize {
35        self.current.load(Ordering::Relaxed)
36    }
37
38    /// Get the total value
39    pub fn total(&self) -> usize {
40        self.total
41    }
42
43    /// Get the progress as a percentage (0-100)
44    pub fn percentage(&self) -> f64 {
45        if self.total == 0 {
46            return 100.0;
47        }
48        let current = self.current();
49        (current as f64 / self.total as f64 * 100.0).min(100.0)
50    }
51
52    /// Get the operation name
53    pub fn name(&self) -> &str {
54        &self.name
55    }
56
57    /// Print the current progress to stderr
58    pub fn print(&self) {
59        eprint!("\r{}: {:.0}% ({}/{})",
60            self.name,
61            self.percentage(),
62            self.current(),
63            self.total
64        );
65    }
66
67    /// Print the current progress with a message
68    pub fn print_with_message(&self, message: &str) {
69        eprint!("\r{}: {:.0}% - {}",
70            self.name,
71            self.percentage(),
72            message
73        );
74    }
75
76    /// Finish the progress display
77    pub fn finish(&self) {
78        eprintln!("\r{}: Complete! (100%)", self.name);
79    }
80
81    /// Finish the progress display with a message
82    pub fn finish_with_message(&self, message: &str) {
83        eprintln!("\r{}: {}", self.name, message);
84    }
85}
86
87/// A callback type for progress updates
88pub type ProgressCallback = Box<dyn Fn(usize, usize) + Send + Sync>;
89
90/// A progress handler that can be passed to operations
91pub struct ProgressHandler {
92    callback: Option<ProgressCallback>,
93}
94
95impl ProgressHandler {
96    /// Create a new progress handler without callback
97    pub fn new() -> Self {
98        Self { callback: None }
99    }
100
101    /// Create a new progress handler with a callback
102    pub fn with_callback(callback: ProgressCallback) -> Self {
103        Self { callback: Some(callback) }
104    }
105
106    /// Report progress
107    pub fn report(&self, current: usize, total: usize) {
108        if let Some(ref callback) = self.callback {
109            callback(current, total);
110        }
111    }
112
113    /// Check if this handler has a callback
114    pub fn has_callback(&self) -> bool {
115        self.callback.is_some()
116    }
117}
118
119impl Default for ProgressHandler {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// Create a simple console progress callback
126pub fn console_progress_callback(name: String) -> ProgressCallback {
127    Box::new(move |current: usize, total: usize| {
128        let percentage = if total > 0 {
129            (current as f64 / total as f64 * 100.0).min(100.0)
130        } else {
131            100.0
132        };
133        eprint!("\r{name}: {percentage:.0}% ({current}/{total})");
134    })
135}
136
137/// Create a silent progress callback (does nothing)
138pub fn silent_progress_callback() -> ProgressCallback {
139    Box::new(|_current: usize, _total: usize| {})
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_progress_creation() {
148        let progress = Progress::new("Test".to_string(), 100);
149        assert_eq!(progress.current(), 0);
150        assert_eq!(progress.total(), 100);
151        assert_eq!(progress.name(), "Test");
152    }
153
154    #[test]
155    fn test_progress_increment() {
156        let progress = Progress::new("Test".to_string(), 100);
157        progress.increment(10);
158        assert_eq!(progress.current(), 10);
159        progress.increment(20);
160        assert_eq!(progress.current(), 30);
161    }
162
163    #[test]
164    fn test_progress_set() {
165        let progress = Progress::new("Test".to_string(), 100);
166        progress.set(50);
167        assert_eq!(progress.current(), 50);
168    }
169
170    #[test]
171    fn test_progress_percentage() {
172        let progress = Progress::new("Test".to_string(), 100);
173        assert_eq!(progress.percentage(), 0.0);
174        progress.set(50);
175        assert_eq!(progress.percentage(), 50.0);
176        progress.set(100);
177        assert_eq!(progress.percentage(), 100.0);
178        progress.set(150); // Cap at 100%
179        assert_eq!(progress.percentage(), 100.0);
180    }
181
182    #[test]
183    fn test_progress_zero_total() {
184        let progress = Progress::new("Test".to_string(), 0);
185        assert_eq!(progress.percentage(), 100.0);
186    }
187
188    #[test]
189    fn test_progress_handler() {
190        let handler = ProgressHandler::new();
191        assert!(!handler.has_callback());
192        handler.report(10, 100); // Should not panic
193
194        let callback = silent_progress_callback();
195        let handler_with_cb = ProgressHandler::with_callback(callback);
196        assert!(handler_with_cb.has_callback());
197        handler_with_cb.report(50, 100); // Should not panic
198    }
199}