Skip to content

Commit

Permalink
feat: better handling of Windows LLHOOK stuck keys
Browse files Browse the repository at this point in the history
There are two changes in this commit.

1) Always live reload after ~1s.

After 1 second if live reload is still not done, there might be a key in
a stuck state. One known instance where this happens is Win+L to lock
the screen when on Windows and using the LLHOOK mechanism. The release
of Win and L keys will not be caught by the kanata process when on the
lock screen. However, the OS knows that these keys have released - only
the kanata state is wrong. And since kanata has a key in a stuck state,
without this 1s fallback, live reload would never activate. Having this
fallback allows live reload to happen which resets the kanata states.

2) Clear normal states (not fake key) after blocking for a while

This code only runs in the LLHOOK Windows version. The reason for this
existing is the same as in 1). The main motivator is the Win+L example.
The thought is that after locking their screen, the user will probably
go away for a while. The software can detect that the zero user inputs
were input for extended period of time and assume that keys got stuck.
  • Loading branch information
jtroo committed Aug 12, 2023
1 parent 3ccbcd1 commit abb6c3f
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 10 deletions.
5 changes: 4 additions & 1 deletion parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2274,9 +2274,12 @@ fn parse_fake_key_op_coord_action(
Ok((Coord { x, y }, action))
}

pub const NORMAL_KEY_ROW: u8 = 0;
pub const FAKE_KEY_ROW: u8 = 1;

fn get_fake_key_coords<T: Into<usize>>(y: T) -> (u8, u16) {
let y: usize = y.into();
(1, y as u16)
(FAKE_KEY_ROW, y as u16)
}

fn parse_fake_key_delay(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> {
Expand Down
75 changes: 66 additions & 9 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,6 @@ impl Kanata {
self.tick_dynamic_macro_state()?;
self.tick_idle_timeout();

if self.live_reload_requested && self.prev_keys.is_empty() && self.cur_keys.is_empty() {
self.live_reload_requested = false;
if let Err(e) = self.do_live_reload() {
log::error!("live reload failed {e}");
}
}

self.prev_keys.clear();
self.prev_keys.append(&mut self.cur_keys);
}
Expand All @@ -465,6 +458,25 @@ impl Kanata {
self.check_handle_layer_change(tx);
}

if self.live_reload_requested
&& ((self.prev_keys.is_empty() && self.cur_keys.is_empty())
|| self.ticks_since_idle > 1000)
{
// Note regarding the ticks_since_idle check above:
// After 1 second if live reload is still not done, there might be a key in a stuck
// state. One known instance where this happens is Win+L to lock the screen in
// Windows with the LLHOOK mechanism. The release of Win and L keys will not be
// caught by the kanata process when on the lock screen. However, the OS knows that
// these keys have released - only the kanata state is wrong. And since kanata has
// a key in a stuck state, without this 1s fallback, live reload would never
// activate. Having this fallback allows live reload to happen which resets the
// kanata states.
self.live_reload_requested = false;
if let Err(e) = self.do_live_reload() {
log::error!("live reload failed {e}");
}
}

#[cfg(feature = "perf_logging")]
log::info!("ms elapsed: {ms_elapsed}");
// Note regarding `as` casting. It doesn't really matter if the result would truncate and
Expand Down Expand Up @@ -1463,7 +1475,8 @@ impl Kanata {
// Note: checking waiting_for_idle can not be part of the computation for
// is_idle() since incrementing ticks_since_idle is dependent on the return
// value of is_idle().
let counting_idle_ticks = !k.waiting_for_idle.is_empty();
let counting_idle_ticks =
!k.waiting_for_idle.is_empty() || k.live_reload_requested;
if !is_idle {
k.ticks_since_idle = 0;
} else if is_idle && counting_idle_ticks {
Expand All @@ -1478,9 +1491,53 @@ impl Kanata {
match rx.recv() {
Ok(kev) => {
let mut k = kanata.lock();
k.last_tick = time::Instant::now()
let now = time::Instant::now()
.checked_sub(time::Duration::from_millis(1))
.expect("subtract 1ms from current time");
#[cfg(all(
not(feature = "interception_driver"),
target_os = "windows"
))]
{
// If kanata has been blocking for long enough, clear all states.
// This won't trigger if there are macros running, or if a key is
// held down for a long time and is sending OS repeats. The reason
// for this code is in case like Win+L which locks the Windows
// desktop. When this happens, the Win key and L key will be stuck
// as pressed in the kanata state because LLHOOK kanata cannot read
// keys in the lock screen or administrator applications. So this
// is heuristic to detect such an issue and clear states assuming
// that's what happened.
//
// Only states in the normal key row are cleared, since those are
// the states that might be stuck. A real use case might be to have
// a fake key pressed for a long period of time, so make sure those
// are not cleared.
if (now - k.last_tick) > time::Duration::from_secs(60) {
log::debug!(
"clearing keyberon normal key states due to blocking for a while"
);
k.layout.bm().states.retain(|s| {
!matches!(
s,
State::NormalKey {
coord: (NORMAL_KEY_ROW, _),
..
} | State::LayerModifier {
coord: (NORMAL_KEY_ROW, _),
..
} | State::Custom {
coord: (NORMAL_KEY_ROW, _),
..
} | State::RepeatingSequence {
coord: (NORMAL_KEY_ROW, _),
..
}
)
});
}
}
k.last_tick = now;

#[cfg(feature = "perf_logging")]
let start = std::time::Instant::now();
Expand Down

0 comments on commit abb6c3f

Please sign in to comment.