scirs2_core/logging/progress/
multi.rs

1//! Multi-progress tracking
2//!
3//! This module provides support for tracking multiple progress operations
4//! simultaneously with coordinated display and management.
5
6use super::tracker::EnhancedProgressTracker;
7use std::collections::HashMap;
8
9/// Manager for multiple progress bars
10pub struct MultiProgress {
11    /// Active progress trackers
12    trackers: HashMap<usize, EnhancedProgressTracker>,
13    /// Next available ID
14    next_id: usize,
15    /// Whether to show all trackers or just the active one
16    show_all: bool,
17}
18
19impl MultiProgress {
20    /// Create a new multi-progress manager
21    pub fn new() -> Self {
22        Self {
23            trackers: HashMap::new(),
24            next_id: 0,
25            show_all: true,
26        }
27    }
28
29    /// Create a new multi-progress manager that shows only the active tracker
30    pub fn new_single_view() -> Self {
31        Self {
32            trackers: HashMap::new(),
33            next_id: 0,
34            show_all: false,
35        }
36    }
37
38    /// Add a progress tracker
39    pub fn add(&mut self, mut tracker: EnhancedProgressTracker) -> usize {
40        let id = self.next_id;
41        self.next_id += 1;
42
43        // Hide individual trackers if we're managing display
44        if !self.show_all {
45            tracker.hide();
46        }
47
48        self.trackers.insert(id, tracker);
49        id
50    }
51
52    /// Get a progress tracker by ID
53    pub fn get(&mut self, id: usize) -> Option<&mut EnhancedProgressTracker> {
54        self.trackers.get_mut(&id)
55    }
56
57    /// Remove a progress tracker
58    pub fn remove(&mut self, id: usize) -> Option<EnhancedProgressTracker> {
59        self.trackers.remove(&id)
60    }
61
62    /// Start a specific tracker
63    pub fn start(&mut self, id: usize) {
64        if let Some(tracker) = self.trackers.get_mut(&id) {
65            tracker.start();
66        }
67    }
68
69    /// Start all progress trackers
70    pub fn start_all(&mut self) {
71        for tracker in self.trackers.values_mut() {
72            tracker.start();
73        }
74    }
75
76    /// Update a specific tracker
77    pub fn update(&mut self, id: usize, processed: u64) {
78        if let Some(tracker) = self.trackers.get_mut(&id) {
79            tracker.update(processed);
80        }
81
82        if !self.show_all {
83            self.render_overview();
84        }
85    }
86
87    /// Increment a specific tracker
88    pub fn increment(&mut self, id: usize, amount: u64) {
89        if let Some(tracker) = self.trackers.get_mut(&id) {
90            tracker.increment(amount);
91        }
92
93        if !self.show_all {
94            self.render_overview();
95        }
96    }
97
98    /// Finish a specific tracker
99    pub fn finish(&mut self, id: usize) {
100        if let Some(tracker) = self.trackers.get_mut(&id) {
101            tracker.finish();
102        }
103
104        if !self.show_all {
105            self.render_overview();
106        }
107    }
108
109    /// Finish all trackers
110    pub fn finish_all(&mut self) {
111        for tracker in self.trackers.values_mut() {
112            tracker.finish();
113        }
114    }
115
116    /// Get the number of active trackers
117    pub fn active_count(&self) -> usize {
118        self.trackers.len()
119    }
120
121    /// Get the overall progress (average of all trackers)
122    pub fn overall_progress(&self) -> f64 {
123        if self.trackers.is_empty() {
124            return 0.0;
125        }
126
127        let total_progress: f64 = self
128            .trackers
129            .values()
130            .map(|tracker| tracker.stats().percentage)
131            .sum();
132
133        total_progress / self.trackers.len() as f64
134    }
135
136    /// Check if all trackers are complete
137    pub fn all_complete(&self) -> bool {
138        self.trackers.values().all(|tracker| {
139            let stats = tracker.stats();
140            stats.is_complete()
141        })
142    }
143
144    /// Get IDs of all trackers
145    pub fn tracker_ids(&self) -> Vec<usize> {
146        self.trackers.keys().copied().collect()
147    }
148
149    /// Clear all completed trackers
150    pub fn clear_completed(&mut self) {
151        self.trackers.retain(|_, tracker| {
152            let stats = tracker.stats();
153            !stats.is_complete()
154        });
155    }
156
157    /// Render an overview of all progress bars (for single view mode)
158    fn render_overview(&self) {
159        if self.show_all || self.trackers.is_empty() {
160            return;
161        }
162
163        // Clear previous output
164        print!("\r\x1b[K");
165
166        // Show overall progress
167        let overall = self.overall_progress();
168        print!("Overall: {overall:.1}% | ");
169
170        // Show active trackers
171        let active_trackers: Vec<_> = self
172            .trackers
173            .iter()
174            .filter(|(_, tracker)| {
175                let stats = tracker.stats();
176                !stats.is_complete()
177            })
178            .collect();
179
180        match active_trackers.len() {
181            1 => {
182                let (_, tracker) = active_trackers[0];
183                let stats = tracker.stats();
184                print!(
185                    "{}: {:.1}% ({}/{})",
186                    tracker.description, stats.percentage, stats.processed, stats.total
187                );
188            }
189            n if n > 1 => {
190                print!("{} active tasks", active_trackers.len());
191            }
192            _ => {
193                print!("All tasks complete");
194            }
195        }
196
197        use std::io::{self, Write};
198        let _ = io::stdout().flush();
199    }
200}
201
202impl Default for MultiProgress {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208/// A progress group that manages related progress trackers
209pub struct ProgressGroup {
210    /// Group name
211    name: String,
212    /// Multi-progress manager
213    multi: MultiProgress,
214    /// Group-level statistics
215    started_at: Option<std::time::Instant>,
216}
217
218impl ProgressGroup {
219    /// Create a new progress group
220    pub fn new(name: &str) -> Self {
221        Self {
222            name: name.to_string(),
223            multi: MultiProgress::new(),
224            started_at: None,
225        }
226    }
227
228    /// Add a tracker to this group
229    pub fn add_tracker(&mut self, tracker: EnhancedProgressTracker) -> usize {
230        if self.started_at.is_none() {
231            self.started_at = Some(std::time::Instant::now());
232        }
233        self.multi.add(tracker)
234    }
235
236    /// Start all trackers in the group
237    pub fn start_all(&mut self) {
238        self.started_at = Some(std::time::Instant::now());
239        self.multi.start_all();
240
241        println!("Starting group: {}", self.name);
242    }
243
244    /// Update a tracker in the group
245    pub fn update(&mut self, id: usize, processed: u64) {
246        self.multi.update(id, processed);
247    }
248
249    /// Finish all trackers in the group
250    pub fn finish_all(&mut self) {
251        self.multi.finish_all();
252
253        if let Some(started) = self.started_at {
254            let elapsed = started.elapsed();
255            println!(
256                "Group '{}' completed in {:.2}s",
257                self.name,
258                elapsed.as_secs_f64()
259            );
260        }
261    }
262
263    /// Get the group's overall progress
264    pub fn overall_progress(&self) -> f64 {
265        self.multi.overall_progress()
266    }
267
268    /// Check if the entire group is complete
269    pub fn is_complete(&self) -> bool {
270        self.multi.all_complete()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::logging::progress::tracker::{ProgressBuilder, ProgressStyle};
278
279    #[test]
280    fn test_multi_progress_creation() {
281        let multi = MultiProgress::new();
282        assert_eq!(multi.active_count(), 0);
283        assert_eq!(multi.overall_progress(), 0.0);
284    }
285
286    #[test]
287    fn test_multi_progress_add_tracker() {
288        let mut multi = MultiProgress::new();
289
290        let tracker = ProgressBuilder::new("Test", 100)
291            .style(ProgressStyle::Bar)
292            .build();
293
294        let id = multi.add(tracker);
295        assert_eq!(id, 0);
296        assert_eq!(multi.active_count(), 1);
297    }
298
299    #[test]
300    fn test_multi_progress_overall_progress() {
301        let mut multi = MultiProgress::new();
302
303        let tracker1 = ProgressBuilder::new("Test 1", 100).build();
304        let tracker2 = ProgressBuilder::new("Test 2", 100).build();
305
306        let id1 = multi.add(tracker1);
307        let id2 = multi.add(tracker2);
308
309        multi.start_all();
310        multi.update(id1, 50); // 50% complete
311        multi.update(id2, 25); // 25% complete
312
313        // Overall should be (50 + 25) / 2 = 37.5%
314        let overall = multi.overall_progress();
315        assert!((overall - 37.5).abs() < 0.1);
316    }
317
318    #[test]
319    fn test_progress_group() {
320        let mut group = ProgressGroup::new("Test Group");
321
322        let tracker = ProgressBuilder::new("Task", 100).build();
323        let id = group.add_tracker(tracker);
324
325        assert_eq!(group.overall_progress(), 0.0);
326        assert!(!group.is_complete());
327    }
328}