Skip to main content

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::META;
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::META;
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 META: ModifiersState = ModifiersState::META;
294
295            let s = match self.0 {
296                state if state == ModifiersState::empty() => "none",
297                META => "meta",
298                ALT => "alt",
299                state if state == ALT | META => "alt-meta",
300                state if state == CONTROL => "ctrl",
301                state if state == CONTROL | META => "ctrl-meta",
302                state if state == CONTROL | ALT => "ctrl-alt",
303                state if state == CONTROL | ALT | META => "ctrl-alt-meta",
304                SHIFT => "shift",
305                state if state == SHIFT | META => "shift-meta",
306                state if state == SHIFT | ALT => "alt-shift",
307                state if state == SHIFT | ALT | META => "alt-shift-meta",
308                state if state == SHIFT | CONTROL => "ctrl-shift",
309                state if state == SHIFT | CONTROL | META => "ctrl-shift-meta",
310                state if state == SHIFT | CONTROL | ALT => "ctrl-alt-shift",
311                _ => "ctrl-alt-shift-meta",
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-meta")
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() && v.starts_with('-') {
331                    *v = &v[1..];
332                }
333            };
334
335            if v.starts_with("ctrl") {
336                state |= ModifiersState::CONTROL;
337                v = &v[v.len().min(4)..];
338                adv_dash_if_not_empty(&mut v);
339            }
340            if v.starts_with("alt") {
341                state |= ModifiersState::ALT;
342                v = &v[v.len().min(3)..];
343                adv_dash_if_not_empty(&mut v);
344            }
345            if v.starts_with("shift") {
346                state |= ModifiersState::SHIFT;
347                v = &v[v.len().min(5)..];
348                adv_dash_if_not_empty(&mut v);
349            }
350            if v.starts_with("meta") {
351                state |= ModifiersState::META;
352                v = &v[v.len().min(5)..];
353            }
354
355            if v.is_empty() || u == "none" {
356                Ok(ModifiersStateDeser(state))
357            } else {
358                Err(E::invalid_value(
359                    de::Unexpected::Str(u),
360                    &"none or (sub-set of) ctrl-alt-shift-meta",
361                ))
362            }
363        }
364    }
365
366    impl<'de> Deserialize<'de> for ModifiersStateDeser {
367        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
368            d.deserialize_str(ModifiersStateDeser(Default::default()))
369        }
370    }
371
372    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
373    pub(super) struct MiscRule<Str = SmolStr> {
374        #[serde(rename = "modifiers")]
375        pub(super) mods: ModifiersStateDeser,
376        #[serde(flatten)]
377        pub(super) key: ComplexKey<Str>,
378        #[serde(rename = "command")]
379        pub(super) cmd: Command,
380    }
381}
382
383#[cfg(feature = "serde")]
384mod ser {
385    use super::common::{ComplexKey, MiscRule, ModifiersStateDeser, SimpleKey};
386    use super::{Key, Shortcuts};
387    use serde::ser::{Serialize, SerializeMap, Serializer};
388
389    fn unpack_key<'a>(key: Key<&'a str>) -> Result<SimpleKey, ComplexKey<&'a str>> {
390        match key {
391            Key::Named(named) => Ok(SimpleKey::Named(named)),
392            Key::Character(c) => {
393                let mut iter = c.chars();
394                if let Some(c) = iter.next()
395                    && iter.next().is_none()
396                {
397                    return Ok(SimpleKey::Char(c));
398                }
399                Err(ComplexKey::Character(c))
400            }
401            Key::Unidentified(code) => Err(ComplexKey::Unidentified(code)),
402            Key::Dead(None) => panic!("invalid shortcut"),
403            Key::Dead(Some(c)) => Err(ComplexKey::Dead(c)),
404        }
405    }
406
407    impl Serialize for Shortcuts {
408        fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
409            // Use BTreeMap for stable order of output
410            use std::collections::BTreeMap;
411
412            let mut serializer = s.serialize_map(Some(self.map.len() + 1))?;
413            let mut misc = Vec::new();
414
415            for (state, key_cmds) in self.map.iter() {
416                let mods = ModifiersStateDeser(*state);
417                let mut map = BTreeMap::new();
418
419                for (key, cmd) in key_cmds.iter() {
420                    match unpack_key(key.as_ref()) {
421                        Ok(sk) => {
422                            map.insert(sk, *cmd);
423                        }
424                        Err(key) => {
425                            let cmd = *cmd;
426                            misc.push(MiscRule { mods, key, cmd });
427                        }
428                    }
429                }
430
431                // Keys are now sorted and filtered
432                if !map.is_empty() {
433                    serializer.serialize_key(&mods)?;
434                    serializer.serialize_value(&map)?;
435                }
436            }
437
438            if !misc.is_empty() {
439                serializer.serialize_key("other")?;
440                misc.sort();
441                serializer.serialize_value(&misc)?;
442            }
443            serializer.end()
444        }
445    }
446}
447
448#[cfg(feature = "serde")]
449mod deser {
450    use super::common::{MiscRule, ModifiersStateDeser, SimpleKey};
451    use super::{Command, Key, ModifiersState, Shortcuts};
452    use linear_map::LinearMap;
453    use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, Visitor};
454    use std::collections::HashMap;
455    use std::fmt;
456
457    enum OptModifiersStateDeser {
458        State(ModifiersStateDeser),
459        Other,
460    }
461
462    impl<'de> Deserialize<'de> for OptModifiersStateDeser {
463        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
464            d.deserialize_str(OptModifiersStateDeser::Other)
465        }
466    }
467
468    impl<'de> Visitor<'de> for OptModifiersStateDeser {
469        type Value = OptModifiersStateDeser;
470
471        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
472            formatter.write_str("none or (sub-set of) ctrl-alt-shift-meta or other")
473        }
474
475        fn visit_str<E: de::Error>(self, u: &str) -> Result<Self::Value, E> {
476            if u == "other" {
477                Ok(OptModifiersStateDeser::Other)
478            } else {
479                ModifiersStateDeser::visit_str(ModifiersStateDeser(Default::default()), u)
480                    .map(OptModifiersStateDeser::State)
481            }
482        }
483    }
484
485    struct DeserSimple<'a>(&'a mut HashMap<Key, Command>);
486
487    impl<'a, 'de> DeserializeSeed<'de> for DeserSimple<'a> {
488        type Value = ();
489
490        fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
491            d.deserialize_map(self)
492        }
493    }
494
495    impl<'a, 'de> Visitor<'de> for DeserSimple<'a> {
496        type Value = ();
497
498        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
499            formatter.write_str("a map")
500        }
501
502        fn visit_map<A>(self, mut reader: A) -> Result<Self::Value, A::Error>
503        where
504            A: de::MapAccess<'de>,
505        {
506            while let Some(sk) = reader.next_key::<SimpleKey>()? {
507                let key: Key = sk.into();
508                let cmd: Command = reader.next_value()?;
509                self.0.insert(key, cmd);
510            }
511
512            Ok(())
513        }
514    }
515
516    struct DeserComplex<'a>(&'a mut LinearMap<ModifiersState, HashMap<Key, Command>>);
517
518    impl<'a, 'de> DeserializeSeed<'de> for DeserComplex<'a> {
519        type Value = ();
520
521        fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
522            d.deserialize_seq(self)
523        }
524    }
525
526    impl<'a, 'de> Visitor<'de> for DeserComplex<'a> {
527        type Value = ();
528
529        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
530            formatter.write_str("a sequence")
531        }
532
533        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
534        where
535            A: de::SeqAccess<'de>,
536        {
537            while let Some(rule) = seq.next_element::<MiscRule>()? {
538                let ModifiersStateDeser(state) = rule.mods;
539                let sub = self.0.entry(state).or_insert_with(Default::default);
540                sub.insert(rule.key.into(), rule.cmd);
541            }
542
543            Ok(())
544        }
545    }
546
547    struct ShortcutsVisitor;
548
549    impl<'de> Visitor<'de> for ShortcutsVisitor {
550        type Value = Shortcuts;
551
552        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
553            formatter.write_str("a map")
554        }
555
556        fn visit_map<A>(self, mut reader: A) -> Result<Self::Value, A::Error>
557        where
558            A: MapAccess<'de>,
559        {
560            let mut map = LinearMap::<ModifiersState, HashMap<Key, Command>>::new();
561            while let Some(opt_state) = reader.next_key::<OptModifiersStateDeser>()? {
562                match opt_state {
563                    OptModifiersStateDeser::State(ModifiersStateDeser(state)) => {
564                        let sub = map.entry(state).or_insert_with(Default::default);
565                        reader.next_value_seed(DeserSimple(sub))?;
566                    }
567                    OptModifiersStateDeser::Other => {
568                        reader.next_value_seed(DeserComplex(&mut map))?;
569                    }
570                }
571            }
572
573            Ok(Shortcuts { map })
574        }
575    }
576
577    impl<'de> Deserialize<'de> for Shortcuts {
578        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
579            d.deserialize_map(ShortcutsVisitor)
580        }
581    }
582}