1use std::time::Instant;
6
7use colored::Colorize;
8use indicatif::{ProgressBar, ProgressStyle};
9
10use super::styling::{Color, Palette, Theme};
11
12#[allow(dead_code)]
14static SPINNER_CHARS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
15
16#[allow(dead_code)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum StepStatus {
20 Pending,
22 Active,
24 Completed,
26 Failed,
28 Skipped,
30}
31
32#[allow(dead_code)]
34#[derive(Debug, Clone)]
35pub struct Step {
36 title: String,
38 detail: Option<String>,
40 status: StepStatus,
42 started_at: Option<Instant>,
44 duration_ms: Option<u64>,
46}
47
48#[allow(dead_code)]
49impl Step {
50 pub fn pending(title: &str) -> Self {
52 Self {
53 title: title.to_string(),
54 detail: None,
55 status: StepStatus::Pending,
56 started_at: None,
57 duration_ms: None,
58 }
59 }
60
61 pub fn active(title: &str, detail: &str) -> Self {
63 Self {
64 title: title.to_string(),
65 detail: Some(detail.to_string()),
66 status: StepStatus::Active,
67 started_at: Some(Instant::now()),
68 duration_ms: None,
69 }
70 }
71
72 pub fn completed(mut self) -> Self {
74 self.status = StepStatus::Completed;
75 if let Some(start) = self.started_at {
76 self.duration_ms = Some(start.elapsed().as_millis() as u64);
77 }
78 self
79 }
80
81 pub fn failed(mut self) -> Self {
83 self.status = StepStatus::Failed;
84 if let Some(start) = self.started_at {
85 self.duration_ms = Some(start.elapsed().as_millis() as u64);
86 }
87 self
88 }
89
90 pub fn with_detail(mut self, detail: &str) -> Self {
92 self.detail = Some(detail.to_string());
93 self
94 }
95
96 pub fn status(&self) -> StepStatus {
98 self.status
99 }
100
101 pub fn title(&self) -> &str {
103 &self.title
104 }
105
106 pub fn detail(&self) -> Option<&str> {
108 self.detail.as_deref()
109 }
110
111 pub fn duration_ms(&self) -> Option<u64> {
113 self.duration_ms
114 }
115}
116
117#[allow(dead_code)]
119#[derive(Debug, Clone)]
120pub struct ProgressTracker {
121 progress_bar: ProgressBar,
123 theme: Theme,
125 steps: Vec<Step>,
127 current_step: usize,
129 started_at: Instant,
131 timing_breakdown: Vec<(String, u64)>,
133}
134
135#[allow(dead_code)]
136impl ProgressTracker {
137 pub fn new(message: &str) -> Self {
139 let pb = ProgressBar::new_spinner();
140 pb.set_style(
141 ProgressStyle::default_spinner()
142 .template("{spinner:.cyan} {msg}")
143 .unwrap(),
144 );
145 pb.set_message(message.to_string());
146 pb.enable_steady_tick(std::time::Duration::from_millis(100));
147
148 Self {
149 progress_bar: pb,
150 theme: Theme::new(),
151 steps: Vec::new(),
152 current_step: 0,
153 started_at: Instant::now(),
154 timing_breakdown: Vec::new(),
155 }
156 }
157
158 pub fn with_theme(message: &str, theme: Theme) -> Self {
160 let mut tracker = Self::new(message);
161 tracker.theme = theme;
162 tracker
163 }
164
165 pub fn steps(mut self, steps: &[Step]) -> Self {
167 self.steps = steps.to_vec();
168 self
169 }
170
171 pub fn set_active(&mut self, index: usize) {
173 if index < self.steps.len() {
174 self.current_step = index;
175 self.steps[index].started_at = Some(Instant::now());
176 self.update_message();
177 }
178 }
179
180 pub fn set_detail(&mut self, detail: &str) {
182 if self.current_step < self.steps.len() {
183 self.steps[self.current_step].detail = Some(detail.to_string());
184 self.update_message();
185 }
186 }
187
188 pub fn complete_current(&mut self) {
190 if self.current_step < self.steps.len() {
191 self.steps[self.current_step].status = StepStatus::Completed;
192 if let Some(start) = self.steps[self.current_step].started_at {
193 let duration = start.elapsed().as_millis() as u64;
194 self.timing_breakdown
195 .push((self.steps[self.current_step].title.clone(), duration));
196 }
197 self.current_step += 1;
198 self.update_message();
199 }
200 }
201
202 pub fn complete_step(&mut self, index: usize) {
204 if index < self.steps.len() {
205 self.steps[index].status = StepStatus::Completed;
206 if let Some(start) = self.steps[index].started_at {
207 let duration = start.elapsed().as_millis() as u64;
208 self.timing_breakdown
209 .push((self.steps[index].title.clone(), duration));
210 }
211 self.update_message();
212 }
213 }
214
215 pub fn fail_current(&mut self) {
217 if self.current_step < self.steps.len() {
218 self.steps[self.current_step].status = StepStatus::Failed;
219 self.update_message();
220 }
221 }
222
223 fn update_message(&self) {
224 if self.current_step < self.steps.len() {
225 let step = &self.steps[self.current_step];
226 let msg = match step.status {
227 StepStatus::Active => {
228 if let Some(detail) = &step.detail {
229 format!("{} ({})", step.title, detail)
230 } else {
231 step.title.clone()
232 }
233 }
234 _ => step.title.clone(),
235 };
236 self.progress_bar.set_message(msg);
237 }
238 }
239
240 pub fn finish_with_success(&self, message: &str) {
242 self.progress_bar.finish_with_message(message.to_string());
243 }
244
245 pub fn finish_with_error(&self, message: &str) {
247 self.progress_bar.finish_with_message(message.to_string());
248 }
249
250 pub fn timing_breakdown(&self) -> &[(String, u64)] {
252 &self.timing_breakdown
253 }
254
255 pub fn elapsed_ms(&self) -> u64 {
257 self.started_at.elapsed().as_millis() as u64
258 }
259
260 pub fn elapsed_formatted(&self) -> String {
262 let ms = self.elapsed_ms();
263 if ms < 1000 {
264 format!("{}ms", ms)
265 } else {
266 format!("{:.1}s", ms as f64 / 1000.0)
267 }
268 }
269
270 pub fn progress_bar(&self) -> &ProgressBar {
272 &self.progress_bar
273 }
274
275 pub fn progress_bar_mut(&mut self) -> &mut ProgressBar {
277 &mut self.progress_bar
278 }
279}
280
281pub fn spinner(message: &str) -> ProgressBar {
283 let pb = ProgressBar::new_spinner();
284 pb.set_style(
285 ProgressStyle::default_spinner()
286 .template("{spinner:.green} {msg}")
287 .unwrap(),
288 );
289 pb.set_message(message.to_string());
290 pb.enable_steady_tick(std::time::Duration::from_millis(100));
291 pb
292}
293
294pub fn oauth_wait_spinner() -> ProgressBar {
296 let pb = ProgressBar::new_spinner();
297 pb.set_style(
298 ProgressStyle::default_spinner()
299 .template("{spinner:.cyan} {msg}")
300 .unwrap()
301 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
302 );
303 pb.set_message("Waiting for authentication...".to_string());
304 pb.enable_steady_tick(std::time::Duration::from_millis(100));
305 pb
306}
307
308#[allow(dead_code)]
310pub fn styled_progress(message: &str, palette: &Palette) -> ProgressBar {
311 let pb = ProgressBar::new_spinner();
312 let template = format!(
313 "{{spinner:.{}}} {{msg}}",
314 match palette.primary {
315 Color::MutedBlue | Color::Cyan => "cyan",
316 Color::Green => "green",
317 Color::Red => "red",
318 Color::Amber => "yellow",
319 Color::Purple => "magenta",
320 _ => "green",
321 }
322 );
323 pb.set_style(
324 ProgressStyle::default_spinner()
325 .template(&template)
326 .unwrap(),
327 );
328 pb.set_message(message.to_string());
329 pb.enable_steady_tick(std::time::Duration::from_millis(100));
330 pb
331}
332
333#[allow(dead_code)]
335pub fn format_timing_breakdown(breakdown: &[(String, u64)], total_ms: u64) -> String {
336 let mut result = String::new();
337
338 for (name, duration) in breakdown {
339 let duration_str = if *duration < 1000 {
340 format!("{}ms", duration)
341 } else {
342 format!("{:.1}s", *duration as f64 / 1000.0)
343 };
344 result.push_str(&format!(" {} {}\n", name.dimmed(), duration_str.green()));
345 }
346
347 let total_str = if total_ms < 1000 {
349 format!("{}ms", total_ms)
350 } else {
351 format!("{:.1}s", total_ms as f64 / 1000.0)
352 };
353 result.push_str(&format!(" {} {}", "Total".dimmed(), total_str.green()));
354
355 result
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_step_pending() {
364 let step = Step::pending("Test Step");
365 assert_eq!(step.title(), "Test Step");
366 assert_eq!(step.status(), StepStatus::Pending);
367 assert!(step.detail().is_none());
368 }
369
370 #[test]
371 fn test_step_active() {
372 let step = Step::active("Active Step", "details here");
373 assert_eq!(step.title(), "Active Step");
374 assert_eq!(step.status(), StepStatus::Active);
375 assert_eq!(step.detail(), Some("details here"));
376 }
377
378 #[test]
379 fn test_step_completed() {
380 let step = Step::active("Test", "detail").completed();
381 assert_eq!(step.status(), StepStatus::Completed);
382 assert!(step.duration_ms().is_some());
383 }
384
385 #[test]
386 fn test_format_timing_breakdown_empty() {
387 let result = format_timing_breakdown(&[], 0);
388 assert!(result.contains("Total"));
389 }
390
391 #[test]
392 fn test_format_timing_breakdown_with_items() {
393 let breakdown = vec![("Step1".to_string(), 100u64), ("Step2".to_string(), 500u64)];
394 let result = format_timing_breakdown(&breakdown, 600);
395 assert!(result.contains("Step1"));
396 assert!(result.contains("Step2"));
397 assert!(result.contains("100ms"));
398 assert!(result.contains("500ms"));
399 assert!(result.contains("600ms"));
400 }
401
402 #[test]
403 fn test_format_timing_breakdown_seconds() {
404 let breakdown = vec![("Long Step".to_string(), 2500u64)];
405 let result = format_timing_breakdown(&breakdown, 2500);
406 assert!(result.contains("2.5s"));
407 }
408}