use tracing::debug;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Gauge, Paragraph, Widget},
};
const SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
#[derive(Debug, Clone)]
pub struct ProgressBar {
value: f32,
label: String,
width: u16,
}
impl ProgressBar {
pub fn new(label: &str) -> Self {
debug!(component = %"ProgressBar", "Component created");
Self {
value: 0.0,
label: label.to_string(),
width: 40,
}
}
pub fn set_value(&mut self, value: f32) {
self.value = value.clamp(0.0, 1.0);
}
pub fn value(&self) -> f32 {
self.value
}
pub fn set_width(&mut self, width: u16) {
self.width = width;
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
if area.height < 2 {
return;
}
let label_area = Rect {
x: area.x,
y: area.y,
width: area.width,
height: 1,
};
let label_paragraph = Paragraph::new(self.label.as_str());
label_paragraph.render(label_area, buf);
let bar_area = Rect {
x: area.x,
y: area.y + 1,
width: self.width.min(area.width),
height: 1,
};
let gauge = Gauge::default()
.percent((self.value * 100.0) as u16)
.style(Style::default().fg(Color::Green))
.label(format!("{:.0}%", self.value * 100.0));
gauge.render(bar_area, buf);
}
}
impl Default for ProgressBar {
fn default() -> Self {
Self::new("Progress")
}
}
#[derive(Debug, Clone)]
pub struct Spinner {
current_frame: usize,
frames: Vec<String>,
label: String,
}
impl Spinner {
pub fn new(label: &str) -> Self {
debug!(component = %"Spinner", "Component created");
Self {
current_frame: 0,
frames: SPINNER_FRAMES.iter().map(|s| s.to_string()).collect(),
label: label.to_string(),
}
}
pub fn with_frames(label: &str, frames: Vec<String>) -> Self {
Self {
current_frame: 0,
frames,
label: label.to_string(),
}
}
pub fn tick(&mut self) {
self.current_frame = (self.current_frame + 1) % self.frames.len();
}
pub fn current_frame(&self) -> &str {
&self.frames[self.current_frame]
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
let text = format!("{} {}", self.current_frame(), self.label);
let paragraph = Paragraph::new(text.as_str());
paragraph.render(area, buf);
}
}
impl Default for Spinner {
fn default() -> Self {
Self::new("Loading...")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_bar_new() {
let bar = ProgressBar::new("Test");
assert_eq!(bar.value(), 0.0);
assert_eq!(bar.label, "Test");
}
#[test]
fn test_progress_bar_default() {
let bar = ProgressBar::default();
assert_eq!(bar.value(), 0.0);
assert_eq!(bar.label, "Progress");
}
#[test]
fn test_progress_bar_set_value() {
let mut bar = ProgressBar::new("Test");
bar.set_value(0.5);
assert_eq!(bar.value(), 0.5);
}
#[test]
fn test_progress_bar_set_value_clamps_high() {
let mut bar = ProgressBar::new("Test");
bar.set_value(1.5);
assert_eq!(bar.value(), 1.0);
}
#[test]
fn test_progress_bar_set_value_clamps_low() {
let mut bar = ProgressBar::new("Test");
bar.set_value(-0.5);
assert_eq!(bar.value(), 0.0);
}
#[test]
fn test_progress_bar_set_width() {
let mut bar = ProgressBar::new("Test");
bar.set_width(50);
assert_eq!(bar.width, 50);
}
#[test]
fn test_progress_bar_render() {
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width: 40,
height: 2,
});
let mut bar = ProgressBar::new("Test");
bar.set_value(0.5);
bar.render(
Rect {
x: 0,
y: 0,
width: 40,
height: 2,
},
&mut buffer,
);
let _cell = buffer.cell((0, 0));
assert!(!buffer.content.is_empty());
}
#[test]
fn test_spinner_new() {
let spinner = Spinner::new("Loading...");
assert_eq!(spinner.label, "Loading...");
assert_eq!(spinner.current_frame, 0);
assert_eq!(spinner.frames.len(), SPINNER_FRAMES.len());
}
#[test]
fn test_spinner_default() {
let spinner = Spinner::default();
assert_eq!(spinner.label, "Loading...");
assert_eq!(spinner.current_frame, 0);
}
#[test]
fn test_spinner_with_frames() {
let custom_frames = vec!["|".to_string(), "/".to_string(), "-".to_string()];
let spinner = Spinner::with_frames("Custom", custom_frames.clone());
assert_eq!(spinner.frames, custom_frames);
}
#[test]
fn test_spinner_tick() {
let mut spinner = Spinner::new("Loading...");
let initial_frame = spinner.current_frame();
let initial_frame_str = initial_frame.to_string();
spinner.tick();
assert_eq!(spinner.current_frame, 1);
assert_ne!(spinner.current_frame(), initial_frame_str.as_str());
}
#[test]
fn test_spinner_tick_wraps() {
let custom_frames = vec!["|".to_string(), "/".to_string(), "-".to_string()];
let mut spinner = Spinner::with_frames("Custom", custom_frames);
spinner.tick(); spinner.tick(); spinner.tick();
assert_eq!(spinner.current_frame, 0);
assert_eq!(spinner.current_frame(), "|");
}
#[test]
fn test_spinner_current_frame() {
let spinner = Spinner::new("Loading...");
let frame = spinner.current_frame();
assert_eq!(frame, SPINNER_FRAMES[0]);
}
#[test]
fn test_spinner_render() {
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width: 20,
height: 1,
});
let spinner = Spinner::new("Loading...");
spinner.render(
Rect {
x: 0,
y: 0,
width: 20,
height: 1,
},
&mut buffer,
);
assert!(!buffer.content.is_empty());
}
}