1use indicatif::{ProgressBar, ProgressStyle};
11
12use crate::interactive;
13
14const FILE_PROGRESS_TEMPLATE: &str =
16 "{msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({percent}%)";
17
18const SPINNER_TEMPLATE: &str = "{spinner:.cyan} {msg}";
20
21const PROGRESS_CHARS: &str = "█▓░";
23
24pub fn file_progress(size: u64, message: &str) -> ProgressBar {
28 let pb = if interactive::is_non_interactive() {
29 ProgressBar::hidden()
30 } else {
31 ProgressBar::new(size)
32 };
33
34 pb.set_style(
35 ProgressStyle::default_bar()
36 .template(FILE_PROGRESS_TEMPLATE)
37 .expect("hardcoded progress template is valid")
38 .progress_chars(PROGRESS_CHARS),
39 );
40 pb.set_message(message.to_string());
41 pb
42}
43
44pub fn spinner(message: &str) -> ProgressBar {
48 let pb = if interactive::is_non_interactive() {
49 ProgressBar::hidden()
50 } else {
51 ProgressBar::new_spinner()
52 };
53
54 pb.set_style(
55 ProgressStyle::default_spinner()
56 .template(SPINNER_TEMPLATE)
57 .expect("hardcoded progress template is valid"),
58 );
59 pb.set_message(message.to_string());
60
61 if !interactive::is_non_interactive() {
62 pb.enable_steady_tick(std::time::Duration::from_millis(100));
63 }
64
65 pb
66}
67
68pub fn item_progress(count: u64, message: &str) -> ProgressBar {
72 let pb = if interactive::is_non_interactive() {
73 ProgressBar::hidden()
74 } else {
75 ProgressBar::new(count)
76 };
77
78 pb.set_style(
79 ProgressStyle::default_bar()
80 .template("{msg} [{bar:40.cyan/blue}] {pos}/{len}")
81 .expect("hardcoded progress template is valid")
82 .progress_chars(PROGRESS_CHARS),
83 );
84 pb.set_message(message.to_string());
85 pb
86}
87
88pub struct ProgressGuard {
92 pb: ProgressBar,
93 abandon_on_drop: bool,
94}
95
96impl ProgressGuard {
97 pub fn new(pb: ProgressBar) -> Self {
99 Self {
100 pb,
101 abandon_on_drop: true,
102 }
103 }
104
105 pub fn finish(mut self, message: &str) {
110 self.abandon_on_drop = false;
111 self.pb.finish_with_message(message.to_string());
112 }
113
114 pub fn progress(&self) -> &ProgressBar {
116 &self.pb
117 }
118}
119
120impl Drop for ProgressGuard {
121 fn drop(&mut self) {
122 if self.abandon_on_drop {
123 self.pb.abandon();
124 }
125 }
126}
127
128impl std::ops::Deref for ProgressGuard {
129 type Target = ProgressBar;
130
131 fn deref(&self) -> &Self::Target {
132 &self.pb
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn reset_interactive_state() {
141 interactive::init(false, false);
142 interactive::MOCK_IS_TERMINAL.store(true, std::sync::atomic::Ordering::Relaxed);
143 }
144
145 fn set_non_interactive() {
146 interactive::init(true, false);
147 }
148
149 #[test]
152 fn test_file_progress_creation() {
153 reset_interactive_state();
154 let pb = file_progress(1000, "Uploading");
155 assert_eq!(pb.length(), Some(1000));
156 }
157
158 #[test]
159 fn test_file_progress_zero_size() {
160 reset_interactive_state();
161 let pb = file_progress(0, "Uploading empty file");
162 assert_eq!(pb.length(), Some(0));
163 }
164
165 #[test]
166 fn test_file_progress_large_size() {
167 reset_interactive_state();
168 let pb = file_progress(u64::MAX, "Uploading huge file");
169 assert_eq!(pb.length(), Some(u64::MAX));
170 }
171
172 #[test]
173 fn test_file_progress_non_interactive() {
174 set_non_interactive();
175 let pb = file_progress(1000, "Uploading");
176 assert!(pb.length().is_none());
179 reset_interactive_state();
180 }
181
182 #[test]
185 fn test_spinner_creation() {
186 reset_interactive_state();
187 let pb = spinner("Processing...");
188 assert!(pb.length().is_none()); }
190
191 #[test]
192 fn test_spinner_non_interactive() {
193 set_non_interactive();
194 let pb = spinner("Processing...");
195 assert!(pb.length().is_none());
196 reset_interactive_state();
197 }
198
199 #[test]
200 fn test_spinner_empty_message() {
201 reset_interactive_state();
202 let pb = spinner("");
203 assert!(pb.length().is_none());
204 }
205
206 #[test]
209 fn test_item_progress_creation() {
210 reset_interactive_state();
211 let pb = item_progress(10, "Processing items");
212 assert_eq!(pb.length(), Some(10));
213 }
214
215 #[test]
216 fn test_item_progress_single_item() {
217 reset_interactive_state();
218 let pb = item_progress(1, "Processing item");
219 assert_eq!(pb.length(), Some(1));
220 }
221
222 #[test]
223 fn test_item_progress_zero_items() {
224 reset_interactive_state();
225 let pb = item_progress(0, "No items");
226 assert_eq!(pb.length(), Some(0));
227 }
228
229 #[test]
230 fn test_item_progress_non_interactive() {
231 set_non_interactive();
232 let pb = item_progress(10, "Processing items");
233 assert!(pb.length().is_none());
235 reset_interactive_state();
236 }
237
238 #[test]
241 fn test_progress_guard_finish() {
242 reset_interactive_state();
243 let pb = file_progress(100, "Test");
244 let guard = ProgressGuard::new(pb);
245 guard.finish("Done");
246 }
248
249 #[test]
250 fn test_progress_guard_abandon_on_drop() {
251 reset_interactive_state();
252 let pb = file_progress(100, "Test");
253 let _guard = ProgressGuard::new(pb);
254 }
256
257 #[test]
258 fn test_progress_guard_deref() {
259 reset_interactive_state();
260 let pb = file_progress(100, "Test");
261 let guard = ProgressGuard::new(pb);
262 assert_eq!(guard.length(), Some(100));
264 }
265
266 #[test]
267 fn test_progress_guard_progress_method() {
268 reset_interactive_state();
269 let pb = file_progress(100, "Test");
270 let guard = ProgressGuard::new(pb);
271 assert_eq!(guard.progress().length(), Some(100));
273 }
274
275 #[test]
276 fn test_progress_guard_increment() {
277 reset_interactive_state();
278 let pb = file_progress(100, "Test");
279 let guard = ProgressGuard::new(pb);
280 guard.inc(50);
281 assert_eq!(guard.position(), 50);
282 }
283
284 #[test]
285 fn test_progress_guard_set_position() {
286 reset_interactive_state();
287 let pb = file_progress(100, "Test");
288 let guard = ProgressGuard::new(pb);
289 guard.set_position(75);
290 assert_eq!(guard.position(), 75);
291 }
292
293 #[test]
296 fn test_progress_chars_length() {
297 assert_eq!(PROGRESS_CHARS.len(), 9);
299 assert_eq!(PROGRESS_CHARS.chars().count(), 3);
300 }
301
302 #[test]
303 fn test_file_template_contains_bar() {
304 assert!(FILE_PROGRESS_TEMPLATE.contains("{bar:"));
305 }
306
307 #[test]
308 fn test_spinner_template_contains_spinner() {
309 assert!(SPINNER_TEMPLATE.contains("{spinner"));
310 }
311}