vtcode_tui/core_tui/widgets/
footer.rs1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 text::{Line, Span},
5 widgets::{Block, Clear, Paragraph, Widget},
6};
7
8use super::layout_mode::LayoutMode;
9use super::panel::PanelStyles;
10use crate::ui::tui::session::styling::SessionStyles;
11use crate::ui::tui::session::terminal_capabilities;
12use tui_shimmer::shimmer_spans_with_style_at_phase;
13
14use crate::ui::tui::session::status_requires_shimmer;
15
16pub struct FooterWidget<'a> {
33 styles: &'a SessionStyles,
34 left_status: Option<&'a str>,
35 right_status: Option<&'a str>,
36 hint: Option<&'a str>,
37 mode: LayoutMode,
38 show_border: bool,
39 spinner: Option<&'a str>,
40 shimmer_phase: Option<f32>,
41}
42
43impl<'a> FooterWidget<'a> {
44 pub fn new(styles: &'a SessionStyles) -> Self {
46 Self {
47 styles,
48 left_status: None,
49 right_status: None,
50 hint: None,
51 mode: LayoutMode::Standard,
52 show_border: false,
53 spinner: None,
54 shimmer_phase: None,
55 }
56 }
57
58 #[must_use]
60 pub fn left_status(mut self, status: &'a str) -> Self {
61 self.left_status = Some(status);
62 self
63 }
64
65 #[must_use]
67 pub fn right_status(mut self, status: &'a str) -> Self {
68 self.right_status = Some(status);
69 self
70 }
71
72 #[must_use]
74 pub fn hint(mut self, hint: &'a str) -> Self {
75 self.hint = Some(hint);
76 self
77 }
78
79 #[must_use]
81 pub fn mode(mut self, mode: LayoutMode) -> Self {
82 self.mode = mode;
83 self
84 }
85
86 #[must_use]
88 pub fn show_border(mut self, show: bool) -> Self {
89 self.show_border = show;
90 self
91 }
92
93 #[must_use]
95 pub fn spinner(mut self, spinner: &'a str) -> Self {
96 self.spinner = Some(spinner);
97 self
98 }
99
100 #[must_use]
102 pub fn shimmer_phase(mut self, phase: f32) -> Self {
103 self.shimmer_phase = Some(phase);
104 self
105 }
106
107 fn build_status_line(&self, width: u16) -> Line<'static> {
108 let mut spans = Vec::new();
109
110 if let Some(left) = self.left_status {
112 if status_requires_shimmer(left) {
113 if let Some(phase) = self.shimmer_phase {
114 spans.extend(shimmer_spans_with_style_at_phase(
115 left,
116 self.styles.muted_style(),
117 phase,
118 ));
119 } else {
120 spans.push(Span::styled(left.to_string(), self.styles.muted_style()));
121 }
122 } else {
123 spans.push(Span::styled(left.to_string(), self.styles.accent_style()));
124 }
125 }
126
127 if let Some(spinner) = self.spinner {
129 if !spans.is_empty() {
130 spans.push(Span::raw(" "));
131 }
132 spans.push(Span::styled(spinner.to_string(), self.styles.muted_style()));
133 }
134
135 let right_text = self.right_status.unwrap_or("");
137 let left_len: usize = spans.iter().map(|s| s.content.len()).sum();
138 let right_len = right_text.len();
139 let available = width as usize;
140
141 if left_len + right_len + 2 <= available {
143 let padding = available.saturating_sub(left_len + right_len);
144 spans.push(Span::raw(" ".repeat(padding)));
145 spans.push(Span::styled(
146 right_text.to_string(),
147 self.styles.muted_style(),
148 ));
149 }
150
151 Line::from(spans)
152 }
153
154 fn build_hint_line(&self) -> Option<Line<'static>> {
155 match self.mode {
156 LayoutMode::Compact => None,
157 _ => self
158 .hint
159 .map(|hint| Line::from(Span::styled(hint.to_string(), self.styles.muted_style()))),
160 }
161 }
162}
163
164impl Widget for FooterWidget<'_> {
165 fn render(self, area: Rect, buf: &mut Buffer) {
166 if area.height == 0 || area.width == 0 {
167 return;
168 }
169
170 Clear.render(area, buf);
171
172 let inner = if self.show_border && self.mode.show_borders() {
173 let block = Block::bordered()
174 .border_type(terminal_capabilities::get_border_type())
175 .border_style(self.styles.border_style());
176 let inner = block.inner(area);
177 block.render(area, buf);
178 inner
179 } else {
180 area
181 };
182
183 if inner.height == 0 {
184 return;
185 }
186
187 let status_line = self.build_status_line(inner.width);
188 let hint_line = self.build_hint_line();
189
190 let lines: Vec<Line<'static>> = if inner.height >= 2 {
191 if let Some(hint) = hint_line {
192 vec![status_line, hint]
193 } else {
194 vec![status_line]
195 }
196 } else {
197 vec![status_line]
198 };
199
200 let paragraph = Paragraph::new(lines);
201 paragraph.render(inner, buf);
202 }
203}
204
205pub mod hints {
207 pub const IDLE: &str = "? help • / command • @ file";
208 pub const PROCESSING: &str = "Ctrl+C cancel";
209 pub const MODAL: &str = "↑↓ navigate • Enter select • Esc close";
210 pub const EDITING: &str = "Enter send • Ctrl+C cancel • ↑ history";
211}