1use std::io::{IsTerminal, Write};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4
5static QUIET: AtomicBool = AtomicBool::new(false);
6
7pub fn set_quiet(quiet: bool) {
9 QUIET.store(quiet, Ordering::Relaxed);
10}
11
12pub fn is_quiet() -> bool {
14 QUIET.load(Ordering::Relaxed)
15}
16
17#[macro_export]
21macro_rules! progress {
22 ($($arg:tt)*) => {
23 if !$crate::output::is_quiet() {
24 eprintln!($($arg)*);
25 }
26 };
27}
28
29#[macro_export]
33macro_rules! progress_inline {
34 ($($arg:tt)*) => {
35 if !$crate::output::is_quiet() {
36 eprint!($($arg)*);
37 }
38 };
39}
40
41const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
46
47pub struct Spinner {
60 stop: Arc<AtomicBool>,
61 handle: Option<std::thread::JoinHandle<()>>,
62}
63
64impl Spinner {
65 pub fn new(message: &str) -> Self {
70 let stop = Arc::new(AtomicBool::new(false));
71
72 if is_quiet() || !std::io::stderr().is_terminal() {
73 return Self { stop, handle: None };
74 }
75
76 let stop_clone = stop.clone();
77 let msg = message.to_string();
78
79 let handle = std::thread::spawn(move || {
80 let mut i = 0;
81 let mut stderr = std::io::stderr();
82 while !stop_clone.load(Ordering::Relaxed) {
83 let _ = write!(
84 stderr,
85 "\r{} {msg}",
86 SPINNER_FRAMES[i % SPINNER_FRAMES.len()]
87 );
88 let _ = stderr.flush();
89 i += 1;
90 std::thread::sleep(std::time::Duration::from_millis(80));
91 }
92 let _ = write!(stderr, "\r{}\r", " ".repeat(msg.len() + 3));
94 let _ = stderr.flush();
95 });
96
97 Self {
98 stop,
99 handle: Some(handle),
100 }
101 }
102
103 pub fn finish(self) {
105 drop(self);
106 }
107}
108
109impl Drop for Spinner {
110 fn drop(&mut self) {
111 self.stop.store(true, Ordering::Relaxed);
112 if let Some(h) = self.handle.take() {
113 let _ = h.join();
114 }
115 }
116}