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) {
8 QUIET.store(quiet, Ordering::Relaxed);
9}
10
11pub fn is_quiet() -> bool {
12 QUIET.load(Ordering::Relaxed)
13}
14
15#[macro_export]
19macro_rules! progress {
20 ($($arg:tt)*) => {
21 if !$crate::output::is_quiet() {
22 eprintln!($($arg)*);
23 }
24 };
25}
26
27#[macro_export]
31macro_rules! progress_inline {
32 ($($arg:tt)*) => {
33 if !$crate::output::is_quiet() {
34 eprint!($($arg)*);
35 }
36 };
37}
38
39const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
44
45fn run_spinner_loop(stop: &AtomicBool, msg: &str) {
46 let mut i = 0usize;
47 let mut stderr = std::io::stderr();
48 while !stop.load(Ordering::Relaxed) {
49 let _ = write!(
50 stderr,
51 "\r{} {msg}",
52 SPINNER_FRAMES[i % SPINNER_FRAMES.len()]
53 );
54 let _ = stderr.flush();
55 i += 1;
56 std::thread::sleep(std::time::Duration::from_millis(80));
57 }
58 let _ = write!(stderr, "\r{}\r", " ".repeat(msg.len() + 3));
60 let _ = stderr.flush();
61}
62
63pub struct Spinner {
76 stop: Arc<AtomicBool>,
77 handle: Option<std::thread::JoinHandle<()>>,
78}
79
80impl Spinner {
81 pub fn new(message: &str) -> Self {
82 let stop = Arc::new(AtomicBool::new(false));
83
84 if is_quiet() || !std::io::stderr().is_terminal() {
85 return Self { stop, handle: None };
86 }
87
88 let stop_clone = stop.clone();
89 let msg = message.to_string();
90
91 let handle = std::thread::spawn(move || run_spinner_loop(&stop_clone, &msg));
92
93 Self {
94 stop,
95 handle: Some(handle),
96 }
97 }
98
99 pub fn finish(self) {
101 drop(self);
102 }
103}
104
105impl Drop for Spinner {
106 fn drop(&mut self) {
107 self.stop.store(true, Ordering::Relaxed);
108 if let Some(h) = self.handle.take() {
109 let _ = h.join();
110 }
111 }
112}