eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
mod utils;
mod renderers;
mod event_handlers;
mod shortcut_handler;
mod rendering;
mod diff_sync;

use crate::components::{Component, StatusPane, BranchesPane, LogPane, StashPane, TagsPane, ReflogPane, DiffPane, HelpPane};
use crate::app::{AppState, Action, reducer};
use crate::app::state::{FocusPane, DiffMode};
use crate::errors::ComponentError;
use crate::input::InputEvent;
use crate::services::GitService;
use crate::palette::PaletteHandler;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use tracing::{debug, warn};
use std::collections::HashMap;
use std::sync::Arc;

pub struct ComponentManager {
    pub components: HashMap<FocusPane, Box<dyn Component>>,
    pub(super) active_component: FocusPane,
    pub(super) git_service: Arc<GitService>,
    pub(super) diff: DiffPane,
    pub(super) help: HelpPane,
    pub(super) palette_handler: PaletteHandler,
    pub(super) shortcut_handler: shortcut_handler::ShortcutHandler,
}

impl ComponentManager {
    pub fn new(git_service: Arc<GitService>) -> Self {
        let mut components: HashMap<FocusPane, Box<dyn Component>> = HashMap::new();
        
        // Initialize StatusPane
        components.insert(
            FocusPane::Status,
            Box::new(StatusPane::new(git_service.clone())),
        );
        
        // Initialize BranchesPane
        components.insert(
            FocusPane::Branches,
            Box::new(BranchesPane::new(git_service.clone())),
        );
        
        // Initialize LogPane
        components.insert(
            FocusPane::Log,
            Box::new(LogPane::new(git_service.clone())),
        );
        
        // Initialize StashPane
        components.insert(
            FocusPane::Stash,
            Box::new(StashPane::new(git_service.clone())),
        );
        
        // Initialize TagsPane
        components.insert(
            FocusPane::Tags,
            Box::new(TagsPane::new(git_service.clone())),
        );

        // Initialize ReflogPane
        components.insert(
            FocusPane::Reflog,
            Box::new(ReflogPane::new(git_service.clone())),
        );

        Self {
            components,
            active_component: FocusPane::Status,
            git_service: git_service.clone(),
            diff: DiffPane::new(),
            help: HelpPane::new(),
            palette_handler: PaletteHandler::new(),
            shortcut_handler: shortcut_handler::ShortcutHandler::new(),
        }
    }

    /// Get active component (helper for event handler dispatch)
    pub(crate) fn active_mut(&mut self) -> Option<&mut Box<dyn Component>> {
        self.components.get_mut(&self.active_component)
    }

    /// Switch active component
    pub fn switch_to(&mut self, pane: FocusPane) {
        self.active_component = pane;
    }

    /// Handle event with active component
    pub fn handle_event(
        &mut self,
        event: InputEvent,
        state: &AppState,
    ) -> Result<Option<Action>, ComponentError> {
        // Dispatch to split event handlers
        event_handlers::handle_event(self, event, state)
    }

    /// Update component with action
    pub fn update(
        &mut self,
        action: Action,
        state: &mut AppState,
    ) -> Result<(), ComponentError> {
        // Handle global actions
        match action {
            Action::StatusUp | Action::StatusDown | Action::StatusPageUp | Action::StatusPageDown | Action::StatusTop | Action::StatusBottom => {
                if let Some(component) = self.components.get_mut(&FocusPane::Status) {
                    component.update(action.clone(), state)?;
                    self.sync_status_selection(state);
                }
                return Ok(());
            }
            Action::SetStatusEntries(_) | Action::SetSelectedPath(_) | Action::ToggleDiffMode => {
                // For updates that change selection, we must sync diff view
                if let Some(component) = self.components.get_mut(&FocusPane::Status) {
                    component.update(action.clone(), state)?;
                }
                
                // If this changed the selected path (e.g. SetSelectedPath), update sync
                self.sync_status_selection(state);
                return Ok(());
            }
            // Delegate focus switching related actions to reducer, but here we switch the component
            Action::FocusNext => {
                 // state.focus is updated by reducer, we just sync our active component
                 self.switch_to(state.focus);
                 return Ok(());
            }
            Action::FocusPrev => {
                 self.switch_to(state.focus);
                 return Ok(());
            }
            _ => {
                // If action targets a specific pane, switch to it (if needed) and dispatch
                if let Some(target) = self.target_pane(&action) {
                    if let Some(component) = self.components.get_mut(&target) {
                        component.update(action, state)?;
                    }
                } else {
                    // Otherwise send to active component
                     if let Some(component) = self.components.get_mut(&self.active_component) {
                        component.update(action, state)?;
                    }
                }
            }
        }
        
        Ok(())
    }

    /// Render all components using existing UI module
    pub fn render(&mut self, frame: &mut Frame, state: &AppState) -> Result<(), ComponentError> {
        rendering::render(self, frame, state)
    }
    
    /// Get next pane in focus order
    fn next_pane(&self) -> FocusPane {
        match self.active_component {
            FocusPane::Status => FocusPane::Branches,
            FocusPane::Branches => FocusPane::Log,
            FocusPane::Log => FocusPane::Stash,
            FocusPane::Stash => FocusPane::Tags,
            FocusPane::Tags => FocusPane::Reflog,
            FocusPane::Reflog => FocusPane::Status,
        }
    }
    
    /// Get previous pane in focus order
    fn prev_pane(&self) -> FocusPane {
        match self.active_component {
            FocusPane::Status => FocusPane::Reflog,
            FocusPane::Branches => FocusPane::Status,
            FocusPane::Log => FocusPane::Branches,
            FocusPane::Stash => FocusPane::Log,
            FocusPane::Tags => FocusPane::Stash,
            FocusPane::Reflog => FocusPane::Tags,
        }
    }

    fn target_pane(&self, action: &Action) -> Option<FocusPane> {
        match action {
            Action::RefreshStatus
            | Action::StatusUp
            | Action::StatusDown
            | Action::StatusPageUp
            | Action::StatusPageDown
            | Action::StatusTop
            | Action::StatusBottom
            | Action::SetStatusEntries(_)
            | Action::SetSelectedPath(_)
            | Action::ToggleDiffMode => Some(FocusPane::Status),
            Action::RefreshBranches
            | Action::BranchUp
            | Action::BranchDown
            | Action::SetBranches(_) => Some(FocusPane::Branches),
            Action::RefreshCommits
            | Action::CommitUp
            | Action::CommitDown
            | Action::SetCommits(_)
            | Action::SetCommitDetail(_)
            | Action::ToggleLogGraph
            | Action::SetLogGraphText(_) => Some(FocusPane::Log),
            Action::RefreshStashes | Action::StashUp | Action::StashDown | Action::SetStashes(_) => {
                Some(FocusPane::Stash)
            }
            Action::RefreshTags | Action::TagUp | Action::TagDown | Action::SetTags(_) => {
                Some(FocusPane::Tags)
            }
            Action::RefreshReflog | Action::ReflogUp | Action::ReflogDown | Action::SetReflog(_) => {
                Some(FocusPane::Reflog)
            }
            _ => None,
        }
    }

    /// Synchronizes the status selection with the diff view.
    pub(crate) fn sync_status_selection(&self, state: &mut AppState) {
        diff_sync::sync_status_selection(self, state)
    }
}