fast_fs/nav/
cls_key_map.rs

1// <FILE>crates/fast-fs/src/nav/cls_key_map.rs</FILE> - <DESC>Configurable key bindings</DESC>
2// <VERS>VERSION: 0.2.0</VERS>
3// <WCTX>Adding range selection and clipboard support</WCTX>
4// <CLOG>Added Shift+Up/Down for range selection, Ctrl+X/C for cut/copy</CLOG>
5
6//! Configurable key bindings
7//!
8//! KeyMap maps KeyInput to Action, allowing users to customize key bindings.
9
10use super::action::Action;
11use super::key_input::KeyInput;
12use hashbrown::HashMap;
13
14/// Configurable key-to-action bindings
15///
16/// KeyMap provides a mapping from `KeyInput` to `Action`, allowing users
17/// to customize key bindings without modifying behavior.
18///
19/// # Examples
20///
21/// ```
22/// use fast_fs::nav::{KeyMap, KeyInput, Action};
23///
24/// // Start with default bindings
25/// let mut keymap = KeyMap::default();
26///
27/// // Customize: use Delete key for delete action
28/// keymap.unbind(&KeyInput::Char('d'));
29/// keymap.bind(KeyInput::Delete, Action::Delete);
30///
31/// // Or start empty and add only what you need
32/// let mut minimal = KeyMap::empty();
33/// minimal.bind(KeyInput::Up, Action::MoveUp);
34/// minimal.bind(KeyInput::Down, Action::MoveDown);
35/// minimal.bind(KeyInput::Enter, Action::Enter);
36/// ```
37#[derive(Debug, Clone)]
38pub struct KeyMap {
39    bindings: HashMap<KeyInput, Action>,
40}
41
42impl KeyMap {
43    /// Create an empty keymap with no bindings
44    pub fn empty() -> Self {
45        Self {
46            bindings: HashMap::new(),
47        }
48    }
49
50    /// Bind a key to an action
51    ///
52    /// If the key was already bound, the previous binding is replaced.
53    pub fn bind(&mut self, key: KeyInput, action: Action) {
54        self.bindings.insert(key, action);
55    }
56
57    /// Remove a key binding
58    pub fn unbind(&mut self, key: &KeyInput) {
59        self.bindings.remove(key);
60    }
61
62    /// Get the action bound to a key, if any
63    pub fn get(&self, key: &KeyInput) -> Option<Action> {
64        self.bindings.get(key).copied()
65    }
66
67    /// Check if a key has a binding
68    pub fn is_bound(&self, key: &KeyInput) -> bool {
69        self.bindings.contains_key(key)
70    }
71
72    /// Get all bindings as an iterator
73    pub fn bindings(&self) -> impl Iterator<Item = (&KeyInput, &Action)> {
74        self.bindings.iter()
75    }
76
77    /// Get the number of bindings
78    pub fn len(&self) -> usize {
79        self.bindings.len()
80    }
81
82    /// Check if the keymap is empty
83    pub fn is_empty(&self) -> bool {
84        self.bindings.is_empty()
85    }
86
87    /// Find all keys bound to a specific action
88    pub fn keys_for(&self, action: Action) -> Vec<&KeyInput> {
89        self.bindings
90            .iter()
91            .filter(|(_, &a)| a == action)
92            .map(|(k, _)| k)
93            .collect()
94    }
95}
96
97impl Default for KeyMap {
98    /// Create a keymap with default vim-style bindings
99    fn default() -> Self {
100        let mut map = Self::empty();
101
102        // Navigation
103        map.bind(KeyInput::Up, Action::MoveUp);
104        map.bind(KeyInput::Down, Action::MoveDown);
105        map.bind(KeyInput::Char('k'), Action::MoveUp);
106        map.bind(KeyInput::Char('j'), Action::MoveDown);
107        map.bind(KeyInput::Char('g'), Action::MoveToTop);
108        map.bind(KeyInput::Shift('G'), Action::MoveToBottom);
109        map.bind(KeyInput::PageUp, Action::PageUp);
110        map.bind(KeyInput::PageDown, Action::PageDown);
111        map.bind(KeyInput::Enter, Action::Enter);
112        map.bind(KeyInput::Right, Action::Enter);
113        map.bind(KeyInput::Backspace, Action::GoParent);
114        map.bind(KeyInput::Left, Action::GoParent);
115        map.bind(KeyInput::Char('-'), Action::GoBack);
116        map.bind(KeyInput::Shift('_'), Action::GoForward);
117
118        // Selection
119        map.bind(KeyInput::Char(' '), Action::ToggleSelect);
120        map.bind(KeyInput::Ctrl('a'), Action::SelectAll);
121        map.bind(KeyInput::Escape, Action::ClearSelection);
122        map.bind(KeyInput::ShiftUp, Action::MoveUpExtend);
123        map.bind(KeyInput::ShiftDown, Action::MoveDownExtend);
124
125        // Clipboard
126        map.bind(KeyInput::Ctrl('x'), Action::Cut);
127        map.bind(KeyInput::Ctrl('c'), Action::Copy);
128
129        // Operations (no-op in readonly mode)
130        map.bind(KeyInput::Char('d'), Action::Delete);
131        map.bind(KeyInput::Char('r'), Action::Rename);
132        map.bind(KeyInput::Char('n'), Action::CreateDir);
133        map.bind(KeyInput::Shift('N'), Action::CreateFile);
134
135        // View
136        map.bind(KeyInput::Char('.'), Action::ToggleHidden);
137        map.bind(KeyInput::Char('s'), Action::CycleSort);
138        map.bind(KeyInput::Shift('R'), Action::Refresh);
139
140        // Filter & Path
141        map.bind(KeyInput::Char('/'), Action::StartFilter);
142        map.bind(KeyInput::Char(':'), Action::StartPathInput);
143
144        map
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_empty_keymap() {
154        let map = KeyMap::empty();
155        assert!(map.is_empty());
156        assert_eq!(map.get(&KeyInput::Up), None);
157    }
158
159    #[test]
160    fn test_bind_and_get() {
161        let mut map = KeyMap::empty();
162        map.bind(KeyInput::Up, Action::MoveUp);
163        assert_eq!(map.get(&KeyInput::Up), Some(Action::MoveUp));
164    }
165
166    #[test]
167    fn test_unbind() {
168        let mut map = KeyMap::default();
169        assert!(map.is_bound(&KeyInput::Up));
170        map.unbind(&KeyInput::Up);
171        assert!(!map.is_bound(&KeyInput::Up));
172    }
173
174    #[test]
175    fn test_default_has_vim_bindings() {
176        let map = KeyMap::default();
177        assert_eq!(map.get(&KeyInput::Char('j')), Some(Action::MoveDown));
178        assert_eq!(map.get(&KeyInput::Char('k')), Some(Action::MoveUp));
179        assert_eq!(map.get(&KeyInput::Char('g')), Some(Action::MoveToTop));
180    }
181
182    #[test]
183    fn test_keys_for_action() {
184        let map = KeyMap::default();
185        let up_keys = map.keys_for(Action::MoveUp);
186        assert!(up_keys.contains(&&KeyInput::Up));
187        assert!(up_keys.contains(&&KeyInput::Char('k')));
188    }
189
190    #[test]
191    fn test_replace_binding() {
192        let mut map = KeyMap::empty();
193        map.bind(KeyInput::Char('x'), Action::Delete);
194        map.bind(KeyInput::Char('x'), Action::Refresh);
195        assert_eq!(map.get(&KeyInput::Char('x')), Some(Action::Refresh));
196    }
197}
198
199// <FILE>crates/fast-fs/src/nav/cls_key_map.rs</FILE>
200// <VERS>END OF VERSION: 0.2.0</VERS>