use crate::console::{Console, ConsoleOptions};
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
pub struct Spinner {
frames: &'static [&'static str],
interval: f64,
text: String,
speed: f64,
style: Option<Style>,
}
impl Spinner {
pub fn new(name: &str) -> Self {
let (interval, frames) = crate::spinner_data::spinner_data(name)
.or_else(|| crate::spinner_data::spinner_data("dots"))
.expect("dots spinner exists");
Spinner {
frames,
interval,
text: String::new(),
speed: 1.0,
style: None,
}
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self
}
pub fn speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
fn frame_index(&self, time: f64) -> usize {
let interval_secs = self.interval / 1000.0;
((time * self.speed / interval_secs) as usize) % self.frames.len()
}
pub fn render(&self, time: f64) -> Text {
let frame = self.frames[self.frame_index(time)];
let mut text = if self.text.is_empty() {
Text::new(frame)
} else {
Text::new(format!("{frame} {}", self.text))
};
if let Some(style) = &self.style {
text.stylize(style.clone(), 0, frame.len());
}
text
}
}
impl Renderable for Spinner {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.render(0.0).rich_render(console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
fn frame_at(name: &str, time: f64) -> String {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.build();
console.render_to_string(&Spinner::new(name).render(time))
}
#[test]
fn dots_frames_match_upstream() {
assert_eq!(frame_at("dots", 0.0), "⠋");
assert_eq!(frame_at("dots", 0.1), "⠙");
assert_eq!(frame_at("dots", 0.25), "⠸");
}
#[test]
fn line_frames_match_upstream() {
assert_eq!(frame_at("line", 0.0), "-");
assert_eq!(frame_at("line", 0.1), "-");
assert_eq!(frame_at("line", 0.25), "\\");
}
#[test]
fn full_table_covers_more_spinners() {
assert_eq!(frame_at("moon", 0.0), "\u{1f311} ");
assert_eq!(frame_at("moon", 0.08), "\u{1f312} ");
assert_eq!(frame_at("bounce", 0.0), "\u{2801}");
}
#[test]
fn arrow_and_dots2_match_upstream() {
assert_eq!(frame_at("arrow", 0.0), "←");
assert_eq!(frame_at("arrow", 0.1), "↖");
assert_eq!(frame_at("dots2", 0.0), "⣾");
}
#[test]
fn text_follows_frame() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(20)
.build();
let out = console.render_to_string(&Spinner::new("dots").text("Working").render(0.0));
assert_eq!(out, "⠋ Working");
}
#[test]
fn styled_frame_only() {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(30)
.no_color(false)
.build();
let spinner = Spinner::new("dots")
.text("Working")
.style(crate::style::Style::parse("green").unwrap());
assert_eq!(
console.render_to_string(&spinner.render(0.0)),
"\x1b[32m⠋\x1b[0m Working"
);
}
}