1use std::{fmt::Display, hash::Hash};
2
3use serde::{Deserialize, Serialize};
4
5use super::key_strike::KeyStrike;
6
7#[derive(
8 Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
9)]
10#[serde(try_from = "String", into = "String")]
11pub struct KeyCombo {
12 pub modifiers: KeyModifiers,
13 pub key: KeyStrike,
14}
15
16impl Display for KeyCombo {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 let modif = self.modifiers.to_string();
19 let key = self.key.to_string();
20 if modif.is_empty() {
21 write!(f, "{}", key)
22 } else {
23 write!(f, "{}&{}", modif, key)
24 }
25 }
26}
27
28impl TryFrom<String> for KeyCombo {
29 type Error = String;
30
31 fn try_from(value: String) -> Result<Self, Self::Error> {
32 let splits = value.split("&").collect::<Vec<_>>();
33 match splits.len() {
34 0 => Err("No Keys found here".to_string()),
35 1 => match KeyStrike::try_from(splits.first().unwrap().trim().to_string()) {
36 Ok(ks) => Ok(KeyCombo {
37 modifiers: KeyModifiers::default(),
38 key: ks,
39 }),
40 Err(e) => Err(e),
41 },
42 2 => {
43 let m = splits.first().unwrap().trim().to_string();
44 let k = splits.last().unwrap().trim().to_string();
45
46 match (KeyModifiers::try_from(m), KeyStrike::try_from(k)) {
47 (Ok(modifiers), Ok(key)) => Ok(KeyCombo { modifiers, key }),
48 (Ok(_), Err(e)) => Err(e),
49 (Err(e), Ok(_)) => Err(e),
50 (Err(em), Err(ek)) => Err(format!("{} - {}", em, ek)),
51 }
52 }
53 _ => Err(format!(
54 "This is a non valid combination, only one key and a modifier combination is allowed: {}",
55 value
56 )),
57 }
58 }
59}
60
61impl From<KeyCombo> for String {
62 fn from(value: KeyCombo) -> Self {
63 value.to_string()
64 }
65}
66
67impl KeyCombo {
97 pub fn new(modifiers: KeyModifiers, key: KeyStrike) -> Self {
98 Self { modifiers, key }
99 }
100
101 pub fn is_valid_binding(&self) -> bool {
105 let is_symbol_combo = (self.modifiers.is_ctrl() || self.modifiers.is_alt())
106 && (self.key >= KeyStrike::Digit0 && self.key <= KeyStrike::Digit9
107 || matches!(
108 self.key,
109 KeyStrike::Comma
110 | KeyStrike::Period
111 | KeyStrike::Slash
112 | KeyStrike::Semicolon
113 | KeyStrike::Quote
114 | KeyStrike::BracketLeft
115 | KeyStrike::BracketRight
116 | KeyStrike::Backslash
117 | KeyStrike::Backquote
118 | KeyStrike::Minus
119 | KeyStrike::Equal
120 ));
121 self.is_letter_chord() || is_symbol_combo || self.key.is_fkey()
122 }
123
124 pub fn is_letter_chord(&self) -> bool {
128 (self.modifiers.is_ctrl() || self.modifiers.is_alt())
129 && self.key >= KeyStrike::KeyA
130 && self.key <= KeyStrike::KeyZ
131 }
132}
133
134#[derive(
139 Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
140)]
141#[serde(try_from = "String", into = "String")]
142pub struct KeyModifiers {
143 alt: bool,
144 ctrl: bool,
145 cmd: bool,
146 shift: bool,
147}
148
149const META: &str = "meta";
151const CMD: &str = "cmd";
152
153const ALT: &str = "alt";
154const CONTROL: &str = "ctrl";
155const SHIFT: &str = "shift";
156
157#[cfg(target_os = "macos")]
159const META_CMD: &str = CMD;
160#[cfg(not(target_os = "macos"))]
161const META_CMD: &str = META;
162
163impl TryFrom<String> for KeyModifiers {
164 type Error = String;
165
166 fn try_from(value: String) -> Result<Self, Self::Error> {
167 let splits = value.split("+");
168 let mut modifiers = KeyModifiers::default();
169 for modif in splits {
170 match modif {
171 "" => {}
172 CONTROL => modifiers.with_ctrl(),
173 SHIFT => modifiers.with_shift(),
174 ALT => modifiers.with_alt(),
175 META => modifiers.with_meta_cmd(),
176 CMD => modifiers.with_meta_cmd(),
177 _ => return Err(format!("Non valid modifier value: {}", modif)),
178 }
179 }
180 Ok(modifiers)
181 }
182}
183
184impl From<KeyModifiers> for String {
185 fn from(value: KeyModifiers) -> Self {
186 value.to_string()
187 }
188}
189
190impl Display for KeyModifiers {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 let mut modifiers = vec![];
212 if self.is_ctrl() {
213 modifiers.push(CONTROL);
214 }
215 if self.is_alt() {
216 modifiers.push(ALT);
217 }
218 if self.is_meta_cmd() {
219 modifiers.push(META_CMD);
220 }
221 if self.is_shift() {
222 modifiers.push(SHIFT);
223 }
224 let string = modifiers.join("+");
225 write!(f, "{}", string)
226 }
227}
228
229impl KeyModifiers {
230 pub fn new() -> Self {
231 KeyModifiers::default()
232 }
233
234 pub fn is_empty(&self) -> bool {
235 !self.alt && !self.ctrl && !self.cmd && !self.shift
236 }
237
238 pub fn with_shift(&mut self) {
239 self.shift = true;
240 }
241 pub fn with_ctrl(&mut self) {
242 self.ctrl = true;
243 }
244 pub fn with_alt(&mut self) {
245 self.alt = true;
246 }
247 pub fn with_meta_cmd(&mut self) {
248 self.cmd = true;
249 }
250
251 pub fn and_shift(mut self) -> Self {
252 self.with_shift();
253 self
254 }
255 pub fn and_ctrl(mut self) -> Self {
256 self.with_ctrl();
257 self
258 }
259 pub fn and_alt(mut self) -> Self {
260 self.with_alt();
261 self
262 }
263 pub fn and_meta_cmd(mut self) -> Self {
264 self.with_meta_cmd();
265 self
266 }
267 pub fn is_shift(&self) -> bool {
269 self.shift
270 }
271
272 pub fn is_ctrl(&self) -> bool {
274 self.ctrl
275 }
276
277 pub fn is_alt(&self) -> bool {
279 self.alt
280 }
281
282 pub fn is_meta_cmd(&self) -> bool {
284 self.cmd
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use color_eyre::eyre;
291
292 use crate::keys::{key_combo::KeyCombo, key_strike::KeyStrike};
293
294 use super::KeyModifiers;
295
296 #[test]
297 fn serialize_keymodifier() -> eyre::Result<()> {
298 let mut km = KeyModifiers::default();
299 km.with_shift();
300
301 let km_ser = km.to_string();
302 assert_eq!("shift", km_ser);
303
304 km.with_ctrl();
305 let km_ser = km.to_string();
306 assert_eq!("ctrl+shift", km_ser);
307 Ok(())
308 }
309
310 #[test]
311 fn deserialize_keymodifier() -> eyre::Result<()> {
312 let text = "meta+shift";
313 let km = KeyModifiers::try_from(text.to_string());
314
315 assert!(km.is_ok());
316
317 let km = km.unwrap();
318 assert!(km.cmd);
319 assert!(km.shift);
320 assert!(!km.ctrl);
321 assert!(!km.alt);
322
323 Ok(())
324 }
325
326 #[test]
327 fn serialize_keycombo() {
328 let kc = KeyCombo::new(
329 KeyModifiers::new().and_meta_cmd().and_ctrl(),
330 crate::keys::key_strike::KeyStrike::KeyN,
331 );
332
333 let kc_ser = kc.to_string();
334
335 #[cfg(target_os = "macos")]
336 assert_eq!("ctrl+cmd&N", kc_ser);
337 #[cfg(not(target_os = "macos"))]
338 assert_eq!("ctrl+meta&N", kc_ser);
339 }
340
341 #[test]
342 fn deserialize_keycombo_meta() {
343 let string = "shift+meta & H".to_string();
344
345 let kc = KeyCombo::try_from(string).unwrap();
346
347 assert!(kc.modifiers.shift);
348 assert!(kc.modifiers.cmd);
349 assert!(!kc.modifiers.ctrl);
350 assert!(!kc.modifiers.alt);
351 assert_eq!(kc.key, KeyStrike::KeyH);
352 }
353
354 #[test]
355 fn deserialize_keycombo_cmd() {
356 let string = "shift+cmd & H".to_string();
357
358 let kc = KeyCombo::try_from(string).unwrap();
359
360 assert!(kc.modifiers.shift);
361 assert!(kc.modifiers.cmd);
362 assert!(!kc.modifiers.ctrl);
363 assert!(!kc.modifiers.alt);
364 assert_eq!(kc.key, KeyStrike::KeyH);
365 }
366
367 #[test]
368 fn deserialize_keycombo_no_mod() {
369 let string = "L".to_string();
370
371 let kc = KeyCombo::try_from(string).unwrap();
372
373 assert!(!kc.modifiers.shift);
374 assert!(!kc.modifiers.cmd);
375 assert!(!kc.modifiers.ctrl);
376 assert!(!kc.modifiers.alt);
377 assert_eq!(kc.key, KeyStrike::KeyL);
378 }
379
380 #[test]
381 fn roundtrip_keycombo_no_modifier() {
382 let kc = KeyCombo::new(KeyModifiers::default(), KeyStrike::Tab);
385 let serialized = kc.to_string();
386 assert_eq!(serialized, "<Tab>");
387
388 let parsed = KeyCombo::try_from(serialized).unwrap();
389 assert_eq!(parsed, kc);
390 }
391
392 #[test]
393 fn deserialize_legacy_no_modifier_with_ampersand() {
394 let kc = KeyCombo::try_from(" & <Tab>".to_string()).unwrap();
396 assert!(!kc.modifiers.is_ctrl());
397 assert!(!kc.modifiers.is_shift());
398 assert_eq!(kc.key, KeyStrike::Tab);
399 }
400}