shellql 0.1.7-beta

A Vim- and tmux-inspired terminal database manager for developers
Documentation
pub mod overlays;

pub use overlays::{
    binding_line, goto_bottom, goto_top, open_popup, remove_selected, render_connection_list,
    render_dismiss_hint, render_empty_connections, select_next, select_prev, selected_connection,
    visible_text,
};
use ratatui::{
    Frame,
    layout::{Constraint, Layout, Rect},
    style::{Color, Style},
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::tui::{state::AppState, ui::home::overlays::render_overlay};

pub fn render_home(frame: &mut Frame, area: Rect, state: &AppState) {
    let content_h: u16 = 10;

    // Compute exact width needed for the longest instruction line so nothing
    // is ever clipped.
    let items: &[(&str, &str)] = &[
        ("connect", "connect to one of your DBs"),
        ("add", "add a new DB connection"),
        ("help", "for help"),
        ("q", "to quit"),
    ];
    let max_cmd_len = items.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);
    let content_w = items
        .iter()
        .map(|(cmd, desc)| {
            // "type  " (6) + ":" (1) + cmd + "<Enter>" (7) + pad + desc
            6 + 1 + cmd.len() + 7 + (max_cmd_len.saturating_sub(cmd.len()) + 3) + desc.len()
        })
        .max()
        .unwrap_or(40) as u16;

    let [_, horiz, _] = Layout::horizontal([
        Constraint::Fill(1),
        Constraint::Length(content_w.min(area.width)),
        Constraint::Fill(1),
    ])
    .areas(area);

    let [_, center, _] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(content_h),
        Constraint::Fill(1),
    ])
    .areas(horiz);

    render_landing(frame, center);

    if state.overlay.is_some() {
        render_overlay(frame, area, state);
    }
}

fn render_landing(frame: &mut Frame, area: Rect) {
    let width = area.width as usize;
    let sep = Span::styled("".repeat(width), Style::default().fg(Color::DarkGray));

    // (command_name, single_key, description)
    let items: &[(&str, &str)] = &[
        ("connect", "connect to one of your DBs"),
        ("add", "add a new DB connection"),
        ("help", "for help"),
        ("q", "to quit"),
    ];

    let mut lines = vec![
        Line::from(""),
        Line::from(Span::styled(
            "ShellQL",
            Style::default().fg(Color::Blue).bold(),
        ))
        .centered(),
        Line::from(Span::styled(
            "SQL Database Manager",
            Style::default().fg(Color::Gray),
        ))
        .centered(),
        Line::from(""),
        Line::from(sep.clone()),
    ];

    let max_cmd_len = items.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);

    for (cmd, desc) in items {
        lines.push(instruction_line(cmd, desc, max_cmd_len));
    }

    lines.push(Line::from(sep));

    frame.render_widget(Paragraph::new(lines), area);
}

fn instruction_line(cmd: &str, desc: &str, max_cmd_len: usize) -> Line<'static> {
    // <Enter> sits directly after the command.
    // Pad after <Enter> so every description starts at the same column.
    let after_pad = max_cmd_len.saturating_sub(cmd.len()) + 3; // 3 = minimum gap

    Line::from(vec![
        Span::styled("type  ".to_string(), Style::default().fg(Color::White)),
        Span::styled(":".to_string(), Style::default().fg(Color::Blue).bold()),
        Span::styled(cmd.to_string(), Style::default().fg(Color::White)),
        Span::styled(
            "<Enter>".to_string(),
            Style::default().fg(Color::Blue).bold(),
        ),
        Span::raw(" ".repeat(after_pad)),
        Span::styled(desc.to_string(), Style::default().fg(Color::White)),
    ])
}