1use crate::console::{Console, RenderContext};
2use crate::progress::columns::{
3 BarColumn, PercentageColumn, ProgressColumn, TextColumn, TimeRemainingColumn,
4};
5use crate::renderable::{Renderable, Segment};
6use crate::style::{Color, Style};
7use crate::text::Span;
8use std::io::{self, Write};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone)]
16pub struct Task {
17 pub id: usize,
19 pub description: String,
21 pub total: Option<u64>,
23 pub completed: u64,
25 pub start_time: Instant,
27 pub finished: bool,
29 pub style: Style,
31}
32
33impl Task {
34 pub fn new(id: usize, description: &str, total: Option<u64>) -> Self {
36 Task {
37 id,
38 description: description.to_string(),
39 total,
40 completed: 0,
41 start_time: Instant::now(),
42 finished: false,
43 style: Style::new().foreground(Color::Cyan),
44 }
45 }
46
47 pub fn percentage(&self) -> f64 {
49 match self.total {
50 Some(total) if total > 0 => (self.completed as f64 / total as f64).min(1.0),
51 _ => 0.0,
52 }
53 }
54
55 pub fn elapsed(&self) -> Duration {
57 self.start_time.elapsed()
58 }
59
60 pub fn eta(&self) -> Option<Duration> {
62 if self.completed == 0 {
63 return None;
64 }
65
66 let elapsed = self.elapsed().as_secs_f64();
67 let rate = self.completed as f64 / elapsed;
68
69 self.total.and_then(|total| {
70 let remaining = total.saturating_sub(self.completed);
71 if rate > 0.0 {
72 Some(Duration::from_secs_f64(remaining as f64 / rate))
73 } else {
74 None
75 }
76 })
77 }
78
79 pub fn speed(&self) -> f64 {
81 let elapsed = self.elapsed().as_secs_f64();
82 if elapsed > 0.0 {
83 self.completed as f64 / elapsed
84 } else {
85 0.0
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
93pub struct ProgressBar {
94 pub bar_width: usize,
96 pub complete_char: char,
98 pub remaining_char: char,
100 pub complete_style: Style,
102 pub remaining_style: Style,
104}
105
106impl Default for ProgressBar {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl ProgressBar {
113 pub fn new() -> Self {
115 ProgressBar {
116 bar_width: 40,
117 complete_char: '━',
118 remaining_char: '━',
119 complete_style: Style::new().foreground(Color::Cyan),
120 remaining_style: Style::new().foreground(Color::BrightBlack),
121 }
122 }
123 pub fn width(mut self, width: usize) -> Self {
126 self.bar_width = width;
127 self
128 }
129}
130
131pub struct Progress {
137 tasks: Arc<Mutex<Vec<Task>>>,
139 next_id: Arc<Mutex<usize>>,
141 columns: Vec<Box<dyn ProgressColumn>>,
143 #[allow(dead_code)]
145 visible: bool,
146 console: Option<Console>,
148 started: bool,
150 height: Arc<Mutex<usize>>,
152 transient: bool,
154 refresh_per_second: f64,
156 auto_refresh: bool,
158 refresh_thread: Option<JoinHandle<()>>,
160 stop_signal: Arc<AtomicBool>,
162}
163
164impl Default for Progress {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl Progress {
171 pub fn new() -> Self {
173 Progress {
174 tasks: Arc::new(Mutex::new(Vec::new())),
175 next_id: Arc::new(Mutex::new(0)),
176 columns: vec![
177 Box::new(TextColumn::new("[progress.description]")),
178 Box::new(BarColumn::new(40)),
179 Box::new(PercentageColumn::new()),
180 Box::new(TimeRemainingColumn),
181 ],
182 visible: true,
183 console: None,
184 started: false,
185 height: Arc::new(Mutex::new(0)),
186 transient: false,
187 refresh_per_second: 10.0,
188 auto_refresh: true,
189 refresh_thread: None,
190 stop_signal: Arc::new(AtomicBool::new(false)),
191 }
192 }
193
194 pub fn with_console(mut self, console: Console) -> Self {
196 self.console = Some(console);
197 self
198 }
199
200 pub fn transient(mut self, transient: bool) -> Self {
202 self.transient = transient;
203 self
204 }
205
206 pub fn refresh_per_second(mut self, rate: f64) -> Self {
210 self.refresh_per_second = rate.max(0.1); self
212 }
213
214 pub fn auto_refresh(mut self, enabled: bool) -> Self {
219 self.auto_refresh = enabled;
220 self
221 }
222
223 pub fn with_columns(mut self, columns: Vec<Box<dyn ProgressColumn>>) -> Self {
225 self.columns = columns;
226 self
227 }
228
229 pub fn add_task(&self, description: &str, total: Option<u64>) -> usize {
231 let mut next_id = self.next_id.lock().unwrap();
232 let id = *next_id;
233 *next_id += 1;
234
235 let task = Task::new(id, description, total);
236 self.tasks.lock().unwrap().push(task);
237
238 id
239 }
240
241 pub fn advance(&self, task_id: usize, amount: u64) {
243 if let Ok(mut tasks) = self.tasks.lock() {
244 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
245 task.completed += amount;
246 if let Some(total) = task.total {
247 if task.completed >= total {
248 task.finished = true;
249 }
250 }
251 }
252 }
253 }
254
255 pub fn update(&self, task_id: usize, completed: u64) {
257 if let Ok(mut tasks) = self.tasks.lock() {
258 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
259 task.completed = completed;
260 if let Some(total) = task.total {
261 if task.completed >= total {
262 task.finished = true;
263 }
264 }
265 }
266 }
267 }
268
269 pub fn finish(&self, task_id: usize) {
271 if let Ok(mut tasks) = self.tasks.lock() {
272 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
273 task.finished = true;
274 }
275 }
276 }
277
278 pub fn remove(&self, task_id: usize) {
280 if let Ok(mut tasks) = self.tasks.lock() {
281 tasks.retain(|t| t.id != task_id);
282 }
283 }
284
285 pub fn is_finished(&self) -> bool {
287 self.tasks
288 .lock()
289 .map(|tasks| tasks.iter().all(|t| t.finished))
290 .unwrap_or(true)
291 }
292
293 pub fn render_to_string(&self) -> String {
295 let context = RenderContext {
296 width: 80,
297 height: None,
298 };
299 let segments = self.render(&context);
300
301 let mut result = String::new();
302 for segment in segments {
303 result.push_str(&segment.plain_text());
304 if segment.newline {
305 result.push('\n');
306 }
307 }
308 result
309 }
310
311 pub fn print(&self) {
313 let output = self.render_to_string();
314
315 let tasks = self.tasks.lock().unwrap();
317 let num_lines = tasks.len();
318 drop(tasks);
319
320 if num_lines > 0 {
321 print!("\x1B[{}A", num_lines);
323 }
324
325 for line in output.lines() {
327 println!("\x1B[2K{}", line);
328 }
329
330 let _ = io::stdout().flush();
331 }
332
333 pub fn start(&mut self) {
339 if self.started {
340 return;
341 }
342 if let Some(ref console) = self.console {
343 console.show_cursor(false);
344 }
345 self.started = true;
346 *self.height.lock().unwrap() = 0;
347 self.stop_signal.store(false, Ordering::SeqCst);
348
349 if self.auto_refresh && self.console.is_some() {
351 let _tasks = Arc::clone(&self.tasks);
352 let _height = Arc::clone(&self.height);
353 let _stop_signal = Arc::clone(&self.stop_signal);
354 let _interval_ms = (1000.0 / self.refresh_per_second) as u64;
355
356 }
362
363 self.refresh();
364 }
365
366 pub fn stop(&mut self) {
371 if !self.started {
372 return;
373 }
374
375 self.stop_signal.store(true, Ordering::SeqCst);
377
378 if let Some(handle) = self.refresh_thread.take() {
380 let _ = handle.join();
381 }
382
383 let current_height = *self.height.lock().unwrap();
384
385 if let Some(ref console) = self.console {
386 if current_height > 0 {
388 console.move_cursor_up(current_height as u16);
389 for _ in 0..current_height {
390 console.clear_line();
391 console.move_cursor_down(1);
392 }
393 console.move_cursor_up(current_height as u16);
394 }
395
396 if !self.transient {
397 console.print_renderable(self);
399 console.newline();
400 }
401
402 console.show_cursor(true);
403 }
404
405 self.started = false;
406 *self.height.lock().unwrap() = 0;
407 }
408
409 pub fn refresh(&mut self) {
414 if let Some(ref console) = self.console {
415 if !self.started {
416 console.print_renderable(self);
418 console.newline();
419 return;
420 }
421
422 let current_height = *self.height.lock().unwrap();
423
424 if current_height > 0 {
426 console.move_cursor_up(current_height as u16);
427 }
428
429 let width = console.get_width();
431 let context = RenderContext {
432 width,
433 height: None,
434 };
435
436 let segments = self.render(&context);
437
438 let mut lines = 0;
440 for segment in &segments {
441 if segment.newline {
442 lines += 1;
443 }
444 }
445
446 console.write_segments(&segments);
448
449 if !segments.is_empty() && !segments.last().unwrap().newline {
451 console.newline();
452 lines += 1;
453 }
454
455 if current_height > lines {
457 let diff = current_height - lines;
458 for _ in 0..diff {
459 console.clear_line();
460 console.newline();
461 }
462 console.move_cursor_up(diff as u16);
463 }
464
465 *self.height.lock().unwrap() = lines;
466 } else {
467 self.print();
469 }
470 }
471
472 pub fn run<F, R>(&mut self, f: F) -> R
491 where
492 F: FnOnce(&mut Self) -> R,
493 {
494 self.start();
495 let result = f(self);
496 self.stop();
497 result
498 }
499
500 pub fn is_started(&self) -> bool {
502 self.started
503 }
504}
505
506impl Renderable for Progress {
507 fn render(&self, _context: &RenderContext) -> Vec<Segment> {
508 let tasks = self.tasks.lock().unwrap();
509 let mut segments = Vec::new();
510
511 for task in tasks.iter() {
512 let mut spans = Vec::new();
513
514 for (i, column) in self.columns.iter().enumerate() {
515 if i > 0 {
516 spans.push(Span::raw(" "));
517 }
518 spans.extend(column.render(task));
519 }
520
521 segments.push(Segment::line(spans));
522 }
523
524 segments
525 }
526}
527
528impl Drop for Progress {
529 fn drop(&mut self) {
530 if self.started {
532 self.stop();
533 }
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn test_task_percentage() {
543 let mut task = Task::new(0, "Test", Some(100));
544 assert_eq!(task.percentage(), 0.0);
545
546 task.completed = 50;
547 assert!((task.percentage() - 0.5).abs() < 0.01);
548
549 task.completed = 100;
550 assert!((task.percentage() - 1.0).abs() < 0.01);
551 }
552
553 #[test]
554 fn test_progress_add_task() {
555 let progress = Progress::new();
556 let id1 = progress.add_task("Task 1", Some(100));
557 let id2 = progress.add_task("Task 2", Some(200));
558
559 assert_eq!(id1, 0);
560 assert_eq!(id2, 1);
561 }
562
563 #[test]
564 fn test_progress_advance() {
565 let progress = Progress::new();
566 let id = progress.add_task("Test", Some(100));
567
568 progress.advance(id, 25);
569 progress.advance(id, 25);
570
571 let tasks = progress.tasks.lock().unwrap();
572 assert_eq!(tasks[0].completed, 50);
573 }
574
575 #[test]
576 fn test_progress_bar_render() {
577 use crate::progress::columns::BarColumn;
578 let bar_col = BarColumn::new(10);
579 let mut task = Task::new(0, "Test", Some(100));
580 task.completed = 50;
581
582 let spans = bar_col.render(&task);
583 assert_eq!(spans.len(), 3);
585 }
586}