fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
// <FILE>crates/fast-fs/src/nav/cls_key_map.rs</FILE> - <DESC>Configurable key bindings</DESC>
// <VERS>VERSION: 0.2.0</VERS>
// <WCTX>Adding range selection and clipboard support</WCTX>
// <CLOG>Added Shift+Up/Down for range selection, Ctrl+X/C for cut/copy</CLOG>

//! Configurable key bindings
//!
//! KeyMap maps KeyInput to Action, allowing users to customize key bindings.

use super::action::Action;
use super::key_input::KeyInput;
use hashbrown::HashMap;

/// Configurable key-to-action bindings
///
/// KeyMap provides a mapping from `KeyInput` to `Action`, allowing users
/// to customize key bindings without modifying behavior.
///
/// # Examples
///
/// ```
/// use fast_fs::nav::{KeyMap, KeyInput, Action};
///
/// // Start with default bindings
/// let mut keymap = KeyMap::default();
///
/// // Customize: use Delete key for delete action
/// keymap.unbind(&KeyInput::Char('d'));
/// keymap.bind(KeyInput::Delete, Action::Delete);
///
/// // Or start empty and add only what you need
/// let mut minimal = KeyMap::empty();
/// minimal.bind(KeyInput::Up, Action::MoveUp);
/// minimal.bind(KeyInput::Down, Action::MoveDown);
/// minimal.bind(KeyInput::Enter, Action::Enter);
/// ```
#[derive(Debug, Clone)]
pub struct KeyMap {
    bindings: HashMap<KeyInput, Action>,
}

impl KeyMap {
    /// Create an empty keymap with no bindings
    pub fn empty() -> Self {
        Self {
            bindings: HashMap::new(),
        }
    }

    /// Bind a key to an action
    ///
    /// If the key was already bound, the previous binding is replaced.
    pub fn bind(&mut self, key: KeyInput, action: Action) {
        self.bindings.insert(key, action);
    }

    /// Remove a key binding
    pub fn unbind(&mut self, key: &KeyInput) {
        self.bindings.remove(key);
    }

    /// Get the action bound to a key, if any
    pub fn get(&self, key: &KeyInput) -> Option<Action> {
        self.bindings.get(key).copied()
    }

    /// Check if a key has a binding
    pub fn is_bound(&self, key: &KeyInput) -> bool {
        self.bindings.contains_key(key)
    }

    /// Get all bindings as an iterator
    pub fn bindings(&self) -> impl Iterator<Item = (&KeyInput, &Action)> {
        self.bindings.iter()
    }

    /// Get the number of bindings
    pub fn len(&self) -> usize {
        self.bindings.len()
    }

    /// Check if the keymap is empty
    pub fn is_empty(&self) -> bool {
        self.bindings.is_empty()
    }

    /// Find all keys bound to a specific action
    pub fn keys_for(&self, action: Action) -> Vec<&KeyInput> {
        self.bindings
            .iter()
            .filter(|(_, &a)| a == action)
            .map(|(k, _)| k)
            .collect()
    }
}

impl Default for KeyMap {
    /// Create a keymap with default vim-style bindings
    fn default() -> Self {
        let mut map = Self::empty();

        // Navigation
        map.bind(KeyInput::Up, Action::MoveUp);
        map.bind(KeyInput::Down, Action::MoveDown);
        map.bind(KeyInput::Char('k'), Action::MoveUp);
        map.bind(KeyInput::Char('j'), Action::MoveDown);
        map.bind(KeyInput::Char('g'), Action::MoveToTop);
        map.bind(KeyInput::Shift('G'), Action::MoveToBottom);
        map.bind(KeyInput::PageUp, Action::PageUp);
        map.bind(KeyInput::PageDown, Action::PageDown);
        map.bind(KeyInput::Enter, Action::Enter);
        map.bind(KeyInput::Right, Action::Enter);
        map.bind(KeyInput::Backspace, Action::GoParent);
        map.bind(KeyInput::Left, Action::GoParent);
        map.bind(KeyInput::Char('-'), Action::GoBack);
        map.bind(KeyInput::Shift('_'), Action::GoForward);

        // Selection
        map.bind(KeyInput::Char(' '), Action::ToggleSelect);
        map.bind(KeyInput::Ctrl('a'), Action::SelectAll);
        map.bind(KeyInput::Escape, Action::ClearSelection);
        map.bind(KeyInput::ShiftUp, Action::MoveUpExtend);
        map.bind(KeyInput::ShiftDown, Action::MoveDownExtend);

        // Clipboard
        map.bind(KeyInput::Ctrl('x'), Action::Cut);
        map.bind(KeyInput::Ctrl('c'), Action::Copy);

        // Operations (no-op in readonly mode)
        map.bind(KeyInput::Char('d'), Action::Delete);
        map.bind(KeyInput::Char('r'), Action::Rename);
        map.bind(KeyInput::Char('n'), Action::CreateDir);
        map.bind(KeyInput::Shift('N'), Action::CreateFile);

        // View
        map.bind(KeyInput::Char('.'), Action::ToggleHidden);
        map.bind(KeyInput::Char('s'), Action::CycleSort);
        map.bind(KeyInput::Shift('R'), Action::Refresh);

        // Filter & Path
        map.bind(KeyInput::Char('/'), Action::StartFilter);
        map.bind(KeyInput::Char(':'), Action::StartPathInput);

        map
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_empty_keymap() {
        let map = KeyMap::empty();
        assert!(map.is_empty());
        assert_eq!(map.get(&KeyInput::Up), None);
    }

    #[test]
    fn test_bind_and_get() {
        let mut map = KeyMap::empty();
        map.bind(KeyInput::Up, Action::MoveUp);
        assert_eq!(map.get(&KeyInput::Up), Some(Action::MoveUp));
    }

    #[test]
    fn test_unbind() {
        let mut map = KeyMap::default();
        assert!(map.is_bound(&KeyInput::Up));
        map.unbind(&KeyInput::Up);
        assert!(!map.is_bound(&KeyInput::Up));
    }

    #[test]
    fn test_default_has_vim_bindings() {
        let map = KeyMap::default();
        assert_eq!(map.get(&KeyInput::Char('j')), Some(Action::MoveDown));
        assert_eq!(map.get(&KeyInput::Char('k')), Some(Action::MoveUp));
        assert_eq!(map.get(&KeyInput::Char('g')), Some(Action::MoveToTop));
    }

    #[test]
    fn test_keys_for_action() {
        let map = KeyMap::default();
        let up_keys = map.keys_for(Action::MoveUp);
        assert!(up_keys.contains(&&KeyInput::Up));
        assert!(up_keys.contains(&&KeyInput::Char('k')));
    }

    #[test]
    fn test_replace_binding() {
        let mut map = KeyMap::empty();
        map.bind(KeyInput::Char('x'), Action::Delete);
        map.bind(KeyInput::Char('x'), Action::Refresh);
        assert_eq!(map.get(&KeyInput::Char('x')), Some(Action::Refresh));
    }
}

// <FILE>crates/fast-fs/src/nav/cls_key_map.rs</FILE>
// <VERS>END OF VERSION: 0.2.0</VERS>