use crate::errors::{Error, Result};
use std::collections::HashMap;
use std::fmt;
use std::sync::LazyLock;
#[derive(Debug, Clone)]
struct SpinnerData {
interval: u64,
frames: Vec<&'static str>,
}
static SPINNERS: LazyLock<HashMap<&'static str, SpinnerData>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert(
"dots",
SpinnerData {
interval: 80,
frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
},
);
m.insert(
"dots2",
SpinnerData {
interval: 80,
frames: vec!["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
},
);
m.insert(
"dots3",
SpinnerData {
interval: 80,
frames: vec!["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
},
);
m.insert(
"line",
SpinnerData {
interval: 130,
frames: vec!["-", "\\", "|", "/"],
},
);
m.insert(
"line2",
SpinnerData {
interval: 100,
frames: vec!["⠂", "-", "–", "—", "–", "-"],
},
);
m.insert(
"pipe",
SpinnerData {
interval: 100,
frames: vec!["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
},
);
m.insert(
"simpleDots",
SpinnerData {
interval: 400,
frames: vec![". ", ".. ", "...", " "],
},
);
m.insert(
"star",
SpinnerData {
interval: 70,
frames: vec!["✶", "✸", "✹", "✺", "✹", "✷"],
},
);
m.insert(
"star2",
SpinnerData {
interval: 80,
frames: vec!["+", "x", "*"],
},
);
m.insert(
"arc",
SpinnerData {
interval: 100,
frames: vec!["◜", "◠", "◝", "◞", "◡", "◟"],
},
);
m.insert(
"circle",
SpinnerData {
interval: 120,
frames: vec!["◡", "⊙", "◠"],
},
);
m.insert(
"squareCorners",
SpinnerData {
interval: 180,
frames: vec!["◰", "◳", "◲", "◱"],
},
);
m.insert(
"circleQuarters",
SpinnerData {
interval: 120,
frames: vec!["◴", "◷", "◶", "◵"],
},
);
m.insert(
"circleHalves",
SpinnerData {
interval: 50,
frames: vec!["◐", "◓", "◑", "◒"],
},
);
m.insert(
"moon",
SpinnerData {
interval: 80,
frames: vec!["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
},
);
m.insert(
"runner",
SpinnerData {
interval: 140,
frames: vec!["🚶", "🏃"],
},
);
m.insert(
"bounce",
SpinnerData {
interval: 120,
frames: vec!["⠁", "⠂", "⠄", "⠂"],
},
);
m.insert(
"bouncingBar",
SpinnerData {
interval: 80,
frames: vec![
"[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]",
"[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]",
],
},
);
m.insert(
"bouncingBall",
SpinnerData {
interval: 80,
frames: vec![
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"( ●)",
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"(● )",
],
},
);
m.insert(
"clock",
SpinnerData {
interval: 100,
frames: vec![
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
],
},
);
m.insert(
"earth",
SpinnerData {
interval: 180,
frames: vec!["🌍", "🌎", "🌏"],
},
);
m.insert(
"hearts",
SpinnerData {
interval: 100,
frames: vec!["💛", "💙", "💜", "💚", "❤️"],
},
);
m.insert(
"arrow",
SpinnerData {
interval: 100,
frames: vec!["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
},
);
m.insert(
"arrow2",
SpinnerData {
interval: 80,
frames: vec!["⬆️", "↗️", "➡️", "↘️", "⬇️", "↙️", "⬅️", "↖️"],
},
);
m.insert(
"arrow3",
SpinnerData {
interval: 120,
frames: vec!["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
},
);
m.insert(
"toggle",
SpinnerData {
interval: 250,
frames: vec!["⊶", "⊷"],
},
);
m.insert(
"toggle2",
SpinnerData {
interval: 80,
frames: vec!["▫", "▪"],
},
);
m.insert(
"toggle3",
SpinnerData {
interval: 120,
frames: vec!["□", "■"],
},
);
m.insert(
"toggle4",
SpinnerData {
interval: 100,
frames: vec!["■", "□", "▪", "▫"],
},
);
m.insert(
"toggle5",
SpinnerData {
interval: 100,
frames: vec!["▮", "▯"],
},
);
m.insert(
"toggle6",
SpinnerData {
interval: 300,
frames: vec!["ဝ", "၀"],
},
);
m.insert(
"toggle7",
SpinnerData {
interval: 80,
frames: vec!["⦾", "⦿"],
},
);
m.insert(
"toggle8",
SpinnerData {
interval: 100,
frames: vec!["◍", "◌"],
},
);
m.insert(
"toggle9",
SpinnerData {
interval: 100,
frames: vec!["◉", "◎"],
},
);
m.insert(
"toggle10",
SpinnerData {
interval: 100,
frames: vec!["㊂", "㊀", "㊁"],
},
);
m.insert(
"toggle11",
SpinnerData {
interval: 50,
frames: vec!["⧇", "⧆"],
},
);
m.insert(
"toggle12",
SpinnerData {
interval: 120,
frames: vec!["☗", "☖"],
},
);
m.insert(
"toggle13",
SpinnerData {
interval: 80,
frames: vec!["=", "*", "-"],
},
);
m.insert(
"boxBounce",
SpinnerData {
interval: 120,
frames: vec!["▖", "▘", "▝", "▗"],
},
);
m.insert(
"boxBounce2",
SpinnerData {
interval: 100,
frames: vec!["▌", "▀", "▐", "▄"],
},
);
m.insert(
"triangle",
SpinnerData {
interval: 50,
frames: vec!["◢", "◣", "◤", "◥"],
},
);
m.insert(
"point",
SpinnerData {
interval: 125,
frames: vec!["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"],
},
);
m.insert(
"layer",
SpinnerData {
interval: 150,
frames: vec!["-", "=", "≡"],
},
);
m.insert(
"aesthetic",
SpinnerData {
interval: 80,
frames: vec![
"▰▱▱▱▱▱▱",
"▰▰▱▱▱▱▱",
"▰▰▰▱▱▱▱",
"▰▰▰▰▱▱▱",
"▰▰▰▰▰▱▱",
"▰▰▰▰▰▰▱",
"▰▰▰▰▰▰▰",
"▰▱▱▱▱▱▱",
],
},
);
m.insert(
"simple",
SpinnerData {
interval: 100,
frames: vec!["/", "-", "\\", "|"],
},
);
m
});
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Spinner {
name: String,
interval: u64,
frames: Vec<String>,
current: usize,
}
impl Spinner {
#[inline]
pub fn new(name: &str) -> Result<Self> {
SPINNERS.get(name).map_or_else(
|| Err(Error::NoSpinner(name.to_owned())),
|data| {
Ok(Self {
name: name.to_owned(),
interval: data.interval,
frames: data.frames.iter().map(|&s| s.to_owned()).collect(),
current: 0,
})
},
)
}
#[inline]
#[must_use]
pub fn custom(frames: Vec<String>, interval: u64) -> Self {
Self {
name: "custom".to_owned(),
interval,
frames,
current: 0,
}
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
#[must_use]
pub fn interval(&self) -> u64 {
self.interval
}
#[inline]
#[must_use]
pub fn frame_count(&self) -> usize {
self.frames.len()
}
#[inline]
#[must_use]
pub fn current_frame(&self) -> &str {
self.frames
.get(self.current)
.map_or("", |frame| frame.as_str())
}
#[inline]
pub fn next_frame(&mut self) -> &str {
if self.frames.is_empty() {
return "";
}
let frame = self.frames.get(self.current).map_or("", |f| f.as_str());
self.current = self
.current
.checked_add(1)
.map_or(0, |n| n % self.frames.len());
frame
}
#[inline]
pub fn reset(&mut self) {
self.current = 0;
}
#[inline]
pub fn frames(&self) -> SpinnerFrames<'_> {
SpinnerFrames {
frames: &self.frames,
current: 0,
}
}
#[inline]
#[must_use]
pub fn exists(name: &str) -> bool {
SPINNERS.contains_key(name)
}
#[inline]
#[must_use]
pub fn count() -> usize {
SPINNERS.len()
}
#[inline]
pub fn names() -> impl Iterator<Item = &'static str> {
SPINNERS.keys().copied()
}
}
impl fmt::Display for Spinner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.current_frame())
}
}
#[derive(Debug)]
pub struct SpinnerFrames<'a> {
frames: &'a [String],
current: usize,
}
impl<'a> Iterator for SpinnerFrames<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.frames.is_empty() {
return None;
}
let frame = self.frames.get(self.current)?;
self.current = self
.current
.checked_add(1)
.map_or(0, |n| n % self.frames.len());
Some(frame.as_str())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_spinner_new_valid() {
let spinner = Spinner::new("dots").unwrap();
assert_eq!(spinner.name(), "dots");
assert_eq!(spinner.interval(), 80);
assert_eq!(spinner.frame_count(), 10);
}
#[test]
fn test_spinner_new_invalid() {
let result = Spinner::new("nonexistent_spinner");
assert!(result.is_err());
}
#[test]
fn test_spinner_exists() {
assert!(Spinner::exists("dots"));
assert!(Spinner::exists("line"));
assert!(Spinner::exists("arc"));
assert!(!Spinner::exists("nonexistent"));
}
#[test]
fn test_spinner_count() {
assert!(Spinner::count() > 30);
}
#[test]
fn test_spinner_names() {
let names: Vec<_> = Spinner::names().collect();
assert!(names.contains(&"dots"));
assert!(names.contains(&"line"));
assert!(names.contains(&"arc"));
}
#[test]
fn test_spinner_current_frame() {
let spinner = Spinner::new("dots").unwrap();
let frame = spinner.current_frame();
assert!(!frame.is_empty());
}
#[test]
fn test_spinner_next_frame() {
let mut spinner = Spinner::new("line").unwrap();
assert_eq!(spinner.next_frame(), "-");
assert_eq!(spinner.next_frame(), "\\");
assert_eq!(spinner.next_frame(), "|");
assert_eq!(spinner.next_frame(), "/");
assert_eq!(spinner.next_frame(), "-");
}
#[test]
fn test_spinner_reset() {
let mut spinner = Spinner::new("line").unwrap();
spinner.next_frame();
spinner.next_frame();
spinner.reset();
assert_eq!(spinner.next_frame(), "-");
}
#[test]
fn test_spinner_frames_iterator() {
let spinner = Spinner::new("line").unwrap();
let frames: Vec<_> = spinner.frames().take(8).collect();
assert_eq!(frames.len(), 8);
assert_eq!(frames[0], "-");
assert_eq!(frames[1], "\\");
assert_eq!(frames[4], "-"); }
#[test]
fn test_spinner_custom() {
let spinner = Spinner::custom(vec![".".to_owned(), "..".to_owned(), "...".to_owned()], 200);
assert_eq!(spinner.name(), "custom");
assert_eq!(spinner.interval(), 200);
assert_eq!(spinner.frame_count(), 3);
}
#[test]
fn test_spinner_display() {
let spinner = Spinner::new("dots").unwrap();
let display = format!("{spinner}");
assert!(!display.is_empty());
}
#[test]
fn test_common_spinners() {
let test_spinners = [
"dots",
"line",
"arc",
"moon",
"clock",
"earth",
"bouncingBar",
"arrow",
"toggle",
];
for name in test_spinners {
let spinner = Spinner::new(name).unwrap();
assert!(spinner.frame_count() > 0, "{name} should have frames");
assert!(spinner.interval() > 0, "{name} should have interval");
}
}
#[test]
fn test_spinner_clone() {
let spinner = Spinner::new("dots").unwrap();
let cloned = spinner.clone();
assert_eq!(spinner.name(), cloned.name());
assert_eq!(spinner.interval(), cloned.interval());
}
}