Skip to main content

ggen_cli_lib/
progress.rs

1//! Progress reporting system for pack installation
2//!
3//! Provides real-time progress feedback with visual indicators,
4//! step tracking, and cancellation support.
5#![allow(clippy::unwrap_used)] // Mutex::lock() poisoning is an unrecoverable programmer error; panic is correct
6
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9use tokio::sync::broadcast;
10use tracing::{debug, info, warn};
11
12/// Progress reporting system for async operations
13#[derive(Clone)]
14pub struct ProgressReporter {
15    progress: Arc<Mutex<ProgressState>>,
16    events: broadcast::Sender<ProgressEvent>,
17}
18
19/// Current progress state
20#[derive(Debug, Clone)]
21pub struct ProgressState {
22    pub current_step: String,
23    pub step_progress: f64,
24    pub total_steps: usize,
25    pub completed_steps: usize,
26    pub current_operation: String,
27    pub start_time: Instant,
28    pub estimated_duration: Option<Duration>,
29    pub bytes_processed: u64,
30    pub total_bytes: u64,
31    pub items_processed: usize,
32    pub total_items: usize,
33    pub is_cancelled: bool,
34    pub error: Option<String>,
35}
36
37/// Progress events for real-time updates
38#[derive(Debug, Clone)]
39pub enum ProgressEvent {
40    StepStarted {
41        step: String,
42        step_number: usize,
43    },
44    StepProgress {
45        progress: f64,
46        message: String,
47    },
48    StepCompleted {
49        step: String,
50        duration_ms: u64,
51    },
52    OverallProgress {
53        percent: f64,
54        message: String,
55    },
56    DataProcessed {
57        bytes: u64,
58        total: u64,
59    },
60    ItemProcessed {
61        item: String,
62        current: usize,
63        total: usize,
64    },
65    Error {
66        message: String,
67        step: String,
68    },
69    Completed {
70        total_duration_ms: u64,
71    },
72    Cancelled,
73}
74
75/// Installation plan for user preview
76#[derive(Debug, Clone, serde::Serialize)]
77pub struct InstallationPlan {
78    pub pack_id: String,
79    pub total_size_mb: f64,
80    pub estimated_duration_seconds: u64,
81    pub total_dependencies: usize,
82    pub steps: Vec<PlanStep>,
83    pub cache_status: CacheStatus,
84}
85
86#[derive(Debug, Clone, serde::Serialize)]
87pub struct PlanStep {
88    pub step_number: usize,
89    pub name: String,
90    pub description: String,
91    pub estimated_duration_ms: u64,
92    pub size_mb: f64,
93}
94
95#[derive(Debug, Clone, serde::Serialize)]
96pub struct CacheStatus {
97    pub is_cached: bool,
98    pub cached_size_mb: Option<f64>,
99    pub cache_hit: bool,
100}
101
102impl ProgressReporter {
103    /// Create a new progress reporter
104    pub fn new() -> Self {
105        let (tx, _) = broadcast::channel(100);
106        Self {
107            progress: Arc::new(Mutex::new(ProgressState::new())),
108            events: tx,
109        }
110    }
111
112    /// Create a progress reporter for a specific operation
113    pub fn for_operation(operation_name: &str) -> Self {
114        let reporter = Self::new();
115        reporter.start_operation(operation_name);
116        reporter
117    }
118
119    /// Start a new operation
120    pub fn start_operation(&self, operation_name: &str) {
121        let mut state = self.progress.lock().unwrap();
122        state.current_operation = operation_name.to_string();
123        state.start_time = Instant::now();
124        state.current_step = "Initializing".to_string();
125        state.step_progress = 0.0;
126        state.is_cancelled = false;
127        state.error = None;
128
129        debug!("Starting operation: {}", operation_name);
130        self.broadcast_event(ProgressEvent::StepStarted {
131            step: "Initializing".to_string(),
132            step_number: 0,
133        });
134    }
135
136    /// Start a new step
137    pub fn start_step(&self, step_name: &str, step_number: usize) {
138        let mut state = self.progress.lock().unwrap();
139        state.current_step = step_name.to_string();
140        state.step_progress = 0.0;
141
142        info!("Starting step {}: {}", step_number, step_name);
143        self.broadcast_event(ProgressEvent::StepStarted {
144            step: step_name.to_string(),
145            step_number,
146        });
147    }
148
149    /// Update step progress
150    pub fn update_step_progress(&self, progress: f64, message: &str) {
151        let mut state = self.progress.lock().unwrap();
152        state.step_progress = progress.clamp(0.0, 100.0);
153
154        let overall_progress = if state.total_steps > 0 {
155            (state.completed_steps as f64 + progress / 100.0) / state.total_steps as f64 * 100.0
156        } else {
157            progress
158        };
159
160        self.broadcast_event(ProgressEvent::StepProgress {
161            progress,
162            message: message.to_string(),
163        });
164
165        self.broadcast_event(ProgressEvent::OverallProgress {
166            percent: overall_progress.clamp(0.0, 100.0),
167            message: format!("{}: {}%", state.current_step, overall_progress as u32),
168        });
169    }
170
171    /// Update data processing progress
172    pub fn update_data_progress(&self, bytes_processed: u64, total_bytes: u64) {
173        let mut state = self.progress.lock().unwrap();
174        state.bytes_processed = bytes_processed;
175        state.total_bytes = total_bytes;
176
177        let _progress = if total_bytes > 0 {
178            (bytes_processed as f64 / total_bytes as f64) * 100.0
179        } else {
180            0.0
181        };
182
183        self.broadcast_event(ProgressEvent::DataProcessed {
184            bytes: bytes_processed,
185            total: total_bytes,
186        });
187    }
188
189    /// Update item processing progress
190    pub fn update_item_progress(&self, item: &str, current: usize, total: usize) {
191        let mut state = self.progress.lock().unwrap();
192        state.items_processed = current;
193        state.total_items = total;
194
195        self.broadcast_event(ProgressEvent::ItemProcessed {
196            item: item.to_string(),
197            current,
198            total,
199        });
200    }
201
202    /// Complete current step
203    pub fn complete_step(&self, step_name: &str) {
204        let mut state = self.progress.lock().unwrap();
205        state.completed_steps += 1;
206        state.step_progress = 100.0;
207
208        let duration = state.start_time.elapsed();
209        info!("Completed step {}: {}ms", step_name, duration.as_millis());
210
211        self.broadcast_event(ProgressEvent::StepCompleted {
212            step: step_name.to_string(),
213            duration_ms: duration.as_millis() as u64,
214        });
215    }
216
217    /// Report error
218    pub fn report_error(&self, message: &str, step: &str) {
219        let mut state = self.progress.lock().unwrap();
220        state.error = Some(message.to_string());
221        state.is_cancelled = true;
222
223        warn!("Error in step {}: {}", step, message);
224        self.broadcast_event(ProgressEvent::Error {
225            message: message.to_string(),
226            step: step.to_string(),
227        });
228    }
229
230    /// Mark operation as completed
231    pub fn complete(&self) {
232        let state = self.progress.lock().unwrap();
233        let total_duration = state.start_time.elapsed();
234
235        info!("Operation completed in {}ms", total_duration.as_millis());
236        self.broadcast_event(ProgressEvent::Completed {
237            total_duration_ms: total_duration.as_millis() as u64,
238        });
239    }
240
241    /// Cancel operation
242    pub fn cancel(&self) {
243        let mut state = self.progress.lock().unwrap();
244        state.is_cancelled = true;
245
246        warn!("Operation cancelled");
247        self.broadcast_event(ProgressEvent::Cancelled);
248    }
249
250    /// Check if operation is cancelled
251    pub fn is_cancelled(&self) -> bool {
252        let state = self.progress.lock().unwrap();
253        state.is_cancelled
254    }
255
256    /// Get current progress state
257    pub fn get_state(&self) -> ProgressState {
258        self.progress.lock().unwrap().clone()
259    }
260
261    /// Set total number of steps
262    pub fn set_total_steps(&self, total: usize) {
263        let mut state = self.progress.lock().unwrap();
264        state.total_steps = total;
265        info!("Total steps for operation: {}", total);
266    }
267
268    /// Set estimated duration
269    pub fn set_estimated_duration(&self, duration: Duration) {
270        let mut state = self.progress.lock().unwrap();
271        state.estimated_duration = Some(duration);
272    }
273
274    /// Subscribe to progress events
275    pub fn subscribe(&self) -> broadcast::Receiver<ProgressEvent> {
276        self.events.subscribe()
277    }
278
279    /// Broadcast progress event
280    fn broadcast_event(&self, event: ProgressEvent) {
281        let _ = self.events.send(event);
282    }
283}
284
285impl ProgressState {
286    /// Create a new progress state
287    pub fn new() -> Self {
288        Self {
289            current_step: "Not started".to_string(),
290            step_progress: 0.0,
291            total_steps: 0,
292            completed_steps: 0,
293            current_operation: "Unknown".to_string(),
294            start_time: Instant::now(),
295            estimated_duration: None,
296            bytes_processed: 0,
297            total_bytes: 0,
298            items_processed: 0,
299            total_items: 0,
300            is_cancelled: false,
301            error: None,
302        }
303    }
304
305    /// Get overall progress percentage
306    pub fn overall_progress(&self) -> f64 {
307        if self.total_steps == 0 {
308            self.step_progress
309        } else {
310            ((self.completed_steps as f64 + self.step_progress / 100.0) / self.total_steps as f64)
311                * 100.0
312        }
313    }
314
315    /// Get elapsed time
316    pub fn elapsed(&self) -> Duration {
317        self.start_time.elapsed()
318    }
319
320    /// Get estimated time remaining
321    pub fn estimated_time_remaining(&self) -> Option<Duration> {
322        if let Some(estimated) = self.estimated_duration {
323            Some(estimated)
324        } else if self.total_steps > 0 && self.completed_steps > 0 {
325            let elapsed = self.elapsed();
326            let avg_step_time = elapsed / self.completed_steps as u32;
327            let remaining_steps = self.total_steps - self.completed_steps;
328            Some(avg_step_time * remaining_steps as u32)
329        } else {
330            None
331        }
332    }
333
334    /// Check if operation is completed
335    pub fn is_completed(&self) -> bool {
336        self.total_steps > 0 && self.completed_steps >= self.total_steps
337    }
338}
339
340/// Progress display for console output
341pub struct ProgressDisplay {
342    reporter: ProgressReporter,
343    show_detailed: bool,
344}
345
346impl ProgressDisplay {
347    pub fn new(reporter: ProgressReporter, show_detailed: bool) -> Self {
348        Self {
349            reporter,
350            show_detailed,
351        }
352    }
353
354    /// Display progress in a formatted way
355    pub fn display(&self) {
356        let state = self.reporter.get_state();
357
358        if self.show_detailed {
359            println!(
360                "📦 {} - {:.1}% complete",
361                state.current_operation,
362                state.overall_progress()
363            );
364            println!(
365                "  Step: {} ({:.1}%)",
366                state.current_step, state.step_progress
367            );
368            println!(
369                "  Progress: {}/{} steps completed",
370                state.completed_steps, state.total_steps
371            );
372
373            if state.total_bytes > 0 {
374                println!(
375                    "  Data: {}/{} MB ({:.1}%)",
376                    state.bytes_processed / 1_048_576,
377                    state.total_bytes / 1_048_576,
378                    (state.bytes_processed as f64 / state.total_bytes as f64) * 100.0
379                );
380            }
381
382            if let Some(remaining) = state.estimated_time_remaining() {
383                println!("  Estimated remaining: {:.0}s", remaining.as_secs_f64());
384            }
385        } else {
386            println!(
387                "📦 {}: {:.1}% - {} ({}/{})",
388                state.current_operation,
389                state.overall_progress(),
390                state.current_step,
391                state.completed_steps,
392                state.total_steps
393            );
394        }
395    }
396
397    /// Display progress bar
398    pub fn display_bar(&self) {
399        let state = self.reporter.get_state();
400        let overall = state.overall_progress();
401        let bar_width = 40;
402
403        let filled_width = (overall / 100.0 * bar_width as f64) as usize;
404        let empty_width = bar_width - filled_width;
405
406        let filled = "â–ˆ".repeat(filled_width);
407        let empty = "â–‘".repeat(empty_width);
408
409        println!(
410            "📦 {} |{}{}| {:.1}% ({}/{})",
411            state.current_operation,
412            filled,
413            empty,
414            overall,
415            state.completed_steps,
416            state.total_steps
417        );
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_progress_state_creation() {
427        let state = ProgressState::new();
428        assert_eq!(state.current_step, "Not started");
429        assert_eq!(state.step_progress, 0.0);
430        assert_eq!(state.total_steps, 0);
431        assert_eq!(state.completed_steps, 0);
432        assert!(!state.is_cancelled);
433        assert!(state.error.is_none());
434    }
435
436    #[test]
437    fn test_progress_calculation() {
438        let mut state = ProgressState::new();
439        state.total_steps = 5;
440        state.completed_steps = 2;
441        state.step_progress = 50.0;
442
443        assert_eq!(state.overall_progress(), 50.0); // 2.5/5 = 50%
444    }
445
446    #[test]
447    fn test_completion_check() {
448        let mut state = ProgressState::new();
449        assert!(!state.is_completed());
450
451        state.total_steps = 3;
452        state.completed_steps = 2;
453        assert!(!state.is_completed());
454
455        state.completed_steps = 3;
456        assert!(state.is_completed());
457    }
458
459    #[tokio::test]
460    async fn test_progress_reporter() {
461        let reporter = ProgressReporter::new();
462
463        reporter.start_test_operation("test");
464        reporter.set_total_steps(3);
465
466        // Test step progress
467        reporter.start_step("Step 1", 1);
468        reporter.update_step_progress(25.0, "Processing...");
469        reporter.complete_step("Step 1");
470
471        reporter.start_step("Step 2", 2);
472        reporter.update_step_progress(75.0, "Almost done");
473        reporter.complete_step("Step 2");
474
475        reporter.start_step("Step 3", 3);
476        reporter.update_step_progress(100.0, "Complete");
477        reporter.complete_step("Step 3");
478
479        let state = reporter.get_state();
480        assert_eq!(state.completed_steps, 3);
481        assert_eq!(state.total_steps, 3);
482        assert!(state.is_completed());
483    }
484
485    impl ProgressReporter {
486        // Helper for testing
487        fn start_test_operation(&self, operation_name: &str) {
488            self.start_operation(operation_name);
489        }
490    }
491}