1use crate::theme::Theme;
4use ratatui::{
5 layout::{Alignment, Constraint, Direction, Layout, Rect},
6 text::{Line, Span},
7 widgets::{Block, BorderType, Borders, Clear, Paragraph},
8 Frame,
9};
10
11pub struct HelpOverlay {
13 visible: bool,
15 theme: Theme,
17}
18
19impl HelpOverlay {
20 pub fn new() -> Self {
21 Self {
22 visible: false,
23 theme: Theme::default(),
24 }
25 }
26
27 pub fn toggle(&mut self) {
29 self.visible = !self.visible;
30 }
31
32 #[allow(dead_code)]
34 pub fn show(&mut self) {
35 self.visible = true;
36 }
37
38 #[allow(dead_code)]
40 pub fn hide(&mut self) {
41 self.visible = false;
42 }
43
44 pub fn is_visible(&self) -> bool {
46 self.visible
47 }
48}
49
50impl super::Component for HelpOverlay {
51 fn handle_action(&mut self, action: &crate::action::Action) -> Option<crate::action::Action> {
52 match action {
53 crate::action::Action::ToggleHelp => {
54 self.toggle();
55 None
56 },
57 _ => None,
58 }
59 }
60
61 fn render(&mut self, f: &mut Frame, _area: Rect) {
62 if !self.visible {
63 return;
64 }
65
66 let area = centered_rect(60, 70, f.area());
68
69 f.render_widget(Clear, area);
71
72 let block = Block::default()
73 .title(" GraphRAG CLI - Help ")
74 .borders(Borders::ALL)
75 .border_type(BorderType::Rounded)
76 .border_style(self.theme.border_focused());
77
78 let help_text = vec![
79 Line::from(""),
80 Line::from(vec![Span::styled("Global Shortcuts", self.theme.title())]),
81 Line::from("━".repeat(55)),
82 keybinding_line("?", "Toggle this help overlay", &self.theme),
83 keybinding_line("Ctrl+C / q", "Quit application", &self.theme),
84 keybinding_line(
85 "Ctrl+N / Ctrl+P",
86 "Cycle focus: Input→Results→Raw→Info",
87 &self.theme,
88 ),
89 keybinding_line(
90 "Ctrl+1/2/3/4",
91 "Direct focus: Input/Results/Raw/Info",
92 &self.theme,
93 ),
94 keybinding_line("Esc", "Return focus to Input", &self.theme),
95 keybinding_line(
96 "Ctrl+N (Info Panel)",
97 "Cycle tabs: Stats → Sources → History",
98 &self.theme,
99 ),
100 Line::from(""),
101 Line::from(vec![Span::styled("Input Box", self.theme.title())]),
102 Line::from("━".repeat(55)),
103 keybinding_line("Enter", "Submit query or /command", &self.theme),
104 keybinding_line("Ctrl+D", "Clear input", &self.theme),
105 keybinding_line("Backspace", "Delete character", &self.theme),
106 Line::from(vec![
107 Span::styled(" Tip: ", self.theme.dimmed()),
108 Span::styled("Type queries directly or use /commands", self.theme.text()),
109 ]),
110 Line::from(""),
111 Line::from(vec![Span::styled(
112 "Results Viewer Navigation",
113 self.theme.title(),
114 )]),
115 Line::from("━".repeat(55)),
116 keybinding_line("j/k or ↑/↓", "Scroll focused panel", &self.theme),
117 keybinding_line("Alt+↑/↓", "Scroll focused panel from anywhere", &self.theme),
118 keybinding_line("Ctrl+D", "Scroll down one page", &self.theme),
119 keybinding_line("Ctrl+U", "Scroll up one page", &self.theme),
120 keybinding_line("Home", "Jump to top", &self.theme),
121 keybinding_line("End", "Jump to bottom", &self.theme),
122 Line::from(""),
123 Line::from(vec![Span::styled("Slash Commands", self.theme.title())]),
124 Line::from("━".repeat(55)),
125 keybinding_line("/config <file>", "Load configuration", &self.theme),
126 keybinding_line("/config show", "Show current config", &self.theme),
127 keybinding_line("/load <file>", "Load document", &self.theme),
128 keybinding_line("/stats", "Show graph statistics", &self.theme),
129 keybinding_line("/entities [filter]", "List entities", &self.theme),
130 keybinding_line("/reason <query>", "One-shot reasoning query", &self.theme),
131 keybinding_line("/mode ask|explain|reason", "Switch query mode", &self.theme),
132 keybinding_line(
133 "/export <file.md>",
134 "Export history to Markdown",
135 &self.theme,
136 ),
137 keybinding_line("/workspace <name>", "Switch workspace", &self.theme),
138 keybinding_line("/help", "Show command help", &self.theme),
139 Line::from(""),
140 Line::from(vec![Span::styled("Status Indicators", self.theme.title())]),
141 Line::from("━".repeat(55)),
142 Line::from(vec![
143 Span::styled("ℹ ", self.theme.info()),
144 Span::styled("Info ", self.theme.text()),
145 Span::styled("✓ ", self.theme.success()),
146 Span::styled("Success ", self.theme.text()),
147 Span::styled("⚠ ", self.theme.warning()),
148 Span::styled("Warning", self.theme.text()),
149 ]),
150 Line::from(vec![
151 Span::styled("✗ ", self.theme.error()),
152 Span::styled("Error ", self.theme.text()),
153 Span::styled("⟳ ", self.theme.progress()),
154 Span::styled("Progress", self.theme.text()),
155 ]),
156 Line::from(""),
157 Line::from(vec![Span::styled(
158 "Press ? to close this help",
159 self.theme.dimmed(),
160 )]),
161 ];
162
163 let paragraph = Paragraph::new(help_text)
164 .block(block)
165 .alignment(Alignment::Left);
166
167 f.render_widget(paragraph, area);
168 }
169}
170
171fn keybinding_line<'a>(key: &'a str, description: &'a str, theme: &'a Theme) -> Line<'a> {
173 Line::from(vec![
174 Span::styled(format!(" {:15} ", key), theme.highlight()),
175 Span::styled(description.to_string(), theme.text()),
176 ])
177}
178
179fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
181 let popup_layout = Layout::default()
182 .direction(Direction::Vertical)
183 .constraints([
184 Constraint::Percentage((100 - percent_y) / 2),
185 Constraint::Percentage(percent_y),
186 Constraint::Percentage((100 - percent_y) / 2),
187 ])
188 .split(r);
189
190 Layout::default()
191 .direction(Direction::Horizontal)
192 .constraints([
193 Constraint::Percentage((100 - percent_x) / 2),
194 Constraint::Percentage(percent_x),
195 Constraint::Percentage((100 - percent_x) / 2),
196 ])
197 .split(popup_layout[1])[1]
198}
199
200impl Default for HelpOverlay {
201 fn default() -> Self {
202 Self::new()
203 }
204}