Skip to main content

mockforge_tui/widgets/
help.rs

1//! Help overlay popup showing all keybindings.
2
3use ratatui::{
4    layout::{Constraint, Flex, Layout, Rect},
5    text::{Line, Span},
6    widgets::{Block, Borders, Clear, Paragraph},
7    Frame,
8};
9
10use crate::theme::Theme;
11
12const HELP_TEXT: &[(&str, &str)] = &[
13    ("Global", ""),
14    ("  q / Ctrl+C", "Quit"),
15    ("  Tab / Shift+Tab", "Next / Previous tab"),
16    ("  1-0", "Jump to tab 1-10"),
17    ("  Ctrl+] / Ctrl+[", "Next / Previous admin server (multi-server mode)"),
18    ("  r", "Refresh current screen"),
19    ("  /", "Open filter input"),
20    ("  ?", "Toggle this help"),
21    ("  :", "Command palette"),
22    ("", ""),
23    ("Navigation", ""),
24    ("  j / ↓", "Scroll down"),
25    ("  k / ↑", "Scroll up"),
26    ("  g / G", "Jump to top / bottom"),
27    ("  PgUp / PgDn", "Page up / down"),
28    ("  Enter", "Select / expand"),
29    ("  Esc", "Close popup / cancel"),
30    ("", ""),
31    ("Screen-specific", ""),
32    ("  f", "Toggle follow mode (Logs)"),
33    ("  e", "Edit selected item (Config)"),
34    ("  t", "Toggle (Chaos, Time Travel)"),
35    ("  s", "Sort column (Routes, Fixtures)"),
36    ("  d", "Delete selected item"),
37];
38
39/// Render the help overlay centred on screen.
40pub fn render(frame: &mut Frame) {
41    let area = centered_rect(60, 70, frame.area());
42
43    // Clear the background behind the popup.
44    frame.render_widget(Clear, area);
45
46    let lines: Vec<Line> = HELP_TEXT
47        .iter()
48        .map(|(key, desc)| {
49            if desc.is_empty() {
50                // Section header or blank line
51                Line::from(Span::styled(*key, Theme::title()))
52            } else {
53                Line::from(vec![
54                    Span::styled(format!("{key:<22}"), Theme::key_hint()),
55                    Span::styled(*desc, Theme::base()),
56                ])
57            }
58        })
59        .collect();
60
61    let block = Block::default()
62        .title(" Help — press ? or Esc to close ")
63        .title_style(Theme::title())
64        .borders(Borders::ALL)
65        .border_style(Theme::dim())
66        .style(Theme::surface());
67
68    let paragraph = Paragraph::new(lines).block(block);
69    frame.render_widget(paragraph, area);
70}
71
72/// Return a centred `Rect` that takes `percent_x`% width and `percent_y`% height.
73fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
74    let vertical = Layout::vertical([Constraint::Percentage(percent_y)])
75        .flex(Flex::Center)
76        .split(area);
77    Layout::horizontal([Constraint::Percentage(percent_x)])
78        .flex(Flex::Center)
79        .split(vertical[0])[0]
80}