Skip to main content

photon_ui/
keybindings.rs

1use std::collections::HashMap;
2
3use thiserror::Error;
4
5use crate::events::Key;
6
7/// Error returned when a keybinding conflicts with an existing binding.
8#[derive(Error, Debug, Clone, PartialEq)]
9pub enum KeybindingError {
10    /// The key is already bound to a different action.
11    #[error("conflict: {0} is already bound to {1}")]
12    Conflict(String, String),
13}
14
15/// Manages a bidirectional mapping between action names and keys.
16///
17/// Actions are human-readable strings like `"editor.cursorLeft"`. The manager
18/// ensures that each key maps to at most one action.
19pub struct KeybindingsManager {
20    bindings: HashMap<String, Key>,
21    reverse: HashMap<Key, String>,
22}
23
24/// Return the framework's default vim-style keybindings.
25///
26/// | Action | Key |
27/// |--------|-----|
28/// | `editor.cursorLeft` | `h` (normal) / Left |
29/// | `editor.cursorRight` | `l` (normal) / Right |
30/// | `editor.cursorUp` | `k` (normal) / Up |
31/// | `editor.cursorDown` | `j` (normal) / Down |
32/// | `editor.wordForward` | `w` (normal) |
33/// | `editor.wordBackward` | `b` (normal) |
34/// | `editor.lineStart` | `0` (normal) / Home |
35/// | `editor.lineEnd` | `$` (normal) / End |
36/// | `editor.deleteChar` | `x` (normal) |
37/// | `editor.undo` | `u` (normal) |
38/// | `editor.redo` | `Ctrl+r` (normal) |
39/// | `editor.yankLine` | `yy` (normal) |
40/// | `editor.paste` | `p` (normal) |
41/// | `editor.insertMode` | `i` (normal) |
42/// | `editor.normalMode` | `Escape` |
43/// | `input.submit` | Enter |
44/// | `input.backspace` | Backspace |
45/// | `input.delete` | Delete |
46/// | `select.next` | `j` / Down |
47/// | `select.prev` | `k` / Up |
48/// | `select.confirm` | Enter |
49/// | `select.cancel` | `q` / Escape |
50pub fn default_bindings() -> Vec<(String, Key)> {
51    vec![
52        ("editor.cursorLeft".into(), Key::left()),
53        ("editor.cursorRight".into(), Key::right()),
54        ("editor.cursorUp".into(), Key::up()),
55        ("editor.cursorDown".into(), Key::down()),
56        ("editor.wordForward".into(), Key::ctrl('f')),
57        ("editor.wordBackward".into(), Key::ctrl('b')),
58        ("editor.lineStart".into(), Key::home()),
59        ("editor.lineEnd".into(), Key::end()),
60        ("editor.deleteChar".into(), Key::delete()),
61        ("editor.undo".into(), Key::ctrl('-')),
62        ("editor.redo".into(), Key::ctrl_shift('z')),
63        ("editor.yankLine".into(), Key::ctrl('y')),
64        ("editor.paste".into(), Key::ctrl('v')),
65        ("editor.insertMode".into(), Key::char('i')),
66        ("editor.normalMode".into(), Key::esc()),
67        ("input.submit".into(), Key::enter()),
68        ("input.backspace".into(), Key::backspace()),
69        ("input.delete".into(), Key::delete()),
70        ("select.next".into(), Key::down()),
71        ("select.prev".into(), Key::up()),
72        ("select.confirm".into(), Key::enter()),
73        ("select.cancel".into(), Key::esc()),
74    ]
75}
76
77impl Default for KeybindingsManager {
78    fn default() -> Self {
79        let mut mgr = Self {
80            bindings: HashMap::new(),
81            reverse: HashMap::new(),
82        };
83        for (action, key) in default_bindings() {
84            mgr.bindings.insert(action.clone(), key.clone());
85            mgr.reverse.insert(key, action);
86        }
87        mgr
88    }
89}
90
91impl KeybindingsManager {
92    /// Create a manager pre-populated with [`default_bindings`].
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Look up the key bound to an action.
98    pub fn get(&self, action: &str) -> Option<&Key> {
99        self.bindings.get(action)
100    }
101
102    /// Bind `action` to `key`, replacing any previous binding for that action.
103    ///
104    /// Returns an error if `key` is already bound to a *different* action.
105    pub fn set(&mut self, action: &str, key: Key) -> Result<(), KeybindingError> {
106        if let Some(existing_action) = self.reverse.get(&key) {
107            if existing_action != action {
108                return Err(KeybindingError::Conflict(
109                    format!("{:?}", key),
110                    existing_action.clone(),
111                ));
112            }
113        }
114        if let Some(old_key) = self.bindings.get(action) {
115            self.reverse.remove(old_key);
116        }
117        self.bindings.insert(action.to_string(), key.clone());
118        self.reverse.insert(key, action.to_string());
119        Ok(())
120    }
121
122    /// Detect any duplicate keys in the binding map.
123    ///
124    /// Returns pairs of `(action, other_action)` that share the same key.
125    pub fn detect_conflicts(&self) -> Vec<(String, String)> {
126        let mut conflicts = Vec::new();
127        let mut seen: HashMap<&Key, &String> = HashMap::new();
128        for (action, key) in &self.bindings {
129            if let Some(other) = seen.get(key) {
130                conflicts.push((action.clone(), other.to_string()));
131            } else {
132                seen.insert(key, action);
133            }
134        }
135        conflicts
136    }
137}