keyboard_codes/parser/
shortcut.rs1use crate::error::KeyParseError;
2use crate::{Key, Modifier};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Shortcut {
7 pub modifiers: Vec<Modifier>,
9 pub key: Key,
11}
12
13impl Shortcut {
14 pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
16 Self { modifiers, key }
17 }
18
19 pub fn is_simple(&self) -> bool {
21 self.modifiers.is_empty()
22 }
23
24 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
44pub 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 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 let key_part = parts
65 .last()
66 .cloned()
67 .ok_or_else(|| KeyParseError::InvalidShortcutFormat(shortcut.to_string()))?;
68
69 let normalized_key = super::normalize_key_name(key_part);
71 let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
72
73 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
82pub 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 let normalized = shortcut.replace(['-', ' '], "+");
92 parse_shortcut_with_aliases(&normalized)
93}
94
95pub 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 input.contains('+') || input.contains('-') || input.contains(' ') {
105 parse_shortcut_flexible(input)
106 } else {
107 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
114pub 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 let shortcut = parse_input("a").unwrap();
158 assert!(shortcut.is_simple());
159 assert_eq!(shortcut.key, Key::A);
160
161 let shortcut = parse_input("esc").unwrap();
163 assert!(shortcut.is_simple());
164 assert_eq!(shortcut.key, Key::Escape);
165
166 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}