1use super::SpinnerHandle;
36use std::sync::{Arc, Mutex};
37use std::time::Duration;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum DisplayMessageType {
42 Info,
43 Warning,
44 Error,
45 Progress,
46 Success,
47 Action, Metric, Status, }
51
52#[derive(Clone, Copy)]
54pub struct IconConfig {
55 info: &'static str,
56 warning: &'static str,
57 error: &'static str,
58 progress: &'static str,
59 success: &'static str,
60 action: &'static str,
61 metric: &'static str,
62 status: &'static str,
63 debug: &'static str,
64}
65
66impl Default for IconConfig {
67 fn default() -> Self {
68 Self {
69 info: "âšī¸",
70 warning: "â ī¸",
71 error: "â",
72 progress: "đ",
73 success: "â
",
74 action: "đ",
75 metric: "đ",
76 status: "đ",
77 debug: "đ",
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
84pub enum VerbosityLevel {
85 Quiet = 0, Normal = 1, Verbose = 2, Debug = 3, Trace = 4, }
91
92impl VerbosityLevel {
93 pub fn from_args(verbosity_count: u8, quiet: bool) -> Self {
95 if quiet {
96 VerbosityLevel::Quiet
97 } else {
98 match verbosity_count {
99 0 => VerbosityLevel::Normal,
100 1 => VerbosityLevel::Verbose,
101 2 => VerbosityLevel::Debug,
102 _ => VerbosityLevel::Trace,
103 }
104 }
105 }
106}
107
108pub trait ProgressDisplay: Send + Sync {
110 fn info(&self, message: &str);
112
113 fn warning(&self, message: &str);
115
116 fn error(&self, message: &str);
118
119 fn progress(&self, message: &str);
121
122 fn success(&self, message: &str);
124
125 fn action(&self, message: &str);
127
128 fn metric(&self, label: &str, value: &str);
130
131 fn status(&self, message: &str);
133
134 fn start_spinner(&self, message: &str) -> Box<dyn SpinnerHandle>;
136
137 fn iteration_start(&self, current: u32, total: u32);
139
140 fn iteration_end(&self, current: u32, duration: Duration, success: bool);
142
143 fn step_start(&self, step: u32, total: u32, description: &str);
145
146 fn step_end(&self, step: u32, success: bool);
148
149 fn command_output(&self, output: &str, verbosity: VerbosityLevel);
151
152 fn debug_output(&self, message: &str, min_verbosity: VerbosityLevel);
154
155 fn verbosity(&self) -> VerbosityLevel;
157}
158
159pub struct ProgressDisplayImpl {
161 verbosity: VerbosityLevel,
162 use_unicode: bool,
163 icons: IconConfig,
164}
165
166impl Default for ProgressDisplayImpl {
167 fn default() -> Self {
168 Self::new(VerbosityLevel::Normal)
169 }
170}
171
172impl ProgressDisplayImpl {
173 pub fn new(verbosity: VerbosityLevel) -> Self {
174 let use_unicode = Self::supports_unicode();
176
177 Self {
178 verbosity,
179 use_unicode,
180 icons: IconConfig::default(),
181 }
182 }
183
184 pub fn from_args(verbosity_count: u8, quiet: bool) -> Self {
186 let verbosity = VerbosityLevel::from_args(verbosity_count, quiet);
187 Self::new(verbosity)
188 }
189
190 fn supports_unicode() -> bool {
192 if let Ok(lang) = std::env::var("LANG") {
194 if lang.contains("UTF-8") || lang.contains("utf8") {
195 return true;
196 }
197 }
198 if let Ok(lc_all) = std::env::var("LC_ALL") {
199 if lc_all.contains("UTF-8") || lc_all.contains("utf8") {
200 return true;
201 }
202 }
203 !cfg!(windows)
205 }
206
207 fn box_chars(&self) -> BoxChars {
209 if self.use_unicode {
210 BoxChars::unicode()
211 } else {
212 BoxChars::ascii()
213 }
214 }
215
216 fn format_duration(duration: Duration) -> String {
218 let secs = duration.as_secs();
219 let millis = duration.subsec_millis();
220
221 if secs >= 60 {
222 let mins = secs / 60;
223 let secs = secs % 60;
224 format!("{mins}m {secs}s")
225 } else if secs > 0 {
226 format!("{secs}.{millis:03}s")
227 } else {
228 format!("{millis}ms")
229 }
230 }
231}
232
233struct BoxChars {
235 horizontal: char,
236 vertical: char,
237 top_left: char,
238 top_right: char,
239 bottom_left: char,
240 bottom_right: char,
241}
242
243impl BoxChars {
244 fn unicode() -> Self {
245 Self {
246 horizontal: 'â',
247 vertical: 'â',
248 top_left: 'â',
249 top_right: 'â',
250 bottom_left: 'â',
251 bottom_right: 'â',
252 }
253 }
254
255 fn ascii() -> Self {
256 Self {
257 horizontal: '=',
258 vertical: '|',
259 top_left: '+',
260 top_right: '+',
261 bottom_left: '+',
262 bottom_right: '+',
263 }
264 }
265}
266
267impl ProgressDisplay for ProgressDisplayImpl {
268 fn info(&self, message: &str) {
269 if self.verbosity >= VerbosityLevel::Normal {
270 println!("{} {message}", self.icons.info);
271 }
272 }
273
274 fn warning(&self, message: &str) {
275 if self.verbosity >= VerbosityLevel::Normal {
276 eprintln!("{} {message}", self.icons.warning);
277 }
278 }
279
280 fn error(&self, message: &str) {
281 eprintln!("{} {message}", self.icons.error);
283 }
284
285 fn progress(&self, message: &str) {
286 if self.verbosity >= VerbosityLevel::Normal {
287 println!("{} {message}", self.icons.progress);
288 }
289 }
290
291 fn success(&self, message: &str) {
292 if self.verbosity >= VerbosityLevel::Normal {
293 println!("{} {message}", self.icons.success);
294 }
295 }
296
297 fn action(&self, message: &str) {
298 if self.verbosity >= VerbosityLevel::Normal {
299 println!("{} {message}", self.icons.action);
300 }
301 }
302
303 fn metric(&self, label: &str, value: &str) {
304 if self.verbosity >= VerbosityLevel::Normal {
305 println!("{} {label}: {value}", self.icons.metric);
306 }
307 }
308
309 fn status(&self, message: &str) {
310 if self.verbosity >= VerbosityLevel::Normal {
311 println!("{} {message}", self.icons.status);
312 }
313 }
314
315 fn start_spinner(&self, message: &str) -> Box<dyn SpinnerHandle> {
316 if self.verbosity >= VerbosityLevel::Normal {
317 println!("âŗ {message}");
318 }
319 Box::new(SimpleSpinnerHandle::new(self.verbosity, self.icons))
320 }
321
322 fn iteration_start(&self, current: u32, total: u32) {
323 if self.verbosity >= VerbosityLevel::Normal {
324 let chars = self.box_chars();
325 let width = 60;
326 let title = format!(" ITERATION {current}/{total} ");
327 let padding = (width - title.len()) / 2;
328
329 println!();
330 println!(
331 "{}{}{}",
332 chars.top_left,
333 std::iter::repeat_n(chars.horizontal, width).collect::<String>(),
334 chars.top_right
335 );
336 println!(
337 "{}{:padding$}{}{:padding$}{}",
338 chars.vertical,
339 "",
340 title,
341 "",
342 chars.vertical,
343 padding = padding
344 );
345 println!(
346 "{}{}{}",
347 chars.bottom_left,
348 std::iter::repeat_n(chars.horizontal, width).collect::<String>(),
349 chars.bottom_right
350 );
351 println!();
352 }
353 }
354
355 fn iteration_end(&self, current: u32, duration: Duration, success: bool) {
356 if self.verbosity >= VerbosityLevel::Normal {
357 let duration_str = Self::format_duration(duration);
358 let status = if success {
359 format!("{} Success", self.icons.success)
360 } else {
361 format!("{} Failed", self.icons.error)
362 };
363
364 println!();
365 println!("ââ Iteration {current} Summary âââââââââââââââââââââââââââââââââââââââ");
366 println!("â Duration: {:<49}â", duration_str);
367 println!("â Status: {:<51}â", status);
368 println!("ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
369 println!();
370 }
371 }
372
373 fn step_start(&self, step: u32, total: u32, description: &str) {
374 if self.verbosity >= VerbosityLevel::Verbose {
375 println!("[Step {step}/{total}] {description}");
376 }
377 }
378
379 fn step_end(&self, step: u32, success: bool) {
380 if self.verbosity >= VerbosityLevel::Verbose {
381 let status = if success {
382 self.icons.success
383 } else {
384 self.icons.error
385 };
386 println!("[Step {step}] {status}");
387 }
388 }
389
390 fn command_output(&self, output: &str, verbosity: VerbosityLevel) {
391 if self.verbosity >= verbosity && !output.trim().is_empty() {
392 println!("{output}");
393 }
394 }
395
396 fn debug_output(&self, message: &str, min_verbosity: VerbosityLevel) {
397 if self.verbosity >= min_verbosity {
398 println!("{} {message}", self.icons.debug);
399 }
400 }
401
402 fn verbosity(&self) -> VerbosityLevel {
403 self.verbosity
404 }
405}
406
407struct SimpleSpinnerHandle {
409 active: Arc<Mutex<bool>>,
410 verbosity: VerbosityLevel,
411 icons: IconConfig,
412}
413
414impl SimpleSpinnerHandle {
415 fn new(verbosity: VerbosityLevel, icons: IconConfig) -> Self {
416 Self {
417 active: Arc::new(Mutex::new(true)),
418 verbosity,
419 icons,
420 }
421 }
422}
423
424impl SpinnerHandle for SimpleSpinnerHandle {
425 fn update_message(&mut self, message: &str) {
426 if *self.active.lock().unwrap() && self.verbosity >= VerbosityLevel::Normal {
427 println!("âŗ {message}");
428 }
429 }
430
431 fn success(&mut self, message: &str) {
432 *self.active.lock().unwrap() = false;
433 if self.verbosity >= VerbosityLevel::Normal {
434 println!("{} {message}", self.icons.success);
435 }
436 }
437
438 fn fail(&mut self, message: &str) {
439 *self.active.lock().unwrap() = false;
440 if self.verbosity >= VerbosityLevel::Normal {
441 println!("{} {message}", self.icons.error);
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 pub struct MockProgressDisplay {
451 messages: Arc<Mutex<Vec<String>>>,
452 verbosity: VerbosityLevel,
453 }
454
455 impl MockProgressDisplay {
456 pub fn new() -> Self {
457 Self {
458 messages: Arc::new(Mutex::new(Vec::new())),
459 verbosity: VerbosityLevel::Normal,
460 }
461 }
462
463 pub fn get_messages(&self) -> Vec<String> {
464 self.messages.lock().unwrap().clone()
465 }
466 }
467
468 impl ProgressDisplay for MockProgressDisplay {
469 fn info(&self, message: &str) {
470 self.messages
471 .lock()
472 .unwrap()
473 .push(format!("INFO: {message}"));
474 }
475
476 fn warning(&self, message: &str) {
477 self.messages
478 .lock()
479 .unwrap()
480 .push(format!("WARN: {message}"));
481 }
482
483 fn error(&self, message: &str) {
484 self.messages
485 .lock()
486 .unwrap()
487 .push(format!("ERROR: {message}"));
488 }
489
490 fn progress(&self, message: &str) {
491 self.messages
492 .lock()
493 .unwrap()
494 .push(format!("PROGRESS: {message}"));
495 }
496
497 fn success(&self, message: &str) {
498 self.messages
499 .lock()
500 .unwrap()
501 .push(format!("SUCCESS: {message}"));
502 }
503
504 fn action(&self, message: &str) {
505 self.messages
506 .lock()
507 .unwrap()
508 .push(format!("ACTION: {message}"));
509 }
510
511 fn metric(&self, label: &str, value: &str) {
512 self.messages
513 .lock()
514 .unwrap()
515 .push(format!("METRIC: {label}: {value}"));
516 }
517
518 fn status(&self, message: &str) {
519 self.messages
520 .lock()
521 .unwrap()
522 .push(format!("STATUS: {message}"));
523 }
524
525 fn start_spinner(&self, message: &str) -> Box<dyn SpinnerHandle> {
526 self.messages
527 .lock()
528 .unwrap()
529 .push(format!("SPINNER: {message}"));
530 Box::new(MockSpinnerHandle::new(self.messages.clone()))
531 }
532
533 fn iteration_start(&self, current: u32, total: u32) {
534 self.messages
535 .lock()
536 .unwrap()
537 .push(format!("ITERATION_START: {current}/{total}"));
538 }
539
540 fn iteration_end(&self, current: u32, duration: Duration, success: bool) {
541 self.messages
542 .lock()
543 .unwrap()
544 .push(format!("ITERATION_END: {current} {:?} {success}", duration));
545 }
546
547 fn step_start(&self, step: u32, total: u32, description: &str) {
548 self.messages
549 .lock()
550 .unwrap()
551 .push(format!("STEP_START: {step}/{total} {description}"));
552 }
553
554 fn step_end(&self, step: u32, success: bool) {
555 self.messages
556 .lock()
557 .unwrap()
558 .push(format!("STEP_END: {step} {success}"));
559 }
560
561 fn command_output(&self, output: &str, _verbosity: VerbosityLevel) {
562 self.messages
563 .lock()
564 .unwrap()
565 .push(format!("COMMAND_OUTPUT: {output}"));
566 }
567
568 fn debug_output(&self, message: &str, _min_verbosity: VerbosityLevel) {
569 self.messages
570 .lock()
571 .unwrap()
572 .push(format!("DEBUG: {message}"));
573 }
574
575 fn verbosity(&self) -> VerbosityLevel {
576 self.verbosity
577 }
578 }
579
580 struct MockSpinnerHandle {
581 messages: Arc<Mutex<Vec<String>>>,
582 }
583
584 impl MockSpinnerHandle {
585 fn new(messages: Arc<Mutex<Vec<String>>>) -> Self {
586 Self { messages }
587 }
588 }
589
590 impl SpinnerHandle for MockSpinnerHandle {
591 fn update_message(&mut self, message: &str) {
592 self.messages
593 .lock()
594 .unwrap()
595 .push(format!("SPINNER_UPDATE: {message}"));
596 }
597
598 fn success(&mut self, message: &str) {
599 self.messages
600 .lock()
601 .unwrap()
602 .push(format!("SPINNER_SUCCESS: {message}"));
603 }
604
605 fn fail(&mut self, message: &str) {
606 self.messages
607 .lock()
608 .unwrap()
609 .push(format!("SPINNER_FAIL: {message}"));
610 }
611 }
612
613 #[test]
614 fn test_mock_display() {
615 let display = MockProgressDisplay::new();
616
617 display.info("Test info");
618 display.warning("Test warning");
619 display.error("Test error");
620 display.progress("Test progress");
621 display.success("Test success");
622
623 let messages = display.get_messages();
624 assert_eq!(messages.len(), 5);
625 assert_eq!(messages[0], "INFO: Test info");
626 assert_eq!(messages[1], "WARN: Test warning");
627 assert_eq!(messages[2], "ERROR: Test error");
628 assert_eq!(messages[3], "PROGRESS: Test progress");
629 assert_eq!(messages[4], "SUCCESS: Test success");
630 }
631
632 #[test]
633 fn test_mock_spinner() {
634 let display = MockProgressDisplay::new();
635 let mut spinner = display.start_spinner("Starting");
636
637 spinner.update_message("Processing");
638 spinner.success("Done");
639
640 let messages = display.get_messages();
641 assert_eq!(messages.len(), 3);
642 assert_eq!(messages[0], "SPINNER: Starting");
643 assert_eq!(messages[1], "SPINNER_UPDATE: Processing");
644 assert_eq!(messages[2], "SPINNER_SUCCESS: Done");
645 }
646
647 #[test]
648 fn test_progress_display_info() {
649 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
650 display.info("Test info message");
652 }
654
655 #[test]
656 fn test_progress_display_warning() {
657 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
658 display.warning("Test warning");
660 }
662
663 #[test]
664 fn test_progress_display_error() {
665 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
666 display.error("Test error");
667 }
669
670 #[test]
671 fn test_spinner_lifecycle() {
672 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
673 let mut spinner = display.start_spinner("Loading...");
674 spinner.update_message("Still processing");
676 spinner.success("Done");
677 }
679
680 #[test]
681 fn test_progress_display_progress() {
682 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
683 display.progress("Test progress message");
684 }
686
687 #[test]
688 fn test_progress_display_success() {
689 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
690 display.success("Test success message");
691 }
693
694 #[test]
695 fn test_simple_spinner_handle_fail() {
696 let display = ProgressDisplayImpl::new(VerbosityLevel::Normal);
697 let mut spinner = display.start_spinner("Starting task");
698 spinner.fail("Failed to complete");
699 }
701
702 #[test]
703 fn test_verbosity_levels() {
704 let quiet = VerbosityLevel::from_args(0, true);
705 assert_eq!(quiet, VerbosityLevel::Quiet);
706
707 let normal = VerbosityLevel::from_args(0, false);
708 assert_eq!(normal, VerbosityLevel::Normal);
709
710 let verbose = VerbosityLevel::from_args(1, false);
711 assert_eq!(verbose, VerbosityLevel::Verbose);
712
713 let debug = VerbosityLevel::from_args(2, false);
714 assert_eq!(debug, VerbosityLevel::Debug);
715
716 let trace = VerbosityLevel::from_args(3, false);
717 assert_eq!(trace, VerbosityLevel::Trace);
718 }
719
720 #[test]
721 fn test_iteration_display() {
722 let display = MockProgressDisplay::new();
723 display.iteration_start(1, 10);
724 display.iteration_end(1, Duration::from_secs(5), true);
725
726 let messages = display.get_messages();
727 assert!(messages.contains(&"ITERATION_START: 1/10".to_string()));
728 assert!(messages.iter().any(|m| m.starts_with("ITERATION_END: 1")));
729 }
730
731 #[test]
732 fn test_step_display() {
733 let display = MockProgressDisplay::new();
734 display.step_start(1, 5, "Running tests");
735 display.step_end(1, true);
736
737 let messages = display.get_messages();
738 assert!(messages.contains(&"STEP_START: 1/5 Running tests".to_string()));
739 assert!(messages.contains(&"STEP_END: 1 true".to_string()));
740 }
741
742 #[test]
743 fn test_verbosity_filtering() {
744 let quiet_display = ProgressDisplayImpl::new(VerbosityLevel::Quiet);
745 quiet_display.info("Should not appear");
746 quiet_display.error("Should appear");
747 let verbose_display = ProgressDisplayImpl::new(VerbosityLevel::Verbose);
750 verbose_display.step_start(1, 3, "test");
751 }
753
754 #[test]
755 fn test_command_output_display() {
756 let display = MockProgressDisplay::new();
757 display.command_output("test output", VerbosityLevel::Debug);
758
759 let messages = display.get_messages();
760 assert!(messages.contains(&"COMMAND_OUTPUT: test output".to_string()));
761 }
762
763 #[test]
764 fn test_debug_output() {
765 let display = MockProgressDisplay::new();
766 display.debug_output("debug info", VerbosityLevel::Trace);
767
768 let messages = display.get_messages();
769 assert!(messages.contains(&"DEBUG: debug info".to_string()));
770 }
771
772 #[test]
773 fn test_format_duration() {
774 assert_eq!(
775 ProgressDisplayImpl::format_duration(Duration::from_millis(500)),
776 "500ms"
777 );
778 assert_eq!(
779 ProgressDisplayImpl::format_duration(Duration::from_secs(5)),
780 "5.000s"
781 );
782 assert_eq!(
783 ProgressDisplayImpl::format_duration(Duration::from_secs(65)),
784 "1m 5s"
785 );
786 }
787}