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 is_paused: bool,
20 pub spinner_frame: usize,
21}
22
23fn shrink_path(cwd: &str, max: usize) -> String {
24 let with_tilde = if let Some(home) = std::env::var_os("HOME") {
25 if let Some(home) = home.to_str() {
26 if cwd.starts_with(home) {
27 format!("~{}", &cwd[home.len()..])
28 } else {
29 cwd.to_string()
30 }
31 } else {
32 cwd.to_string()
33 }
34 } else {
35 cwd.to_string()
36 };
37
38 if with_tilde.len() <= max {
39 return with_tilde;
40 }
41 let take = max.saturating_sub(1);
42 let start = with_tilde.len().saturating_sub(take);
43 format!("…{}", &with_tilde[start..])
44}
45
46fn pretty_model(model: &str) -> String {
47 let s = model.trim_start_matches("claude-");
48 s.split('-').collect::<Vec<_>>().join("·")
49}
50
51impl<'a> Widget for Footer<'a> {
52 fn render(self, area: Rect, buf: &mut Buffer) {
53 let bg = theme::SURFACE();
54 let fg_dim = theme::SUBTLE();
55 let dim_attr = Modifier::DIM;
56
57 for x in area.x..area.x + area.width {
58 buf[(x, area.y)].set_style(Style::default().bg(bg));
59 }
60
61 let max_path = (area.width as usize).saturating_sub(60).max(10);
62 let path = shrink_path(self.cwd, max_path);
63
64 let mut left_spans: Vec<Span<'static>> = vec![
65 Span::styled(" ", Style::default().bg(bg)),
66 Span::styled(
67 path,
68 Style::default().fg(theme::TEXT_MUTED()).bg(bg),
69 ),
70 ];
71
72 if let Some(branch) = self.git_branch {
73 left_spans.push(Span::styled(
74 " ",
75 Style::default().fg(fg_dim).bg(bg).add_modifier(dim_attr),
76 ));
77 left_spans.push(Span::styled(
78 "\u{E0A0} ",
79 Style::default().fg(theme::SUCCESS()).bg(bg),
80 ));
81 left_spans.push(Span::styled(
82 branch.to_string(),
83 Style::default().fg(theme::SUCCESS()).bg(bg),
84 ));
85 }
86
87 let mut right_spans: Vec<Span<'static>> = Vec::new();
88 let sep = Span::styled(
89 " ",
90 Style::default().fg(fg_dim).bg(bg).add_modifier(dim_attr),
91 );
92
93 if self.is_paused {
94 right_spans.push(Span::styled(
95 "⏸ ",
96 Style::default().fg(theme::WARNING()).bg(bg).add_modifier(Modifier::BOLD),
97 ));
98 right_spans.push(Span::styled(
99 "paused",
100 Style::default().fg(theme::WARNING()).bg(bg).add_modifier(Modifier::ITALIC),
101 ));
102 right_spans.push(sep.clone());
103 } else if self.is_streaming {
104 let ch = FRAMES[self.spinner_frame % FRAMES.len()];
105 right_spans.push(Span::styled(
106 format!("{ch} "),
107 Style::default().fg(theme::PRIMARY()).bg(bg).add_modifier(Modifier::BOLD),
108 ));
109 right_spans.push(Span::styled(
110 "generating",
111 Style::default().fg(theme::PRIMARY()).bg(bg).add_modifier(Modifier::ITALIC),
112 ));
113 right_spans.push(sep.clone());
114 }
115
116 let (mode_icon, mode_color) = match self.mode {
117 "Auto-accept" => ("⚡", theme::WARNING()),
118 "Plan" => ("◆", theme::PRIMARY()),
119 "Bypass" => ("⚠", theme::ERROR()),
120 _ => ("●", theme::ACCENT()),
121 };
122 right_spans.push(Span::styled(
123 format!("{mode_icon} "),
124 Style::default().fg(mode_color).bg(bg).add_modifier(Modifier::BOLD),
125 ));
126 right_spans.push(Span::styled(
127 self.mode.to_string(),
128 Style::default().fg(mode_color).bg(bg),
129 ));
130 right_spans.push(sep.clone());
131
132 right_spans.push(Span::styled(
133 "◇ ",
134 Style::default().fg(theme::IRIS()).bg(bg),
135 ));
136 right_spans.push(Span::styled(
137 pretty_model(self.model),
138 Style::default().fg(theme::IRIS()).bg(bg).add_modifier(Modifier::BOLD),
139 ));
140 right_spans.push(sep.clone());
141
142 right_spans.push(Span::styled(
143 format!("${:.4} ", self.cost),
144 Style::default().fg(theme::GOLD()).bg(bg),
145 ));
146
147 let left = Line::from(left_spans);
148 let right = Line::from(right_spans);
149 let right_width: u16 = right.width() as u16;
150 let left_width: u16 = left.width() as u16;
151
152 Paragraph::new(left)
153 .style(Style::default().bg(bg))
154 .render(Rect { width: left_width.min(area.width), ..area }, buf);
155
156 if area.width > right_width {
157 let right_area = Rect {
158 x: area.x + area.width - right_width,
159 y: area.y,
160 width: right_width,
161 height: 1,
162 };
163 Paragraph::new(right)
164 .style(Style::default().bg(bg))
165 .render(right_area, buf);
166 }
167 }
168}