Skip to main content

ipfrs_cli/
progress.rs

1//! Progress bar and spinner utilities for IPFRS CLI
2
3#![allow(dead_code)]
4
5use indicatif::{ProgressBar, ProgressStyle};
6use std::time::Duration;
7
8/// Minimum file size (10 MB) for which a progress bar is shown.
9///
10/// Files smaller than this threshold use a hidden progress bar so that
11/// the caller code can call `pb.inc()` without any visible output.
12pub const LARGE_FILE_THRESHOLD: u64 = 10 * 1024 * 1024; // 10 MB
13
14/// Create a progress bar for a file operation that is conditionally visible.
15///
16/// The progress bar is **hidden** (no terminal output) when:
17/// - `total_bytes` is below [`LARGE_FILE_THRESHOLD`] (10 MB), or
18/// - stdout is not a TTY (e.g. when piped to another process or script).
19///
20/// For large files on a TTY the bar shows bytes transferred, rate, and ETA,
21/// matching the style used across all other IPFRS file operations.
22///
23/// # Arguments
24///
25/// * `total_bytes` – Total expected size of the transfer in bytes.
26/// * `operation`   – Short verb shown at the start of the bar line (e.g. `"Adding"`, `"Downloading"`).
27pub fn file_progress_bar(total_bytes: u64, operation: &str) -> ProgressBar {
28    // Hide for small files or non-interactive output.
29    use std::io::IsTerminal;
30    let is_tty = std::io::stdout().is_terminal();
31    if total_bytes < LARGE_FILE_THRESHOLD || !is_tty {
32        return ProgressBar::hidden();
33    }
34
35    let pb = ProgressBar::new(total_bytes);
36    pb.set_style(
37        ProgressStyle::default_bar()
38            .template(&format!(
39                "{{spinner:.green}} {} [{{elapsed_precise}}] [{{bar:40.cyan/blue}}] {{bytes}}/{{total_bytes}} ({{bytes_per_sec}}, {{eta}})",
40                operation
41            ))
42            .expect("valid progress template")
43            .progress_chars("=>-"),
44    );
45    pb.enable_steady_tick(Duration::from_millis(100));
46    pb
47}
48
49/// Create a progress bar for file operations
50pub fn file_progress(total: u64, message: &str) -> ProgressBar {
51    let pb = ProgressBar::new(total);
52    pb.set_style(
53        ProgressStyle::default_bar()
54            .template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})")
55            .expect("valid template")
56            .progress_chars("=>-"),
57    );
58    pb.set_message(message.to_string());
59    pb.enable_steady_tick(Duration::from_millis(100));
60    pb
61}
62
63/// Create a progress bar for block operations
64pub fn block_progress(total: u64, message: &str) -> ProgressBar {
65    let pb = ProgressBar::new(total);
66    pb.set_style(
67        ProgressStyle::default_bar()
68            .template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} blocks")
69            .expect("valid template")
70            .progress_chars("=>-"),
71    );
72    pb.set_message(message.to_string());
73    pb.enable_steady_tick(Duration::from_millis(100));
74    pb
75}
76
77/// Create a spinner for operations with unknown duration
78pub fn spinner(message: &str) -> ProgressBar {
79    let pb = ProgressBar::new_spinner();
80    pb.set_style(
81        ProgressStyle::default_spinner()
82            .template("{spinner:.green} {msg} [{elapsed_precise}]")
83            .expect("valid template"),
84    );
85    pb.set_message(message.to_string());
86    pb.enable_steady_tick(Duration::from_millis(100));
87    pb
88}
89
90/// Create a download progress bar
91pub fn download_progress(total: u64, filename: &str) -> ProgressBar {
92    let pb = ProgressBar::new(total);
93    pb.set_style(
94        ProgressStyle::default_bar()
95            .template("{spinner:.green} Downloading {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
96            .expect("valid template")
97            .progress_chars("=>-"),
98    );
99    pb.set_message(filename.to_string());
100    pb.enable_steady_tick(Duration::from_millis(100));
101    pb
102}
103
104/// Create an upload progress bar
105pub fn upload_progress(total: u64, filename: &str) -> ProgressBar {
106    let pb = ProgressBar::new(total);
107    pb.set_style(
108        ProgressStyle::default_bar()
109            .template("{spinner:.green} Uploading {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
110            .expect("valid template")
111            .progress_chars("=>-"),
112    );
113    pb.set_message(filename.to_string());
114    pb.enable_steady_tick(Duration::from_millis(100));
115    pb
116}
117
118/// Create a multi-progress indicator for batch operations
119pub fn batch_progress(total: u64, message: &str) -> ProgressBar {
120    let pb = ProgressBar::new(total);
121    pb.set_style(
122        ProgressStyle::default_bar()
123            .template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%)")
124            .expect("valid template")
125            .progress_chars("=>-"),
126    );
127    pb.set_message(message.to_string());
128    pb.enable_steady_tick(Duration::from_millis(100));
129    pb
130}
131
132/// Finish progress bar with success message
133pub fn finish_success(pb: &ProgressBar, message: &str) {
134    pb.set_style(
135        ProgressStyle::default_bar()
136            .template("{msg}")
137            .expect("valid template"),
138    );
139    pb.finish_with_message(format!("\x1b[32m✓\x1b[0m {}", message));
140}
141
142/// Finish progress bar with error message
143pub fn finish_error(pb: &ProgressBar, message: &str) {
144    pb.set_style(
145        ProgressStyle::default_bar()
146            .template("{msg}")
147            .expect("valid template"),
148    );
149    pb.finish_with_message(format!("\x1b[31m✗\x1b[0m {}", message));
150}
151
152/// Finish spinner with success
153pub fn finish_spinner_success(pb: &ProgressBar, message: &str) {
154    pb.finish_with_message(format!("\x1b[32m✓\x1b[0m {}", message));
155}
156
157/// Finish spinner with error
158pub fn finish_spinner_error(pb: &ProgressBar, message: &str) {
159    pb.finish_with_message(format!("\x1b[31m✗\x1b[0m {}", message));
160}
161
162/// Progress tracker for streaming operations
163pub struct StreamProgress {
164    pb: ProgressBar,
165    total: u64,
166    current: u64,
167}
168
169impl StreamProgress {
170    /// Create a new stream progress tracker
171    pub fn new(total: u64, message: &str) -> Self {
172        let pb = file_progress(total, message);
173        Self {
174            pb,
175            total,
176            current: 0,
177        }
178    }
179
180    /// Update progress with bytes written/read
181    pub fn update(&mut self, bytes: u64) {
182        self.current += bytes;
183        self.pb.set_position(self.current);
184    }
185
186    /// Set absolute position
187    pub fn set_position(&mut self, pos: u64) {
188        self.current = pos;
189        self.pb.set_position(pos);
190    }
191
192    /// Get current progress percentage
193    pub fn percentage(&self) -> f64 {
194        if self.total == 0 {
195            100.0
196        } else {
197            (self.current as f64 / self.total as f64) * 100.0
198        }
199    }
200
201    /// Finish with success
202    pub fn finish(self, message: &str) {
203        finish_success(&self.pb, message);
204    }
205
206    /// Finish with error
207    pub fn finish_error(self, message: &str) {
208        finish_error(&self.pb, message);
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_spinner_creation() {
218        let pb = spinner("Testing...");
219        pb.finish_with_message("Done");
220    }
221
222    #[test]
223    fn test_progress_creation() {
224        let pb = file_progress(1000, "Processing");
225        pb.set_position(500);
226        pb.finish_with_message("Complete");
227    }
228
229    #[test]
230    fn test_stream_progress() {
231        let mut sp = StreamProgress::new(100, "Streaming");
232        sp.update(25);
233        assert_eq!(sp.percentage(), 25.0);
234        sp.update(25);
235        assert_eq!(sp.percentage(), 50.0);
236        sp.finish("Done");
237    }
238}