Skip to main content

void_graph/
keybind.rs

1//! Keybinding system for the void-graph TUI.
2//!
3//! This module is adapted from [Serie](https://github.com/lusingander/serie),
4//! a Git commit graph visualizer by lusingander. Licensed under MIT.
5
6use std::ops::{Deref, DerefMut};
7
8use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9use rustc_hash::FxHashMap;
10use serde::{
11    de::{Deserializer, Error as DeError},
12    Deserialize,
13};
14
15use crate::event::UserEvent;
16
17const DEFAULT_KEY_BIND: &str = include_str!("../assets/default-keybind.toml");
18
19#[derive(Debug, Default, Clone, PartialEq, Eq)]
20pub struct KeyBind(FxHashMap<KeyEvent, UserEvent>);
21
22impl Deref for KeyBind {
23    type Target = FxHashMap<KeyEvent, UserEvent>;
24
25    fn deref(&self) -> &Self::Target {
26        &self.0
27    }
28}
29
30impl DerefMut for KeyBind {
31    fn deref_mut(&mut self) -> &mut Self::Target {
32        &mut self.0
33    }
34}
35
36impl KeyBind {
37    pub fn new(custom_keybind_patch: Option<KeyBind>) -> Self {
38        let mut keybind: KeyBind =
39            toml::from_str(DEFAULT_KEY_BIND).expect("default key bind should be correct");
40
41        if let Some(mut custom_keybind_patch) = custom_keybind_patch {
42            for (key_event, user_event) in custom_keybind_patch.drain() {
43                keybind.insert(key_event, user_event);
44            }
45        }
46
47        keybind
48    }
49
50    pub fn keys_for_event(&self, user_event: UserEvent) -> Vec<String> {
51        let mut key_events: Vec<KeyEvent> = self
52            .iter()
53            .filter(|(_, ue)| **ue == user_event)
54            .map(|(ke, _)| *ke)
55            .collect();
56        key_events.sort_by(|a, b| a.partial_cmp(b).unwrap()); // At least when used for key bindings, it doesn't seem to be a problem...
57        key_events.into_iter().map(key_event_to_string).collect()
58    }
59
60    pub fn user_command_view_toggle_event_numbers(&self) -> Vec<usize> {
61        let mut numbers: Vec<usize> = self
62            .values()
63            .filter_map(|ue| {
64                if let UserEvent::UserCommandViewToggle(n) = ue {
65                    Some(*n)
66                } else {
67                    None
68                }
69            })
70            .collect();
71        numbers.sort_unstable();
72        numbers
73    }
74}
75
76impl<'de> Deserialize<'de> for KeyBind {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        let parsed_map = FxHashMap::<UserEvent, Vec<String>>::deserialize(deserializer)?;
82        let mut key_map = FxHashMap::<KeyEvent, UserEvent>::default();
83        for (user_event, key_events) in parsed_map {
84            for key_event_str in key_events {
85                let key_event = match parse_key_event(&key_event_str) {
86                    Ok(e) => e,
87                    Err(s) => {
88                        let msg = format!("{key_event_str:?} is not a valid key event: {s:}");
89                        return Err(DeError::custom(msg));
90                    }
91                };
92                if let Some(conflict_user_event) = key_map.insert(key_event, user_event) {
93                    let msg = format!(
94                        "{key_event:?} map to multiple events: {user_event:?}, {conflict_user_event:?}"
95                    );
96                    return Err(DeError::custom(msg));
97                }
98            }
99        }
100
101        Ok(KeyBind(key_map))
102    }
103}
104
105fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
106    let raw_lower = raw.to_ascii_lowercase().replace(' ', "");
107    let (remaining, modifiers) = extract_modifiers(&raw_lower);
108    parse_key_code_with_modifiers(remaining, modifiers)
109}
110
111fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
112    let mut modifiers = KeyModifiers::empty();
113    let mut current = raw;
114
115    loop {
116        match current {
117            rest if rest.starts_with("ctrl-") => {
118                modifiers.insert(KeyModifiers::CONTROL);
119                current = &rest[5..];
120            }
121            rest if rest.starts_with("alt-") => {
122                modifiers.insert(KeyModifiers::ALT);
123                current = &rest[4..];
124            }
125            rest if rest.starts_with("shift-") => {
126                modifiers.insert(KeyModifiers::SHIFT);
127                current = &rest[6..];
128            }
129            _ => break, // break out of the loop if no known prefix is detected
130        };
131    }
132
133    (current, modifiers)
134}
135
136fn parse_key_code_with_modifiers(
137    raw: &str,
138    mut modifiers: KeyModifiers,
139) -> Result<KeyEvent, String> {
140    let c = match raw {
141        "esc" => KeyCode::Esc,
142        "enter" => KeyCode::Enter,
143        "left" => KeyCode::Left,
144        "right" => KeyCode::Right,
145        "up" => KeyCode::Up,
146        "down" => KeyCode::Down,
147        "home" => KeyCode::Home,
148        "end" => KeyCode::End,
149        "pageup" => KeyCode::PageUp,
150        "pagedown" => KeyCode::PageDown,
151        "backtab" => {
152            modifiers.insert(KeyModifiers::SHIFT);
153            KeyCode::BackTab
154        }
155        "backspace" => KeyCode::Backspace,
156        "delete" => KeyCode::Delete,
157        "insert" => KeyCode::Insert,
158        "f1" => KeyCode::F(1),
159        "f2" => KeyCode::F(2),
160        "f3" => KeyCode::F(3),
161        "f4" => KeyCode::F(4),
162        "f5" => KeyCode::F(5),
163        "f6" => KeyCode::F(6),
164        "f7" => KeyCode::F(7),
165        "f8" => KeyCode::F(8),
166        "f9" => KeyCode::F(9),
167        "f10" => KeyCode::F(10),
168        "f11" => KeyCode::F(11),
169        "f12" => KeyCode::F(12),
170        "space" => KeyCode::Char(' '),
171        "hyphen" => KeyCode::Char('-'),
172        "minus" => KeyCode::Char('-'),
173        "tab" => KeyCode::Tab,
174        c if c.len() == 1 => {
175            let mut c = c.chars().next().unwrap();
176            if modifiers.contains(KeyModifiers::SHIFT) {
177                c = c.to_ascii_uppercase();
178            }
179            KeyCode::Char(c)
180        }
181        _ => return Err(format!("Unable to parse {raw}")),
182    };
183    Ok(KeyEvent::new(c, modifiers))
184}
185
186fn key_event_to_string(key_event: KeyEvent) -> String {
187    if let KeyCode::Char(c) = key_event.code {
188        if key_event.modifiers == KeyModifiers::SHIFT {
189            return c.to_ascii_uppercase().into();
190        }
191    }
192
193    let char;
194    let key_code = match key_event.code {
195        KeyCode::Backspace => "Backspace",
196        KeyCode::Enter => "Enter",
197        KeyCode::Left => "Left",
198        KeyCode::Right => "Right",
199        KeyCode::Up => "Up",
200        KeyCode::Down => "Down",
201        KeyCode::Home => "Home",
202        KeyCode::End => "End",
203        KeyCode::PageUp => "PageUp",
204        KeyCode::PageDown => "PageDown",
205        KeyCode::Tab => "Tab",
206        KeyCode::BackTab => "BackTab",
207        KeyCode::Delete => "Delete",
208        KeyCode::Insert => "Insert",
209        KeyCode::F(n) => {
210            char = format!("F{n}");
211            &char
212        }
213        KeyCode::Char(' ') => "Space",
214        KeyCode::Char(c) => {
215            char = c.to_string();
216            &char
217        }
218        KeyCode::Esc => "Esc",
219        KeyCode::Null => "",
220        KeyCode::CapsLock => "",
221        KeyCode::Menu => "",
222        KeyCode::ScrollLock => "",
223        KeyCode::Media(_) => "",
224        KeyCode::NumLock => "",
225        KeyCode::PrintScreen => "",
226        KeyCode::Pause => "",
227        KeyCode::KeypadBegin => "",
228        KeyCode::Modifier(_) => "",
229    };
230
231    let mut modifiers = Vec::with_capacity(3);
232
233    if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
234        modifiers.push("Ctrl");
235    }
236
237    if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
238        modifiers.push("Shift");
239    }
240
241    if key_event.modifiers.intersects(KeyModifiers::ALT) {
242        modifiers.push("Alt");
243    }
244
245    let mut key = modifiers.join("-");
246
247    if !key.is_empty() {
248        key.push('-');
249    }
250    key.push_str(key_code);
251
252    key
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[rustfmt::skip]
260    #[test]
261    fn test_deserialize_keybind() {
262        let toml = r#"
263            navigate_up = ["k"]
264            navigate_down = ["j", "down"]
265            navigate_left = ["ctrl-h", "shift-h", "alt-h"]
266            navigate_right = ["ctrl-shift-l", "alt-shift-ctrl-l"]
267            quit = ["esc", "f12"]
268            user_command_view_toggle_1 = ["d"]
269            user_command_view_toggle_10 = ["e"]
270        "#;
271
272        let expected = KeyBind(
273            [
274                (
275                    KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty()),
276                    UserEvent::NavigateUp,
277                ),
278                (
279                    KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty()),
280                    UserEvent::NavigateDown,
281                ),
282                (
283                    KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
284                    UserEvent::NavigateDown,
285                ),
286                (
287                    KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL),
288                    UserEvent::NavigateLeft,
289                ),
290                (
291                    KeyEvent::new(KeyCode::Char('h'), KeyModifiers::SHIFT),
292                    UserEvent::NavigateLeft,
293                ),
294                (
295                    KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT),
296                    UserEvent::NavigateLeft,
297                ),
298                (
299                    KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT),
300                    UserEvent::NavigateRight,
301                ),
302                (
303                    KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT),
304                    UserEvent::NavigateRight,
305                ),
306                (
307                    KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
308                    UserEvent::Quit,
309                ),
310                (
311                    KeyEvent::new(KeyCode::F(12), KeyModifiers::empty()),
312                    UserEvent::Quit,
313                ),
314                (
315                    KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty()),
316                    UserEvent::UserCommandViewToggle(1),
317                ),
318                (
319                    KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()),
320                    UserEvent::UserCommandViewToggle(10),
321                ),
322            ]
323            .into_iter()
324            .collect(),
325        );
326
327        let actual: KeyBind = toml::from_str(toml).unwrap();
328
329        assert_eq!(actual, expected);
330    }
331
332    #[rustfmt::skip]
333    #[test]
334    fn test_key_event_to_string() {
335        let key_event = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty());
336        assert_eq!(key_event_to_string(key_event), "k");
337
338        let key_event = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty());
339        assert_eq!(key_event_to_string(key_event), "j");
340
341        let key_event = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
342        assert_eq!(key_event_to_string(key_event), "Down");
343
344        let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL);
345        assert_eq!(key_event_to_string(key_event), "Ctrl-h");
346
347        let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::SHIFT);
348        assert_eq!(key_event_to_string(key_event), "H");
349
350        let key_event = KeyEvent::new(KeyCode::Char('H'), KeyModifiers::SHIFT);
351        assert_eq!(key_event_to_string(key_event), "H");
352
353        let key_event = KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT);
354        assert_eq!(key_event_to_string(key_event), "Shift-Left");
355
356        let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT);
357        assert_eq!(key_event_to_string(key_event), "Alt-h");
358
359        let key_event = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT);
360        assert_eq!(key_event_to_string(key_event), "Ctrl-Shift-l");
361
362        let key_event = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT);
363        assert_eq!(key_event_to_string(key_event), "Ctrl-Shift-Alt-l");
364
365        let key_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
366        assert_eq!(key_event_to_string(key_event), "Esc");
367
368        let key_event = KeyEvent::new(KeyCode::F(12), KeyModifiers::empty());
369        assert_eq!(key_event_to_string(key_event), "F12");
370    }
371
372    #[test]
373    fn test_default_keybind_loads() {
374        // This test validates the bundled default-keybind.toml file
375        // If this fails, there's a duplicate key or invalid event in the config
376        let keybind = KeyBind::new(None);
377
378        // Verify some essential bindings exist
379        assert!(keybind.values().any(|e| *e == UserEvent::Quit), "quit binding missing");
380        assert!(keybind.values().any(|e| *e == UserEvent::NavigateDown), "navigate_down binding missing");
381        assert!(keybind.values().any(|e| *e == UserEvent::NavigateUp), "navigate_up binding missing");
382        assert!(keybind.values().any(|e| *e == UserEvent::Confirm), "confirm binding missing");
383        assert!(keybind.values().any(|e| *e == UserEvent::Cancel), "cancel binding missing");
384    }
385}