use crate::debug_service::DebugProvider;
use crate::help_text::HelpText;
use crate::widget_traits::DebugInfoProvider;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
#[derive(Debug, Clone)]
pub enum HelpAction {
None,
Exit,
ShowDebug,
ScrollUp,
ScrollDown,
PageUp,
PageDown,
Home,
End,
Search(String),
}
#[derive(Debug, Clone)]
pub struct HelpState {
pub scroll_offset: u16,
pub max_scroll: u16,
pub search_query: String,
pub search_active: bool,
pub search_match_index: usize,
pub search_matches: Vec<usize>,
pub selected_section: HelpSection,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HelpSection {
General,
Commands,
Navigation,
Search,
Advanced,
Debug,
}
impl Default for HelpState {
fn default() -> Self {
Self {
scroll_offset: 0,
max_scroll: 0,
search_query: String::new(),
search_active: false,
search_match_index: 0,
search_matches: Vec::new(),
selected_section: HelpSection::General,
}
}
}
pub struct HelpWidget {
state: HelpState,
}
impl Default for HelpWidget {
fn default() -> Self {
Self::new()
}
}
impl HelpWidget {
#[must_use]
pub fn new() -> Self {
Self {
state: HelpState::default(),
}
}
pub fn handle_key(&mut self, key: KeyEvent) -> HelpAction {
if key.code == KeyCode::F(5) {
return HelpAction::Exit;
}
if self.state.search_active {
match key.code {
KeyCode::Esc => {
self.state.search_active = false;
self.state.search_query.clear();
self.state.search_matches.clear();
return HelpAction::None;
}
KeyCode::Enter => {
self.perform_search();
return HelpAction::None;
}
KeyCode::Char(c) => {
self.state.search_query.push(c);
return HelpAction::None;
}
KeyCode::Backspace => {
self.state.search_query.pop();
return HelpAction::None;
}
_ => return HelpAction::None,
}
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => HelpAction::Exit,
KeyCode::F(1) => HelpAction::Exit,
KeyCode::Char('/') => {
self.state.search_active = true;
HelpAction::None
}
KeyCode::Char('j') | KeyCode::Down => {
self.scroll_down();
HelpAction::ScrollDown
}
KeyCode::Char('k') | KeyCode::Up => {
self.scroll_up();
HelpAction::ScrollUp
}
KeyCode::Char('G') if key.modifiers.contains(KeyModifiers::SHIFT) => {
self.scroll_to_end();
HelpAction::End
}
KeyCode::Char('g') => {
self.scroll_to_home();
HelpAction::Home
}
KeyCode::PageDown | KeyCode::Char(' ') => {
self.page_down();
HelpAction::PageDown
}
KeyCode::PageUp | KeyCode::Char('b') => {
self.page_up();
HelpAction::PageUp
}
KeyCode::Home => {
self.scroll_to_home();
HelpAction::Home
}
KeyCode::End => {
self.scroll_to_end();
HelpAction::End
}
KeyCode::Char('1') => {
self.state.selected_section = HelpSection::General;
self.state.scroll_offset = 0;
HelpAction::None
}
KeyCode::Char('2') => {
self.state.selected_section = HelpSection::Commands;
self.state.scroll_offset = 0;
HelpAction::None
}
KeyCode::Char('3') => {
self.state.selected_section = HelpSection::Navigation;
self.state.scroll_offset = 0;
HelpAction::None
}
KeyCode::Char('4') => {
self.state.selected_section = HelpSection::Search;
self.state.scroll_offset = 0;
HelpAction::None
}
KeyCode::Char('5') => {
self.state.selected_section = HelpSection::Advanced;
self.state.scroll_offset = 0;
HelpAction::None
}
KeyCode::Char('6') => {
self.state.selected_section = HelpSection::Debug;
self.state.scroll_offset = 0;
HelpAction::None
}
_ => HelpAction::None,
}
}
fn perform_search(&mut self) {
self.state.search_matches.clear();
}
fn scroll_up(&mut self) {
if self.state.scroll_offset > 0 {
self.state.scroll_offset = self.state.scroll_offset.saturating_sub(1);
}
}
fn scroll_down(&mut self) {
if self.state.scroll_offset < self.state.max_scroll {
self.state.scroll_offset = self.state.scroll_offset.saturating_add(1);
}
}
fn page_up(&mut self) {
self.state.scroll_offset = self.state.scroll_offset.saturating_sub(10);
}
fn page_down(&mut self) {
self.state.scroll_offset = (self.state.scroll_offset + 10).min(self.state.max_scroll);
}
fn scroll_to_home(&mut self) {
self.state.scroll_offset = 0;
}
fn scroll_to_end(&mut self) {
self.state.scroll_offset = self.state.max_scroll;
}
pub fn render(&mut self, f: &mut Frame, area: Rect) {
self.render_help_content(f, area);
}
fn render_help_content(&mut self, f: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(2), ])
.split(area);
self.render_section_tabs(f, chunks[0]);
match self.state.selected_section {
HelpSection::General => {
self.render_two_column_content(f, chunks[1]);
}
_ => {
self.render_single_column_content(f, chunks[1]);
}
}
self.render_status_bar(f, chunks[2]);
}
fn render_two_column_content(&mut self, f: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
let left_content = HelpText::left_column();
let right_content = HelpText::right_column();
let visible_height = area.height.saturating_sub(2) as usize; let max_lines = left_content.len().max(right_content.len());
self.state.max_scroll = max_lines.saturating_sub(visible_height) as u16;
let scroll_offset = self.state.scroll_offset as usize;
let left_visible: Vec<Line> = left_content
.into_iter()
.skip(scroll_offset)
.take(visible_height)
.collect();
let right_visible: Vec<Line> = right_content
.into_iter()
.skip(scroll_offset)
.take(visible_height)
.collect();
let scroll_indicator = if max_lines > visible_height {
format!(
" ({}/{})",
scroll_offset + 1,
max_lines.saturating_sub(visible_height) + 1
)
} else {
String::new()
};
let left_text = Text::from(left_visible);
let right_text = Text::from(right_visible);
let left_paragraph = Paragraph::new(left_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("Commands & Editing{scroll_indicator}")),
)
.style(Style::default());
let right_paragraph = Paragraph::new(right_text)
.block(
Block::default()
.borders(Borders::ALL)
.title("Navigation & Features"),
)
.style(Style::default());
f.render_widget(left_paragraph, chunks[0]);
f.render_widget(right_paragraph, chunks[1]);
}
fn render_single_column_content(&mut self, f: &mut Frame, area: Rect) {
let content = self.get_section_content();
let visible_height = area.height.saturating_sub(2) as usize;
let content_height = content.lines().count();
self.state.max_scroll = content_height.saturating_sub(visible_height) as u16;
let paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.title(self.get_section_title()),
)
.wrap(Wrap { trim: false })
.scroll((self.state.scroll_offset, 0));
f.render_widget(paragraph, area);
}
fn render_section_tabs(&self, f: &mut Frame, area: Rect) {
let sections = [
("1:General", HelpSection::General),
("2:Commands", HelpSection::Commands),
("3:Navigation", HelpSection::Navigation),
("4:Search", HelpSection::Search),
("5:Advanced", HelpSection::Advanced),
("6:Debug", HelpSection::Debug),
];
let mut spans = Vec::new();
for (i, (label, section)) in sections.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" | "));
}
let style = if *section == self.state.selected_section {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
spans.push(Span::styled(*label, style));
}
let tabs = Paragraph::new(Line::from(spans)).block(
Block::default()
.borders(Borders::ALL)
.title("Help Sections"),
);
f.render_widget(tabs, area);
}
fn get_section_content(&self) -> String {
match self.state.selected_section {
HelpSection::General => {
HelpText::left_column()
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
HelpSection::Commands => {
HelpText::right_column()
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
HelpSection::Navigation => self.get_navigation_help(),
HelpSection::Search => self.get_search_help(),
HelpSection::Advanced => self.get_advanced_help(),
HelpSection::Debug => self.get_debug_help(),
}
}
fn get_section_title(&self) -> &str {
match self.state.selected_section {
HelpSection::General => "General Help",
HelpSection::Commands => "Command Reference",
HelpSection::Navigation => "Navigation",
HelpSection::Search => "Search & Filter",
HelpSection::Advanced => "Advanced Features",
HelpSection::Debug => "Debug Information",
}
}
fn get_navigation_help(&self) -> String {
r"NAVIGATION HELP
Within Results:
↑/↓ - Move between rows
←/→ - Scroll columns horizontally
Home/End - Jump to first/last row
PgUp/PgDn - Page up/down
g - Go to first row
G - Go to last row
[number]g - Go to row number
Column Navigation:
Tab - Next column
Shift+Tab - Previous column
[number] - Jump to column by number
\ - Search for column by name
Selection Modes:
v - Toggle between row/cell selection
V - Select entire column
Ctrl+A - Select all
Viewport Control:
Ctrl+L - Lock/unlock viewport
z - Center current row
zt - Current row to top
zb - Current row to bottom"
.to_string()
}
fn get_search_help(&self) -> String {
r"SEARCH & FILTER HELP
Search Modes:
/ - Search forward in results
? - Search backward in results
n - Next search match
N - Previous search match
* - Search for word under cursor
Filter Modes:
F - Filter rows (case-sensitive)
Shift+F - Filter rows (case-insensitive)
f - Fuzzy filter
Ctrl+F - Clear all filters
Column Search:
\ - Search for column by name
Tab - Next matching column
Shift+Tab - Previous matching column
Enter - Jump to column
Search Within Help:
/ - Search in help text
n - Next match
N - Previous match
Esc - Exit search mode"
.to_string()
}
fn get_advanced_help(&self) -> String {
r"ADVANCED FEATURES
Query Management:
Ctrl+S - Save query to file
Ctrl+O - Open query from file
Ctrl+R - Query history
Tab - Auto-complete
Export Options:
Ctrl+E, C - Export to CSV
Ctrl+E, J - Export to JSON
Ctrl+E, M - Export to Markdown
Ctrl+E, H - Export to HTML
Cache Management:
F7 - Show cache list
Ctrl+K - Clear cache
:cache list - List cached results
:cache clear - Clear all cache
Buffer Management:
Ctrl+N - New buffer
Ctrl+Tab - Next buffer
Ctrl+Shift+Tab - Previous buffer
:ls - List all buffers
:b [n] - Switch to buffer n"
.to_string()
}
fn get_debug_help(&self) -> String {
String::from(
r"DEBUG FEATURES
Debug Keys:
F5 - Toggle debug overlay (in help)
F5 - Show full debug view (from main)
Ctrl+D - Dump state to clipboard
Debug Commands:
:debug on - Enable debug logging
:debug off - Disable debug logging
:debug clear - Clear debug log
:debug save - Save debug log to file
Debug Information Available:
- Application state
- Mode transitions
- SQL parser state
- Buffer contents
- Widget states
- Performance metrics
- Error logs
",
)
}
fn render_status_bar(&self, f: &mut Frame, area: Rect) {
let mut spans = Vec::new();
if self.state.search_active {
spans.push(Span::styled("Search: ", Style::default().fg(Color::Yellow)));
spans.push(Span::raw(&self.state.search_query));
spans.push(Span::raw(" (Enter to search, Esc to cancel)"));
} else {
spans.push(Span::raw("/:Search | "));
let scroll_info = format!(
"{}/{} ",
self.state.scroll_offset + 1,
self.state.max_scroll + 1
);
spans.push(Span::raw(scroll_info));
spans.push(Span::styled(
"| Esc:Exit",
Style::default().fg(Color::DarkGray),
));
}
let status =
Paragraph::new(Line::from(spans)).block(Block::default().borders(Borders::ALL));
f.render_widget(status, area);
}
#[must_use]
pub fn get_state(&self) -> &HelpState {
&self.state
}
pub fn reset(&mut self) {
self.state = HelpState::default();
}
pub fn on_enter(&mut self) {
self.state.selected_section = HelpSection::General;
self.state.scroll_offset = 0;
}
pub fn on_exit(&mut self) {}
}
impl DebugProvider for HelpWidget {
fn component_name(&self) -> &'static str {
"HelpWidget"
}
fn debug_info(&self) -> String {
format!(
"HelpWidget: section={:?}, scroll={}/{}, search_active={}",
self.state.selected_section,
self.state.scroll_offset,
self.state.max_scroll,
self.state.search_active
)
}
fn debug_summary(&self) -> Option<String> {
Some(format!("Help: {:?}", self.state.selected_section))
}
}
impl DebugInfoProvider for HelpWidget {
fn debug_info(&self) -> String {
let mut info = String::from("=== HELP WIDGET ===\n");
info.push_str(&format!("Section: {:?}\n", self.state.selected_section));
info.push_str(&format!(
"Scroll: {}/{}\n",
self.state.scroll_offset, self.state.max_scroll
));
info.push_str(&format!("Search Active: {}\n", self.state.search_active));
if self.state.search_active {
info.push_str(&format!("Search Query: '{}'\n", self.state.search_query));
info.push_str(&format!("Matches: {}\n", self.state.search_matches.len()));
}
info
}
fn debug_summary(&self) -> String {
format!(
"HelpWidget: {:?} (scroll {}/{})",
self.state.selected_section, self.state.scroll_offset, self.state.max_scroll
)
}
}