mod layout;
mod node;
mod reconcile;
pub use layout::measure_spinner;
pub use node::SpinnerNode;
pub use reconcile::reconcile_spinner;
use std::sync::Arc;
use crate::core::element::{Element, ElementKind};
use crate::style::{Length, Style};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SpinnerStyle {
#[default]
Dots,
Line,
Circle,
Arc,
Braille,
Moon,
Box,
Bar,
Arrow,
Fade,
Trail,
Earth,
Claude,
OpenCode,
ThreeDot,
ThreeDotFade,
SquareFade,
Lightsaber,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SpinnerSpeed {
Slow,
#[default]
Normal,
Fast,
Custom {
frame_ms: u16,
},
}
impl SpinnerStyle {
pub fn frames(self) -> &'static [&'static str] {
match self {
Self::Dots => &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
Self::Line => &["|", "/", "-", "\\"],
Self::Circle => &["◐", "◓", "◑", "◒"],
Self::Arc => &["◜", "◠", "◝", "◞", "◡", "◟"],
Self::Braille => &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
Self::Moon => &["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
Self::Box => &["▖", "▘", "▝", "▗"],
Self::Bar => &[
" ", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂",
],
Self::Arrow => &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
Self::Fade => &["█", "▓", "▒", "░", "▒", "▓"],
Self::Trail => &[
"█ ",
"▓█ ",
"▒▓█ ",
"░▒▓█ ",
" ░▒▓█",
" ░▒▓",
" ░▒",
" ░",
],
Self::Earth => &["🌍", "🌎", "🌏"],
Self::Claude => &["·", "✢", "✳", "✶", "✻", "*", "✻", "✶", "✳", "✢"],
Self::OpenCode => &[
"⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝",
"⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝", "⬝",
],
Self::ThreeDot => &["∙∙∙", "●∙∙", "∙●∙", "∙∙●"],
Self::ThreeDotFade => &["∙∙∙", "●∙∙", "•●∙", "∙•●", "∙∙•"],
Self::SquareFade => &["▱▱▱", "▰▱▱", "▰▰▱", "▰▰▰", "▰▰▱", "▰▱▱", "▱▱▱"],
Self::Lightsaber => &[
"═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
"═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
"═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
"═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
"═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═", "═",
],
}
}
pub fn width(self) -> u16 {
match self {
Self::Moon | Self::Earth => 2,
Self::Trail => 5,
Self::OpenCode => 8,
Self::Lightsaber => 16,
Self::ThreeDot | Self::ThreeDotFade | Self::SquareFade => 3,
_ => 1,
}
}
}
#[cfg(test)]
mod tests {
use super::{Spinner, SpinnerStyle};
#[test]
fn claude_spinner_uses_requested_mirrored_sequence() {
assert_eq!(
SpinnerStyle::Claude.frames(),
&["·", "✢", "✳", "✶", "✻", "*", "✻", "✶", "✳", "✢"],
);
}
#[test]
fn claude_spinner_tick_wraps_after_one_cycle() {
let mut spinner = Spinner::new().spinner_style(SpinnerStyle::Claude);
for _ in 0..SpinnerStyle::Claude.frames().len() {
spinner.tick();
}
assert_eq!(spinner.current_frame(), "·");
}
}
#[derive(Clone, Debug)]
pub struct Spinner {
pub spinner_style: SpinnerStyle,
pub speed: SpinnerSpeed,
pub frame: Option<usize>,
pub label: Option<Arc<str>>,
pub gap: u16,
pub style: Style,
pub label_style: Style,
pub width: Length,
pub height: Length,
}
impl Default for Spinner {
fn default() -> Self {
Self {
spinner_style: SpinnerStyle::Dots,
speed: SpinnerSpeed::Normal,
frame: None,
label: None,
gap: 1,
style: Style::default(),
label_style: Style::default(),
width: Length::Auto,
height: Length::Auto,
}
}
}
impl Spinner {
pub fn new() -> Self {
Self::default()
}
pub fn spinner_style(mut self, style: SpinnerStyle) -> Self {
self.spinner_style = style;
self
}
pub fn speed(mut self, speed: SpinnerSpeed) -> Self {
self.speed = speed;
self
}
pub fn frame(mut self, frame: usize) -> Self {
self.frame = Some(frame);
self
}
pub fn tick(&mut self) {
let frames = self.spinner_style.frames();
let current = self.frame.unwrap_or(0);
self.frame = Some((current + 1) % frames.len());
}
pub fn current_frame(&self) -> &'static str {
let frames = self.spinner_style.frames();
frames[self.frame.unwrap_or(0) % frames.len()]
}
pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
self.label = Some(label.into());
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn label_style(mut self, style: Style) -> Self {
self.label_style = style;
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
}
impl From<Spinner> for Element {
fn from(value: Spinner) -> Self {
Element::new(ElementKind::Spinner(value))
}
}
impl crate::layout::hash::LayoutHash for Spinner {
fn layout_hash(
&self,
hasher: &mut impl std::hash::Hasher,
_recurse: &dyn Fn(&Element) -> Option<u64>,
) -> Option<()> {
use std::hash::Hash;
self.width.hash(hasher);
self.height.hash(hasher);
self.spinner_style.hash(hasher);
self.gap.hash(hasher);
self.label.hash(hasher);
Some(())
}
}