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 states after ~65.5s of no external input but still not idle.

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 the abnormality of zero
user inputs for an extended period of time while still not being idle
and assume that keys got stuck.
  • Loading branch information
jtroo committed Aug 11, 2023
1 parent 3ccbcd1 commit e8f55d6
Showing 1 changed file with 46 additions and 8 deletions.
54 changes: 46 additions & 8 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ pub struct Kanata {
pub waiting_for_idle: HashSet<FakeKeyOnIdle>,
/// Number of ticks since kanata was idle.
pub ticks_since_idle: u16,

/// Number of ticks since the last input was sent to kanata.
/// This is only used with the LLHOOK version of kanata. If doing anything that will make
/// kanata stop receiving inputs while keys are pressed, e.g. locking the screen with Win+L or
/// Alt+Tab-ing to an administrator-privileged terminal, there will be keys that end up in a
/// stuck state and mess with the idle state. If this hits the max value and kanata is still
/// not idle, assuming there are stuck keys and clear normal key states.
#[cfg(all(not(feature = "interception_driver"), target_os = "windows"))]
pub ticks_since_last_input: u16,
}

pub struct ScrollState {
Expand Down Expand Up @@ -349,6 +358,8 @@ impl Kanata {
defcfg_items: cfg.items,
waiting_for_idle: HashSet::default(),
ticks_since_idle: 0,
#[cfg(all(not(feature = "interception_driver"), target_os = "windows"))]
ticks_since_last_input: 0,
})
}

Expand Down Expand Up @@ -391,6 +402,10 @@ impl Kanata {

/// Update keyberon layout state for press/release, handle repeat separately
fn handle_key_event(&mut self, event: &KeyEvent) -> Result<()> {
#[cfg(all(not(feature = "interception_driver"), target_os = "windows"))]
{
self.ticks_since_last_input = 0;
}
log::debug!("process recv ev {event:?}");
let evc: u16 = event.code.into();
self.ticks_since_idle = 0;
Expand Down Expand Up @@ -436,13 +451,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 +473,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 @@ -1459,11 +1486,22 @@ impl Kanata {
let err = loop {
let can_block = {
let mut k = kanata.lock();
#[cfg(all(not(feature = "interception_driver"), target_os = "windows"))]
{
k.ticks_since_last_input =
k.ticks_since_last_input.saturating_add(ms_elapsed);
if k.ticks_since_last_input >= u16::MAX {
// For more explanation, see the definition of ticks_since_last_input.
log::warn!("Removing all states - detected possibility of stuck keys");
k.layout.bm().states().clear()
}
}
let is_idle = k.is_idle();
// 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 Down

0 comments on commit e8f55d6

Please sign in to comment.