1use crate::debug_service::DebugProvider;
2use crate::help_text::HelpText;
3use crate::widget_traits::DebugInfoProvider;
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use ratatui::{
7 layout::{Constraint, Direction, Layout, Rect},
8 style::{Color, Modifier, Style},
9 text::{Line, Span, Text},
10 widgets::{Block, Borders, Paragraph, Wrap},
11 Frame,
12};
13
14#[derive(Debug, Clone)]
16pub enum HelpAction {
17 None,
18 Exit,
19 ShowDebug,
20 ScrollUp,
21 ScrollDown,
22 PageUp,
23 PageDown,
24 Home,
25 End,
26 Search(String),
27}
28
29#[derive(Debug, Clone)]
31pub struct HelpState {
32 pub scroll_offset: u16,
34
35 pub max_scroll: u16,
37
38 pub search_query: String,
40
41 pub search_active: bool,
43
44 pub search_match_index: usize,
46
47 pub search_matches: Vec<usize>,
49
50 pub selected_section: HelpSection,
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub enum HelpSection {
56 General,
57 Commands,
58 Navigation,
59 Search,
60 Advanced,
61 Debug,
62}
63
64impl Default for HelpState {
65 fn default() -> Self {
66 Self {
67 scroll_offset: 0,
68 max_scroll: 0,
69 search_query: String::new(),
70 search_active: false,
71 search_match_index: 0,
72 search_matches: Vec::new(),
73 selected_section: HelpSection::General,
74 }
75 }
76}
77
78pub struct HelpWidget {
80 state: HelpState,
81 }
83
84impl Default for HelpWidget {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl HelpWidget {
91 #[must_use]
92 pub fn new() -> Self {
93 Self {
94 state: HelpState::default(),
95 }
96 }
97
98 pub fn handle_key(&mut self, key: KeyEvent) -> HelpAction {
104 if key.code == KeyCode::F(5) {
106 return HelpAction::Exit;
107 }
108
109 if self.state.search_active {
111 match key.code {
112 KeyCode::Esc => {
113 self.state.search_active = false;
114 self.state.search_query.clear();
115 self.state.search_matches.clear();
116 return HelpAction::None;
117 }
118 KeyCode::Enter => {
119 self.perform_search();
120 return HelpAction::None;
121 }
122 KeyCode::Char(c) => {
123 self.state.search_query.push(c);
124 return HelpAction::None;
125 }
126 KeyCode::Backspace => {
127 self.state.search_query.pop();
128 return HelpAction::None;
129 }
130 _ => return HelpAction::None,
131 }
132 }
133
134 match key.code {
136 KeyCode::Esc | KeyCode::Char('q') => HelpAction::Exit,
137 KeyCode::F(1) => HelpAction::Exit,
138 KeyCode::Char('/') => {
139 self.state.search_active = true;
140 HelpAction::None
141 }
142 KeyCode::Char('j') | KeyCode::Down => {
143 self.scroll_down();
144 HelpAction::ScrollDown
145 }
146 KeyCode::Char('k') | KeyCode::Up => {
147 self.scroll_up();
148 HelpAction::ScrollUp
149 }
150 KeyCode::Char('G') if key.modifiers.contains(KeyModifiers::SHIFT) => {
151 self.scroll_to_end();
152 HelpAction::End
153 }
154 KeyCode::Char('g') => {
155 self.scroll_to_home();
156 HelpAction::Home
157 }
158 KeyCode::PageDown | KeyCode::Char(' ') => {
159 self.page_down();
160 HelpAction::PageDown
161 }
162 KeyCode::PageUp | KeyCode::Char('b') => {
163 self.page_up();
164 HelpAction::PageUp
165 }
166 KeyCode::Home => {
167 self.scroll_to_home();
168 HelpAction::Home
169 }
170 KeyCode::End => {
171 self.scroll_to_end();
172 HelpAction::End
173 }
174 KeyCode::Char('1') => {
176 self.state.selected_section = HelpSection::General;
177 self.state.scroll_offset = 0;
178 HelpAction::None
179 }
180 KeyCode::Char('2') => {
181 self.state.selected_section = HelpSection::Commands;
182 self.state.scroll_offset = 0;
183 HelpAction::None
184 }
185 KeyCode::Char('3') => {
186 self.state.selected_section = HelpSection::Navigation;
187 self.state.scroll_offset = 0;
188 HelpAction::None
189 }
190 KeyCode::Char('4') => {
191 self.state.selected_section = HelpSection::Search;
192 self.state.scroll_offset = 0;
193 HelpAction::None
194 }
195 KeyCode::Char('5') => {
196 self.state.selected_section = HelpSection::Advanced;
197 self.state.scroll_offset = 0;
198 HelpAction::None
199 }
200 KeyCode::Char('6') => {
201 self.state.selected_section = HelpSection::Debug;
202 self.state.scroll_offset = 0;
203 HelpAction::None
204 }
205 _ => HelpAction::None,
206 }
207 }
208
209 fn perform_search(&mut self) {
211 self.state.search_matches.clear();
213 }
214
215 fn scroll_up(&mut self) {
217 if self.state.scroll_offset > 0 {
218 self.state.scroll_offset = self.state.scroll_offset.saturating_sub(1);
219 }
220 }
221
222 fn scroll_down(&mut self) {
223 if self.state.scroll_offset < self.state.max_scroll {
224 self.state.scroll_offset = self.state.scroll_offset.saturating_add(1);
225 }
226 }
227
228 fn page_up(&mut self) {
229 self.state.scroll_offset = self.state.scroll_offset.saturating_sub(10);
230 }
231
232 fn page_down(&mut self) {
233 self.state.scroll_offset = (self.state.scroll_offset + 10).min(self.state.max_scroll);
234 }
235
236 fn scroll_to_home(&mut self) {
237 self.state.scroll_offset = 0;
238 }
239
240 fn scroll_to_end(&mut self) {
241 self.state.scroll_offset = self.state.max_scroll;
242 }
243
244 pub fn render(&mut self, f: &mut Frame, area: Rect) {
246 self.render_help_content(f, area);
248 }
249
250 fn render_help_content(&mut self, f: &mut Frame, area: Rect) {
252 let chunks = Layout::default()
254 .direction(Direction::Vertical)
255 .constraints([
256 Constraint::Length(3), Constraint::Min(0), Constraint::Length(2), ])
260 .split(area);
261
262 self.render_section_tabs(f, chunks[0]);
264
265 match self.state.selected_section {
267 HelpSection::General => {
268 self.render_two_column_content(f, chunks[1]);
270 }
271 _ => {
272 self.render_single_column_content(f, chunks[1]);
274 }
275 }
276
277 self.render_status_bar(f, chunks[2]);
279 }
280
281 fn render_two_column_content(&mut self, f: &mut Frame, area: Rect) {
283 let chunks = Layout::default()
285 .direction(Direction::Horizontal)
286 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
287 .split(area);
288
289 let left_content = HelpText::left_column();
291 let right_content = HelpText::right_column();
292
293 let visible_height = area.height.saturating_sub(2) as usize; let max_lines = left_content.len().max(right_content.len());
296 self.state.max_scroll = max_lines.saturating_sub(visible_height) as u16;
297
298 let scroll_offset = self.state.scroll_offset as usize;
300
301 let left_visible: Vec<Line> = left_content
303 .into_iter()
304 .skip(scroll_offset)
305 .take(visible_height)
306 .collect();
307
308 let right_visible: Vec<Line> = right_content
309 .into_iter()
310 .skip(scroll_offset)
311 .take(visible_height)
312 .collect();
313
314 let scroll_indicator = if max_lines > visible_height {
316 format!(
317 " ({}/{})",
318 scroll_offset + 1,
319 max_lines.saturating_sub(visible_height) + 1
320 )
321 } else {
322 String::new()
323 };
324
325 let left_text = Text::from(left_visible);
327 let right_text = Text::from(right_visible);
328
329 let left_paragraph = Paragraph::new(left_text)
331 .block(
332 Block::default()
333 .borders(Borders::ALL)
334 .title(format!("Commands & Editing{scroll_indicator}")),
335 )
336 .style(Style::default());
337
338 let right_paragraph = Paragraph::new(right_text)
340 .block(
341 Block::default()
342 .borders(Borders::ALL)
343 .title("Navigation & Features"),
344 )
345 .style(Style::default());
346
347 f.render_widget(left_paragraph, chunks[0]);
348 f.render_widget(right_paragraph, chunks[1]);
349 }
350
351 fn render_single_column_content(&mut self, f: &mut Frame, area: Rect) {
353 let content = self.get_section_content();
354
355 let visible_height = area.height.saturating_sub(2) as usize;
357 let content_height = content.lines().count();
358 self.state.max_scroll = content_height.saturating_sub(visible_height) as u16;
359
360 let paragraph = Paragraph::new(content)
362 .block(
363 Block::default()
364 .borders(Borders::ALL)
365 .title(self.get_section_title()),
366 )
367 .wrap(Wrap { trim: false })
368 .scroll((self.state.scroll_offset, 0));
369
370 f.render_widget(paragraph, area);
371 }
372
373 fn render_section_tabs(&self, f: &mut Frame, area: Rect) {
375 let sections = [
376 ("1:General", HelpSection::General),
377 ("2:Commands", HelpSection::Commands),
378 ("3:Navigation", HelpSection::Navigation),
379 ("4:Search", HelpSection::Search),
380 ("5:Advanced", HelpSection::Advanced),
381 ("6:Debug", HelpSection::Debug),
382 ];
383
384 let mut spans = Vec::new();
385 for (i, (label, section)) in sections.iter().enumerate() {
386 if i > 0 {
387 spans.push(Span::raw(" | "));
388 }
389
390 let style = if *section == self.state.selected_section {
391 Style::default()
392 .fg(Color::Yellow)
393 .add_modifier(Modifier::BOLD)
394 } else {
395 Style::default().fg(Color::DarkGray)
396 };
397
398 spans.push(Span::styled(*label, style));
399 }
400
401 let tabs = Paragraph::new(Line::from(spans)).block(
402 Block::default()
403 .borders(Borders::ALL)
404 .title("Help Sections"),
405 );
406
407 f.render_widget(tabs, area);
408 }
409
410 fn get_section_content(&self) -> String {
412 match self.state.selected_section {
413 HelpSection::General => {
414 HelpText::left_column()
416 .iter()
417 .map(std::string::ToString::to_string)
418 .collect::<Vec<_>>()
419 .join("\n")
420 }
421 HelpSection::Commands => {
422 HelpText::right_column()
424 .iter()
425 .map(std::string::ToString::to_string)
426 .collect::<Vec<_>>()
427 .join("\n")
428 }
429 HelpSection::Navigation => self.get_navigation_help(),
430 HelpSection::Search => self.get_search_help(),
431 HelpSection::Advanced => self.get_advanced_help(),
432 HelpSection::Debug => self.get_debug_help(),
433 }
434 }
435
436 fn get_section_title(&self) -> &str {
438 match self.state.selected_section {
439 HelpSection::General => "General Help",
440 HelpSection::Commands => "Command Reference",
441 HelpSection::Navigation => "Navigation",
442 HelpSection::Search => "Search & Filter",
443 HelpSection::Advanced => "Advanced Features",
444 HelpSection::Debug => "Debug Information",
445 }
446 }
447
448 fn get_navigation_help(&self) -> String {
449 r"NAVIGATION HELP
450
451Within Results:
452 ↑/↓ - Move between rows
453 ←/→ - Scroll columns horizontally
454 Home/End - Jump to first/last row
455 PgUp/PgDn - Page up/down
456 g - Go to first row
457 G - Go to last row
458 [number]g - Go to row number
459
460Column Navigation:
461 Tab - Next column
462 Shift+Tab - Previous column
463 [number] - Jump to column by number
464 \ - Search for column by name
465
466Selection Modes:
467 v - Toggle between row/cell selection
468 V - Select entire column
469 Ctrl+A - Select all
470
471Viewport Control:
472 Ctrl+L - Lock/unlock viewport
473 z - Center current row
474 zt - Current row to top
475 zb - Current row to bottom"
476 .to_string()
477 }
478
479 fn get_search_help(&self) -> String {
480 r"SEARCH & FILTER HELP
481
482Search Modes:
483 / - Search forward in results
484 ? - Search backward in results
485 n - Next search match
486 N - Previous search match
487 * - Search for word under cursor
488
489Filter Modes:
490 F - Filter rows (case-sensitive)
491 Shift+F - Filter rows (case-insensitive)
492 f - Fuzzy filter
493 Ctrl+F - Clear all filters
494
495Column Search:
496 \ - Search for column by name
497 Tab - Next matching column
498 Shift+Tab - Previous matching column
499 Enter - Jump to column
500
501Search Within Help:
502 / - Search in help text
503 n - Next match
504 N - Previous match
505 Esc - Exit search mode"
506 .to_string()
507 }
508
509 fn get_advanced_help(&self) -> String {
510 r"ADVANCED FEATURES
511
512Query Management:
513 Ctrl+S - Save query to file
514 Ctrl+O - Open query from file
515 Ctrl+R - Query history
516 Tab - Auto-complete
517
518Export Options:
519 Ctrl+E, C - Export to CSV
520 Ctrl+E, J - Export to JSON
521 Ctrl+E, M - Export to Markdown
522 Ctrl+E, H - Export to HTML
523
524Cache Management:
525 F7 - Show cache list
526 Ctrl+K - Clear cache
527 :cache list - List cached results
528 :cache clear - Clear all cache
529
530Buffer Management:
531 Ctrl+N - New buffer
532 Ctrl+Tab - Next buffer
533 Ctrl+Shift+Tab - Previous buffer
534 :ls - List all buffers
535 :b [n] - Switch to buffer n"
536 .to_string()
537 }
538
539 fn get_debug_help(&self) -> String {
540 String::from(
543 r"DEBUG FEATURES
544
545Debug Keys:
546 F5 - Toggle debug overlay (in help)
547 F5 - Show full debug view (from main)
548 Ctrl+D - Dump state to clipboard
549
550Debug Commands:
551 :debug on - Enable debug logging
552 :debug off - Disable debug logging
553 :debug clear - Clear debug log
554 :debug save - Save debug log to file
555
556Debug Information Available:
557 - Application state
558 - Mode transitions
559 - SQL parser state
560 - Buffer contents
561 - Widget states
562 - Performance metrics
563 - Error logs
564
565",
566 )
567 }
568
569 fn render_status_bar(&self, f: &mut Frame, area: Rect) {
571 let mut spans = Vec::new();
572
573 if self.state.search_active {
574 spans.push(Span::styled("Search: ", Style::default().fg(Color::Yellow)));
575 spans.push(Span::raw(&self.state.search_query));
576 spans.push(Span::raw(" (Enter to search, Esc to cancel)"));
577 } else {
578 spans.push(Span::raw("/:Search | "));
579 let scroll_info = format!(
580 "{}/{} ",
581 self.state.scroll_offset + 1,
582 self.state.max_scroll + 1
583 );
584 spans.push(Span::raw(scroll_info));
585 spans.push(Span::styled(
586 "| Esc:Exit",
587 Style::default().fg(Color::DarkGray),
588 ));
589 }
590
591 let status =
592 Paragraph::new(Line::from(spans)).block(Block::default().borders(Borders::ALL));
593
594 f.render_widget(status, area);
595 }
596
597 #[must_use]
599 pub fn get_state(&self) -> &HelpState {
600 &self.state
601 }
602
603 pub fn reset(&mut self) {
605 self.state = HelpState::default();
606 }
607
608 pub fn on_enter(&mut self) {
610 self.state.selected_section = HelpSection::General;
612 self.state.scroll_offset = 0;
613 }
614
615 pub fn on_exit(&mut self) {}
617}
618
619impl DebugProvider for HelpWidget {
620 fn component_name(&self) -> &'static str {
621 "HelpWidget"
622 }
623
624 fn debug_info(&self) -> String {
625 format!(
626 "HelpWidget: section={:?}, scroll={}/{}, search_active={}",
627 self.state.selected_section,
628 self.state.scroll_offset,
629 self.state.max_scroll,
630 self.state.search_active
631 )
632 }
633
634 fn debug_summary(&self) -> Option<String> {
635 Some(format!("Help: {:?}", self.state.selected_section))
636 }
637}
638
639impl DebugInfoProvider for HelpWidget {
640 fn debug_info(&self) -> String {
641 let mut info = String::from("=== HELP WIDGET ===\n");
642 info.push_str(&format!("Section: {:?}\n", self.state.selected_section));
643 info.push_str(&format!(
644 "Scroll: {}/{}\n",
645 self.state.scroll_offset, self.state.max_scroll
646 ));
647 info.push_str(&format!("Search Active: {}\n", self.state.search_active));
648 if self.state.search_active {
649 info.push_str(&format!("Search Query: '{}'\n", self.state.search_query));
650 info.push_str(&format!("Matches: {}\n", self.state.search_matches.len()));
651 }
652 info
653 }
654
655 fn debug_summary(&self) -> String {
656 format!(
657 "HelpWidget: {:?} (scroll {}/{})",
658 self.state.selected_section, self.state.scroll_offset, self.state.max_scroll
659 )
660 }
661}