eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
mod layout;
mod utils;
mod preview;
mod list;

use super::filter::ScoredCommand;
use crate::app::AppState;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::widgets::{Clear, ListState};

use layout::{determine_layout, center_rect};
use utils::{parse_category_filter, group_by_category, get_custom_commands};
use list::{render_command_list_with_custom, render_command_list_grouped, render_empty};
use preview::{render_preview, render_custom_preview};

pub struct PaletteRenderer;

impl PaletteRenderer {
    pub fn render(
        frame: &mut Frame,
        area: Rect,
        state: &AppState,
        commands: &[ScoredCommand],
    ) {
        // Handle empty state
        if commands.is_empty() {
            render_empty(frame, area, state);
            return;
        }
        
        // Check for category filter (e.g., "@git")
        let (query, category_filter) = parse_category_filter(&state.palette_input);
        
        let selected = state.palette_selected;

        // Only group by category if query is empty (showing all commands)
        // When filtering, preserve the filtered order for correct indexing
        let should_group = query.is_empty() && !state.palette_input.starts_with("@");
        
        // Determine layout based on result count and screen size
        let (width_percent, height_percent, use_two_column) = determine_layout(commands.len(), area.width);
        
        let popup = center_rect(width_percent, height_percent, area);
        frame.render_widget(Clear, popup);
        
        // Split into list and preview areas
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Min(3),
                Constraint::Length(4),  // Preview area with more space
            ])
            .split(popup);
        
        let list_area = chunks[0];
        let preview_area = chunks[1];
        
        // Get custom commands that match query
        let custom_commands = get_custom_commands(state, &query);
        let total_items = commands.len() + custom_commands.len();
        let adjusted_selected = selected.min(total_items.saturating_sub(1));
        
        // Determine if selected item is a custom command
        let is_custom_selected = adjusted_selected >= commands.len();
        let custom_index = if is_custom_selected {
            adjusted_selected - commands.len()
        } else {
            0
        };
        
        // Render command list (grouped or flat based on query)
        let preview_command_idx = if should_group {
            let grouped = group_by_category(commands, category_filter);
            // When grouped, we need to map the visual selected index back to the original command index
            let (original_idx, _) = render_command_list_grouped(
                frame, 
                list_area, 
                state, 
                &grouped, 
                selected.min(commands.len().saturating_sub(1)), 
                use_two_column, 
                commands
            );
            original_idx
        } else {
            // Preserve filtered order - no grouping, include custom commands
            render_command_list_with_custom(
                frame, 
                list_area, 
                state, 
                commands, 
                &custom_commands, 
                adjusted_selected
            );
            adjusted_selected
        };
        
        // Render preview for selected command
        if is_custom_selected {
            if let Some((name, cmd)) = custom_commands.get(custom_index) {
                render_custom_preview(frame, preview_area, state, name, cmd);
            }
        } else if let Some(scored) = commands.get(preview_command_idx.min(commands.len().saturating_sub(1))) {
            render_preview(frame, preview_area, state, scored);
        }
    }
}