modcli/output/
progress.rs1use crate::output::hook;
2use crossterm::style::{Color, Stylize};
3use std::io::{stdout, Write};
4use std::thread;
5use std::time::Duration;
6
7#[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
31pub 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
111pub 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}