stynx-code-tui 3.12.1

Terminal user interface with ratatui for interactive sessions
Documentation
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Paragraph, Widget},
};

use crate::theme;
use crate::widgets::spinner::FRAMES;

pub struct Footer<'a> {
    pub cwd: &'a str,
    pub model: &'a str,
    pub mode: &'a str,
    pub cost: f64,
    pub git_branch: Option<&'a str>,
    pub is_streaming: bool,
    pub is_paused: bool,
    pub spinner_frame: usize,
}

fn shrink_path(cwd: &str, max: usize) -> String {
    let with_tilde = if let Some(home) = std::env::var_os("HOME") {
        if let Some(home) = home.to_str() {
            if cwd.starts_with(home) {
                format!("~{}", &cwd[home.len()..])
            } else {
                cwd.to_string()
            }
        } else {
            cwd.to_string()
        }
    } else {
        cwd.to_string()
    };

    if with_tilde.len() <= max {
        return with_tilde;
    }
    let take = max.saturating_sub(1);
    let start = with_tilde.len().saturating_sub(take);
    format!("{}", &with_tilde[start..])
}

fn pretty_model(model: &str) -> String {
    let s = model.trim_start_matches("claude-");
    s.split('-').collect::<Vec<_>>().join("·")
}

impl<'a> Widget for Footer<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let bg = theme::SURFACE();
        let fg_dim = theme::SUBTLE();
        let dim_attr = Modifier::DIM;

        for x in area.x..area.x + area.width {
            buf[(x, area.y)].set_style(Style::default().bg(bg));
        }

        let max_path = (area.width as usize).saturating_sub(60).max(10);
        let path = shrink_path(self.cwd, max_path);

        let mut left_spans: Vec<Span<'static>> = vec![
            Span::styled(" ", Style::default().bg(bg)),
            Span::styled(
                path,
                Style::default().fg(theme::TEXT_MUTED()).bg(bg),
            ),
        ];

        if let Some(branch) = self.git_branch {
            left_spans.push(Span::styled(
                "  ",
                Style::default().fg(fg_dim).bg(bg).add_modifier(dim_attr),
            ));
            left_spans.push(Span::styled(
                "\u{E0A0} ",
                Style::default().fg(theme::SUCCESS()).bg(bg),
            ));
            left_spans.push(Span::styled(
                branch.to_string(),
                Style::default().fg(theme::SUCCESS()).bg(bg),
            ));
        }

        let mut right_spans: Vec<Span<'static>> = Vec::new();
        let sep = Span::styled(
            "  ",
            Style::default().fg(fg_dim).bg(bg).add_modifier(dim_attr),
        );

        if self.is_paused {
            right_spans.push(Span::styled(
                "",
                Style::default().fg(theme::WARNING()).bg(bg).add_modifier(Modifier::BOLD),
            ));
            right_spans.push(Span::styled(
                "paused",
                Style::default().fg(theme::WARNING()).bg(bg).add_modifier(Modifier::ITALIC),
            ));
            right_spans.push(sep.clone());
        } else if self.is_streaming {
            let ch = FRAMES[self.spinner_frame % FRAMES.len()];
            right_spans.push(Span::styled(
                format!("{ch} "),
                Style::default().fg(theme::PRIMARY()).bg(bg).add_modifier(Modifier::BOLD),
            ));
            right_spans.push(Span::styled(
                "generating",
                Style::default().fg(theme::PRIMARY()).bg(bg).add_modifier(Modifier::ITALIC),
            ));
            right_spans.push(sep.clone());
        }

        let (mode_icon, mode_color) = match self.mode {
            "Auto-accept" => ("", theme::WARNING()),
            "Plan" => ("", theme::PRIMARY()),
            "Bypass" => ("", theme::ERROR()),
            _ => ("", theme::ACCENT()),
        };
        right_spans.push(Span::styled(
            format!("{mode_icon} "),
            Style::default().fg(mode_color).bg(bg).add_modifier(Modifier::BOLD),
        ));
        right_spans.push(Span::styled(
            self.mode.to_string(),
            Style::default().fg(mode_color).bg(bg),
        ));
        right_spans.push(sep.clone());

        right_spans.push(Span::styled(
            "",
            Style::default().fg(theme::IRIS()).bg(bg),
        ));
        right_spans.push(Span::styled(
            pretty_model(self.model),
            Style::default().fg(theme::IRIS()).bg(bg).add_modifier(Modifier::BOLD),
        ));
        right_spans.push(sep.clone());

        right_spans.push(Span::styled(
            format!("${:.4} ", self.cost),
            Style::default().fg(theme::GOLD()).bg(bg),
        ));

        let left = Line::from(left_spans);
        let right = Line::from(right_spans);
        let right_width: u16 = right.width() as u16;
        let left_width: u16 = left.width() as u16;

        Paragraph::new(left)
            .style(Style::default().bg(bg))
            .render(Rect { width: left_width.min(area.width), ..area }, buf);

        if area.width > right_width {
            let right_area = Rect {
                x: area.x + area.width - right_width,
                y: area.y,
                width: right_width,
                height: 1,
            };
            Paragraph::new(right)
                .style(Style::default().bg(bg))
                .render(right_area, buf);
        }
    }
}