From 2745257ce9e8413d0e845de2e167815561dcab5a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 12 Nov 2021 20:49:56 +0100 Subject: [PATCH] fix(detect): add workaround to fix inconsistent modifier states on macOS. Fix #825 Fix #858 --- espanso-detect/src/mac/mod.rs | 96 +++++++++++++++++++++++++++++++- espanso-detect/src/mac/native.h | 10 ++++ espanso-detect/src/mac/native.mm | 27 +++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs index 5b93558..5f49ea0 100644 --- a/espanso-detect/src/mac/mod.rs +++ b/espanso-detect/src/mac/mod.rs @@ -32,7 +32,7 @@ use log::{error, trace, warn}; use anyhow::Result; use thiserror::Error; -use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Variant}; +use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Status, Variant}; use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::{event::Status::*, Source, SourceCallback}; use crate::{event::Variant::*, hotkey::HotKey}; @@ -50,6 +50,7 @@ const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3; // Take a look at the native.h header file for an explanation of the fields #[repr(C)] +#[derive(Debug)] pub struct RawInputEvent { pub event_type: i32, @@ -58,6 +59,12 @@ pub struct RawInputEvent { pub key_code: i32, pub status: i32, + + pub is_caps_lock_pressed: i32, + pub is_shift_pressed: i32, + pub is_control_pressed: i32, + pub is_option_pressed: i32, + pub is_command_pressed: i32, } #[repr(C)] @@ -82,15 +89,97 @@ extern "C" { ); } +#[derive(Debug, Default)] +struct ModifierState { + is_ctrl_down: bool, + is_shift_down: bool, + is_command_down: bool, + is_option_down: bool, +} + lazy_static! { static ref CURRENT_SENDER: Arc>>> = Arc::new(Mutex::new(None)); + static ref MODIFIER_STATE: Arc> = + Arc::new(Mutex::new(ModifierState::default())); } extern "C" fn native_callback(raw_event: RawInputEvent) { let lock = CURRENT_SENDER .lock() .expect("unable to acquire CocoaSource sender lock"); + + // Most of the times, when pressing a modifier key (such as Alt, Ctrl, Shift, Cmd), + // we get both a Pressed and Released event. This is important to keep Espanso's + // internal representation of modifiers in sync. + // Unfortunately, there are times when the corresponding "release" event is not sent, + // and this causes Espanso to mistakenly think that the modifier is still pressed. + // This can happen for various reasons, such as when using external bluetooth keyboards + // or certain keyboard shortcuts. + // Luckily, most key events include the "modifiers flag" information, that tells us which + // modifier keys were currently pressed at that time. + // We use this modifier flag information to detect "inconsistent" states to send the corresponding + // modifier release events, keeping espanso's state in sync. + // For more info, see: + // https://github.com/federico-terzi/espanso/issues/825 + // https://github.com/federico-terzi/espanso/issues/858 + let mut compensating_events = Vec::new(); + if raw_event.event_type == INPUT_EVENT_TYPE_KEYBOARD { + let (key_code, _) = key_code_to_key(raw_event.key_code); + let mut current_mod_state = MODIFIER_STATE + .lock() + .expect("unable to acquire modifier state in cocoa detector"); + + if let Key::Alt = &key_code { + current_mod_state.is_option_down = raw_event.status == INPUT_STATUS_PRESSED; + } else if let Key::Meta = &key_code { + current_mod_state.is_command_down = raw_event.status == INPUT_STATUS_PRESSED; + } else if let Key::Shift = &key_code { + current_mod_state.is_shift_down = raw_event.status == INPUT_STATUS_PRESSED; + } else if let Key::Control = &key_code { + current_mod_state.is_ctrl_down = raw_event.status == INPUT_STATUS_PRESSED; + } else { + if current_mod_state.is_command_down && raw_event.is_command_pressed == 0 { + compensating_events.push((Key::Meta, 0x37)); + current_mod_state.is_command_down = false; + } + if current_mod_state.is_ctrl_down && raw_event.is_control_pressed == 0 { + compensating_events.push((Key::Control, 0x3B)); + current_mod_state.is_ctrl_down = false; + } + if current_mod_state.is_shift_down && raw_event.is_shift_pressed == 0 { + compensating_events.push((Key::Shift, 0x38)); + current_mod_state.is_shift_down = false; + } + if current_mod_state.is_option_down && raw_event.is_option_pressed == 0 { + compensating_events.push((Key::Alt, 0x3A)); + current_mod_state.is_option_down = false; + } + } + } + + if !compensating_events.is_empty() { + warn!( + "detected inconsistent modifier state for keys {:?}, sending compensating events...", + compensating_events + ); + } + if let Some(sender) = lock.as_ref() { + for (key, code) in compensating_events { + if let Err(error) = sender.send(InputEvent::Keyboard(KeyboardEvent { + key, + value: None, + status: Status::Released, + variant: None, + code, + })) { + error!( + "Unable to send compensating event to Cocoa Sender: {}", + error + ); + } + } + let event: Option = raw_event.into(); if let Some(event) = event { if let Err(error) = sender.send(event) { @@ -386,6 +475,11 @@ mod tests { buffer_len: 0, key_code: 0, status: INPUT_STATUS_PRESSED, + is_caps_lock_pressed: 0, + is_shift_pressed: 0, + is_control_pressed: 0, + is_option_pressed: 0, + is_command_pressed: 0, } } diff --git a/espanso-detect/src/mac/native.h b/espanso-detect/src/mac/native.h index a86d943..50836e6 100644 --- a/espanso-detect/src/mac/native.h +++ b/espanso-detect/src/mac/native.h @@ -52,6 +52,16 @@ typedef struct { // Pressed or Released status int32_t status; + + // Modifier keys status, this is needed to "correct" missing modifier release events. + // For more info, see the following issues: + // https://github.com/federico-terzi/espanso/issues/825 + // https://github.com/federico-terzi/espanso/issues/858 + int32_t is_caps_lock_pressed; + int32_t is_shift_pressed; + int32_t is_control_pressed; + int32_t is_option_pressed; + int32_t is_command_pressed; } InputEvent; typedef void (*EventCallback)(InputEvent data); diff --git a/espanso-detect/src/mac/native.mm b/espanso-detect/src/mac/native.mm index 6eb5e9a..b96d7ac 100644 --- a/espanso-detect/src/mac/native.mm +++ b/espanso-detect/src/mac/native.mm @@ -76,6 +76,19 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) { strncpy(inputEvent.buffer, chars, 23); inputEvent.buffer_len = event.characters.length; + // We also send the modifier key status to "correct" missing modifier release events + if (([event modifierFlags] & NSEventModifierFlagShift) != 0) { + inputEvent.is_shift_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) { + inputEvent.is_control_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) { + inputEvent.is_caps_lock_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) { + inputEvent.is_option_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) { + inputEvent.is_command_pressed = 1; + } + callback(inputEvent); }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown || event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) { @@ -106,6 +119,20 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) { } else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) { inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED; } + + // We also send the modifier key status to "correct" missing modifier release events + if (([event modifierFlags] & NSEventModifierFlagShift) != 0) { + inputEvent.is_shift_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) { + inputEvent.is_control_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) { + inputEvent.is_caps_lock_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) { + inputEvent.is_option_pressed = 1; + } else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) { + inputEvent.is_command_pressed = 1; + } + callback(inputEvent); } }];