use bubbletea_rs::{tick as bubbletea_tick, Cmd, Model as BubbleTeaModel, Msg};
use lipgloss_extras::prelude::*;
use once_cell::sync::Lazy;
use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;
static LAST_ID: AtomicI64 = AtomicI64::new(0);
fn next_id() -> i64 {
LAST_ID.fetch_add(1, Ordering::SeqCst) + 1
}
#[derive(Debug, Clone)]
pub struct Spinner {
pub frames: Vec<String>,
pub fps: Duration,
}
impl Spinner {
pub fn new(frames: Vec<String>, fps: Duration) -> Self {
Self { frames, fps }
}
}
pub static LINE: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"|".to_string(),
"/".to_string(),
"-".to_string(),
"\\".to_string(),
],
fps: Duration::from_millis(100), });
pub static DOT: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"⣾ ".to_string(),
"⣽ ".to_string(),
"⣻ ".to_string(),
"⢿ ".to_string(),
"⡿ ".to_string(),
"⣟ ".to_string(),
"⣯ ".to_string(),
"⣷ ".to_string(),
],
fps: Duration::from_millis(100), });
pub static MINI_DOT: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"⠋".to_string(),
"⠙".to_string(),
"⠹".to_string(),
"⠸".to_string(),
"⠼".to_string(),
"⠴".to_string(),
"⠦".to_string(),
"⠧".to_string(),
"⠇".to_string(),
"⠏".to_string(),
],
fps: Duration::from_millis(83), });
pub static JUMP: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"⢄".to_string(),
"⢂".to_string(),
"⢁".to_string(),
"⡁".to_string(),
"⡈".to_string(),
"⡐".to_string(),
"⡠".to_string(),
],
fps: Duration::from_millis(100), });
pub static PULSE: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"█".to_string(),
"▓".to_string(),
"▒".to_string(),
"░".to_string(),
],
fps: Duration::from_millis(125), });
pub static POINTS: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"∙∙∙".to_string(),
"●∙∙".to_string(),
"∙●∙".to_string(),
"∙∙●".to_string(),
],
fps: Duration::from_millis(143), });
pub static GLOBE: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec!["🌍".to_string(), "🌎".to_string(), "🌏".to_string()],
fps: Duration::from_millis(250), });
pub static MOON: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"🌑".to_string(),
"🌒".to_string(),
"🌓".to_string(),
"🌔".to_string(),
"🌕".to_string(),
"🌖".to_string(),
"🌗".to_string(),
"🌘".to_string(),
],
fps: Duration::from_millis(125), });
pub static MONKEY: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec!["🙈".to_string(), "🙉".to_string(), "🙊".to_string()],
fps: Duration::from_millis(333), });
pub static METER: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"▱▱▱".to_string(),
"▰▱▱".to_string(),
"▰▰▱".to_string(),
"▰▰▰".to_string(),
"▰▰▱".to_string(),
"▰▱▱".to_string(),
"▱▱▱".to_string(),
],
fps: Duration::from_millis(143), });
pub static HAMBURGER: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"☱".to_string(),
"☲".to_string(),
"☴".to_string(),
"☲".to_string(),
],
fps: Duration::from_millis(333), });
pub static ELLIPSIS: Lazy<Spinner> = Lazy::new(|| Spinner {
frames: vec![
"".to_string(),
".".to_string(),
"..".to_string(),
"...".to_string(),
],
fps: Duration::from_millis(333), });
#[deprecated(since = "0.0.7", note = "use LINE constant instead")]
pub fn line() -> Spinner {
LINE.clone()
}
#[deprecated(since = "0.0.7", note = "use DOT constant instead")]
pub fn dot() -> Spinner {
DOT.clone()
}
#[deprecated(since = "0.0.7", note = "use MINI_DOT constant instead")]
pub fn mini_dot() -> Spinner {
MINI_DOT.clone()
}
#[deprecated(since = "0.0.7", note = "use JUMP constant instead")]
pub fn jump() -> Spinner {
JUMP.clone()
}
#[deprecated(since = "0.0.7", note = "use PULSE constant instead")]
pub fn pulse() -> Spinner {
PULSE.clone()
}
#[deprecated(since = "0.0.7", note = "use POINTS constant instead")]
pub fn points() -> Spinner {
POINTS.clone()
}
#[deprecated(since = "0.0.7", note = "use GLOBE constant instead")]
pub fn globe() -> Spinner {
GLOBE.clone()
}
#[deprecated(since = "0.0.7", note = "use MOON constant instead")]
pub fn moon() -> Spinner {
MOON.clone()
}
#[deprecated(since = "0.0.7", note = "use MONKEY constant instead")]
pub fn monkey() -> Spinner {
MONKEY.clone()
}
#[deprecated(since = "0.0.7", note = "use METER constant instead")]
pub fn meter() -> Spinner {
METER.clone()
}
#[deprecated(since = "0.0.7", note = "use HAMBURGER constant instead")]
pub fn hamburger() -> Spinner {
HAMBURGER.clone()
}
#[deprecated(since = "0.0.7", note = "use ELLIPSIS constant instead")]
pub fn ellipsis() -> Spinner {
ELLIPSIS.clone()
}
#[derive(Debug, Clone)]
pub struct TickMsg {
pub time: std::time::SystemTime,
pub id: i64,
tag: i64,
}
#[derive(Debug)]
pub struct Model {
pub spinner: Spinner,
pub style: Style,
frame: usize,
id: i64,
tag: i64,
}
pub enum SpinnerOption {
WithSpinner(Spinner),
WithStyle(Box<Style>),
}
impl SpinnerOption {
fn apply(&self, m: &mut Model) {
match self {
SpinnerOption::WithSpinner(spinner) => m.spinner = spinner.clone(),
SpinnerOption::WithStyle(style) => m.style = style.as_ref().clone(),
}
}
}
pub fn with_spinner(spinner: Spinner) -> SpinnerOption {
SpinnerOption::WithSpinner(spinner)
}
pub fn with_style(style: Style) -> SpinnerOption {
SpinnerOption::WithStyle(Box::new(style))
}
impl Model {
pub fn new() -> Self {
Self {
spinner: LINE.clone(),
style: Style::new(),
frame: 0,
id: next_id(),
tag: 0,
}
}
pub fn new_with_options(opts: &[SpinnerOption]) -> Self {
let mut m = Self {
spinner: LINE.clone(),
style: Style::new(),
frame: 0,
id: next_id(),
tag: 0,
};
for opt in opts {
opt.apply(&mut m);
}
m
}
pub fn with_spinner(mut self, spinner: Spinner) -> Self {
self.spinner = spinner;
self
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn id(&self) -> i64 {
self.id
}
pub fn tick_msg(&self) -> TickMsg {
TickMsg {
time: std::time::SystemTime::now(),
id: self.id,
tag: self.tag,
}
}
fn tick(&self) -> Cmd {
let id = self.id;
let tag = self.tag;
let fps = self.spinner.fps;
bubbletea_tick(fps, move |_| {
Box::new(TickMsg {
time: std::time::SystemTime::now(),
id,
tag,
}) as Msg
})
}
}
impl Default for Model {
fn default() -> Self {
Self::new()
}
}
impl Model {
pub fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
if let Some(tick_msg) = msg.downcast_ref::<TickMsg>() {
if tick_msg.id > 0 && tick_msg.id != self.id {
return None;
}
if tick_msg.tag > 0 && tick_msg.tag != self.tag {
return None;
}
self.frame += 1;
if self.frame >= self.spinner.frames.len() {
self.frame = 0;
}
self.tag += 1;
return std::option::Option::Some(self.tick());
}
std::option::Option::None
}
pub fn view(&self) -> String {
if self.frame >= self.spinner.frames.len() {
return "(error)".to_string();
}
self.style.render(&self.spinner.frames[self.frame])
}
}
impl BubbleTeaModel for Model {
fn init() -> (Self, std::option::Option<Cmd>) {
let model = Self::new();
let cmd = model.tick();
(model, std::option::Option::Some(cmd))
}
fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
self.update(msg)
}
fn view(&self) -> String {
self.view()
}
}
pub fn new(opts: &[SpinnerOption]) -> Model {
Model::new_with_options(opts)
}
#[deprecated(since = "0.0.7", note = "use new instead")]
pub fn new_model(opts: &[SpinnerOption]) -> Model {
new(opts)
}
#[deprecated(since = "0.0.7", note = "use Model::tick_msg instead")]
pub fn tick() -> TickMsg {
TickMsg {
time: std::time::SystemTime::now(),
id: 0,
tag: 0,
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::spinner::{
dot, line, new, new_model, tick, with_spinner, with_style, DOT, ELLIPSIS, GLOBE, HAMBURGER,
JUMP, LINE, METER, MINI_DOT, MONKEY, MOON, POINTS, PULSE,
};
#[test]
fn test_spinner_constants() {
assert_eq!(LINE.frames.len(), 4);
assert_eq!(DOT.frames.len(), 8);
assert_eq!(MINI_DOT.frames.len(), 10);
assert_eq!(JUMP.frames.len(), 7);
assert_eq!(PULSE.frames.len(), 4);
assert_eq!(POINTS.frames.len(), 4);
assert_eq!(GLOBE.frames.len(), 3);
assert_eq!(MOON.frames.len(), 8);
assert_eq!(MONKEY.frames.len(), 3);
assert_eq!(METER.frames.len(), 7);
assert_eq!(HAMBURGER.frames.len(), 4);
assert_eq!(ELLIPSIS.frames.len(), 4);
}
#[test]
fn test_spinner_frames_match_go() {
assert_eq!(LINE.frames, vec!["|", "/", "-", "\\"]);
assert_eq!(
DOT.frames,
vec!["⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "]
);
assert_eq!(
MINI_DOT.frames,
vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
);
assert_eq!(JUMP.frames, vec!["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"]);
assert_eq!(PULSE.frames, vec!["█", "▓", "▒", "░"]);
assert_eq!(POINTS.frames, vec!["∙∙∙", "●∙∙", "∙●∙", "∙∙●"]);
assert_eq!(GLOBE.frames, vec!["🌍", "🌎", "🌏"]);
assert_eq!(
MOON.frames,
vec!["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
);
assert_eq!(MONKEY.frames, vec!["🙈", "🙉", "🙊"]);
assert_eq!(
METER.frames,
vec!["▱▱▱", "▰▱▱", "▰▰▱", "▰▰▰", "▰▰▱", "▰▱▱", "▱▱▱"]
);
assert_eq!(HAMBURGER.frames, vec!["☱", "☲", "☴", "☲"]);
assert_eq!(ELLIPSIS.frames, vec!["", ".", "..", "..."]);
}
#[test]
fn test_new_with_no_options() {
let spinner = new(&[]);
assert!(spinner.id() > 0); assert_eq!(spinner.spinner.frames, LINE.frames); }
#[test]
fn test_new_with_spinner_option() {
let spinner = new(&[with_spinner(DOT.clone())]);
assert_eq!(spinner.spinner.frames, DOT.frames);
}
#[test]
fn test_new_with_style_option() {
let style = Style::new().foreground(lipgloss::Color::from("red"));
let _spinner = new(&[with_style(style.clone())]);
}
#[test]
fn test_new_with_multiple_options() {
let style = Style::new().foreground(lipgloss::Color::from("blue"));
let spinner = new(&[with_spinner(JUMP.clone()), with_style(style.clone())]);
assert_eq!(spinner.spinner.frames, JUMP.frames);
}
#[test]
fn test_model_id() {
let spinner1 = new(&[]);
let spinner2 = new(&[]);
assert_ne!(spinner1.id(), spinner2.id());
assert!(spinner1.id() > 0);
assert!(spinner2.id() > 0);
}
#[test]
fn test_model_tick_msg() {
let spinner = new(&[]);
let tick_msg = spinner.tick_msg();
assert_eq!(tick_msg.id, spinner.id());
let now = std::time::SystemTime::now();
let elapsed = now.duration_since(tick_msg.time).unwrap();
assert!(elapsed.as_secs() < 1);
}
#[test]
fn test_global_tick_deprecated() {
let tick_msg = tick();
assert_eq!(tick_msg.id, 0); }
#[test]
fn test_update_with_wrong_id() {
let mut spinner = new(&[]);
let wrong_tick = TickMsg {
time: std::time::SystemTime::now(),
id: spinner.id() + 999, tag: 0,
};
let result = spinner.update(Box::new(wrong_tick));
assert!(result.is_none()); }
#[test]
fn test_update_with_correct_id() {
let mut spinner = new(&[]);
let correct_tick = TickMsg {
time: std::time::SystemTime::now(),
id: spinner.id(),
tag: 0,
};
let result = spinner.update(Box::new(correct_tick));
assert!(result.is_some()); }
#[test]
fn test_view_renders_correctly() {
let mut spinner = new(&[with_spinner(LINE.clone())]);
let view = spinner.view();
assert_eq!(view, "|");
let tick_msg = spinner.tick_msg();
spinner.update(Box::new(tick_msg));
let view = spinner.view();
assert_eq!(view, "/"); }
#[test]
fn test_frame_wrapping() {
let mut spinner = new(&[with_spinner(LINE.clone())]);
for expected_frame in &["|", "/", "-", "\\", "|"] {
let view = spinner.view();
assert_eq!(view, *expected_frame);
if expected_frame != &"|" || view == "|" {
let tick_msg = spinner.tick_msg();
spinner.update(Box::new(tick_msg));
}
}
}
#[test]
fn test_deprecated_functions() {
#[allow(deprecated)]
{
let spinner_line = line();
assert_eq!(spinner_line.frames, LINE.frames);
let spinner_dot = dot();
assert_eq!(spinner_dot.frames, DOT.frames);
let model = new_model(&[]);
assert!(model.id() > 0);
}
}
#[test]
fn test_builder_methods_still_work() {
let spinner = Model::new()
.with_spinner(PULSE.clone())
.with_style(Style::new());
assert_eq!(spinner.spinner.frames, PULSE.frames);
}
}