eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Action handlers organized by category.
//!
//! This module dispatches input events to appropriate action handlers.

mod theme;
mod config;
mod log_action;
mod commit;
mod async_handlers;

use crate::app::{Action, reducer};
use crate::input::InputEvent;
use crate::errors::ApplicationError;
use super::HandlerContext;
use super::init::{preflight_amend, preflight_pick_revert, validate_amend_input};

/// Handle an input event by dispatching to the appropriate handler.
pub async fn handle_input_event(
    ctx: &mut HandlerContext<'_>,
    input_event: InputEvent,
) -> Result<(), ApplicationError> {
    // Convert input event to action via component manager
    let action = match ctx.app.component_manager.handle_event(input_event, &ctx.app.state) {
        Ok(Some(action)) => action,
        Ok(None) => return Ok(()),
        Err(e) => {
            tracing::error!(error = %e, "Event handling failed");
            return Ok(());
        }
    };

    // Dispatch to specialized handlers first
    if theme::handle(&action, ctx).await? { return Ok(()); }
    if config::handle(&action, ctx).await? { return Ok(()); }
    
    // Preflight checks
    if !run_preflights(&action, ctx) { return Ok(()); }
    
    // Handle confirm modal
    let action = handle_confirm(action, ctx);
    
    // Handle special actions
    if log_action::handle(&action, ctx).await? { return Ok(()); }
    if commit::handle(&action, ctx).await? { return Ok(()); }
    if async_handlers::handle(&action, ctx).await? { return Ok(()); }
    
    // Execute command if applicable
    execute_command(&action, ctx).await?;
    
    // Apply reducer
    ctx.app.state = reducer(ctx.app.state.clone(), action.clone());
    
    // Post-action updates
    post_action_updates(&action, ctx);

    Ok(())
}

/// Run preflight checks for certain actions.
fn run_preflights(action: &Action, ctx: &mut HandlerContext<'_>) -> bool {
    if matches!(action, Action::StartAmend) {
        if let Err(msg) = preflight_amend(&ctx.app.state) {
            ctx.app.state = reducer(ctx.app.state.clone(), Action::SetStatusError(Some(msg)));
            return false;
        }
    }

    if matches!(action, Action::CherryPickCommit(_) | Action::CherryPickSelected) {
        if let Err(msg) = preflight_pick_revert(&ctx.app.state, "Cherry-pick") {
            ctx.app.state = reducer(ctx.app.state.clone(), Action::SetStatusError(Some(msg)));
            return false;
        }
    }

    if matches!(action, Action::RevertCommit(_) | Action::RevertSelected) {
        if let Err(msg) = preflight_pick_revert(&ctx.app.state, "Revert") {
            ctx.app.state = reducer(ctx.app.state.clone(), Action::SetStatusError(Some(msg)));
            return false;
        }
    }

    if matches!(action, Action::CommitSubmit) && ctx.app.state.amend_mode {
        if let Err(msg) = validate_amend_input(&ctx.app.state) {
            ctx.app.state = reducer(ctx.app.state.clone(), Action::SetStatusError(Some(msg)));
            return false;
        }
    }

    true
}

/// Handle confirm modal execution.
fn handle_confirm(action: Action, ctx: &mut HandlerContext<'_>) -> Action {
    if matches!(action, Action::ConfirmExecute) {
        if let Some(next) = ctx.app.state.confirm_action.clone() {
            ctx.app.state = reducer(ctx.app.state.clone(), Action::ConfirmCancel);
            return next;
        }
        ctx.app.state = reducer(ctx.app.state.clone(), Action::ConfirmCancel);
    }
    action
}

/// Execute command for action if applicable.
async fn execute_command(action: &Action, ctx: &mut HandlerContext<'_>) -> Result<(), ApplicationError> {
    let was_palette_open = ctx.app.state.command_palette;
    
    if let Some(cmd) = ctx.app.action_to_command(action) {
        match cmd.execute(&ctx.app.git_service, &ctx.app.state) {
            Ok(result) => {
                let needs_commit_refresh = matches!(
                    action,
                    Action::Push | Action::ForcePushAfterAmend | Action::Pull
                    | Action::PullRebase | Action::PullMerge | Action::FetchAllPrune
                );
                
                ctx.app.apply_command_result(result)?;
                
                if needs_commit_refresh {
                    if let Err(e) = ctx.app.component_manager.update(Action::RefreshCommits, &mut ctx.app.state) {
                        tracing::warn!(error = %e, "Failed to refresh commits after push/pull/fetch");
                    }
                }
                
                if was_palette_open {
                    ctx.app.state = reducer(ctx.app.state.clone(), Action::HidePalette);
                }
            }
            Err(e) => {
                tracing::error!(error = %e, "Command execution failed");
                ctx.app.state = reducer(ctx.app.state.clone(), Action::SetStatusError(Some(e.to_string())));
                if was_palette_open {
                    ctx.app.state = reducer(ctx.app.state.clone(), Action::HidePalette);
                }
            }
        }
    }
    
    Ok(())
}

/// Post-action updates (selection sync, workflow context).
fn post_action_updates(action: &Action, ctx: &mut HandlerContext<'_>) {
    // Selection changes -> sync diff
    if matches!(action,
        Action::StatusUp | Action::StatusDown | Action::StatusPageUp | Action::StatusPageDown
        | Action::StatusTop | Action::StatusBottom | Action::SetStatusEntries(_)
        | Action::SetSelectedPath(_) | Action::RefreshStatus | Action::ToggleDiffMode
        | Action::SelectConflict(_) | Action::OpenConflictInEditor(_)
    ) {
        ctx.app.component_manager.sync_status_selection(&mut ctx.app.state);
    }
    
    // Workflow context updates
    if matches!(action,
        Action::SetStatusEntries(_) | Action::RefreshStatus | Action::RebaseContinue
        | Action::RebaseAbort | Action::RebaseSkip | Action::StartCommit | Action::CancelCommit
        | Action::ShowRebaseTodo | Action::ShowConflictsGuided | Action::CherryPickCommit(_)
        | Action::CherryPickSelected | Action::RevertCommit(_) | Action::RevertSelected
    ) {
        ctx.app.state = reducer(ctx.app.state.clone(), Action::UpdateWorkflowContext);
    }
    
    // Component update for non-UI actions
    let is_ui_nav = matches!(action,
        Action::PaletteUp | Action::PaletteDown | Action::SetPaletteInput(_)
        | Action::ShowLogActionMenu(_) | Action::HideLogActionMenu
        | Action::LogActionMenuUp | Action::LogActionMenuDown
    );
    if !is_ui_nav {
        if let Err(e) = ctx.app.component_manager.update(action.clone(), &mut ctx.app.state) {
            tracing::error!(error = %e, "Component update failed");
        }
    }
}