1use std::collections::HashMap;
40use std::time::{Duration, Instant};
41
42use crate::style::Style;
43
44#[derive(Debug, Clone)]
50pub struct ProgressBar {
51 pub total: Option<f64>,
53 pub completed: f64,
55 pub width: Option<usize>,
57 pub complete_char: char,
59 pub remaining_char: char,
61 pub pulse: bool,
63 pub complete_style: Style,
65 pub remaining_style: Style,
67 pub pulse_style: Style,
69}
70
71impl ProgressBar {
72 pub fn new() -> Self {
74 Self {
75 total: Some(100.0),
76 completed: 0.0,
77 width: None,
78 complete_char: '█',
79 remaining_char: '░',
80 pulse: false,
81 complete_style: Style::new(),
82 remaining_style: Style::new(),
83 pulse_style: Style::new(),
84 }
85 }
86
87 pub fn total(mut self, total: f64) -> Self { self.total = Some(total); self }
89
90 pub fn completed(mut self, completed: f64) -> Self { self.completed = completed; self }
92
93 pub fn width(mut self, width: usize) -> Self { self.width = Some(width); self }
95
96 pub fn complete_style(mut self, style: Style) -> Self { self.complete_style = style; self }
98
99 pub fn remaining_style(mut self, style: Style) -> Self { self.remaining_style = style; self }
101
102 pub fn percentage(&self) -> f64 {
104 if let Some(total) = self.total {
105 if total > 0.0 {
106 (self.completed / total).min(1.0).max(0.0)
107 } else {
108 0.0
109 }
110 } else {
111 0.0
112 }
113 }
114
115 pub fn render(&self, width: usize) -> String {
117 let w = self.width.unwrap_or(width).saturating_sub(2); if w < 3 {
119 return "[]".to_string();
120 }
121
122 if self.pulse || self.total.is_none() {
123 let pos = ((self.completed as usize / 8) % (w - 1)).min(w);
125 let left = " ".repeat(pos);
126 let right = " ".repeat(w.saturating_sub(pos + 1));
127 format!("[{left}⣿{right}]")
128 } else {
129 let pct = self.percentage();
130 let filled = (w as f64 * pct) as usize;
131 let empty = w - filled;
132 let complete_ansi = self.complete_style.to_ansi();
133 let complete_reset = if complete_ansi.is_empty() { "" } else { "\x1b[0m" };
134 format!(
135 "[{complete_ansi}{}{complete_reset}{}]",
136 self.complete_char.to_string().repeat(filled),
137 self.remaining_char.to_string().repeat(empty)
138 )
139 }
140 }
141}
142
143impl Default for ProgressBar {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149#[derive(Debug, Clone)]
155pub struct Task {
156 pub id: usize,
157 pub description: String,
158 pub total: Option<f64>,
159 pub completed: f64,
160 pub visible: bool,
161 pub start_time: Instant,
162 pub fields: HashMap<String, String>,
163}
164
165impl Task {
166 pub fn new(id: usize, description: impl Into<String>, total: Option<f64>) -> Self {
168 Self {
169 id,
170 description: description.into(),
171 total,
172 completed: 0.0,
173 visible: true,
174 start_time: Instant::now(),
175 fields: HashMap::new(),
176 }
177 }
178
179 pub fn progress(&self) -> f64 {
181 if let Some(t) = self.total {
182 if t > 0.0 {
183 (self.completed / t).min(1.0).max(0.0)
184 } else {
185 0.0
186 }
187 } else {
188 0.0
189 }
190 }
191
192 pub fn elapsed(&self) -> Duration {
194 self.start_time.elapsed()
195 }
196
197 pub fn time_remaining(&self) -> Option<Duration> {
200 let pct = self.progress();
201 if pct > 0.0 {
202 let elapsed = self.elapsed();
203 let total = elapsed.div_f64(pct);
204 Some(total.saturating_sub(elapsed))
205 } else {
206 None
207 }
208 }
209
210 pub fn is_finished(&self) -> bool {
212 if let Some(t) = self.total {
213 self.completed >= t
214 } else {
215 false
216 }
217 }
218}
219
220#[derive(Debug)]
226pub struct Progress {
227 pub tasks: Vec<Task>,
228 pub auto_refresh: bool,
229 pub refresh_per_second: f64,
230 pub transient: bool,
231 pub columns: Option<Vec<Box<dyn crate::progress_columns::ProgressColumn>>>,
233 next_id: usize,
234}
235
236impl Progress {
237 pub fn new() -> Self {
239 Self {
240 tasks: Vec::new(),
241 auto_refresh: true,
242 refresh_per_second: 4.0,
243 transient: false,
244 columns: None,
245 next_id: 1,
246 }
247 }
248
249 pub fn with_columns(mut self, columns: Vec<Box<dyn crate::progress_columns::ProgressColumn>>) -> Self {
253 self.columns = Some(columns);
254 self
255 }
256
257 pub fn add_task(
259 &mut self,
260 description: impl Into<String>,
261 total: Option<f64>,
262 ) -> usize {
263 let id = self.next_id;
264 self.next_id += 1;
265 self.tasks.push(Task::new(id, description, total));
266 id
267 }
268
269 pub fn advance(&mut self, task_id: usize, delta: f64) {
271 if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
272 task.completed += delta;
273 if let Some(total) = task.total {
274 if task.completed > total {
275 task.completed = total;
276 }
277 }
278 }
279 }
280
281 pub fn update(&mut self, task_id: usize, completed: f64) {
283 if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
284 task.completed = completed;
285 }
286 }
287
288 pub fn remove_task(&mut self, task_id: usize) {
290 self.tasks.retain(|t| t.id != task_id);
291 }
292
293 pub fn render(&self, width: usize) -> String {
295 if let Some(ref columns) = self.columns {
296 self.render_with_columns(width, columns)
297 } else {
298 self.render_default(width)
299 }
300 }
301
302 fn render_with_columns(&self, _width: usize, columns: &[Box<dyn crate::progress_columns::ProgressColumn>]) -> String {
304 let mut out = String::new();
305 let now = std::time::Instant::now();
306 for task in &self.tasks {
307 if !task.visible {
308 continue;
309 }
310 let elapsed = now.duration_since(task.start_time);
311 let mut line = String::new();
312 for (i, col) in columns.iter().enumerate() {
313 if i > 0 { line.push(' '); }
314 line.push_str(&col.render(task, 20, elapsed));
315 }
316 out.push_str(&line);
317 out.push('\n');
318 }
319 out
320 }
321
322 fn render_default(&self, width: usize) -> String {
324 let mut out = String::new();
325 for task in &self.tasks {
326 if !task.visible {
327 continue;
328 }
329 let bar_width = width.saturating_sub(30).max(10);
330 let bar = self.render_task_bar(task, bar_width);
331 let pct = (task.progress() * 100.0) as usize;
332 let elapsed = format_duration(&task.elapsed());
333 let remaining = task
334 .time_remaining()
335 .map(|d| format_duration(&d))
336 .unwrap_or_else(|| "?".to_string());
337
338 out.push_str(&format!(
339 "{desc:<20} {pct:>3}% {bar} {elapsed}<{remaining}\n",
340 desc = task.description.chars().take(20).collect::<String>(),
341 ));
342 }
343 out
344 }
345
346 fn render_task_bar(&self, task: &Task, width: usize) -> String {
347 let w = width.saturating_sub(2);
348 if w < 3 {
349 return "[]".to_string();
350 }
351 let pct = task.progress();
352 let filled = (w as f64 * pct) as usize;
353 let empty = w - filled;
354 format!("[{}░{}]",
355 "█".repeat(filled),
356 " ".repeat(empty.saturating_sub(1))
357 )
358 }
359
360 pub fn track<I: IntoIterator>(
364 &mut self,
365 sequence: I,
366 description: impl Into<String>,
367 total: Option<f64>,
368 ) -> TrackIterator<I::IntoIter> {
369 let iter = sequence.into_iter();
370 let (lower, upper) = iter.size_hint();
371 let total = total.unwrap_or(upper.unwrap_or(lower) as f64);
372 let task_id = self.add_task(description, Some(total));
373
374 TrackIterator {
375 inner: iter,
376 progress_id: task_id,
377 count: 0,
378 total,
379 }
380 }
381
382 pub fn advance_bytes(&mut self, task_id: usize, bytes: u64) {
384 self.advance(task_id, bytes as f64);
385 }
386
387 pub fn open(
391 &mut self,
392 path: impl AsRef<std::path::Path>,
393 description: impl Into<String>,
394 ) -> std::io::Result<ProgressFile> {
395 let path = path.as_ref();
396 let metadata = std::fs::metadata(path)?;
397 let total = metadata.len();
398 let file = std::fs::File::open(path)?;
399 Ok(self.wrap_file(file, total, description))
400 }
401
402 pub fn wrap_file(
404 &mut self,
405 file: std::fs::File,
406 total: u64,
407 description: impl Into<String>,
408 ) -> ProgressFile {
409 let task_id = self.add_task(description, Some(total as f64));
410 ProgressFile::new(file, task_id, total)
411 }
412}
413
414impl Default for Progress {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420pub struct TrackIterator<I: Iterator> {
427 inner: I,
428 pub progress_id: usize,
430 count: usize,
431 total: f64,
432}
433
434impl<I: Iterator> Iterator for TrackIterator<I> {
435 type Item = I::Item;
436
437 fn next(&mut self) -> Option<Self::Item> {
438 let item = self.inner.next();
439 if item.is_some() {
440 self.count += 1;
441 }
442 item
443 }
444
445 fn size_hint(&self) -> (usize, Option<usize>) {
446 self.inner.size_hint()
447 }
448}
449
450impl<I: Iterator> TrackIterator<I> {
451 pub fn count(&self) -> usize { self.count }
453
454 pub fn total(&self) -> f64 { self.total }
456}
457
458#[derive(Debug)]
464pub struct ProgressFile {
465 inner: std::fs::File,
466 task_id: usize,
467 total: u64,
468 bytes_read: u64,
469}
470
471impl ProgressFile {
472 pub fn new(file: std::fs::File, task_id: usize, total: u64) -> Self {
474 Self { inner: file, task_id, total, bytes_read: 0 }
475 }
476
477 pub fn bytes_read(&self) -> u64 { self.bytes_read }
479
480 pub fn total(&self) -> u64 { self.total }
482
483 pub fn task_id(&self) -> usize { self.task_id }
485
486 pub fn sync(&self, progress: &mut Progress) {
488 if let Some(task) = progress.tasks.iter_mut().find(|t| t.id == self.task_id) {
489 task.completed = self.bytes_read as f64;
490 }
491 }
492
493 pub fn inner(&self) -> &std::fs::File { &self.inner }
495
496 pub fn inner_mut(&mut self) -> &mut std::fs::File { &mut self.inner }
498
499 pub fn into_inner(self) -> std::fs::File { self.inner }
501}
502
503impl std::io::Read for ProgressFile {
504 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
505 let n = self.inner.read(buf)?;
506 self.bytes_read += n as u64;
507 Ok(n)
508 }
509}
510
511fn format_duration(d: &Duration) -> String {
514 let secs = d.as_secs();
515 if secs < 60 {
516 format!("0:{secs:02}")
517 } else if secs < 3600 {
518 format!("{}:{:02}", secs / 60, secs % 60)
519 } else {
520 format!("{}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_progress_bar_render() {
530 let bar = ProgressBar::new().total(100.0).completed(50.0);
531 let r = bar.render(20);
532 assert!(r.contains('█'));
533 }
534
535 #[test]
536 fn test_progress_add_task() {
537 let mut p = Progress::new();
538 let id = p.add_task("Download", Some(100.0));
539 assert_eq!(id, 1);
540 p.advance(1, 50.0);
541 assert_eq!(p.tasks[0].completed, 50.0);
542 }
543
544 #[test]
545 fn test_advance_bytes() {
546 let mut p = Progress::new();
547 let id = p.add_task("Download", Some(1000.0));
548 p.advance_bytes(id, 256);
549 assert_eq!(p.tasks[0].completed, 256.0);
550 }
551
552 #[test]
553 fn test_progress_file_wrap_and_read() {
554 use std::io::Read;
555 let data = b"hello world";
556 let dir = std::env::temp_dir();
557 let path = dir.join("rusty_rich_test_progress.txt");
558
559 std::fs::write(&path, data).unwrap();
561
562 let mut p = Progress::new();
563 let mut pf = p.open(&path, "test file").unwrap();
564 assert_eq!(pf.total(), 11);
565 assert_eq!(pf.bytes_read(), 0);
566
567 let mut buf = [0u8; 5];
569 let n = pf.read(&mut buf).unwrap();
570 assert_eq!(n, 5);
571 assert_eq!(pf.bytes_read(), 5);
572
573 pf.sync(&mut p);
575 assert_eq!(p.tasks[0].completed, 5.0);
576
577 let mut buf = Vec::new();
579 pf.read_to_end(&mut buf).unwrap();
580 assert_eq!(pf.bytes_read(), 11);
581
582 pf.sync(&mut p);
584 assert_eq!(p.tasks[0].completed, 11.0);
585
586 drop(pf);
587 std::fs::remove_file(&path).unwrap();
588 }
589
590 #[test]
591 fn test_progress_file_wrap_existing() {
592 let data = b"test data for wrap";
593 let dir = std::env::temp_dir();
594 let path = dir.join("rusty_rich_test_wrap.txt");
595 std::fs::write(&path, data).unwrap();
596
597 let file = std::fs::File::open(&path).unwrap();
598 let mut p = Progress::new();
599 let pf = p.wrap_file(file, data.len() as u64, "wrapped");
600 assert_eq!(pf.total(), data.len() as u64);
601 assert_eq!(pf.task_id(), 1);
602
603 drop(pf);
604 std::fs::remove_file(&path).unwrap();
605 }
606}