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
47fn run_spinner_loop(stop: &AtomicBool, msg: &str) {
48 let mut i = 0usize;
49 let mut stderr = std::io::stderr();
50 while !stop.load(Ordering::Relaxed) {
51 let _ = write!(
52 stderr,
53 "\r{} {msg}",
54 SPINNER_FRAMES[i % SPINNER_FRAMES.len()]
55 );
56 let _ = stderr.flush();
57 i += 1;
58 std::thread::sleep(std::time::Duration::from_millis(80));
59 }
60 let _ = write!(stderr, "\r{}\r", " ".repeat(msg.len() + 3));
62 let _ = stderr.flush();
63}
64
65pub struct Spinner {
78 stop: Arc<AtomicBool>,
79 handle: Option<std::thread::JoinHandle<()>>,
80}
81
82impl Spinner {
83 pub fn new(message: &str) -> Self {
88 let stop = Arc::new(AtomicBool::new(false));
89
90 if is_quiet() || !std::io::stderr().is_terminal() {
91 return Self { stop, handle: None };
92 }
93
94 let stop_clone = stop.clone();
95 let msg = message.to_string();
96
97 let handle = std::thread::spawn(move || run_spinner_loop(&stop_clone, &msg));
98
99 Self {
100 stop,
101 handle: Some(handle),
102 }
103 }
104
105 pub fn finish(self) {
107 drop(self);
108 }
109}
110
111impl Drop for Spinner {
112 fn drop(&mut self) {
113 self.stop.store(true, Ordering::Relaxed);
114 if let Some(h) = self.handle.take() {
115 let _ = h.join();
116 }
117 }
118}