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