Initial draft of wayland event source
This commit is contained in:
parent
1a21a81ace
commit
1d6b152c15
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -142,10 +142,14 @@ dependencies = [
|
|||
name = "espanso-detect"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
"enum-as-inner",
|
||||
"lazycell",
|
||||
"libc",
|
||||
"log",
|
||||
"scopeguard",
|
||||
"thiserror",
|
||||
"widestring",
|
||||
]
|
||||
|
||||
|
@ -204,9 +208,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.84"
|
||||
version = "0.2.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff"
|
||||
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
|
@ -377,6 +381,12 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.123"
|
||||
|
|
|
@ -12,6 +12,12 @@ lazycell = "1.3.0"
|
|||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "0.4.3"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
libc = "0.2.85"
|
||||
anyhow = "1.0.38"
|
||||
thiserror = "1.0.23"
|
||||
scopeguard = "1.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.66"
|
||||
|
||||
|
|
|
@ -37,15 +37,24 @@ fn cc_config() {
|
|||
fn cc_config() {
|
||||
println!("cargo:rerun-if-changed=src/x11/native.cpp");
|
||||
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");
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("src/x11/native.h")
|
||||
.file("src/x11/native.cpp")
|
||||
.compile("espansodetect");
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("src/evdev/native.h")
|
||||
.file("src/evdev/native.cpp")
|
||||
.compile("espansodetectevdev");
|
||||
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
||||
println!("cargo:rustc-link-lib=static=espansodetect");
|
||||
println!("cargo:rustc-link-lib=static=espansodetectevdev");
|
||||
println!("cargo:rustc-link-lib=dylib=X11");
|
||||
println!("cargo:rustc-link-lib=dylib=Xtst");
|
||||
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
49
espanso-detect/src/evdev/context.rs
Normal file
49
espanso-detect/src/evdev/context.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
// 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_NO_FLAGS, xkb_context, xkb_context_new, xkb_context_unref};
|
||||
use thiserror::Error;
|
||||
use anyhow::Result;
|
||||
|
||||
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(),
|
||||
}
|
248
espanso-detect/src/evdev/device.rs
Normal file
248
espanso-detect/src/evdev/device.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
// This code is a port of the libxkbcommon "interactive-evdev.c" example
|
||||
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
|
||||
|
||||
use anyhow::Result;
|
||||
use libc::{input_event, size_t, ssize_t, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY};
|
||||
use log::{trace};
|
||||
use scopeguard::ScopeGuard;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::{
|
||||
ffi::{c_void, CStr},
|
||||
fs::OpenOptions,
|
||||
mem::zeroed,
|
||||
};
|
||||
use std::{fs::File, os::unix::fs::OpenOptionsExt};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{
|
||||
ffi::{
|
||||
is_keyboard, xkb_key_direction, xkb_keycode_t, xkb_keymap_key_repeats,
|
||||
xkb_state, xkb_state_get_keymap, xkb_state_key_get_one_sym,
|
||||
xkb_state_key_get_utf8, xkb_state_new, xkb_state_unref,
|
||||
xkb_state_update_key, EV_KEY,
|
||||
},
|
||||
keymap::Keymap,
|
||||
};
|
||||
|
||||
const EVDEV_OFFSET: i32 = 8;
|
||||
const KEY_STATE_RELEASE: i32 = 0;
|
||||
const KEY_STATE_PRESS: i32 = 1;
|
||||
const KEY_STATE_REPEAT: i32 = 2;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RawInputEvent {
|
||||
Keyboard(KeyboardEvent),
|
||||
Mouse(MouseEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyboardEvent {
|
||||
pub sym: u32,
|
||||
pub value: String,
|
||||
pub is_down: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MouseEvent {
|
||||
pub code: u16,
|
||||
pub is_down: bool,
|
||||
}
|
||||
|
||||
pub struct Device {
|
||||
path: String,
|
||||
file: File,
|
||||
state: *mut xkb_state,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn from(path: &str, keymap: &Keymap) -> Result<Device> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(O_NONBLOCK | O_CLOEXEC | O_RDONLY)
|
||||
.open(&path)?;
|
||||
|
||||
if unsafe { is_keyboard(file.as_raw_fd()) == 0 } {
|
||||
return Err(DeviceError::InvalidDevice(path.to_string()).into());
|
||||
}
|
||||
|
||||
let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
|
||||
// Automatically close the state if the function does not return correctly
|
||||
let state = scopeguard::guard(raw_state, |raw_state| {
|
||||
unsafe {
|
||||
xkb_state_unref(raw_state);
|
||||
}
|
||||
});
|
||||
|
||||
if raw_state.is_null() {
|
||||
return Err(DeviceError::InvalidState(path.to_string()).into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.to_string(),
|
||||
file,
|
||||
// Release the state without freeing it
|
||||
state: ScopeGuard::into_inner(state),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> *mut xkb_state {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn get_raw_fd(&self) -> i32 {
|
||||
self.file.as_raw_fd()
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> String {
|
||||
self.path.to_string()
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Result<Vec<RawInputEvent>> {
|
||||
let errno_ptr = unsafe { libc::__errno_location() };
|
||||
let mut len: ssize_t;
|
||||
let mut evs: [input_event; 16] = unsafe { std::mem::zeroed() };
|
||||
let mut events = Vec::new();
|
||||
|
||||
loop {
|
||||
len = unsafe {
|
||||
libc::read(
|
||||
self.file.as_raw_fd(),
|
||||
evs.as_mut_ptr() as *mut c_void,
|
||||
std::mem::size_of_val(&evs),
|
||||
)
|
||||
};
|
||||
if len <= 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let nevs: size_t = len as usize / std::mem::size_of::<input_event>();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..nevs {
|
||||
let event = self.process_event(evs[i].type_, evs[i].code, evs[i].value);
|
||||
if let Some(event) = event {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len < 0 && unsafe { *errno_ptr } != EWOULDBLOCK {
|
||||
return Err(DeviceError::BlockingReadOperation().into());
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
fn process_event(&self, _type: u16, code: u16, value: i32) -> Option<RawInputEvent> {
|
||||
if _type != EV_KEY {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_down = value == KEY_STATE_PRESS;
|
||||
|
||||
// Check if the current event originated from a mouse
|
||||
if code >= 0x110 && code <= 0x117 {
|
||||
// Mouse event
|
||||
return Some(RawInputEvent::Mouse(MouseEvent { code, is_down }));
|
||||
}
|
||||
|
||||
// Keyboard event
|
||||
|
||||
let keycode: xkb_keycode_t = EVDEV_OFFSET as u32 + code as u32;
|
||||
let keymap = unsafe { xkb_state_get_keymap(self.get_state()) };
|
||||
|
||||
if value == KEY_STATE_REPEAT && unsafe { xkb_keymap_key_repeats(keymap, keycode) } != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sym = unsafe { xkb_state_key_get_one_sym(self.get_state(), keycode) };
|
||||
if sym == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the utf8 char
|
||||
let mut buffer: [u8; 16] = [0; 16];
|
||||
unsafe {
|
||||
xkb_state_key_get_utf8(
|
||||
self.get_state(),
|
||||
keycode,
|
||||
buffer.as_mut_ptr() as *mut i8,
|
||||
std::mem::size_of_val(&buffer),
|
||||
)
|
||||
};
|
||||
let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) };
|
||||
let content = content_raw.to_string_lossy().to_string();
|
||||
|
||||
let event = KeyboardEvent {
|
||||
is_down,
|
||||
sym,
|
||||
value: content,
|
||||
};
|
||||
|
||||
if value == KEY_STATE_RELEASE {
|
||||
unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::UP) };
|
||||
} else {
|
||||
unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::DOWN) };
|
||||
}
|
||||
|
||||
Some(RawInputEvent::Keyboard(event))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Device {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
xkb_state_unref(self.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_devices(keymap: &Keymap) -> Result<Vec<Device>> {
|
||||
let mut keyboards = Vec::new();
|
||||
let dirs = std::fs::read_dir("/dev/input/")?;
|
||||
for entry in dirs {
|
||||
match entry {
|
||||
Ok(device) => {
|
||||
// Skip non-eventX devices
|
||||
if !device.file_name().to_string_lossy().starts_with("event") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = device.path().to_string_lossy().to_string();
|
||||
let keyboard = Device::from(&path, keymap);
|
||||
match keyboard {
|
||||
Ok(keyboard) => {
|
||||
keyboards.push(keyboard);
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("error opening keyboard: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("could not read keyboard device: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keyboards.is_empty() {
|
||||
return Err(DeviceError::NoDevicesFound().into());
|
||||
}
|
||||
|
||||
Ok(keyboards)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DeviceError {
|
||||
#[error("could not create xkb state for `{0}`")]
|
||||
InvalidState(String),
|
||||
|
||||
#[error("`{0}` is not a valid device")]
|
||||
InvalidDevice(String),
|
||||
|
||||
#[error("no devices found")]
|
||||
NoDevicesFound(),
|
||||
|
||||
#[error("read operation can't block device")]
|
||||
BlockingReadOperation(),
|
||||
}
|
77
espanso-detect/src/evdev/ffi.rs
Normal file
77
espanso-detect/src/evdev/ffi.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
// 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;
|
||||
|
||||
#[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_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_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;
|
||||
}
|
||||
|
||||
#[link(name = "espansodetectevdev", kind = "static")]
|
||||
extern "C" {
|
||||
pub fn is_keyboard(fd: i32) -> i32;
|
||||
}
|
50
espanso-detect/src/evdev/keymap.rs
Normal file
50
espanso-detect/src/evdev/keymap.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
// 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 thiserror::Error;
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{context::Context, ffi::{XKB_KEYMAP_COMPILE_NO_FLAGS, xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref}};
|
||||
|
||||
pub struct Keymap {
|
||||
keymap: *mut xkb_keymap,
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
pub fn new(context: &Context) -> Result<Keymap> {
|
||||
let raw_keymap = unsafe { xkb_keymap_new_from_names(context.get_handle(), std::ptr::null(), 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
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
333
espanso-detect/src/evdev/mod.rs
Normal file
333
espanso-detect/src/evdev/mod.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
// This code is a port of the libxkbcommon "interactive-evdev.c" example
|
||||
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
|
||||
|
||||
/*
|
||||
* 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 device;
|
||||
mod ffi;
|
||||
mod keymap;
|
||||
|
||||
use context::Context;
|
||||
use device::{get_devices, Device};
|
||||
use keymap::Keymap;
|
||||
use libc::{
|
||||
__errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD,
|
||||
};
|
||||
use log::{error, trace};
|
||||
|
||||
use crate::event::Status::*;
|
||||
use crate::event::Variant::*;
|
||||
use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
|
||||
use crate::event::{Key::*, MouseButton, MouseEvent};
|
||||
|
||||
use self::device::{DeviceError, RawInputEvent};
|
||||
|
||||
const BTN_LEFT: u16 = 0x110;
|
||||
const BTN_RIGHT: u16 = 0x111;
|
||||
const BTN_MIDDLE: u16 = 0x112;
|
||||
const BTN_SIDE: u16 = 0x113;
|
||||
const BTN_EXTRA: u16 = 0x114;
|
||||
|
||||
pub type EVDEVSourceCallback = Box<dyn Fn(InputEvent)>;
|
||||
pub struct EVDEVSource {
|
||||
devices: Vec<Device>,
|
||||
}
|
||||
|
||||
#[allow(clippy::new_without_default)]
|
||||
impl EVDEVSource {
|
||||
pub fn new() -> EVDEVSource {
|
||||
Self {
|
||||
devices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
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) => {
|
||||
if let Some(device_error) = error.downcast_ref::<DeviceError>() {
|
||||
if matches!(device_error, DeviceError::NoDevicesFound()) {
|
||||
error!("Unable to open EVDEV devices, this usually has to do with permissions.");
|
||||
error!(
|
||||
"You can either add the current user to the 'input' group or run espanso as root"
|
||||
);
|
||||
}
|
||||
}
|
||||
panic!("error when initilizing EVDEV source {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventloop(&self, event_callback: EVDEVSourceCallback) {
|
||||
if self.devices.is_empty() {
|
||||
panic!("can't start eventloop without evdev devices");
|
||||
}
|
||||
|
||||
let raw_epfd = unsafe { libc::epoll_create1(0) };
|
||||
let epfd = scopeguard::guard(raw_epfd, |raw_epfd| unsafe {
|
||||
close(raw_epfd);
|
||||
});
|
||||
|
||||
if *epfd < 0 {
|
||||
panic!("could not create epoll instance");
|
||||
}
|
||||
|
||||
// Setup epoll for all input devices
|
||||
let errno_ptr = unsafe { __errno_location() };
|
||||
for (i, device) in self.devices.iter().enumerate() {
|
||||
let mut ev: epoll_event = unsafe { std::mem::zeroed() };
|
||||
ev.events = EPOLLIN as u32;
|
||||
ev.u64 = i as u64;
|
||||
if unsafe { epoll_ctl(*epfd, EPOLL_CTL_ADD, device.get_raw_fd(), &mut ev) } != 0 {
|
||||
panic!(format!(
|
||||
"Could not add {} to epoll, errno {}",
|
||||
device.get_path(),
|
||||
unsafe { *errno_ptr }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Read events indefinitely
|
||||
let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() };
|
||||
loop {
|
||||
let ret = unsafe { epoll_wait(*epfd, evs.as_mut_ptr(), 16, -1) };
|
||||
if ret < 0 {
|
||||
if unsafe { *errno_ptr } == EINTR {
|
||||
continue;
|
||||
} else {
|
||||
panic!(format!("Could not poll for events, {}", unsafe {
|
||||
*errno_ptr
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
for ev in evs.iter() {
|
||||
let device = &self.devices[ev.u64 as usize];
|
||||
match device.read() {
|
||||
Ok(events) if !events.is_empty() => {
|
||||
// Convert raw events to the common format and invoke the callback
|
||||
events.into_iter().for_each(|raw_event| {
|
||||
let event: Option<InputEvent> = raw_event.into();
|
||||
if let Some(event) = event {
|
||||
event_callback(event);
|
||||
} else {
|
||||
trace!("unable to convert raw event to input event");
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(_) => { /* SKIP EMPTY */ }
|
||||
Err(err) => error!("Can't read from device {}: {}", device.get_path(), err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawInputEvent> for Option<InputEvent> {
|
||||
fn from(raw: RawInputEvent) -> Option<InputEvent> {
|
||||
match raw {
|
||||
RawInputEvent::Keyboard(keyboard_event) => {
|
||||
let (key, variant) = key_sym_to_key(keyboard_event.sym as i32);
|
||||
let value = if keyboard_event.value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(keyboard_event.value)
|
||||
};
|
||||
|
||||
let status = if keyboard_event.is_down { Pressed } else { Released };
|
||||
|
||||
return Some(InputEvent::Keyboard(KeyboardEvent {
|
||||
key,
|
||||
value,
|
||||
status,
|
||||
variant,
|
||||
}));
|
||||
}
|
||||
RawInputEvent::Mouse(mouse_event) => {
|
||||
let button = raw_to_mouse_button(mouse_event.code);
|
||||
|
||||
let status = if mouse_event.is_down { Pressed } else { Released };
|
||||
|
||||
if let Some(button) = button {
|
||||
return Some(InputEvent::Mouse(MouseEvent { button, status }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
|
||||
match key_sym {
|
||||
// Modifiers
|
||||
0xFFE9 => (Alt, Some(Left)),
|
||||
0xFFEA => (Alt, Some(Right)),
|
||||
0xFFE5 => (CapsLock, None),
|
||||
0xFFE3 => (Control, Some(Left)),
|
||||
0xFFE4 => (Control, Some(Right)),
|
||||
0xFFE7 | 0xFFEB => (Meta, Some(Left)),
|
||||
0xFFE8 | 0xFFEC => (Meta, Some(Right)),
|
||||
0xFF7F => (NumLock, None),
|
||||
0xFFE1 => (Shift, Some(Left)),
|
||||
0xFFE2 => (Shift, Some(Right)),
|
||||
|
||||
// Whitespace
|
||||
0xFF0D => (Enter, None),
|
||||
0xFF09 => (Tab, None),
|
||||
0x20 => (Space, None),
|
||||
|
||||
// Navigation
|
||||
0xFF54 => (ArrowDown, None),
|
||||
0xFF51 => (ArrowLeft, None),
|
||||
0xFF53 => (ArrowRight, None),
|
||||
0xFF52 => (ArrowUp, None),
|
||||
0xFF57 => (End, None),
|
||||
0xFF50 => (Home, None),
|
||||
0xFF56 => (PageDown, None),
|
||||
0xFF55 => (PageUp, None),
|
||||
|
||||
// Editing keys
|
||||
0xFF08 => (Backspace, None),
|
||||
|
||||
// Function keys
|
||||
0xFFBE => (F1, None),
|
||||
0xFFBF => (F2, None),
|
||||
0xFFC0 => (F3, None),
|
||||
0xFFC1 => (F4, None),
|
||||
0xFFC2 => (F5, None),
|
||||
0xFFC3 => (F6, None),
|
||||
0xFFC4 => (F7, None),
|
||||
0xFFC5 => (F8, None),
|
||||
0xFFC6 => (F9, None),
|
||||
0xFFC7 => (F10, None),
|
||||
0xFFC8 => (F11, None),
|
||||
0xFFC9 => (F12, None),
|
||||
0xFFCA => (F13, None),
|
||||
0xFFCB => (F14, None),
|
||||
0xFFCC => (F15, None),
|
||||
0xFFCD => (F16, None),
|
||||
0xFFCE => (F17, None),
|
||||
0xFFCF => (F18, None),
|
||||
0xFFD0 => (F19, None),
|
||||
0xFFD1 => (F20, None),
|
||||
|
||||
// Other keys, includes the raw code provided by the operating system
|
||||
_ => (Other(key_sym), None),
|
||||
}
|
||||
}
|
||||
|
||||
// These codes can be found in the "input-event-codes.h" header file
|
||||
fn raw_to_mouse_button(raw: u16) -> Option<MouseButton> {
|
||||
match raw {
|
||||
BTN_LEFT => Some(MouseButton::Left),
|
||||
BTN_RIGHT=> Some(MouseButton::Right),
|
||||
BTN_MIDDLE=> Some(MouseButton::Middle),
|
||||
BTN_SIDE=> Some(MouseButton::Button1),
|
||||
BTN_EXTRA=> Some(MouseButton::Button2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO convert tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::CString;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn default_raw_input_event() -> RawInputEvent {
|
||||
RawInputEvent {
|
||||
event_type: INPUT_EVENT_TYPE_KEYBOARD,
|
||||
buffer: [0; 24],
|
||||
buffer_len: 0,
|
||||
key_code: 0,
|
||||
key_sym: 0,
|
||||
status: INPUT_STATUS_PRESSED,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_to_input_event_keyboard_works_correctly() {
|
||||
let c_string = CString::new("k".to_string()).unwrap();
|
||||
let mut buffer: [u8; 24] = [0; 24];
|
||||
buffer[..1].copy_from_slice(c_string.as_bytes());
|
||||
|
||||
let mut raw = default_raw_input_event();
|
||||
raw.buffer = buffer;
|
||||
raw.buffer_len = 1;
|
||||
raw.status = INPUT_STATUS_RELEASED;
|
||||
raw.key_sym = 0x4B;
|
||||
|
||||
let result: Option<InputEvent> = raw.into();
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
InputEvent::Keyboard(KeyboardEvent {
|
||||
key: Other(0x4B),
|
||||
status: Released,
|
||||
value: Some("k".to_string()),
|
||||
variant: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_to_input_event_mouse_works_correctly() {
|
||||
let mut raw = default_raw_input_event();
|
||||
raw.event_type = INPUT_EVENT_TYPE_MOUSE;
|
||||
raw.status = INPUT_STATUS_RELEASED;
|
||||
raw.key_code = INPUT_MOUSE_RIGHT_BUTTON;
|
||||
|
||||
let result: Option<InputEvent> = raw.into();
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
InputEvent::Mouse(MouseEvent {
|
||||
status: Released,
|
||||
button: MouseButton::Right,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_to_input_invalid_buffer() {
|
||||
let buffer: [u8; 24] = [123; 24];
|
||||
|
||||
let mut raw = default_raw_input_event();
|
||||
raw.buffer = buffer;
|
||||
raw.buffer_len = 5;
|
||||
|
||||
let result: Option<InputEvent> = raw.into();
|
||||
assert!(result.unwrap().into_keyboard().unwrap().value.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_to_input_event_returns_none_when_missing_type() {
|
||||
let mut raw = default_raw_input_event();
|
||||
raw.event_type = 0;
|
||||
let result: Option<InputEvent> = raw.into();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
84
espanso-detect/src/evdev/native.cpp
Normal file
84
espanso-detect/src/evdev/native.cpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
// A good portion of the following code has been taken by the "interactive-evdev.c"
|
||||
// example of "libxkbcommon" by Ran Benita. The original license is included as follows:
|
||||
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
|
||||
|
||||
/*
|
||||
* Copyright © 2012 Ran Benita <ran234@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "native.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <fnmatch.h>
|
||||
#include <getopt.h>
|
||||
#include <limits.h>
|
||||
#include <locale.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <string>
|
||||
|
||||
#include <sys/epoll.h>
|
||||
#include <linux/input.h>
|
||||
|
||||
#include "xkbcommon/xkbcommon.h"
|
||||
|
||||
#define NLONGS(n) (((n) + LONG_BIT - 1) / LONG_BIT)
|
||||
|
||||
static bool
|
||||
evdev_bit_is_set(const unsigned long *array, int bit)
|
||||
{
|
||||
return array[bit / LONG_BIT] & (1LL << (bit % LONG_BIT));
|
||||
}
|
||||
|
||||
/* Some heuristics to see if the device is a keyboard. */
|
||||
int32_t is_keyboard(int fd)
|
||||
{
|
||||
int i;
|
||||
unsigned long evbits[NLONGS(EV_CNT)] = {0};
|
||||
unsigned long keybits[NLONGS(KEY_CNT)] = {0};
|
||||
|
||||
errno = 0;
|
||||
ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), evbits);
|
||||
if (errno)
|
||||
return false;
|
||||
|
||||
if (!evdev_bit_is_set(evbits, EV_KEY))
|
||||
return false;
|
||||
|
||||
errno = 0;
|
||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybits)), keybits);
|
||||
if (errno)
|
||||
return false;
|
||||
|
||||
for (i = KEY_RESERVED; i <= KEY_MIN_INTERESTING; i++)
|
||||
if (evdev_bit_is_set(keybits, i))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
27
espanso-detect/src/evdev/native.h
Normal file
27
espanso-detect/src/evdev/native.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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_DETECT_EVDEV_H
|
||||
#define ESPANSO_DETECT_EVDEV_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern "C" int32_t is_keyboard(int fd);
|
||||
|
||||
#endif //ESPANSO_DETECT_EVDEV_H
|
|
@ -23,4 +23,7 @@ pub mod event;
|
|||
pub mod win32;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod x11;
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod evdev;
|
|
@ -38,7 +38,7 @@ fn main() {
|
|||
|
||||
let handle = std::thread::spawn(move || {
|
||||
//let mut source = espanso_detect::win32::Win32Source::new();
|
||||
let mut source = espanso_detect::x11::X11Source::new();
|
||||
let mut source = espanso_detect::evdev::EVDEVSource::new();
|
||||
source.initialize();
|
||||
source.eventloop(Box::new(move |event: InputEvent| {
|
||||
println!("ev {:?}", event);
|
||||
|
|
75
espanso/src/main_old.rs
Normal file
75
espanso/src/main_old.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use espanso_detect::event::{InputEvent, Status};
|
||||
use espanso_ui::{linux::LinuxUIOptions, menu::*};
|
||||
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!z");
|
||||
CombinedLogger::init(vec![
|
||||
TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
|
||||
// WriteLogger::new(
|
||||
// LevelFilter::Info,
|
||||
// Config::default(),
|
||||
// File::create("my_rust_binary.log").unwrap(),
|
||||
// ),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let icon_paths = vec![
|
||||
(
|
||||
espanso_ui::icons::TrayIcon::Normal,
|
||||
r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
|
||||
),
|
||||
(
|
||||
espanso_ui::icons::TrayIcon::Disabled,
|
||||
r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
// let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions {
|
||||
// show_icon: true,
|
||||
// icon_paths: &icon_paths,
|
||||
// notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
|
||||
// .to_string(),
|
||||
// });
|
||||
let (remote, mut eventloop) = espanso_ui::linux::create(LinuxUIOptions {
|
||||
notification_icon_path: r"/home/freddy/insync/Development/Espanso/Images/icongreensmall.png".to_owned(),
|
||||
});
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
//let mut source = espanso_detect::win32::Win32Source::new();
|
||||
let mut source = espanso_detect::x11::X11Source::new();
|
||||
source.initialize();
|
||||
source.eventloop(Box::new(move |event: InputEvent| {
|
||||
println!("ev {:?}", event);
|
||||
match event {
|
||||
InputEvent::Mouse(_) => {}
|
||||
InputEvent::Keyboard(evt) => {
|
||||
if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
|
||||
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
|
||||
remote.show_notification("Espanso is running!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// eventloop.initialize();
|
||||
// eventloop.run(Box::new(move |event| {
|
||||
// println!("ui {:?}", event);
|
||||
// let menu = Menu::from(vec![
|
||||
// MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
|
||||
// MenuItem::Separator,
|
||||
// MenuItem::Sub(SubMenuItem::new(
|
||||
// "Sub",
|
||||
// vec![
|
||||
// MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
|
||||
// MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
|
||||
// ],
|
||||
// )),
|
||||
// ])
|
||||
// .unwrap();
|
||||
// remote.show_context_menu(&menu);
|
||||
// }))
|
||||
eventloop.run();
|
||||
}
|
Loading…
Reference in New Issue
Block a user