libthrob/
lib.rs

1use std::{io::{self, Write}, sync::{atomic::{AtomicBool, AtomicUsize, Ordering}, Arc}, thread::{self, JoinHandle}, time::Duration};
2
3use crossterm::{cursor, execute, terminal};
4
5pub struct Throbber {
6    frame: Arc<AtomicUsize>,
7    animation_length: Arc<usize>,
8    animation: Arc<Vec<char>>,
9    frame_time: Arc<f32>,
10    run: Arc<AtomicBool>,
11    killed: Arc<AtomicBool>,
12    join_handle: Option<JoinHandle<()>>
13}
14
15impl Throbber {
16    fn spawn(animation: Arc<Vec<char>>, animation_length: Arc<usize>, frame: Arc<AtomicUsize>, frame_time: Arc<f32>, killed: Arc<AtomicBool>, run: Arc<AtomicBool>) -> JoinHandle<()> {
17        thread::spawn(move || {
18            loop {
19                if killed.load(Ordering::SeqCst) {
20                    break;
21                }
22
23                if run.load(Ordering::SeqCst) {
24                    let mut stdout = io::stdout();
25
26                    execute!(stdout, cursor::SavePosition {}).expect("Failed to save the cursor's position");
27                    print!("{}", animation.get(frame.load(Ordering::SeqCst)).unwrap());
28                    stdout.flush().expect("Failed to flush stdout");
29                    execute!(stdout, cursor::RestorePosition {}).expect("Failed to restore the cursor's position");
30                    frame.store((frame.load(Ordering::SeqCst) + 1) % animation_length.as_ref().clone(), Ordering::SeqCst);
31
32                    thread::sleep(Duration::from_secs_f32(frame_time.as_ref().clone()));
33                }
34            }
35        })
36    }
37
38    pub fn custom(animation: Vec<char>, frame_time: f32) -> Self {
39        let mut new = Throbber {
40            frame: Arc::new(AtomicUsize::new(0)),
41            animation_length: Arc::new(animation.len()),
42            animation: Arc::new(animation),
43            run: Arc::new(AtomicBool::new(false)),
44            frame_time: Arc::new(frame_time),
45            killed: Arc::new(AtomicBool::new(false)),
46            join_handle: None
47        };
48
49        new.join_handle = Some(Self::spawn(new.animation.clone(), new.animation_length.clone(), new.frame.clone(), new.frame_time.clone(), new.killed.clone(), new.run.clone()));
50        new
51    }
52
53    pub fn classic(frame_time: f32) -> Self {
54        let mut new = Throbber {
55            frame: Arc::new(AtomicUsize::new(0)),
56            animation_length: Arc::new(4),
57            animation: Arc::new(['|', '/', '-', '\\'].to_vec()),
58            run: Arc::new(AtomicBool::new(false)),
59            frame_time: Arc::new(frame_time),
60            killed: Arc::new(AtomicBool::new(false)),
61            join_handle: None
62        };
63
64        new.join_handle = Some(Self::spawn(new.animation.clone(), new.animation_length.clone(), new.frame.clone(), new.frame_time.clone(), new.killed.clone(), new.run.clone()));
65        new
66    }
67
68    pub fn braille(frame_time: f32) -> Self {
69        let mut new = Throbber {
70            frame: Arc::new(AtomicUsize::new(0)),
71            animation_length: Arc::new(8),
72            animation: Arc::new(['⠇', '⠋', '⠙', '⠸', '⢰', '⣠', '⣄', '⡆'].to_vec()),
73            run: Arc::new(AtomicBool::new(false)),
74            frame_time: Arc::new(frame_time),
75            killed: Arc::new(AtomicBool::new(false)),
76            join_handle: None
77        };
78
79        new.join_handle = Some(Self::spawn(new.animation.clone(), new.animation_length.clone(), new.frame.clone(), new.frame_time.clone(), new.killed.clone(), new.run.clone()));
80        new
81    }
82
83    /// It's recommended to use raw mode so the animation can't be messed up by users.
84    pub fn start(&self, raw_mode: bool) {
85        if raw_mode {
86            terminal::enable_raw_mode().expect("Failed to enable raw mode");
87        }
88
89        self.frame.store(0, Ordering::SeqCst);
90        self.run.store(true, Ordering::SeqCst);
91    }
92    
93    /// It's recommended to use raw mode so the animation can't be messed up by users. If you
94    /// started the throbber with raw mode you for sure want to disable it with raw mode.
95    pub fn stop(&self, raw_mode: bool) {
96        if raw_mode {
97            terminal::disable_raw_mode().expect("Failed to disable raw mode");
98        }
99
100        self.run.store(false, Ordering::SeqCst);
101    }
102
103    /// This automatically runs once out of scope, but in case you want to kill the thread early,
104    /// call this
105    pub fn kill_thread(&self) {
106        self.killed.store(true, Ordering::SeqCst);
107    }
108}
109
110impl Drop for Throbber {
111    fn drop(&mut self) {
112        self.kill_thread();
113    }
114}
115