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)]
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,
};

View File

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

View File

@ -17,52 +17,128 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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<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> {
let mut map = HashMap::new();
impl HotKeyFilter {
pub fn new() -> Self {
Self {
map: HashMap::new(),
memory: HotkeyMemoryMap::new(),
hotkey_raw_map: HashMap::new(),
}
}
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) {
map.insert(sym, code);
self.map.insert(sym, code);
}
}
map
}
pub fn convert_hotkey_to_codes(hk: &HotKey, map: &SymMap) -> Option<Vec<KeyCode>> {
// 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();
}
pub fn process_event(
&mut self,
event: &KeyboardEvent,
) -> Option<i32> {
let mut hotkey = None;
let mut key_code = None;
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<Vec<KeyCode>> {
let mut codes = Vec::new();
let key_code = map.get(&hk.key.to_code()?)?;
let key_code = self.map.get(&hk.key.to_code()?)?;
codes.push(*key_code);
for modifier in hk.modifiers.iter() {
let code = map.get(&modifier.to_code()?)?;
let code = self.map.get(&modifier.to_code()?)?;
codes.push(*code);
}
Some(codes)
}
pub fn detect_hotkey(
event: &KeyboardEvent,
memory: &mut HotkeyMemoryMap,
hotkeys: &HashMap<i32, Vec<KeyCode>>,
) -> Option<i32> {
// TODO: implement the actual matching mechanism
// We need to "clean" the old entries
}

View File

@ -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<KeyboardConfig>,
_context: LazyCell<Context>,
_keymap: LazyCell<Keymap>,
_hotkey_codes: HashMap<i32, Vec<KeyCode>>,
_hotkey_filter: RefCell<HotKeyFilter>,
}
#[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<InputEvent> = 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<RawInputEvent> for Option<InputEvent> {
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<InputEvent> = raw.into();
@ -356,6 +356,7 @@ mod tests {
status: Released,
value: Some("k".to_string()),
variant: None,
code: 0,
})
);
}

View File

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

View File

@ -63,6 +63,7 @@ pub struct KeyboardEvent {
pub value: Option<String>,
pub status: Status,
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

View File

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

View File

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

View File

@ -17,10 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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<InputEvent> = 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,
})
);
}