Skip to main content

turbo_cdn/
cli_progress.rs

1// Licensed under the MIT License
2// Copyright (c) 2025 Hal <hal.long@outlook.com>
3
4//! CLI progress display utilities using indicatif
5//!
6//! This module provides professional progress indicators for CLI usage.
7
8use indicatif::{ProgressBar, ProgressStyle};
9use std::io::{self, Write};
10use std::time::Duration;
11
12/// Create a spinner with message
13pub fn create_spinner(message: &str) -> ProgressBar {
14    let pb = ProgressBar::new_spinner();
15    pb.set_style(
16        ProgressStyle::default_spinner()
17            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
18            .template("{spinner:.blue} {msg}")
19            .unwrap(),
20    );
21    pb.set_message(message.to_string());
22    pb.enable_steady_tick(Duration::from_millis(100));
23    pb
24}
25
26/// Create a progress bar for downloads
27pub fn create_download_progress(total_size: u64, message: &str) -> ProgressBar {
28    let pb = ProgressBar::new(total_size);
29    pb.set_style(
30        ProgressStyle::default_bar()
31            .template("{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
32            .unwrap()
33            .progress_chars("#>-")
34    );
35    pb.set_message(message.to_string());
36    pb
37}
38
39/// Create a simple progress bar
40pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
41    let pb = ProgressBar::new(total);
42    pb.set_style(
43        ProgressStyle::default_bar()
44            .template("{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
45            .unwrap()
46            .progress_chars("#>-"),
47    );
48    pb.set_message(message.to_string());
49    pb
50}
51
52/// Show a quick spinner for async operations
53pub async fn show_quick_spinner(message: &str) {
54    let spinner = create_spinner(message);
55    tokio::time::sleep(Duration::from_secs(1)).await;
56    spinner.finish_and_clear();
57}
58
59/// Display a clean status message
60pub fn status_message(icon: &str, message: &str) {
61    println!("{icon} {message}");
62}
63
64/// Display an info message (only in verbose mode)
65pub fn info_message(message: &str, verbose: bool) {
66    if verbose {
67        println!("ℹ️  {message}");
68    }
69}
70
71/// Display a success message
72pub fn success_message(message: &str) {
73    println!("✅ {message}");
74}
75
76/// Display an error message
77pub fn error_message(message: &str) {
78    println!("❌ {message}");
79}
80
81/// Display a warning message
82pub fn warning_message(message: &str) {
83    println!("⚠️  {message}");
84}
85
86/// Clear current line
87pub fn clear_line() {
88    print!("\r{:width$}\r", "", width = 80);
89    io::stdout().flush().unwrap();
90}
91
92/// Display download result in a clean format
93pub fn display_download_result(
94    path: &std::path::Path,
95    size_mb: f64,
96    speed_mbps: f64,
97    duration_secs: f64,
98    verbose: bool,
99) {
100    success_message(&format!(
101        "Downloaded {size_mb:.1} MB in {duration_secs:.1}s ({speed_mbps:.1} MB/s)"
102    ));
103
104    if verbose {
105        info_message(&format!("Saved to: {}", path.display()), true);
106    } else {
107        let path_display = path.display();
108        println!("📁 {path_display}");
109    }
110}
111
112/// Display speed test results in a clean format
113pub fn display_speed_test_results(
114    results: &[(String, f64, bool)], // (method, speed_mbps, selected)
115    verbose: bool,
116) {
117    if verbose {
118        println!("🚀 Speed test results:");
119        for (method, speed, selected) in results {
120            let status = if *selected { " ✓ (selected)" } else { "" };
121            if *speed > 0.0 {
122                println!("   ├─ {method}: {speed:.2} MB/s{status}");
123            } else {
124                println!("   ├─ {method}: failed{status}");
125            }
126        }
127
128        if let Some((selected_method, _, _)) = results.iter().find(|(_, _, selected)| *selected) {
129            println!("   └─ Using {selected_method} method");
130        }
131        println!();
132    } else {
133        // Compact display for non-verbose mode
134        if let Some((selected_method, speed, _)) = results.iter().find(|(_, _, selected)| *selected)
135        {
136            if *speed > 0.0 {
137                status_message("⚡", &format!("Using {selected_method} ({speed:.1} MB/s)"));
138            } else {
139                status_message("⚡", &format!("Using {selected_method}"));
140            }
141        }
142    }
143}
144
145/// Create a multi-progress container for multiple progress bars
146pub fn create_multi_progress() -> indicatif::MultiProgress {
147    indicatif::MultiProgress::new()
148}
149
150/// Finish a progress bar with success message
151pub fn finish_progress_with_message(pb: &ProgressBar, message: &str) {
152    pb.finish_with_message(format!("✅ {message}"));
153}
154
155/// Finish a progress bar with error message
156pub fn finish_progress_with_error(pb: &ProgressBar, message: &str) {
157    pb.finish_with_message(format!("❌ {message}"));
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_spinner_creation() {
166        let spinner = create_spinner("Loading...");
167        assert!(!spinner.is_finished());
168    }
169
170    #[test]
171    fn test_progress_bar_creation() {
172        let pb = create_progress_bar(100, "Testing");
173        assert_eq!(pb.length(), Some(100));
174    }
175
176    #[test]
177    fn test_download_progress_creation() {
178        let pb = create_download_progress(1024 * 1024, "Downloading");
179        assert_eq!(pb.length(), Some(1024 * 1024));
180    }
181}