carch-core 5.3.2

Core library for carch, providing script management and UI components.
Documentation
use ratatui::Frame;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};

use crate::ui::state::App;

fn create_rounded_block() -> Block<'static> {
    Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)
}

pub fn render_help_popup(f: &mut Frame, app: &App, area: Rect) -> u16 {
    f.render_widget(Clear, area);

    let popup_block = create_rounded_block()
        .title("Keyboard Shortcuts")
        .border_style(Style::default().fg(app.theme.primary));

    f.render_widget(popup_block.clone(), area);

    let content_area = Rect {
        x:      popup_block.inner(area).x + 1,
        y:      popup_block.inner(area).y,
        width:  popup_block.inner(area).width.saturating_sub(2),
        height: popup_block.inner(area).height.saturating_sub(2),
    };

    let mut help_content = Vec::new();

    help_content.push(Line::from(vec![Span::styled(
        "Navigation",
        Style::default().fg(app.theme.warning).add_modifier(Modifier::BOLD),
    )]));
    help_content.push(Line::from(""));

    let nav_color = app.theme.primary;
    help_content.push(Line::from(vec![
        Span::styled(" ↑/↓ ", Style::default().bg(nav_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Move up/down in the script list", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" h/l ", Style::default().bg(nav_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled(
            "Switch between categories and scripts",
            Style::default().fg(app.theme.foreground),
        ),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" Home/End ", Style::default().bg(nav_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Jump to top/bottom of list", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![Span::styled(
        "Actions",
        Style::default().fg(app.theme.warning).add_modifier(Modifier::BOLD),
    )]));
    help_content.push(Line::from(""));

    let action_color = app.theme.success;
    help_content.push(Line::from(vec![
        Span::styled(" Enter ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Run selected script", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" Space ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled(
            "Toggle script selection in multi-select mode",
            Style::default().fg(app.theme.foreground),
        ),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" p ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Toggle preview for scripts", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" d ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Show script description", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" q, Esc ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Quit | Go back", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![Span::styled(
        "Modes",
        Style::default().fg(app.theme.warning).add_modifier(Modifier::BOLD),
    )]));
    help_content.push(Line::from(""));

    let mode_color = app.theme.accent;
    help_content.push(Line::from(vec![
        Span::styled(" / ", Style::default().bg(mode_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Search mode", Style::default().fg(app.theme.foreground)),
    ]));
    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" m ", Style::default().bg(mode_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Toggle multi-select mode", Style::default().fg(app.theme.foreground)),
        Span::raw(" | "),
        Span::styled(" Esc ", Style::default().bg(mode_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Exit multi-select mode", Style::default().fg(app.theme.foreground)),
    ]));

    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" t ", Style::default().bg(mode_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Cycle themes", Style::default().fg(app.theme.foreground)),
    ]));

    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" ? ", Style::default().bg(mode_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Show this help", Style::default().fg(app.theme.foreground)),
    ]));

    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![Span::styled(
        "Quick Actions",
        Style::default().fg(app.theme.warning).add_modifier(Modifier::BOLD),
    )]));

    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" l/-> ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled(
            "Confirm selection (same as Enter)",
            Style::default().fg(app.theme.foreground),
        ),
    ]));

    help_content.push(Line::from(""));

    help_content.push(Line::from(vec![
        Span::styled(" h/<- ", Style::default().bg(action_color).fg(app.theme.background)),
        Span::raw(" "),
        Span::styled("Cancel selection (same as Esc)", Style::default().fg(app.theme.foreground)),
    ]));

    let content_height = help_content.len() as u16;
    let visible_height = content_area.height;
    let max_scroll = content_height.saturating_sub(visible_height);

    let help_paragraph =
        Paragraph::new(help_content).block(Block::default()).scroll((app.help.scroll, 0));

    f.render_widget(help_paragraph, content_area);

    let footer_area = Rect {
        x:      area.x + 1,
        y:      area.y + area.height - 2,
        width:  area.width - 2,
        height: 1,
    };

    let scroll_status = if max_scroll > 0 {
        format!("{}/{}", app.help.scroll.min(max_scroll), max_scroll)
    } else {
        "No scroll needed".to_string()
    };

    let footer = Paragraph::new(Line::from(vec![
        Span::styled("↑/↓/j/k: ", Style::default().fg(app.theme.foreground)),
        Span::styled("Scroll  ", Style::default().fg(app.theme.primary)),
        Span::styled("Esc/q: ", Style::default().fg(app.theme.foreground)),
        Span::styled("Close", Style::default().fg(app.theme.primary)),
        Span::raw("  "),
        Span::styled(scroll_status, Style::default().fg(app.theme.secondary)),
    ]))
    .alignment(Alignment::Center);

    f.render_widget(footer, footer_area);

    max_scroll
}