diff --git a/Cargo.lock b/Cargo.lock index 204ea24..89bf3e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "enum-as-inner" version = "0.3.3" @@ -171,6 +177,7 @@ dependencies = [ "anyhow", "cc", "enum-as-inner", + "itertools", "lazy_static", "lazycell", "libc", @@ -216,6 +223,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.7" diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs index 9273f9d..488a74b 100644 --- a/espanso-detect/src/evdev/mod.rs +++ b/espanso-detect/src/evdev/mod.rs @@ -29,6 +29,7 @@ use anyhow::Result; use context::Context; use device::{get_devices, Device}; use keymap::Keymap; +use lazycell::LazyCell; use libc::{ __errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD, }; @@ -51,6 +52,9 @@ const BTN_EXTRA: u16 = 0x114; pub type EVDEVSourceCallback = Box; pub struct EVDEVSource { devices: Vec, + + _context: LazyCell, + _keymap: LazyCell, } #[allow(clippy::new_without_default)] @@ -58,12 +62,15 @@ impl EVDEVSource { pub fn new() -> EVDEVSource { Self { devices: Vec::new(), + _context: LazyCell::new(), + _keymap: LazyCell::new(), } } pub fn initialize(&mut self) -> Result<()> { let context = Context::new().expect("unable to obtain xkb context"); let keymap = Keymap::new(&context).expect("unable to create xkb keymap"); + match get_devices(&keymap) { Ok(devices) => self.devices = devices, Err(error) => { @@ -80,6 +87,13 @@ impl EVDEVSource { } } + if self._context.fill(context).is_err() { + return Err(EVDEVSourceError::InitFailure().into()); + } + if self._keymap.fill(keymap).is_err() { + return Err(EVDEVSourceError::InitFailure().into()); + } + Ok(()) } @@ -150,6 +164,9 @@ impl EVDEVSource { #[derive(Error, Debug)] pub enum EVDEVSourceError { + #[error("initialization failed")] + InitFailure(), + #[error("permission denied")] PermissionDenied(), } diff --git a/espanso-inject/Cargo.toml b/espanso-inject/Cargo.toml index 8d09fce..4669352 100644 --- a/espanso-inject/Cargo.toml +++ b/espanso-inject/Cargo.toml @@ -19,6 +19,7 @@ widestring = "0.4.3" [target.'cfg(target_os="linux")'.dependencies] libc = "0.2.85" scopeguard = "1.1.0" +itertools = "0.10.0" [build-dependencies] cc = "1.0.66" diff --git a/espanso-inject/build.rs b/espanso-inject/build.rs index 3b39860..ad40878 100644 --- a/espanso-inject/build.rs +++ b/espanso-inject/build.rs @@ -39,19 +39,20 @@ fn cc_config() { // println!("cargo:rerun-if-changed=src/x11/native.h"); // println!("cargo:rerun-if-changed=src/evdev/native.cpp"); // println!("cargo:rerun-if-changed=src/evdev/native.h"); + println!("cargo:rerun-if-changed=src/evdev/native.h"); + println!("cargo:rerun-if-changed=src/evdev/native.c"); // cc::Build::new() // .cpp(true) // .include("src/x11/native.h") // .file("src/x11/native.cpp") // .compile("espansoinject"); - // cc::Build::new() - // .cpp(true) - // .include("src/evdev/native.h") - // .file("src/evdev/native.cpp") - // .compile("espansoinjectevdev"); + cc::Build::new() + .include("src/evdev/native.h") + .file("src/evdev/native.c") + .compile("espansoinjectev"); println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); // println!("cargo:rustc-link-lib=static=espansoinject"); - // println!("cargo:rustc-link-lib=static=espansoinjectevdev"); + println!("cargo:rustc-link-lib=static=espansoinjectev"); println!("cargo:rustc-link-lib=dylib=X11"); println!("cargo:rustc-link-lib=dylib=Xtst"); println!("cargo:rustc-link-lib=dylib=xkbcommon"); diff --git a/espanso-inject/src/evdev/README.md b/espanso-inject/src/evdev/README.md index 73275f6..0c70cb0 100644 --- a/espanso-inject/src/evdev/README.md +++ b/espanso-inject/src/evdev/README.md @@ -38,6 +38,8 @@ The available modifiers can be found in the https://github.com/torvalds/linux/bl #define KEY_RIGHTMETA 126 #define KEY_RIGHTCTRL 97 #define KEY_RIGHTALT 100 +#define KEY_CAPSLOCK 58 +#define KEY_NUMLOCK 69 All these codes have to be added the EVDEV_OFFSET = 8 diff --git a/espanso-inject/src/evdev/context.rs b/espanso-inject/src/evdev/context.rs new file mode 100644 index 0000000..bcda581 --- /dev/null +++ b/espanso-inject/src/evdev/context.rs @@ -0,0 +1,47 @@ +// This code is a port of the libxkbcommon "interactive-evdev.c" example +// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c + +use scopeguard::ScopeGuard; + +use super::ffi::{xkb_context, xkb_context_new, xkb_context_unref, XKB_CONTEXT_NO_FLAGS}; +use anyhow::Result; +use thiserror::Error; + +pub struct Context { + context: *mut xkb_context, +} + +impl Context { + pub fn new() -> Result { + let raw_context = unsafe { xkb_context_new(XKB_CONTEXT_NO_FLAGS) }; + let context = scopeguard::guard(raw_context, |raw_context| unsafe { + xkb_context_unref(raw_context); + }); + + if raw_context.is_null() { + return Err(ContextError::FailedCreation().into()); + } + + Ok(Self { + context: ScopeGuard::into_inner(context), + }) + } + + pub fn get_handle(&self) -> *mut xkb_context { + self.context + } +} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + xkb_context_unref(self.context); + } + } +} + +#[derive(Error, Debug)] +pub enum ContextError { + #[error("could not create xkb context")] + FailedCreation(), +} diff --git a/espanso-inject/src/evdev/ffi.rs b/espanso-inject/src/evdev/ffi.rs new file mode 100644 index 0000000..8391306 --- /dev/null +++ b/espanso-inject/src/evdev/ffi.rs @@ -0,0 +1,84 @@ +// Bindings taken from: https://github.com/rtbo/xkbcommon-rs/blob/master/src/xkb/ffi.rs + +use std::os::raw::c_int; + +use libc::{c_char, c_uint, c_ulong}; + +#[allow(non_camel_case_types)] +pub enum xkb_context {} +#[allow(non_camel_case_types)] +pub enum xkb_state {} +#[allow(non_camel_case_types)] +pub enum xkb_keymap {} +#[allow(non_camel_case_types)] +pub type xkb_keycode_t = u32; +#[allow(non_camel_case_types)] +pub type xkb_keysym_t = u32; + +#[repr(C)] +pub struct xkb_rule_names { + pub rules: *const c_char, + pub model: *const c_char, + pub layout: *const c_char, + pub variant: *const c_char, + pub options: *const c_char, +} + +#[repr(C)] +pub enum xkb_key_direction { + UP, + DOWN, +} + +#[allow(non_camel_case_types)] +pub type xkb_keymap_compile_flags = u32; +pub const XKB_KEYMAP_COMPILE_NO_FLAGS: u32 = 0; + +#[allow(non_camel_case_types)] +pub type xkb_context_flags = u32; +pub const XKB_CONTEXT_NO_FLAGS: u32 = 0; + +#[allow(non_camel_case_types)] +pub type xkb_state_component = u32; + +pub const EV_KEY: u16 = 0x01; + +#[link(name = "xkbcommon")] +extern "C" { + pub fn xkb_state_unref(state: *mut xkb_state); + pub fn xkb_state_new(keymap: *mut xkb_keymap) -> *mut xkb_state; + pub fn xkb_keymap_new_from_names( + context: *mut xkb_context, + names: *const xkb_rule_names, + flags: xkb_keymap_compile_flags, + ) -> *mut xkb_keymap; + pub fn xkb_keymap_unref(keymap: *mut xkb_keymap); + pub fn xkb_context_new(flags: xkb_context_flags) -> *mut xkb_context; + pub fn xkb_context_unref(context: *mut xkb_context); + pub fn xkb_state_update_key( + state: *mut xkb_state, + key: xkb_keycode_t, + direction: xkb_key_direction, + ) -> xkb_state_component; + pub fn xkb_state_key_get_utf8( + state: *mut xkb_state, + key: xkb_keycode_t, + buffer: *mut c_char, + size: usize, + ) -> c_int; + pub fn xkb_state_key_get_one_sym(state: *mut xkb_state, key: xkb_keycode_t) -> xkb_keysym_t; +} + +// These are used to retrieve constants from the C side. +// This is needed as those constants are defined with C macros, +// and converting them manually is error-prone. +#[link(name = "espansoinjectev", kind = "static")] +extern "C" { + pub fn ui_dev_create() -> c_ulong; + pub fn ui_dev_destroy() -> c_ulong; + pub fn ui_set_evbit() -> c_ulong; + pub fn ui_set_keybit() -> c_ulong; + + pub fn setup_uinput_device(fd: c_int) -> c_int; + pub fn uinput_emit(fd: c_int, code: c_uint, pressed: c_int); +} \ No newline at end of file diff --git a/espanso-inject/src/evdev/keymap.rs b/espanso-inject/src/evdev/keymap.rs new file mode 100644 index 0000000..b652bdd --- /dev/null +++ b/espanso-inject/src/evdev/keymap.rs @@ -0,0 +1,86 @@ +// 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::CString; + +use scopeguard::ScopeGuard; + +use anyhow::Result; +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, + }, +}; + +pub struct Keymap { + keymap: *mut xkb_keymap, +} + +impl Keymap { + pub fn new(context: &Context, rmlvo: Option) -> Result { + let names = rmlvo.map(|rmlvo| { + Self::generate_names(rmlvo) + }); + + let names_ptr = names.map_or(std::ptr::null(), |names| &names); + + let raw_keymap = unsafe { + xkb_keymap_new_from_names( + context.get_handle(), + names_ptr, + XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + let keymap = scopeguard::guard(raw_keymap, |raw_keymap| unsafe { + xkb_keymap_unref(raw_keymap); + }); + + if raw_keymap.is_null() { + return Err(KeymapError::FailedCreation().into()); + } + + Ok(Self { + keymap: ScopeGuard::into_inner(keymap), + }) + } + + pub fn get_handle(&self) -> *mut xkb_keymap { + self.keymap + } + + fn generate_names(rmlvo: KeyboardConfig) -> xkb_rule_names { + let rules = rmlvo.rules.map(|s| { CString::new(s).expect("unable to create CString for keymap") }); + let model = rmlvo.model.map(|s| { CString::new(s).expect("unable to create CString for keymap") }); + let layout = rmlvo.layout.map(|s| { CString::new(s).expect("unable to create CString for keymap") }); + let variant = rmlvo.variant.map(|s| { CString::new(s).expect("unable to create CString for keymap") }); + let options = rmlvo.options.map(|s| { CString::new(s).expect("unable to create CString for keymap") }); + + xkb_rule_names { + rules: rules.map_or(std::ptr::null(), |s| s.as_ptr()), + model: model.map_or(std::ptr::null(), |s| s.as_ptr()), + layout: layout.map_or(std::ptr::null(), |s| s.as_ptr()), + variant: variant.map_or(std::ptr::null(), |s| s.as_ptr()), + options: options.map_or(std::ptr::null(), |s| s.as_ptr()), + } + } +} + +impl Drop for Keymap { + fn drop(&mut self) { + unsafe { + xkb_keymap_unref(self.keymap); + } + } +} + +#[derive(Error, Debug)] +pub enum KeymapError { + #[error("could not create xkb keymap")] + FailedCreation(), +} diff --git a/espanso-inject/src/evdev/mod.rs b/espanso-inject/src/evdev/mod.rs new file mode 100644 index 0000000..d66fb18 --- /dev/null +++ b/espanso-inject/src/evdev/mod.rs @@ -0,0 +1,332 @@ +/* + * 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 . + */ + +mod context; +mod ffi; +mod keymap; +mod state; +mod uinput; + +use std::{ + collections::{HashMap, HashSet}, + ffi::{CString}, +}; + +use context::Context; +use keymap::Keymap; +use log::error; +use std::iter::FromIterator; +use uinput::UInputDevice; + +use crate::{ + linux::raw_keys::{convert_to_sym_array}, + InjectorCreationOptions, +}; +use anyhow::Result; +use itertools::Itertools; +use thiserror::Error; + +use crate::{keys, InjectionOptions, Injector}; + +use self::state::State; + +// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB +// keycode set (where ESC is 9). +const EVDEV_OFFSET: u32 = 8; + +// List of modifier keycodes, as defined in the "input-event-codes.h" header +// These can be overridden by changing the "evdev_modifier" option during initialization +const KEY_LEFTCTRL: u32 = 29; +const KEY_LEFTSHIFT: u32 = 42; +const KEY_RIGHTSHIFT: u32 = 54; +const KEY_LEFTALT: u32 = 56; +const KEY_LEFTMETA: u32 = 125; +const KEY_RIGHTMETA: u32 = 126; +const KEY_RIGHTCTRL: u32 = 97; +const KEY_RIGHTALT: u32 = 100; +const KEY_CAPSLOCK: u32 = 58; +const KEY_NUMLOCK: u32 = 69; + +const DEFAULT_MODIFIERS: [u32; 10] = [ + KEY_LEFTCTRL, + KEY_LEFTSHIFT, + KEY_RIGHTSHIFT, + KEY_LEFTALT, + KEY_LEFTMETA, + KEY_RIGHTMETA, + KEY_RIGHTCTRL, + KEY_RIGHTALT, + KEY_CAPSLOCK, + KEY_NUMLOCK, +]; + +const DEFAULT_MAX_MODIFIER_COMBINATION_LEN: i32 = 3; + +pub type KeySym = u32; + +#[derive(Clone, Debug)] +struct KeyRecord { + // Keycode + code: u32, + // List of modifiers that must be pressed + modifiers: Vec, +} + +type CharMap = HashMap; +type SymMap = HashMap; + +pub struct EVDEVInjector { + device: UInputDevice, + + // Lookup maps + char_map: CharMap, + sym_map: SymMap, + + // Ownership + _context: Context, + _keymap: Keymap, +} + +#[allow(clippy::new_without_default)] +impl EVDEVInjector { + pub fn new(options: InjectorCreationOptions) -> Result { + let modifiers = options + .evdev_modifiers + .unwrap_or_else(|| DEFAULT_MODIFIERS.to_vec()); + let max_modifier_combination_len = options + .evdev_max_modifier_combination_len + .unwrap_or(DEFAULT_MAX_MODIFIER_COMBINATION_LEN); + + // Necessary to properly handle non-ascii chars + let empty_string = CString::new("")?; + unsafe { + libc::setlocale(libc::LC_ALL, empty_string.as_ptr()); + } + + let context = Context::new().expect("unable to obtain xkb context"); + let keymap = Keymap::new(&context, options.evdev_keyboard_rmlvo).expect("unable to create xkb keymap"); + + let (char_map, sym_map) = + Self::generate_maps(&modifiers, max_modifier_combination_len, &keymap)?; + + // Create the uinput virtual device + let device = UInputDevice::new()?; + + Ok(Self { + device, + char_map, + sym_map, + _context: context, + _keymap: keymap, + }) + } + + fn generate_maps( + modifiers: &[u32], + max_modifier_sequence_len: i32, + keymap: &Keymap, + ) -> Result<(CharMap, SymMap)> { + let mut char_map = HashMap::new(); + let mut sym_map = HashMap::new(); + + let modifier_combinations = Self::generate_combinations(modifiers, max_modifier_sequence_len); + + // Cycle through all code/modifiers combinations to populate the reverse lookup tables + for key_code in 8..256u32 { + for modifier_combination in modifier_combinations.iter() { + let state = State::new(keymap)?; + + // Apply the modifiers + for modifier in modifier_combination.iter() { + // We need to add the EVDEV offset for xkbcommon to recognize it correctly + state.update_key(*modifier + EVDEV_OFFSET, true); + } + + let key_record = KeyRecord { + code: key_code - EVDEV_OFFSET, + modifiers: modifier_combination.clone(), + }; + + // Keysym was found + if let Some(sym) = state.get_sym(key_code) { + sym_map.entry(sym).or_insert_with(|| key_record.clone()); + } + + // Char was found + if let Some(string) = state.get_string(key_code) { + char_map.entry(string).or_insert(key_record); + } + } + } + + Ok((char_map, sym_map)) + } + + fn generate_combinations(modifiers: &[u32], max_modifier_sequence_len: i32) -> Vec> { + let mut combinations = vec![vec![]]; // Initial empty combination + + for sequence_len in 1..=max_modifier_sequence_len { + let current_combinations = modifiers + .iter() + .cloned() + .combinations(sequence_len as usize); + combinations.extend(current_combinations); + } + + combinations + } + + fn convert_to_record_array(&self, syms: &[u64]) -> Result> { + syms + .iter() + .map(|sym| { + self + .sym_map + .get(&(*sym as u32)) + .cloned() + .ok_or_else(|| EVDEVInjectorError::SymMappingFailure(*sym as u32).into()) + }) + .collect() + } + + fn send_key(&self, code: u32, pressed: bool, delay_us: u32) { + self.device.emit(code, pressed); + if delay_us != 0 { + unsafe { + libc::usleep(delay_us); + } + } + } +} + +impl Injector for EVDEVInjector { + fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> { + // Compute all the key record sequence first to make sure a mapping is available + let records: Result> = string + .chars() + .map(|c| c.to_string()) + .map(|char| { + self + .char_map + .get(&char) + .cloned() + .ok_or_else(|| EVDEVInjectorError::CharMappingFailure(char).into()) + }) + .collect(); + + let delay_us = options.delay as u32 * 1000; // Convert to micro seconds + + // We need to keep track of the modifiers currently pressed to + // press or release them accordingly + let mut current_modifiers: HashSet = HashSet::new(); + + for record in records? { + let record_modifiers = HashSet::from_iter(record.modifiers.iter().cloned()); + + // Release all the modifiers that are not needed anymore + for expired_modifier in current_modifiers.difference(&record_modifiers) { + self.send_key(*expired_modifier, false, delay_us); + } + + // Press all the new modifiers that are now needed + for new_modifier in record_modifiers.difference(¤t_modifiers) { + self.send_key(*new_modifier, true, delay_us); + } + + // Send the char + self.send_key(record.code, true, delay_us); + self.send_key(record.code, false, delay_us); + + current_modifiers = record_modifiers; + } + + // Release all the remaining modifiers + for expired_modifier in current_modifiers { + self.send_key(expired_modifier, false, delay_us); + } + + Ok(()) + } + + fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> { + // Compute all the key record sequence first to make sure a mapping is available + let syms = convert_to_sym_array(keys)?; + let records = self.convert_to_record_array(&syms)?; + + let delay_us = options.delay as u32 * 1000; // Convert to micro seconds + + for record in records { + // Press the modifiers + for modifier in record.modifiers.iter() { + self.send_key(*modifier, true, delay_us); + } + + // Send the key + self.send_key(record.code, true, delay_us); + self.send_key(record.code, false, delay_us); + + // Release the modifiers + for modifier in record.modifiers.iter() { + self.send_key(*modifier, false, delay_us); + } + } + + Ok(()) + } + + fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> { + // Compute all the key record sequence first to make sure a mapping is available + let syms = convert_to_sym_array(keys)?; + let records = self.convert_to_record_array(&syms)?; + + let delay_us = options.delay as u32 * 1000; // Convert to micro seconds + + // First press the keys + for record in records.iter() { + // Press the modifiers + for modifier in record.modifiers.iter() { + self.send_key(*modifier, true, delay_us); + } + + // Send the key + self.send_key(record.code, true, delay_us); + } + + // Then release them + for record in records.iter().rev() { + self.send_key(record.code, false, delay_us); + + // Release the modifiers + for modifier in record.modifiers.iter() { + self.send_key(*modifier, false, delay_us); + } + } + + Ok(()) + } +} + +#[derive(Error, Debug)] +pub enum EVDEVInjectorError { + #[error("missing vkey mapping for char `{0}`")] + CharMappingFailure(String), + + #[error("missing record mapping for sym `{0}`")] + SymMappingFailure(u32), +} diff --git a/espanso-inject/src/evdev/native.c b/espanso-inject/src/evdev/native.c new file mode 100644 index 0000000..69e52ea --- /dev/null +++ b/espanso-inject/src/evdev/native.c @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +#include "native.h" +#include +#include + +unsigned long ui_dev_destroy() +{ + return UI_DEV_DESTROY; +} + +unsigned long ui_dev_create() +{ + return UI_DEV_CREATE; +} + +unsigned long ui_set_evbit() +{ + return UI_SET_EVBIT; +} + +unsigned long ui_set_keybit() +{ + return UI_SET_KEYBIT; +} + +int setup_uinput_device(int fd) +{ + struct uinput_setup usetup; + + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x1234; // sample vendor + usetup.id.product = 0x5678; // sample product + strcpy(usetup.name, "Espanso virtual device"); + + return ioctl(fd, UI_DEV_SETUP, &usetup); +} + +void emit(int fd, int type, int code, int val) +{ + struct input_event ie; + ie.type = type; + ie.code = code; + ie.value = val; + // timestamp values below are ignored + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + write(fd, &ie, sizeof(ie)); +} + +void uinput_emit(int fd, unsigned int code, int pressed) { + emit(fd, EV_KEY, code, pressed); + emit(fd, EV_SYN, SYN_REPORT, 0); +} \ No newline at end of file diff --git a/espanso-inject/src/evdev/native.h b/espanso-inject/src/evdev/native.h new file mode 100644 index 0000000..5a56ca8 --- /dev/null +++ b/espanso-inject/src/evdev/native.h @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#ifndef ESPANSO_INJECT_EV_CONSTANTS_H +#define ESPANSO_INJECT_EV_CONSTANTS_H + +unsigned long ui_dev_destroy(); +unsigned long ui_dev_create(); +unsigned long ui_set_evbit(); +unsigned long ui_set_keybit(); + +int setup_uinput_device(int fd); +void uinput_emit(int fd, unsigned int code, int pressed); + +#endif //ESPANSO_INJECT_EV_CONSTANTS_H \ No newline at end of file diff --git a/espanso-inject/src/evdev/state.rs b/espanso-inject/src/evdev/state.rs new file mode 100644 index 0000000..0eee303 --- /dev/null +++ b/espanso-inject/src/evdev/state.rs @@ -0,0 +1,89 @@ +// 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_key_get_utf8, xkb_state_new, xkb_state_unref, xkb_state_update_key}, 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 update_key(&self, code: u32, pressed: bool) { + let direction = if pressed { + super::ffi::xkb_key_direction::DOWN + } else { + super::ffi::xkb_key_direction::UP + }; + unsafe { + xkb_state_update_key(self.state, code, direction); + } + } + + pub fn get_string(&self, code: u32) -> Option { + let mut buffer: [u8; 16] = [0; 16]; + let len = unsafe { + xkb_state_key_get_utf8( + self.state, + code, + buffer.as_mut_ptr() as *mut i8, + std::mem::size_of_val(&buffer), + ) + }; + if len > 0 { + let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) }; + let string = content_raw.to_string_lossy().to_string(); + if string.is_empty() { + None + } else { + Some(string) + } + } else { + None + } + } + + 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-inject/src/evdev/uinput.rs b/espanso-inject/src/evdev/uinput.rs new file mode 100644 index 0000000..331b2a9 --- /dev/null +++ b/espanso-inject/src/evdev/uinput.rs @@ -0,0 +1,108 @@ +/* + * 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 std::ffi::CString; + +use libc::{c_uint, close, ioctl, open, O_NONBLOCK, O_WRONLY}; +use scopeguard::ScopeGuard; + +use anyhow::Result; +use thiserror::Error; + +use super::ffi::{ + setup_uinput_device, ui_dev_create, ui_dev_destroy, ui_set_evbit, ui_set_keybit, uinput_emit, + EV_KEY, +}; + +pub struct UInputDevice { + fd: i32, +} + +impl UInputDevice { + pub fn new() -> Result { + let uinput_path = CString::new("/dev/uinput").expect("unable to generate /dev/uinput path"); + let raw_fd = unsafe { open(uinput_path.as_ptr(), O_WRONLY | O_NONBLOCK) }; + if raw_fd < 0 { + return Err(UInputDeviceError::OpenFailed().into()); + } + let fd = scopeguard::guard(raw_fd, |raw_fd| unsafe { + close(raw_fd); + }); + + // Enable keyboard events + if unsafe { ioctl(*fd, ui_set_evbit(), EV_KEY as c_uint) } != 0 { + return Err(UInputDeviceError::KeyEVBitFailed().into()); + } + + // Register all keycodes + for key_code in 0..256 { + if unsafe { ioctl(*fd, ui_set_keybit(), key_code) } != 0 { + return Err(UInputDeviceError::KeyBitFailed().into()); + } + } + + // Register the virtual device + if unsafe { setup_uinput_device(*fd) } != 0 { + return Err(UInputDeviceError::DeviceSetupFailed().into()); + } + + // Create the device + if unsafe { ioctl(*fd, ui_dev_create()) } != 0 { + return Err(UInputDeviceError::DeviceCreateFailed().into()); + } + + Ok(Self { + fd: ScopeGuard::into_inner(fd), + }) + } + + pub fn emit(&self, key_code: u32, pressed: bool) { + let pressed = if pressed { 1 } else { 0 }; + unsafe { + uinput_emit(self.fd, key_code, pressed); + } + } +} + +impl Drop for UInputDevice { + fn drop(&mut self) { + unsafe { + ioctl(self.fd, ui_dev_destroy()); + close(self.fd); + } + } +} + +#[derive(Error, Debug)] +pub enum UInputDeviceError { + #[error("could not open uinput device")] + OpenFailed(), + + #[error("could not set keyboard evbit")] + KeyEVBitFailed(), + + #[error("could not set keyboard keybit")] + KeyBitFailed(), + + #[error("could not register virtual device")] + DeviceSetupFailed(), + + #[error("could not create uinput device")] + DeviceCreateFailed(), +} diff --git a/espanso-inject/src/keys.rs b/espanso-inject/src/keys.rs index 78b5d94..987f6e5 100644 --- a/espanso-inject/src/keys.rs +++ b/espanso-inject/src/keys.rs @@ -1,3 +1,22 @@ +/* + * 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 std::fmt::Display; use regex::Regex; diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs index 3aa5444..243c3c3 100644 --- a/espanso-inject/src/lib.rs +++ b/espanso-inject/src/lib.rs @@ -27,8 +27,11 @@ mod win32; #[cfg(target_os = "linux")] mod x11; -//#[cfg(target_os = "linux")] -//mod evdev; +#[cfg(target_os = "linux")] +mod evdev; + +#[cfg(target_os = "linux")] +mod linux; #[cfg(target_os = "macos")] mod mac; @@ -72,17 +75,42 @@ impl Default for InjectionOptions { } } - #[allow(dead_code)] pub struct InjectorCreationOptions { // Only relevant in Linux systems use_evdev: bool, + + // Overwrite the list of modifiers to be scanned when + // populating the evdev injector lookup maps + evdev_modifiers: Option>, + + // Overwrite the maximum number of modifiers used tested in + // a single combination to populate the lookup maps + evdev_max_modifier_combination_len: Option, + + // Can be used to overwrite the keymap configuration + // used by espanso to inject key presses. + evdev_keyboard_rmlvo: Option, +} + +// This struct identifies the keyboard layout that +// should be used by EVDEV when loading the keymap. +// For more information: https://xkbcommon.org/doc/current/structxkb__rule__names.html +pub struct KeyboardConfig { + pub rules: Option, + pub model: Option, + pub layout: Option, + pub variant: Option, + pub options: Option, } impl Default for InjectorCreationOptions { fn default() -> Self { Self { use_evdev: false, + evdev_modifiers: None, + evdev_max_modifier_combination_len: None, + evdev_keyboard_rmlvo: None, } } } @@ -100,6 +128,7 @@ pub fn get_injector(_options: InjectorOptions) -> impl Injector { #[cfg(target_os = "linux")] pub fn get_injector(options: InjectorCreationOptions) -> Result { // TODO: differenciate based on the options - x11::X11Injector::new() + //x11::X11Injector::new() + evdev::EVDEVInjector::new(options) } diff --git a/espanso-inject/src/linux/mod.rs b/espanso-inject/src/linux/mod.rs new file mode 100644 index 0000000..9deeca4 --- /dev/null +++ b/espanso-inject/src/linux/mod.rs @@ -0,0 +1,20 @@ +/* + * 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 . + */ + +pub mod raw_keys; \ No newline at end of file diff --git a/espanso-inject/src/x11/raw_keys.rs b/espanso-inject/src/linux/raw_keys.rs similarity index 69% rename from espanso-inject/src/x11/raw_keys.rs rename to espanso-inject/src/linux/raw_keys.rs index 4899d13..26db27d 100644 --- a/espanso-inject/src/x11/raw_keys.rs +++ b/espanso-inject/src/linux/raw_keys.rs @@ -1,4 +1,25 @@ +/* + * 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::keys::Key; +use anyhow::Result; +use thiserror::Error; pub fn convert_key_to_sym(key: &Key) -> Option { match key { @@ -104,3 +125,22 @@ pub fn convert_key_to_sym(key: &Key) -> Option { Key::Raw(code) => Some(*code as u32), } } + +pub fn convert_to_sym_array(keys: &[Key]) -> Result> { + let mut virtual_keys: Vec = Vec::new(); + for key in keys.iter() { + let vk = convert_key_to_sym(key); + if let Some(vk) = vk { + virtual_keys.push(vk as u64) + } else { + return Err(LinuxRawKeyError::MappingFailure(key.clone()).into()); + } + } + Ok(virtual_keys) +} + +#[derive(Error, Debug)] +pub enum LinuxRawKeyError { + #[error("missing mapping for key `{0}`")] + MappingFailure(Key), +} \ No newline at end of file diff --git a/espanso-inject/src/mac/raw_keys.rs b/espanso-inject/src/mac/raw_keys.rs index 663632d..9e1fd6a 100644 --- a/espanso-inject/src/mac/raw_keys.rs +++ b/espanso-inject/src/mac/raw_keys.rs @@ -1,3 +1,22 @@ +/* + * 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::keys::Key; pub fn convert_key_to_vkey(key: &Key) -> Option { diff --git a/espanso-inject/src/win32/raw_keys.rs b/espanso-inject/src/win32/raw_keys.rs index 81d8576..c1374c4 100644 --- a/espanso-inject/src/win32/raw_keys.rs +++ b/espanso-inject/src/win32/raw_keys.rs @@ -1,3 +1,22 @@ +/* + * 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::keys::Key; pub fn convert_key_to_vkey(key: &Key) -> Option { diff --git a/espanso-inject/src/x11/mod.rs b/espanso-inject/src/x11/mod.rs index af32330..4e439bf 100644 --- a/espanso-inject/src/x11/mod.rs +++ b/espanso-inject/src/x11/mod.rs @@ -18,7 +18,6 @@ */ mod ffi; -mod raw_keys; use std::{ collections::HashMap, @@ -31,7 +30,7 @@ use ffi::{Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, use log::error; use anyhow::Result; -use raw_keys::convert_key_to_sym; +use crate::linux::raw_keys::{convert_key_to_sym, convert_to_sym_array}; use thiserror::Error; use crate::{keys, InjectionOptions, Injector}; @@ -146,19 +145,6 @@ impl X11Injector { (char_map, sym_map) } - fn convert_to_sym_array(keys: &[keys::Key]) -> Result> { - let mut virtual_keys: Vec = Vec::new(); - for key in keys.iter() { - let vk = convert_key_to_sym(key); - if let Some(vk) = vk { - virtual_keys.push(vk as u64) - } else { - return Err(X11InjectorError::MappingFailure(key.clone()).into()); - } - } - Ok(virtual_keys) - } - fn convert_to_record_array(&self, syms: &[KeySym]) -> Result> { syms .iter() @@ -371,7 +357,7 @@ impl Injector for X11Injector { let focused_window = self.get_focused_window(); // Compute all the key record sequence first to make sure a mapping is available - let syms = Self::convert_to_sym_array(keys)?; + let syms = convert_to_sym_array(keys)?; let records = self.convert_to_record_array(&syms)?; if options.disable_fast_inject { @@ -397,7 +383,7 @@ impl Injector for X11Injector { let focused_window = self.get_focused_window(); // Compute all the key record sequence first to make sure a mapping is available - let syms = Self::convert_to_sym_array(keys)?; + let syms = convert_to_sym_array(keys)?; let records = self.convert_to_record_array(&syms)?; // Render the correct modifier mask for the given sequence @@ -439,9 +425,6 @@ pub enum X11InjectorError { #[error("missing vkey mapping for char `{0}`")] CharMappingFailure(String), - #[error("missing sym mapping for key `{0}`")] - MappingFailure(keys::Key), - #[error("missing record mapping for sym `{0}`")] SymMappingFailure(u64), } \ No newline at end of file