Skip to main content

kimun_notes/keys/
key_combo.rs

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
67// impl TryFrom<KeyboardData> for KeyCombo {
68//     type Error = String;
69
70//     fn try_from(value: KeyboardData) -> Result<Self, Self::Error> {
71//         let key: KeyStrike = value.key().into();
72//         let modifiers: KeyModifiers = value.modifiers().into();
73
74//         if key == KeyStrike::Unknown {
75//             Err(format!("Unknown Key: {}", value.key()))
76//         } else {
77//             Ok(KeyCombo { modifiers, key })
78//         }
79//     }
80// }
81
82// impl From<Rc<KeyboardData>> for KeyCombo {
83//     fn from(value: Rc<KeyboardData>) -> Self {
84//         let key: KeyStrike = value.key().into();
85//         let modifiers: KeyModifiers = value.modifiers().into();
86
87//         if key == KeyStrike::Unknown {
88//             error!("Unknown Key: {}", value.key());
89//             KeyCombo::default()
90//         } else {
91//             KeyCombo { modifiers, key }
92//         }
93//     }
94// }
95
96impl KeyCombo {
97    pub fn new(modifiers: KeyModifiers, key: KeyStrike) -> Self {
98        Self { modifiers, key }
99    }
100
101    /// Returns `true` for combinations accepted in the config file:
102    /// - ctrl/alt (with optional shift) + a letter key (a–z), **or**
103    /// - a bare F-key (F1–F12, no modifier required)
104    pub fn is_valid_binding(&self) -> bool {
105        let is_letter_combo = (self.modifiers.is_ctrl() || self.modifiers.is_alt())
106            && self.key >= KeyStrike::KeyA
107            && self.key <= KeyStrike::KeyZ;
108        let is_fkey = matches!(
109            self.key,
110            KeyStrike::F1
111                | KeyStrike::F2
112                | KeyStrike::F3
113                | KeyStrike::F4
114                | KeyStrike::F5
115                | KeyStrike::F6
116                | KeyStrike::F7
117                | KeyStrike::F8
118                | KeyStrike::F9
119                | KeyStrike::F10
120                | KeyStrike::F11
121                | KeyStrike::F12
122        );
123        is_letter_combo || is_fkey
124    }
125}
126
127/// Pressed modifier keys.
128///
129/// Specification:
130/// <https://w3c.github.io/uievents-key/#keys-modifier>
131#[derive(
132    Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
133)]
134#[serde(try_from = "String", into = "String")]
135pub struct KeyModifiers {
136    alt: bool,
137    ctrl: bool,
138    cmd: bool,
139    shift: bool,
140}
141
142// For compatibility
143const META: &str = "meta";
144const CMD: &str = "cmd";
145
146const ALT: &str = "alt";
147const CONTROL: &str = "ctrl";
148const SHIFT: &str = "shift";
149
150// For compatibility
151#[cfg(target_os = "macos")]
152const META_CMD: &str = CMD;
153#[cfg(not(target_os = "macos"))]
154const META_CMD: &str = META;
155
156impl TryFrom<String> for KeyModifiers {
157    type Error = String;
158
159    fn try_from(value: String) -> Result<Self, Self::Error> {
160        let splits = value.split("+");
161        let mut modifiers = KeyModifiers::default();
162        for modif in splits {
163            match modif {
164                "" => {}
165                CONTROL => modifiers.with_ctrl(),
166                SHIFT => modifiers.with_shift(),
167                ALT => modifiers.with_alt(),
168                META => modifiers.with_meta_cmd(),
169                CMD => modifiers.with_meta_cmd(),
170                _ => return Err(format!("Non valid modifier value: {}", modif)),
171            }
172        }
173        Ok(modifiers)
174    }
175}
176
177impl From<KeyModifiers> for String {
178    fn from(value: KeyModifiers) -> Self {
179        value.to_string()
180    }
181}
182
183// impl From<Modifiers> for KeyModifiers {
184//     fn from(value: Modifiers) -> Self {
185//         let mut km = KeyModifiers::default();
186//         if value.shift() {
187//             km.with_shift();
188//         }
189//         if value.ctrl() {
190//             km.with_ctrl();
191//         }
192//         if value.alt() {
193//             km.with_alt();
194//         }
195//         if value.meta() {
196//             km.with_meta_cmd();
197//         }
198//         km
199//     }
200// }
201
202impl Display for KeyModifiers {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        let mut modifiers = vec![];
205        if self.is_ctrl() {
206            modifiers.push(CONTROL);
207        }
208        if self.is_alt() {
209            modifiers.push(ALT);
210        }
211        if self.is_meta_cmd() {
212            modifiers.push(META_CMD);
213        }
214        if self.is_shift() {
215            modifiers.push(SHIFT);
216        }
217        let string = modifiers.join("+");
218        write!(f, "{}", string)
219    }
220}
221
222impl KeyModifiers {
223    pub fn new() -> Self {
224        KeyModifiers::default()
225    }
226
227    pub fn is_empty(&self) -> bool {
228        !self.alt && !self.ctrl && !self.cmd && !self.shift
229    }
230
231    pub fn with_shift(&mut self) {
232        self.shift = true;
233    }
234    pub fn with_ctrl(&mut self) {
235        self.ctrl = true;
236    }
237    pub fn with_alt(&mut self) {
238        self.alt = true;
239    }
240    pub fn with_meta_cmd(&mut self) {
241        self.cmd = true;
242    }
243
244    pub fn and_shift(mut self) -> Self {
245        self.with_shift();
246        self
247    }
248    pub fn and_ctrl(mut self) -> Self {
249        self.with_ctrl();
250        self
251    }
252    pub fn and_alt(mut self) -> Self {
253        self.with_alt();
254        self
255    }
256    pub fn and_meta_cmd(mut self) -> Self {
257        self.with_meta_cmd();
258        self
259    }
260    /// Return `true` if a shift key is pressed.
261    pub fn is_shift(&self) -> bool {
262        self.shift
263    }
264
265    /// Return `true` if a control key is pressed.
266    pub fn is_ctrl(&self) -> bool {
267        self.ctrl
268    }
269
270    /// Return `true` if an alt key is pressed.
271    pub fn is_alt(&self) -> bool {
272        self.alt
273    }
274
275    /// Return `true` if a meta key is pressed.
276    pub fn is_meta_cmd(&self) -> bool {
277        self.cmd
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use color_eyre::eyre;
284
285    use crate::keys::{key_combo::KeyCombo, key_strike::KeyStrike};
286
287    use super::KeyModifiers;
288
289    #[test]
290    fn serialize_keymodifier() -> eyre::Result<()> {
291        let mut km = KeyModifiers::default();
292        km.with_shift();
293
294        let km_ser = km.to_string();
295        assert_eq!("shift", km_ser);
296
297        km.with_ctrl();
298        let km_ser = km.to_string();
299        assert_eq!("ctrl+shift", km_ser);
300        Ok(())
301    }
302
303    #[test]
304    fn deserialize_keymodifier() -> eyre::Result<()> {
305        let text = "meta+shift";
306        let km = KeyModifiers::try_from(text.to_string());
307
308        assert!(km.is_ok());
309
310        let km = km.unwrap();
311        assert!(km.cmd);
312        assert!(km.shift);
313        assert!(!km.ctrl);
314        assert!(!km.alt);
315
316        Ok(())
317    }
318
319    #[test]
320    fn serialize_keycombo() {
321        let kc = KeyCombo::new(
322            KeyModifiers::new().and_meta_cmd().and_ctrl(),
323            crate::keys::key_strike::KeyStrike::KeyN,
324        );
325
326        let kc_ser = kc.to_string();
327
328        #[cfg(target_os = "macos")]
329        assert_eq!("ctrl+cmd&N", kc_ser);
330        #[cfg(not(target_os = "macos"))]
331        assert_eq!("ctrl+meta&N", kc_ser);
332    }
333
334    #[test]
335    fn deserialize_keycombo_meta() {
336        let string = "shift+meta & H".to_string();
337
338        let kc = KeyCombo::try_from(string).unwrap();
339
340        assert!(kc.modifiers.shift);
341        assert!(kc.modifiers.cmd);
342        assert!(!kc.modifiers.ctrl);
343        assert!(!kc.modifiers.alt);
344        assert_eq!(kc.key, KeyStrike::KeyH);
345    }
346
347    #[test]
348    fn deserialize_keycombo_cmd() {
349        let string = "shift+cmd & H".to_string();
350
351        let kc = KeyCombo::try_from(string).unwrap();
352
353        assert!(kc.modifiers.shift);
354        assert!(kc.modifiers.cmd);
355        assert!(!kc.modifiers.ctrl);
356        assert!(!kc.modifiers.alt);
357        assert_eq!(kc.key, KeyStrike::KeyH);
358    }
359
360    #[test]
361    fn deserialize_keycombo_no_mod() {
362        let string = "L".to_string();
363
364        let kc = KeyCombo::try_from(string).unwrap();
365
366        assert!(!kc.modifiers.shift);
367        assert!(!kc.modifiers.cmd);
368        assert!(!kc.modifiers.ctrl);
369        assert!(!kc.modifiers.alt);
370        assert_eq!(kc.key, KeyStrike::KeyL);
371    }
372
373    #[test]
374    fn roundtrip_keycombo_no_modifier() {
375        // A combo with no modifiers must serialize without " & " prefix
376        // and deserialize back correctly.
377        let kc = KeyCombo::new(KeyModifiers::default(), KeyStrike::Tab);
378        let serialized = kc.to_string();
379        assert_eq!(serialized, "<Tab>");
380
381        let parsed = KeyCombo::try_from(serialized).unwrap();
382        assert_eq!(parsed, kc);
383    }
384
385    #[test]
386    fn deserialize_legacy_no_modifier_with_ampersand() {
387        // Old config files wrote " & <Tab>" for no-modifier Tab — must still parse.
388        let kc = KeyCombo::try_from(" & <Tab>".to_string()).unwrap();
389        assert!(!kc.modifiers.is_ctrl());
390        assert!(!kc.modifiers.is_shift());
391        assert_eq!(kc.key, KeyStrike::Tab);
392    }
393}