sql_cli/widgets/
help_widget.rs

1use crate::debug_service::DebugProvider;
2use crate::help_text::HelpText;
3// ServiceContainer removed - no longer used
4use 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/// Actions that can be returned from the help widget
15#[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/// State for the help widget
30#[derive(Debug, Clone)]
31pub struct HelpState {
32    /// Current scroll offset
33    pub scroll_offset: u16,
34
35    /// Maximum scroll position
36    pub max_scroll: u16,
37
38    /// Search query within help
39    pub search_query: String,
40
41    /// Whether search mode is active
42    pub search_active: bool,
43
44    /// Current search match index
45    pub search_match_index: usize,
46
47    /// All search match positions
48    pub search_matches: Vec<usize>,
49
50    /// Selected help section
51    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
78/// Help widget that manages its own state and rendering
79pub struct HelpWidget {
80    state: HelpState,
81    // ServiceContainer removed - debug service no longer tracked here
82}
83
84impl HelpWidget {
85    pub fn new() -> Self {
86        Self {
87            state: HelpState::default(),
88        }
89    }
90
91    // set_services method removed - ServiceContainer no longer used
92
93    // log_debug method removed - ServiceContainer no longer used
94
95    /// Handle key input
96    pub fn handle_key(&mut self, key: KeyEvent) -> HelpAction {
97        // F5 should exit help and show debug - let main app handle it
98        if key.code == KeyCode::F(5) {
99            return HelpAction::Exit;
100        }
101
102        // Handle search mode
103        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        // Normal mode key handling
128        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            // Section navigation with number keys
168            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    /// Perform search within help content
203    fn perform_search(&mut self) {
204        // TODO: Implement actual search logic
205        self.state.search_matches.clear();
206    }
207
208    /// Scroll helpers
209    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    /// Render the help widget
238    pub fn render(&mut self, f: &mut Frame, area: Rect) {
239        // Simple rendering - no split screen
240        self.render_help_content(f, area);
241    }
242
243    /// Render the main help content
244    fn render_help_content(&mut self, f: &mut Frame, area: Rect) {
245        // Create layout with header
246        let chunks = Layout::default()
247            .direction(Direction::Vertical)
248            .constraints([
249                Constraint::Length(3), // Header with sections
250                Constraint::Min(0),    // Content
251                Constraint::Length(2), // Status/search bar
252            ])
253            .split(area);
254
255        // Render section tabs
256        self.render_section_tabs(f, chunks[0]);
257
258        // Render content based on selected section - now with two columns for some sections
259        match self.state.selected_section {
260            HelpSection::General => {
261                // General section uses two columns
262                self.render_two_column_content(f, chunks[1]);
263            }
264            _ => {
265                // Other sections use single column for now
266                self.render_single_column_content(f, chunks[1]);
267            }
268        }
269
270        // Render status/search bar
271        self.render_status_bar(f, chunks[2]);
272    }
273
274    /// Render content in two columns
275    fn render_two_column_content(&mut self, f: &mut Frame, area: Rect) {
276        // Split into two columns
277        let chunks = Layout::default()
278            .direction(Direction::Horizontal)
279            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
280            .split(area);
281
282        // Get the two-column content from HelpText
283        let left_content = HelpText::left_column();
284        let right_content = HelpText::right_column();
285
286        // Calculate visible area for scrolling
287        let visible_height = area.height.saturating_sub(2) as usize; // Account for borders
288        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        // Apply scroll offset
292        let scroll_offset = self.state.scroll_offset as usize;
293
294        // Get visible portions with scrolling
295        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        // Create scroll indicator in title
308        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        // Convert Vec<Line> to Text for proper rendering
319        let left_text = Text::from(left_visible);
320        let right_text = Text::from(right_visible);
321
322        // Render left column
323        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        // Render right column
332        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    /// Render content in single column (for other tabs)
345    fn render_single_column_content(&mut self, f: &mut Frame, area: Rect) {
346        let content = self.get_section_content();
347
348        // Calculate max scroll
349        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        // Create the paragraph with scrolling
354        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    /// Render section tabs
367    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    /// Get content for the selected section
404    fn get_section_content(&self) -> String {
405        match self.state.selected_section {
406            HelpSection::General => {
407                // Convert Vec<Line> to String
408                HelpText::left_column()
409                    .iter()
410                    .map(|line| line.to_string())
411                    .collect::<Vec<_>>()
412                    .join("\n")
413            }
414            HelpSection::Commands => {
415                // Convert Vec<Line> to String
416                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    /// Get section title
430    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        // Debug status display removed - ServiceContainer no longer used
560
561        help
562    }
563
564    /// Render status bar
565    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    /// Get current state for external use
593    pub fn get_state(&self) -> &HelpState {
594        &self.state
595    }
596
597    /// Reset the widget state
598    pub fn reset(&mut self) {
599        self.state = HelpState::default();
600    }
601
602    /// Called when help mode is entered
603    pub fn on_enter(&mut self) {
604        // Reset to general section when entering
605        self.state.selected_section = HelpSection::General;
606        self.state.scroll_offset = 0;
607    }
608
609    /// Called when help mode is exited
610    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}