eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::AppState;
use crate::ui::style::{self, Emphasis};
use crate::palette::filter::ScoredCommand;
use crate::palette::command::{CommandDef, CommandCategory};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{List, ListItem, ListState, Clear, Paragraph};
use ratatui::text::{Line, Span};
use ratatui::style::Modifier;
use std::collections::HashMap;
use super::utils::{category_color, category_abbreviation, highlight_matches};
use super::layout::center_rect;

pub fn render_command_list_with_custom(
    frame: &mut Frame,
    area: Rect,
    state: &AppState,
    commands: &[ScoredCommand],
    custom_commands: &[(String, String)],
    selected: usize,
) {
    let mut items = Vec::new();
    
    // Add regular commands
    for (i, scored) in commands.iter().enumerate() {
        let cmd = scored.command;
        let is_selected = i == selected;
        items.push(build_list_item(cmd, scored, &cmd.category, is_selected, false, true, state));
    }
    
    // Add separator if both regular and custom commands exist
    if !commands.is_empty() && !custom_commands.is_empty() {
        items.push(ListItem::new(Line::from(vec![
            Span::styled("─── Custom Commands ───", style::text(&state.theme, Emphasis::Muted)),
        ])));
    }
    
    // Add custom commands
    let custom_start = commands.len();
    for (i, (name, _)) in custom_commands.iter().enumerate() {
        let idx = custom_start + i;
        let is_selected = idx == selected;
        let style = if is_selected {
            style::selection(&state.theme)
        } else {
            style::body_style(&state.theme)
        };
        items.push(ListItem::new(Line::from(vec![
            Span::styled(format!("🔧 {}", name), style),
        ])).style(style));
    }
    
    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, "Commands", false));
    
    frame.render_widget(list, area);
}

pub fn render_command_list_grouped(
    frame: &mut Frame,
    area: Rect,
    state: &AppState,
    grouped: &HashMap<CommandCategory, Vec<&ScoredCommand>>,
    selected: usize,
    _use_two_column: bool,
    original_commands: &[ScoredCommand],
) -> (usize, usize) {
    // Flatten grouped commands while preserving category info and original index
    // Returns (original_command_index, visual_selected_index)
    let mut flat_commands: Vec<(CommandCategory, &ScoredCommand, usize)> = Vec::new();
    
    // Sort categories for consistent ordering
    let mut categories: Vec<_> = grouped.keys().collect();
    categories.sort_by_key(|c| format!("{:?}", c));
    
    for category in categories {
        let commands_in_category = &grouped[category];
        
        for cmd in commands_in_category {
            // Find the original index of this command in the original commands array
            let original_idx = original_commands
                .iter()
                .position(|c| std::ptr::eq(c.command, cmd.command))
                .unwrap_or(flat_commands.len());
            flat_commands.push((*category, *cmd, original_idx));
        }
    }
    
    // Find the original command index for the selected visual position
    let visual_selected = selected.min(flat_commands.len().saturating_sub(1));
    let original_idx = flat_commands
        .get(visual_selected)
        .map(|(_, _, idx)| *idx)
        .unwrap_or(selected);
    
    // Progressive disclosure: show top 5 with full details, next 10 with name+shortcut
    let items: Vec<ListItem> = flat_commands
        .iter()
        .enumerate()
        .map(|(i, (category, scored, _global_idx))| {
            let cmd = scored.command;
            let is_selected = i == visual_selected;
            
            // Determine detail level based on position
            let show_full_details = i < 5;  // Top 5: full details
            let show_description = i < 15;  // Next 10: name + shortcut, rest: name only
            
            build_list_item(cmd, scored, category, is_selected, show_full_details, show_description, state)
        })
        .collect();
    
    let title = if state.palette_input.is_empty() {
        "Command Palette".to_string()
    } else {
        format!("Palette: {} [{} / {}]", state.palette_input, visual_selected + 1, flat_commands.len().max(1))
    };
    
    let mut list_state = ListState::default();
    list_state.select(Some(visual_selected));
    
    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title.as_str(), true));
    
    frame.render_stateful_widget(list, area, &mut list_state);
    
    (original_idx, visual_selected)
}

fn build_list_item(
    cmd: &CommandDef,
    scored: &ScoredCommand,
    category: &CommandCategory,
    is_selected: bool,
    show_full_details: bool,
    show_description: bool,
    state: &AppState,
) -> ListItem<'static> {
    let mut spans = Vec::new();
    
    // Category indicator (colored)
    let category_color = category_color(category, &state.theme);
    let category_abbr = category_abbreviation(category);
    spans.push(Span::styled(
        format!("[{}] ", category_abbr),
        category_color,
    ));
    
    // Command name with match highlighting
    let name_spans = highlight_matches(cmd.name, &scored.match_ranges, state);
    spans.extend(name_spans);
    
    // Shortcut badge
    spans.push(Span::styled(
        format!(" ({})", cmd.key),
        style::text(&state.theme, Emphasis::Muted).add_modifier(Modifier::BOLD),
    ));
    
    // Description (only for full detail level)
    if show_full_details && show_description {
        spans.push(Span::styled(
            format!("{}", cmd.description),
            style::text(&state.theme, Emphasis::Muted),
        ));
    }
    
    let line = Line::from(spans);
    let item_style = if is_selected {
        style::selection(&state.theme)
    } else {
        style::text(&state.theme, Emphasis::Normal)
    };
    
    ListItem::new(line).style(item_style)
}

pub fn render_empty(frame: &mut Frame, area: Rect, state: &AppState) {
    let popup = center_rect(55, 25, area);
    frame.render_widget(Clear, popup);
    
    let block = style::pane_block(&state.theme, "Command Palette", true);
    
    let text = if state.palette_input.is_empty() {
        vec![
            Line::from("Type to search commands..."),
            Line::from(""),
            Line::from(vec![
                Span::styled("Tip: ", style::text(&state.theme, Emphasis::Header)),
                Span::styled("Use @category to filter (e.g., @git, @staging)", style::text(&state.theme, Emphasis::Muted)),
            ]),
            Line::from(""),
            Line::from(vec![
                Span::styled("Examples: ", style::text(&state.theme, Emphasis::Header)),
                Span::styled("@git push", style::text(&state.theme, Emphasis::Normal)),
            ]),
        ]
    } else {
        vec![
            Line::from("No matching commands found"),
            Line::from(""),
            Line::from(vec![
                Span::styled("Try: ", style::text(&state.theme, Emphasis::Header)),
                Span::styled("• Different keywords", style::text(&state.theme, Emphasis::Muted)),
            ]),
            Line::from(vec![
                Span::styled("     ", style::text(&state.theme, Emphasis::Muted)),
                Span::styled("• Category filter: @git, @staging, etc.", style::text(&state.theme, Emphasis::Muted)),
            ]),
        ]
    };
    
    let paragraph = Paragraph::new(text)
        .block(block)
        .style(style::body_style(&state.theme))
        .alignment(ratatui::layout::Alignment::Left);
    
    frame.render_widget(paragraph, popup);
}