use std::time::Instant;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use super::styling::{Color, Palette, Theme};
#[allow(dead_code)]
static SPINNER_CHARS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepStatus {
Pending,
Active,
Completed,
Failed,
Skipped,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Step {
title: String,
detail: Option<String>,
status: StepStatus,
started_at: Option<Instant>,
duration_ms: Option<u64>,
}
#[allow(dead_code)]
impl Step {
pub fn pending(title: &str) -> Self {
Self {
title: title.to_string(),
detail: None,
status: StepStatus::Pending,
started_at: None,
duration_ms: None,
}
}
pub fn active(title: &str, detail: &str) -> Self {
Self {
title: title.to_string(),
detail: Some(detail.to_string()),
status: StepStatus::Active,
started_at: Some(Instant::now()),
duration_ms: None,
}
}
pub fn completed(mut self) -> Self {
self.status = StepStatus::Completed;
if let Some(start) = self.started_at {
self.duration_ms = Some(start.elapsed().as_millis() as u64);
}
self
}
pub fn failed(mut self) -> Self {
self.status = StepStatus::Failed;
if let Some(start) = self.started_at {
self.duration_ms = Some(start.elapsed().as_millis() as u64);
}
self
}
pub fn with_detail(mut self, detail: &str) -> Self {
self.detail = Some(detail.to_string());
self
}
pub fn status(&self) -> StepStatus {
self.status
}
pub fn title(&self) -> &str {
&self.title
}
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
pub fn duration_ms(&self) -> Option<u64> {
self.duration_ms
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ProgressTracker {
progress_bar: ProgressBar,
theme: Theme,
steps: Vec<Step>,
current_step: usize,
started_at: Instant,
timing_breakdown: Vec<(String, u64)>,
}
#[allow(dead_code)]
impl ProgressTracker {
pub fn new(message: &str) -> Self {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
Self {
progress_bar: pb,
theme: Theme::new(),
steps: Vec::new(),
current_step: 0,
started_at: Instant::now(),
timing_breakdown: Vec::new(),
}
}
pub fn with_theme(message: &str, theme: Theme) -> Self {
let mut tracker = Self::new(message);
tracker.theme = theme;
tracker
}
pub fn steps(mut self, steps: &[Step]) -> Self {
self.steps = steps.to_vec();
self
}
pub fn set_active(&mut self, index: usize) {
if index < self.steps.len() {
self.current_step = index;
self.steps[index].started_at = Some(Instant::now());
self.update_message();
}
}
pub fn set_detail(&mut self, detail: &str) {
if self.current_step < self.steps.len() {
self.steps[self.current_step].detail = Some(detail.to_string());
self.update_message();
}
}
pub fn complete_current(&mut self) {
if self.current_step < self.steps.len() {
self.steps[self.current_step].status = StepStatus::Completed;
if let Some(start) = self.steps[self.current_step].started_at {
let duration = start.elapsed().as_millis() as u64;
self.timing_breakdown
.push((self.steps[self.current_step].title.clone(), duration));
}
self.current_step += 1;
self.update_message();
}
}
pub fn complete_step(&mut self, index: usize) {
if index < self.steps.len() {
self.steps[index].status = StepStatus::Completed;
if let Some(start) = self.steps[index].started_at {
let duration = start.elapsed().as_millis() as u64;
self.timing_breakdown
.push((self.steps[index].title.clone(), duration));
}
self.update_message();
}
}
pub fn fail_current(&mut self) {
if self.current_step < self.steps.len() {
self.steps[self.current_step].status = StepStatus::Failed;
self.update_message();
}
}
fn update_message(&self) {
if self.current_step < self.steps.len() {
let step = &self.steps[self.current_step];
let msg = match step.status {
StepStatus::Active => {
if let Some(detail) = &step.detail {
format!("{} ({})", step.title, detail)
} else {
step.title.clone()
}
}
_ => step.title.clone(),
};
self.progress_bar.set_message(msg);
}
}
pub fn finish_with_success(&self, message: &str) {
self.progress_bar.finish_with_message(message.to_string());
}
pub fn finish_with_error(&self, message: &str) {
self.progress_bar.finish_with_message(message.to_string());
}
pub fn timing_breakdown(&self) -> &[(String, u64)] {
&self.timing_breakdown
}
pub fn elapsed_ms(&self) -> u64 {
self.started_at.elapsed().as_millis() as u64
}
pub fn elapsed_formatted(&self) -> String {
let ms = self.elapsed_ms();
if ms < 1000 {
format!("{}ms", ms)
} else {
format!("{:.1}s", ms as f64 / 1000.0)
}
}
pub fn progress_bar(&self) -> &ProgressBar {
&self.progress_bar
}
pub fn progress_bar_mut(&mut self) -> &mut ProgressBar {
&mut self.progress_bar
}
}
pub fn spinner(message: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
}
pub fn oauth_wait_spinner() -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
);
pb.set_message("Waiting for authentication...".to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
}
#[allow(dead_code)]
pub fn styled_progress(message: &str, palette: &Palette) -> ProgressBar {
let pb = ProgressBar::new_spinner();
let template = format!(
"{{spinner:.{}}} {{msg}}",
match palette.primary {
Color::MutedBlue | Color::Cyan => "cyan",
Color::Green => "green",
Color::Red => "red",
Color::Amber => "yellow",
Color::Purple => "magenta",
_ => "green",
}
);
pb.set_style(
ProgressStyle::default_spinner()
.template(&template)
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
}
#[allow(dead_code)]
pub fn format_timing_breakdown(breakdown: &[(String, u64)], total_ms: u64) -> String {
let mut result = String::new();
for (name, duration) in breakdown {
let duration_str = if *duration < 1000 {
format!("{}ms", duration)
} else {
format!("{:.1}s", *duration as f64 / 1000.0)
};
result.push_str(&format!(" {} {}\n", name.dimmed(), duration_str.green()));
}
let total_str = if total_ms < 1000 {
format!("{}ms", total_ms)
} else {
format!("{:.1}s", total_ms as f64 / 1000.0)
};
result.push_str(&format!(" {} {}", "Total".dimmed(), total_str.green()));
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_step_pending() {
let step = Step::pending("Test Step");
assert_eq!(step.title(), "Test Step");
assert_eq!(step.status(), StepStatus::Pending);
assert!(step.detail().is_none());
}
#[test]
fn test_step_active() {
let step = Step::active("Active Step", "details here");
assert_eq!(step.title(), "Active Step");
assert_eq!(step.status(), StepStatus::Active);
assert_eq!(step.detail(), Some("details here"));
}
#[test]
fn test_step_completed() {
let step = Step::active("Test", "detail").completed();
assert_eq!(step.status(), StepStatus::Completed);
assert!(step.duration_ms().is_some());
}
#[test]
fn test_format_timing_breakdown_empty() {
let result = format_timing_breakdown(&[], 0);
assert!(result.contains("Total"));
}
#[test]
fn test_format_timing_breakdown_with_items() {
let breakdown = vec![("Step1".to_string(), 100u64), ("Step2".to_string(), 500u64)];
let result = format_timing_breakdown(&breakdown, 600);
assert!(result.contains("Step1"));
assert!(result.contains("Step2"));
assert!(result.contains("100ms"));
assert!(result.contains("500ms"));
assert!(result.contains("600ms"));
}
#[test]
fn test_format_timing_breakdown_seconds() {
let breakdown = vec![("Long Step".to_string(), 2500u64)];
let result = format_timing_breakdown(&breakdown, 2500);
assert!(result.contains("2.5s"));
}
}