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