sabiql 1.10.0

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::ThemePalette;

use crate::app::model::app_state::AppState;
use crate::app::update::input::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, HISTORY_KEYS, INSPECTOR_DDL_KEYS, JSONB_DETAIL_KEYS, JSONB_EDIT_KEYS,
    JSONB_SEARCH_KEYS, KeyBinding, NAVIGATION_KEYS, OVERLAY_KEYS, QUERY_HISTORY_PICKER_ROWS,
    RESULT_ACTIVE_KEYS, SQL_MODAL_COMPARE_KEYS, SQL_MODAL_CONFIRMING_KEYS, SQL_MODAL_KEYS,
    SQL_MODAL_NORMAL_KEYS, SQL_MODAL_PLAN_KEYS, TABLE_PICKER_ROWS,
};

use crate::ui::primitives::atoms::scroll_indicator::{
    VerticalScrollParams, clamp_scroll_offset, render_vertical_scroll_indicator_bar,
};
use crate::ui::primitives::molecules::render_modal;

pub struct HelpOverlay;

impl HelpOverlay {
    pub fn render(frame: &mut Frame, state: &AppState, theme: &ThemePalette) {
        let (_, inner) = render_modal(
            frame,
            Constraint::Percentage(70),
            Constraint::Percentage(80),
            " Help ",
            " ?/Esc Close ",
            theme,
        );

        let mut help_lines = vec![Self::section("Global Keys", theme)];
        Self::push_dedup(&mut help_lines, GLOBAL_KEYS, theme);

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

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Result History", theme));
        Self::push_dedup(&mut help_lines, HISTORY_KEYS, theme);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("Query History Picker", theme));
        for row in QUERY_HISTORY_PICKER_ROWS {
            help_lines.push(Self::key_line(row.key, row.description, theme));
        }

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

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

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

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

        help_lines.push(Line::from(""));
        help_lines.push(Self::section("JSONB Detail", theme));
        for kb in JSONB_DETAIL_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description, theme));
        }

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

        help_lines.push(Line::raw(""));
        help_lines.push(Self::section("JSONB Search", theme));
        for kb in JSONB_SEARCH_KEYS {
            help_lines.push(Self::key_line(kb.key, kb.description, theme));
        }

        let total_lines = help_lines.len();
        let viewport_height = inner.height as usize;
        let scroll_offset =
            clamp_scroll_offset(state.ui.help_scroll_offset, viewport_height, total_lines);

        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,
                has_horizontal_scrollbar: false,
            },
            theme,
        );
    }

    fn section(title: &str, theme: &ThemePalette) -> 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 push_dedup(lines: &mut Vec<Line<'static>>, bindings: &[KeyBinding], theme: &ThemePalette) {
        let mut i = 0;
        while i < bindings.len() {
            if i + 1 < bindings.len() && bindings[i].key == bindings[i + 1].key {
                let toggle_desc = format!("Toggle {}", bindings[i].desc_short);
                lines.push(Self::key_line(bindings[i].key, &toggle_desc, theme));
                i += 2;
            } else {
                lines.push(Self::key_line(
                    bindings[i].key,
                    bindings[i].description,
                    theme,
                ));
                i += 1;
            }
        }
    }

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