Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(override): add defcfg option to release output early #1120

Merged
merged 3 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ If you need help, please feel welcome to ask in the GitHub discussions.
;; changes will be logged.
;;
;; log-layer-changes no

;; This configuration will press and then immediately release the non-modifier key
;; as soon as the override activates, meaning you are unlikely as a human to ever
;; release modifiers first, which can result in unintended behaviour.
;;
;; The downside of this configuration is that the non-modifier key
;; does not remain held which is important to consider for your use cases.
override-release-on-activation yes
)

;; deflocalkeys-* enables you to define and use key names that match your locale
Expand Down
36 changes: 36 additions & 0 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,42 @@ The default (and minimum) value is `5` and the unit is milliseconds.
)
----

[[override-release-on-activation]]
=== override-release-on-activation
<<table-of-contents,Back to ToC>>

This configuration item changes activation behaviour from `defoverrides`.

Take this example override:

[source]
----
(defoverrides (lsft a) (lsft 9))
----

The default behaviour is that if `lsft` is released **before** releasing `a`,
kanata's behaviour would be to send `a`.

A future improvement could be to make the `9` continue to be the key held,
but that is not implemented today.

The workaround in case the above behaviour negatively impacts your workflow
is to enable this configuration.
This configuration will press and then immediately release the `9` output
as soon as the override activates, meaning you are unlikely as a human to ever
release `lsft` first.

The effect of this configuration is that the `9` key cannot remain held
when activated by the override which is important to consider for your use cases.

.Example:
[source]
----
(defcfg
override-release-on-activation yes
)
----

[[linux-only-linux-dev]]
=== Linux only: linux-dev
<<table-of-contents,Back to ToC>>
Expand Down
5 changes: 4 additions & 1 deletion keyberon/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,10 @@ impl<'a, T: 'a> State<'a, T> {
}
pub fn release_state(&self, s: ReleasableState) -> Option<Self> {
match (*self, s) {
(NormalKey { keycode: k1, .. }, ReleasableState::KeyCode(k2)) => {
(
NormalKey { keycode: k1, .. } | FakeKey { keycode: k1 },
ReleasableState::KeyCode(k2),
) => {
if k1 == k2 {
None
} else {
Expand Down
5 changes: 5 additions & 0 deletions parser/src/cfg/defcfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub struct CfgOptions {
pub delegate_to_first_layer: bool,
pub movemouse_inherit_accel_state: bool,
pub movemouse_smooth_diagonals: bool,
pub override_release_on_activation: bool,
pub dynamic_macro_max_presses: u16,
pub dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour,
pub concurrent_tap_hold: bool,
Expand Down Expand Up @@ -115,6 +116,7 @@ impl Default for CfgOptions {
delegate_to_first_layer: false,
movemouse_inherit_accel_state: false,
movemouse_smooth_diagonals: false,
override_release_on_activation: false,
dynamic_macro_max_presses: 128,
dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour::Recorded,
concurrent_tap_hold: false,
Expand Down Expand Up @@ -587,6 +589,9 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
"movemouse-inherit-accel-state" => {
cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)?
}
"override-release-on-activation" => {
cfg.override_release_on_activation = parse_defcfg_val_bool(val, label)?
}
"concurrent-tap-hold" => {
cfg.concurrent_tap_hold = parse_defcfg_val_bool(val, label)?
}
Expand Down
6 changes: 5 additions & 1 deletion parser/src/cfg/key_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ impl OverrideStates {
fn add_overrides(&self, oscs: &mut Vec<KeyCode>) {
oscs.extend(self.oscs_to_add.iter().copied().map(KeyCode::from));
}

pub fn removed_oscs(&self) -> impl Iterator<Item = OsCode> + '_ {
self.oscs_to_remove.iter().copied()
}
}

/// A collection of global key overrides.
Expand Down Expand Up @@ -84,12 +88,12 @@ impl Overrides {
if self.is_empty() {
return;
}
states.cleanup();
for kc in kcs.iter().copied() {
states.update(kc.into(), self);
}
kcs.retain(|kc| !states.is_key_overridden((*kc).into()));
states.add_overrides(kcs);
states.cleanup();
}

pub fn output_non_mods_for_input_non_mod(&self, in_osc: OsCode) -> Vec<OsCode> {
Expand Down
1 change: 1 addition & 0 deletions parser/src/cfg/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,7 @@ fn parse_all_defcfg() {
delegate-to-first-layer yes
movemouse-inherit-accel-state yes
movemouse-smooth-diagonals yes
override-release-on-activation yes
dynamic-macro-max-presses 1000
concurrent-tap-hold yes
rapid-event-delay 5
Expand Down
14 changes: 14 additions & 0 deletions parser/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ impl OsCode {
#[cfg(target_os = "macos")]
return OsCode::from_u16_macos(code);
}

pub fn is_modifier(self) -> bool {
matches!(
self,
OsCode::KEY_LEFTSHIFT
| OsCode::KEY_RIGHTSHIFT
| OsCode::KEY_LEFTMETA
| OsCode::KEY_RIGHTMETA
| OsCode::KEY_LEFTCTRL
| OsCode::KEY_RIGHTCTRL
| OsCode::KEY_LEFTALT
| OsCode::KEY_RIGHTALT
)
}
}

static CUSTOM_STRS_TO_OSCODES: Lazy<Mutex<HashMap<String, OsCode>>> = Lazy::new(|| {
Expand Down
16 changes: 16 additions & 0 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::sync::mpsc::{Receiver, SyncSender as Sender, TryRecvError};
#[cfg(feature = "passthru_ahk")]
use std::sync::mpsc::Sender as ASender;

use kanata_keyberon::action::ReleasableState;
use kanata_keyberon::key_code::*;
use kanata_keyberon::layout::{CustomEvent, Event, Layout, State};

Expand Down Expand Up @@ -186,6 +187,7 @@ pub struct Kanata {
/// gets stored in this buffer and if the next movemouse action is opposite axis
/// than the one stored in the buffer, both events are outputted at the same time.
movemouse_buffer: Option<(Axis, CalculatedMouseMove)>,
override_release_on_activation: bool,
/// Configured maximum for dynamic macro recording, to protect users from themselves if they
/// have accidentally left it on.
dynamic_macro_max_presses: u16,
Expand Down Expand Up @@ -355,6 +357,7 @@ impl Kanata {
.unwrap_or(cfg.options.log_layer_changes),
caps_word: None,
movemouse_smooth_diagonals: cfg.options.movemouse_smooth_diagonals,
override_release_on_activation: cfg.options.override_release_on_activation,
movemouse_inherit_accel_state: cfg.options.movemouse_inherit_accel_state,
dynamic_macro_max_presses: cfg.options.dynamic_macro_max_presses,
dynamic_macro_replay_behaviour: ReplayBehaviour {
Expand Down Expand Up @@ -454,6 +457,7 @@ impl Kanata {
.unwrap_or(cfg.options.log_layer_changes),
caps_word: None,
movemouse_smooth_diagonals: cfg.options.movemouse_smooth_diagonals,
override_release_on_activation: cfg.options.override_release_on_activation,
movemouse_inherit_accel_state: cfg.options.movemouse_inherit_accel_state,
dynamic_macro_max_presses: cfg.options.dynamic_macro_max_presses,
dynamic_macro_replay_behaviour: ReplayBehaviour {
Expand Down Expand Up @@ -511,6 +515,7 @@ impl Kanata {
self.log_layer_changes =
get_forced_log_layer_changes().unwrap_or(cfg.options.log_layer_changes);
self.movemouse_smooth_diagonals = cfg.options.movemouse_smooth_diagonals;
self.override_release_on_activation = cfg.options.override_release_on_activation;
self.movemouse_inherit_accel_state = cfg.options.movemouse_inherit_accel_state;
self.dynamic_macro_max_presses = cfg.options.dynamic_macro_max_presses;
self.dynamic_macro_replay_behaviour = ReplayBehaviour {
Expand Down Expand Up @@ -935,6 +940,17 @@ impl Kanata {

self.overrides
.override_keys(cur_keys, &mut self.override_states);
if self.override_release_on_activation {
for removed in self.override_states.removed_oscs() {
if !removed.is_modifier() {
layout.states.retain(|s| {
s.release_state(ReleasableState::KeyCode(removed.into()))
.is_some()
});
}
}
}

if let Some(caps_word) = &mut self.caps_word {
if caps_word.maybe_add_lsft(cur_keys) == CapsWordNextState::End {
self.caps_word = None;
Expand Down
9 changes: 1 addition & 8 deletions src/kanata/sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,7 @@ pub(super) fn do_successful_sequence_termination(
// desired to fix this, a shorter list of keys would
// probably be the list of keys that **do** output
// characters than those that don't.
OsCode::KEY_LEFTSHIFT
| OsCode::KEY_RIGHTSHIFT
| OsCode::KEY_LEFTMETA
| OsCode::KEY_RIGHTMETA
| OsCode::KEY_LEFTCTRL
| OsCode::KEY_RIGHTCTRL
| OsCode::KEY_LEFTALT
| OsCode::KEY_RIGHTALT => continue,
osc if osc.is_modifier() => continue,
osc if matches!(u16::from(osc), KEY_IGNORE_MIN..=KEY_IGNORE_MAX) => continue,
_ => {
kbd_out.press_key(OsCode::KEY_BACKSPACE)?;
Expand Down
33 changes: 33 additions & 0 deletions src/tests/sim_tests/override_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,36 @@ fn override_with_unmod() {
result
);
}

#[test]
fn override_release_mod_change_key() {
let result = simulate(
"
(defsrc)
(deflayer base)
(defoverrides (lsft a) (lsft 9))
",
"d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10",
)
.to_ascii()
.no_time();
assert_eq!("dn:LShift dn:Kb9 up:LShift up:Kb9 dn:A up:A", result);
}

#[test]
fn override_eagerly_releases() {
let result = simulate(
"
(defcfg override-release-on-activation yes)
(defsrc)
(deflayer base)
(defoverrides (lsft a) (lsft 9))
",
"d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10",
)
.to_ascii();
assert_eq!(
"dn:LShift t:10ms dn:Kb9 t:1ms up:Kb9 t:9ms up:LShift",
result
);
}
Loading