a3s-tui 0.1.4

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

pub struct StatusBar {
    left: String,
    center: String,
    right: String,
    fg: Color,
    bg: Color,
    bold: bool,
}

impl StatusBar {
    pub fn new() -> Self {
        Self {
            left: String::new(),
            center: String::new(),
            right: String::new(),
            fg: Color::White,
            bg: Color::BrightBlack,
            bold: false,
        }
    }

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

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

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

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

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

    pub fn bold(mut self, enabled: bool) -> Self {
        self.bold = enabled;
        self
    }

    pub fn element<Msg>(&self) -> Element<Msg> {
        let mut children: Vec<Element<Msg>> = Vec::new();

        if !self.left.is_empty() {
            children.push(Element::Text(self.text_element(&self.left)));
        }

        children.push(Element::Spacer);

        if !self.center.is_empty() {
            children.push(Element::Text(self.text_element(&self.center)));
            children.push(Element::Spacer);
        }

        if !self.right.is_empty() {
            children.push(Element::Text(self.text_element(&self.right)));
        }

        Element::Box(
            BoxElement::new()
                .direction(FlexDirection::Row)
                .bg(self.bg)
                .children(children),
        )
    }

    pub fn view(&self, width: u16) -> String {
        let w = width as usize;
        if w == 0 {
            return String::new();
        }

        let right = truncate_visible(&self.right, w);
        let right_width = visible_len(&right);
        let right_start = w.saturating_sub(right_width);
        let center = truncate_visible(&self.center, w);
        let center_width = visible_len(&center);
        let center_start = (w.saturating_sub(center_width)) / 2;
        let left_full_width = visible_len(&self.left);
        let center_fits = center_width > 0
            && center_start > left_full_width
            && center_start + center_width < right_start;

        let line = if center_fits {
            let left_budget = center_start.saturating_sub(1);
            let left = truncate_visible(&self.left, left_budget);
            let left_width = visible_len(&left);
            let mut line = left;
            line.push_str(&" ".repeat(center_start.saturating_sub(left_width)));
            line.push_str(&center);
            line.push_str(&" ".repeat(right_start.saturating_sub(center_start + center_width)));
            line.push_str(&right);
            line
        } else {
            let left_budget = w.saturating_sub(right_width);
            let left = truncate_visible(&self.left, left_budget);
            let left_width = visible_len(&left);
            let mut line = left;
            line.push_str(&" ".repeat(w.saturating_sub(left_width + right_width)));
            line.push_str(&right);
            line
        };

        self.style().render(&line)
    }

    fn text_element(&self, text: &str) -> TextElement {
        let mut element = TextElement::new(text).fg(self.fg);
        if self.bold {
            element = element.bold();
        }
        element
    }

    fn style(&self) -> Style {
        let mut style = Style::new().fg(self.fg).bg(self.bg);
        if self.bold {
            style = style.bold();
        }
        style
    }
}

impl Default for StatusBar {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn empty_status_bar() {
        let sb = StatusBar::new();
        let view = sb.view(40);
        let plain = strip_ansi(&view);
        assert_eq!(visible_len(&plain), 40);
    }

    #[test]
    fn left_content() {
        let sb = StatusBar::new().left("hello");
        let view = sb.view(40);
        let plain = strip_ansi(&view);
        assert!(plain.starts_with("hello"));
    }

    #[test]
    fn right_content() {
        let sb = StatusBar::new().right("end");
        let view = sb.view(40);
        let plain = strip_ansi(&view);
        assert!(plain.ends_with("end"));
    }

    #[test]
    fn left_and_right() {
        let sb = StatusBar::new().left("L").right("R");
        let view = sb.view(20);
        let plain = strip_ansi(&view);
        assert!(plain.starts_with('L'));
        assert!(plain.ends_with('R'));
        assert_eq!(visible_len(&plain), 20);
    }

    #[test]
    fn hides_center_when_it_would_touch_left_or_right() {
        let sb = StatusBar::new()
            .left("left side is fairly long")
            .center("center")
            .right("right");
        let view = sb.view(32);
        let plain = strip_ansi(&view);

        assert!(!plain.contains("longcenter"));
        assert!(!plain.contains("centerright"));
        assert!(!plain.contains("center"));
        assert_eq!(visible_len(&plain), 32);
    }

    #[test]
    fn truncates_left_to_preserve_right_status() {
        let sb = StatusBar::new()
            .left(" a3s top boxes:10 agents:20 processes:300 events:400 ")
            .right("live")
            .bold(true);
        let view = sb.view(32);
        let plain = strip_ansi(&view);

        assert_eq!(visible_len(&view), 32);
        assert!(plain.ends_with("live"));
        assert!(plain.contains(''));
        assert!(view.contains("\x1b[1;"));
    }

    #[test]
    fn handles_cjk_width_when_truncating() {
        let sb = StatusBar::new()
            .left(" 状态 中文测试内容")
            .right("运行中")
            .fg(Color::BrightWhite)
            .bg(Color::Blue);
        let view = sb.view(18);
        let plain = strip_ansi(&view);

        assert_eq!(visible_len(&view), 18);
        assert!(plain.ends_with("运行中"));
    }

    #[test]
    fn element_produces_row() {
        let sb = StatusBar::new().left("a").right("b");
        let el: Element<()> = sb.element();
        match el {
            Element::Box(b) => {
                assert_eq!(b.style.flex_direction, crate::element::FlexDirection::Row);
            }
            _ => panic!("expected Box"),
        }
    }
}