throbber/
lib.rs

1//! [![Crates.io](https://img.shields.io/crates/v/throbber)](https://crates.io/crates/throbber)
2//! [![docs.rs](https://docs.rs/throbber/badge.svg)](https://docs.rs/throbber)
3//! [![GitHub last commit](https://img.shields.io/github/last-commit/Treeniks/throbber)](https://github.com/Treeniks/throbber)
4//! [![License](https://img.shields.io/github/license/Treeniks/throbber)](https://github.com/Treeniks/throbber/blob/master/LICENSE)
5//!
6//! This crate serves as an alternative to [loading](https://crates.io/crates/loading). It is used to display a throbber animation in the terminal while other calculations are done in the main program.
7//!
8//! ![Throbber Preview](https://user-images.githubusercontent.com/56131826/109326392-68c28b00-7857-11eb-8e8d-dd576c868e7f.gif "Throbber Preview")
9//!
10//! # Usage
11//!
12//! Add this to your Cargo.toml:
13//!
14//! ```toml
15//! [dependencies]
16//! throbber = "1.0"
17//! ```
18//!
19//! To display a throbber animation, first create a [`Throbber`] object:
20//!
21//! ```rust
22//! use throbber::Throbber;
23//! let mut throbber = Throbber::default();
24//! ```
25//!
26//! You can also customize certain settings like the displayed animation and the displayed message:
27//!
28//! ```rust
29//! # use throbber::Throbber;
30//! let mut throbber = Throbber::default()
31//!     .message("calculating stuff")
32//!     .frames(&throbber::MOVE_EQ_F); // this crate comes with a few predefined animations
33//!                                    // see the Constants section
34//! ```
35//!
36//! Then you can simply call [`start`] wherever you want to start the animation and a _finish function_ like [`success`] where you want to stop it.
37//!
38//! ```rust
39//! # use throbber::Throbber;
40//! # let mut throbber = Throbber::default();
41//! throbber.start();
42//! // do calculations
43//! throbber.success("calculations successful!");
44//! ```
45//!
46//! After, you can call [`start`] or [`start_with_msg`] again to start the animation again.
47//! Setters are also provided, e.g. [`set_message`] and [`set_frames`]. This also works while an animation is running.
48//!
49//! ## Thread Lifetime
50//!
51//! The Throbber thread gets spawned on the first call to [`start`] or [`start_with_msg`]. After that, the thread only ever gets parked.
52//! If you want to end the thread, you must drop the Throbber object:
53//!
54//! ```rust
55//! # use throbber::Throbber;
56//! # let mut throbber = Throbber::default();
57//! drop(throbber);
58//! ```
59//!
60//! # Examples
61//!
62//! This is the example from the gif above:
63//!
64//! ```rust
65#![doc = include_str!("../examples/calculation.rs")]
66//! ```
67//!
68//! You can also keep track of progress with [`set_message`]:
69//!
70//! ```rust
71#![doc = include_str!("../examples/download.rs")]
72//! ```
73//!
74//! [`Throbber`]: Throbber
75//! [`start`]: Throbber::start
76//! [`start_with_msg`]: Throbber::start_with_msg
77//! [`set_message`]: Throbber::set_message
78//! [`set_frames`]: Throbber::set_frames
79//! [`success`]: Throbber::success
80
81use std::io::Write;
82use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
83use std::thread::{self, JoinHandle};
84use std::time::Duration;
85
86/// `⠋   ⠙   ⠹   ⠸   ⠼   ⠴   ⠦   ⠧   ⠇   ⠏`
87pub const DEFAULT_F: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
88/// `◐   ◓   ◑   ◒`
89pub const CIRCLE_F: [&str; 4] = ["◐", "◓", "◑", "◒"];
90/// `|   /   -   \`
91pub const ROTATE_F: [&str; 4] = ["|", "/", "-", "\\"];
92/// `[=  ]   [ = ]   [  =]   [ = ]`
93pub const MOVE_EQ_F: [&str; 4] = ["[=  ]", "[ = ]", "[  =]", "[ = ]"];
94/// `[-  ]   [ - ]   [  -]   [ - ]`
95pub const MOVE_MIN_F: [&str; 4] = ["[-  ]", "[ - ]", "[  -]", "[ - ]"];
96/// `[=    ]   [==   ]   [ ==  ]   [  == ]   [   ==]   [    =]   [   ==]   [  == ]   [ ==  ]   [==   ]`
97pub const MOVE_EQ_LONG_F: [&str; 10] = [
98    "[=    ]", "[==   ]", "[ ==  ]", "[  == ]", "[   ==]", "[    =]", "[   ==]", "[  == ]",
99    "[ ==  ]", "[==   ]",
100];
101/// `[-    ]   [--   ]   [ --  ]   [  -- ]   [   --]   [    -]   [   --]   [  -- ]   [ --  ]   [--   ]`
102pub const MOVE_MIN_LONG_F: [&str; 10] = [
103    "[-    ]", "[--   ]", "[ --  ]", "[  -- ]", "[   --]", "[    -]", "[   --]", "[  -- ]",
104    "[ --  ]", "[--   ]",
105];
106
107/// Representation of a throbber animation. It can start, succeed, fail or finish at any point.
108///
109/// Note that the Throbber thread gets spawned on the first call to [`start`](Throbber::start) or [`start_with_msg`](Throbber::start_with_msg). After that, the thread only ever gets parked.
110/// If you want to end the thread, you must drop the Throbber object.
111///
112/// # Examples
113///
114/// ```rust
115/// use std::thread;
116/// use std::time::Duration;
117/// use throbber::Throbber;
118///
119/// let mut throbber = Throbber::new(
120///     "calculating stuff",
121///     Duration::from_millis(50),
122///     &throbber::ROTATE_F,
123/// );
124///
125/// throbber.start();
126///
127/// // do stuff
128/// thread::sleep(Duration::from_secs(5));
129///
130/// throbber.success("calculation successful");
131/// ```
132pub struct Throbber {
133    anim: Option<ThrobberAnim>,
134    message: String,
135    interval: Duration,
136    frames: &'static [&'static str],
137}
138
139struct ThrobberAnim {
140    thread: JoinHandle<()>,
141    sender: Sender<ThrobberSignal>,
142}
143
144enum ThrobberSignal {
145    Start,
146    Finish,
147    Succ(String),
148    Fail(String),
149    ChMsg(String),
150    ChInt(Duration),
151    ChFrames(&'static [&'static str]),
152    End,
153}
154
155impl Default for Throbber {
156    /// # Default Values
157    ///
158    /// - message: `""`
159    /// - interval: `Duration::from_millis(200)`
160    /// - frames: `DEFAULT_F (⠋   ⠙   ⠹   ⠸   ⠼   ⠴   ⠦   ⠧   ⠇   ⠏)`
161    fn default() -> Self {
162        Self {
163            anim: None,
164            message: "".to_owned(),
165            interval: Duration::from_millis(200),
166            frames: &DEFAULT_F,
167        }
168    }
169}
170
171impl Drop for Throbber {
172    fn drop(&mut self) {
173        if let Some(anim) = self.anim.take() {
174            anim.sender.send(ThrobberSignal::End).unwrap();
175            anim.thread.thread().unpark();
176            anim.thread.join().unwrap();
177        }
178    }
179}
180
181impl Throbber {
182    /// Creates a new Throbber object.
183    pub fn new<S: Into<String>>(
184        message: S,
185        interval: Duration,
186        frames: &'static [&'static str],
187    ) -> Self {
188        Self {
189            anim: None,
190            message: message.into(),
191            interval,
192            frames,
193        }
194    }
195
196    /// Sets the message displayed next to the throbber.
197    pub fn message<S: Into<String>>(mut self, msg: S) -> Self {
198        self.set_message(msg);
199        self
200    }
201
202    /// Sets the message displayed next to the throbber.
203    pub fn set_message<S: Into<String>>(&mut self, msg: S) {
204        self.message = msg.into();
205        if let Some(ref anim) = self.anim {
206            anim.sender
207                .send(ThrobberSignal::ChMsg(self.message.clone()))
208                .unwrap();
209            anim.thread.thread().unpark();
210        }
211    }
212
213    /// Sets the animation frame interval, i.e. the time between frames.
214    pub fn interval<D: Into<Duration>>(mut self, interval: D) -> Self {
215        self.set_interval(interval);
216        self
217    }
218
219    /// Sets the animation frame interval, i.e. the time between frames.
220    pub fn set_interval<D: Into<Duration>>(&mut self, interval: D) {
221        self.interval = interval.into();
222        if let Some(ref anim) = self.anim {
223            anim.sender
224                .send(ThrobberSignal::ChInt(self.interval))
225                .unwrap();
226            anim.thread.thread().unpark();
227        }
228    }
229
230    /// Sets the animation frames.
231    pub fn frames(mut self, frames: &'static [&'static str]) -> Self {
232        self.set_frames(frames);
233        self
234    }
235
236    /// Sets the animation frames.
237    pub fn set_frames(&mut self, frames: &'static [&'static str]) {
238        self.frames = frames.into();
239        if let Some(ref anim) = self.anim {
240            anim.sender
241                .send(ThrobberSignal::ChFrames(self.frames))
242                .unwrap();
243            anim.thread.thread().unpark();
244        }
245    }
246
247    /// Starts the animation.
248    ///
249    /// If this is the first call to [`start`](Throbber::start), a new thread gets created to play the animation. Otherwise the thread that already exists gets unparked and starts the animation again.
250    pub fn start(&mut self) {
251        if let Some(ref anim) = self.anim {
252            anim.sender.send(ThrobberSignal::Start).unwrap();
253            anim.thread.thread().unpark();
254            return;
255        }
256
257        let (sender, receiver): (Sender<ThrobberSignal>, Receiver<ThrobberSignal>) =
258            mpsc::channel();
259
260        let msg = self.message.clone();
261        let interval = self.interval;
262        let frames = self.frames;
263        let thread = thread::spawn(move || animation_thread(receiver, msg, interval, frames));
264
265        self.anim = Some(ThrobberAnim { thread, sender });
266    }
267
268    /// Starts the animation with the specified `msg`.
269    ///
270    /// Equivalent to `throbber.set_message(msg); throbber.start();`.
271    pub fn start_with_msg<S: Into<String>>(&mut self, msg: S) {
272        self.set_message(msg);
273        self.start();
274    }
275
276    /// Stops the current animation, leaving a blank line.
277    pub fn finish(&mut self) {
278        if let Some(ref anim) = self.anim {
279            anim.sender.send(ThrobberSignal::Finish).unwrap();
280            anim.thread.thread().unpark();
281        }
282    }
283
284    /// Stops the current animation and prints `msg` as a *success message* (`✔`).
285    pub fn success<'a, S: Into<String> + std::fmt::Display>(&mut self, msg: S) {
286        if let Some(ref anim) = self.anim {
287            anim.sender.send(ThrobberSignal::Succ(msg.into())).unwrap();
288            anim.thread.thread().unpark();
289        } else {
290            println!("\x1B[2K\r✔ {}", msg);
291        }
292    }
293
294    /// Stops the current animation and prints `msg` as a *fail message* (`✖`).
295    ///
296    /// This still prints to stdout, *not* stderr.
297    pub fn fail<'a, S: Into<String>>(&mut self, msg: S) {
298        let msg = msg.into();
299        if let Some(ref anim) = self.anim {
300            anim.sender.send(ThrobberSignal::Fail(msg)).unwrap();
301            anim.thread.thread().unpark();
302        } else {
303            println!("\x1B[2K\r✖ {}", msg);
304        }
305    }
306}
307
308fn animation_thread<'a>(
309    receiver: Receiver<ThrobberSignal>,
310    mut msg: String,
311    mut interval: Duration,
312    mut frames: &'static [&'static str],
313) {
314    let mut play_anim = true;
315    let mut frame = 0;
316    loop {
317        match receiver.try_recv() {
318            Ok(ThrobberSignal::Start) => {
319                play_anim = true;
320                continue;
321            }
322            Ok(ThrobberSignal::Finish) => {
323                print!("\x1B[2K\r");
324                std::io::stdout().flush().unwrap();
325                play_anim = false;
326                continue;
327            }
328            Ok(ThrobberSignal::Succ(succ_msg)) => {
329                println!("\x1B[2K\r✔ {}", succ_msg);
330                play_anim = false;
331                continue;
332            }
333            Ok(ThrobberSignal::Fail(fail_msg)) => {
334                println!("\x1B[2K\r✖ {}", fail_msg);
335                play_anim = false;
336                continue;
337            }
338            Ok(ThrobberSignal::ChMsg(new_msg)) => {
339                msg = new_msg;
340                continue;
341            }
342            Ok(ThrobberSignal::ChInt(new_dur)) => {
343                interval = new_dur;
344                continue;
345            }
346            Ok(ThrobberSignal::ChFrames(new_frames)) => {
347                frames = new_frames;
348                frame = 0;
349                continue;
350            }
351            Ok(ThrobberSignal::End) => {
352                print!("\x1B[2K\r");
353                std::io::stdout().flush().unwrap();
354                break;
355            }
356            Err(TryRecvError::Disconnected) => {
357                print!("\x1B[2K\r");
358                std::io::stdout().flush().unwrap();
359                break;
360            }
361            Err(TryRecvError::Empty) => {
362                if play_anim == false {
363                    thread::park();
364                    continue;
365                }
366            }
367        }
368        print!("\x1B[2K\r");
369        print!("{} {}", frames[frame], msg);
370        std::io::stdout().flush().unwrap();
371        thread::sleep(interval);
372        frame = (frame + 1) % frames.len();
373    }
374}