multi_spinner/
lib.rs

1pub mod spinners;
2
3use std::{
4    sync::{Arc, Mutex},
5    thread::{self},
6    time::Duration,
7};
8
9use anyhow::Result;
10use crossterm::{
11    cursor::MoveToRow,
12    style::Print,
13    terminal::{Clear, ClearType},
14    ExecutableCommand,
15};
16
17use crate::spinners::Animation;
18
19/// # Example:
20/// ```rust
21///use multi_spinner::{Spinner, spinners::Animation};
22///use std::{ thread::{self}, time::Duration, sync::{Arc, Mutex}, };
23///
24///fn main() {
25///    let stdout = Arc::new(Mutex::new(std::io::stdout()));
26///    let files = ["file1", "file2", "file3", "file4", "file5"];
27///    let handles = files.iter().enumerate().map(|(i, file)|{
28///        let file = file.to_owned();
29///        let stdout = stdout.clone(); 
30///        thread::spawn(move ||{
31///
32///            let mut spinner = Spinner::builder()
33///                .spinner(Animation::Bars10(0))
34///                .msg(format!("downloading {file}\n"))
35///                .row(i)
36///                .stdout(stdout)
37///                .build();
38///
39///            spinner.start();
40///            thread::sleep(Duration::from_secs(3));
41///            spinner.stop().expect("stopped");
42///        })
43///    }).collect::<Vec<_>>();
44///
45///    for handle in handles {
46///        let () = handle.join().expect("join thread");
47///    }
48///}
49///```
50#[must_use]
51#[derive(Clone)]
52pub struct Spinner {
53    spinner: Animation,
54    row: usize,
55    msg: String,
56    active: Arc<Mutex<bool>>,
57    stdout: Arc<Mutex<std::io::Stdout>>,
58    handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
59}
60
61impl Spinner {
62    pub fn builder() -> SpinnerBuilder {
63        SpinnerBuilder::default()
64    }
65
66    /// Starts the spinner in a separate thread.
67    ///
68    /// # Panics
69    /// Will panic if active, stdout or handle is poisoned.
70    pub fn start(&mut self) {
71        *self.active.lock().expect("lock active's mutex") = true;
72
73        let active = self.active.clone();
74        let mut spinner = self.clone();
75
76        let handle = thread::spawn(move || {
77            while *active.lock().expect("lock active's mutex") {
78                spinner.spin().expect("spin");
79                thread::sleep(Duration::from_millis(65_u64));
80            }
81        });
82
83        *self.handle.lock().expect("lock handle's mutex") = Some(handle);
84    }
85
86    /// # Panics
87    /// Will panic if stdout is poisoned.
88    ///
89    /// # Errors
90    /// Fails if crossterm fails to execute or if conversion of usize to u16 fails.
91    fn spin(&mut self) -> Result<()> {
92        let mut stdout = self.stdout.lock().expect("lock stdout's mutex");
93        let row = u16::try_from(self.row)?;
94        stdout.execute(MoveToRow(row))?;
95        stdout.execute(Clear(ClearType::CurrentLine))?;
96
97        let frame = self.spinner.next_frame();
98        let msg = &self.msg;
99
100        stdout.execute(MoveToRow(row))?;
101        stdout.execute(Print(format!("{frame} {msg}")))?;
102        drop(stdout);
103
104        Ok(())
105    }
106
107    /// Stops the spinner
108    ///
109    /// # Panics
110    /// Panics if either active, handle or stdout is poisoned or if joining thread panics.
111    ///
112    /// # Errors
113    /// Fails if crossterm fails to execute or if conversion of usize to u16 fails.
114    pub fn stop(&mut self) -> Result<()> {
115        *self.active.lock().expect("lock active's mutex") = false;
116
117        let handle = self
118            .handle
119            .clone()
120            .lock()
121            .expect("lock handle's mutex")
122            .take();
123
124        if let Some(handle) = handle {
125            let () = handle.join().expect("join spinner thread");
126        }
127
128        let mut stdout = self.stdout.lock().expect("lock stdout's mutex");
129        let row = u16::try_from(self.row)?;
130        stdout.execute(MoveToRow(row))?;
131        stdout.execute(Clear(ClearType::CurrentLine))?;
132        drop(stdout);
133
134        Ok(())
135    }
136}
137
138#[must_use]
139#[derive(Clone)]
140pub struct SpinnerBuilder {
141    spinner: Animation,
142    row: usize,
143    msg: String,
144    active: Arc<Mutex<bool>>,
145    stdout: Arc<Mutex<std::io::Stdout>>,
146    handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
147}
148
149impl Default for SpinnerBuilder {
150    fn default() -> Self {
151        Self {
152            spinner: Animation::Dots2(0),
153            row: 0,
154            msg: "Loading".to_owned(),
155            active: Arc::new(Mutex::new(false)),
156            stdout: Arc::new(Mutex::new(std::io::stdout())),
157            handle: Arc::new(Mutex::new(None)),
158        }
159    }
160}
161
162impl SpinnerBuilder {
163    /// If stdout is not provided, it will default to `std::io::stdout()`.
164    pub fn stdout(mut self, stdout: Arc<Mutex<std::io::Stdout>>) -> Self {
165        self.stdout = stdout;
166        self
167    }
168
169    /// Message to display after the spinner.
170    pub fn msg(mut self, msg: String) -> Self {
171        self.msg = msg + "\n";
172        self
173    }
174
175    /// Terminal row to display the spinner on.
176    /// Defaults to 0.
177    pub const fn row(mut self, row: usize) -> Self {
178        self.row = row;
179        self
180    }
181
182    /// Animation to use for the spinner.
183    /// Defaults to `Animation::Dots2(0)`.
184    pub const fn spinner(mut self, spinner: Animation) -> Self {
185        self.spinner = spinner;
186        self
187    }
188
189    /// Builds and starts the spinner.
190    pub fn start(self) -> Spinner {
191        let mut spinner = self.build();
192        spinner.start();
193        spinner
194    }
195
196    /// Builds the spinner.
197    pub fn build(self) -> Spinner {
198        Spinner {
199            spinner: self.spinner,
200            row: self.row,
201            msg: self.msg,
202            active: self.active,
203            stdout: self.stdout,
204            handle: self.handle,
205        }
206    }
207}