Skip to main content

par_term_keybindings/
lib.rs

1//! Keybinding system for par-term.
2//!
3//! This module provides runtime-configurable keybindings that allow users
4//! to define custom keyboard shortcuts in their config.yaml.
5//!
6//! Features:
7//! - Configurable key combinations (Ctrl+Shift+B, CmdOrCtrl+V, etc.)
8//! - Modifier remapping (swap Ctrl and Super, etc.)
9//! - Physical key support for language-agnostic bindings
10
11mod matcher;
12pub mod parser;
13
14pub use matcher::KeybindingMatcher;
15pub use parser::KeyCombo;
16// ParseError exported for consumers who might want to handle parsing errors
17#[allow(unused_imports)]
18pub use parser::ParseError;
19pub use parser::{key_combo_to_bytes, parse_key_sequence};
20
21use par_term_config::{KeyBinding, ModifierRemapping};
22use std::collections::HashMap;
23
24/// Registry of keybindings mapping key combinations to action names.
25#[derive(Debug, Default)]
26pub struct KeybindingRegistry {
27    /// Map of parsed key combos to action names
28    bindings: HashMap<KeyCombo, String>,
29}
30
31impl KeybindingRegistry {
32    /// Create a new empty registry.
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Build a registry from config keybindings.
38    ///
39    /// Invalid keybinding strings are logged and skipped.
40    pub fn from_config(keybindings: &[KeyBinding]) -> Self {
41        let mut registry = Self::new();
42
43        log::info!(
44            "Building keybinding registry from {} config keybindings",
45            keybindings.len()
46        );
47        for binding in keybindings {
48            match parser::parse_key_combo(&binding.key) {
49                Ok(combo) => {
50                    log::info!(
51                        "Registered keybinding: {} -> {} (parsed as: {:?})",
52                        binding.key,
53                        binding.action,
54                        combo
55                    );
56                    registry.bindings.insert(combo, binding.action.clone());
57                }
58                Err(e) => {
59                    log::warn!(
60                        "Invalid keybinding '{}' for action '{}': {}",
61                        binding.key,
62                        binding.action,
63                        e
64                    );
65                }
66            }
67        }
68
69        log::info!(
70            "Keybinding registry initialized with {} bindings",
71            registry.bindings.len()
72        );
73        registry
74    }
75
76    /// Look up an action for a key event.
77    ///
78    /// Returns the action name if a matching keybinding is found.
79    pub fn lookup(
80        &self,
81        event: &winit::event::KeyEvent,
82        modifiers: &winit::event::Modifiers,
83    ) -> Option<&str> {
84        self.lookup_with_options(event, modifiers, &ModifierRemapping::default(), false)
85    }
86
87    /// Look up an action for a key event with advanced options.
88    ///
89    /// # Arguments
90    /// * `event` - The key event from winit
91    /// * `modifiers` - Current modifier state
92    /// * `remapping` - Modifier key remapping configuration
93    /// * `use_physical_keys` - If true, match by physical key position (scan code) for
94    ///   language-agnostic bindings. This makes keybindings consistent across keyboard layouts.
95    ///
96    /// Returns the action name if a matching keybinding is found.
97    pub fn lookup_with_options(
98        &self,
99        event: &winit::event::KeyEvent,
100        modifiers: &winit::event::Modifiers,
101        remapping: &ModifierRemapping,
102        use_physical_keys: bool,
103    ) -> Option<&str> {
104        let matcher = KeybindingMatcher::from_event_with_remapping(event, modifiers, remapping);
105
106        for (combo, action) in &self.bindings {
107            if matcher.matches_with_physical_preference(combo, use_physical_keys) {
108                return Some(action.as_str());
109            }
110        }
111
112        None
113    }
114
115    /// Check if the registry has any bindings.
116    #[allow(dead_code)]
117    pub fn is_empty(&self) -> bool {
118        self.bindings.is_empty()
119    }
120
121    /// Get the number of registered bindings.
122    #[allow(dead_code)]
123    pub fn len(&self) -> usize {
124        self.bindings.len()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_empty_registry() {
134        let registry = KeybindingRegistry::new();
135        assert!(registry.is_empty());
136        assert_eq!(registry.len(), 0);
137    }
138
139    #[test]
140    fn test_from_config() {
141        let bindings = vec![
142            KeyBinding {
143                key: "Ctrl+Shift+B".to_string(),
144                action: "toggle_background_shader".to_string(),
145            },
146            KeyBinding {
147                key: "Ctrl+Shift+U".to_string(),
148                action: "toggle_cursor_shader".to_string(),
149            },
150        ];
151
152        let registry = KeybindingRegistry::from_config(&bindings);
153        assert_eq!(registry.len(), 2);
154    }
155
156    #[test]
157    fn test_invalid_keybinding_skipped() {
158        let bindings = vec![
159            KeyBinding {
160                key: "InvalidKey".to_string(),
161                action: "some_action".to_string(),
162            },
163            KeyBinding {
164                key: "Ctrl+A".to_string(),
165                action: "valid_action".to_string(),
166            },
167        ];
168
169        let registry = KeybindingRegistry::from_config(&bindings);
170        // Only valid bindings should be registered
171        assert_eq!(registry.len(), 1);
172    }
173}