First draft of evdev inject backend
This commit is contained in:
parent
ff6bfa20cb
commit
a9d24d400d
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -125,6 +125,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-as-inner"
|
name = "enum-as-inner"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -171,6 +177,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cc",
|
"cc",
|
||||||
"enum-as-inner",
|
"enum-as-inner",
|
||||||
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -216,6 +223,15 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
|
|
@ -29,6 +29,7 @@ use anyhow::Result;
|
||||||
use context::Context;
|
use context::Context;
|
||||||
use device::{get_devices, Device};
|
use device::{get_devices, Device};
|
||||||
use keymap::Keymap;
|
use keymap::Keymap;
|
||||||
|
use lazycell::LazyCell;
|
||||||
use libc::{
|
use libc::{
|
||||||
__errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD,
|
__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<dyn Fn(InputEvent)>;
|
pub type EVDEVSourceCallback = Box<dyn Fn(InputEvent)>;
|
||||||
pub struct EVDEVSource {
|
pub struct EVDEVSource {
|
||||||
devices: Vec<Device>,
|
devices: Vec<Device>,
|
||||||
|
|
||||||
|
_context: LazyCell<Context>,
|
||||||
|
_keymap: LazyCell<Keymap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::new_without_default)]
|
#[allow(clippy::new_without_default)]
|
||||||
|
@ -58,12 +62,15 @@ impl EVDEVSource {
|
||||||
pub fn new() -> EVDEVSource {
|
pub fn new() -> EVDEVSource {
|
||||||
Self {
|
Self {
|
||||||
devices: Vec::new(),
|
devices: Vec::new(),
|
||||||
|
_context: LazyCell::new(),
|
||||||
|
_keymap: LazyCell::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize(&mut self) -> Result<()> {
|
pub fn initialize(&mut self) -> Result<()> {
|
||||||
let context = Context::new().expect("unable to obtain xkb context");
|
let context = Context::new().expect("unable to obtain xkb context");
|
||||||
let keymap = Keymap::new(&context).expect("unable to create xkb keymap");
|
let keymap = Keymap::new(&context).expect("unable to create xkb keymap");
|
||||||
|
|
||||||
match get_devices(&keymap) {
|
match get_devices(&keymap) {
|
||||||
Ok(devices) => self.devices = devices,
|
Ok(devices) => self.devices = devices,
|
||||||
Err(error) => {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +164,9 @@ impl EVDEVSource {
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum EVDEVSourceError {
|
pub enum EVDEVSourceError {
|
||||||
|
#[error("initialization failed")]
|
||||||
|
InitFailure(),
|
||||||
|
|
||||||
#[error("permission denied")]
|
#[error("permission denied")]
|
||||||
PermissionDenied(),
|
PermissionDenied(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ widestring = "0.4.3"
|
||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
libc = "0.2.85"
|
libc = "0.2.85"
|
||||||
scopeguard = "1.1.0"
|
scopeguard = "1.1.0"
|
||||||
|
itertools = "0.10.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0.66"
|
cc = "1.0.66"
|
||||||
|
|
|
@ -39,19 +39,20 @@ fn cc_config() {
|
||||||
// println!("cargo:rerun-if-changed=src/x11/native.h");
|
// 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.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.h");
|
||||||
|
println!("cargo:rerun-if-changed=src/evdev/native.c");
|
||||||
// cc::Build::new()
|
// cc::Build::new()
|
||||||
// .cpp(true)
|
// .cpp(true)
|
||||||
// .include("src/x11/native.h")
|
// .include("src/x11/native.h")
|
||||||
// .file("src/x11/native.cpp")
|
// .file("src/x11/native.cpp")
|
||||||
// .compile("espansoinject");
|
// .compile("espansoinject");
|
||||||
// cc::Build::new()
|
cc::Build::new()
|
||||||
// .cpp(true)
|
.include("src/evdev/native.h")
|
||||||
// .include("src/evdev/native.h")
|
.file("src/evdev/native.c")
|
||||||
// .file("src/evdev/native.cpp")
|
.compile("espansoinjectev");
|
||||||
// .compile("espansoinjectevdev");
|
|
||||||
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
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=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=X11");
|
||||||
println!("cargo:rustc-link-lib=dylib=Xtst");
|
println!("cargo:rustc-link-lib=dylib=Xtst");
|
||||||
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
||||||
|
|
|
@ -38,6 +38,8 @@ The available modifiers can be found in the https://github.com/torvalds/linux/bl
|
||||||
#define KEY_RIGHTMETA 126
|
#define KEY_RIGHTMETA 126
|
||||||
#define KEY_RIGHTCTRL 97
|
#define KEY_RIGHTCTRL 97
|
||||||
#define KEY_RIGHTALT 100
|
#define KEY_RIGHTALT 100
|
||||||
|
#define KEY_CAPSLOCK 58
|
||||||
|
#define KEY_NUMLOCK 69
|
||||||
|
|
||||||
All these codes have to be added the EVDEV_OFFSET = 8
|
All these codes have to be added the EVDEV_OFFSET = 8
|
||||||
|
|
||||||
|
|
47
espanso-inject/src/evdev/context.rs
Normal file
47
espanso-inject/src/evdev/context.rs
Normal file
|
@ -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<Context> {
|
||||||
|
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(),
|
||||||
|
}
|
84
espanso-inject/src/evdev/ffi.rs
Normal file
84
espanso-inject/src/evdev/ffi.rs
Normal file
|
@ -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);
|
||||||
|
}
|
86
espanso-inject/src/evdev/keymap.rs
Normal file
86
espanso-inject/src/evdev/keymap.rs
Normal file
|
@ -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<KeyboardConfig>) -> Result<Keymap> {
|
||||||
|
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(),
|
||||||
|
}
|
332
espanso-inject/src/evdev/mod.rs
Normal file
332
espanso-inject/src/evdev/mod.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type CharMap = HashMap<String, KeyRecord>;
|
||||||
|
type SymMap = HashMap<KeySym, KeyRecord>;
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
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<Vec<u32>> {
|
||||||
|
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<Vec<KeyRecord>> {
|
||||||
|
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<Vec<KeyRecord>> = 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<u32> = 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),
|
||||||
|
}
|
73
espanso-inject/src/evdev/native.c
Normal file
73
espanso-inject/src/evdev/native.c
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "native.h"
|
||||||
|
#include <linux/uinput.h>
|
||||||
|
#include <memory.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
31
espanso-inject/src/evdev/native.h
Normal file
31
espanso-inject/src/evdev/native.h
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
89
espanso-inject/src/evdev/state.rs
Normal file
89
espanso-inject/src/evdev/state.rs
Normal file
|
@ -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<State> {
|
||||||
|
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<String> {
|
||||||
|
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<u32> {
|
||||||
|
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(),
|
||||||
|
}
|
108
espanso-inject/src/evdev/uinput.rs
Normal file
108
espanso-inject/src/evdev/uinput.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<UInputDevice> {
|
||||||
|
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(),
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
|
@ -27,8 +27,11 @@ mod win32;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod x11;
|
mod x11;
|
||||||
|
|
||||||
//#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
//mod evdev;
|
mod evdev;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod mac;
|
mod mac;
|
||||||
|
@ -72,17 +75,42 @@ impl Default for InjectionOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct InjectorCreationOptions {
|
pub struct InjectorCreationOptions {
|
||||||
// Only relevant in Linux systems
|
// Only relevant in Linux systems
|
||||||
use_evdev: bool,
|
use_evdev: bool,
|
||||||
|
|
||||||
|
// Overwrite the list of modifiers to be scanned when
|
||||||
|
// populating the evdev injector lookup maps
|
||||||
|
evdev_modifiers: Option<Vec<u32>>,
|
||||||
|
|
||||||
|
// Overwrite the maximum number of modifiers used tested in
|
||||||
|
// a single combination to populate the lookup maps
|
||||||
|
evdev_max_modifier_combination_len: Option<i32>,
|
||||||
|
|
||||||
|
// Can be used to overwrite the keymap configuration
|
||||||
|
// used by espanso to inject key presses.
|
||||||
|
evdev_keyboard_rmlvo: Option<KeyboardConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub layout: Option<String>,
|
||||||
|
pub variant: Option<String>,
|
||||||
|
pub options: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InjectorCreationOptions {
|
impl Default for InjectorCreationOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
use_evdev: false,
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn get_injector(options: InjectorCreationOptions) -> Result<impl Injector> {
|
pub fn get_injector(options: InjectorCreationOptions) -> Result<impl Injector> {
|
||||||
// TODO: differenciate based on the options
|
// TODO: differenciate based on the options
|
||||||
x11::X11Injector::new()
|
//x11::X11Injector::new()
|
||||||
|
evdev::EVDEVInjector::new(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
espanso-inject/src/linux/mod.rs
Normal file
20
espanso-inject/src/linux/mod.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod raw_keys;
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use crate::keys::Key;
|
use crate::keys::Key;
|
||||||
|
use anyhow::Result;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
pub fn convert_key_to_sym(key: &Key) -> Option<u32> {
|
pub fn convert_key_to_sym(key: &Key) -> Option<u32> {
|
||||||
match key {
|
match key {
|
||||||
|
@ -104,3 +125,22 @@ pub fn convert_key_to_sym(key: &Key) -> Option<u32> {
|
||||||
Key::Raw(code) => Some(*code as u32),
|
Key::Raw(code) => Some(*code as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn convert_to_sym_array(keys: &[Key]) -> Result<Vec<u64>> {
|
||||||
|
let mut virtual_keys: Vec<u64> = 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),
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use crate::keys::Key;
|
use crate::keys::Key;
|
||||||
|
|
||||||
pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
|
pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use crate::keys::Key;
|
use crate::keys::Key;
|
||||||
|
|
||||||
pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
|
pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
mod ffi;
|
mod ffi;
|
||||||
mod raw_keys;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
@ -31,7 +30,7 @@ use ffi::{Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay,
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
use anyhow::Result;
|
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 thiserror::Error;
|
||||||
|
|
||||||
use crate::{keys, InjectionOptions, Injector};
|
use crate::{keys, InjectionOptions, Injector};
|
||||||
|
@ -146,19 +145,6 @@ impl X11Injector {
|
||||||
(char_map, sym_map)
|
(char_map, sym_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_to_sym_array(keys: &[keys::Key]) -> Result<Vec<u64>> {
|
|
||||||
let mut virtual_keys: Vec<u64> = 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<Vec<KeyRecord>> {
|
fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
|
||||||
syms
|
syms
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -371,7 +357,7 @@ impl Injector for X11Injector {
|
||||||
let focused_window = self.get_focused_window();
|
let focused_window = self.get_focused_window();
|
||||||
|
|
||||||
// Compute all the key record sequence first to make sure a mapping is available
|
// 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)?;
|
let records = self.convert_to_record_array(&syms)?;
|
||||||
|
|
||||||
if options.disable_fast_inject {
|
if options.disable_fast_inject {
|
||||||
|
@ -397,7 +383,7 @@ impl Injector for X11Injector {
|
||||||
let focused_window = self.get_focused_window();
|
let focused_window = self.get_focused_window();
|
||||||
|
|
||||||
// Compute all the key record sequence first to make sure a mapping is available
|
// 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)?;
|
let records = self.convert_to_record_array(&syms)?;
|
||||||
|
|
||||||
// Render the correct modifier mask for the given sequence
|
// Render the correct modifier mask for the given sequence
|
||||||
|
@ -439,9 +425,6 @@ pub enum X11InjectorError {
|
||||||
#[error("missing vkey mapping for char `{0}`")]
|
#[error("missing vkey mapping for char `{0}`")]
|
||||||
CharMappingFailure(String),
|
CharMappingFailure(String),
|
||||||
|
|
||||||
#[error("missing sym mapping for key `{0}`")]
|
|
||||||
MappingFailure(keys::Key),
|
|
||||||
|
|
||||||
#[error("missing record mapping for sym `{0}`")]
|
#[error("missing record mapping for sym `{0}`")]
|
||||||
SymMappingFailure(u64),
|
SymMappingFailure(u64),
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user