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 (`KeyStrike::is_fkey`, no modifier required)
104    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    /// Ctrl/Alt (optional shift) plus a letter key — the chord shape both
125    /// the binding validator and the editor's footer chord flash test for.
126    /// One predicate so the two sites cannot drift.
127    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/// Pressed modifier keys.
135///
136/// Specification:
137/// <https://w3c.github.io/uievents-key/#keys-modifier>
138#[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
149// For compatibility
150const META: &str = "meta";
151const CMD: &str = "cmd";
152
153const ALT: &str = "alt";
154const CONTROL: &str = "ctrl";
155const SHIFT: &str = "shift";
156
157// For compatibility
158#[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
190// impl From<Modifiers> for KeyModifiers {
191//     fn from(value: Modifiers) -> Self {
192//         let mut km = KeyModifiers::default();
193//         if value.shift() {
194//             km.with_shift();
195//         }
196//         if value.ctrl() {
197//             km.with_ctrl();
198//         }
199//         if value.alt() {
200//             km.with_alt();
201//         }
202//         if value.meta() {
203//             km.with_meta_cmd();
204//         }
205//         km
206//     }
207// }
208
209impl 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    /// Return `true` if a shift key is pressed.
268    pub fn is_shift(&self) -> bool {
269        self.shift
270    }
271
272    /// Return `true` if a control key is pressed.
273    pub fn is_ctrl(&self) -> bool {
274        self.ctrl
275    }
276
277    /// Return `true` if an alt key is pressed.
278    pub fn is_alt(&self) -> bool {
279        self.alt
280    }
281
282    /// Return `true` if a meta key is pressed.
283    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        // A combo with no modifiers must serialize without " & " prefix
383        // and deserialize back correctly.
384        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        // Old config files wrote " & <Tab>" for no-modifier Tab — must still parse.
395        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}