1use crate::style::{Color, Style};
4use crate::text::Span;
5use std::time::Instant;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum SpinnerStyle {
10 #[default]
12 Dots,
13 Line,
15 Dots2,
17 Arc,
19 Circle,
21 Square,
23 Star,
25 Bounce,
27 Box,
29 Simple,
31}
32
33impl SpinnerStyle {
34 pub fn frames(&self) -> &'static [&'static str] {
36 match self {
37 SpinnerStyle::Dots => &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
38 SpinnerStyle::Line => &["-", "\\", "|", "/"],
39 SpinnerStyle::Dots2 => &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
40 SpinnerStyle::Arc => &["◜", "◠", "◝", "◞", "◡", "◟"],
41 SpinnerStyle::Circle => &["◐", "◓", "◑", "◒"],
42 SpinnerStyle::Square => &["◰", "◳", "◲", "◱"],
43 SpinnerStyle::Star => &["✶", "✸", "✹", "✺", "✹", "✷"],
44 SpinnerStyle::Bounce => &["⠁", "⠂", "⠄", "⠂"],
45 SpinnerStyle::Box => &["▖", "▘", "▝", "▗"],
46 SpinnerStyle::Simple => &["◴", "◷", "◶", "◵"],
47 }
48 }
49
50 pub fn interval_ms(&self) -> u64 {
52 match self {
53 SpinnerStyle::Dots | SpinnerStyle::Dots2 => 80,
54 SpinnerStyle::Line => 130,
55 SpinnerStyle::Arc | SpinnerStyle::Circle => 100,
56 SpinnerStyle::Square => 120,
57 SpinnerStyle::Star => 70,
58 SpinnerStyle::Bounce => 120,
59 SpinnerStyle::Box => 100,
60 SpinnerStyle::Simple => 100,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct Spinner {
68 style: SpinnerStyle,
70 start_time: Instant,
72 text: String,
74 spinner_style: Style,
76 text_style: Style,
78}
79
80impl Spinner {
81 pub fn new(text: &str) -> Self {
83 Spinner {
84 style: SpinnerStyle::Dots,
85 start_time: Instant::now(),
86 text: text.to_string(),
87 spinner_style: Style::new().foreground(Color::Cyan),
88 text_style: Style::new(),
89 }
90 }
91
92 pub fn style(mut self, style: SpinnerStyle) -> Self {
94 self.style = style;
95 self
96 }
97
98 pub fn spinner_style(mut self, style: Style) -> Self {
100 self.spinner_style = style;
101 self
102 }
103
104 pub fn text_style(mut self, style: Style) -> Self {
106 self.text_style = style;
107 self
108 }
109
110 pub fn text(mut self, text: &str) -> Self {
112 self.text = text.to_string();
113 self
114 }
115
116 pub fn set_text(&mut self, text: &str) {
118 self.text = text.to_string();
119 }
120
121 pub fn get_text(&self) -> &str {
123 &self.text
124 }
125
126 pub fn get_style(&self) -> SpinnerStyle {
128 self.style
129 }
130
131 fn current_frame_index(&self) -> usize {
133 let elapsed_ms = self.start_time.elapsed().as_millis() as u64;
134 let interval = self.style.interval_ms();
135 let frames = self.style.frames();
136 ((elapsed_ms / interval) as usize) % frames.len()
137 }
138
139 pub fn current_frame(&self) -> &'static str {
141 let frames = self.style.frames();
142 let idx = self.current_frame_index();
143 frames[idx]
144 }
145
146 pub fn render(&self) -> Vec<Span> {
148 vec![
149 Span::styled(self.current_frame().to_string(), self.spinner_style),
150 Span::raw(" "),
151 Span::styled(self.text.clone(), self.text_style),
152 ]
153 }
154
155 pub fn to_string_colored(&self) -> String {
157 format!("{} {}", self.current_frame(), self.text)
158 }
159}
160
161impl Default for Spinner {
162 fn default() -> Self {
163 Spinner::new("")
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_spinner_frames() {
173 let style = SpinnerStyle::Dots;
174 let frames = style.frames();
175 assert!(!frames.is_empty());
176 assert_eq!(frames[0], "⠋");
177 }
178
179 #[test]
180 fn test_spinner_render() {
181 let spinner = Spinner::new("Loading...");
182 let spans = spinner.render();
183 assert_eq!(spans.len(), 3);
184 }
185
186 #[test]
187 fn test_all_spinner_styles() {
188 let styles = [
189 SpinnerStyle::Dots,
190 SpinnerStyle::Line,
191 SpinnerStyle::Dots2,
192 SpinnerStyle::Arc,
193 SpinnerStyle::Circle,
194 SpinnerStyle::Square,
195 SpinnerStyle::Star,
196 SpinnerStyle::Bounce,
197 SpinnerStyle::Box,
198 SpinnerStyle::Simple,
199 ];
200
201 for style in styles {
202 let frames = style.frames();
203 assert!(!frames.is_empty(), "{:?} has no frames", style);
204 }
205 }
206}