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(vkeys): add hold-for-duration action #1355

Merged
merged 5 commits into from
Nov 16, 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
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
);
}
Loading