diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs index e07e730..61763f3 100644 --- a/espanso-detect/src/evdev/device.rs +++ b/espanso-detect/src/evdev/device.rs @@ -23,9 +23,9 @@ use super::{ }; const EVDEV_OFFSET: i32 = 8; -const KEY_STATE_RELEASE: i32 = 0; -const KEY_STATE_PRESS: i32 = 1; -const KEY_STATE_REPEAT: i32 = 2; +pub const KEY_STATE_RELEASE: i32 = 0; +pub const KEY_STATE_PRESS: i32 = 1; +pub const KEY_STATE_REPEAT: i32 = 2; #[derive(Debug)] pub enum RawInputEvent { @@ -37,7 +37,7 @@ pub enum RawInputEvent { pub struct RawKeyboardEvent { pub sym: u32, pub value: String, - pub is_down: bool, + pub state: i32, } #[derive(Debug)] @@ -170,7 +170,7 @@ impl Device { let content = content_raw.to_string_lossy().to_string(); let event = RawKeyboardEvent { - is_down, + state: value, sym, value: content, }; diff --git a/espanso-detect/src/evdev/ffi.rs b/espanso-detect/src/evdev/ffi.rs index e242ca5..31bf665 100644 --- a/espanso-detect/src/evdev/ffi.rs +++ b/espanso-detect/src/evdev/ffi.rs @@ -43,6 +43,8 @@ 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); @@ -57,6 +59,7 @@ 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 new file mode 100644 index 0000000..2549efe --- /dev/null +++ b/espanso-detect/src/evdev/hotkey.rs @@ -0,0 +1,68 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use crate::{event::KeyboardEvent, hotkey::HotKey}; +use std::{ + collections::{HashMap, VecDeque}, + time::Instant, +}; + +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; + +pub type KeySym = u32; +pub type KeyCode = u32; +pub type SymMap = HashMap; + +pub type HotkeyMemoryMap = VecDeque<(KeyCode, Instant)>; + +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); + } + } + 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); + + for modifier in hk.modifiers.iter() { + let code = map.get(&modifier.to_code()?)?; + codes.push(*code); + } + + Some(codes) +} + +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 +} diff --git a/espanso-detect/src/evdev/keymap.rs b/espanso-detect/src/evdev/keymap.rs index bb2f266..89d1c72 100644 --- a/espanso-detect/src/evdev/keymap.rs +++ b/espanso-detect/src/evdev/keymap.rs @@ -10,13 +10,7 @@ use thiserror::Error; use crate::KeyboardConfig; -use super::{ - context::Context, - ffi::{ - xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref, xkb_rule_names, - XKB_KEYMAP_COMPILE_NO_FLAGS, - }, -}; +use super::{context::Context, ffi::{XKB_KEYMAP_COMPILE_NO_FLAGS, xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref, xkb_rule_names}}; pub struct Keymap { keymap: *mut xkb_keymap, @@ -24,7 +18,7 @@ pub struct Keymap { impl Keymap { pub fn new(context: &Context, rmlvo: Option) -> Result { - let names = rmlvo.map(|rmlvo| Self::generate_names(rmlvo)); + let names = rmlvo.map(Self::generate_names); let names_ptr = names.map_or(std::ptr::null(), |names| &names); let raw_keymap = unsafe { diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs index 54814f8..01550c5 100644 --- a/espanso-detect/src/evdev/mod.rs +++ b/espanso-detect/src/evdev/mod.rs @@ -1,4 +1,4 @@ -// This code is a port of the libxkbcommon "interactive-evdev.c" example +// This code is heavily inspired by the libxkbcommon "interactive-evdev.c" example // https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c /* @@ -24,10 +24,15 @@ mod context; mod device; mod ffi; mod keymap; +mod hotkey; +mod state; + +use std::collections::HashMap; use anyhow::Result; use context::Context; use device::{get_devices, Device}; +use hotkey::HotkeyMemoryMap; use keymap::Keymap; use lazycell::LazyCell; use libc::{ @@ -36,12 +41,12 @@ use libc::{ use log::{error, trace}; use thiserror::Error; -use crate::event::Variant::*; +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 self::device::{DeviceError, RawInputEvent}; +use self::{device::{DeviceError, KEY_STATE_PRESS, KEY_STATE_RELEASE, RawInputEvent}, hotkey::{KeyCode, convert_hotkey_to_codes, generate_sym_map}, state::State}; const BTN_LEFT: u16 = 0x110; const BTN_RIGHT: u16 = 0x111; @@ -51,10 +56,12 @@ const BTN_EXTRA: u16 = 0x114; pub struct EVDEVSource { devices: Vec, + hotkeys: Vec, _keyboard_rmlvo: Option, _context: LazyCell, _keymap: LazyCell, + _hotkey_codes: HashMap>, } #[allow(clippy::new_without_default)] @@ -62,9 +69,11 @@ impl EVDEVSource { pub fn new(options: SourceCreationOptions) -> EVDEVSource { Self { devices: Vec::new(), + hotkeys: options.hotkeys, _context: LazyCell::new(), _keymap: LazyCell::new(), _keyboard_rmlvo: options.evdev_keyboard_rmlvo, + _hotkey_codes: HashMap::new(), } } } @@ -91,6 +100,17 @@ 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(); + if self._context.fill(context).is_err() { return Err(EVDEVSourceError::InitFailure().into()); } @@ -133,6 +153,8 @@ impl Source for EVDEVSource { } } + let mut hotkey_memory = HotkeyMemoryMap::new(); + // Read events indefinitely let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() }; loop { @@ -154,6 +176,9 @@ 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 + + event_callback(event); } else { trace!("unable to convert raw event to input event"); @@ -194,10 +219,13 @@ impl From for Option { Some(keyboard_event.value) }; - let status = if keyboard_event.is_down { + let status = if keyboard_event.state == KEY_STATE_PRESS { Pressed - } else { + } else if keyboard_event.state == KEY_STATE_RELEASE { Released + } else { + // Filter out the "repeated" events + return None; }; return Some(InputEvent::Keyboard(KeyboardEvent { diff --git a/espanso-detect/src/evdev/state.rs b/espanso-detect/src/evdev/state.rs new file mode 100644 index 0000000..5de799b --- /dev/null +++ b/espanso-detect/src/evdev/state.rs @@ -0,0 +1,60 @@ +// 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; +use thiserror::Error; + +use super::{ + ffi::{ + xkb_state, xkb_state_key_get_one_sym, xkb_state_new, xkb_state_unref, + }, + keymap::Keymap, +}; + +pub struct State { + state: *mut xkb_state, +} + +impl State { + pub fn new(keymap: &Keymap) -> Result { + let raw_state = unsafe { xkb_state_new(keymap.get_handle()) }; + let state = scopeguard::guard(raw_state, |raw_state| unsafe { + xkb_state_unref(raw_state); + }); + + if raw_state.is_null() { + return Err(StateError::FailedCreation().into()); + } + + Ok(Self { + state: ScopeGuard::into_inner(state), + }) + } + + pub fn get_sym(&self, code: u32) -> Option { + let sym = unsafe { xkb_state_key_get_one_sym(self.state, code) }; + if sym == 0 { + None + } else { + Some(sym) + } + } +} + +impl Drop for State { + fn drop(&mut self) { + unsafe { + xkb_state_unref(self.state); + } + } +} + +#[derive(Error, Debug)] +pub enum StateError { + #[error("could not create xkb state")] + FailedCreation(), +} diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 89a233e..3c2bbd2 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -61,6 +61,7 @@ fn main() { let handle = std::thread::spawn(move || { let injector = get_injector(Default::default()).unwrap(); let mut source = get_source(SourceCreationOptions { + use_evdev: true, hotkeys: vec![ HotKey::new(1, "OPTION+SPACE").unwrap(), HotKey::new(2, "CTRL+OPTION+3").unwrap(),