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: tap-hold-release-keys #343

Merged
merged 1 commit into from
Mar 12, 2023
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
6 changes: 6 additions & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ If you need help, you are welcome to ask.
;; tap: e hold: chords layer timeout: esc
ect (tap-hold-release-timeout 200 200 e @chr esc)

;; There is another variant of `tap-hold-release` that takes a 5th parameter
;; that is a list of keys that will trigger an early tap.

;; tap: u hold: misc layer early tap if any of: (a o e) are pressed
umk (tap-hold-release-keys 200 200 u @msc (a o e))

;; tap for capslk, hold for lctl
cap (tap-hold 200 200 caps lctl)

Expand Down
15 changes: 0 additions & 15 deletions cfg_samples/minimal.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,6 @@ configuration entries.
process-unmapped-keys yes
)


#|
The defsrc entry below chooses which keys are remappable by kanata. The
deflayer entries that follow change the behaviour of the corresponding keys in
the order that they appear in defsrc. Note that any extra spaces or newlines
are ignored — defsrc and deflayer are treated as lists of keys/actions that are
separated by 1 or more whitespace characters.

The reason for mapping the Shift keys is so that they get processed correctly
by kanata for tap-hold keys. However, you can add
the line below to defcfg above to tell kanata to process all keys:

process-unmapped-keys yes
|#

(defsrc
caps grv i
j k l
Expand Down
8 changes: 8 additions & 0 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,12 @@ variants. The 5th parameter is another action, which will activate if the hold
timeout expires as opposed to being triggered by other key actions, whereas the
non `-timeout` variants will activate the hold action in both cases.

- `tap-hold-release-keys`

This variant takes a 5th parameter which is a list of keys that trigger an
early tap when they are pressed while the `tap-hold-release-keys` action is
waiting.

Example:

----
Expand All @@ -1098,6 +1104,8 @@ Example:
oat (tap-hold-press-timeout 200 200 o @arr bspc)
;; tap: e hold: chords layer timeout: esc
ect (tap-hold-release-timeout 200 200 e @chr esc)
;; tap: u hold: misc layer early tap if any of: (a o e) are pressed
umk (tap-hold-release-keys 200 200 u @msc (a o e))
)
----

Expand Down
31 changes: 12 additions & 19 deletions keyberon/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl<'a, T> Debug for SequenceEvent<'a, T> {
/// Behavior configuration of HoldTap.
#[non_exhaustive]
#[derive(Clone, Copy)]
pub enum HoldTapConfig {
pub enum HoldTapConfig<'a> {
/// Only the timeout will determine between hold and tap action.
///
/// This is a sane default.
Expand Down Expand Up @@ -94,39 +94,32 @@ pub enum HoldTapConfig {
/// value will cause a fallback to the timeout-based approach. If the
/// timeout is not triggered, the next tick will call the custom handler
/// again.
Custom(fn(QueuedIter) -> Option<WaitingAction>),
Custom(&'a (dyn Fn(QueuedIter) -> Option<WaitingAction> + Send + Sync)),
}

impl Debug for HoldTapConfig {
impl<'a> Debug for HoldTapConfig<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
HoldTapConfig::Default => f.write_str("Default"),
HoldTapConfig::HoldOnOtherKeyPress => f.write_str("HoldOnOtherKeyPress"),
HoldTapConfig::PermissiveHold => f.write_str("PermissiveHold"),
HoldTapConfig::Custom(func) => f
.debug_tuple("Custom")
.field(&(*func as fn(QueuedIter<'static>) -> Option<WaitingAction>) as &dyn Debug)
.finish(),
HoldTapConfig::Custom(_) => f.write_str("Custom"),
}
}
}

impl PartialEq for HoldTapConfig {
impl<'a> PartialEq for HoldTapConfig<'a> {
fn eq(&self, other: &Self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match (self, other) {
(HoldTapConfig::Default, HoldTapConfig::Default)
| (HoldTapConfig::HoldOnOtherKeyPress, HoldTapConfig::HoldOnOtherKeyPress)
| (HoldTapConfig::PermissiveHold, HoldTapConfig::PermissiveHold) => true,
(HoldTapConfig::Custom(self_func), HoldTapConfig::Custom(other_func)) => {
*self_func as fn(QueuedIter<'static>) -> Option<WaitingAction> == *other_func
}
_ => false,
}
}
}

impl Eq for HoldTapConfig {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// A state that that can be released from the active states via the ReleaseState action.
pub enum ReleasableState {
Expand All @@ -149,7 +142,7 @@ pub enum ReleasableState {
/// but whatever the configuration is, if the key is pressed more
/// than `timeout`, the hold action is activated (if no other
/// action was determined before).
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HoldTapAction<'a, T>
where
T: 'a,
Expand All @@ -164,7 +157,7 @@ where
/// The timeout action
pub timeout_action: Action<'a, T>,
/// Behavior configuration.
pub config: HoldTapConfig,
pub config: HoldTapConfig<'a>,
/// Configuration of the tap and hold holds the tap action.
///
/// If you press and release the key in such a way that the tap
Expand All @@ -184,7 +177,7 @@ where
}

/// Define one shot key behaviour.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OneShot<'a, T = core::convert::Infallible>
where
T: 'a,
Expand Down Expand Up @@ -214,7 +207,7 @@ pub enum OneShotEndConfig {
pub const ONE_SHOT_MAX_ACTIVE: usize = 8;

/// Define tap dance behaviour.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TapDance<'a, T = core::convert::Infallible>
where
T: 'a,
Expand All @@ -240,7 +233,7 @@ pub enum TapDanceConfig {
}

/// A group of chords (actions mapped to a combination of multiple physical keys pressed together).
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ChordsGroup<'a, T = core::convert::Infallible>
where
T: 'a,
Expand Down Expand Up @@ -296,7 +289,7 @@ pub type ChordKeys = u32;
pub const MAX_CHORD_KEYS: usize = ChordKeys::BITS as usize;

/// The different actions that can be done.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Action<'a, T = core::convert::Infallible>
where
T: 'a,
Expand Down
22 changes: 12 additions & 10 deletions keyberon/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ impl<'a, T> TapDanceEagerState<'a, T> {

#[derive(Debug)]
enum WaitingConfig<'a, T: 'a + std::fmt::Debug> {
HoldTap(HoldTapConfig),
HoldTap(HoldTapConfig<'a>),
TapDance(TapDanceState<'a, T>),
Chord(&'a ChordsGroup<'a, T>),
}
Expand Down Expand Up @@ -347,11 +347,12 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> {
}
}
HoldTapConfig::PermissiveHold => {
for (x, s) in queued.iter().enumerate() {
if s.event.is_press() {
let (i, j) = s.event.coord();
let mut queued = queued.iter();
while let Some(q) = queued.next() {
if q.event.is_press() {
let (i, j) = q.event.coord();
let target = Event::Release(i, j);
if queued.iter().skip(x + 1).any(|s| s.event == target) {
if queued.clone().any(|q| q.event == target) {
return Some(WaitingAction::Hold);
}
}
Expand Down Expand Up @@ -702,6 +703,7 @@ impl OneShotState {
/// An iterator over the currently queued events.
///
/// Events can be retrieved by iterating over this struct and calling [Queued::event].
#[derive(Clone)]
pub struct QueuedIter<'a>(arraydeque::Iter<'a, Queued>);

impl<'a> Iterator for QueuedIter<'a> {
Expand All @@ -715,7 +717,7 @@ impl<'a> Iterator for QueuedIter<'a> {
}

/// An event, waiting in a queue to be processed.
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub struct Queued {
event: Event,
since: u16,
Expand Down Expand Up @@ -1642,31 +1644,31 @@ mod test {
hold: k(Kb1),
timeout_action: k(Kb1),
tap: k(Kb0),
config: HoldTapConfig::Custom(always_tap),
config: HoldTapConfig::Custom(&always_tap),
tap_hold_interval: 0,
}),
HoldTap(&HoldTapAction {
timeout: 200,
hold: k(Kb3),
timeout_action: k(Kb3),
tap: k(Kb2),
config: HoldTapConfig::Custom(always_hold),
config: HoldTapConfig::Custom(&always_hold),
tap_hold_interval: 0,
}),
HoldTap(&HoldTapAction {
timeout: 200,
hold: k(Kb5),
timeout_action: k(Kb5),
tap: k(Kb4),
config: HoldTapConfig::Custom(always_nop),
config: HoldTapConfig::Custom(&always_nop),
tap_hold_interval: 0,
}),
HoldTap(&HoldTapAction {
timeout: 200,
hold: k(Kb7),
timeout_action: k(Kb7),
tap: k(Kb6),
config: HoldTapConfig::Custom(always_none),
config: HoldTapConfig::Custom(&always_none),
tap_hold_interval: 0,
}),
]]];
Expand Down
32 changes: 32 additions & 0 deletions src/cfg/custom_tap_hold.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use kanata_keyberon::layout::{Event, QueuedIter, WaitingAction};

use crate::keys::OsCode;

use super::alloc::Allocations;

/// Returns a closure that can be used in `HoldTapConfig::Custom`, which will return early with a
/// Tap action in the case that any of `keys` are pressed. Otherwise it behaves as
/// `HoldTapConfig::PermissiveHold` would.
pub(crate) fn custom_tap_hold_release(
keys: &[OsCode],
a: &Allocations,
) -> &'static (dyn Fn(QueuedIter) -> Option<WaitingAction> + Send + Sync) {
let keys = a.sref_vec(Vec::from_iter(keys.iter().copied()));
a.sref(move |mut queued: QueuedIter| -> Option<WaitingAction> {
while let Some(q) = queued.next() {
if q.event().is_press() {
let (i, j) = q.event().coord();
// If any key matches the input, do a tap right away.
if keys.iter().copied().map(u16::from).any(|j2| j2 == j) {
return Some(WaitingAction::Tap);
}
// Otherwise do the PermissiveHold algorithm.
let target = Event::Release(i, j);
if queued.clone().copied().any(|q| q.event() == target) {
return Some(WaitingAction::Hold);
}
}
}
None
})
}
58 changes: 56 additions & 2 deletions src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ use alloc::*;
mod key_override;
pub use key_override::*;

mod custom_tap_hold;
use custom_tap_hold::*;

use crate::custom_action::*;
use crate::keys::*;
use crate::layers::*;
Expand Down Expand Up @@ -911,6 +914,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct
"tap-hold-release-timeout" => {
parse_tap_hold_timeout(&ac[1..], s, HoldTapConfig::PermissiveHold)
}
"tap-hold-release-keys" => parse_tap_hold_release_keys(&ac[1..], s),
"multi" => parse_multi(&ac[1..], s),
"macro" => parse_macro(&ac[1..], s),
"macro-release-cancel" => parse_macro_release_cancel(&ac[1..], s),
Expand Down Expand Up @@ -979,7 +983,7 @@ fn layer_idx(ac_params: &[SExpr], layers: &LayerIndexes) -> Result<usize> {
fn parse_tap_hold(
ac_params: &[SExpr],
s: &ParsedState,
config: HoldTapConfig,
config: HoldTapConfig<'static>,
) -> Result<&'static KanataAction> {
if ac_params.len() != 4 {
bail!(
Expand Down Expand Up @@ -1009,7 +1013,7 @@ Params in order:
fn parse_tap_hold_timeout(
ac_params: &[SExpr],
s: &ParsedState,
config: HoldTapConfig,
config: HoldTapConfig<'static>,
) -> Result<&'static KanataAction> {
if ac_params.len() != 5 {
bail!(
Expand Down Expand Up @@ -1037,6 +1041,36 @@ Params in order:
}))))
}

fn parse_tap_hold_release_keys(
ac_params: &[SExpr],
s: &ParsedState,
) -> Result<&'static KanataAction> {
if ac_params.len() != 5 {
bail!(
r"tap-hold-release-keys expects 5 items after it, got {}.
Params in order:
<tap-timeout> <hold-timeout> <tap-action> <hold-action> <tap-trigger-keys>",
ac_params.len(),
)
}
let tap_timeout = parse_non_zero_u16(&ac_params[0], "tap timeout")?;
let hold_timeout = parse_non_zero_u16(&ac_params[1], "hold timeout")?;
let tap_action = parse_action(&ac_params[2], s)?;
let hold_action = parse_action(&ac_params[3], s)?;
let tap_trigger_keys = parse_key_list(&ac_params[4], "tap-trigger-keys")?;
if matches!(tap_action, Action::HoldTap { .. }) {
bail!("tap-hold does not work in the tap-action of tap-hold")
}
Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction {
config: HoldTapConfig::Custom(custom_tap_hold_release(&tap_trigger_keys, &s.a)),
tap_hold_interval: tap_timeout,
timeout: hold_timeout,
tap: *tap_action,
hold: *hold_action,
timeout_action: *hold_action,
}))))
}

fn parse_u16(expr: &SExpr, label: &str) -> Result<u16> {
expr.atom()
.map(str::parse::<u16>)
Expand All @@ -1054,6 +1088,26 @@ fn parse_non_zero_u16(expr: &SExpr, label: &str) -> Result<u16> {
.ok_or_else(|| anyhow_expr!(expr, "{label} must be 1-65535"))
}

fn parse_key_list(expr: &SExpr, label: &str) -> Result<Vec<OsCode>> {
expr.list()
.map(|keys| {
keys.iter().try_fold(vec![], |mut keys, key| {
match key {
SExpr::Atom(a) => {
keys.push(str_to_oscode(&a.t).ok_or_else(|| {
anyhow_expr!(key, "string of a known key is expected")
})?);
}
SExpr::List(_) => {
bail_expr!(key, "string of a known key is expected, found list instead")
}
};
Ok(keys)
})
})
.ok_or_else(|| anyhow_expr!(expr, "{label} must be a list of keys"))?
}

fn parse_multi(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> {
if ac_params.is_empty() {
bail!("multi expects at least one item after it")
Expand Down