1#![allow(clippy::unwrap_used)] use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9use tokio::sync::broadcast;
10use tracing::{debug, info, warn};
11
12#[derive(Clone)]
14pub struct ProgressReporter {
15 progress: Arc<Mutex<ProgressState>>,
16 events: broadcast::Sender<ProgressEvent>,
17}
18
19#[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#[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#[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 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 pub fn for_operation(operation_name: &str) -> Self {
114 let reporter = Self::new();
115 reporter.start_operation(operation_name);
116 reporter
117 }
118
119 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 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 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 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 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 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 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 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 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 pub fn is_cancelled(&self) -> bool {
252 let state = self.progress.lock().unwrap();
253 state.is_cancelled
254 }
255
256 pub fn get_state(&self) -> ProgressState {
258 self.progress.lock().unwrap().clone()
259 }
260
261 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 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 pub fn subscribe(&self) -> broadcast::Receiver<ProgressEvent> {
276 self.events.subscribe()
277 }
278
279 fn broadcast_event(&self, event: ProgressEvent) {
281 let _ = self.events.send(event);
282 }
283}
284
285impl ProgressState {
286 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 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 pub fn elapsed(&self) -> Duration {
317 self.start_time.elapsed()
318 }
319
320 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 pub fn is_completed(&self) -> bool {
336 self.total_steps > 0 && self.completed_steps >= self.total_steps
337 }
338}
339
340pub 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 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 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); }
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 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 fn start_test_operation(&self, operation_name: &str) {
488 self.start_operation(operation_name);
489 }
490 }
491}