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/// ```
74#[allow(clippy::literal_string_with_formatting_args)]
75pub fn create_progress_bar(total: u64) -> ProgressBar {
76    let pb = ProgressBar::new(total);
77    pb.set_style(
78        ProgressStyle::default_bar()
79            .template("[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} ({eta})")
80            .unwrap_or_else(|_| ProgressStyle::default_bar())
81            .progress_chars("=>-"),
82    );
83    pb
84}
85
86/// Creates a spinner for indeterminate operations.
87///
88/// Spinners are used when the total duration or progress cannot be determined,
89/// such as waiting for network responses or performing iterative searches.
90///
91/// # Arguments
92///
93/// * `message` - Initial status message to display next to the spinner
94///
95/// # Returns
96///
97/// A configured [`ProgressBar`] in spinner mode with a green spinner animation.
98///
99/// # Display Format
100///
101/// ```text
102/// ⠋ Connecting to remote storage...
103/// ⠙ Connecting to remote storage...
104/// ⠹ Connecting to remote storage...
105/// ```
106///
107/// # Example
108///
109/// ```no_run
110/// # use hexz_cli::ui::progress::create_spinner;
111/// let sp = create_spinner("Initializing...");
112///
113/// std::thread::sleep(std::time::Duration::from_millis(100));
114/// sp.set_message("Connecting...");
115///
116/// std::thread::sleep(std::time::Duration::from_millis(100));
117/// sp.finish_with_message("Ready");
118/// ```
119///
120/// # Notes
121///
122/// - The spinner auto-ticks to create the animation effect
123/// - Update the message with [`set_message`](ProgressBar::set_message)
124/// - Call [`finish`](ProgressBar::finish) or [`finish_with_message`](ProgressBar::finish_with_message) when done
125pub fn create_spinner(message: &str) -> ProgressBar {
126    let pb = ProgressBar::new_spinner();
127    pb.set_style(
128        ProgressStyle::default_spinner()
129            .template("{spinner:.green} {msg}")
130            .unwrap_or_else(|_| ProgressStyle::default_spinner()),
131    );
132    pb.set_message(message.to_string());
133    pb
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_create_progress_bar_basic() {
142        let pb = create_progress_bar(1000);
143        assert_eq!(pb.length(), Some(1000));
144        assert_eq!(pb.position(), 0);
145    }
146
147    #[test]
148    fn test_create_progress_bar_zero_total() {
149        let pb = create_progress_bar(0);
150        assert_eq!(pb.length(), Some(0));
151    }
152
153    #[test]
154    fn test_create_progress_bar_large_total() {
155        let total = 10 * 1024 * 1024 * 1024u64; // 10 GB
156        let pb = create_progress_bar(total);
157        assert_eq!(pb.length(), Some(total));
158    }
159
160    #[test]
161    fn test_progress_bar_increment() {
162        let pb = create_progress_bar(100);
163        pb.inc(50);
164        assert_eq!(pb.position(), 50);
165        pb.inc(50);
166        assert_eq!(pb.position(), 100);
167    }
168
169    #[test]
170    fn test_progress_bar_finish() {
171        let pb = create_progress_bar(100);
172        pb.inc(100);
173        pb.finish_with_message("done");
174        assert!(pb.is_finished());
175    }
176
177    #[test]
178    fn test_create_spinner_basic() {
179        let sp = create_spinner("Loading...");
180        assert!(!sp.is_finished());
181    }
182
183    #[test]
184    fn test_spinner_finish() {
185        let sp = create_spinner("Working...");
186        sp.finish_with_message("Complete");
187        assert!(sp.is_finished());
188    }
189
190    #[test]
191    fn test_spinner_update_message() {
192        let sp = create_spinner("Step 1");
193        sp.set_message("Step 2");
194        sp.finish();
195        assert!(sp.is_finished());
196    }
197}