From bf5127533ea170373fc396bf4f180864a8775445 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Tue, 29 Aug 2023 22:37:01 -0700 Subject: [PATCH] feat!: clear non-oneshot output chord on next action This commit changes output chord behavior to be more user-friendly by clearing output chord keys on the next action. I have also been told (though have not confirmed) that QMK has similar behaviour. I am not sure that the change in this commit replicates QMK's behaviour perfectly, but it seems good enough. Clearing output chord keys on the next action allows a subsequent typed key to not have modifiers pressed alongside it. Without this change, typing a new key without first releasing an output chord can have the unintended typing result. Users have asked about this few times in issues/discussions and the current workaround is to use a macro. However, this can be hard to discover. Some users may just be living with the annoyance because aren't aware that there is a workaround and they haven't asked. Output chords within a `one-shot` are ignored because someone might do something like `(one-shot C-S-lalt)` to get 3 modifiers within a one-shot action. These are probably intended to remain held. However, other output chords are usually used to type symbols or accented characters, e.g. S-1 or RA-a. If the symbol or accented character is held down, key repeat works just fine because OS key repeats are not keyberon actions. --- cfg_samples/kanata.kbd | 8 ++++-- docs/config.adoc | 14 ++++++++++ keyberon/src/layout.rs | 54 +++++++++++++++++++++++++++++++++++++-- parser/src/cfg/mod.rs | 5 +++- src/kanata/windows/mod.rs | 7 ++++- src/tests.rs | 12 +++++++++ 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 47b0dd892..e812c0c79 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -27,12 +27,12 @@ be able to configure kanata. If you follow along with the examples, you should be fine. Kanata should also hopefully have helpful error messages in case something goes wrong. -If you need help, you are welcome to ask. +If you need help, please feel welcome to ask in the GitHub discussions. |# ;; One defcfg entry may be added if desired. This is used for configuration ;; key-value pairs that change kanata's behaviour at a global level. -;; All configuration items are optional. +;; All defcfg options are optional. (defcfg ;; Your keyboard device will likely differ from this. I believe /dev/input/by-id/ ;; is preferable; I recall reading that it's less likely to change names on you, @@ -384,6 +384,10 @@ If you need help, you are welcome to ask. ;; same and only one is allowed in a single chord. This chord can be useful for ;; international layouts. ;; + ;; A special behaviour of output chords is that if another key is pressed, + ;; all of the chord keys will be released. For the explanation about why + ;; this is the case, see the configuration guide. + ;; ;; This use case for multi is typing an all-caps string. alp (multi lsft a b c d e f g h i j k l m n o p q r s t u v w x y z) diff --git a/docs/config.adoc b/docs/config.adoc index e56e26bd5..7175a1980 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -932,6 +932,8 @@ These modifiers may be combined together if desired. [source] ---- (defalias + ;; Type exclamation mark (US layout) + ex! S-1 ;; Ctrl+C: send SIGINT to a Linux terminal program int C-c ;; Win+Tab: open Windows' Task View @@ -942,6 +944,18 @@ These modifiers may be combined together if desired. ) ---- +A special behaviour of output chords is that if another key is pressed, +all of the chord keys will be released +before the newly pressed key action activates. +Output chords are typically used do one-off actions such as: + +- type a symbol, e.g. `S-1` +- type a special/accented character, e.g. `RA-a` +- do a special action like `C-c` to send `SIGTERM` in the terminal + +The modifier pressed with these one-off action +is usually not desired for subsequent actions. + [[repeat-key]] === Repeat key <> diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 7512ab04f..6f46f11ea 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -169,11 +169,24 @@ impl<'a, T> CustomEvent<'a, T> { } } +/// Metadata about normal key flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct NormalKeyFlags(u16); + +const NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION: u16 = 0x0001; + +impl NormalKeyFlags { + pub fn clear_on_next_action(self) -> bool { + (self.0 & NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION) == NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + } +} + #[derive(Debug, Eq, PartialEq)] pub enum State<'a, T: 'a> { NormalKey { keycode: KeyCode, coord: KCoord, + flags: NormalKeyFlags, }, LayerModifier { value: usize, @@ -1167,6 +1180,10 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.last_press_tracker.tap_hold_timeout = 0; } use Action::*; + self.states.retain(|s| match s { + NormalKey { flags, .. } => !flags.clear_on_next_action(), + _ => true, + }); match action { NoOp | Trans => { if !is_oneshot { @@ -1303,7 +1320,11 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } &KeyCode(keycode) => { self.last_press_tracker.coord = coord; - let _ = self.states.push(NormalKey { coord, keycode }); + let _ = self.states.push(NormalKey { + coord, + keycode, + flags: NormalKeyFlags(0), + }); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); @@ -1313,7 +1334,23 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt &MultipleKeyCodes(v) => { self.last_press_tracker.coord = coord; for &keycode in *v { - let _ = self.states.push(NormalKey { coord, keycode }); + let _ = self.states.push(NormalKey { + coord, + keycode, + // In Kanata, this action is only ever used with output chords. Output + // chords within a one-shot are ignored because someone might do something + // like (one-shot C-S-lalt to get 3 modifiers. These are probably intended + // to remain held. However, other output chords are usually used to type + // symbols or accented characters, e.g. S-1 or RA-a. Clearing chord keys on + // the next action allows a subsequent typed key to not have modifiers + // alongside it. But if the symbol or accented character is held down, key + // repeat works just fine. + flags: NormalKeyFlags(if is_oneshot { + 0 + } else { + NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + }), + }); } if !is_oneshot { self.oneshot @@ -3357,4 +3394,17 @@ mod test { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } + + #[test] + fn test_clear_multiple_keycodes() { + static LAYERS: Layers<2, 1, 1> = [[[k(A), MultipleKeyCodes(&[LCtrl, Enter].as_slice())]]]; + let mut layout = Layout::new(&LAYERS); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LCtrl, Enter], layout.keycodes()); + // Cancel chord keys on next keypress. + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[A], layout.keycodes()); + } } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index b7f4b40cb..72a5bd20b 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2900,7 +2900,10 @@ fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataA let action = parse_action(action, s)?; let Some(break_or_fallthrough) = break_or_fallthrough_expr.atom(s.vars()) else { - bail_expr!(break_or_fallthrough_expr, "{ERR_STR}\nthis must be one of: break, fallthrough"); + bail_expr!( + break_or_fallthrough_expr, + "{ERR_STR}\nthis must be one of: break, fallthrough" + ); }; let break_or_fallthrough = match break_or_fallthrough { "break" => BreakOrFallthrough::Break, diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 658d831e3..12679ef49 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -53,9 +53,14 @@ impl Kanata { pub fn check_release_non_physical_shift(&mut self) -> Result<()> { fn state_filter(v: &State<'_, &&[&CustomAction]>) -> Option> { match v { - State::NormalKey { keycode, coord } => Some(State::NormalKey::<()> { + State::NormalKey { + keycode, + coord, + flags, + } => Some(State::NormalKey::<()> { keycode: *keycode, coord: *coord, + flags: *flags, }), State::FakeKey { keycode } => Some(State::FakeKey::<()> { keycode: *keycode }), _ => None, diff --git a/src/tests.rs b/src/tests.rs index c714694e4..9c337ee54 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -60,3 +60,15 @@ fn parse_all_keys() { )) .unwrap(); } + +#[test] +fn sizeof_state() { + assert_eq!( + std::mem::size_of::< + kanata_keyberon::layout::State< + &'static &'static [&'static kanata_parser::custom_action::CustomAction], + >, + >(), + 2 * std::mem::size_of::() + ); +}