keyboard_codes/parser/
shortcut.rs1use crate::error::KeyParseError;
2use crate::{Key, Modifier};
3use std::hash::{Hash, Hasher};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Shortcut {
8 pub modifiers: Vec<Modifier>,
10 pub key: Key,
12}
13
14impl Shortcut {
15 pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
17 Self { modifiers, key }
18 }
19
20 pub fn is_simple(&self) -> bool {
22 self.modifiers.is_empty()
23 }
24
25 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
45impl Hash for Shortcut {
47 fn hash<H: Hasher>(&self, state: &mut H) {
48 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
56pub 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 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 let key_part = parts
77 .last()
78 .cloned()
79 .ok_or_else(|| KeyParseError::InvalidShortcutFormat(shortcut.to_string()))?;
80
81 let normalized_key = super::normalize_key_name(key_part);
83 let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
84
85 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
94pub 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 let normalized = shortcut.replace(['-', ' '], "+");
104 parse_shortcut_with_aliases(&normalized)
105}
106
107pub 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 input.contains('+') || input.contains('-') || input.contains(' ') {
117 parse_shortcut_flexible(input)
118 } else {
119 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
126pub 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 let shortcut = parse_input("a").unwrap();
170 assert!(shortcut.is_simple());
171 assert_eq!(shortcut.key, Key::A);
172
173 let shortcut = parse_input("esc").unwrap();
175 assert!(shortcut.is_simple());
176 assert_eq!(shortcut.key, Key::Escape);
177
178 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}