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