eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Keybinding registry for customizable shortcuts.
//!
//! Provides a centralized registry for keyboard shortcuts with
//! context-aware binding and conflict detection.

use std::collections::HashMap;
use crossterm::event::{KeyCode, KeyModifiers};

/// A keyboard shortcut.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Keybind {
    pub code: KeyCode,
    pub modifiers: KeyModifiers,
}

impl Keybind {
    /// Create a new keybind.
    pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
        Self { code, modifiers }
    }
    
    /// Create a simple keybind (no modifiers).
    pub fn key(code: KeyCode) -> Self {
        Self::new(code, KeyModifiers::NONE)
    }
    
    /// Create a Ctrl+key keybind.
    pub fn ctrl(code: KeyCode) -> Self {
        Self::new(code, KeyModifiers::CONTROL)
    }
    
    /// Create an Alt+key keybind.
    pub fn alt(code: KeyCode) -> Self {
        Self::new(code, KeyModifiers::ALT)
    }
    
    /// Format for display.
    pub fn display(&self) -> String {
        let mut parts = Vec::new();
        if self.modifiers.contains(KeyModifiers::CONTROL) {
            parts.push("Ctrl".to_string());
        }
        if self.modifiers.contains(KeyModifiers::ALT) {
            parts.push("Alt".to_string());
        }
        if self.modifiers.contains(KeyModifiers::SHIFT) {
            parts.push("Shift".to_string());
        }
        parts.push(self.key_name());
        parts.join("+")
    }
    
    fn key_name(&self) -> String {
        match self.code {
            KeyCode::Char(c) => c.to_uppercase().to_string(),
            KeyCode::Enter => "Enter".to_string(),
            KeyCode::Esc => "Esc".to_string(),
            KeyCode::Tab => "Tab".to_string(),
            KeyCode::Backspace => "Backspace".to_string(),
            KeyCode::Up => "".to_string(),
            KeyCode::Down => "".to_string(),
            KeyCode::Left => "".to_string(),
            KeyCode::Right => "".to_string(),
            _ => format!("{:?}", self.code),
        }
    }
}

/// Context for keybindings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyContext {
    /// Global (always active)
    Global,
    /// Status panel focused
    Status,
    /// Commits panel focused  
    Commits,
    /// Branches panel focused
    Branches,
    /// Stashes panel focused
    Stashes,
    /// Diff view
    Diff,
    /// Input mode (commit message, etc.)
    Input,
}

/// Keybinding registry.
pub struct KeybindRegistry {
    /// Bindings by context and keybind
    bindings: HashMap<(KeyContext, Keybind), String>,
}

impl KeybindRegistry {
    /// Create a new registry.
    pub fn new() -> Self {
        Self {
            bindings: HashMap::new(),
        }
    }
    
    /// Register a keybinding.
    pub fn bind(&mut self, context: KeyContext, keybind: Keybind, action: impl Into<String>) {
        self.bindings.insert((context, keybind), action.into());
    }
    
    /// Look up an action for a keybind.
    pub fn lookup(&self, context: KeyContext, keybind: &Keybind) -> Option<&str> {
        // Try context-specific first, then global
        self.bindings.get(&(context, keybind.clone()))
            .or_else(|| self.bindings.get(&(KeyContext::Global, keybind.clone())))
            .map(|s| s.as_str())
    }
    
    /// Get all bindings for a context.
    pub fn bindings_for(&self, context: KeyContext) -> Vec<(&Keybind, &str)> {
        self.bindings
            .iter()
            .filter(|((ctx, _), _)| *ctx == context || *ctx == KeyContext::Global)
            .map(|((_, kb), action)| (kb, action.as_str()))
            .collect()
    }
    
    /// Create with default bindings.
    pub fn with_defaults() -> Self {
        let mut r = Self::new();
        
        // Global bindings
        r.bind(KeyContext::Global, Keybind::key(KeyCode::Char('q')), "Quit");
        r.bind(KeyContext::Global, Keybind::key(KeyCode::Char('?')), "Help");
        r.bind(KeyContext::Global, Keybind::key(KeyCode::Tab), "NextPane");
        r.bind(KeyContext::Global, Keybind::ctrl(KeyCode::Char('r')), "Refresh");
        
        // Status bindings
        r.bind(KeyContext::Status, Keybind::key(KeyCode::Char('s')), "Stage");
        r.bind(KeyContext::Status, Keybind::key(KeyCode::Char('u')), "Unstage");
        r.bind(KeyContext::Status, Keybind::key(KeyCode::Enter), "ShowDiff");
        
        // Commit bindings
        r.bind(KeyContext::Commits, Keybind::key(KeyCode::Char('c')), "Commit");
        r.bind(KeyContext::Commits, Keybind::key(KeyCode::Char('a')), "Amend");
        
        r
    }
}

impl Default for KeybindRegistry {
    fn default() -> Self {
        Self::with_defaults()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_keybind_display() {
        let kb = Keybind::ctrl(KeyCode::Char('s'));
        assert_eq!(kb.display(), "Ctrl+S");
    }
    
    #[test]
    fn test_lookup() {
        let r = KeybindRegistry::default();
        
        // Context-specific
        let action = r.lookup(KeyContext::Status, &Keybind::key(KeyCode::Char('s')));
        assert_eq!(action, Some("Stage"));
        
        // Global fallback
        let action = r.lookup(KeyContext::Status, &Keybind::key(KeyCode::Char('q')));
        assert_eq!(action, Some("Quit"));
    }
}