Skip to main content

tui_kit/
footer.rs

1//! Footer / keybind bar widget.
2//!
3//! Renders a single-line row of keyboard shortcut hints at the bottom of the screen.
4//! Each hint is a `(key, action)` pair styled as:
5//!
6//! ```text
7//! Navigate: j/k  |  Open: Enter  |  Leader: space  |  Quit: q
8//! ^^^^^^^^^^ ^^^    ^^^^  ^^^^^                      ^^^^  ^
9//! action     key    action key                       action separator
10//! (dark gray)(yellow)                               (dark gray)
11//! ```
12//!
13//! ## Usage
14//!
15//! ```rust
16//! // In your render function, at the bottom of the layout:
17//! render_footer(f, chunks[FOOTER], &[
18//!     ("j/k",   "navigate"),
19//!     ("enter", "open"),
20//!     ("space", "leader"),
21//!     ("q",     "quit"),
22//! ], &theme);
23//! ```
24//!
25//! The pairs are context-aware — build them based on the current app state
26//! (e.g. show different hints when a popup is open):
27//!
28//! ```rust
29//! let mut pairs: Vec<(&str, &str)> = Vec::new();
30//! if popup_open {
31//!     pairs.push(("Esc",   "close"));
32//!     pairs.push(("Enter", "confirm"));
33//! } else {
34//!     pairs.push(("j/k",   "navigate"));
35//!     pairs.push(("space", "leader"));
36//!     pairs.push(("q",     "quit"));
37//! }
38//! render_footer(f, footer_area, &pairs, &theme);
39//! ```
40//!
41//! ## Optional right-hand label
42//!
43//! Use [`render_footer_with_app`] to also show an app name + version pinned to
44//! the right edge (as rad does):
45//!
46//! ```rust
47//! render_footer_with_app(f, chunks[FOOTER], &pairs, "myapp", "1.0.0", &theme);
48//! ```
49
50use ratatui::{
51    layout::{Alignment, Constraint, Direction, Layout, Rect},
52    text::{Line, Span},
53    widgets::Paragraph,
54    Frame,
55};
56
57use crate::theme::Theme;
58
59/// Render a keybind bar from a list of `(key, action)` pairs.
60///
61/// Pairs are separated by `" | "` and styled with the theme's
62/// `shortcut_key` (yellow) for keys and `hint` (dark gray) for actions and
63/// separators. A 1-column margin is applied on both sides so the bar aligns
64/// with bordered widgets that have their own borders.
65///
66/// # Arguments
67/// * `pairs` — slice of `(key_label, action_label)`. Action labels should be
68///   lowercase, no leading colon needed (the colon is added automatically).
69pub fn render_footer(f: &mut Frame, area: Rect, pairs: &[(&str, &str)], theme: &Theme) {
70    let inner = horizontal_margin(area);
71    f.render_widget(Paragraph::new(Line::from(keybind_spans(pairs, theme))), inner);
72}
73
74/// Like [`render_footer`] but also renders `"name version"` right-aligned.
75///
76/// Useful for showing the app name and version in the bottom-right corner,
77/// exactly as the rad project does.
78pub fn render_footer_with_app(
79    f: &mut Frame,
80    area: Rect,
81    pairs: &[(&str, &str)],
82    app_name: &str,
83    app_version: &str,
84    theme: &Theme,
85) {
86    let inner = horizontal_margin(area);
87    let right_width = (app_name.len() + 1 + app_version.len()) as u16;
88    let chunks = Layout::default()
89        .direction(Direction::Horizontal)
90        .constraints([Constraint::Min(0), Constraint::Length(right_width)])
91        .split(inner);
92
93    f.render_widget(
94        Paragraph::new(Line::from(keybind_spans(pairs, theme))),
95        chunks[0],
96    );
97
98    let right_line = Line::from(vec![
99        Span::styled(app_name.to_string(), theme.shortcut_key),
100        Span::styled(format!(" {}", app_version), theme.hint),
101    ]);
102    f.render_widget(
103        Paragraph::new(right_line).alignment(Alignment::Right),
104        chunks[1],
105    );
106}
107
108/// Build the `Vec<Span>` for a keybind bar without rendering it.
109///
110/// Useful when you need to embed the hints inside a larger `Line` or a block
111/// title rather than a full-width footer row.
112pub fn keybind_spans<'a>(pairs: &[(&'a str, &'a str)], theme: &Theme) -> Vec<Span<'a>> {
113    let mut spans = Vec::new();
114    for (i, (key, action)) in pairs.iter().enumerate() {
115        if i > 0 {
116            spans.push(Span::styled(" | ", theme.hint));
117        }
118        spans.push(Span::styled(*action, theme.shortcut_key));
119        spans.push(Span::styled(format!(": {}", key), theme.hint));
120    }
121    spans
122}
123
124/// Apply a 1-column left+right margin so the footer aligns with bordered panels.
125fn horizontal_margin(area: Rect) -> Rect {
126    let chunks = Layout::default()
127        .direction(Direction::Horizontal)
128        .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
129        .split(area);
130    chunks[1]
131}