use std::io::Stdout;
use std::time::Instant;
use crate::Renderable;
use crate::console::{Console, ConsoleOptions};
use crate::measure::Measurement;
use crate::segment::Segments;
use crate::style::Style;
use crate::text::{Text, TextPart};
#[derive(Debug, Clone)]
pub struct SpinnerDef {
pub frames: &'static [&'static str],
pub interval_ms: u64,
}
include!("spinner_data.inc.rs");
#[derive(Debug, Clone)]
pub struct Spinner {
name: String,
frames: Vec<&'static str>,
interval_ms: u64,
style: Option<Style>,
speed: f64,
start: Instant,
text: Option<Text>,
}
impl Spinner {
pub fn new(name: &str) -> Result<Self, String> {
let def = get_spinner_def(name).ok_or_else(|| format!("no spinner called {name:?}"))?;
Ok(Self {
name: name.to_string(),
frames: def.frames.to_vec(),
interval_ms: def.interval_ms,
style: None,
speed: 1.0,
start: Instant::now(),
text: None,
})
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn with_speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(Text::plain(text.into()));
self
}
pub fn update(&mut self, text: Option<String>, style: Option<Style>, speed: Option<f64>) {
if let Some(t) = text {
self.text = Some(Text::plain(t));
}
if let Some(s) = style {
self.style = Some(s);
}
if let Some(sp) = speed {
self.speed = sp;
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn frame_count(&self) -> usize {
self.frames.len()
}
pub fn interval_ms(&self) -> u64 {
self.interval_ms
}
pub fn render_at(
&self,
time_seconds: f64,
start_time_seconds: Option<f64>,
style: Option<Style>,
) -> Text {
let start = start_time_seconds.unwrap_or(time_seconds);
let interval = (self.interval_ms as f64) / 1000.0;
let frame_no = ((time_seconds - start) * self.speed) / interval;
let frame = self.frames[(frame_no as usize) % self.frames.len()];
match style.or(self.style) {
Some(s) => Text::styled(frame, s),
None => Text::plain(frame),
}
}
pub fn render_frame(&self) -> Text {
let elapsed = self.start.elapsed().as_secs_f64();
self.render_at(elapsed, Some(0.0), self.style)
}
pub fn text(&self) -> Option<&Text> {
self.text.as_ref()
}
}
impl Renderable for Spinner {
fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
let frame_text = self.render_frame();
if let Some(ref text) = self.text {
let assembled = Text::assemble([
TextPart::Text(frame_text),
TextPart::Plain(" ".to_string()),
TextPart::Text(text.clone()),
]);
assembled.render(console, options)
} else {
frame_text.render(console, options)
}
}
fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
let frame_text = self.render_frame();
if let Some(ref text) = self.text {
let assembled = Text::assemble([
TextPart::Text(frame_text),
TextPart::Plain(" ".to_string()),
TextPart::Text(text.clone()),
]);
assembled.measure(console, options)
} else {
frame_text.measure(console, options)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spinner_catalog_contains_rich_default() {
let spinner = Spinner::new("dots").unwrap();
assert_eq!(spinner.frame_count(), 10);
assert_eq!(spinner.interval_ms(), 80);
}
#[test]
fn test_spinner_catalog_count_matches_rich() {
assert_eq!(spinner_names().len(), 73);
}
#[test]
fn test_spinner_unknown_errors() {
assert!(Spinner::new("not-a-spinner").is_err());
}
#[test]
fn test_spinner_with_text() {
let spinner = Spinner::new("dots").unwrap().with_text("Loading...");
assert!(spinner.text().is_some());
assert_eq!(spinner.text().unwrap().plain_text(), "Loading...");
}
#[test]
fn test_spinner_update() {
let mut spinner = Spinner::new("dots").unwrap();
assert!(spinner.text().is_none());
spinner.update(Some("Working".to_string()), None, Some(2.0));
assert!(spinner.text().is_some());
assert_eq!(spinner.text().unwrap().plain_text(), "Working");
}
#[test]
fn test_spinner_render_frame() {
let spinner = Spinner::new("dots").unwrap();
let frame = spinner.render_frame();
assert!(!frame.plain_text().is_empty());
}
#[test]
fn test_spinner_renderable() {
let spinner = Spinner::new("dots").unwrap().with_text("Test");
let console = Console::new();
let options = ConsoleOptions::default();
let segments = Renderable::render(&spinner, &console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("Test"));
}
}