Skip to content

Commit

Permalink
feat(vkeys): add hold-for-duration action (#1355)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtroo authored Nov 16, 2024
1 parent fde197f commit fc1b95d
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 2 deletions.
1 change: 1 addition & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ If you need help, please feel welcome to ask in the GitHub discussions.
pal (on-press-fakekey pal tap)
ral (on-press-fakekey ral tap)
rdl (on-idle-fakekey ral tap 1000)
hfd (hold-for-duration 1000 met)

;; Test of on-press-fakekey and on-release-fakekey in a macro
t1 (macro-release-cancel @fsp 5 a b c @fsr 5 c b a)
Expand Down
9 changes: 7 additions & 2 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3225,9 +3225,13 @@ Virtual keys can be activated via special actions:
Activate a virtual key action when pressing the associated input key.
* `(on-release <action> <virtual key name>)` or `on↑`:
Activate a virtual key action when releasing the associated input key.
* `(on-idle <milliseconds> <action> <virtual key name>)`:
* `(on-idle <idle time> <action> <virtual key name>)`:
Activate a virtual key action when kanata has been idle
for at least `idle time` milliseconds.
* `(hold-for-duration <hold time> <virtual key name>`):
Press a virtual key for `hold time` milliseconds.
If `hold-for-duration` retriggered on a virtual key before release,
the time will be reset with no additional press/release events.

The `<action>` parameter can be one of:

Expand Down Expand Up @@ -3284,10 +3288,11 @@ will not yet be counting even if you no longer have any keyboard keys pressed.
mac (on-press tap-vkey vkmacro)
isf (on-idle 1000 tap-vkey sft)
hfd (hold-for-duration 1000 met)
)
(deflayer use-virtual-keys
@psf @rsf @tal @mac a s d f @isf
@psf @rsf @tal @mac a s d f @isf @hfd
)
----

Expand Down
19 changes: 19 additions & 0 deletions parser/src/cfg/fake_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,22 @@ pub(crate) fn parse_on_idle(ac_params: &[SExpr], s: &ParserState) -> Result<&'st
}),
)))))
}

pub(crate) fn parse_hold_for_duration(
ac_params: &[SExpr],
s: &ParserState,
) -> Result<&'static KanataAction> {
const ERR_MSG: &str = "hold-for-duration expects two parameters: <hold-duration> <key-name>";
if ac_params.len() != 2 {
bail!("{ERR_MSG}");
}
let hold_duration = parse_non_zero_u16(&ac_params[0], s, "hold-duration")?;
let coord = parse_vkey_coord(&ac_params[1], s)?;

Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice(
CustomAction::FakeKeyHoldForDuration(FakeKeyHoldForDuration {
coord,
hold_duration,
}),
)))))
}
2 changes: 2 additions & 0 deletions parser/src/cfg/list_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub const ON_PRESS_A: &str = "on↓";
pub const ON_RELEASE: &str = "on-release";
pub const ON_RELEASE_A: &str = "on↑";
pub const ON_IDLE: &str = "on-idle";
pub const HOLD_FOR_DURATION: &str = "hold-for-duration";

pub fn is_list_action(ac: &str) -> bool {
const LIST_ACTIONS: &[&str] = &[
Expand Down Expand Up @@ -227,6 +228,7 @@ pub fn is_list_action(ac: &str) -> bool {
ON_RELEASE,
ON_RELEASE_A,
ON_IDLE,
HOLD_FOR_DURATION,
MACRO_CANCEL_ON_NEXT_PRESS,
MACRO_REPEAT_CANCEL_ON_NEXT_PRESS,
MACRO_CANCEL_ON_NEXT_PRESS_CANCEL_ON_RELEASE,
Expand Down
1 change: 1 addition & 0 deletions parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParserState) -> Result<&'static KanataAct
ON_PRESS | ON_PRESS_A => parse_on_press(&ac[1..], s),
ON_RELEASE | ON_RELEASE_A => parse_on_release(&ac[1..], s),
ON_IDLE => parse_on_idle(&ac[1..], s),
HOLD_FOR_DURATION => parse_hold_for_duration(&ac[1..], s),
MWHEEL_UP | MWHEEL_UP_A => parse_mwheel(&ac[1..], MWheelDirection::Up, s),
MWHEEL_DOWN | MWHEEL_DOWN_A => parse_mwheel(&ac[1..], MWheelDirection::Down, s),
MWHEEL_LEFT | MWHEEL_LEFT_A => parse_mwheel(&ac[1..], MWheelDirection::Left, s),
Expand Down
9 changes: 9 additions & 0 deletions parser/src/custom_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum CustomAction {
action: FakeKeyAction,
},
FakeKeyOnIdle(FakeKeyOnIdle),
FakeKeyHoldForDuration(FakeKeyHoldForDuration),
Delay(u16),
DelayOnRelease(u16),
MWheel {
Expand Down Expand Up @@ -127,6 +128,14 @@ pub struct FakeKeyOnIdle {
pub idle_duration: u16,
}

/// Information for an action that presses a fake key / vkey that becomes released on a
/// renewable-when-reactivated deadline.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct FakeKeyHoldForDuration {
pub coord: Coord,
pub hold_duration: u16,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MWheelDirection {
Up,
Expand Down
36 changes: 36 additions & 0 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ pub struct Kanata {
pub device_detect_mode: DeviceDetectMode,
/// Fake key actions that are waiting for a certain duration of keyboard idling.
pub waiting_for_idle: HashSet<FakeKeyOnIdle>,
/// Fake key actions that are being held and are pending release.
/// The key is the coordinate and the value is the number of ticks until release should be
/// done.
pub vkeys_pending_release: HashMap<Coord, u16>,
/// Number of ticks since kanata was idle.
pub ticks_since_idle: u16,
/// If a mousemove action is active and another mousemove action is activated,
Expand Down Expand Up @@ -411,6 +415,7 @@ impl Kanata {
.linux_device_detect_mode
.expect("parser should default to some"),
waiting_for_idle: HashSet::default(),
vkeys_pending_release: HashMap::default(),
ticks_since_idle: 0,
movemouse_buffer: None,
unmodded_keys: vec![],
Expand Down Expand Up @@ -541,6 +546,7 @@ impl Kanata {
.linux_device_detect_mode
.expect("parser should default to some"),
waiting_for_idle: HashSet::default(),
vkeys_pending_release: HashMap::default(),
ticks_since_idle: 0,
movemouse_buffer: None,
unmodded_keys: vec![],
Expand Down Expand Up @@ -796,6 +802,23 @@ impl Kanata {
Ok(())
}

fn tick_held_vkeys(&mut self) {
if self.vkeys_pending_release.is_empty() {
return;
}
let layout = self.layout.bm();
self.vkeys_pending_release.retain(|coord, deadline| {
*deadline = deadline.saturating_sub(1);
match deadline {
0 => {
layout.event(Event::Release(coord.x, coord.y));
false
}
_ => true,
}
});
}

fn tick_states(&mut self, _tx: &Option<Sender<ServerMessage>>) -> Result<()> {
self.live_reload_requested |= self.handle_keystate_changes(_tx)?;
self.handle_scrolling()?;
Expand All @@ -807,6 +830,7 @@ impl Kanata {
zippy_tick(self.caps_word.is_some());
self.prev_keys.clear();
self.prev_keys.append(&mut self.cur_keys);
self.tick_held_vkeys();
#[cfg(feature = "simulated_output")]
{
self.kbd_out.tick();
Expand Down Expand Up @@ -1557,6 +1581,17 @@ impl Kanata {
self.ticks_since_idle = 0;
self.waiting_for_idle.insert(*fkd);
}
CustomAction::FakeKeyHoldForDuration(fk_hfd) => {
let x = fk_hfd.coord.x;
let y = fk_hfd.coord.y;
let duration = fk_hfd.hold_duration;
self.vkeys_pending_release.entry(fk_hfd.coord)
.and_modify(|d| *d = duration)
.or_insert_with(|| {
layout.event(Event::Press(x, y));
duration
});
}
CustomAction::FakeKeyOnRelease { .. }
| CustomAction::DelayOnRelease(_)
| CustomAction::Unmodded { .. }
Expand Down Expand Up @@ -2072,6 +2107,7 @@ impl Kanata {
&& self.move_mouse_state_horizontal.is_none()
&& self.dynamic_macro_replay_state.is_none()
&& self.caps_word.is_none()
&& self.vkeys_pending_release.is_empty()
&& !self.layout.b().states.iter().any(|s| {
matches!(s, State::SeqCustomPending(_) | State::SeqCustomActive(_))
|| (pressed_keys_means_not_idle && matches!(s, State::NormalKey { .. }))
Expand Down
1 change: 1 addition & 0 deletions src/tests/sim_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod switch_sim_tests;
mod unicode_sim_tests;
mod unmod_sim_tests;
mod use_defsrc_sim_tests;
mod vkey_sim_tests;
mod zippychord_sim_tests;

fn simulate<S: AsRef<str>>(cfg: S, sim: S) -> String {
Expand Down
25 changes: 25 additions & 0 deletions src/tests/sim_tests/vkey_sim_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use super::*;

const CFG: &str = r"
(defsrc a b c)
(defvirtualkeys lmet lmet)
(defalias hm (hold-for-duration 50 lmet))
(deflayer base
(multi @hm (macro-repeat 40 @hm))
(multi 1 @hm)
(release-key lmet)
)
";

#[test]
fn hold_for_duration() {
let result = simulate(CFG, "d:a t:200 u:a t:60").to_ascii();
assert_eq!("t:1ms dn:LGui t:258ms up:LGui", result);
let result = simulate(CFG, "d:a u:a t:25 d:c u:c t:25").to_ascii();
assert_eq!("t:2ms dn:LGui t:23ms up:LGui", result);
let result = simulate(CFG, "d:a u:a t:25 d:b u:b t:25 d:b u:b t:60").to_ascii();
assert_eq!(
"t:2ms dn:LGui t:23ms dn:Kb1 t:1ms up:Kb1 t:24ms dn:Kb1 t:1ms up:Kb1 t:49ms up:LGui",
result
);
}

0 comments on commit fc1b95d

Please sign in to comment.