diff --git a/Cargo.lock b/Cargo.lock index eda81e9..0280887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index 3a1e5d0..c314944 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -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" diff --git a/espanso-detect/build.rs b/espanso-detect/build.rs index 291ab70..9347e70 100644 --- a/espanso-detect/build.rs +++ b/espanso-detect/build.rs @@ -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")] diff --git a/espanso-detect/src/evdev/context.rs b/espanso-detect/src/evdev/context.rs new file mode 100644 index 0000000..2fa57c0 --- /dev/null +++ b/espanso-detect/src/evdev/context.rs @@ -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 { + 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(), +} \ No newline at end of file diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs new file mode 100644 index 0000000..51da12e --- /dev/null +++ b/espanso-detect/src/evdev/device.rs @@ -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 { + 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> { + 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::(); + + #[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 { + 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> { + 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(), +} diff --git a/espanso-detect/src/evdev/ffi.rs b/espanso-detect/src/evdev/ffi.rs new file mode 100644 index 0000000..837b8af --- /dev/null +++ b/espanso-detect/src/evdev/ffi.rs @@ -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; +} diff --git a/espanso-detect/src/evdev/keymap.rs b/espanso-detect/src/evdev/keymap.rs new file mode 100644 index 0000000..d751e3f --- /dev/null +++ b/espanso-detect/src/evdev/keymap.rs @@ -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 { + 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(), +} \ No newline at end of file diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs new file mode 100644 index 0000000..1be8517 --- /dev/null +++ b/espanso-detect/src/evdev/mod.rs @@ -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 . + */ + +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; +pub struct EVDEVSource { + devices: Vec, +} + +#[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::() { + 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 = 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 for Option { + fn from(raw: RawInputEvent) -> Option { + 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) { + 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 { + 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 = 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 = 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 = 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 = raw.into(); + assert!(result.is_none()); + } +} + +*/ diff --git a/espanso-detect/src/evdev/native.cpp b/espanso-detect/src/evdev/native.cpp new file mode 100644 index 0000000..dc34fa0 --- /dev/null +++ b/espanso-detect/src/evdev/native.cpp @@ -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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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; +} \ No newline at end of file diff --git a/espanso-detect/src/evdev/native.h b/espanso-detect/src/evdev/native.h new file mode 100644 index 0000000..3a0d8bc --- /dev/null +++ b/espanso-detect/src/evdev/native.h @@ -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 . + */ + +#ifndef ESPANSO_DETECT_EVDEV_H +#define ESPANSO_DETECT_EVDEV_H + +#include + +extern "C" int32_t is_keyboard(int fd); + +#endif //ESPANSO_DETECT_EVDEV_H \ No newline at end of file diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index 7f6fa97..384a43b 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -23,4 +23,7 @@ pub mod event; pub mod win32; #[cfg(target_os = "linux")] -pub mod x11; \ No newline at end of file +pub mod x11; + +#[cfg(target_os = "linux")] +pub mod evdev; \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 8b2025f..e51724e 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -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); diff --git a/espanso/src/main_old.rs b/espanso/src/main_old.rs new file mode 100644 index 0000000..8b2025f --- /dev/null +++ b/espanso/src/main_old.rs @@ -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(); +}