eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{AppState, Action};
use crate::errors::ComponentError;
use crate::input::InputEvent;
use std::time::{Duration, Instant};

/// Handles keyboard shortcuts by looking up commands in the registry.
/// Supports both single-character and multi-character shortcuts.
pub struct ShortcutHandler {
    /// Buffer for multi-character shortcuts (e.g., "lb", "cp")
    buffer: String,
    /// Timestamp of last keypress for timeout
    last_keypress: Option<Instant>,
    /// Timeout for multi-character shortcuts (500ms)
    timeout: Duration,
}

impl ShortcutHandler {
    pub fn new() -> Self {
        Self {
            buffer: String::new(),
            last_keypress: None,
            timeout: Duration::from_millis(500),
        }
    }

    /// Handle a keypress and return an action if a shortcut matches.
    /// Returns None if no shortcut matches or if more characters are needed.
    pub fn handle_keypress(
        &mut self,
        event: InputEvent,
        state: &AppState,
    ) -> Result<Option<Action>, ComponentError> {
        if let InputEvent::Key(key) = &event {
            if key.kind != crossterm::event::KeyEventKind::Press {
                return Ok(None);
            }

            // Check for control/modifier keys first
            if key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
                // Handle Ctrl+key combinations
                if let crossterm::event::KeyCode::Char('c') = key.code {
                    return Ok(Some(Action::Quit));
                }
                return Ok(None);
            }

            // Handle character keys
            if let crossterm::event::KeyCode::Char(c) = key.code {
                // Special case: 't' opens theme picker (not a command shortcut)
                if c == 't' && !state.command_palette {
                    self.buffer.clear();
                    return Ok(Some(Action::ShowThemePicker));
                }

                // Special case: 'c' in commit mode should not trigger shortcuts
                if c == 'c' && state.commit_mode {
                    self.buffer.clear(); // Clear buffer when in commit mode
                    return Ok(None);
                }

                let now = Instant::now();

                // Check if buffer has timed out
                if let Some(last) = self.last_keypress {
                    if now.duration_since(last) > self.timeout {
                        self.buffer.clear();
                    }
                }

                // Add character to buffer
                self.buffer.push(c);
                self.last_keypress = Some(now);

                // Try to find a command with this shortcut
                let registry = crate::palette::registry::CommandRegistry::instance();
                
                // Try exact match first
                if let Some(cmd) = registry.by_key(&self.buffer, state) {
                    // Try to create action - some commands require additional input
                    if let Some(action) = cmd.create_action(state) {
                        // Command can execute immediately
                        self.buffer.clear();
                        return Ok(Some(action));
                    }
                    // Command exists but create_action returned None (needs input or disabled)
                    // Clear buffer and don't execute - let user use palette instead
                    self.buffer.clear();
                    return Ok(None);
                }

                // Check if any command starts with this buffer (multi-character shortcut)
                let enabled = registry.enabled_commands(state);
                let has_prefix_match = enabled.iter().any(|cmd| cmd.key.starts_with(&self.buffer));

                if !has_prefix_match {
                    // No command starts with this buffer, clear it
                    self.buffer.clear();
                }
                // Otherwise, keep buffer and wait for more characters
            } else {
                // Non-character key pressed - clear buffer to avoid conflicts
                self.buffer.clear();
            }
        }

        Ok(None)
    }

    /// Clear the shortcut buffer (called when other input is received)
    #[allow(dead_code)]
    pub fn clear_buffer(&mut self) {
        self.buffer.clear();
        self.last_keypress = None;
    }
}

impl Default for ShortcutHandler {
    fn default() -> Self {
        Self::new()
    }
}