gitkraft-tui 0.9.0

GitKraft — Git IDE terminal application (Ratatui TUI)
Documentation
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Padding, Paragraph};
use ratatui::Frame;

use crate::app::App;
use crate::utils::pad_right;

/// Render the options panel with grouped sections in bordered inner blocks,
/// matching the tui-file-explorer options style.
pub fn render(app: &App, frame: &mut Frame, area: Rect) {
    let theme = app.theme();

    let key_style = Style::default()
        .fg(theme.warning)
        .add_modifier(Modifier::BOLD);
    let desc_style = Style::default().fg(theme.text_primary);
    let value_style = Style::default()
        .fg(theme.accent)
        .add_modifier(Modifier::BOLD);
    let section_title_style = Style::default().fg(theme.text_muted);
    let muted_style = Style::default().fg(theme.text_muted);

    // ── Outer block ───────────────────────────────────────────────────────
    let outer_block = Block::default()
        .title(Line::from(vec![
            Span::styled("", Style::default().fg(theme.accent)),
            Span::styled(
                "Options",
                Style::default()
                    .fg(theme.accent)
                    .add_modifier(Modifier::BOLD),
            ),
        ]))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border_active))
        .style(Style::default().bg(theme.bg))
        .padding(Padding::new(1, 1, 0, 0));

    let inner_area = outer_block.inner(area);
    frame.render_widget(outer_block, area);

    // ── Close hint ────────────────────────────────────────────────────────
    let close_line = Paragraph::new(Line::from(vec![
        Span::styled("Shift + O", key_style),
        Span::styled(" close", desc_style),
    ]));

    // ── Layout: close hint, then sections ─────────────────────────────────
    let sections = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),  // close hint
            Constraint::Length(1),  // spacer
            Constraint::Length(9),  // Settings section
            Constraint::Length(1),  // spacer
            Constraint::Length(8),  // Navigation section
            Constraint::Length(1),  // spacer
            Constraint::Length(8),  // Staging section
            Constraint::Length(1),  // spacer
            Constraint::Length(10), // Git section
            Constraint::Length(1),  // spacer
            Constraint::Length(7),  // Branch Actions section
            Constraint::Length(1),  // spacer
            Constraint::Length(10), // Commit Actions section
            Constraint::Min(0),     // remaining
        ])
        .split(inner_area);

    frame.render_widget(close_line, sections[0]);

    // ── Settings section ──────────────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Settings ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let theme_name = app.current_theme_name();
        let editor_name = app.editor.to_string();
        let last_repo = app
            .tab()
            .repo_path
            .as_ref()
            .and_then(|p| p.file_name())
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| "none".to_string());

        let on_style = Style::default()
            .fg(theme.success)
            .add_modifier(Modifier::BOLD);

        let settings_path = gitkraft_core::features::persistence::ops::tui_settings_json_path()
            .ok()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "~/.config/gitkraft/tui-settings.json".to_string());

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("t", 14), key_style),
                Span::styled(pad_right("next theme", 16), desc_style),
                Span::styled(theme_name, value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Shift + T", 14), key_style),
                Span::styled(pad_right("theme picker", 16), desc_style),
                Span::styled("panel", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Shift + E", 14), key_style),
                Span::styled(pad_right("editor", 16), desc_style),
                Span::styled(editor_name, value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right(",", 14), key_style),
                Span::styled(pad_right("edit settings", 16), desc_style),
                Span::styled(settings_path, value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("commits", 14), muted_style),
                Span::styled(pad_right("max loaded", 16), desc_style),
                Span::styled("500", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("repo", 14), muted_style),
                Span::styled(pad_right("current", 16), desc_style),
                Span::styled(last_repo, on_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[2]);
    }

    // ── Navigation section ────────────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Navigation ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("Tab", 14), key_style),
                Span::styled("cycle pane forward", desc_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Shift + Tab", 14), key_style),
                Span::styled("cycle pane backward", desc_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("j / ↓", 14), key_style),
                Span::styled("next item", desc_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("k / ↑", 14), key_style),
                Span::styled("previous item", desc_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Enter", 14), key_style),
                Span::styled("select / activate", desc_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Esc", 14), key_style),
                Span::styled("dismiss / cancel", desc_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[4]);
    }

    // ── Staging section ───────────────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Staging ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("s", 14), key_style),
                Span::styled(pad_right("stage file", 18), desc_style),
                Span::styled("selected", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("u", 14), key_style),
                Span::styled(pad_right("unstage file", 18), desc_style),
                Span::styled("selected", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("S", 14), key_style),
                Span::styled(pad_right("stage all", 18), desc_style),
                Span::styled("all files", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("U", 14), key_style),
                Span::styled(pad_right("unstage all", 18), desc_style),
                Span::styled("all files", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("d", 14), key_style),
                Span::styled(pad_right("discard", 18), desc_style),
                Span::styled("confirm ×2", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Tab", 14), key_style),
                Span::styled("toggle focus", desc_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[6]);
    }

    // ── Git section ───────────────────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Git ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("c", 14), key_style),
                Span::styled(pad_right("commit", 18), desc_style),
                Span::styled("staged changes", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("b", 14), key_style),
                Span::styled(pad_right("create branch", 18), desc_style),
                Span::styled("from HEAD", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("z", 14), key_style),
                Span::styled(pad_right("stash save", 18), desc_style),
                Span::styled("working dir", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("Z", 14), key_style),
                Span::styled(pad_right("stash pop", 18), desc_style),
                Span::styled("latest", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("f", 14), key_style),
                Span::styled(pad_right("fetch", 18), desc_style),
                Span::styled("from origin", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("p", 14), key_style),
                Span::styled(pad_right("pull (rebase)", 18), desc_style),
                Span::styled("from origin", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("P", 14), key_style),
                Span::styled(pad_right("push", 18), desc_style),
                Span::styled("to origin", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("F", 14), key_style),
                Span::styled(pad_right("force push", 18), desc_style),
                Span::styled("--force-with-lease", value_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[8]);
    }

    // ── Branch Actions section ────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Branch Actions ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("Enter", 14), key_style),
                Span::styled(pad_right("checkout", 18), desc_style),
                Span::styled("selected branch", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("m", 14), key_style),
                Span::styled(pad_right("merge", 18), desc_style),
                Span::styled("selected → HEAD", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("R", 14), key_style),
                Span::styled(pad_right("rebase onto", 18), desc_style),
                Span::styled("selected branch", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("D", 14), key_style),
                Span::styled(pad_right("delete branch", 18), desc_style),
                Span::styled("selected", value_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[10]);
    }

    // ── Commit Actions section ────────────────────────────────────────
    {
        let block = Block::default()
            .title(Span::styled(" Commit Actions ", section_title_style))
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border_inactive))
            .style(Style::default().bg(theme.bg))
            .padding(Padding::new(1, 1, 0, 0));

        let lines = vec![
            Line::from(vec![
                Span::styled(pad_right("C", 14), key_style),
                Span::styled(pad_right("cherry-pick", 18), desc_style),
                Span::styled("onto current", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("n", 14), key_style),
                Span::styled(pad_right("reset mixed", 18), desc_style),
                Span::styled("keep files", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("e", 14), key_style),
                Span::styled(pad_right("revert", 18), desc_style),
                Span::styled("selected commit", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("x", 14), key_style),
                Span::styled(pad_right("reset soft", 18), desc_style),
                Span::styled("to selected", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("X", 14), key_style),
                Span::styled(pad_right("reset hard", 18), desc_style),
                Span::styled("to selected", value_style),
            ]),
            Line::from(vec![
                Span::styled(pad_right("a", 14), key_style),
                Span::styled(pad_right("all actions…", 18), desc_style),
                Span::styled("popup menu", value_style),
            ]),
        ];

        let paragraph = Paragraph::new(lines).block(block);
        frame.render_widget(paragraph, sections[12]);
    }
}