Skip to main content

stynx_code_tui/widgets/
footer.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Paragraph, Widget},
7};
8
9use crate::theme;
10use crate::widgets::spinner::FRAMES;
11
12pub struct Footer<'a> {
13    pub cwd: &'a str,
14    pub model: &'a str,
15    pub mode: &'a str,
16    pub cost: f64,
17    pub git_branch: Option<&'a str>,
18    pub is_streaming: bool,
19    pub spinner_frame: usize,
20}
21
22fn shrink_path(cwd: &str, max: usize) -> String {
23    if cwd.len() <= max {
24        return cwd.to_string();
25    }
26    if let Some(home) = std::env::var_os("HOME") {
27        if let Some(home) = home.to_str() {
28            if cwd.starts_with(home) {
29                let rest = &cwd[home.len()..];
30                let candidate = format!("~{rest}");
31                if candidate.len() <= max {
32                    return candidate;
33                }
34                let take = max.saturating_sub(3);
35                let start = candidate.len().saturating_sub(take);
36                return format!("…{}", &candidate[start..]);
37            }
38        }
39    }
40    let take = max.saturating_sub(1);
41    let start = cwd.len().saturating_sub(take);
42    format!("…{}", &cwd[start..])
43}
44
45impl<'a> Widget for Footer<'a> {
46    fn render(self, area: Rect, buf: &mut Buffer) {
47        for x in area.x..area.x + area.width {
48            buf[(x, area.y)].set_style(Style::default().bg(theme::BACKGROUND()));
49        }
50
51        let max_path = (area.width as usize).saturating_sub(50).max(10);
52        let path = shrink_path(self.cwd, max_path);
53
54        let left = Line::from(Span::styled(
55            path,
56            Style::default().fg(theme::TEXT_MUTED()).bg(theme::BACKGROUND()),
57        ));
58
59        let mut right_spans: Vec<Span<'static>> = Vec::new();
60        let sep = Span::styled("  ", Style::default().bg(theme::BACKGROUND()));
61
62        if self.is_streaming {
63            let ch = FRAMES[self.spinner_frame % FRAMES.len()];
64            right_spans.push(Span::styled(
65                format!("{ch} generating"),
66                Style::default()
67                    .fg(theme::PRIMARY())
68                    .bg(theme::BACKGROUND())
69                    .add_modifier(Modifier::ITALIC),
70            ));
71            right_spans.push(sep.clone());
72        }
73
74        let (mode_icon, mode_color) = match self.mode {
75            "Auto-accept" => ("⚡", theme::WARNING()),
76            "Plan" => ("◆", theme::PRIMARY()),
77            "Bypass" => ("⚠", theme::ERROR()),
78            _ => ("●", theme::ACCENT()),
79        };
80        right_spans.push(Span::styled(
81            format!("{mode_icon} {}", self.mode),
82            Style::default().fg(mode_color).bg(theme::BACKGROUND()),
83        ));
84        right_spans.push(sep.clone());
85
86        right_spans.push(Span::styled(
87            self.model.to_string(),
88            Style::default()
89                .fg(theme::ACCENT())
90                .bg(theme::BACKGROUND())
91                .add_modifier(Modifier::BOLD),
92        ));
93        right_spans.push(sep.clone());
94
95        right_spans.push(Span::styled(
96            format!("${:.4}", self.cost),
97            Style::default().fg(theme::PRIMARY()).bg(theme::BACKGROUND()),
98        ));
99
100        if let Some(branch) = self.git_branch {
101            right_spans.push(sep);
102            right_spans.push(Span::styled(
103                format!("\u{E0A0} {branch}"),
104                Style::default().fg(theme::SUCCESS()).bg(theme::BACKGROUND()),
105            ));
106        }
107
108        let right = Line::from(right_spans);
109        let right_width: u16 = right.width() as u16;
110        let left_width: u16 = left.width() as u16;
111
112        Paragraph::new(left)
113            .style(Style::default().bg(theme::BACKGROUND()))
114            .render(Rect { width: left_width.min(area.width), ..area }, buf);
115
116        if area.width > right_width {
117            let right_area = Rect {
118                x: area.x + area.width - right_width,
119                y: area.y,
120                width: right_width,
121                height: 1,
122            };
123            Paragraph::new(right)
124                .style(Style::default().bg(theme::BACKGROUND()))
125                .render(right_area, buf);
126        }
127    }
128}