Skip to main content

vtcode_tui/core_tui/widgets/
footer.rs

1use 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
16/// Widget for rendering the footer area with status and hints
17///
18/// The footer provides a stable region for:
19/// - Left status (git branch, model info)
20/// - Right status (token count, context usage)
21/// - Help hints (shown conditionally)
22///
23/// # Example
24/// ```ignore
25/// FooterWidget::new(&styles)
26///     .left_status("main ✓")
27///     .right_status("claude-4 | 12K tokens")
28///     .hint("? for help")
29///     .mode(LayoutMode::Standard)
30///     .render(footer_area, buf);
31/// ```
32pub 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    /// Create a new footer widget
45    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    /// Set the left status text (e.g., git branch)
59    #[must_use]
60    pub fn left_status(mut self, status: &'a str) -> Self {
61        self.left_status = Some(status);
62        self
63    }
64
65    /// Set the right status text (e.g., model info)
66    #[must_use]
67    pub fn right_status(mut self, status: &'a str) -> Self {
68        self.right_status = Some(status);
69        self
70    }
71
72    /// Set the hint text (shown when idle)
73    #[must_use]
74    pub fn hint(mut self, hint: &'a str) -> Self {
75        self.hint = Some(hint);
76        self
77    }
78
79    /// Set the layout mode
80    #[must_use]
81    pub fn mode(mut self, mode: LayoutMode) -> Self {
82        self.mode = mode;
83        self
84    }
85
86    /// Show a top border
87    #[must_use]
88    pub fn show_border(mut self, show: bool) -> Self {
89        self.show_border = show;
90        self
91    }
92
93    /// Set spinner text (shown when processing)
94    #[must_use]
95    pub fn spinner(mut self, spinner: &'a str) -> Self {
96        self.spinner = Some(spinner);
97        self
98    }
99
100    /// Set shimmer phase for animated status text
101    #[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        // Left status
111        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        // Spinner (if active)
128        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        // Calculate space needed for right status
136        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        // Add padding and right status if there's room
142        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
205/// Default keybind hints for different contexts
206pub 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}