use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge, Widget},
};
#[derive(Debug, Clone)]
pub struct ProgressStyle {
pub filled_color: Color,
pub unfilled_color: Color,
pub label_style: Style,
pub bordered: bool,
}
impl Default for ProgressStyle {
fn default() -> Self {
Self {
filled_color: Color::Green,
unfilled_color: Color::DarkGray,
label_style: Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
bordered: true,
}
}
}
impl From<&crate::theme::Theme> for ProgressStyle {
fn from(theme: &crate::theme::Theme) -> Self {
let p = &theme.palette;
Self {
filled_color: p.success,
unfilled_color: p.text_disabled,
label_style: Style::default().fg(p.text).add_modifier(Modifier::BOLD),
bordered: true,
}
}
}
impl ProgressStyle {
pub fn new(filled: Color, unfilled: Color) -> Self {
Self {
filled_color: filled,
unfilled_color: unfilled,
..Default::default()
}
}
pub fn success() -> Self {
Self::default()
}
pub fn warning() -> Self {
Self {
filled_color: Color::Yellow,
..Default::default()
}
}
pub fn error() -> Self {
Self {
filled_color: Color::Red,
..Default::default()
}
}
pub fn info() -> Self {
Self {
filled_color: Color::Cyan,
..Default::default()
}
}
pub fn bordered(mut self, bordered: bool) -> Self {
self.bordered = bordered;
self
}
}
#[derive(Debug, Clone)]
pub struct Progress<'a> {
ratio: f64,
label: Option<&'a str>,
steps: Option<(usize, usize)>,
style: ProgressStyle,
}
impl<'a> Progress<'a> {
pub fn new(ratio: f64) -> Self {
Self {
ratio: ratio.clamp(0.0, 1.0),
label: None,
steps: None,
style: ProgressStyle::default(),
}
}
pub fn from_steps(current: usize, total: usize) -> Self {
let ratio = if total > 0 {
current as f64 / total as f64
} else {
0.0
};
Self::new(ratio).steps(current, total)
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
pub fn steps(mut self, current: usize, total: usize) -> Self {
self.steps = Some((current, total));
self
}
pub fn style(mut self, style: ProgressStyle) -> Self {
self.style = style;
self
}
pub fn theme(self, theme: &crate::theme::Theme) -> Self {
self.style(ProgressStyle::from(theme))
}
fn build_label(&self) -> String {
let percent = (self.ratio * 100.0) as u16;
match (&self.label, &self.steps) {
(Some(label), Some((current, total))) => {
format!("{} - {}/{} steps ({}%)", label, current, total, percent)
}
(Some(label), None) => {
format!("{} ({}%)", label, percent)
}
(None, Some((current, total))) => {
format!("{}/{} ({}%)", current, total, percent)
}
(None, None) => {
format!("{}%", percent)
}
}
}
}
impl Widget for Progress<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let label = self.build_label();
let label_span = Span::styled(label, self.style.label_style);
let mut gauge = Gauge::default()
.gauge_style(
Style::default()
.fg(self.style.filled_color)
.bg(self.style.unfilled_color),
)
.percent((self.ratio * 100.0) as u16)
.label(label_span);
if self.style.bordered {
gauge = gauge.block(Block::default().borders(Borders::ALL));
}
gauge.render(area, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_new() {
let p = Progress::new(0.5);
assert!((p.ratio - 0.5).abs() < 0.001);
}
#[test]
fn test_progress_clamp() {
let p = Progress::new(1.5);
assert!((p.ratio - 1.0).abs() < 0.001);
let p = Progress::new(-0.5);
assert!((p.ratio - 0.0).abs() < 0.001);
}
#[test]
fn test_progress_from_steps() {
let p = Progress::from_steps(5, 10);
assert!((p.ratio - 0.5).abs() < 0.001);
assert_eq!(p.steps, Some((5, 10)));
}
#[test]
fn test_progress_label() {
let p = Progress::new(0.75).label("Building");
assert_eq!(p.build_label(), "Building (75%)");
}
#[test]
fn test_progress_label_with_steps() {
let p = Progress::new(0.5).label("Processing").steps(5, 10);
assert_eq!(p.build_label(), "Processing - 5/10 steps (50%)");
}
#[test]
fn test_progress_render() {
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 3));
let progress = Progress::new(0.5).label("Test");
progress.render(Rect::new(0, 0, 40, 3), &mut buf);
}
}