stynx_code_tui/widgets/
footer.rs1use 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}