kas_core/config/
shortcuts.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Shortcut matching
7
8use crate::event::{Command, Key, ModifiersState};
9use linear_map::LinearMap;
10use std::collections::HashMap;
11use winit::keyboard::NamedKey;
12
13/// Shortcut manager
14#[allow(clippy::derive_partial_eq_without_eq)]
15#[derive(Clone, Debug, PartialEq)]
16pub struct Shortcuts {
17    // NOTE: we do not permit Key::Dead(None) here
18    map: LinearMap<ModifiersState, HashMap<Key, Command>>,
19}
20
21impl Shortcuts {
22    /// Construct, with no bindings
23    #[inline]
24    pub fn empty() -> Self {
25        Shortcuts {
26            map: Default::default(),
27        }
28    }
29
30    /// Construct, with default bindings
31    #[inline]
32    pub fn platform_defaults() -> Self {
33        let mut s = Self::empty();
34        s.load_platform_defaults();
35        s
36    }
37
38    /// Load default shortcuts for the current platform
39    pub fn load_platform_defaults(&mut self) {
40        #[cfg(target_os = "macos")]
41        const CMD: ModifiersState = ModifiersState::SUPER;
42        #[cfg(not(target_os = "macos"))]
43        const CMD: ModifiersState = ModifiersState::CONTROL;
44
45        // No modifiers
46        #[cfg(not(target_os = "macos"))]
47        {
48            let modifiers = ModifiersState::empty();
49            let map = self.map.entry(modifiers).or_insert_with(Default::default);
50            let shortcuts = [
51                (NamedKey::F1.into(), Command::Help),
52                (NamedKey::F2.into(), Command::Rename),
53                (NamedKey::F3.into(), Command::FindNext),
54                (NamedKey::F5.into(), Command::Refresh),
55                (NamedKey::F7.into(), Command::SpellCheck),
56                (NamedKey::F8.into(), Command::Debug),
57                (NamedKey::F10.into(), Command::Menu),
58                (NamedKey::F11.into(), Command::Fullscreen),
59            ];
60            map.extend(shortcuts.iter().cloned());
61        }
62
63        // Shift
64        #[cfg(not(target_os = "macos"))]
65        {
66            let modifiers = ModifiersState::SHIFT;
67            let map = self.map.entry(modifiers).or_insert_with(Default::default);
68            map.insert(NamedKey::F3.into(), Command::FindPrevious);
69        }
70
71        // Alt (Option on MacOS)
72        let modifiers = ModifiersState::ALT;
73        let map = self.map.entry(modifiers).or_insert_with(Default::default);
74        #[cfg(not(target_os = "macos"))]
75        {
76            let shortcuts = [
77                (NamedKey::F4.into(), Command::Close),
78                (NamedKey::ArrowLeft.into(), Command::NavPrevious),
79                (NamedKey::ArrowRight.into(), Command::NavNext),
80                (NamedKey::ArrowUp.into(), Command::NavParent),
81                (NamedKey::ArrowDown.into(), Command::NavDown),
82            ];
83            map.extend(shortcuts.iter().cloned());
84        }
85        #[cfg(target_os = "macos")]
86        {
87            // Missing functionality: move to start/end of paragraph on (Shift)+Alt+Up/Down
88            let shortcuts = [
89                (NamedKey::ArrowLeft.into(), Command::WordLeft),
90                (NamedKey::ArrowRight.into(), Command::WordRight),
91            ];
92
93            map.insert(NamedKey::Delete.into(), Command::DelWordBack);
94            map.extend(shortcuts.iter().cloned());
95
96            // Shift + Option
97            let modifiers = ModifiersState::SHIFT | ModifiersState::ALT;
98            let map = self.map.entry(modifiers).or_insert_with(Default::default);
99            map.extend(shortcuts.iter().cloned());
100        }
101
102        // Command (MacOS) or Ctrl (other OS)
103        let map = self.map.entry(CMD).or_insert_with(Default::default);
104        let shortcuts = [
105            (Key::Character("a".into()), Command::SelectAll),
106            (Key::Character("b".into()), Command::Bold),
107            (Key::Character("c".into()), Command::Copy),
108            (Key::Character("f".into()), Command::Find),
109            (Key::Character("i".into()), Command::Italic),
110            (Key::Character("k".into()), Command::Link),
111            (Key::Character("n".into()), Command::New),
112            (Key::Character("o".into()), Command::Open),
113            (Key::Character("p".into()), Command::Print),
114            (Key::Character("s".into()), Command::Save),
115            (Key::Character("t".into()), Command::TabNew),
116            (Key::Character("u".into()), Command::Underline),
117            (Key::Character("v".into()), Command::Paste),
118            (Key::Character("w".into()), Command::Close),
119            (Key::Character("x".into()), Command::Cut),
120            (Key::Character("z".into()), Command::Undo),
121            (NamedKey::Tab.into(), Command::TabNext),
122        ];
123        map.extend(shortcuts.iter().cloned());
124        #[cfg(target_os = "macos")]
125        {
126            let shortcuts = [
127                (Key::Character("g".into()), Command::FindNext),
128                (NamedKey::ArrowUp.into(), Command::DocHome),
129                (NamedKey::ArrowDown.into(), Command::DocEnd),
130                (NamedKey::ArrowLeft.into(), Command::Home),
131                (NamedKey::ArrowRight.into(), Command::End),
132            ];
133            map.extend(shortcuts.iter().cloned());
134        }
135        #[cfg(not(target_os = "macos"))]
136        {
137            let shortcuts = [
138                (Key::Character("q".into()), Command::Exit),
139                (Key::Character("r".into()), Command::FindReplace),
140            ];
141            map.extend(shortcuts.iter().cloned());
142
143            let shortcuts = [
144                (NamedKey::ArrowUp.into(), Command::ViewUp),
145                (NamedKey::ArrowDown.into(), Command::ViewDown),
146                (NamedKey::ArrowLeft.into(), Command::WordLeft),
147                (NamedKey::ArrowRight.into(), Command::WordRight),
148                (NamedKey::Backspace.into(), Command::DelWordBack),
149                (NamedKey::Delete.into(), Command::DelWord),
150                (NamedKey::Home.into(), Command::DocHome),
151                (NamedKey::End.into(), Command::DocEnd),
152                (NamedKey::PageUp.into(), Command::TabPrevious),
153                (NamedKey::PageDown.into(), Command::TabNext),
154            ];
155            map.extend(shortcuts.iter().cloned());
156
157            // Shift + Ctrl
158            let modifiers = ModifiersState::SHIFT | CMD;
159            let map = self.map.entry(modifiers).or_insert_with(Default::default);
160            map.extend(shortcuts.iter().cloned());
161        }
162
163        // Ctrl + Command (MacOS)
164        #[cfg(target_os = "macos")]
165        {
166            let modifiers = ModifiersState::CONTROL | ModifiersState::SUPER;
167            let map = self.map.entry(modifiers).or_insert_with(Default::default);
168            map.insert(Key::Character("f".into()), Command::Fullscreen);
169        }
170
171        // Shift + Ctrl/Command
172        let modifiers = ModifiersState::SHIFT | CMD;
173        let map = self.map.entry(modifiers).or_insert_with(Default::default);
174        let shortcuts = [
175            (Key::Character("a".into()), Command::Deselect),
176            (Key::Character("z".into()), Command::Redo),
177            (NamedKey::Tab.into(), Command::TabPrevious),
178        ];
179        map.extend(shortcuts.iter().cloned());
180        #[cfg(target_os = "macos")]
181        {
182            let shortcuts = [
183                (Key::Character("g".into()), Command::FindPrevious),
184                (Key::Character(":".into()), Command::SpellCheck),
185                (NamedKey::ArrowUp.into(), Command::DocHome),
186                (NamedKey::ArrowDown.into(), Command::DocEnd),
187                (NamedKey::ArrowLeft.into(), Command::Home),
188                (NamedKey::ArrowRight.into(), Command::End),
189            ];
190            map.extend(shortcuts.iter().cloned());
191        }
192
193        // Alt + Command (MacOS)
194        #[cfg(target_os = "macos")]
195        {
196            let modifiers = ModifiersState::ALT | CMD;
197            let map = self.map.entry(modifiers).or_insert_with(Default::default);
198            map.insert(Key::Character("w".into()), Command::Exit);
199        }
200    }
201
202    /// Match shortcuts
203    ///
204    /// Note: text-editor navigation keys (e.g. arrows, home/end) result in the
205    /// same output with and without Shift pressed. Editors should check the
206    /// status of the Shift modifier directly where this has an affect.
207    pub fn try_match(&self, mut modifiers: ModifiersState, key: &Key) -> Option<Command> {
208        if let Some(result) = self.map.get(&modifiers).and_then(|m| m.get(key)) {
209            return Some(*result);
210        }
211        modifiers.remove(ModifiersState::SHIFT);
212        if modifiers.is_empty() {
213            // These keys get matched with and without Shift:
214            return Command::new(key);
215        }
216        None
217    }
218}
219
220#[cfg(feature = "serde")]
221mod common {
222    use super::{Command, Key, ModifiersState, NamedKey};
223    use serde::de::{self, Deserializer, Visitor};
224    use serde::ser::Serializer;
225    use serde::{Deserialize, Serialize};
226    use std::fmt;
227    use winit::keyboard::{NativeKey, SmolStr};
228
229    /// A subset of [`Key`] which serialises to a simple value usable as a map key
230    #[derive(Deserialize)]
231    #[serde(untagged)]
232    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
233    pub(super) enum SimpleKey {
234        Named(NamedKey),
235        Char(char),
236    }
237
238    impl From<SimpleKey> for Key<SmolStr> {
239        fn from(sk: SimpleKey) -> Self {
240            match sk {
241                SimpleKey::Named(key) => Key::Named(key),
242                SimpleKey::Char(c) => {
243                    let mut buf = [0; 4];
244                    let s = c.encode_utf8(&mut buf);
245                    Key::Character(SmolStr::new(s))
246                }
247            }
248        }
249    }
250
251    // NOTE: the only reason we don't use derive is that TOML does not support char as a map key,
252    // thus we must convrt with char::encode_utf8. See toml-lang/toml#1001
253    impl Serialize for SimpleKey {
254        fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
255            match self {
256                SimpleKey::Named(key) => key.serialize(s),
257                SimpleKey::Char(c) => {
258                    let mut buf = [0; 4];
259                    let cs = c.encode_utf8(&mut buf);
260                    s.serialize_str(cs)
261                }
262            }
263        }
264    }
265
266    /// A subset of [`Key`], excluding anything which is a [`SimpleKey`]
267    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
268    pub(super) enum ComplexKey<Str> {
269        Character(Str),
270        Dead(char),
271        #[serde(untagged)]
272        Unidentified(NativeKey),
273    }
274
275    impl From<ComplexKey<SmolStr>> for Key<SmolStr> {
276        fn from(ck: ComplexKey<SmolStr>) -> Self {
277            match ck {
278                ComplexKey::Character(c) => Key::Character(c),
279                ComplexKey::Dead(c) => Key::Dead(Some(c)),
280                ComplexKey::Unidentified(code) => Key::Unidentified(code),
281            }
282        }
283    }
284
285    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
286    pub(super) struct ModifiersStateDeser(pub ModifiersState);
287
288    impl Serialize for ModifiersStateDeser {
289        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
290            const SHIFT: ModifiersState = ModifiersState::SHIFT;
291            const CONTROL: ModifiersState = ModifiersState::CONTROL;
292            const ALT: ModifiersState = ModifiersState::ALT;
293            const SUPER: ModifiersState = ModifiersState::SUPER;
294
295            let s = match self.0 {
296                state if state == ModifiersState::empty() => "none",
297                SUPER => "super",
298                ALT => "alt",
299                state if state == ALT | SUPER => "alt-super",
300                state if state == CONTROL => "ctrl",
301                state if state == CONTROL | SUPER => "ctrl-super",
302                state if state == CONTROL | ALT => "ctrl-alt",
303                state if state == CONTROL | ALT | SUPER => "ctrl-alt-super",
304                SHIFT => "shift",
305                state if state == SHIFT | SUPER => "shift-super",
306                state if state == SHIFT | ALT => "alt-shift",
307                state if state == SHIFT | ALT | SUPER => "alt-shift-super",
308                state if state == SHIFT | CONTROL => "ctrl-shift",
309                state if state == SHIFT | CONTROL | SUPER => "ctrl-shift-super",
310                state if state == SHIFT | CONTROL | ALT => "ctrl-alt-shift",
311                _ => "ctrl-alt-shift-super",
312            };
313
314            serializer.serialize_str(s)
315        }
316    }
317
318    impl<'de> Visitor<'de> for ModifiersStateDeser {
319        type Value = ModifiersStateDeser;
320
321        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
322            formatter.write_str("none or (sub-set of) ctrl-alt-shift-super")
323        }
324
325        fn visit_str<E: de::Error>(self, u: &str) -> Result<Self::Value, E> {
326            let mut v = u;
327            let mut state = ModifiersState::empty();
328
329            let adv_dash_if_not_empty = |v: &mut &str| {
330                if !v.is_empty() {
331                    if v.starts_with('-') {
332                        *v = &v[1..];
333                    }
334                }
335            };
336
337            if v.starts_with("ctrl") {
338                state |= ModifiersState::CONTROL;
339                v = &v[v.len().min(4)..];
340                adv_dash_if_not_empty(&mut v);
341            }
342            if v.starts_with("alt") {
343                state |= ModifiersState::ALT;
344                v = &v[v.len().min(3)..];
345                adv_dash_if_not_empty(&mut v);
346            }
347            if v.starts_with("shift") {
348                state |= ModifiersState::SHIFT;
349                v = &v[v.len().min(5)..];
350                adv_dash_if_not_empty(&mut v);
351            }
352            if v.starts_with("super") {
353                state |= ModifiersState::SUPER;
354                v = &v[v.len().min(5)..];
355            }
356
357            if v.is_empty() || u == "none" {
358                Ok(ModifiersStateDeser(state))
359            } else {
360                Err(E::invalid_value(
361                    de::Unexpected::Str(u),
362                    &"none or (sub-set of) ctrl-alt-shift-super",
363                ))
364            }
365        }
366    }
367
368    impl<'de> Deserialize<'de> for ModifiersStateDeser {
369        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
370            d.deserialize_str(ModifiersStateDeser(Default::default()))
371        }
372    }
373
374    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
375    pub(super) struct MiscRule<Str = SmolStr> {
376        #[serde(rename = "modifiers")]
377        pub(super) mods: ModifiersStateDeser,
378        #[serde(flatten)]
379        pub(super) key: ComplexKey<Str>,
380        #[serde(rename = "command")]
381        pub(super) cmd: Command,
382    }
383}
384
385#[cfg(feature = "serde")]
386mod ser {
387    use super::common::{ComplexKey, MiscRule, ModifiersStateDeser, SimpleKey};
388    use super::{Key, Shortcuts};
389    use serde::ser::{Serialize, SerializeMap, Serializer};
390
391    fn unpack_key<'a>(key: Key<&'a str>) -> Result<SimpleKey, ComplexKey<&'a str>> {
392        match key {
393            Key::Named(named) => Ok(SimpleKey::Named(named)),
394            Key::Character(c) => {
395                let mut iter = c.chars();
396                if let Some(c) = iter.next() {
397                    if iter.next().is_none() {
398                        return Ok(SimpleKey::Char(c));
399                    }
400                }
401                Err(ComplexKey::Character(c))
402            }
403            Key::Unidentified(code) => Err(ComplexKey::Unidentified(code)),
404            Key::Dead(None) => panic!("invalid shortcut"),
405            Key::Dead(Some(c)) => Err(ComplexKey::Dead(c)),
406        }
407    }
408
409    impl Serialize for Shortcuts {
410        fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
411            // Use BTreeMap for stable order of output
412            use std::collections::BTreeMap;
413
414            let mut serializer = s.serialize_map(Some(self.map.len() + 1))?;
415            let mut misc = Vec::new();
416
417            for (state, key_cmds) in self.map.iter() {
418                let mods = ModifiersStateDeser(*state);
419                let mut map = BTreeMap::new();
420
421                for (key, cmd) in key_cmds.iter() {
422                    match unpack_key(key.as_ref()) {
423                        Ok(sk) => {
424                            map.insert(sk, *cmd);
425                        }
426                        Err(key) => {
427                            let cmd = *cmd;
428                            misc.push(MiscRule { mods, key, cmd });
429                        }
430                    }
431                }
432
433                // Keys are now sorted and filtered
434                if !map.is_empty() {
435                    serializer.serialize_key(&mods)?;
436                    serializer.serialize_value(&map)?;
437                }
438            }
439
440            if !misc.is_empty() {
441                serializer.serialize_key("other")?;
442                misc.sort();
443                serializer.serialize_value(&misc)?;
444            }
445            serializer.end()
446        }
447    }
448}
449
450#[cfg(feature = "serde")]
451mod deser {
452    use super::common::{MiscRule, ModifiersStateDeser, SimpleKey};
453    use super::{Command, Key, ModifiersState, Shortcuts};
454    use linear_map::LinearMap;
455    use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, Visitor};
456    use std::collections::HashMap;
457    use std::fmt;
458
459    enum OptModifiersStateDeser {
460        State(ModifiersStateDeser),
461        Other,
462    }
463
464    impl<'de> Deserialize<'de> for OptModifiersStateDeser {
465        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
466            d.deserialize_str(OptModifiersStateDeser::Other)
467        }
468    }
469
470    impl<'de> Visitor<'de> for OptModifiersStateDeser {
471        type Value = OptModifiersStateDeser;
472
473        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
474            formatter.write_str("none or (sub-set of) ctrl-alt-shift-super or other")
475        }
476
477        fn visit_str<E: de::Error>(self, u: &str) -> Result<Self::Value, E> {
478            if u == "other" {
479                Ok(OptModifiersStateDeser::Other)
480            } else {
481                ModifiersStateDeser::visit_str(ModifiersStateDeser(Default::default()), u)
482                    .map(OptModifiersStateDeser::State)
483            }
484        }
485    }
486
487    struct DeserSimple<'a>(&'a mut HashMap<Key, Command>);
488
489    impl<'a, 'de> DeserializeSeed<'de> for DeserSimple<'a> {
490        type Value = ();
491
492        fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
493            d.deserialize_map(self)
494        }
495    }
496
497    impl<'a, 'de> Visitor<'de> for DeserSimple<'a> {
498        type Value = ();
499
500        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
501            formatter.write_str("a map")
502        }
503
504        fn visit_map<A>(self, mut reader: A) -> Result<Self::Value, A::Error>
505        where
506            A: de::MapAccess<'de>,
507        {
508            while let Some(sk) = reader.next_key::<SimpleKey>()? {
509                let key: Key = sk.into();
510                let cmd: Command = reader.next_value()?;
511                self.0.insert(key, cmd);
512            }
513
514            Ok(())
515        }
516    }
517
518    struct DeserComplex<'a>(&'a mut LinearMap<ModifiersState, HashMap<Key, Command>>);
519
520    impl<'a, 'de> DeserializeSeed<'de> for DeserComplex<'a> {
521        type Value = ();
522
523        fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
524            d.deserialize_seq(self)
525        }
526    }
527
528    impl<'a, 'de> Visitor<'de> for DeserComplex<'a> {
529        type Value = ();
530
531        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
532            formatter.write_str("a sequence")
533        }
534
535        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
536        where
537            A: de::SeqAccess<'de>,
538        {
539            while let Some(rule) = seq.next_element::<MiscRule>()? {
540                let ModifiersStateDeser(state) = rule.mods;
541                let sub = self.0.entry(state).or_insert_with(Default::default);
542                sub.insert(rule.key.into(), rule.cmd);
543            }
544
545            Ok(())
546        }
547    }
548
549    struct ShortcutsVisitor;
550
551    impl<'de> Visitor<'de> for ShortcutsVisitor {
552        type Value = Shortcuts;
553
554        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
555            formatter.write_str("a map")
556        }
557
558        fn visit_map<A>(self, mut reader: A) -> Result<Self::Value, A::Error>
559        where
560            A: MapAccess<'de>,
561        {
562            let mut map = LinearMap::<ModifiersState, HashMap<Key, Command>>::new();
563            while let Some(opt_state) = reader.next_key::<OptModifiersStateDeser>()? {
564                match opt_state {
565                    OptModifiersStateDeser::State(ModifiersStateDeser(state)) => {
566                        let sub = map.entry(state).or_insert_with(Default::default);
567                        reader.next_value_seed(DeserSimple(sub))?;
568                    }
569                    OptModifiersStateDeser::Other => {
570                        reader.next_value_seed(DeserComplex(&mut map))?;
571                    }
572                }
573            }
574
575            Ok(Shortcuts { map })
576        }
577    }
578
579    impl<'de> Deserialize<'de> for Shortcuts {
580        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
581            d.deserialize_map(ShortcutsVisitor)
582        }
583    }
584}