1use super::spinner_data::*;
11use crate::style::{Color, Style};
12use crate::text::Span;
13use std::time::Instant;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum SpinnerStyle {
31 #[default]
34 Dots,
35 Dots2,
37 Dots3,
39 Dots4,
41 Dots5,
43 Dots6,
45 Dots7,
47 Dots8,
49 Dots9,
51 Dots10,
53 Dots11,
55 Dots12,
57 Dots13,
59 Dots14,
61 DotsCircle,
63 Sand,
65 Bounce,
67
68 Line,
71 Line2,
73 Pipe,
75 RollingLine,
77 SimpleDots,
79 SimpleDotsScrolling,
81
82 Star,
85 Star2,
87
88 Arc,
91 Circle,
93 CircleHalves,
95 CircleQuarters,
97 SquareCorners,
99 Triangle,
101 Binary,
103 Squish,
105 Flip,
107 Hamburger,
109
110 BoxBounce,
113 BoxBounce2,
115 Noise,
117
118 GrowVertical,
121 GrowHorizontal,
123 Balloon,
125 Balloon2,
127
128 Toggle,
131 Toggle2,
133 Toggle3,
135 Toggle4,
137 Toggle5,
139 Toggle6,
141 Toggle7,
143 Toggle8,
145 Toggle9,
147 Toggle10,
149 Toggle11,
151 Toggle12,
153 Toggle13,
155
156 Arrow,
159 Arrow2,
161 Arrow3,
163
164 BouncingBar,
167 BouncingBall,
169 Pong,
171 Shark,
173 BetaWave,
175 Aesthetic,
177 Material,
179
180 Clock,
183 Moon,
185 Earth,
187 Hearts,
189 Smiley,
191 Monkey,
193 Runner,
195 Weather,
197 Christmas,
199 Grenade,
201 FingerDance,
203 Speaker,
205 OrangePulse,
207 BluePulse,
209 OrangeBluePulse,
211 TimeTravel,
213 Mindblown,
215
216 Dqpb,
219 Point,
221 Layer,
223}
224
225impl SpinnerStyle {
226 pub fn def(&self) -> &'static SpinnerDef {
228 match self {
229 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 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 SpinnerStyle::Star => &STAR,
258 SpinnerStyle::Star2 => &STAR2,
259
260 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 SpinnerStyle::BoxBounce => &BOX_BOUNCE,
274 SpinnerStyle::BoxBounce2 => &BOX_BOUNCE2,
275 SpinnerStyle::Noise => &NOISE,
276
277 SpinnerStyle::GrowVertical => &GROW_VERTICAL,
279 SpinnerStyle::GrowHorizontal => &GROW_HORIZONTAL,
280 SpinnerStyle::Balloon => &BALLOON,
281 SpinnerStyle::Balloon2 => &BALLOON2,
282
283 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 SpinnerStyle::Arrow => &ARROW,
300 SpinnerStyle::Arrow2 => &ARROW2,
301 SpinnerStyle::Arrow3 => &ARROW3,
302
303 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 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 SpinnerStyle::Dqpb => &DQPB,
333 SpinnerStyle::Point => &POINT,
334 SpinnerStyle::Layer => &LAYER,
335 }
336 }
337
338 pub fn frames(&self) -> &'static [&'static str] {
340 self.def().frames
341 }
342
343 pub fn interval_ms(&self) -> u64 {
345 self.def().interval_ms
346 }
347
348 pub fn from_name(name: &str) -> Option<SpinnerStyle> {
362 let name_lower = name.to_lowercase().replace('_', "");
363 match name_lower.as_str() {
364 "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 "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 "star" => Some(SpinnerStyle::Star),
393 "star2" => Some(SpinnerStyle::Star2),
394
395 "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 "boxbounce" => Some(SpinnerStyle::BoxBounce),
409 "boxbounce2" => Some(SpinnerStyle::BoxBounce2),
410 "noise" => Some(SpinnerStyle::Noise),
411
412 "growvertical" => Some(SpinnerStyle::GrowVertical),
414 "growhorizontal" => Some(SpinnerStyle::GrowHorizontal),
415 "balloon" => Some(SpinnerStyle::Balloon),
416 "balloon2" => Some(SpinnerStyle::Balloon2),
417
418 "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 "arrow" => Some(SpinnerStyle::Arrow),
435 "arrow2" => Some(SpinnerStyle::Arrow2),
436 "arrow3" => Some(SpinnerStyle::Arrow3),
437
438 "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 "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 "dqpb" => Some(SpinnerStyle::Dqpb),
468 "point" => Some(SpinnerStyle::Point),
469 "layer" => Some(SpinnerStyle::Layer),
470
471 _ => None,
472 }
473 }
474
475 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#[derive(Debug, Clone)]
569pub struct Spinner {
570 style: SpinnerStyle,
572 start_time: Instant,
574 text: String,
576 spinner_style: Style,
578 text_style: Style,
580}
581
582impl Spinner {
583 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 pub fn style(mut self, style: SpinnerStyle) -> Self {
596 self.style = style;
597 self
598 }
599
600 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 pub fn spinner_style(mut self, style: Style) -> Self {
612 self.spinner_style = style;
613 self
614 }
615
616 pub fn text_style(mut self, style: Style) -> Self {
618 self.text_style = style;
619 self
620 }
621
622 pub fn text(mut self, text: &str) -> Self {
624 self.text = text.to_string();
625 self
626 }
627
628 pub fn set_text(&mut self, text: &str) {
630 self.text = text.to_string();
631 }
632
633 pub fn get_text(&self) -> &str {
635 &self.text
636 }
637
638 pub fn get_style(&self) -> SpinnerStyle {
640 self.style
641 }
642
643 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 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 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 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 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}