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;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::widgets::{List, ListItem};
use ratatui::Frame;

#[allow(dead_code)]
pub fn render_status(frame: &mut Frame, area: Rect, state: &AppState) {
    let theme = &state.theme;
    let selection_style = style::selection(theme);

    // Constants for rendering
    const FOOTER_HEIGHT: usize = 1; // Space reserved for footer
    
    let total_files = state.status_entries.len();
    let available_height = area.height.saturating_sub(FOOTER_HEIGHT as u16) as usize;
    let selected_idx = state.status_selected.min(total_files.saturating_sub(1));

    // Calculate visible window to keep selected item centered when possible
    // Strategy: Center selected item, but adjust if near top/bottom of list
    let viewport_start = selected_idx
        .saturating_sub(available_height / 2) // Try to center selected item
        .min(total_files.saturating_sub(available_height).max(0)); // Clamp to valid range
    let viewport_end = (viewport_start + available_height).min(total_files);

    // Build list items for visible files
    let items: Vec<ListItem> = state
        .status_entries
        .iter()
        .enumerate()
        .skip(viewport_start)
        .take(viewport_end.saturating_sub(viewport_start))
        .filter_map(|(file_idx, entry)| {
            // Filter: if showing conflicts only, skip non-conflict files
            if state.conflicts_only && !entry.conflict {
                return None;
            }
            
            // Determine visual style and indicator based on file state
            let (item_style, status_indicator) = determine_file_status_style(
                file_idx,
                selected_idx,
                entry,
                theme,
                &selection_style,
            );
            
            Some(ListItem::new(format!("{}{}", status_indicator, entry.path)).style(item_style))
        })
        .collect();

    // Layout: list + footer
    let chunks = ratatui::layout::Layout::default()
        .direction(ratatui::layout::Direction::Vertical)
        .constraints([
            ratatui::layout::Constraint::Min(1),
            ratatui::layout::Constraint::Length(1),
        ])
        .split(area);

    let list = List::new(items)
        .style(style::body_style(theme))
        .block(style::pane_block(theme, "Status", false));
    frame.render_widget(list, chunks[0]);

    let footer = if total_files == 0 {
        "no files".to_string()
    } else {
        let selected_file_name = state
            .status_entries
            .get(selected_idx)
            .map(|e| e.path.as_str())
            .unwrap_or("");
        let mut footer_parts: Vec<String> = Vec::new();
        if let Some(op) = &state.op_status {
            footer_parts.push(op.clone());
        }
        
        // Show persistent workflow state indicators
        if let Some(ref workflow) = state.workflow_context {
            match workflow.state {
                crate::app::workflow::WorkflowState::RebaseInProgress => {
                    if let Some(ref progress) = workflow.progress {
                        footer_parts.push(format!("REBASE {}/{}", progress.current, progress.total));
                    } else {
                        footer_parts.push("REBASE".to_string());
                    }
                }
                crate::app::workflow::WorkflowState::CherryPickInProgress => {
                    footer_parts.push("CHERRY-PICK".to_string());
                }
                crate::app::workflow::WorkflowState::RevertInProgress => {
                    footer_parts.push("REVERT".to_string());
                }
                crate::app::workflow::WorkflowState::MergeInProgress => {
                    footer_parts.push("MERGE".to_string());
                }
                crate::app::workflow::WorkflowState::Conflicts => {
                    let conflict_count = state.status_entries.iter().filter(|e| e.conflict).count();
                    footer_parts.push(format!("CONFLICTS ({})", conflict_count));
                }
                _ => {}
            }
        }
        
        if state.refreshing {
            // Prominent refresh indicator - this is the primary abuse prevention mechanism
            // Users see "⟳ REFRESHING…" and naturally won't spam if already in progress
            // No artificial rate limits - just clear visual feedback like professional IDEs
            footer_parts.push("⟳ REFRESHING…".to_string());
        }
        footer_parts.push(format!("{}/{}", selected_idx + 1, total_files));
        footer_parts.push(format!("{}-{}", viewport_start + 1, viewport_end));
        if !selected_file_name.is_empty() {
            // Truncate long paths to fit in footer
            const MAX_FILENAME_DISPLAY_LEN: usize = 30;
            let display_name = if selected_file_name.len() > MAX_FILENAME_DISPLAY_LEN {
                let truncate_start = selected_file_name.len().saturating_sub(MAX_FILENAME_DISPLAY_LEN - 3);
                format!("...{}", &selected_file_name[truncate_start..])
            } else {
                selected_file_name.to_string()
            };
            footer_parts.push(display_name);
        }
        
        // Add compact keyboard shortcuts (only essential ones to fit in left pane)
        const KEYBOARD_SHORTCUTS: &[&str] = &["r", "s", "u", "P"];
        footer_parts.push(format!("[{}]", KEYBOARD_SHORTCUTS.join(" ")));
        
        footer_parts.join("")
    };

    // Footer may include last op log line for quick context (errors or last op).
    let mut footer_lines = vec![footer];
    if let Some(ahead) = state.merge_notifier_ahead {
        if ahead > 0 {
            footer_lines.push(format!("{} +{} ahead", state.merge_base_branch, ahead));
        } else if ahead == 0 {
            footer_lines.push(format!("{} up to date", state.merge_base_branch));
        }
    }
    if let Some(last) = state.op_log.back() {
        if !last.is_empty() {
            footer_lines.push(last.clone());
        }
    }

    let footer_text = footer_lines.join("");
    
    // Style the footer - make refresh indicator more prominent if refreshing
    let footer_style = if state.refreshing {
        // Highlight refresh indicator with emphasis
        style::text(theme, style::Emphasis::Header)
    } else {
        style::text(theme, style::Emphasis::Muted)
    };
    
    let footer_widget = ratatui::widgets::Paragraph::new(footer_text).style(footer_style);
    frame.render_widget(footer_widget, chunks[1]);
}

/// Determines the visual style and status indicator for a file entry.
/// 
/// **Status Priority:**
/// 1. Selected file → highlighted with "> " prefix
/// 2. Unstaged changes → "○ " (working tree changes)
/// 3. Staged changes only → "● " (staged changes)
/// 4. Untracked files → "○ " (new files)
/// 
/// **Special Case:** Files with both staged and unstaged show "●○ "
fn determine_file_status_style(
    file_idx: usize,
    selected_idx: usize,
    entry: &crate::git::parsers::status::StatusEntry,
    theme: &crate::config::ThemeConfig,
    selection_style: &Style,
) -> (Style, &'static str) {
    // Selected file gets special highlighting
    if file_idx == selected_idx {
        return (*selection_style, "> ");
    }
    
    // Determine status indicator based on file state
    // Priority: unstaged > staged > untracked
    let (color, base_indicator) = if entry.unstaged {
        // Unstaged changes take priority (shows current working tree state)
        (theme.unstaged_color(), "")
    } else if entry.staged {
        // Only staged, no unstaged changes
        (theme.staged_color(), "")
    } else {
        // Untracked files (new files not yet in git)
        (theme.untracked_color(), "")
    };
    
    let style = Style::default().fg(color);
    
    // Special case: file has both staged and unstaged changes
    let indicator = if entry.staged && entry.unstaged {
        "●○ " // Both indicators
    } else {
        base_indicator
    };
    
    (style, indicator)
}