keyboard_codes/parser/
shortcut.rs

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