fast_rich/progress/
spinner.rs

1//! Spinner animations for indeterminate progress.
2//!
3//! Provides 85 spinner styles from cli-spinners.
4//!
5//! # Attribution
6//! Spinner definitions are sourced from cli-spinners:
7//! MIT License - Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
8//! https://github.com/sindresorhus/cli-spinners
9
10use super::spinner_data::*;
11use crate::style::{Color, Style};
12use crate::text::Span;
13use std::time::Instant;
14
15/// Spinner animation style.
16///
17/// Use `SpinnerStyle::from_name()` for string-based lookup (Python parity).
18///
19/// # Examples
20/// ```
21/// use fast_rich::progress::SpinnerStyle;
22///
23/// // Using enum directly
24/// let style = SpinnerStyle::Dots;
25///
26/// // Using string lookup
27/// let style = SpinnerStyle::from_name("moon").unwrap();
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum SpinnerStyle {
31    // ==================== BRAILLE DOTS ====================
32    /// Default dots animation (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
33    #[default]
34    Dots,
35    /// Dots variant 2
36    Dots2,
37    /// Dots variant 3
38    Dots3,
39    /// Dots variant 4
40    Dots4,
41    /// Dots variant 5
42    Dots5,
43    /// Dots variant 6
44    Dots6,
45    /// Dots variant 7
46    Dots7,
47    /// Dots variant 8
48    Dots8,
49    /// Dots variant 9
50    Dots9,
51    /// Dots variant 10
52    Dots10,
53    /// Dots variant 11
54    Dots11,
55    /// Dots variant 12 (two-character)
56    Dots12,
57    /// Dots variant 13
58    Dots13,
59    /// Dots variant 14
60    Dots14,
61    /// Circular dots pattern
62    DotsCircle,
63    /// Sand falling animation
64    Sand,
65    /// Bounce animation
66    Bounce,
67
68    // ==================== LINES ====================
69    /// Classic line spinner (-\|/)
70    Line,
71    /// Line variant 2
72    Line2,
73    /// Pipe corners
74    Pipe,
75    /// Rolling line
76    RollingLine,
77    /// Simple dots (...)
78    SimpleDots,
79    /// Simple dots scrolling
80    SimpleDotsScrolling,
81
82    // ==================== STARS ====================
83    /// Star animation
84    Star,
85    /// Star variant 2
86    Star2,
87
88    // ==================== SHAPES ====================
89    /// Arc animation
90    Arc,
91    /// Circle animation
92    Circle,
93    /// Circle halves
94    CircleHalves,
95    /// Circle quarters
96    CircleQuarters,
97    /// Square corners
98    SquareCorners,
99    /// Triangle animation
100    Triangle,
101    /// Binary sequence
102    Binary,
103    /// Squish animation
104    Squish,
105    /// Flip animation
106    Flip,
107    /// Hamburger menu animation
108    Hamburger,
109
110    // ==================== BOXES ====================
111    /// Box bounce
112    BoxBounce,
113    /// Box bounce variant 2
114    BoxBounce2,
115    /// Noise animation
116    Noise,
117
118    // ==================== GROWING ====================
119    /// Vertical growing bar
120    GrowVertical,
121    /// Horizontal growing bar
122    GrowHorizontal,
123    /// Balloon animation
124    Balloon,
125    /// Balloon variant 2
126    Balloon2,
127
128    // ==================== TOGGLES ====================
129    /// Toggle animation
130    Toggle,
131    /// Toggle variant 2
132    Toggle2,
133    /// Toggle variant 3
134    Toggle3,
135    /// Toggle variant 4
136    Toggle4,
137    /// Toggle variant 5
138    Toggle5,
139    /// Toggle variant 6
140    Toggle6,
141    /// Toggle variant 7
142    Toggle7,
143    /// Toggle variant 8
144    Toggle8,
145    /// Toggle variant 9
146    Toggle9,
147    /// Toggle variant 10
148    Toggle10,
149    /// Toggle variant 11
150    Toggle11,
151    /// Toggle variant 12
152    Toggle12,
153    /// Toggle variant 13
154    Toggle13,
155
156    // ==================== ARROWS ====================
157    /// Arrow animation
158    Arrow,
159    /// Arrow variant 2 (emoji)
160    Arrow2,
161    /// Arrow variant 3
162    Arrow3,
163
164    // ==================== ANIMATIONS ====================
165    /// Bouncing bar animation
166    BouncingBar,
167    /// Bouncing ball animation
168    BouncingBall,
169    /// Pong game animation
170    Pong,
171    /// Shark animation
172    Shark,
173    /// Beta wave animation
174    BetaWave,
175    /// Aesthetic loading bar
176    Aesthetic,
177    /// Material design animation
178    Material,
179
180    // ==================== EMOJI ====================
181    /// Clock animation 🕐
182    Clock,
183    /// Moon phases 🌑🌒🌓🌔🌕
184    Moon,
185    /// Earth rotation 🌍🌎🌏
186    Earth,
187    /// Hearts 💛💙💜💚
188    Hearts,
189    /// Smiley 😄
190    Smiley,
191    /// See no evil monkey 🙈🙉🙊
192    Monkey,
193    /// Runner 🚶🏃
194    Runner,
195    /// Weather animation ☀️🌧
196    Weather,
197    /// Christmas tree 🌲🎄
198    Christmas,
199    /// Grenade explosion
200    Grenade,
201    /// Finger dance 🤘🤟
202    FingerDance,
203    /// Speaker 🔈🔉🔊
204    Speaker,
205    /// Orange pulse 🔸🔶🟠
206    OrangePulse,
207    /// Blue pulse 🔹🔷🔵
208    BluePulse,
209    /// Orange-blue pulse
210    OrangeBluePulse,
211    /// Time travel (reverse clock)
212    TimeTravel,
213    /// Mind blown 🤯
214    Mindblown,
215
216    // ==================== MISC ====================
217    /// dqpb animation
218    Dqpb,
219    /// Point animation
220    Point,
221    /// Layer animation
222    Layer,
223}
224
225impl SpinnerStyle {
226    /// Get the spinner definition (frames and interval).
227    pub fn def(&self) -> &'static SpinnerDef {
228        match self {
229            // Braille dots
230            SpinnerStyle::Dots => &DOTS,
231            SpinnerStyle::Dots2 => &DOTS2,
232            SpinnerStyle::Dots3 => &DOTS3,
233            SpinnerStyle::Dots4 => &DOTS4,
234            SpinnerStyle::Dots5 => &DOTS5,
235            SpinnerStyle::Dots6 => &DOTS6,
236            SpinnerStyle::Dots7 => &DOTS7,
237            SpinnerStyle::Dots8 => &DOTS8,
238            SpinnerStyle::Dots9 => &DOTS9,
239            SpinnerStyle::Dots10 => &DOTS10,
240            SpinnerStyle::Dots11 => &DOTS11,
241            SpinnerStyle::Dots12 => &DOTS12,
242            SpinnerStyle::Dots13 => &DOTS13,
243            SpinnerStyle::Dots14 => &DOTS14,
244            SpinnerStyle::DotsCircle => &DOTS_CIRCLE,
245            SpinnerStyle::Sand => &SAND,
246            SpinnerStyle::Bounce => &BOUNCE,
247
248            // Lines
249            SpinnerStyle::Line => &LINE,
250            SpinnerStyle::Line2 => &LINE2,
251            SpinnerStyle::Pipe => &PIPE,
252            SpinnerStyle::RollingLine => &ROLLING_LINE,
253            SpinnerStyle::SimpleDots => &SIMPLE_DOTS,
254            SpinnerStyle::SimpleDotsScrolling => &SIMPLE_DOTS_SCROLLING,
255
256            // Stars
257            SpinnerStyle::Star => &STAR,
258            SpinnerStyle::Star2 => &STAR2,
259
260            // Shapes
261            SpinnerStyle::Arc => &ARC,
262            SpinnerStyle::Circle => &CIRCLE,
263            SpinnerStyle::CircleHalves => &CIRCLE_HALVES,
264            SpinnerStyle::CircleQuarters => &CIRCLE_QUARTERS,
265            SpinnerStyle::SquareCorners => &SQUARE_CORNERS,
266            SpinnerStyle::Triangle => &TRIANGLE,
267            SpinnerStyle::Binary => &BINARY,
268            SpinnerStyle::Squish => &SQUISH,
269            SpinnerStyle::Flip => &FLIP,
270            SpinnerStyle::Hamburger => &HAMBURGER,
271
272            // Boxes
273            SpinnerStyle::BoxBounce => &BOX_BOUNCE,
274            SpinnerStyle::BoxBounce2 => &BOX_BOUNCE2,
275            SpinnerStyle::Noise => &NOISE,
276
277            // Growing
278            SpinnerStyle::GrowVertical => &GROW_VERTICAL,
279            SpinnerStyle::GrowHorizontal => &GROW_HORIZONTAL,
280            SpinnerStyle::Balloon => &BALLOON,
281            SpinnerStyle::Balloon2 => &BALLOON2,
282
283            // Toggles
284            SpinnerStyle::Toggle => &TOGGLE,
285            SpinnerStyle::Toggle2 => &TOGGLE2,
286            SpinnerStyle::Toggle3 => &TOGGLE3,
287            SpinnerStyle::Toggle4 => &TOGGLE4,
288            SpinnerStyle::Toggle5 => &TOGGLE5,
289            SpinnerStyle::Toggle6 => &TOGGLE6,
290            SpinnerStyle::Toggle7 => &TOGGLE7,
291            SpinnerStyle::Toggle8 => &TOGGLE8,
292            SpinnerStyle::Toggle9 => &TOGGLE9,
293            SpinnerStyle::Toggle10 => &TOGGLE10,
294            SpinnerStyle::Toggle11 => &TOGGLE11,
295            SpinnerStyle::Toggle12 => &TOGGLE12,
296            SpinnerStyle::Toggle13 => &TOGGLE13,
297
298            // Arrows
299            SpinnerStyle::Arrow => &ARROW,
300            SpinnerStyle::Arrow2 => &ARROW2,
301            SpinnerStyle::Arrow3 => &ARROW3,
302
303            // Animations
304            SpinnerStyle::BouncingBar => &BOUNCING_BAR,
305            SpinnerStyle::BouncingBall => &BOUNCING_BALL,
306            SpinnerStyle::Pong => &PONG,
307            SpinnerStyle::Shark => &SHARK,
308            SpinnerStyle::BetaWave => &BETA_WAVE,
309            SpinnerStyle::Aesthetic => &AESTHETIC,
310            SpinnerStyle::Material => &MATERIAL,
311
312            // Emoji
313            SpinnerStyle::Clock => &CLOCK,
314            SpinnerStyle::Moon => &MOON,
315            SpinnerStyle::Earth => &EARTH,
316            SpinnerStyle::Hearts => &HEARTS,
317            SpinnerStyle::Smiley => &SMILEY,
318            SpinnerStyle::Monkey => &MONKEY,
319            SpinnerStyle::Runner => &RUNNER,
320            SpinnerStyle::Weather => &WEATHER,
321            SpinnerStyle::Christmas => &CHRISTMAS,
322            SpinnerStyle::Grenade => &GRENADE,
323            SpinnerStyle::FingerDance => &FINGER_DANCE,
324            SpinnerStyle::Speaker => &SPEAKER,
325            SpinnerStyle::OrangePulse => &ORANGE_PULSE,
326            SpinnerStyle::BluePulse => &BLUE_PULSE,
327            SpinnerStyle::OrangeBluePulse => &ORANGE_BLUE_PULSE,
328            SpinnerStyle::TimeTravel => &TIME_TRAVEL,
329            SpinnerStyle::Mindblown => &MINDBLOWN,
330
331            // Misc
332            SpinnerStyle::Dqpb => &DQPB,
333            SpinnerStyle::Point => &POINT,
334            SpinnerStyle::Layer => &LAYER,
335        }
336    }
337
338    /// Get the frames for this spinner style.
339    pub fn frames(&self) -> &'static [&'static str] {
340        self.def().frames
341    }
342
343    /// Get the interval between frames in milliseconds.
344    pub fn interval_ms(&self) -> u64 {
345        self.def().interval_ms
346    }
347
348    /// Create a SpinnerStyle from a string name (Python parity).
349    ///
350    /// Names are case-insensitive and support both camelCase and snake_case.
351    ///
352    /// # Examples
353    /// ```
354    /// use fast_rich::progress::SpinnerStyle;
355    ///
356    /// assert!(SpinnerStyle::from_name("dots").is_some());
357    /// assert!(SpinnerStyle::from_name("moon").is_some());
358    /// assert!(SpinnerStyle::from_name("bouncingBar").is_some());
359    /// assert!(SpinnerStyle::from_name("bouncing_bar").is_some());
360    /// ```
361    pub fn from_name(name: &str) -> Option<SpinnerStyle> {
362        let name_lower = name.to_lowercase().replace('_', "");
363        match name_lower.as_str() {
364            // Braille dots
365            "dots" => Some(SpinnerStyle::Dots),
366            "dots2" => Some(SpinnerStyle::Dots2),
367            "dots3" => Some(SpinnerStyle::Dots3),
368            "dots4" => Some(SpinnerStyle::Dots4),
369            "dots5" => Some(SpinnerStyle::Dots5),
370            "dots6" => Some(SpinnerStyle::Dots6),
371            "dots7" => Some(SpinnerStyle::Dots7),
372            "dots8" => Some(SpinnerStyle::Dots8),
373            "dots9" => Some(SpinnerStyle::Dots9),
374            "dots10" => Some(SpinnerStyle::Dots10),
375            "dots11" => Some(SpinnerStyle::Dots11),
376            "dots12" => Some(SpinnerStyle::Dots12),
377            "dots13" => Some(SpinnerStyle::Dots13),
378            "dots14" => Some(SpinnerStyle::Dots14),
379            "dotscircle" => Some(SpinnerStyle::DotsCircle),
380            "sand" => Some(SpinnerStyle::Sand),
381            "bounce" => Some(SpinnerStyle::Bounce),
382
383            // Lines
384            "line" => Some(SpinnerStyle::Line),
385            "line2" => Some(SpinnerStyle::Line2),
386            "pipe" => Some(SpinnerStyle::Pipe),
387            "rollingline" => Some(SpinnerStyle::RollingLine),
388            "simpledots" => Some(SpinnerStyle::SimpleDots),
389            "simpledotsscrolling" => Some(SpinnerStyle::SimpleDotsScrolling),
390
391            // Stars
392            "star" => Some(SpinnerStyle::Star),
393            "star2" => Some(SpinnerStyle::Star2),
394
395            // Shapes
396            "arc" => Some(SpinnerStyle::Arc),
397            "circle" => Some(SpinnerStyle::Circle),
398            "circlehalves" => Some(SpinnerStyle::CircleHalves),
399            "circlequarters" => Some(SpinnerStyle::CircleQuarters),
400            "squarecorners" => Some(SpinnerStyle::SquareCorners),
401            "triangle" => Some(SpinnerStyle::Triangle),
402            "binary" => Some(SpinnerStyle::Binary),
403            "squish" => Some(SpinnerStyle::Squish),
404            "flip" => Some(SpinnerStyle::Flip),
405            "hamburger" => Some(SpinnerStyle::Hamburger),
406
407            // Boxes
408            "boxbounce" => Some(SpinnerStyle::BoxBounce),
409            "boxbounce2" => Some(SpinnerStyle::BoxBounce2),
410            "noise" => Some(SpinnerStyle::Noise),
411
412            // Growing
413            "growvertical" => Some(SpinnerStyle::GrowVertical),
414            "growhorizontal" => Some(SpinnerStyle::GrowHorizontal),
415            "balloon" => Some(SpinnerStyle::Balloon),
416            "balloon2" => Some(SpinnerStyle::Balloon2),
417
418            // Toggles
419            "toggle" => Some(SpinnerStyle::Toggle),
420            "toggle2" => Some(SpinnerStyle::Toggle2),
421            "toggle3" => Some(SpinnerStyle::Toggle3),
422            "toggle4" => Some(SpinnerStyle::Toggle4),
423            "toggle5" => Some(SpinnerStyle::Toggle5),
424            "toggle6" => Some(SpinnerStyle::Toggle6),
425            "toggle7" => Some(SpinnerStyle::Toggle7),
426            "toggle8" => Some(SpinnerStyle::Toggle8),
427            "toggle9" => Some(SpinnerStyle::Toggle9),
428            "toggle10" => Some(SpinnerStyle::Toggle10),
429            "toggle11" => Some(SpinnerStyle::Toggle11),
430            "toggle12" => Some(SpinnerStyle::Toggle12),
431            "toggle13" => Some(SpinnerStyle::Toggle13),
432
433            // Arrows
434            "arrow" => Some(SpinnerStyle::Arrow),
435            "arrow2" => Some(SpinnerStyle::Arrow2),
436            "arrow3" => Some(SpinnerStyle::Arrow3),
437
438            // Animations
439            "bouncingbar" => Some(SpinnerStyle::BouncingBar),
440            "bouncingball" => Some(SpinnerStyle::BouncingBall),
441            "pong" => Some(SpinnerStyle::Pong),
442            "shark" => Some(SpinnerStyle::Shark),
443            "betawave" => Some(SpinnerStyle::BetaWave),
444            "aesthetic" => Some(SpinnerStyle::Aesthetic),
445            "material" => Some(SpinnerStyle::Material),
446
447            // Emoji
448            "clock" => Some(SpinnerStyle::Clock),
449            "moon" => Some(SpinnerStyle::Moon),
450            "earth" => Some(SpinnerStyle::Earth),
451            "hearts" => Some(SpinnerStyle::Hearts),
452            "smiley" => Some(SpinnerStyle::Smiley),
453            "monkey" => Some(SpinnerStyle::Monkey),
454            "runner" => Some(SpinnerStyle::Runner),
455            "weather" => Some(SpinnerStyle::Weather),
456            "christmas" => Some(SpinnerStyle::Christmas),
457            "grenade" => Some(SpinnerStyle::Grenade),
458            "fingerdance" => Some(SpinnerStyle::FingerDance),
459            "speaker" => Some(SpinnerStyle::Speaker),
460            "orangepulse" => Some(SpinnerStyle::OrangePulse),
461            "bluepulse" => Some(SpinnerStyle::BluePulse),
462            "orangebluepulse" => Some(SpinnerStyle::OrangeBluePulse),
463            "timetravel" => Some(SpinnerStyle::TimeTravel),
464            "mindblown" => Some(SpinnerStyle::Mindblown),
465
466            // Misc
467            "dqpb" => Some(SpinnerStyle::Dqpb),
468            "point" => Some(SpinnerStyle::Point),
469            "layer" => Some(SpinnerStyle::Layer),
470
471            _ => None,
472        }
473    }
474
475    /// Get all available spinner style names.
476    pub fn all_names() -> &'static [&'static str] {
477        &[
478            "dots",
479            "dots2",
480            "dots3",
481            "dots4",
482            "dots5",
483            "dots6",
484            "dots7",
485            "dots8",
486            "dots9",
487            "dots10",
488            "dots11",
489            "dots12",
490            "dots13",
491            "dots14",
492            "dotsCircle",
493            "sand",
494            "bounce",
495            "line",
496            "line2",
497            "pipe",
498            "rollingLine",
499            "simpleDots",
500            "simpleDotsScrolling",
501            "star",
502            "star2",
503            "arc",
504            "circle",
505            "circleHalves",
506            "circleQuarters",
507            "squareCorners",
508            "triangle",
509            "binary",
510            "squish",
511            "flip",
512            "hamburger",
513            "boxBounce",
514            "boxBounce2",
515            "noise",
516            "growVertical",
517            "growHorizontal",
518            "balloon",
519            "balloon2",
520            "toggle",
521            "toggle2",
522            "toggle3",
523            "toggle4",
524            "toggle5",
525            "toggle6",
526            "toggle7",
527            "toggle8",
528            "toggle9",
529            "toggle10",
530            "toggle11",
531            "toggle12",
532            "toggle13",
533            "arrow",
534            "arrow2",
535            "arrow3",
536            "bouncingBar",
537            "bouncingBall",
538            "pong",
539            "shark",
540            "betaWave",
541            "aesthetic",
542            "material",
543            "clock",
544            "moon",
545            "earth",
546            "hearts",
547            "smiley",
548            "monkey",
549            "runner",
550            "weather",
551            "christmas",
552            "grenade",
553            "fingerDance",
554            "speaker",
555            "orangePulse",
556            "bluePulse",
557            "orangeBluePulse",
558            "timeTravel",
559            "mindblown",
560            "dqpb",
561            "point",
562            "layer",
563        ]
564    }
565}
566
567/// A spinner for indeterminate progress.
568#[derive(Debug, Clone)]
569pub struct Spinner {
570    /// Spinner style
571    style: SpinnerStyle,
572    /// Start time for animation
573    start_time: Instant,
574    /// Text to display after the spinner
575    text: String,
576    /// Style for the spinner character
577    spinner_style: Style,
578    /// Style for the text
579    text_style: Style,
580}
581
582impl Spinner {
583    /// Create a new spinner with optional text.
584    pub fn new(text: &str) -> Self {
585        Spinner {
586            style: SpinnerStyle::Dots,
587            start_time: Instant::now(),
588            text: text.to_string(),
589            spinner_style: Style::new().foreground(Color::Cyan),
590            text_style: Style::new(),
591        }
592    }
593
594    /// Set the spinner style.
595    pub fn style(mut self, style: SpinnerStyle) -> Self {
596        self.style = style;
597        self
598    }
599
600    /// Set the spinner style by name (Python parity).
601    ///
602    /// Returns `None` if the name is not recognized.
603    pub fn style_name(mut self, name: &str) -> Option<Self> {
604        SpinnerStyle::from_name(name).map(|s| {
605            self.style = s;
606            self
607        })
608    }
609
610    /// Set the spinner character style.
611    pub fn spinner_style(mut self, style: Style) -> Self {
612        self.spinner_style = style;
613        self
614    }
615
616    /// Set the text style.
617    pub fn text_style(mut self, style: Style) -> Self {
618        self.text_style = style;
619        self
620    }
621
622    /// Set the text.
623    pub fn text(mut self, text: &str) -> Self {
624        self.text = text.to_string();
625        self
626    }
627
628    /// Update the text.
629    pub fn set_text(&mut self, text: &str) {
630        self.text = text.to_string();
631    }
632
633    /// Get the text.
634    pub fn get_text(&self) -> &str {
635        &self.text
636    }
637
638    /// Get the spinner style.
639    pub fn get_style(&self) -> SpinnerStyle {
640        self.style
641    }
642
643    /// Get the current frame index.
644    fn current_frame_index(&self) -> usize {
645        let elapsed_ms = self.start_time.elapsed().as_millis() as u64;
646        let interval = self.style.interval_ms();
647        let frames = self.style.frames();
648        ((elapsed_ms / interval) as usize) % frames.len()
649    }
650
651    /// Get the current frame character.
652    pub fn current_frame(&self) -> &'static str {
653        let frames = self.style.frames();
654        let idx = self.current_frame_index();
655        frames[idx]
656    }
657
658    /// Render the spinner to spans.
659    pub fn render(&self) -> Vec<Span> {
660        vec![
661            Span::styled(self.current_frame().to_string(), self.spinner_style),
662            Span::raw(" "),
663            Span::styled(self.text.clone(), self.text_style),
664        ]
665    }
666
667    /// Render to a string (for simple output).
668    pub fn to_string_colored(&self) -> String {
669        format!("{} {}", self.current_frame(), self.text)
670    }
671}
672
673impl Default for Spinner {
674    fn default() -> Self {
675        Spinner::new("")
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[test]
684    fn test_spinner_frames() {
685        let style = SpinnerStyle::Dots;
686        let frames = style.frames();
687        assert!(!frames.is_empty());
688        assert_eq!(frames[0], "⠋");
689    }
690
691    #[test]
692    fn test_spinner_render() {
693        let spinner = Spinner::new("Loading...");
694        let spans = spinner.render();
695        assert_eq!(spans.len(), 3);
696    }
697
698    #[test]
699    fn test_all_spinner_styles_have_frames() {
700        for name in SpinnerStyle::all_names() {
701            let style = SpinnerStyle::from_name(name)
702                .unwrap_or_else(|| panic!("Failed to find style: {}", name));
703            let frames = style.frames();
704            assert!(!frames.is_empty(), "{} has no frames", name);
705            assert!(style.interval_ms() > 0, "{} has invalid interval", name);
706        }
707    }
708
709    #[test]
710    fn test_spinner_from_name() {
711        // Test various names
712        assert!(SpinnerStyle::from_name("dots").is_some());
713        assert!(SpinnerStyle::from_name("Dots").is_some());
714        assert!(SpinnerStyle::from_name("DOTS").is_some());
715        assert!(SpinnerStyle::from_name("moon").is_some());
716        assert!(SpinnerStyle::from_name("bouncingBar").is_some());
717        assert!(SpinnerStyle::from_name("bouncing_bar").is_some());
718        assert!(SpinnerStyle::from_name("invalid_name").is_none());
719    }
720
721    #[test]
722    fn test_emoji_spinners() {
723        let clock = SpinnerStyle::Clock;
724        assert!(clock.frames().iter().any(|f| f.contains("🕐")));
725
726        let moon = SpinnerStyle::Moon;
727        assert!(moon.frames().iter().any(|f| f.contains("🌕")));
728    }
729}