keyboard_codes/parser/
shortcut.rs

1use crate::error::KeyParseError;
2use crate::{Key, Modifier};
3
4/// Represents a parsed keyboard shortcut
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Shortcut {
7    /// Modifier keys in the shortcut
8    pub modifiers: Vec<Modifier>,
9    /// The main key in the shortcut
10    pub key: Key,
11}
12
13impl Shortcut {
14    /// Create a new shortcut
15    pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
16        Self { modifiers, key }
17    }
18
19    /// Check if the shortcut has no modifiers
20    pub fn is_simple(&self) -> bool {
21        self.modifiers.is_empty()
22    }
23
24    /// Convert to string representation
25    ///
26    /// This returns the string representation of the shortcut in the format
27    /// "Modifier1+Modifier2+Key" or just "Key" if there are no modifiers.
28    pub fn as_string(&self) -> String {
29        if self.modifiers.is_empty() {
30            self.key.to_string()
31        } else {
32            let mods: Vec<String> = self.modifiers.iter().map(|m| m.to_string()).collect();
33            format!("{}+{}", mods.join("+"), self.key)
34        }
35    }
36}
37
38impl std::fmt::Display for Shortcut {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "{}", self.as_string())
41    }
42}
43
44/// Parse a shortcut combination with alias and case-insensitive support
45/// Format: [modifier1+][modifier2+]...+key (e.g., "ctrl+shift+a", "win+enter")
46pub fn parse_shortcut_with_aliases(shortcut: &str) -> Result<Shortcut, KeyParseError> {
47    if shortcut.is_empty() {
48        return Err(KeyParseError::InvalidShortcutFormat(
49            "empty shortcut".to_string(),
50        ));
51    }
52
53    let parts: Vec<&str> = shortcut.split('+').map(|s| s.trim()).collect();
54
55    // Validate format: at least one modifier and one key (length >= 2)
56    if parts.len() < 2 {
57        return Err(KeyParseError::InvalidShortcutFormat(format!(
58            "shortcut must contain at least one modifier and one key: {}",
59            shortcut
60        )));
61    }
62
63    // Parse key part (last element)
64    let key_part = parts
65        .last()
66        .cloned()
67        .ok_or_else(|| KeyParseError::InvalidShortcutFormat(shortcut.to_string()))?;
68
69    // Use alias-aware key parsing
70    let normalized_key = super::normalize_key_name(key_part);
71    let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
72
73    // Parse modifier parts (all elements except the last)
74    let modifiers: Vec<Modifier> = parts[0..parts.len() - 1]
75        .iter()
76        .map(|&m| super::parse_modifier_with_aliases(m))
77        .collect::<Result<_, _>>()?;
78
79    Ok(Shortcut::new(modifiers, key))
80}
81
82/// Parse shortcut with flexible separator support (supports '+', '-', and space)
83pub fn parse_shortcut_flexible(shortcut: &str) -> Result<Shortcut, KeyParseError> {
84    if shortcut.is_empty() {
85        return Err(KeyParseError::InvalidShortcutFormat(
86            "empty shortcut".to_string(),
87        ));
88    }
89
90    // Replace common separators with '+' for consistent parsing
91    let normalized = shortcut.replace(['-', ' '], "+");
92    parse_shortcut_with_aliases(&normalized)
93}
94
95/// Parse single key or shortcut (auto-detection)
96pub fn parse_input(input: &str) -> Result<Shortcut, KeyParseError> {
97    if input.is_empty() {
98        return Err(KeyParseError::InvalidShortcutFormat(
99            "empty input".to_string(),
100        ));
101    }
102
103    // If contains separators, parse as shortcut
104    if input.contains('+') || input.contains('-') || input.contains(' ') {
105        parse_shortcut_flexible(input)
106    } else {
107        // Otherwise parse as single key
108        let normalized_key = super::normalize_key_name(input);
109        let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
110        Ok(Shortcut::new(Vec::new(), key))
111    }
112}
113
114/// Parse multiple shortcuts separated by commas
115pub fn parse_shortcut_sequence(sequence: &str) -> Result<Vec<Shortcut>, KeyParseError> {
116    sequence
117        .split(',')
118        .map(|s| s.trim())
119        .filter(|s| !s.is_empty())
120        .map(parse_shortcut_flexible)
121        .collect()
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_parse_shortcut_with_aliases() {
130        let shortcut = parse_shortcut_with_aliases("ctrl+shift+a").unwrap();
131        assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Shift]);
132        assert_eq!(shortcut.key, Key::A);
133
134        let shortcut = parse_shortcut_with_aliases("cmd+q").unwrap();
135        assert_eq!(shortcut.modifiers, vec![Modifier::Meta]);
136        assert_eq!(shortcut.key, Key::Q);
137
138        let shortcut = parse_shortcut_with_aliases("ctrl+alt+del").unwrap();
139        assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Alt]);
140        assert_eq!(shortcut.key, Key::Delete);
141    }
142
143    #[test]
144    fn test_parse_shortcut_flexible() {
145        let shortcut = parse_shortcut_flexible("ctrl-shift-a").unwrap();
146        assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Shift]);
147        assert_eq!(shortcut.key, Key::A);
148
149        let shortcut = parse_shortcut_flexible("cmd alt delete").unwrap();
150        assert_eq!(shortcut.modifiers, vec![Modifier::Meta, Modifier::Alt]);
151        assert_eq!(shortcut.key, Key::Delete);
152    }
153
154    #[test]
155    fn test_parse_input() {
156        // Single key
157        let shortcut = parse_input("a").unwrap();
158        assert!(shortcut.is_simple());
159        assert_eq!(shortcut.key, Key::A);
160
161        // Single key with alias
162        let shortcut = parse_input("esc").unwrap();
163        assert!(shortcut.is_simple());
164        assert_eq!(shortcut.key, Key::Escape);
165
166        // Shortcut
167        let shortcut = parse_input("ctrl+a").unwrap();
168        assert_eq!(shortcut.modifiers, vec![Modifier::Control]);
169        assert_eq!(shortcut.key, Key::A);
170    }
171
172    #[test]
173    fn test_parse_shortcut_sequence() {
174        let shortcuts = parse_shortcut_sequence("ctrl+a, cmd+q, shift-enter").unwrap();
175        assert_eq!(shortcuts.len(), 3);
176
177        assert_eq!(shortcuts[0].modifiers, vec![Modifier::Control]);
178        assert_eq!(shortcuts[0].key, Key::A);
179
180        assert_eq!(shortcuts[1].modifiers, vec![Modifier::Meta]);
181        assert_eq!(shortcuts[1].key, Key::Q);
182
183        assert_eq!(shortcuts[2].modifiers, vec![Modifier::Shift]);
184        assert_eq!(shortcuts[2].key, Key::Enter);
185    }
186
187    #[test]
188    fn test_shortcut_display() {
189        let shortcut = Shortcut::new(vec![Modifier::Control, Modifier::Shift], Key::A);
190        assert_eq!(shortcut.to_string(), "Control+Shift+A");
191
192        let simple = Shortcut::new(Vec::new(), Key::Enter);
193        assert_eq!(simple.to_string(), "Enter");
194    }
195
196    #[test]
197    fn test_shortcut_as_string() {
198        let shortcut = Shortcut::new(vec![Modifier::Control, Modifier::Shift], Key::A);
199        assert_eq!(shortcut.as_string(), "Control+Shift+A");
200    }
201}