modcli/output/
progress.rs

1use std::io::{stdout, Write};
2use std::thread;
3use std::time::Duration;
4use crossterm::style::{Stylize, Color};
5
6/// Customizable style for the progress bar
7#[derive(Clone)]
8pub struct ProgressStyle {
9    pub fill: char,
10    pub start_cap: char,
11    pub end_cap: char,
12    pub done_label: &'static str,
13    pub show_percent: bool,
14    pub color: Option<Color>,
15}
16
17impl Default for ProgressStyle {
18    fn default() -> Self {
19        Self {
20            fill: '#',
21            start_cap: '[',
22            end_cap: ']',
23            done_label: "Done!",
24            show_percent: true,
25            color: None,
26        }
27    }
28}
29
30/// Struct-based progress bar
31pub struct ProgressBar {
32    pub total_steps: usize,
33    pub current: usize,
34    pub label: Option<String>,
35    pub style: ProgressStyle,
36}
37
38impl ProgressBar {
39    pub fn new(total_steps: usize, style: ProgressStyle) -> Self {
40        Self {
41            total_steps,
42            current: 0,
43            label: None,
44            style,
45        }
46    }
47
48    pub fn set_label(&mut self, label: &str) {
49        self.label = Some(label.to_string());
50    }
51
52    pub fn set_progress(&mut self, value: usize) {
53        self.current = value.min(self.total_steps);
54        self.render();
55    }
56
57    pub fn tick(&mut self) {
58        self.current += 1;
59        if self.current > self.total_steps {
60            self.current = self.total_steps;
61        }
62        self.render();
63    }
64
65    pub fn start_auto(&mut self, duration_ms: u64) {
66        let interval = duration_ms / self.total_steps.max(1) as u64;
67        for _ in 0..self.total_steps {
68            self.tick();
69            thread::sleep(Duration::from_millis(interval));
70        }
71        println!(" {}", self.style.done_label);
72    }
73
74    fn render(&self) {
75        let percent = if self.style.show_percent {
76            format!(" {:>3}%", self.current * 100 / self.total_steps.max(1))
77        } else {
78            "".to_string()
79        };
80
81        let fill_count = self.current;
82        let empty_count = self.total_steps - self.current;
83
84        let mut bar = format!(
85            "{}{}{}{}",
86            self.style.start_cap,
87            self.style.fill.to_string().repeat(fill_count),
88            " ".repeat(empty_count),
89            self.style.end_cap
90        );
91
92        if let Some(color) = self.style.color {
93            bar = bar.with(color).to_string();
94        }
95        print!("\r");
96    
97        if let Some(ref label) = self.label {
98            print!("{} {}", label, bar);
99        } else {
100            print!("{}", bar);
101        }
102
103        print!("{}", percent);
104        stdout().flush().unwrap();
105    }
106}
107
108// Procedural-style one-liners
109
110pub fn show_progress_bar(label: &str, total_steps: usize, duration_ms: u64) {
111    let mut bar = ProgressBar::new(total_steps, ProgressStyle::default());
112    bar.set_label(label);
113    bar.start_auto(duration_ms);
114}
115
116pub fn show_percent_progress(label: &str, percent: usize) {
117    let clamped = percent.clamp(0, 100);
118    print!("\r{}: {:>3}% complete", label, clamped);
119    stdout().flush().unwrap();
120}
121
122pub fn show_spinner(label: &str, cycles: usize, delay_ms: u64) {
123    let spinner = vec!['|', '/', '-', '\\'];
124    let mut stdout = stdout();
125    print!("{} ", label);
126
127    for i in 0..cycles {
128        let frame = spinner[i % spinner.len()];
129        print!("\r{} {}", label, frame);
130        stdout.flush().unwrap();
131        thread::sleep(Duration::from_millis(delay_ms));
132    }
133
134    println!("{} ✓", label);
135}