eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Event loop module - refactored for maintainability.
//!
//! Structure:
//! - mod.rs: Main run() function (simplified)
//! - init.rs: Initialization and helper functions
//! - handlers/: Action handlers organized by category

pub mod handlers;
mod init;

pub use init::{initialize_data_sources, preflight_amend, preflight_pick_revert, validate_amend_input};
pub use init::{RebaseRecoveryInfo, detect_rebase_recovery_state};

use crate::app::{Action, reducer, Application};
use crate::input::InputEvent;
use crate::errors::ApplicationError;
use crate::config;
use crate::async_result::AsyncResult;
use crate::app::workflow::WorkflowState;
use ratatui::backend::CrosstermBackend;
use crate::ui::{BackgroundManager, font};
use std::time::Duration;
use ratatui::Terminal;
use futures::StreamExt;

/// Context passed to action handlers for shared state access.
pub struct HandlerContext<'a> {
    pub app: &'a mut Application,
    pub terminal: &'a mut Terminal<CrosstermBackend<std::io::Stdout>>,
    pub bg_manager: &'a mut BackgroundManager,
}

/// Central event loop runner.
pub async fn run(
    app: &mut Application,
    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
) -> Result<(), ApplicationError> {
    // Initialize components
    for component in app.component_manager.components.values_mut() {
        component.init(&app.state).map_err(ApplicationError::ComponentInitFailed)?;
    }

    // Initial data loading
    app.state = reducer(app.state.clone(), Action::SetRefreshing(true));
    if let Err(e) = app.component_manager.update(Action::RefreshStatus, &mut app.state) {
        tracing::warn!(error = %e, "Initial status refresh failed");
    }
    app.state = reducer(app.state.clone(), Action::SetRefreshing(false));
    
    initialize_data_sources(app);
    app.component_manager.sync_status_selection(&mut app.state);
    app.state = reducer(app.state.clone(), Action::UpdateWorkflowContext);

    // Initialize background manager
    let mut bg_manager = BackgroundManager::new();
    bg_manager.set_opacity(app.state.background_opacity);
    if let Some(path) = &app.state.background_image_path {
        if let Err(e) = bg_manager.load(path) {
            tracing::error!("Failed to load background image: {}", e);
        }
    }

    // Set font
    if let Some(f) = &app.state.font_family {
        font::set_font(f);
    }

    // Handle rebase recovery
    init::handle_rebase_recovery(app);

    // Main event loop
    while app.state.running && !*app.cancel_rx.borrow() {
        // 1. Render
        terminal.draw(|frame| {
            if let Err(e) = app.component_manager.render(frame, &app.state) {
                tracing::error!(error = %e, "Render failed");
            }
            bg_manager.process_buffer(frame.buffer_mut());
        }).map_err(ApplicationError::IoError)?;

        // 2. Wait for event
        let event = wait_for_event(app).await;
        if event.is_none() {
            if *app.cancel_rx.borrow() {
                break;
            }
            continue;
        }

        // 3. Handle event
        if let Some(input_event) = event {
            let mut ctx = HandlerContext { app, terminal, bg_manager: &mut bg_manager };
            if let Err(e) = handlers::handle_input_event(&mut ctx, input_event).await {
                tracing::error!(error = %e, "Event handling failed");
            }
        }
    }

    Ok(())
}

/// Wait for next event from various sources.
async fn wait_for_event(app: &mut Application) -> Option<InputEvent> {
    tokio::select! {
        event = app.event_stream.next() => {
            match event {
                Some(Ok(e)) => Some(InputEvent::from(e)),
                Some(Err(e)) => {
                    tracing::error!(error = %e, "Event stream error");
                    None
                }
                None => None,
            }
        }
        async_result = app.async_rx.recv() => {
            if let Some(result) = async_result {
                let _ = app.handle_async_result(result);
            }
            None // Continue loop after handling async result
        }
        _ = app.cancel_rx.changed() => {
            None
        }
        _ = tokio::time::sleep(Duration::from_millis(100)) => {
            None // Timeout
        }
    }
}