throbber/lib.rs
1//! [](https://crates.io/crates/throbber)
2//! [](https://docs.rs/throbber)
3//! [](https://github.com/Treeniks/throbber)
4//! [](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//! 
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}