Skip to main content

rusty_rich/
spinner.rs

1//! Spinner — animated spinner. Equivalent to Rich's `spinner.py`.
2
3use std::time::Duration;
4
5// ---------------------------------------------------------------------------
6// Spinner frames
7// ---------------------------------------------------------------------------
8
9/// Predefined spinner animations (matching Rich's spinner set).
10#[derive(Debug, Clone)]
11pub struct SpinnerFrames {
12    pub frames: &'static [&'static str],
13    pub interval: f64, // seconds per frame
14}
15
16// ===========================================================================
17// Classic / dots spinners
18// ===========================================================================
19
20pub const SPINNER_DOTS: SpinnerFrames = SpinnerFrames {
21    frames: &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
22    interval: 0.08,
23};
24
25pub const SPINNER_LINE: SpinnerFrames = SpinnerFrames {
26    frames: &["-", "\\", "|", "/"],
27    interval: 0.1,
28};
29
30pub const SPINNER_DOTS2: SpinnerFrames = SpinnerFrames {
31    frames: &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
32    interval: 0.08,
33};
34
35pub const SPINNER_DOTS3: SpinnerFrames = SpinnerFrames {
36    frames: &["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
37    interval: 0.08,
38};
39
40pub const SPINNER_DOTS4: SpinnerFrames = SpinnerFrames {
41    frames: &["⠄", "⠆", "⠇", "⠋", "⠙", "⠸", "⠰", "⠠", "⠰", "⠸", "⠙", "⠋", "⠇", "⠆"],
42    interval: 0.08,
43};
44
45pub const SPINNER_DOTS5: SpinnerFrames = SpinnerFrames {
46    frames: &["⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"],
47    interval: 0.08,
48};
49
50pub const SPINNER_DOTS6: SpinnerFrames = SpinnerFrames {
51    frames: &[
52        "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒",
53        "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁",
54    ],
55    interval: 0.08,
56};
57
58pub const SPINNER_DOTS7: SpinnerFrames = SpinnerFrames {
59    frames: &[
60        "⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒",
61        "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈",
62    ],
63    interval: 0.08,
64};
65
66pub const SPINNER_DOTS8: SpinnerFrames = SpinnerFrames {
67    frames: &[
68        "⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠",
69        "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈",
70    ],
71    interval: 0.08,
72};
73
74pub const SPINNER_DOTS9: SpinnerFrames = SpinnerFrames {
75    frames: &["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"],
76    interval: 0.08,
77};
78
79pub const SPINNER_DOTS10: SpinnerFrames = SpinnerFrames {
80    frames: &["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"],
81    interval: 0.08,
82};
83
84pub const SPINNER_DOTS11: SpinnerFrames = SpinnerFrames {
85    frames: &["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"],
86    interval: 0.1,
87};
88
89pub const SPINNER_SIMPLE_DOTS: SpinnerFrames = SpinnerFrames {
90    frames: &[".  ", ".. ", "...", " ..", "  .", "   "],
91    interval: 0.2,
92};
93
94// ===========================================================================
95// Icon / theme spinners
96// ===========================================================================
97
98pub const SPINNER_MOON: SpinnerFrames = SpinnerFrames {
99    frames: &["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
100    interval: 0.08,
101};
102
103pub const SPINNER_SMILEY: SpinnerFrames = SpinnerFrames {
104    frames: &["😄", "😝"],
105    interval: 0.2,
106};
107
108// ===========================================================================
109// New spinner styles (20+ from Python Rich)
110// ===========================================================================
111
112pub const SPINNER_ARC: SpinnerFrames = SpinnerFrames {
113    frames: &["◜", "◠", "◝", "◞", "◡", "◟"],
114    interval: 0.1,
115};
116
117pub const SPINNER_ARROW: SpinnerFrames = SpinnerFrames {
118    frames: &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
119    interval: 0.1,
120};
121
122pub const SPINNER_ARROW2: SpinnerFrames = SpinnerFrames {
123    frames: &["⬆️", "↗️", "➡️", "↘️", "⬇️", "↙️", "⬅️", "↖️"],
124    interval: 0.1,
125};
126
127pub const SPINNER_ARROW3: SpinnerFrames = SpinnerFrames {
128    frames: &["▹", "▸", "▹", "▸", "▹", "▸"],
129    interval: 0.1,
130};
131
132pub const SPINNER_BOUNCING_BAR: SpinnerFrames = SpinnerFrames {
133    frames: &["[    ]", "[=   ]", "[==  ]", "[=== ]", "[ ===]", "[  ==]", "[   =]", "[    ]"],
134    interval: 0.15,
135};
136
137pub const SPINNER_BOUNCING_BALL: SpinnerFrames = SpinnerFrames {
138    frames: &[
139        "( ●    )", "(  ●   )", "(   ●  )", "(    ● )", "(     ●)", "(    ● )", "(   ●  )",
140        "(  ●   )",
141    ],
142    interval: 0.15,
143};
144
145pub const SPINNER_CHRISTMAS: SpinnerFrames = SpinnerFrames {
146    frames: &["🌲", "🎄"],
147    interval: 0.4,
148};
149
150pub const SPINNER_CIRCLE: SpinnerFrames = SpinnerFrames {
151    frames: &["◐", "◓", "◑", "◒"],
152    interval: 0.1,
153};
154
155pub const SPINNER_CLOCK: SpinnerFrames = SpinnerFrames {
156    frames: &[
157        "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛",
158    ],
159    interval: 0.1,
160};
161
162pub const SPINNER_EARTH: SpinnerFrames = SpinnerFrames {
163    frames: &["🌍", "🌎", "🌏"],
164    interval: 0.2,
165};
166
167pub const SPINNER_GRENADE: SpinnerFrames = SpinnerFrames {
168    frames: &["،  💣  ", "۔  💣  ", " ﹒ 💣  "],
169    interval: 0.1,
170};
171
172pub const SPINNER_GROW_HORIZONTAL: SpinnerFrames = SpinnerFrames {
173    frames: &["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▉", "▊", "▋", "▌", "▍", "▎", "▏"],
174    interval: 0.08,
175};
176
177pub const SPINNER_GROW_VERTICAL: SpinnerFrames = SpinnerFrames {
178    frames: &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"],
179    interval: 0.08,
180};
181
182pub const SPINNER_HAMBURGER: SpinnerFrames = SpinnerFrames {
183    frames: &["☱", "☲", "☴"],
184    interval: 0.12,
185};
186
187pub const SPINNER_HEARTS: SpinnerFrames = SpinnerFrames {
188    frames: &["🩷", "❤️", "🧡", "💛", "💚", "💙", "🩵", "💜", "🤎", "🖤", "🩶", "🤍"],
189    interval: 0.12,
190};
191
192pub const SPINNER_MONKEY: SpinnerFrames = SpinnerFrames {
193    frames: &["🐒", "🐒", "🐒", "🐒", "🙈", "🙉", "🙊", "🐒", "🐒", "🐒", "🐒"],
194    interval: 0.15,
195};
196
197pub const SPINNER_NOISE: SpinnerFrames = SpinnerFrames {
198    frames: &["▓", "▒", "░", "▓", "▒", "░", "▓", "▒", "░"],
199    interval: 0.08,
200};
201
202pub const SPINNER_PONG: SpinnerFrames = SpinnerFrames {
203    frames: &[
204        "▐⠂       ▌", "▐⠈       ▌", "▐ ⠂      ▌", "▐ ⠠      ▌",
205        "▐  ⡀     ▌", "▐  ⠠     ▌", "▐   ⠂    ▌", "▐   ⠈    ▌",
206        "▐    ⠂   ▌", "▐    ⠠   ▌", "▐     ⡀  ▌", "▐     ⠠  ▌",
207        "▐      ⠂ ▌", "▐      ⠈ ▌", "▐       ⠂▌", "▐       ⠠▌",
208        "▐       ⡀▌", "▐      ⠠ ▌", "▐      ⠂ ▌", "▐     ⠈  ▌",
209        "▐     ⠂  ▌", "▐    ⠠   ▌", "▐    ⡀   ▌", "▐   ⠠    ▌",
210        "▐   ⠂    ▌", "▐  ⠈     ▌", "▐  ⠂     ▌", "▐ ⠠      ▌",
211        "▐ ⠂      ▌", "▐⠈       ▌",
212    ],
213    interval: 0.08,
214};
215
216pub const SPINNER_RUNNER: SpinnerFrames = SpinnerFrames {
217    frames: &["🚶", "🏃", "🏃", "🏃", "🚶", "🚶"],
218    interval: 0.15,
219};
220
221pub const SPINNER_SHARK: SpinnerFrames = SpinnerFrames {
222    frames: &["🦈", "🌀", "🦈", "🌀", "🦈", "🌀"],
223    interval: 0.15,
224};
225
226pub const SPINNER_TOGGLE: SpinnerFrames = SpinnerFrames {
227    frames: &["⊶", "⊷"],
228    interval: 0.2,
229};
230
231pub const SPINNER_TRIANGLE: SpinnerFrames = SpinnerFrames {
232    frames: &["◢", "◣", "◤", "◥"],
233    interval: 0.1,
234};
235
236pub const SPINNER_VERTICAL_BARS: SpinnerFrames = SpinnerFrames {
237    frames: &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"],
238    interval: 0.08,
239};
240
241// ===========================================================================
242// Additional spinners from Python Rich (bringing total to 55+)
243// ===========================================================================
244
245pub const SPINNER_DOTS12: SpinnerFrames = SpinnerFrames {
246    frames: &["⣀", "⣤", "⣶", "⣿", "⣶", "⣤"],
247    interval: 0.08,
248};
249
250pub const SPINNER_DOTS13: SpinnerFrames = SpinnerFrames {
251    frames: &["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
252    interval: 0.08,
253};
254
255pub const SPINNER_DOTS8_BIT: SpinnerFrames = SpinnerFrames {
256    frames: &["⠀", "⠁", "⠂", "⠃", "⠇", "⠏", "⠟", "⠿", "⡿", "⣿", "⣿", "⣿"],
257    interval: 0.08,
258};
259
260pub const SPINNER_SIMPLE_DOTS_SCROLLING: SpinnerFrames = SpinnerFrames {
261    frames: &[".  ", ".. ", "...", " ..", "  .", " ..", "...", ".. "],
262    interval: 0.2,
263};
264
265pub const SPINNER_STAR: SpinnerFrames = SpinnerFrames {
266    frames: &["✶", "✸", "✹", "✺", "✹", "✷"],
267    interval: 0.1,
268};
269
270pub const SPINNER_STAR2: SpinnerFrames = SpinnerFrames {
271    frames: &["+", "x", "*"],
272    interval: 0.12,
273};
274
275pub const SPINNER_FLIP: SpinnerFrames = SpinnerFrames {
276    frames: &["_", "_", "_", "-", "`", "`", "'", "¯", "_", "_", "_", "-"],
277    interval: 0.1,
278};
279
280pub const SPINNER_BALLOON: SpinnerFrames = SpinnerFrames {
281    frames: &[". ", "o ", "O ", "@ ", "* ", " "],
282    interval: 0.12,
283};
284
285pub const SPINNER_BALLOON2: SpinnerFrames = SpinnerFrames {
286    frames: &[".", "o", "O", "°", "O", "o", "."],
287    interval: 0.12,
288};
289
290pub const SPINNER_PIPE: SpinnerFrames = SpinnerFrames {
291    frames: &["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
292    interval: 0.1,
293};
294
295pub const SPINNER_SQUARE_CORNERS: SpinnerFrames = SpinnerFrames {
296    frames: &["◰", "◳", "◲", "◱"],
297    interval: 0.12,
298};
299
300pub const SPINNER_CIRCLE_QUARTERS: SpinnerFrames = SpinnerFrames {
301    frames: &["◴", "◷", "◶", "◵"],
302    interval: 0.12,
303};
304
305pub const SPINNER_CIRCLE_HALVES: SpinnerFrames = SpinnerFrames {
306    frames: &["◐", "◓", "◑", "◒"],
307    interval: 0.12,
308};
309
310pub const SPINNER_AESTHETIC: SpinnerFrames = SpinnerFrames {
311    frames: &["▰", "▱"],
312    interval: 0.15,
313};
314
315pub const SPINNER_BRAILLE_LONG: SpinnerFrames = SpinnerFrames {
316    frames: &["⠁", "⠂", "⠄", "⠠", "⠐", "⠈", "⡀", "⢀"],
317    interval: 0.08,
318};
319
320pub const SPINNER_BRAILLE_CRAWL: SpinnerFrames = SpinnerFrames {
321    frames: &["⡀", "⡄", "⡆", "⡇", "⡏", "⡟", "⡿", "⣿"],
322    interval: 0.08,
323};
324
325pub const SPINNER_PULSE: SpinnerFrames = SpinnerFrames {
326    frames: &["█", "▓", "▒", "░"],
327    interval: 0.1,
328};
329
330pub const SPINNER_BOUNCE: SpinnerFrames = SpinnerFrames {
331    frames: &["⠁", "⠂", "⠄", "⠂"],
332    interval: 0.12,
333};
334
335pub const SPINNER_MATERIAL: SpinnerFrames = SpinnerFrames {
336    frames: &["◐", "◓", "◑", "◒"],
337    interval: 0.08,
338};
339
340pub const SPINNER_WINDOWS: SpinnerFrames = SpinnerFrames {
341    frames: &["/", "-", "\\", "|"],
342    interval: 0.1,
343};
344
345pub const SPINNER_SHADED_BLOCKS: SpinnerFrames = SpinnerFrames {
346    frames: &["░", "▒", "▓", "█", "▓", "▒"],
347    interval: 0.08,
348};
349
350/// Default spinner.
351pub const DEFAULT_SPINNER: SpinnerFrames = SPINNER_DOTS;
352
353// ===========================================================================
354// Name-based lookup
355// =========================================================================--
356
357/// All known spinners mapped by name (lowercase) for runtime lookup.
358pub const SPINNERS: &[(&str, &SpinnerFrames)] = &[
359    ("arc", &SPINNER_ARC),
360    ("arrow", &SPINNER_ARROW),
361    ("arrow2", &SPINNER_ARROW2),
362    ("arrow3", &SPINNER_ARROW3),
363    ("bouncingBar", &SPINNER_BOUNCING_BAR),
364    ("bouncingBall", &SPINNER_BOUNCING_BALL),
365    ("christmas", &SPINNER_CHRISTMAS),
366    ("circle", &SPINNER_CIRCLE),
367    ("clock", &SPINNER_CLOCK),
368    ("dots", &SPINNER_DOTS),
369    ("dots2", &SPINNER_DOTS2),
370    ("dots3", &SPINNER_DOTS3),
371    ("dots4", &SPINNER_DOTS4),
372    ("dots5", &SPINNER_DOTS5),
373    ("dots6", &SPINNER_DOTS6),
374    ("dots7", &SPINNER_DOTS7),
375    ("dots8", &SPINNER_DOTS8),
376    ("dots9", &SPINNER_DOTS9),
377    ("dots10", &SPINNER_DOTS10),
378    ("dots11", &SPINNER_DOTS11),
379    ("earth", &SPINNER_EARTH),
380    ("grenade", &SPINNER_GRENADE),
381    ("growHorizontal", &SPINNER_GROW_HORIZONTAL),
382    ("growVertical", &SPINNER_GROW_VERTICAL),
383    ("hamburger", &SPINNER_HAMBURGER),
384    ("hearts", &SPINNER_HEARTS),
385    ("line", &SPINNER_LINE),
386    ("monkey", &SPINNER_MONKEY),
387    ("moon", &SPINNER_MOON),
388    ("noise", &SPINNER_NOISE),
389    ("pong", &SPINNER_PONG),
390    ("runner", &SPINNER_RUNNER),
391    ("shark", &SPINNER_SHARK),
392    ("simpleDots", &SPINNER_SIMPLE_DOTS),
393    ("smiley", &SPINNER_SMILEY),
394    ("toggle", &SPINNER_TOGGLE),
395    ("triangle", &SPINNER_TRIANGLE),
396    ("verticalBars", &SPINNER_VERTICAL_BARS),
397    ("dots12", &SPINNER_DOTS12),
398    ("dots13", &SPINNER_DOTS13),
399    ("dots8Bit", &SPINNER_DOTS8_BIT),
400    ("simpleDotsScrolling", &SPINNER_SIMPLE_DOTS_SCROLLING),
401    ("star", &SPINNER_STAR),
402    ("star2", &SPINNER_STAR2),
403    ("flip", &SPINNER_FLIP),
404    ("balloon", &SPINNER_BALLOON),
405    ("balloon2", &SPINNER_BALLOON2),
406    ("pipe", &SPINNER_PIPE),
407    ("squareCorners", &SPINNER_SQUARE_CORNERS),
408    ("circleQuarters", &SPINNER_CIRCLE_QUARTERS),
409    ("circleHalves", &SPINNER_CIRCLE_HALVES),
410    ("aesthetic", &SPINNER_AESTHETIC),
411    ("brailleLong", &SPINNER_BRAILLE_LONG),
412    ("brailleCrawl", &SPINNER_BRAILLE_CRAWL),
413    ("pulse", &SPINNER_PULSE),
414    ("bounce", &SPINNER_BOUNCE),
415    ("material", &SPINNER_MATERIAL),
416    ("windows", &SPINNER_WINDOWS),
417    ("shadedBlocks", &SPINNER_SHADED_BLOCKS),
418];
419
420/// Get a spinner by name (case-insensitive).
421///
422/// Returns `None` if no spinner with the given name exists.
423///
424/// # Example
425///
426/// ```rust
427/// use rusty_rich::get_spinner;
428///
429/// let s = get_spinner("arc").unwrap();
430/// assert_eq!(s.frames.len(), 6);
431/// ```
432pub fn get_spinner(name: &str) -> Option<&'static SpinnerFrames> {
433    // First try direct lowercase match
434    for (key, spinner) in SPINNERS {
435        if key.eq_ignore_ascii_case(name) {
436            return Some(spinner);
437        }
438    }
439    // Fallback: try stripping spaces and hyphens, matching lowercase
440    let normalized: String = name.chars().filter(|c| !c.is_whitespace()).collect();
441    let normalized = normalized.to_lowercase();
442    for (key, spinner) in SPINNERS {
443        let key_normalized: String = key.chars().filter(|c| !c.is_whitespace()).collect();
444        let key_normalized = key_normalized.to_lowercase();
445        if key_normalized == normalized {
446            return Some(spinner);
447        }
448    }
449    None
450}
451
452// ---------------------------------------------------------------------------
453// Spinner
454// ---------------------------------------------------------------------------
455
456/// An animated spinner renderable.
457#[derive(Debug, Clone)]
458pub struct Spinner {
459    pub frames: &'static [&'static str],
460    pub interval: f64,
461    /// Text displayed alongside the spinner.
462    pub text: String,
463    /// Style for the spinner.
464    pub style: crate::style::Style,
465}
466
467impl Spinner {
468    /// Create a new spinner.
469    pub fn new(spinner: &'static SpinnerFrames) -> Self {
470        Self {
471            frames: spinner.frames,
472            interval: spinner.interval,
473            text: String::new(),
474            style: crate::style::Style::new(),
475        }
476    }
477
478    /// Builder: set the text.
479    pub fn text(mut self, text: impl Into<String>) -> Self {
480        self.text = text.into();
481        self
482    }
483
484    /// Builder: set the style.
485    pub fn style(mut self, style: crate::style::Style) -> Self {
486        self.style = style;
487        self
488    }
489
490    /// Get the frame at the given elapsed time.
491    pub fn frame_at(&self, elapsed: Duration) -> &'static str {
492        let idx = (elapsed.as_secs_f64() / self.interval) as usize % self.frames.len();
493        self.frames[idx]
494    }
495
496    /// Get the display string for the current time.
497    pub fn render(&self, elapsed: Duration) -> String {
498        let frame = self.frame_at(elapsed);
499        let style_ansi = self.style.to_ansi();
500        let reset = if style_ansi.is_empty() { "" } else { "\x1b[0m" };
501        if self.text.is_empty() {
502            format!("{style_ansi}{frame}{reset}")
503        } else {
504            format!("{style_ansi}{frame}{reset} {}", self.text)
505        }
506    }
507}
508
509impl Default for Spinner {
510    fn default() -> Self {
511        Self::new(&DEFAULT_SPINNER)
512    }
513}
514
515// ---------------------------------------------------------------------------
516// Tests
517// ---------------------------------------------------------------------------
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    #[test]
524    fn test_spinner_frame_at() {
525        let s = Spinner::new(&SPINNER_LINE);
526        let f = s.frame_at(Duration::from_millis(200));
527        assert!(f == "-" || f == "\\" || f == "|" || f == "/");
528    }
529
530    #[test]
531    fn test_get_spinner_found() {
532        let s = get_spinner("dots").unwrap();
533        assert!(!s.frames.is_empty());
534
535        let s = get_spinner("DOTS").unwrap();
536        assert!(!s.frames.is_empty());
537
538        let s = get_spinner("arc").unwrap();
539        assert_eq!(s.frames.len(), 6);
540    }
541
542    #[test]
543    fn test_get_spinner_not_found() {
544        assert!(get_spinner("nonexistent").is_none());
545    }
546
547    #[test]
548    fn test_get_spinner_case_insensitive() {
549        let s1 = get_spinner("ARC").unwrap();
550        let s2 = get_spinner("arc").unwrap();
551        assert_eq!(s1.frames, s2.frames);
552    }
553
554    #[test]
555    fn test_get_spinner_camel_case() {
556        let s = get_spinner("bouncingBar").unwrap();
557        assert!(!s.frames.is_empty());
558
559        let s = get_spinner("BOUNCINGBAR").unwrap();
560        assert!(!s.frames.is_empty());
561    }
562
563    #[test]
564    fn test_spinners_list_all_accessible() {
565        for (name, frames) in SPINNERS {
566            let found = get_spinner(name).unwrap();
567            assert!(!frames.frames.is_empty(), "spinner '{}' has no frames", name);
568            // Compare frame content rather than raw pointers, since `const`
569            // values may be inlined at different addresses by the compiler.
570            assert_eq!(
571                frames.frames, found.frames,
572                "spinner '{}' points to different frames than expected",
573                name
574            );
575            assert!(
576                (frames.interval - found.interval).abs() < f64::EPSILON,
577                "spinner '{}' interval mismatch",
578                name
579            );
580        }
581    }
582
583    #[test]
584    fn test_spinner_arc_frames() {
585        assert_eq!(SPINNER_ARC.frames.len(), 6);
586        assert!(SPINNER_ARC.interval > 0.0);
587    }
588
589    #[test]
590    fn test_spinner_arrow_frames() {
591        assert_eq!(SPINNER_ARROW.frames.len(), 8);
592    }
593
594    #[test]
595    fn test_spinner_arrow2_frames() {
596        assert_eq!(SPINNER_ARROW2.frames.len(), 8);
597    }
598
599    #[test]
600    fn test_spinner_arrow3_frames() {
601        assert_eq!(SPINNER_ARROW3.frames.len(), 6);
602    }
603
604    #[test]
605    fn test_spinner_bouncing_bar() {
606        assert_eq!(SPINNER_BOUNCING_BAR.frames.len(), 8);
607    }
608
609    #[test]
610    fn test_spinner_bouncing_ball() {
611        assert_eq!(SPINNER_BOUNCING_BALL.frames.len(), 8);
612    }
613
614    #[test]
615    fn test_spinner_christmas() {
616        assert_eq!(SPINNER_CHRISTMAS.frames.len(), 2);
617    }
618
619    #[test]
620    fn test_spinner_circle() {
621        assert_eq!(SPINNER_CIRCLE.frames.len(), 4);
622    }
623
624    #[test]
625    fn test_spinner_clock() {
626        assert_eq!(SPINNER_CLOCK.frames.len(), 12);
627    }
628
629    #[test]
630    fn test_spinner_earth() {
631        assert_eq!(SPINNER_EARTH.frames.len(), 3);
632    }
633
634    #[test]
635    fn test_spinner_grenade() {
636        assert_eq!(SPINNER_GRENADE.frames.len(), 3);
637    }
638
639    #[test]
640    fn test_spinner_grow_horizontal() {
641        assert_eq!(SPINNER_GROW_HORIZONTAL.frames.len(), 14);
642    }
643
644    #[test]
645    fn test_spinner_grow_vertical() {
646        assert_eq!(SPINNER_GROW_VERTICAL.frames.len(), 14);
647    }
648
649    #[test]
650    fn test_spinner_hamburger() {
651        assert_eq!(SPINNER_HAMBURGER.frames.len(), 3);
652    }
653
654    #[test]
655    fn test_spinner_hearts() {
656        assert_eq!(SPINNER_HEARTS.frames.len(), 12);
657    }
658
659    #[test]
660    fn test_spinner_monkey() {
661        assert_eq!(SPINNER_MONKEY.frames.len(), 11);
662    }
663
664    #[test]
665    fn test_spinner_noise() {
666        assert_eq!(SPINNER_NOISE.frames.len(), 9);
667    }
668
669    #[test]
670    fn test_spinner_pong() {
671        assert_eq!(SPINNER_PONG.frames.len(), 30);
672    }
673
674    #[test]
675    fn test_spinner_runner() {
676        assert_eq!(SPINNER_RUNNER.frames.len(), 6);
677    }
678
679    #[test]
680    fn test_spinner_shark() {
681        assert_eq!(SPINNER_SHARK.frames.len(), 6);
682    }
683
684    #[test]
685    fn test_spinner_toggle() {
686        assert_eq!(SPINNER_TOGGLE.frames.len(), 2);
687    }
688
689    #[test]
690    fn test_spinner_triangle() {
691        assert_eq!(SPINNER_TRIANGLE.frames.len(), 4);
692    }
693
694    #[test]
695    fn test_spinner_vertical_bars() {
696        assert_eq!(SPINNER_VERTICAL_BARS.frames.len(), 15);
697    }
698
699    #[test]
700    fn test_spinner_interval_positive() {
701        for (name, frames) in SPINNERS {
702            assert!(
703                frames.interval > 0.0,
704                "spinner '{}' has non-positive interval",
705                name
706            );
707        }
708    }
709
710    #[test]
711    fn test_default_spinner_is_dots() {
712        assert_eq!(DEFAULT_SPINNER.frames, SPINNER_DOTS.frames);
713    }
714
715    #[test]
716    fn test_spinner_dots12() { assert!(!SPINNER_DOTS12.frames.is_empty()); assert!(SPINNER_DOTS12.interval > 0.0); }
717    #[test]
718    fn test_spinner_dots13() { assert!(!SPINNER_DOTS13.frames.is_empty()); assert!(SPINNER_DOTS13.interval > 0.0); }
719    #[test]
720    fn test_spinner_star() { assert!(!SPINNER_STAR.frames.is_empty()); assert!(SPINNER_STAR.interval > 0.0); }
721    #[test]
722    fn test_spinner_flip() { assert!(!SPINNER_FLIP.frames.is_empty()); assert!(SPINNER_FLIP.interval > 0.0); }
723    #[test]
724    fn test_spinner_balloon() { assert!(!SPINNER_BALLOON.frames.is_empty()); assert!(SPINNER_BALLOON.interval > 0.0); }
725    #[test]
726    fn test_spinner_pipe() { assert!(!SPINNER_PIPE.frames.is_empty()); assert!(SPINNER_PIPE.interval > 0.0); }
727    #[test]
728    fn test_spinner_pulse() { assert!(!SPINNER_PULSE.frames.is_empty()); assert!(SPINNER_PULSE.interval > 0.0); }
729    #[test]
730    fn test_spinner_windows() { assert!(!SPINNER_WINDOWS.frames.is_empty()); assert!(SPINNER_WINDOWS.interval > 0.0); }
731    #[test]
732    fn test_spinner_shaded_blocks() { assert!(!SPINNER_SHADED_BLOCKS.frames.is_empty()); assert!(SPINNER_SHADED_BLOCKS.interval > 0.0); }
733}