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 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    // set_services method removed - ServiceContainer no longer used
99
100    // log_debug method removed - ServiceContainer no longer used
101
102    /// Handle key input
103    pub fn handle_key(&mut self, key: KeyEvent) -> HelpAction {
104        // F5 should exit help and show debug - let main app handle it
105        if key.code == KeyCode::F(5) {
106            return HelpAction::Exit;
107        }
108
109        // Handle search mode
110        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        // Normal mode key handling
135        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            // Section navigation with number keys
175            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    /// Perform search within help content
210    fn perform_search(&mut self) {
211        // TODO: Implement actual search logic
212        self.state.search_matches.clear();
213    }
214
215    /// Scroll helpers
216    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    /// Render the help widget
245    pub fn render(&mut self, f: &mut Frame, area: Rect) {
246        // Simple rendering - no split screen
247        self.render_help_content(f, area);
248    }
249
250    /// Render the main help content
251    fn render_help_content(&mut self, f: &mut Frame, area: Rect) {
252        // Create layout with header
253        let chunks = Layout::default()
254            .direction(Direction::Vertical)
255            .constraints([
256                Constraint::Length(3), // Header with sections
257                Constraint::Min(0),    // Content
258                Constraint::Length(2), // Status/search bar
259            ])
260            .split(area);
261
262        // Render section tabs
263        self.render_section_tabs(f, chunks[0]);
264
265        // Render content based on selected section - now with two columns for some sections
266        match self.state.selected_section {
267            HelpSection::General => {
268                // General section uses two columns
269                self.render_two_column_content(f, chunks[1]);
270            }
271            _ => {
272                // Other sections use single column for now
273                self.render_single_column_content(f, chunks[1]);
274            }
275        }
276
277        // Render status/search bar
278        self.render_status_bar(f, chunks[2]);
279    }
280
281    /// Render content in two columns
282    fn render_two_column_content(&mut self, f: &mut Frame, area: Rect) {
283        // Split into two columns
284        let chunks = Layout::default()
285            .direction(Direction::Horizontal)
286            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
287            .split(area);
288
289        // Get the two-column content from HelpText
290        let left_content = HelpText::left_column();
291        let right_content = HelpText::right_column();
292
293        // Calculate visible area for scrolling
294        let visible_height = area.height.saturating_sub(2) as usize; // Account for borders
295        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        // Apply scroll offset
299        let scroll_offset = self.state.scroll_offset as usize;
300
301        // Get visible portions with scrolling
302        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        // Create scroll indicator in title
315        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        // Convert Vec<Line> to Text for proper rendering
326        let left_text = Text::from(left_visible);
327        let right_text = Text::from(right_visible);
328
329        // Render left column
330        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        // Render right column
339        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    /// Render content in single column (for other tabs)
352    fn render_single_column_content(&mut self, f: &mut Frame, area: Rect) {
353        let content = self.get_section_content();
354
355        // Calculate max scroll
356        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        // Create the paragraph with scrolling
361        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    /// Render section tabs
374    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    /// Get content for the selected section
411    fn get_section_content(&self) -> String {
412        match self.state.selected_section {
413            HelpSection::General => {
414                // Convert Vec<Line> to String
415                HelpText::left_column()
416                    .iter()
417                    .map(std::string::ToString::to_string)
418                    .collect::<Vec<_>>()
419                    .join("\n")
420            }
421            HelpSection::Commands => {
422                // Convert Vec<Line> to String
423                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    /// Get section title
437    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        // Debug status display removed - ServiceContainer no longer used
541
542        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    /// Render status bar
570    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    /// Get current state for external use
598    #[must_use]
599    pub fn get_state(&self) -> &HelpState {
600        &self.state
601    }
602
603    /// Reset the widget state
604    pub fn reset(&mut self) {
605        self.state = HelpState::default();
606    }
607
608    /// Called when help mode is entered
609    pub fn on_enter(&mut self) {
610        // Reset to general section when entering
611        self.state.selected_section = HelpSection::General;
612        self.state.scroll_offset = 0;
613    }
614
615    /// Called when help mode is exited
616    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}