sabiql 1.6.2

A fast, driver-less TUI for browsing and editing PostgreSQL databases
Documentation
use ratatui::Frame;
use ratatui::layout::Constraint;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};

use crate::ui::theme::Theme;

use crate::app::keybindings::{
    CELL_EDIT_KEYS, COMMAND_LINE_KEYS, COMMAND_PALETTE_ROWS, CONFIRM_DIALOG_KEYS,
    CONNECTION_ERROR_ROWS, CONNECTION_SELECTOR_ROWS, CONNECTION_SETUP_KEYS, ER_PICKER_ROWS,
    GLOBAL_KEYS, HELP_ROWS, INSPECTOR_DDL_KEYS, NAVIGATION_KEYS, OVERLAY_KEYS, RESULT_ACTIVE_KEYS,
    SQL_MODAL_KEYS, TABLE_PICKER_ROWS,
};
use crate::app::state::AppState;

use super::molecules::render_modal;
use super::scroll_indicator::{VerticalScrollParams, render_vertical_scroll_indicator_bar};

pub struct HelpOverlay;

impl HelpOverlay {
    pub fn render(frame: &mut Frame, state: &AppState) {
        let (_, inner) = render_modal(
            frame,
            Constraint::Percentage(70),
            Constraint::Percentage(80),
            " Help ",
            " j/k / ↑↓ to scroll, ? or Esc to close ",
        );

        let mut help_lines = vec![Self::section("Global Keys")];
        for kb in GLOBAL_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Navigation"));
        for kb in NAVIGATION_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Result Pane"));
        for kb in RESULT_ACTIVE_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Inspector Pane (DDL tab)"));
        for kb in INSPECTOR_DDL_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Cell Edit"));
        for kb in CELL_EDIT_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("SQL Editor"));
        for kb in SQL_MODAL_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Overlays"));
        for kb in OVERLAY_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Command Line"));
        for kb in COMMAND_LINE_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Connection Setup"));
        for kb in CONNECTION_SETUP_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Connection Error"));
        for row in CONNECTION_ERROR_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Connection Selector"));
        for row in CONNECTION_SELECTOR_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("ER Diagram Picker"));
        for row in ER_PICKER_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Table Picker"));
        for row in TABLE_PICKER_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Command Palette"));
        for row in COMMAND_PALETTE_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Help Overlay"));
        for row in HELP_ROWS {
            help_lines.push(Self::key_line(row.key, row.description));
        }

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Confirm Dialog"));
        for kb in CONFIRM_DIALOG_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description));
        }

        let total_lines = help_lines.len();
        let viewport_height = inner.height as usize;
        let max_scroll = total_lines.saturating_sub(viewport_height);
        let scroll_offset = state.ui.help_scroll_offset.min(max_scroll);

        let help = Paragraph::new(help_lines)
            .wrap(Wrap { trim: false })
            .style(Style::default())
            .scroll((scroll_offset as u16, 0));

        frame.render_widget(help, inner);

        render_vertical_scroll_indicator_bar(
            frame,
            inner,
            VerticalScrollParams {
                position: scroll_offset,
                viewport_size: viewport_height,
                total_items: total_lines,
            },
        );
    }

    fn section(title: &str) -> Line<'static> {
        Line::from(vec![
            Span::styled("", Style::default().fg(Theme::SECTION_HEADER)),
            Span::styled(
                title.to_string(),
                Style::default()
                    .fg(Theme::SECTION_HEADER)
                    .add_modifier(Modifier::BOLD),
            ),
        ])
    }

    fn key_line(key: &str, desc: &str) -> Line<'static> {
        Line::from(vec![
            Span::styled(
                format!("  {:<20}", key),
                Style::default()
                    .fg(Theme::TEXT_ACCENT)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(desc.to_string(), Style::default().fg(Theme::TEXT_SECONDARY)),
        ])
    }
}