Finish implementation of hotkey detection on Wayland

This commit is contained in:
Federico Terzi 2021-03-15 19:05:49 +01:00
parent b18cf1c153
commit 307599b761
9 changed files with 139 additions and 62 deletions

View File

@ -36,6 +36,7 @@ pub enum RawInputEvent {
#[derive(Debug)] #[derive(Debug)]
pub struct RawKeyboardEvent { pub struct RawKeyboardEvent {
pub sym: u32, pub sym: u32,
pub code: u32,
pub value: String, pub value: String,
pub state: i32, pub state: i32,
} }
@ -171,6 +172,7 @@ impl Device {
let event = RawKeyboardEvent { let event = RawKeyboardEvent {
state: value, state: value,
code: keycode,
sym, sym,
value: content, value: content,
}; };

View File

@ -43,8 +43,6 @@ pub type xkb_state_component = u32;
pub const EV_KEY: u16 = 0x01; pub const EV_KEY: u16 = 0x01;
pub const XKB_KEYCODE_INVALID: u32 = 0xffffffff;
#[link(name = "xkbcommon")] #[link(name = "xkbcommon")]
extern "C" { extern "C" {
pub fn xkb_state_unref(state: *mut xkb_state); 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_context_unref(context: *mut xkb_context);
pub fn xkb_state_get_keymap(state: *mut xkb_state) -> *mut xkb_keymap; 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_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( pub fn xkb_state_update_key(
state: *mut xkb_state, state: *mut xkb_state,
key: xkb_keycode_t, key: xkb_keycode_t,

View File

@ -17,52 +17,128 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::{event::KeyboardEvent, hotkey::HotKey}; use log::error;
use std::{
collections::{HashMap, VecDeque}, use crate::{event::{KeyboardEvent, Status}, hotkey::HotKey};
time::Instant, use std::{collections::HashMap, time::Instant};
use super::{
state::State,
}; };
use super::state::State; // Number of milliseconds that define how long the hotkey memory
// should retain pressed keys
// Number of milliseconds between the first and last codes of an hotkey const HOTKEY_WINDOW_TIMEOUT: u128 = 5000;
// to be considered valid
const HOTKEY_WINDOW: i32 = 5000;
pub type KeySym = u32; pub type KeySym = u32;
pub type KeyCode = u32; pub type KeyCode = u32;
pub type SymMap = HashMap<KeySym, KeyCode>; pub type HotkeyMemoryMap = Vec<(KeyCode, Instant)>;
pub type HotkeyMemoryMap = VecDeque<(KeyCode, Instant)>; pub struct HotKeyFilter {
map: HashMap<KeySym, KeyCode>,
memory: HotkeyMemoryMap,
hotkey_raw_map: HashMap<i32, Vec<KeyCode>>,
}
pub fn generate_sym_map(state: &State) -> HashMap<KeySym, KeyCode> { impl HotKeyFilter {
let mut map = HashMap::new(); pub fn new() -> Self {
for code in 0..256 { Self {
if let Some(sym) = state.get_sym(code) { map: HashMap::new(),
map.insert(sym, code); memory: HotkeyMemoryMap::new(),
hotkey_raw_map: HashMap::new(),
} }
} }
map
}
pub fn convert_hotkey_to_codes(hk: &HotKey, map: &SymMap) -> Option<Vec<KeyCode>> { pub fn initialize(&mut self, state: &State, hotkeys: &[HotKey]) {
let mut codes = Vec::new(); // First load the map
let key_code = map.get(&hk.key.to_code()?)?; self.map = HashMap::new();
codes.push(*key_code); for code in 0..256 {
if let Some(sym) = state.get_sym(code) {
self.map.insert(sym, code);
}
}
for modifier in hk.modifiers.iter() { // Then the actual hotkeys
let code = map.get(&modifier.to_code()?)?; self.hotkey_raw_map = hotkeys
codes.push(*code); .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<i32> {
let mut hotkey = None;
let mut key_code = None;
pub fn detect_hotkey( let mut to_be_removed = Vec::new();
event: &KeyboardEvent,
memory: &mut HotkeyMemoryMap, if event.status == Status::Released {
hotkeys: &HashMap<i32, Vec<KeyCode>>, // Remove from the memory all the key occurrences
) -> Option<i32> { to_be_removed.extend(self.memory.iter().enumerate().filter_map(|(i, (code, _))| {
// TODO: implement the actual matching mechanism if *code == event.code {
// We need to "clean" the old entries 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<Vec<KeyCode>> {
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)
}
} }

View File

@ -23,16 +23,15 @@
mod context; mod context;
mod device; mod device;
mod ffi; mod ffi;
mod keymap;
mod hotkey; mod hotkey;
mod keymap;
mod state; mod state;
use std::collections::HashMap; use std::{cell::RefCell};
use anyhow::Result; use anyhow::Result;
use context::Context; use context::Context;
use device::{get_devices, Device}; use device::{get_devices, Device};
use hotkey::HotkeyMemoryMap;
use keymap::Keymap; use keymap::Keymap;
use lazycell::LazyCell; use lazycell::LazyCell;
use libc::{ use libc::{
@ -41,12 +40,12 @@ use libc::{
use log::{error, trace}; use log::{error, trace};
use thiserror::Error; use thiserror::Error;
use crate::{event::Variant::*, hotkey::HotKey};
use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::event::{Key::*, MouseButton, MouseEvent};
use crate::{event::Status::*, KeyboardConfig, Source, SourceCallback, SourceCreationOptions}; 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_LEFT: u16 = 0x110;
const BTN_RIGHT: u16 = 0x111; const BTN_RIGHT: u16 = 0x111;
@ -61,7 +60,7 @@ pub struct EVDEVSource {
_keyboard_rmlvo: Option<KeyboardConfig>, _keyboard_rmlvo: Option<KeyboardConfig>,
_context: LazyCell<Context>, _context: LazyCell<Context>,
_keymap: LazyCell<Keymap>, _keymap: LazyCell<Keymap>,
_hotkey_codes: HashMap<i32, Vec<KeyCode>>, _hotkey_filter: RefCell<HotKeyFilter>,
} }
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
@ -73,7 +72,7 @@ impl EVDEVSource {
_context: LazyCell::new(), _context: LazyCell::new(),
_keymap: LazyCell::new(), _keymap: LazyCell::new(),
_keyboard_rmlvo: options.evdev_keyboard_rmlvo, _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 // Initialize the hotkeys
let state = State::new(&keymap)?; let state = State::new(&keymap)?;
let sym_map = generate_sym_map(&state); self._hotkey_filter.borrow_mut().initialize(&state, &self.hotkeys);
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();
if self._context.fill(context).is_err() { if self._context.fill(context).is_err() {
return Err(EVDEVSourceError::InitFailure().into()); 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 // Read events indefinitely
let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() }; 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| { events.into_iter().for_each(|raw_event| {
let event: Option<InputEvent> = raw_event.into(); let event: Option<InputEvent> = raw_event.into();
if let Some(event) = event { 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); event_callback(event);
} else { } else {
@ -233,6 +231,7 @@ impl From<RawInputEvent> for Option<InputEvent> {
value, value,
status, status,
variant, variant,
code: keyboard_event.code,
})); }));
} }
RawInputEvent::Mouse(mouse_event) => { RawInputEvent::Mouse(mouse_event) => {
@ -345,7 +344,8 @@ mod tests {
let raw = RawInputEvent::Keyboard(RawKeyboardEvent { let raw = RawInputEvent::Keyboard(RawKeyboardEvent {
sym: 0x4B, sym: 0x4B,
value: "k".to_owned(), value: "k".to_owned(),
is_down: false, state: KEY_STATE_RELEASE,
code: 0,
}); });
let result: Option<InputEvent> = raw.into(); let result: Option<InputEvent> = raw.into();
@ -356,6 +356,7 @@ mod tests {
status: Released, status: Released,
value: Some("k".to_string()), value: Some("k".to_string()),
variant: None, variant: None,
code: 0,
}) })
); );
} }

View File

@ -1,8 +1,6 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example // This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c // https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use std::ffi::CStr;
use scopeguard::ScopeGuard; use scopeguard::ScopeGuard;
use anyhow::Result; use anyhow::Result;

View File

@ -63,6 +63,7 @@ pub struct KeyboardEvent {
pub value: Option<String>, pub value: Option<String>,
pub status: Status, pub status: Status,
pub variant: Option<Variant>, pub variant: Option<Variant>,
pub code: u32,
} }
// A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values // A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values

View File

@ -265,6 +265,7 @@ impl From<RawInputEvent> for Option<InputEvent> {
value, value,
status, status,
variant, variant,
code: raw.key_code.try_into().expect("unable to convert keycode to u32"),
})); }));
} }
// Mouse events // Mouse events

View File

@ -274,6 +274,7 @@ impl From<RawInputEvent> for Option<InputEvent> {
value, value,
status, status,
variant, variant,
code: raw.key_code.try_into().expect("unable to convert keycode to u32"),
})); }));
} }
// Mouse events // Mouse events

View File

@ -17,10 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{ use std::{collections::HashMap, convert::TryInto, ffi::{c_void, CStr}};
collections::HashMap,
ffi::{c_void, CStr},
};
use lazycell::LazyCell; use lazycell::LazyCell;
use log::{debug, error, trace, warn}; use log::{debug, error, trace, warn};
@ -309,6 +306,7 @@ fn convert_raw_input_event_to_input_event(
value, value,
status, status,
variant, variant,
code: raw.key_code.try_into().expect("invalid keycode conversion to u32"),
})); }));
} }
// Mouse events // Mouse events
@ -436,6 +434,7 @@ mod tests {
raw.buffer_len = 1; raw.buffer_len = 1;
raw.status = INPUT_STATUS_RELEASED; raw.status = INPUT_STATUS_RELEASED;
raw.key_sym = 0x4B; raw.key_sym = 0x4B;
raw.key_code = 1;
let result: Option<InputEvent> = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0); let result: Option<InputEvent> = convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
assert_eq!( assert_eq!(
@ -445,6 +444,7 @@ mod tests {
status: Released, status: Released,
value: Some("k".to_string()), value: Some("k".to_string()),
variant: None, variant: None,
code: 1,
}) })
); );
} }