eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Modal and popup renderers.

use crate::components::manager::ComponentManager;
use crate::app::AppState;
use crate::ui::style::{self, Emphasis};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{Paragraph, List, ListItem, Clear};
use crate::components::manager::utils;

pub fn render_pr_helper(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let max_idx = state.pr_list.len().saturating_sub(1);
    let selected = state.pr_selected.min(max_idx);

    let items: Vec<ListItem> = if state.pr_list.is_empty() {
        vec![ListItem::new("No PRs found").style(style::text(&state.theme, Emphasis::Muted))]
    } else {
        state
            .pr_list
            .iter()
            .enumerate()
            .map(|(i, pr)| {
                let style_item = if i == selected {
                    style::selection(&state.theme)
                } else {
                    style::body_style(&state.theme)
                };
                ListItem::new(format!("#{} {}", pr.number, pr.title)).style(style_item)
            })
            .collect()
    };

    let title = format!(
        "PR helper [{} / {}] (Enter=checkout, o=open, Esc=close)",
        selected.saturating_add(1),
        state.pr_list.len().max(1)
    );
    let popup = utils::center_rect(60, 50, area);
    frame.render_widget(Clear, popup);

    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title.to_string(), true));

    frame.render_widget(list, popup);
}

pub fn render_log_action_menu(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    use crate::app::actions::LogAction;
    use crate::components::manager::event_handlers;

    let dirty = state.status_entries.iter().any(|e| e.staged || e.unstaged || e.conflict);
    let in_progress = state.workflow_context.as_ref().map_or(false, |ctx| {
        matches!(
            ctx.state,
            crate::app::workflow::WorkflowState::RebaseInProgress
                | crate::app::workflow::WorkflowState::CherryPickInProgress
                | crate::app::workflow::WorkflowState::MergeInProgress
                | crate::app::workflow::WorkflowState::Conflicts
        )
    });
    let block_reason = if in_progress {
        Some("blocked: rebase/cherry-pick/merge/conflicts in progress")
    } else if dirty {
        Some("blocked: requires clean working tree")
    } else {
        None
    };

    let actions: Vec<LogAction> = event_handlers::get_available_log_actions(state);
    let action_labels: Vec<(String, bool)> = actions.iter().map(|action| {
        let base = match action {
            LogAction::ShowDetail => "Show detail",
            LogAction::CherryPick => "Cherry-pick",
            LogAction::Revert => "Revert",
            LogAction::Amend => "Amend (HEAD only)",
            LogAction::ResetSoft => "Reset to here (soft)",
            LogAction::ResetMixed => "Reset to here (mixed)",
            LogAction::ResetHard => "Reset to here (hard)",
            LogAction::CreateBranch => "Create branch",
            LogAction::CreateTag => "Create tag",
        };
        let blocked = matches!(action, LogAction::CherryPick | LogAction::Revert | LogAction::ResetSoft | LogAction::ResetMixed | LogAction::ResetHard) && block_reason.is_some();
        let label = if blocked { format!("{base} ({})", block_reason.unwrap_or_default()) } else { base.to_string() };
        (label, blocked)
    }).collect();
    
    let max_idx = actions.len().saturating_sub(1);
    let selected = state.log_action_selected.min(max_idx);
    
    let items: Vec<ListItem> = action_labels.iter().enumerate().map(|(i, (label, blocked))| {
        let style_item = if i == selected {
            style::selection(&state.theme)
        } else if *blocked {
            style::text(&state.theme, Emphasis::Muted)
        } else {
            style::body_style(&state.theme)
        };
        ListItem::new(label.as_str()).style(style_item)
    }).collect();
    
    let commit_info = state.log_action_commit_hash.as_ref().map_or("Unknown commit".to_string(), |hash| {
        state.commits.iter().find(|c| c.hash == *hash)
            .map_or(hash.clone(), |c| format!("{} - {}", c.short_hash, c.message))
    });
    
    let clean_hint = block_reason.unwrap_or("Ready: clean working tree");
    let title = format!("Actions for: {}", commit_info);
    let popup_height = (actions.len() + 7).min(28) as u16;
    let popup = utils::center_rect(80, popup_height, area);
    frame.render_widget(Clear, popup);
    
    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title, true));
    
    frame.render_widget(list, popup);

    let footer_area = Rect { x: popup.x, y: popup.y.saturating_add(popup.height.saturating_sub(2)), width: popup.width, height: 2 };
    frame.render_widget(Paragraph::new(clean_hint).style(style::body_style(&state.theme)), footer_area);

    if let Some(msg) = &state.confirm_message {
        let confirm_area = utils::center_rect(50, 20, area);
        frame.render_widget(Clear, confirm_area);
        let text = format!("{msg}\n\nEnter = confirm   Esc = cancel");
        frame.render_widget(Paragraph::new(text).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, "Confirm", true)), confirm_area);
    }
}

pub fn render_rebase_todo(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let max_idx = state.rebase_todo.len().saturating_sub(1);
    let selected = state.rebase_todo_selected.min(max_idx);

    let items: Vec<ListItem> = if state.rebase_todo.is_empty() {
        vec![ListItem::new("No rebase todo").style(style::text(&state.theme, Emphasis::Muted))]
    } else {
        state.rebase_todo.iter().enumerate().map(|(i, line)| {
            let display = if state.rebase_todo_editing && i == selected {
                format!("{}  [editing]", state.rebase_todo_edit_buffer)
            } else { line.clone() };
            let style_item = if i == selected { style::selection(&state.theme) } else { style::body_style(&state.theme) };
            ListItem::new(display).style(style_item)
        }).collect()
    };

    let title = format!(
        "Rebase todo{} [{} / {}] (Esc=close)",
        if state.rebase_todo_dirty { " *" } else { "" },
        selected.saturating_add(1),
        state.rebase_todo.len().max(1)
    );
    let popup = utils::center_rect(60, 50, area);
    frame.render_widget(Clear, popup);
    frame.render_widget(List::new(items).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, title, true)), popup);
}

pub fn render_merge_log(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let popup = utils::center_rect(60, 30, area);
    frame.render_widget(Clear, popup);

    let lines: Vec<ListItem> = state.merge_notifier_log.iter().rev().take(15).rev()
        .map(|l| ListItem::new(l.clone()).style(style::text(&state.theme, Emphasis::Muted)))
        .collect();

    frame.render_widget(List::new(lines).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, "Merge notifier log".to_string(), true)), popup);
}

// Stubs for removed rebase feature
pub fn render_rebase_builder(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let entries = &state.rebase_session.entries;
    let cursor = state.rebase_session.cursor;
    let max_idx = entries.len().saturating_sub(1);
    let selected = cursor.min(max_idx);

    let items: Vec<ListItem> = if entries.is_empty() {
        vec![ListItem::new("No commits to rebase").style(style::text(&state.theme, Emphasis::Muted))]
    } else {
        entries.iter().enumerate().map(|(i, entry)| {
            let action_symbol = match entry.action {
                crate::app::rebase::RebaseAction::Pick => "p",
                crate::app::rebase::RebaseAction::Reword => "r",
                crate::app::rebase::RebaseAction::Edit => "e",
                crate::app::rebase::RebaseAction::Squash => "s",
                crate::app::rebase::RebaseAction::Fixup => "f",
                crate::app::rebase::RebaseAction::Drop => "d",
            };

            let display = format!("{} {} {}", action_symbol, entry.short_hash, entry.message);
            let style_item = if i == selected {
                style::selection(&state.theme)
            } else {
                style::body_style(&state.theme)
            };
            ListItem::new(display).style(style_item)
        }).collect()
    };

    let base_info = state.rebase_session.base_commit
        .as_ref()
        .map(|h| format!(" onto {}", &h[..8]))
        .unwrap_or_else(|| "".to_string());

    let title = format!(
        "Rebase builder{} [{} / {}] (Enter=execute, Esc=cancel)",
        if state.rebase_session.dirty { " *" } else { "" },
        selected.saturating_add(1),
        entries.len().max(1)
    );

    let popup_height = (entries.len().min(15) + 8).max(10) as u16;
    let popup = utils::center_rect(90, popup_height, area);
    frame.render_widget(Clear, popup);

    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title.as_str(), true));

    frame.render_widget(list, popup);

    // Instructions at bottom
    let base_commit_info = format!("Base commit: {}{}", state.rebase_session.base_commit.as_deref().unwrap_or("HEAD~1"), base_info);
    let instructions = vec![
        "j/k or ↑/↓ = navigate",
        "[/] = move up/down",
        "p/r/e/s/f/d = change action",
        &base_commit_info,
        "Enter = start rebase    Esc = cancel",
    ];

    let instructions_text = instructions.join("  |  ");
    let footer_area = Rect {
        x: popup.x,
        y: popup.y.saturating_add(popup.height.saturating_sub(3)),
        width: popup.width,
        height: 3
    };

    frame.render_widget(
        Paragraph::new(instructions_text)
            .style(style::text(&state.theme, Emphasis::Muted))
            .wrap(ratatui::widgets::Wrap { trim: true }),
        footer_area
    );
}

pub fn render_rebase_recovery(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let recovery_info = match crate::app::rebase::operations::RebaseRecovery::detect_interrupted_rebase(&state.repo_path) {
        Ok(Some(info)) => info,
        _ => return, // No recovery needed
    };

    let title = "Interrupted Rebase Detected";
    let message = format!(
        "An interactive rebase was interrupted.\n\n\
         Type: {}\n\
         Current Commit: {}\n\
         Progress: {} / {} steps completed\n\
         Conflicts: {}\n\n\
         Choose how to proceed:",
        match recovery_info.recovery_type {
            crate::app::rebase::RebaseRecoveryType::Interactive => "Interactive Rebase",
            crate::app::rebase::RebaseRecoveryType::Apply => "Apply-style Rebase",
        },
        recovery_info.current_commit,
        recovery_info.completed_steps,
        recovery_info.completed_steps + recovery_info.remaining_steps,
        if recovery_info.has_conflicts { "Yes - resolve first" } else { "No" }
    );

    let popup_height = 15;
    let popup = utils::center_rect(80, popup_height, area);
    frame.render_widget(Clear, popup);

    let block = style::pane_block(&state.theme, title, true);
    let inner_area = block.inner(popup);
    frame.render_widget(block, popup);

    // Render the message
    let message_paragraph = Paragraph::new(message)
        .style(style::body_style(&state.theme))
        .wrap(ratatui::widgets::Wrap { trim: true });
    frame.render_widget(message_paragraph, inner_area);

    // Instructions at bottom
    let instructions = vec![
        "c = Continue rebase",
        "a = Abort rebase",
        "Esc = Dismiss (you can recover later)",
    ];

    let instructions_text = instructions.join("  |  ");
    let footer_area = Rect {
        x: popup.x,
        y: popup.y.saturating_add(popup.height.saturating_sub(2)),
        width: popup.width,
        height: 2
    };

    frame.render_widget(
        Paragraph::new(instructions_text)
            .style(style::text(&state.theme, Emphasis::Muted))
            .wrap(ratatui::widgets::Wrap { trim: true }),
        footer_area
    );
}

pub fn render_reword_modal(_manager: &ComponentManager, _frame: &mut Frame, _area: Rect, _state: &AppState) {}
pub fn render_edit_modal(_manager: &ComponentManager, _frame: &mut Frame, _area: Rect, _state: &AppState) {}