From fbeca8b6e984c009e567b3f87d5fe7fdd3d22967 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 14 Mar 2021 21:53:17 +0100 Subject: [PATCH] Implement hotkeys handling on X11 --- espanso-detect/src/hotkey/keys.rs | 90 +++++++++++- espanso-detect/src/lib.rs | 4 +- espanso-detect/src/mac/native.h | 2 +- espanso-detect/src/win32/native.h | 2 +- espanso-detect/src/x11/mod.rs | 236 ++++++++++++++++++++++-------- espanso-detect/src/x11/native.cpp | 89 +++++++++++ espanso-detect/src/x11/native.h | 35 ++++- 7 files changed, 390 insertions(+), 68 deletions(-) diff --git a/espanso-detect/src/hotkey/keys.rs b/espanso-detect/src/hotkey/keys.rs index f430cfa..fa1ccf7 100644 --- a/espanso-detect/src/hotkey/keys.rs +++ b/espanso-detect/src/hotkey/keys.rs @@ -502,9 +502,97 @@ impl ShortcutKey { Some(vkey) } + // Linux mappings + // NOTE: on linux, this method returns the KeySym and not the KeyCode + // which should be obtained in other ways depending on the backend. + // (X11 or Wayland) #[cfg(target_os = "linux")] pub fn to_code(&self) -> Option { - None // Not supported on Linux + match self { + ShortcutKey::Alt => Some(0xFFE9), + ShortcutKey::Control => Some(0xFFE3), + ShortcutKey::Meta => Some(0xFFEB), + ShortcutKey::Shift => Some(0xFFE1), + ShortcutKey::Enter => Some(0xFF0D), + ShortcutKey::Tab => Some(0xFF09), + ShortcutKey::Space => Some(0x20), + ShortcutKey::ArrowDown => Some(0xFF54), + ShortcutKey::ArrowLeft => Some(0xFF51), + ShortcutKey::ArrowRight => Some(0xFF53), + ShortcutKey::ArrowUp => Some(0xFF52), + ShortcutKey::End => Some(0xFF57), + ShortcutKey::Home => Some(0xFF50), + ShortcutKey::PageDown => Some(0xFF56), + ShortcutKey::PageUp => Some(0xFF55), + ShortcutKey::Insert => Some(0xff63), + ShortcutKey::F1 => Some(0xFFBE), + ShortcutKey::F2 => Some(0xFFBF), + ShortcutKey::F3 => Some(0xFFC0), + ShortcutKey::F4 => Some(0xFFC1), + ShortcutKey::F5 => Some(0xFFC2), + ShortcutKey::F6 => Some(0xFFC3), + ShortcutKey::F7 => Some(0xFFC4), + ShortcutKey::F8 => Some(0xFFC5), + ShortcutKey::F9 => Some(0xFFC6), + ShortcutKey::F10 => Some(0xFFC7), + ShortcutKey::F11 => Some(0xFFC8), + ShortcutKey::F12 => Some(0xFFC9), + ShortcutKey::F13 => Some(0xFFCA), + ShortcutKey::F14 => Some(0xFFCB), + ShortcutKey::F15 => Some(0xFFCC), + ShortcutKey::F16 => Some(0xFFCD), + ShortcutKey::F17 => Some(0xFFCE), + ShortcutKey::F18 => Some(0xFFCF), + ShortcutKey::F19 => Some(0xFFD0), + ShortcutKey::F20 => Some(0xFFD1), + ShortcutKey::A => Some(0x0061), + ShortcutKey::B => Some(0x0062), + ShortcutKey::C => Some(0x0063), + ShortcutKey::D => Some(0x0064), + ShortcutKey::E => Some(0x0065), + ShortcutKey::F => Some(0x0066), + ShortcutKey::G => Some(0x0067), + ShortcutKey::H => Some(0x0068), + ShortcutKey::I => Some(0x0069), + ShortcutKey::J => Some(0x006a), + ShortcutKey::K => Some(0x006b), + ShortcutKey::L => Some(0x006c), + ShortcutKey::M => Some(0x006d), + ShortcutKey::N => Some(0x006e), + ShortcutKey::O => Some(0x006f), + ShortcutKey::P => Some(0x0070), + ShortcutKey::Q => Some(0x0071), + ShortcutKey::R => Some(0x0072), + ShortcutKey::S => Some(0x0073), + ShortcutKey::T => Some(0x0074), + ShortcutKey::U => Some(0x0075), + ShortcutKey::V => Some(0x0076), + ShortcutKey::W => Some(0x0077), + ShortcutKey::X => Some(0x0078), + ShortcutKey::Y => Some(0x0079), + ShortcutKey::Z => Some(0x007a), + ShortcutKey::N0 => Some(0x0030), + ShortcutKey::N1 => Some(0x0031), + ShortcutKey::N2 => Some(0x0032), + ShortcutKey::N3 => Some(0x0033), + ShortcutKey::N4 => Some(0x0034), + ShortcutKey::N5 => Some(0x0035), + ShortcutKey::N6 => Some(0x0036), + ShortcutKey::N7 => Some(0x0037), + ShortcutKey::N8 => Some(0x0038), + ShortcutKey::N9 => Some(0x0039), + ShortcutKey::Numpad0 => Some(0xffb0), + ShortcutKey::Numpad1 => Some(0xffb1), + ShortcutKey::Numpad2 => Some(0xffb2), + ShortcutKey::Numpad3 => Some(0xffb3), + ShortcutKey::Numpad4 => Some(0xffb4), + ShortcutKey::Numpad5 => Some(0xffb5), + ShortcutKey::Numpad6 => Some(0xffb6), + ShortcutKey::Numpad7 => Some(0xffb7), + ShortcutKey::Numpad8 => Some(0xffb8), + ShortcutKey::Numpad9 => Some(0xffb9), + ShortcutKey::Raw(code) => Some(*code as u32), + } } } diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index 117c33b..dcc1d20 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -57,7 +57,7 @@ pub struct SourceCreationOptions { pub evdev_keyboard_rmlvo: Option, // List of global hotkeys the detection module has to register - // NOTE: Hotkeys are ignored on Linux + // NOTE: Hotkeys don't work under the EVDEV backend yet (Wayland) pub hotkeys: Vec, } @@ -103,7 +103,7 @@ pub fn get_source(options: SourceCreationOptions) -> Result> { Ok(Box::new(evdev::EVDEVSource::new(options))) } else { info!("using X11Source"); - Ok(Box::new(x11::X11Source::new())) + Ok(Box::new(x11::X11Source::new(&options.hotkeys))) } } diff --git a/espanso-detect/src/mac/native.h b/espanso-detect/src/mac/native.h index 9be5357..a86d943 100644 --- a/espanso-detect/src/mac/native.h +++ b/espanso-detect/src/mac/native.h @@ -37,7 +37,7 @@ #define INPUT_MOUSE_MIDDLE_BUTTON 3 typedef struct { - // Keyboard or Mouse event + // Keyboard, Mouse or Hotkey event int32_t event_type; // Contains the string corresponding to the key, if any diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h index 6ac73ff..c6e7dee 100644 --- a/espanso-detect/src/win32/native.h +++ b/espanso-detect/src/win32/native.h @@ -42,7 +42,7 @@ #define INPUT_MOUSE_BUTTON_5 8 typedef struct { - // Keyboard or Mouse event + // Keyboard, Mouse or Hotkey event int32_t event_type; // Contains the string corresponding to the key, if any diff --git a/espanso-detect/src/x11/mod.rs b/espanso-detect/src/x11/mod.rs index 6648eb5..a147938 100644 --- a/espanso-detect/src/x11/mod.rs +++ b/espanso-detect/src/x11/mod.rs @@ -17,21 +17,25 @@ * along with espanso. If not, see . */ -use std::ffi::{c_void, CStr}; +use std::{ + collections::HashMap, + ffi::{c_void, CStr}, +}; use lazycell::LazyCell; -use log::{error, trace, warn}; +use log::{debug, error, trace, warn}; use anyhow::Result; use thiserror::Error; -use crate::event::Variant::*; +use crate::event::{HotKeyEvent, Key::*, MouseButton, MouseEvent}; use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; -use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::{event::Status::*, Source, SourceCallback}; +use crate::{event::Variant::*, hotkey::HotKey}; const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1; const INPUT_EVENT_TYPE_MOUSE: i32 = 2; +const INPUT_EVENT_TYPE_HOTKEY: i32 = 3; const INPUT_STATUS_PRESSED: i32 = 1; const INPUT_STATUS_RELEASED: i32 = 2; @@ -53,6 +57,33 @@ pub struct RawInputEvent { pub key_sym: i32, pub key_code: i32, pub status: i32, + pub state: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RawModifierIndexes { + pub ctrl: i32, + pub alt: i32, + pub shift: i32, + pub meta: i32, +} + +#[repr(C)] +pub struct RawHotKeyRequest { + pub key_sym: u32, + pub ctrl: i32, + pub alt: i32, + pub shift: i32, + pub meta: i32, +} + +#[repr(C)] +#[derive(Debug)] +pub struct RawHotKeyResult { + pub success: i32, + pub key_code: i32, + pub state: u32, } #[allow(improper_ctypes)] @@ -62,25 +93,40 @@ extern "C" { pub fn detect_initialize(_self: *const X11Source, error_code: *mut i32) -> *mut c_void; + pub fn detect_get_modifier_indexes(context: *const c_void) -> RawModifierIndexes; + + pub fn detect_register_hotkey( + context: *const c_void, + request: RawHotKeyRequest, + mod_indexes: RawModifierIndexes, + ) -> RawHotKeyResult; + pub fn detect_eventloop( - window: *const c_void, + context: *const c_void, event_callback: extern "C" fn(_self: *mut X11Source, event: RawInputEvent), ) -> i32; - pub fn detect_destroy(window: *const c_void) -> i32; + pub fn detect_destroy(context: *const c_void) -> i32; } pub struct X11Source { handle: *mut c_void, callback: LazyCell, + hotkeys: Vec, + + raw_hotkey_mapping: HashMap<(i32, u32), i32>, // (key_code, state) -> hotkey ID + valid_modifiers_mask: u32, } #[allow(clippy::new_without_default)] impl X11Source { - pub fn new() -> X11Source { + pub fn new(hotkeys: &[HotKey]) -> X11Source { Self { handle: std::ptr::null_mut(), callback: LazyCell::new(), + hotkeys: hotkeys.to_vec(), + raw_hotkey_mapping: HashMap::new(), + valid_modifiers_mask: 0, } } @@ -107,6 +153,29 @@ impl Source for X11Source { return Err(error.into()); } + let mod_indexes = unsafe { detect_get_modifier_indexes(handle) }; + self.valid_modifiers_mask |= 1 << mod_indexes.ctrl; + self.valid_modifiers_mask |= 1 << mod_indexes.alt; + self.valid_modifiers_mask |= 1 << mod_indexes.meta; + self.valid_modifiers_mask |= 1 << mod_indexes.shift; + + // Register the hotkeys + let raw_hotkey_mapping = &mut self.raw_hotkey_mapping; + self.hotkeys.iter().for_each(|hk| { + let raw = convert_hotkey_to_raw(&hk); + if let Some(raw_hk) = raw { + let result = unsafe { detect_register_hotkey(handle, raw_hk, mod_indexes) }; + if result.success == 0 { + error!("unable to register hotkey: {}", hk); + } else { + raw_hotkey_mapping.insert((result.key_code, result.state), hk.id); + debug!("registered hotkey: {}", hk); + } + } else { + error!("unable to generate raw hotkey mapping: {}", hk); + } + }); + self.handle = handle; Ok(()) @@ -124,8 +193,13 @@ impl Source for X11Source { } extern "C" fn callback(_self: *mut X11Source, event: RawInputEvent) { - let event: Option = event.into(); - if let Some(callback) = unsafe { (*_self).callback.borrow() } { + let source_self = unsafe { &*_self }; + let event: Option = convert_raw_input_event_to_input_event( + event, + &source_self.raw_hotkey_mapping, + source_self.valid_modifiers_mask, + ); + if let Some(callback) = source_self.callback.borrow() { if let Some(event) = event { callback(event) } else { @@ -160,6 +234,17 @@ impl Drop for X11Source { } } +fn convert_hotkey_to_raw(hk: &HotKey) -> Option { + let key_sym = hk.key.to_code()?; + Some(RawHotKeyRequest { + key_sym, + ctrl: if hk.has_ctrl() { 1 } else { 0 }, + alt: if hk.has_alt() { 1 } else { 0 }, + shift: if hk.has_shift() { 1 } else { 0 }, + meta: if hk.has_meta() { 1 } else { 0 }, + }) +} + #[derive(Error, Debug)] pub enum X11SourceError { #[error("cannot open displays")] @@ -181,61 +266,70 @@ pub enum X11SourceError { Internal(), } -impl From for Option { - fn from(raw: RawInputEvent) -> Option { - let status = match raw.status { - INPUT_STATUS_RELEASED => Released, - INPUT_STATUS_PRESSED => Pressed, - _ => Pressed, - }; +fn convert_raw_input_event_to_input_event( + raw: RawInputEvent, + raw_hotkey_mapping: &HashMap<(i32, u32), i32>, + valid_modifiers_mask: u32, +) -> Option { + let status = match raw.status { + INPUT_STATUS_RELEASED => Released, + INPUT_STATUS_PRESSED => Pressed, + _ => Pressed, + }; - match raw.event_type { - // Keyboard events - INPUT_EVENT_TYPE_KEYBOARD => { - let (key, variant) = key_sym_to_key(raw.key_sym); - let value = if raw.buffer_len > 0 { - let raw_string_result = - CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]); - match raw_string_result { - Ok(c_string) => { - let string_result = c_string.to_str(); - match string_result { - Ok(value) => Some(value.to_string()), - Err(err) => { - warn!("char conversion error: {}", err); - None - } + match raw.event_type { + // Keyboard events + INPUT_EVENT_TYPE_KEYBOARD => { + let (key, variant) = key_sym_to_key(raw.key_sym); + let value = if raw.buffer_len > 0 { + let raw_string_result = + CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]); + match raw_string_result { + Ok(c_string) => { + let string_result = c_string.to_str(); + match string_result { + Ok(value) => Some(value.to_string()), + Err(err) => { + warn!("char conversion error: {}", err); + None } } - Err(err) => { - warn!("Received malformed char: {}", err); - None - } } - } else { - None - }; - - return Some(InputEvent::Keyboard(KeyboardEvent { - key, - value, - status, - variant, - })); - } - // Mouse events - INPUT_EVENT_TYPE_MOUSE => { - let button = raw_to_mouse_button(raw.key_code); - - if let Some(button) = button { - return Some(InputEvent::Mouse(MouseEvent { button, status })); + Err(err) => { + warn!("Received malformed char: {}", err); + None + } } - } - _ => {} - } + } else { + None + }; - None + return Some(InputEvent::Keyboard(KeyboardEvent { + key, + value, + status, + variant, + })); + } + // Mouse events + INPUT_EVENT_TYPE_MOUSE => { + let button = raw_to_mouse_button(raw.key_code); + + if let Some(button) = button { + return Some(InputEvent::Mouse(MouseEvent { button, status })); + } + } + // Hotkey events + INPUT_EVENT_TYPE_HOTKEY => { + let state = raw.state & valid_modifiers_mask; + if let Some(id) = raw_hotkey_mapping.get(&(raw.key_code, state)) { + return Some(InputEvent::HotKey(HotKeyEvent { hotkey_id: *id })); + } + } + _ => {} } + + None } // Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values @@ -327,6 +421,7 @@ mod tests { key_code: 0, key_sym: 0, status: INPUT_STATUS_PRESSED, + state: 0, } } @@ -342,7 +437,7 @@ mod tests { raw.status = INPUT_STATUS_RELEASED; raw.key_sym = 0x4B; - let result: Option = raw.into(); + let result: Option = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); assert_eq!( result.unwrap(), InputEvent::Keyboard(KeyboardEvent { @@ -361,7 +456,7 @@ mod tests { raw.status = INPUT_STATUS_RELEASED; raw.key_code = INPUT_MOUSE_RIGHT_BUTTON; - let result: Option = raw.into(); + let result: Option = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); assert_eq!( result.unwrap(), InputEvent::Mouse(MouseEvent { @@ -371,6 +466,25 @@ mod tests { ); } + #[test] + fn raw_to_input_event_hotkey_works_correctly() { + let mut raw = default_raw_input_event(); + raw.event_type = INPUT_EVENT_TYPE_HOTKEY; + raw.state = 0b00000011; + raw.key_code = 10; + + let mut raw_hotkey_mapping = HashMap::new(); + raw_hotkey_mapping.insert((10, 1), 20); + + let result: Option = convert_raw_input_event_to_input_event(raw, &raw_hotkey_mapping, 1); + assert_eq!( + result.unwrap(), + InputEvent::HotKey(HotKeyEvent { + hotkey_id: 20, + }) + ); + } + #[test] fn raw_to_input_invalid_buffer() { let buffer: [u8; 24] = [123; 24]; @@ -379,7 +493,7 @@ mod tests { raw.buffer = buffer; raw.buffer_len = 5; - let result: Option = raw.into(); + let result: Option = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); assert!(result.unwrap().into_keyboard().unwrap().value.is_none()); } @@ -387,7 +501,7 @@ mod tests { fn raw_to_input_event_returns_none_when_missing_type() { let mut raw = default_raw_input_event(); raw.event_type = 0; - let result: Option = raw.into(); + let result: Option = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); assert!(result.is_none()); } } diff --git a/espanso-detect/src/x11/native.cpp b/espanso-detect/src/x11/native.cpp index e8436af..7dcf9d4 100644 --- a/espanso-detect/src/x11/native.cpp +++ b/espanso-detect/src/x11/native.cpp @@ -32,6 +32,7 @@ We will refer to this extension as RE from now on. #include #include #include +#include #include #include @@ -170,6 +171,85 @@ void *detect_initialize(void *_rust_instance, int32_t *error_code) return context.release(); } +ModifierIndexes detect_get_modifier_indexes(void *_context) { + DetectContext *context = (DetectContext *)_context; + XModifierKeymap *map = XGetModifierMapping(context->ctrl_disp); + + ModifierIndexes indexes = {}; + + for (int i = 0; i<8; i++) { + if (map->max_keypermod > 0) { + int code = map->modifiermap[i * map->max_keypermod]; + KeySym sym = XkbKeycodeToKeysym(context->ctrl_disp, code, 0, 0); + if (sym == XK_Control_L || sym == XK_Control_R) { + indexes.ctrl = i; + } else if (sym == XK_Super_L || sym == XK_Super_R) { + indexes.meta = i; + } else if (sym == XK_Shift_L || sym == XK_Shift_R) { + indexes.shift = i; + } else if (sym == XK_Alt_L || sym == XK_Alt_R) { + indexes.alt = i; + } + } + } + + XFreeModifiermap(map); + + return indexes; +} + +HotKeyResult detect_register_hotkey(void *_context, HotKeyRequest request, ModifierIndexes mod_indexes) { + DetectContext *context = (DetectContext *)_context; + KeyCode key_code = XKeysymToKeycode(context->ctrl_disp, request.key_sym); + + HotKeyResult result = {}; + if (key_code == 0) { + return result; + } + + uint32_t valid_modifiers = 0; + valid_modifiers |= 1 << mod_indexes.alt; + valid_modifiers |= 1 << mod_indexes.ctrl; + valid_modifiers |= 1 << mod_indexes.shift; + valid_modifiers |= 1 << mod_indexes.meta; + + uint32_t target_modifiers = 0; + if (request.ctrl) { + target_modifiers |= 1 << mod_indexes.ctrl; + } + if (request.alt) { + target_modifiers |= 1 << mod_indexes.alt; + } + if (request.shift) { + target_modifiers |= 1 << mod_indexes.shift; + } + if (request.meta) { + target_modifiers |= 1 << mod_indexes.meta; + } + + result.state = target_modifiers; + result.key_code = key_code; + result.success = 1; + + Window root = DefaultRootWindow(context->ctrl_disp); + + // We need to register an hotkey for all combinations of "useless" modifiers, + // such as the NumLock, as the XGrabKey method wants an exact match. + for (uint state = 0; state<256; state++) { + // Check if the current state includes a "useless modifier" but none of the valid ones + if ((state == 0 || (state & ~valid_modifiers) != 0) && (state & valid_modifiers) == 0) { + uint final_modifiers = state | target_modifiers; + + int res = XGrabKey(context->ctrl_disp, key_code, final_modifiers, root, False, GrabModeAsync, GrabModeAsync); + if (res == BadAccess || res == BadValue) { + result.success = 0; + } + } + } + + return result; +} + int32_t detect_eventloop(void *_context, EventCallback _callback) { DetectContext *context = (DetectContext *)_context; @@ -211,6 +291,15 @@ int32_t detect_eventloop(void *_context, EventCallback _callback) { XRefreshKeyboardMapping(e); } + } else if (event.type == KeyPress) { + InputEvent inputEvent = {}; + inputEvent.event_type = INPUT_EVENT_TYPE_HOTKEY; + inputEvent.key_code = event.xkey.keycode; + inputEvent.state = event.xkey.state; + if (context->event_callback) + { + context->event_callback(context->rust_instance, inputEvent); + } } } } diff --git a/espanso-detect/src/x11/native.h b/espanso-detect/src/x11/native.h index 8e31b13..5cb35b4 100644 --- a/espanso-detect/src/x11/native.h +++ b/espanso-detect/src/x11/native.h @@ -24,13 +24,14 @@ #define INPUT_EVENT_TYPE_KEYBOARD 1 #define INPUT_EVENT_TYPE_MOUSE 2 +#define INPUT_EVENT_TYPE_HOTKEY 3 #define INPUT_STATUS_PRESSED 1 #define INPUT_STATUS_RELEASED 2 typedef struct { - // Keyboard or Mouse event + // Keyboard, Mouse or Hotkey event int32_t event_type; // Contains the string corresponding to the key, if any @@ -42,13 +43,37 @@ typedef struct int32_t key_sym; // Virtual key code of the pressed key in case of keyboard events - // Mouse button code otherwise. + // Mouse button code for mouse events. int32_t key_code; // Pressed or Released status int32_t status; + + // Keycode state (modifiers) in a Hotkey event + uint32_t state; } InputEvent; +typedef struct { + int32_t key_sym; + int32_t ctrl; + int32_t alt; + int32_t shift; + int32_t meta; +} HotKeyRequest; + +typedef struct { + int32_t success; + int32_t key_code; + uint32_t state; +} HotKeyResult; + +typedef struct { + int32_t ctrl; + int32_t alt; + int32_t shift; + int32_t meta; +} ModifierIndexes; + typedef void (*EventCallback)(void *rust_istance, InputEvent data); // Check if a X11 context is available, returning a non-zero code if true. @@ -57,6 +82,12 @@ extern "C" int32_t detect_check_x11(); // Initialize the XRecord API and return the context pointer extern "C" void *detect_initialize(void *rust_istance, int32_t *error_code); +// Get the modifiers indexes in the field mask +extern "C" ModifierIndexes detect_get_modifier_indexes(void *context); + +// Register the given hotkey +extern "C" HotKeyResult detect_register_hotkey(void *context, HotKeyRequest request, ModifierIndexes mod_indexes); + // Run the event loop. Blocking call. extern "C" int32_t detect_eventloop(void *context, EventCallback callback);