1mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_spinner;
8pub use node::SpinnerNode;
9pub use reconcile::reconcile_spinner;
10
11use std::sync::Arc;
12
13use crate::core::element::{Element, ElementKind};
14use crate::style::{Length, Style};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum SpinnerStyle {
19 #[default]
21 Dots,
22 Line,
24 Circle,
26 Arc,
28 Braille,
30 Moon,
32 Box,
34 Bar,
36 Arrow,
38 Fade,
40 Trail,
42 Earth,
44 Claude,
46 OpenCode,
48 ThreeDot,
50 ThreeDotFade,
52 SquareFade,
54 Lightsaber,
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
60pub enum SpinnerSpeed {
61 Slow,
63 #[default]
65 Normal,
66 Fast,
68 Custom {
70 frame_ms: u16,
72 },
73}
74
75impl SpinnerStyle {
76 pub fn frames(self) -> &'static [&'static str] {
78 match self {
79 Self::Dots => &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
80 Self::Line => &["|", "/", "-", "\\"],
81 Self::Circle => &["◐", "◓", "◑", "◒"],
82 Self::Arc => &["◜", "◠", "◝", "◞", "◡", "◟"],
83 Self::Braille => &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
84 Self::Moon => &["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
85 Self::Box => &["▖", "▘", "▝", "▗"],
86 Self::Bar => &[
87 " ", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂",
88 ],
89 Self::Arrow => &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
90 Self::Fade => &["█", "▓", "▒", "░", "▒", "▓"],
91 Self::Trail => &[
92 "█ ",
93 "▓█ ",
94 "▒▓█ ",
95 "░▒▓█ ",
96 " ░▒▓█",
97 " ░▒▓",
98 " ░▒",
99 " ░",
100 ],
101 Self::Earth => &["🌍", "🌎", "🌏"],
102 Self::Claude => &["·", "✢", "✳", "✶", "✻", "*", "✻", "✶", "✳", "✢"],
103 Self::OpenCode => &[
106 "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝",
107 "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝",
108 ],
109 Self::ThreeDot => &["∙∙∙", "●∙∙", "∙●∙", "∙∙●"],
110 Self::ThreeDotFade => &["∙∙∙", "●∙∙", "•●∙", "∙•●", "∙∙•"],
111 Self::SquareFade => &["▱▱▱", "▰▱▱", "▰▰▱", "▰▰▰", "▰▰▱", "▰▱▱", "▱▱▱"],
112 Self::Lightsaber => &[
113 "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
114 "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
115 "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
116 "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
117 "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
118 ],
119 }
120 }
121
122 pub fn width(self) -> u16 {
124 match self {
125 Self::Moon | Self::Earth => 2,
126 Self::Trail => 5,
127 Self::OpenCode => 8,
128 Self::Lightsaber => 16,
129 Self::ThreeDot | Self::ThreeDotFade | Self::SquareFade => 3,
130 _ => 1,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::{Spinner, SpinnerStyle};
138
139 #[test]
140 fn claude_spinner_uses_requested_mirrored_sequence() {
141 assert_eq!(
142 SpinnerStyle::Claude.frames(),
143 &["·", "✢", "✳", "✶", "✻", "*", "✻", "✶", "✳", "✢"],
144 );
145 }
146
147 #[test]
148 fn claude_spinner_tick_wraps_after_one_cycle() {
149 let mut spinner = Spinner::new().spinner_style(SpinnerStyle::Claude);
150 for _ in 0..SpinnerStyle::Claude.frames().len() {
151 spinner.tick();
152 }
153 assert_eq!(spinner.current_frame(), "·");
154 }
155}
156
157#[derive(Clone, Debug)]
159pub struct Spinner {
160 pub spinner_style: SpinnerStyle,
162 pub speed: SpinnerSpeed,
164 pub frame: Option<usize>,
166 pub label: Option<Arc<str>>,
168 pub gap: u16,
170 pub style: Style,
172 pub label_style: Style,
174 pub width: Length,
177 pub height: Length,
180}
181
182impl Default for Spinner {
183 fn default() -> Self {
184 Self {
185 spinner_style: SpinnerStyle::Dots,
186 speed: SpinnerSpeed::Normal,
187 frame: None,
188 label: None,
189 gap: 1,
190 style: Style::default(),
191 label_style: Style::default(),
192 width: Length::Auto,
193 height: Length::Auto,
194 }
195 }
196}
197
198impl Spinner {
199 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn spinner_style(mut self, style: SpinnerStyle) -> Self {
206 self.spinner_style = style;
207 self
208 }
209
210 pub fn speed(mut self, speed: SpinnerSpeed) -> Self {
212 self.speed = speed;
213 self
214 }
215
216 pub fn frame(mut self, frame: usize) -> Self {
218 self.frame = Some(frame);
219 self
220 }
221
222 pub fn tick(&mut self) {
224 let frames = self.spinner_style.frames();
225 let current = self.frame.unwrap_or(0);
226 self.frame = Some((current + 1) % frames.len());
227 }
228
229 pub fn current_frame(&self) -> &'static str {
231 let frames = self.spinner_style.frames();
232 frames[self.frame.unwrap_or(0) % frames.len()]
233 }
234
235 pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
237 self.label = Some(label.into());
238 self
239 }
240
241 pub fn gap(mut self, gap: u16) -> Self {
243 self.gap = gap;
244 self
245 }
246
247 pub fn style(mut self, style: Style) -> Self {
249 self.style = style;
250 self
251 }
252
253 pub fn label_style(mut self, style: Style) -> Self {
255 self.label_style = style;
256 self
257 }
258
259 pub fn width(mut self, width: Length) -> Self {
261 self.width = width;
262 self
263 }
264
265 pub fn height(mut self, height: Length) -> Self {
267 self.height = height;
268 self
269 }
270}
271
272impl From<Spinner> for Element {
273 fn from(value: Spinner) -> Self {
274 Element::new(ElementKind::Spinner(value))
275 }
276}
277
278impl crate::layout::hash::LayoutHash for Spinner {
279 fn layout_hash(
280 &self,
281 hasher: &mut impl std::hash::Hasher,
282 _recurse: &dyn Fn(&Element) -> Option<u64>,
283 ) -> Option<()> {
284 use std::hash::Hash;
285 self.width.hash(hasher);
286 self.height.hash(hasher);
287 self.spinner_style.hash(hasher);
288 self.gap.hash(hasher);
289 self.label.hash(hasher);
290 Some(())
291 }
292}