diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs index 61763f3..df4ab9a 100644 --- a/espanso-detect/src/evdev/device.rs +++ b/espanso-detect/src/evdev/device.rs @@ -36,6 +36,7 @@ pub enum RawInputEvent { #[derive(Debug)] pub struct RawKeyboardEvent { pub sym: u32, + pub code: u32, pub value: String, pub state: i32, } @@ -171,6 +172,7 @@ impl Device { let event = RawKeyboardEvent { state: value, + code: keycode, sym, value: content, }; diff --git a/espanso-detect/src/evdev/ffi.rs b/espanso-detect/src/evdev/ffi.rs index 31bf665..e242ca5 100644 --- a/espanso-detect/src/evdev/ffi.rs +++ b/espanso-detect/src/evdev/ffi.rs @@ -43,8 +43,6 @@ pub type xkb_state_component = u32; pub const EV_KEY: u16 = 0x01; -pub const XKB_KEYCODE_INVALID: u32 = 0xffffffff; - #[link(name = "xkbcommon")] extern "C" { pub fn xkb_state_unref(state: *mut xkb_state); @@ -59,7 +57,6 @@ extern "C" { pub fn xkb_context_unref(context: *mut xkb_context); pub fn xkb_state_get_keymap(state: *mut xkb_state) -> *mut xkb_keymap; pub fn xkb_keymap_key_repeats(keymap: *mut xkb_keymap, key: xkb_keycode_t) -> c_int; - pub fn xkb_keymap_key_by_name(keymap: *mut xkb_keymap, name: *const c_char) -> u32; pub fn xkb_state_update_key( state: *mut xkb_state, key: xkb_keycode_t, diff --git a/espanso-detect/src/evdev/hotkey.rs b/espanso-detect/src/evdev/hotkey.rs index 2549efe..b7b2a1f 100644 --- a/espanso-detect/src/evdev/hotkey.rs +++ b/espanso-detect/src/evdev/hotkey.rs @@ -17,52 +17,128 @@ * along with espanso. If not, see . */ -use crate::{event::KeyboardEvent, hotkey::HotKey}; -use std::{ - collections::{HashMap, VecDeque}, - time::Instant, +use log::error; + +use crate::{event::{KeyboardEvent, Status}, hotkey::HotKey}; +use std::{collections::HashMap, time::Instant}; + +use super::{ + state::State, }; -use super::state::State; - -// Number of milliseconds between the first and last codes of an hotkey -// to be considered valid -const HOTKEY_WINDOW: i32 = 5000; +// Number of milliseconds that define how long the hotkey memory +// should retain pressed keys +const HOTKEY_WINDOW_TIMEOUT: u128 = 5000; pub type KeySym = u32; pub type KeyCode = u32; -pub type SymMap = HashMap; +pub type HotkeyMemoryMap = Vec<(KeyCode, Instant)>; -pub type HotkeyMemoryMap = VecDeque<(KeyCode, Instant)>; +pub struct HotKeyFilter { + map: HashMap, + memory: HotkeyMemoryMap, + hotkey_raw_map: HashMap>, +} -pub fn generate_sym_map(state: &State) -> HashMap { - let mut map = HashMap::new(); - for code in 0..256 { - if let Some(sym) = state.get_sym(code) { - map.insert(sym, code); +impl HotKeyFilter { + pub fn new() -> Self { + Self { + map: HashMap::new(), + memory: HotkeyMemoryMap::new(), + hotkey_raw_map: HashMap::new(), } } - map -} -pub fn convert_hotkey_to_codes(hk: &HotKey, map: &SymMap) -> Option> { - let mut codes = Vec::new(); - let key_code = map.get(&hk.key.to_code()?)?; - codes.push(*key_code); + pub fn initialize(&mut self, state: &State, hotkeys: &[HotKey]) { + // First load the map + self.map = HashMap::new(); + for code in 0..256 { + if let Some(sym) = state.get_sym(code) { + self.map.insert(sym, code); + } + } - for modifier in hk.modifiers.iter() { - let code = map.get(&modifier.to_code()?)?; - codes.push(*code); + // Then the actual hotkeys + self.hotkey_raw_map = hotkeys + .iter() + .filter_map(|hk| { + let codes = Self::convert_hotkey_to_codes(self, hk); + if codes.is_none() { + error!("unable to register hotkey {:?}", hk); + } + Some((hk.id, codes?)) + }) + .collect(); } - Some(codes) -} + pub fn process_event( + &mut self, + event: &KeyboardEvent, + ) -> Option { + let mut hotkey = None; + let mut key_code = None; -pub fn detect_hotkey( - event: &KeyboardEvent, - memory: &mut HotkeyMemoryMap, - hotkeys: &HashMap>, -) -> Option { - // TODO: implement the actual matching mechanism - // We need to "clean" the old entries + let mut to_be_removed = Vec::new(); + + if event.status == Status::Released { + // Remove from the memory all the key occurrences + to_be_removed.extend(self.memory.iter().enumerate().filter_map(|(i, (code, _))| { + if *code == event.code { + Some(i) + } else { + None + } + })); + } else { + key_code = Some(event.code) + } + + // Remove the old entries + to_be_removed.extend(self.memory.iter().enumerate().filter_map(|(i, (_, instant))| { + if instant.elapsed().as_millis() > HOTKEY_WINDOW_TIMEOUT { + Some(i) + } else { + None + } + })); + + // Remove duplicates and revert + if !to_be_removed.is_empty() { + to_be_removed.sort(); + to_be_removed.dedup(); + to_be_removed.reverse(); + to_be_removed.into_iter().for_each(|index| { + self.memory.remove(index); + }) + } + + if let Some(code) = key_code { + self.memory.push((code, Instant::now())); + + for (id, codes) in self.hotkey_raw_map.iter() { + if codes + .iter() + .all(|hk_code| self.memory.iter().any(|(m_code, _)| m_code == hk_code)) + { + hotkey = Some(*id); + break; + } + } + } + + hotkey + } + + fn convert_hotkey_to_codes(&self, hk: &HotKey) -> Option> { + let mut codes = Vec::new(); + let key_code = self.map.get(&hk.key.to_code()?)?; + codes.push(*key_code); + + for modifier in hk.modifiers.iter() { + let code = self.map.get(&modifier.to_code()?)?; + codes.push(*code); + } + + Some(codes) + } } diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs index 01550c5..392a764 100644 --- a/espanso-detect/src/evdev/mod.rs +++ b/espanso-detect/src/evdev/mod.rs @@ -23,16 +23,15 @@ mod context; mod device; mod ffi; -mod keymap; mod hotkey; +mod keymap; mod state; -use std::collections::HashMap; +use std::{cell::RefCell}; use anyhow::Result; use context::Context; use device::{get_devices, Device}; -use hotkey::HotkeyMemoryMap; use keymap::Keymap; use lazycell::LazyCell; use libc::{ @@ -41,12 +40,12 @@ use libc::{ use log::{error, trace}; use thiserror::Error; -use crate::{event::Variant::*, hotkey::HotKey}; use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::{event::Status::*, KeyboardConfig, Source, SourceCallback, SourceCreationOptions}; +use crate::{event::Variant::*, event::HotKeyEvent, hotkey::HotKey}; -use self::{device::{DeviceError, KEY_STATE_PRESS, KEY_STATE_RELEASE, RawInputEvent}, hotkey::{KeyCode, convert_hotkey_to_codes, generate_sym_map}, state::State}; +use self::{device::{DeviceError, RawInputEvent, KEY_STATE_PRESS, KEY_STATE_RELEASE}, hotkey::{HotKeyFilter}, state::State}; const BTN_LEFT: u16 = 0x110; const BTN_RIGHT: u16 = 0x111; @@ -61,7 +60,7 @@ pub struct EVDEVSource { _keyboard_rmlvo: Option, _context: LazyCell, _keymap: LazyCell, - _hotkey_codes: HashMap>, + _hotkey_filter: RefCell, } #[allow(clippy::new_without_default)] @@ -73,7 +72,7 @@ impl EVDEVSource { _context: LazyCell::new(), _keymap: LazyCell::new(), _keyboard_rmlvo: options.evdev_keyboard_rmlvo, - _hotkey_codes: HashMap::new(), + _hotkey_filter: RefCell::new(HotKeyFilter::new()), } } } @@ -102,14 +101,7 @@ impl Source for EVDEVSource { // Initialize the hotkeys let state = State::new(&keymap)?; - let sym_map = generate_sym_map(&state); - self._hotkey_codes = self.hotkeys.iter().filter_map(|hk| { - let codes = convert_hotkey_to_codes(hk, &sym_map); - if codes.is_none() { - error!("unable to register hotkey {:?}", hk); - } - Some((hk.id, codes?)) - }).collect(); + self._hotkey_filter.borrow_mut().initialize(&state, &self.hotkeys); if self._context.fill(context).is_err() { return Err(EVDEVSourceError::InitFailure().into()); @@ -153,7 +145,7 @@ impl Source for EVDEVSource { } } - let mut hotkey_memory = HotkeyMemoryMap::new(); + let mut hotkey_filter = self._hotkey_filter.borrow_mut(); // Read events indefinitely let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() }; @@ -176,8 +168,14 @@ impl Source for EVDEVSource { events.into_iter().for_each(|raw_event| { let event: Option = raw_event.into(); if let Some(event) = event { - // First process the hotkey - + // On Wayland we need to detect the global shortcuts manually + if let InputEvent::Keyboard(key_event) = &event { + if let Some(hotkey) = (*hotkey_filter).process_event(&key_event) { + event_callback(InputEvent::HotKey(HotKeyEvent { + hotkey_id: hotkey, + })) + } + } event_callback(event); } else { @@ -233,6 +231,7 @@ impl From for Option { value, status, variant, + code: keyboard_event.code, })); } RawInputEvent::Mouse(mouse_event) => { @@ -345,7 +344,8 @@ mod tests { let raw = RawInputEvent::Keyboard(RawKeyboardEvent { sym: 0x4B, value: "k".to_owned(), - is_down: false, + state: KEY_STATE_RELEASE, + code: 0, }); let result: Option = raw.into(); @@ -356,6 +356,7 @@ mod tests { status: Released, value: Some("k".to_string()), variant: None, + code: 0, }) ); } diff --git a/espanso-detect/src/evdev/state.rs b/espanso-detect/src/evdev/state.rs index 5de799b..9dcb843 100644 --- a/espanso-detect/src/evdev/state.rs +++ b/espanso-detect/src/evdev/state.rs @@ -1,8 +1,6 @@ // This code is a port of the libxkbcommon "interactive-evdev.c" example // https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c -use std::ffi::CStr; - use scopeguard::ScopeGuard; use anyhow::Result; diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs index a1135cf..98412c4 100644 --- a/espanso-detect/src/event.rs +++ b/espanso-detect/src/event.rs @@ -63,6 +63,7 @@ pub struct KeyboardEvent { pub value: Option, pub status: Status, pub variant: Option, + pub code: u32, } // A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs index ab997ee..979ce5a 100644 --- a/espanso-detect/src/mac/mod.rs +++ b/espanso-detect/src/mac/mod.rs @@ -265,6 +265,7 @@ impl From for Option { value, status, variant, + code: raw.key_code.try_into().expect("unable to convert keycode to u32"), })); } // Mouse events diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs index 0680cd6..18b413d 100644 --- a/espanso-detect/src/win32/mod.rs +++ b/espanso-detect/src/win32/mod.rs @@ -274,6 +274,7 @@ impl From for Option { value, status, variant, + code: raw.key_code.try_into().expect("unable to convert keycode to u32"), })); } // Mouse events diff --git a/espanso-detect/src/x11/mod.rs b/espanso-detect/src/x11/mod.rs index a147938..a5887d6 100644 --- a/espanso-detect/src/x11/mod.rs +++ b/espanso-detect/src/x11/mod.rs @@ -17,10 +17,7 @@ * along with espanso. If not, see . */ -use std::{ - collections::HashMap, - ffi::{c_void, CStr}, -}; +use std::{collections::HashMap, convert::TryInto, ffi::{c_void, CStr}}; use lazycell::LazyCell; use log::{debug, error, trace, warn}; @@ -309,6 +306,7 @@ fn convert_raw_input_event_to_input_event( value, status, variant, + code: raw.key_code.try_into().expect("invalid keycode conversion to u32"), })); } // Mouse events @@ -436,6 +434,7 @@ mod tests { raw.buffer_len = 1; raw.status = INPUT_STATUS_RELEASED; raw.key_sym = 0x4B; + raw.key_code = 1; let result: Option = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); assert_eq!( @@ -445,6 +444,7 @@ mod tests { status: Released, value: Some("k".to_string()), variant: None, + code: 1, }) ); }