stynx-code-tui 3.6.2

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 spinner_frame: usize,
}

fn shrink_path(cwd: &str, max: usize) -> String {
    if cwd.len() <= max {
        return cwd.to_string();
    }
    if let Some(home) = std::env::var_os("HOME") {
        if let Some(home) = home.to_str() {
            if cwd.starts_with(home) {
                let rest = &cwd[home.len()..];
                let candidate = format!("~{rest}");
                if candidate.len() <= max {
                    return candidate;
                }
                let take = max.saturating_sub(3);
                let start = candidate.len().saturating_sub(take);
                return format!("{}", &candidate[start..]);
            }
        }
    }
    let take = max.saturating_sub(1);
    let start = cwd.len().saturating_sub(take);
    format!("{}", &cwd[start..])
}

impl<'a> Widget for Footer<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        for x in area.x..area.x + area.width {
            buf[(x, area.y)].set_style(Style::default().bg(theme::BACKGROUND()));
        }

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

        let left = Line::from(Span::styled(
            path,
            Style::default().fg(theme::TEXT_MUTED()).bg(theme::BACKGROUND()),
        ));

        let mut right_spans: Vec<Span<'static>> = Vec::new();
        let sep = Span::styled("  ", Style::default().bg(theme::BACKGROUND()));

        if self.is_streaming {
            let ch = FRAMES[self.spinner_frame % FRAMES.len()];
            right_spans.push(Span::styled(
                format!("{ch} generating"),
                Style::default()
                    .fg(theme::PRIMARY())
                    .bg(theme::BACKGROUND())
                    .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} {}", self.mode),
            Style::default().fg(mode_color).bg(theme::BACKGROUND()),
        ));
        right_spans.push(sep.clone());

        right_spans.push(Span::styled(
            self.model.to_string(),
            Style::default()
                .fg(theme::ACCENT())
                .bg(theme::BACKGROUND())
                .add_modifier(Modifier::BOLD),
        ));
        right_spans.push(sep.clone());

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

        if let Some(branch) = self.git_branch {
            right_spans.push(sep);
            right_spans.push(Span::styled(
                format!("\u{E0A0} {branch}"),
                Style::default().fg(theme::SUCCESS()).bg(theme::BACKGROUND()),
            ));
        }

        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(theme::BACKGROUND()))
            .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(theme::BACKGROUND()))
                .render(right_area, buf);
        }
    }
}