Skip to main content

hexz_cli/ui/
progress.rs

1//! Centralized progress bar and spinner utilities.
2//!
3//! This module provides consistent progress bar creation and styling across all
4//! CLI commands, ensuring uniform user feedback during long-running operations.
5//!
6//! # Progress Bar Styling
7//!
8//! All progress bars use a standardized format:
9//! ```text
10//! [00:01:23] =========>------------------------------ 234MB/1GB (00:02:15)
11//!  ^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^     ^^^^^^^^^^^  ^^^^^^^^
12//!  elapsed   visual bar (40 chars)                    bytes        ETA
13//! ```
14//!
15//! # Spinner Styling
16//!
17//! Spinners are used for indeterminate operations (e.g., waiting for network):
18//! ```text
19//! ⠋ Connecting to remote storage...
20//! ```
21//!
22//! # Usage
23//!
24//! ```no_run
25//! use hexz_cli::ui::progress::{create_progress_bar, create_spinner};
26//!
27//! // Determinate operation
28//! let pb = create_progress_bar(1024 * 1024 * 100); // 100 MB
29//! for _ in 0..100 {
30//!     pb.inc(1024 * 1024); // 1 MB per iteration
31//! }
32//! pb.finish_with_message("Complete");
33//!
34//! // Indeterminate operation
35//! let sp = create_spinner("Processing...");
36//! std::thread::sleep(std::time::Duration::from_millis(100));
37//! sp.finish_with_message("Done");
38//! ```
39
40use indicatif::{ProgressBar, ProgressStyle};
41
42/// Creates a standardized progress bar for determinate operations.
43///
44/// # Arguments
45///
46/// * `total` - Total number of bytes or units to process
47///
48/// # Returns
49///
50/// A configured [`ProgressBar`] with:
51/// - 40-character visual bar
52/// - Elapsed time display
53/// - Bytes progress (current/total)
54/// - Estimated time to completion (ETA)
55///
56/// # Display Format
57///
58/// ```text
59/// [00:01:23] =========>------------------------------ 234MB/1GB (00:02:15)
60/// ```
61///
62/// # Example
63///
64/// ```no_run
65/// # use hexz_cli::ui::progress::create_progress_bar;
66/// let file_size = 1024 * 1024 * 100; // 100 MB
67/// let pb = create_progress_bar(file_size);
68///
69/// for _ in 0..100 {
70///     pb.inc(1024 * 1024);
71/// }
72/// pb.finish_with_message("Download complete");
73/// ```
74pub fn create_progress_bar(total: u64) -> ProgressBar {
75    let pb = ProgressBar::new(total);
76    pb.set_style(
77        ProgressStyle::default_bar()
78            .template("[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} ({eta})")
79            .unwrap_or_else(|_| ProgressStyle::default_bar())
80            .progress_chars("=>-"),
81    );
82    pb
83}
84
85/// Creates a spinner for indeterminate operations.
86///
87/// Spinners are used when the total duration or progress cannot be determined,
88/// such as waiting for network responses or performing iterative searches.
89///
90/// # Arguments
91///
92/// * `message` - Initial status message to display next to the spinner
93///
94/// # Returns
95///
96/// A configured [`ProgressBar`] in spinner mode with a green spinner animation.
97///
98/// # Display Format
99///
100/// ```text
101/// ⠋ Connecting to remote storage...
102/// ⠙ Connecting to remote storage...
103/// ⠹ Connecting to remote storage...
104/// ```
105///
106/// # Example
107///
108/// ```no_run
109/// # use hexz_cli::ui::progress::create_spinner;
110/// let sp = create_spinner("Initializing...");
111///
112/// std::thread::sleep(std::time::Duration::from_millis(100));
113/// sp.set_message("Connecting...");
114///
115/// std::thread::sleep(std::time::Duration::from_millis(100));
116/// sp.finish_with_message("Ready");
117/// ```
118///
119/// # Notes
120///
121/// - The spinner auto-ticks to create the animation effect
122/// - Update the message with [`set_message`](ProgressBar::set_message)
123/// - Call [`finish`](ProgressBar::finish) or [`finish_with_message`](ProgressBar::finish_with_message) when done
124pub fn create_spinner(message: &str) -> ProgressBar {
125    let pb = ProgressBar::new_spinner();
126    pb.set_style(
127        ProgressStyle::default_spinner()
128            .template("{spinner:.green} {msg}")
129            .unwrap_or_else(|_| ProgressStyle::default_spinner()),
130    );
131    pb.set_message(message.to_string());
132    pb
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_create_progress_bar_basic() {
141        let pb = create_progress_bar(1000);
142        assert_eq!(pb.length(), Some(1000));
143        assert_eq!(pb.position(), 0);
144    }
145
146    #[test]
147    fn test_create_progress_bar_zero_total() {
148        let pb = create_progress_bar(0);
149        assert_eq!(pb.length(), Some(0));
150    }
151
152    #[test]
153    fn test_create_progress_bar_large_total() {
154        let total = 10 * 1024 * 1024 * 1024u64; // 10 GB
155        let pb = create_progress_bar(total);
156        assert_eq!(pb.length(), Some(total));
157    }
158
159    #[test]
160    fn test_progress_bar_increment() {
161        let pb = create_progress_bar(100);
162        pb.inc(50);
163        assert_eq!(pb.position(), 50);
164        pb.inc(50);
165        assert_eq!(pb.position(), 100);
166    }
167
168    #[test]
169    fn test_progress_bar_finish() {
170        let pb = create_progress_bar(100);
171        pb.inc(100);
172        pb.finish_with_message("done");
173        assert!(pb.is_finished());
174    }
175
176    #[test]
177    fn test_create_spinner_basic() {
178        let sp = create_spinner("Loading...");
179        assert!(!sp.is_finished());
180    }
181
182    #[test]
183    fn test_spinner_finish() {
184        let sp = create_spinner("Working...");
185        sp.finish_with_message("Complete");
186        assert!(sp.is_finished());
187    }
188
189    #[test]
190    fn test_spinner_update_message() {
191        let sp = create_spinner("Step 1");
192        sp.set_message("Step 2");
193        sp.finish();
194        assert!(sp.is_finished());
195    }
196}