modcli/output/
progress.rs

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