composition_cli/display/
spinner.rs

1use std::{
2    io::{self, Write},
3    sync::{
4        Arc, Mutex,
5        atomic::{AtomicBool, Ordering},
6    },
7    thread::{self, JoinHandle},
8    time::Duration,
9};
10
11static SPINNER: Mutex<Option<SpinnerState>> = Mutex::new(None);
12
13struct SpinnerState {
14    running: Arc<AtomicBool>,
15    handle: Option<JoinHandle<()>>,
16}
17
18pub fn start(message: &str) {
19    let mut spinner_guard = SPINNER.lock().unwrap();
20
21    if spinner_guard.is_some() {
22        drop(spinner_guard);
23        end();
24        spinner_guard = SPINNER.lock().unwrap();
25    }
26
27    let running = Arc::new(AtomicBool::new(true));
28    let running_clone = running.clone();
29    let message = message.to_string();
30
31    let handle = thread::spawn(move || {
32        let spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
33        let mut index = 0;
34
35        while running_clone.load(Ordering::SeqCst) {
36            print!("\r{} {}", spinner_chars[index], message);
37            io::stdout().flush().unwrap();
38            index = (index + 1) % spinner_chars.len();
39            thread::sleep(Duration::from_millis(100));
40        }
41
42        print!("\r{} \r", " ".repeat(message.len() + 2));
43        io::stdout().flush().unwrap();
44    });
45
46    *spinner_guard = Some(SpinnerState {
47        running,
48        handle: Some(handle),
49    });
50}
51
52pub fn end() {
53    let mut spinner_guard = SPINNER.lock().unwrap();
54
55    if let Some(state) = spinner_guard.take() {
56        state.running.store(false, Ordering::SeqCst);
57
58        if let Some(handle) = state.handle {
59            drop(spinner_guard); // Release the lock before joining
60            let _ = handle.join();
61        }
62    }
63}