Implement hotkeys handling on X11

This commit is contained in:
Federico Terzi 2021-03-14 21:53:17 +01:00
parent 474eae69d5
commit fbeca8b6e9
7 changed files with 390 additions and 68 deletions

View File

@ -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<u32> {
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),
}
}
}

View File

@ -57,7 +57,7 @@ pub struct SourceCreationOptions {
pub evdev_keyboard_rmlvo: Option<KeyboardConfig>,
// 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<HotKey>,
}
@ -103,7 +103,7 @@ pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
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)))
}
}

View File

@ -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

View File

@ -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

View File

@ -17,21 +17,25 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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<SourceCallback>,
hotkeys: Vec<HotKey>,
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<InputEvent> = event.into();
if let Some(callback) = unsafe { (*_self).callback.borrow() } {
let source_self = unsafe { &*_self };
let event: Option<InputEvent> = 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<RawHotKeyRequest> {
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<RawInputEvent> for Option<InputEvent> {
fn from(raw: RawInputEvent) -> Option<InputEvent> {
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<InputEvent> {
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<InputEvent> = raw.into();
let result: Option<InputEvent> = 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<InputEvent> = raw.into();
let result: Option<InputEvent> = 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<InputEvent> = 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<InputEvent> = raw.into();
let result: Option<InputEvent> = 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<InputEvent> = raw.into();
let result: Option<InputEvent> = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
assert!(result.is_none());
}
}

View File

@ -32,6 +32,7 @@ We will refer to this extension as RE from now on.
#include <array>
#include <string.h>
#include <memory>
#include <iostream>
#include <X11/Xlibint.h>
#include <X11/Xlib.h>
@ -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);
}
}
}
}

View File

@ -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);