a3s-tui 0.1.4

TEA (The Elm Architecture) framework for terminal user interfaces
Documentation
use crate::style::{truncate_visible, visible_len, Color, Style};

#[derive(Debug, Clone)]
pub struct Meter {
    label: String,
    value: f64,
    max: f64,
    width: usize,
    fg: Color,
    empty_fg: Color,
    fill: char,
    empty: char,
}

impl Meter {
    pub fn new(value: f64) -> Self {
        Self {
            label: String::new(),
            value,
            max: 100.0,
            width: 32,
            fg: Color::Cyan,
            empty_fg: Color::BrightBlack,
            fill: '',
            empty: '',
        }
    }

    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }

    pub fn max(mut self, max: f64) -> Self {
        self.max = max.max(f64::EPSILON);
        self
    }

    pub fn width(mut self, width: usize) -> Self {
        self.width = width.max(1);
        self
    }

    pub fn fg(mut self, color: Color) -> Self {
        self.fg = color;
        self
    }

    pub fn empty_fg(mut self, color: Color) -> Self {
        self.empty_fg = color;
        self
    }

    pub fn glyphs(mut self, fill: char, empty: char) -> Self {
        self.fill = fill;
        self.empty = empty;
        self
    }

    pub fn view(&self) -> String {
        let value = if self.value.is_finite() {
            self.value
        } else {
            0.0
        };
        let ratio = (value / self.max).clamp(0.0, 1.0);
        let value_label = if (self.max - 100.0).abs() < f64::EPSILON {
            format!("{value:>5.1}%")
        } else {
            format!("{value:>6.1}")
        };
        let prefix = if self.label.is_empty() {
            format!("{value_label} ")
        } else {
            format!("{} {value_label} ", self.label)
        };

        let prefix_width = visible_len(&prefix).min(self.width);
        let bar_width = self.width.saturating_sub(prefix_width);
        if bar_width == 0 {
            return truncate_visible(&prefix, self.width);
        }

        let filled = ((bar_width as f64) * ratio).round() as usize;
        let filled = filled.min(bar_width);
        let empty = bar_width.saturating_sub(filled);
        let bar = format!(
            "{}{}",
            Style::new()
                .fg(self.fg)
                .render(&self.fill.to_string().repeat(filled)),
            Style::new()
                .fg(self.empty_fg)
                .render(&self.empty.to_string().repeat(empty))
        );

        format!("{prefix}{bar}")
    }

    pub fn plain(&self) -> String {
        crate::style::strip_ansi(&self.view())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::style::strip_ansi;

    #[test]
    fn renders_fixed_width() {
        let line = Meter::new(25.0).label("CPU").width(20).view();

        assert_eq!(visible_len(&line), 20);
        assert!(strip_ansi(&line).contains("25.0%"));
    }

    #[test]
    fn clamps_to_full_bar() {
        let line = Meter::new(200.0).label("MEM").width(16).plain();

        assert_eq!(visible_len(&line), 16);
        assert!(!line.contains(''));
    }

    #[test]
    fn truncates_when_prefix_exceeds_width() {
        let line = Meter::new(1.0).label("very-long-label").width(8).plain();

        assert_eq!(visible_len(&line), 8);
    }
}