First draft of espanso inject x11 implementation
This commit is contained in:
parent
e0bf94013d
commit
ff6bfa20cb
|
@ -35,23 +35,23 @@ fn cc_config() {
|
|||
|
||||
#[cfg(target_os = "linux")]
|
||||
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("espansoinject");
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("src/evdev/native.h")
|
||||
.file("src/evdev/native.cpp")
|
||||
.compile("espansoinjectevdev");
|
||||
// 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("espansoinject");
|
||||
// cc::Build::new()
|
||||
// .cpp(true)
|
||||
// .include("src/evdev/native.h")
|
||||
// .file("src/evdev/native.cpp")
|
||||
// .compile("espansoinjectevdev");
|
||||
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
||||
println!("cargo:rustc-link-lib=static=espansoinject");
|
||||
println!("cargo:rustc-link-lib=static=espansoinjectevdev");
|
||||
// println!("cargo:rustc-link-lib=static=espansoinject");
|
||||
// println!("cargo:rustc-link-lib=static=espansoinjectevdev");
|
||||
println!("cargo:rustc-link-lib=dylib=X11");
|
||||
println!("cargo:rustc-link-lib=dylib=Xtst");
|
||||
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
||||
|
|
46
espanso-inject/src/evdev/README.md
Normal file
46
espanso-inject/src/evdev/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
To support EVDEV injection
|
||||
|
||||
At startup, the EVDEVInjector has to populate a lookup table to find the string <-> keycode + modifiers pairs.
|
||||
|
||||
* We know that the keycode goes from 1 to 256
|
||||
* We can then rely on the `xkb_state_key_get_utf8` to find the string correspondent to each keycode
|
||||
* Then we cycle between every modifier combination, updating the `xkb_state` with `xkb_state_update_key`
|
||||
|
||||
Ref: https://xkbcommon.org/doc/current/structxkb__keymap.html
|
||||
|
||||
```
|
||||
1 #include <xkbcommon/xkbcommon.h>
|
||||
2 #include <stdio.h>
|
||||
3
|
||||
4 int main() {
|
||||
5 struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
6 struct xkb_keymap *keymap =xkb_keymap_new_from_names(ctx, NULL, 0);
|
||||
7 struct xkb_state *state = xkb_state_new(keymap);
|
||||
8 // a = 38
|
||||
9
|
||||
10 xkb_state_update_key(state, 42 + 8, XKB_KEY_DOWN);
|
||||
11
|
||||
12 xkb_layout_index_t num = xkb_keymap_num_layouts_for_key(keymap, 42 + 8);
|
||||
13 char buff[10];
|
||||
14 xkb_state_key_get_utf8(state, 38, buff, 9);
|
||||
15
|
||||
16 printf("hey %s %d\n", buff, num);
|
||||
17 }
|
||||
```
|
||||
|
||||
The available modifiers can be found in the https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||
|
||||
#define KEY_LEFTCTRL 29
|
||||
#define KEY_LEFTSHIFT 42
|
||||
#define KEY_RIGHTSHIFT 54
|
||||
#define KEY_LEFTALT 56
|
||||
#define KEY_LEFTMETA 125
|
||||
#define KEY_RIGHTMETA 126
|
||||
#define KEY_RIGHTCTRL 97
|
||||
#define KEY_RIGHTALT 100
|
||||
|
||||
All these codes have to be added the EVDEV_OFFSET = 8
|
||||
|
||||
From the lookup table, we can generate the input event as shown here: https://github.com/ReimuNotMoe/ydotool/blob/7972e5e3390489c1395b06ca9dc7639763c7cc98/Tools/Type/Type.cpp
|
||||
|
||||
Note that we also need to inject the correct modifiers to obtain the text
|
|
@ -27,8 +27,8 @@ mod win32;
|
|||
#[cfg(target_os = "linux")]
|
||||
mod x11;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod evdev;
|
||||
//#[cfg(target_os = "linux")]
|
||||
//mod evdev;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
@ -37,19 +37,49 @@ mod mac;
|
|||
extern crate lazy_static;
|
||||
|
||||
pub trait Injector {
|
||||
fn send_string(&self, string: &str) -> Result<()>;
|
||||
fn send_keys(&self, keys: &[keys::Key], delay: i32) -> Result<()>;
|
||||
fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()>;
|
||||
fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()>;
|
||||
fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()>;
|
||||
fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()>;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct InjectionOptions {
|
||||
// Delay between injected events
|
||||
delay: i32,
|
||||
|
||||
// Use original libxdo methods instead of patched version
|
||||
// using XSendEvent rather than XTestFakeKeyEvent
|
||||
// NOTE: Only relevant on X11 linux systems.
|
||||
disable_fast_inject: bool,
|
||||
}
|
||||
|
||||
impl Default for InjectionOptions {
|
||||
fn default() -> Self {
|
||||
let default_delay = if cfg!(target_os = "windows") {
|
||||
0
|
||||
} else if cfg!(target_os = "macos") {
|
||||
2
|
||||
} else if cfg!(target_os = "linux") {
|
||||
0
|
||||
} else {
|
||||
panic!("unsupported OS");
|
||||
};
|
||||
|
||||
Self {
|
||||
delay: default_delay,
|
||||
disable_fast_inject: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct InjectorOptions {
|
||||
pub struct InjectorCreationOptions {
|
||||
// Only relevant in Linux systems
|
||||
use_evdev: bool,
|
||||
}
|
||||
|
||||
impl Default for InjectorOptions {
|
||||
impl Default for InjectorCreationOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
use_evdev: false,
|
||||
|
@ -68,7 +98,8 @@ pub fn get_injector(_options: InjectorOptions) -> impl Injector {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn get_injector(options: InjectorOptions) -> impl Injector {
|
||||
pub fn get_injector(options: InjectorCreationOptions) -> Result<impl Injector> {
|
||||
// TODO: differenciate based on the options
|
||||
x11::X11Injector::new()
|
||||
}
|
||||
|
||||
|
|
53
espanso-inject/src/x11/README.md
Normal file
53
espanso-inject/src/x11/README.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
Same approach as evdev, but the lookup logic is:
|
||||
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/Xlibint.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysymdef.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/extensions/record.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
Display *data_disp = NULL;
|
||||
|
||||
int main() {
|
||||
data_disp = XOpenDisplay(NULL);
|
||||
|
||||
for (int code = 0; code<256; code++) {
|
||||
for (int state = 0; state < 256; state++) {
|
||||
XKeyEvent event;
|
||||
event.display = data_disp;
|
||||
event.window = XDefaultRootWindow(data_disp);
|
||||
event.root = XDefaultRootWindow(data_disp);
|
||||
event.subwindow = None;
|
||||
event.time = 0;
|
||||
event.x = 1;
|
||||
event.y = 1;
|
||||
event.x_root = 1;
|
||||
event.y_root = 1;
|
||||
event.same_screen = True;
|
||||
event.keycode = code + 8;
|
||||
event.state = state;
|
||||
event.type = KeyPress;
|
||||
|
||||
char buffer[10];
|
||||
int res = XLookupString(&event, buffer, 9, NULL, NULL);
|
||||
|
||||
printf("hey %d %d %s\n", code, state, buffer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
This way, we get the state mask associated with a character, and we can pass it directly when injecting a character:
|
||||
https://github.com/federico-terzi/espanso/blob/master/native/liblinuxbridge/fast_xdo.cpp#L37
|
85
espanso-inject/src/x11/ffi.rs
Normal file
85
espanso-inject/src/x11/ffi.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Some of these structures/methods are taken from the X11-rs project
|
||||
// https://github.com/erlepereira/x11-rs
|
||||
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
os::raw::{c_char, c_long, c_uint, c_ulong},
|
||||
};
|
||||
|
||||
use libc::c_int;
|
||||
|
||||
pub enum Display {}
|
||||
pub type Window = u64;
|
||||
pub type Bool = i32;
|
||||
pub type Time = u64;
|
||||
pub type KeySym = u64;
|
||||
pub type KeyCode = u8;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const KeyPress: c_int = 2;
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const KeyRelease: c_int = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct XKeyEvent {
|
||||
pub type_: c_int,
|
||||
pub serial: c_ulong,
|
||||
pub send_event: Bool,
|
||||
pub display: *mut Display,
|
||||
pub window: Window,
|
||||
pub root: Window,
|
||||
pub subwindow: Window,
|
||||
pub time: Time,
|
||||
pub x: c_int,
|
||||
pub y: c_int,
|
||||
pub x_root: c_int,
|
||||
pub y_root: c_int,
|
||||
pub state: c_uint,
|
||||
pub keycode: c_uint,
|
||||
pub same_screen: Bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct XModifierKeymap {
|
||||
pub max_keypermod: c_int,
|
||||
pub modifiermap: *mut KeyCode,
|
||||
}
|
||||
|
||||
#[link(name = "X11")]
|
||||
extern "C" {
|
||||
pub fn XOpenDisplay(name: *const c_char) -> *mut Display;
|
||||
pub fn XCloseDisplay(display: *mut Display);
|
||||
pub fn XLookupString(
|
||||
event: *const XKeyEvent,
|
||||
buffer_return: *mut c_char,
|
||||
bytes_buffer: c_int,
|
||||
keysym_return: *mut KeySym,
|
||||
status_in_out: *const c_void,
|
||||
) -> c_int;
|
||||
pub fn XDefaultRootWindow(display: *mut Display) -> Window;
|
||||
pub fn XGetInputFocus(
|
||||
display: *mut Display,
|
||||
window_out: *mut Window,
|
||||
revert_to: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn XFlush(display: *mut Display) -> c_int;
|
||||
pub fn XSendEvent(
|
||||
display: *mut Display,
|
||||
window: Window,
|
||||
propagate: c_int,
|
||||
event_mask: c_long,
|
||||
event_send: *mut XKeyEvent,
|
||||
) -> c_int;
|
||||
pub fn XGetModifierMapping(display: *mut Display) -> *mut XModifierKeymap;
|
||||
pub fn XFreeModifiermap(map: *mut XModifierKeymap) -> c_int;
|
||||
pub fn XTestFakeKeyEvent(
|
||||
display: *mut Display,
|
||||
key_code: c_uint,
|
||||
is_press: c_int,
|
||||
time: c_ulong,
|
||||
) -> c_int;
|
||||
pub fn XSync(display: *mut Display, discard: c_int) -> c_int;
|
||||
pub fn XQueryKeymap(display: *mut Display, keys_return: *mut u8);
|
||||
}
|
447
espanso-inject/src/x11/mod.rs
Normal file
447
espanso-inject/src/x11/mod.rs
Normal file
|
@ -0,0 +1,447 @@
|
|||
/*
|
||||
* 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 ffi;
|
||||
mod raw_keys;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString},
|
||||
os::raw::c_char,
|
||||
slice,
|
||||
};
|
||||
|
||||
use ffi::{Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow, XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString, XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent};
|
||||
use log::error;
|
||||
|
||||
use anyhow::Result;
|
||||
use raw_keys::convert_key_to_sym;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{keys, InjectionOptions, Injector};
|
||||
|
||||
// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
|
||||
// keycode set (where ESC is 9).
|
||||
const EVDEV_OFFSET: u32 = 8;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct KeyRecord {
|
||||
// Keycode
|
||||
code: u32,
|
||||
// Modifier state which combined with the code produces the char
|
||||
// This is a bit mask:
|
||||
state: u32,
|
||||
}
|
||||
|
||||
type CharMap = HashMap<String, KeyRecord>;
|
||||
type SymMap = HashMap<KeySym, KeyRecord>;
|
||||
|
||||
pub struct X11Injector {
|
||||
display: *mut Display,
|
||||
|
||||
char_map: CharMap,
|
||||
sym_map: SymMap,
|
||||
}
|
||||
|
||||
#[allow(clippy::new_without_default)]
|
||||
impl X11Injector {
|
||||
pub fn new() -> Result<Self> {
|
||||
// Necessary to properly handle non-ascii chars
|
||||
let empty_string = CString::new("")?;
|
||||
unsafe {
|
||||
libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
|
||||
}
|
||||
|
||||
let display = unsafe { ffi::XOpenDisplay(std::ptr::null()) };
|
||||
if display.is_null() {
|
||||
return Err(X11InjectorError::InitFailure().into());
|
||||
}
|
||||
|
||||
let (char_map, sym_map) = Self::generate_maps(display);
|
||||
|
||||
Ok(Self {
|
||||
display,
|
||||
char_map,
|
||||
sym_map,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_maps(display: *mut Display) -> (CharMap, SymMap) {
|
||||
let mut char_map = HashMap::new();
|
||||
let mut sym_map = HashMap::new();
|
||||
|
||||
let root_window = unsafe { XDefaultRootWindow(display) };
|
||||
|
||||
// Cycle through all state/code combinations to populate the reverse lookup tables
|
||||
for key_code in 0..256u32 {
|
||||
for modifier_state in 0..256u32 {
|
||||
let code_with_offset = key_code + EVDEV_OFFSET;
|
||||
let event = XKeyEvent {
|
||||
display,
|
||||
keycode: code_with_offset,
|
||||
state: modifier_state,
|
||||
|
||||
// These might not even need to be filled
|
||||
window: root_window,
|
||||
root: root_window,
|
||||
same_screen: 1,
|
||||
time: 0,
|
||||
type_: KeyRelease,
|
||||
x_root: 1,
|
||||
y_root: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
subwindow: 0,
|
||||
serial: 0,
|
||||
send_event: 0,
|
||||
};
|
||||
|
||||
let mut sym: KeySym = 0;
|
||||
let mut buffer: [c_char; 10] = [0; 10];
|
||||
let result = unsafe {
|
||||
XLookupString(
|
||||
&event,
|
||||
buffer.as_mut_ptr(),
|
||||
(buffer.len() - 1) as i32,
|
||||
&mut sym,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
|
||||
let key_record = KeyRecord {
|
||||
code: code_with_offset,
|
||||
state: modifier_state,
|
||||
};
|
||||
|
||||
// Keysym was found
|
||||
if sym != 0 {
|
||||
sym_map.entry(sym).or_insert(key_record);
|
||||
}
|
||||
|
||||
// Char was found
|
||||
if result > 0 {
|
||||
let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
|
||||
let string = raw_string.to_string_lossy().to_string();
|
||||
char_map.entry(string).or_insert(key_record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(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>> {
|
||||
syms
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
self
|
||||
.sym_map
|
||||
.get(&sym)
|
||||
.cloned()
|
||||
.ok_or_else(|| X11InjectorError::SymMappingFailure(*sym).into())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// This method was inspired by the wonderful xdotool by Jordan Sissel
|
||||
// https://github.com/jordansissel/xdotool
|
||||
fn get_modifier_codes(&self) -> Vec<Vec<KeyCode>> {
|
||||
let modifiers_ptr = unsafe { XGetModifierMapping(self.display) };
|
||||
let modifiers = unsafe { *modifiers_ptr };
|
||||
|
||||
let mut modifiers_codes = Vec::new();
|
||||
|
||||
for mod_index in 0..=7 {
|
||||
let mut modifier_codes = Vec::new();
|
||||
for mod_key in 0..modifiers.max_keypermod {
|
||||
let modifier_map = unsafe {
|
||||
slice::from_raw_parts(
|
||||
modifiers.modifiermap,
|
||||
(8 * modifiers.max_keypermod) as usize,
|
||||
)
|
||||
|
||||
};
|
||||
let keycode = modifier_map[(mod_index * modifiers.max_keypermod + mod_key) as usize];
|
||||
if keycode != 0 {
|
||||
modifier_codes.push(keycode);
|
||||
}
|
||||
}
|
||||
modifiers_codes.push(modifier_codes);
|
||||
}
|
||||
|
||||
unsafe { XFreeModifiermap(modifiers_ptr) };
|
||||
|
||||
modifiers_codes
|
||||
}
|
||||
|
||||
fn render_key_combination(&self, original_records: &[KeyRecord]) -> Vec<KeyRecord> {
|
||||
let modifiers_codes = self.get_modifier_codes();
|
||||
let mut records = Vec::new();
|
||||
|
||||
let mut current_state = 0u32;
|
||||
for record in original_records {
|
||||
let mut current_record = *record;
|
||||
|
||||
// Render the state by applying the modifiers
|
||||
for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
|
||||
if modifier.contains(&(record.code as u8)) {
|
||||
current_state |= 1 << mod_index;
|
||||
}
|
||||
}
|
||||
|
||||
current_record.state = current_state;
|
||||
records.push(current_record);
|
||||
}
|
||||
|
||||
records
|
||||
}
|
||||
|
||||
fn get_focused_window(&self) -> Window {
|
||||
let mut focused_window: Window = 0;
|
||||
let mut revert_to = 0;
|
||||
unsafe {
|
||||
XGetInputFocus(self.display, &mut focused_window, &mut revert_to);
|
||||
}
|
||||
focused_window
|
||||
}
|
||||
|
||||
fn send_key(&self, window: Window, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
||||
let root_window = unsafe { XDefaultRootWindow(self.display) };
|
||||
let mut event = XKeyEvent {
|
||||
display: self.display,
|
||||
keycode: record.code,
|
||||
state: record.state,
|
||||
window,
|
||||
root: root_window,
|
||||
same_screen: 1,
|
||||
time: 0,
|
||||
type_: if pressed { KeyPress } else { KeyRelease },
|
||||
x_root: 1,
|
||||
y_root: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
subwindow: 0,
|
||||
serial: 0,
|
||||
send_event: 0,
|
||||
};
|
||||
unsafe {
|
||||
XSendEvent(self.display, window, 1, 0, &mut event);
|
||||
XFlush(self.display);
|
||||
}
|
||||
|
||||
if delay_us != 0 {
|
||||
unsafe {
|
||||
libc::usleep(delay_us);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn xtest_send_modifiers(&self, modmask: u32, pressed: bool) {
|
||||
let modifiers_codes = self.get_modifier_codes();
|
||||
for (mod_index, modifier_codes) in modifiers_codes.into_iter().enumerate() {
|
||||
if (modmask & (1 << mod_index)) != 0 {
|
||||
for keycode in modifier_codes {
|
||||
let is_press = if pressed { 1 } else { 0 };
|
||||
unsafe {
|
||||
XTestFakeKeyEvent(self.display, keycode as u32, is_press, 0);
|
||||
XSync(self.display, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn xtest_send_key(&self, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
||||
// If the key requires any modifier, we need to send those events
|
||||
if record.state != 0 {
|
||||
self.xtest_send_modifiers(record.state, pressed);
|
||||
}
|
||||
|
||||
let is_press = if pressed { 1 } else { 0 };
|
||||
unsafe {
|
||||
XTestFakeKeyEvent(self.display, record.code, is_press, 0);
|
||||
XSync(self.display, 0);
|
||||
XFlush(self.display);
|
||||
}
|
||||
|
||||
if delay_us != 0 {
|
||||
unsafe {
|
||||
libc::usleep(delay_us);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn xtest_release_all_keys(&self) {
|
||||
let mut keys: [u8; 32] = [0; 32];
|
||||
unsafe {
|
||||
XQueryKeymap(self.display, keys.as_mut_ptr());
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..32 {
|
||||
// Only those that are pressed should be changed
|
||||
if keys[i] != 0 {
|
||||
for k in 0..8 {
|
||||
if (keys[i] & (1 << k)) != 0 {
|
||||
let key_code = i * 8 + k;
|
||||
unsafe {
|
||||
XTestFakeKeyEvent(self.display, key_code as u32, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Injector {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
XCloseDisplay(self.display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Injector for X11Injector {
|
||||
fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> {
|
||||
let focused_window = self.get_focused_window();
|
||||
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_release_all_keys();
|
||||
}
|
||||
|
||||
// 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(|| X11InjectorError::CharMappingFailure(char).into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
||||
|
||||
for record in records? {
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_send_key(&record, true, delay_us);
|
||||
self.xtest_send_key(&record, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, true, delay_us);
|
||||
self.send_key(focused_window, &record, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
|
||||
let focused_window = self.get_focused_window();
|
||||
|
||||
// Compute all the key record sequence first to make sure a mapping is available
|
||||
let syms = Self::convert_to_sym_array(keys)?;
|
||||
let records = self.convert_to_record_array(&syms)?;
|
||||
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_release_all_keys();
|
||||
}
|
||||
|
||||
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
||||
|
||||
for record in records {
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_send_key(&record, true, delay_us);
|
||||
self.xtest_send_key(&record, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, true, delay_us);
|
||||
self.send_key(focused_window, &record, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
|
||||
let focused_window = self.get_focused_window();
|
||||
|
||||
// Compute all the key record sequence first to make sure a mapping is available
|
||||
let syms = Self::convert_to_sym_array(keys)?;
|
||||
let records = self.convert_to_record_array(&syms)?;
|
||||
|
||||
// Render the correct modifier mask for the given sequence
|
||||
let records = self.render_key_combination(&records);
|
||||
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_release_all_keys();
|
||||
}
|
||||
|
||||
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
||||
|
||||
// First press the keys
|
||||
for record in records.iter() {
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_send_key(&record, true, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, true, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
// Then release them
|
||||
for record in records.iter().rev() {
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_send_key(&record, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum X11InjectorError {
|
||||
#[error("failed to initialize x11 display")]
|
||||
InitFailure(),
|
||||
|
||||
#[error("missing vkey mapping for char `{0}`")]
|
||||
CharMappingFailure(String),
|
||||
|
||||
#[error("missing sym mapping for key `{0}`")]
|
||||
MappingFailure(keys::Key),
|
||||
|
||||
#[error("missing record mapping for sym `{0}`")]
|
||||
SymMappingFailure(u64),
|
||||
}
|
106
espanso-inject/src/x11/raw_keys.rs
Normal file
106
espanso-inject/src/x11/raw_keys.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use crate::keys::Key;
|
||||
|
||||
pub fn convert_key_to_sym(key: &Key) -> Option<u32> {
|
||||
match key {
|
||||
Key::Alt => Some(0xFFE9),
|
||||
Key::CapsLock => Some(0xFFE5),
|
||||
Key::Control => Some(0xFFE3),
|
||||
Key::Meta => Some(0xFFEB),
|
||||
Key::NumLock => Some(0xFF7F),
|
||||
Key::Shift => Some(0xFFE1),
|
||||
|
||||
// Whitespace
|
||||
Key::Enter => Some(0xFF0D),
|
||||
Key::Tab => Some(0xFF09),
|
||||
Key::Space => Some(0x20),
|
||||
|
||||
// Navigation
|
||||
Key::ArrowDown => Some(0xFF54),
|
||||
Key::ArrowLeft => Some(0xFF51),
|
||||
Key::ArrowRight => Some(0xFF53),
|
||||
Key::ArrowUp => Some(0xFF52),
|
||||
Key::End => Some(0xFF57),
|
||||
Key::Home => Some(0xFF50),
|
||||
Key::PageDown => Some(0xFF56),
|
||||
Key::PageUp => Some(0xFF55),
|
||||
|
||||
// UI keys
|
||||
Key::Escape => Some(0xFF1B),
|
||||
|
||||
// Editing keys
|
||||
Key::Backspace => Some(0xFF08),
|
||||
Key::Insert => Some(0xff63),
|
||||
Key::Delete => Some(0xffff),
|
||||
|
||||
// Function keys
|
||||
Key::F1 => Some(0xFFBE),
|
||||
Key::F2 => Some(0xFFBF),
|
||||
Key::F3 => Some(0xFFC0),
|
||||
Key::F4 => Some(0xFFC1),
|
||||
Key::F5 => Some(0xFFC2),
|
||||
Key::F6 => Some(0xFFC3),
|
||||
Key::F7 => Some(0xFFC4),
|
||||
Key::F8 => Some(0xFFC5),
|
||||
Key::F9 => Some(0xFFC6),
|
||||
Key::F10 => Some(0xFFC7),
|
||||
Key::F11 => Some(0xFFC8),
|
||||
Key::F12 => Some(0xFFC9),
|
||||
Key::F13 => Some(0xFFCA),
|
||||
Key::F14 => Some(0xFFCB),
|
||||
Key::F15 => Some(0xFFCC),
|
||||
Key::F16 => Some(0xFFCD),
|
||||
Key::F17 => Some(0xFFCE),
|
||||
Key::F18 => Some(0xFFCF),
|
||||
Key::F19 => Some(0xFFD0),
|
||||
Key::F20 => Some(0xFFD1),
|
||||
|
||||
Key::A => Some(0x0061),
|
||||
Key::B => Some(0x0062),
|
||||
Key::C => Some(0x0063),
|
||||
Key::D => Some(0x0064),
|
||||
Key::E => Some(0x0065),
|
||||
Key::F => Some(0x0066),
|
||||
Key::G => Some(0x0067),
|
||||
Key::H => Some(0x0068),
|
||||
Key::I => Some(0x0069),
|
||||
Key::J => Some(0x006a),
|
||||
Key::K => Some(0x006b),
|
||||
Key::L => Some(0x006c),
|
||||
Key::M => Some(0x006d),
|
||||
Key::N => Some(0x006e),
|
||||
Key::O => Some(0x006f),
|
||||
Key::P => Some(0x0070),
|
||||
Key::Q => Some(0x0071),
|
||||
Key::R => Some(0x0072),
|
||||
Key::S => Some(0x0073),
|
||||
Key::T => Some(0x0074),
|
||||
Key::U => Some(0x0075),
|
||||
Key::V => Some(0x0076),
|
||||
Key::W => Some(0x0077),
|
||||
Key::X => Some(0x0078),
|
||||
Key::Y => Some(0x0079),
|
||||
Key::Z => Some(0x007a),
|
||||
|
||||
Key::N0 => Some(0x0030),
|
||||
Key::N1 => Some(0x0031),
|
||||
Key::N2 => Some(0x0032),
|
||||
Key::N3 => Some(0x0033),
|
||||
Key::N4 => Some(0x0034),
|
||||
Key::N5 => Some(0x0035),
|
||||
Key::N6 => Some(0x0036),
|
||||
Key::N7 => Some(0x0037),
|
||||
Key::N8 => Some(0x0038),
|
||||
Key::N9 => Some(0x0039),
|
||||
Key::Numpad0 => Some(0xffb0),
|
||||
Key::Numpad1 => Some(0xffb1),
|
||||
Key::Numpad2 => Some(0xffb2),
|
||||
Key::Numpad3 => Some(0xffb3),
|
||||
Key::Numpad4 => Some(0xffb4),
|
||||
Key::Numpad5 => Some(0xffb5),
|
||||
Key::Numpad6 => Some(0xffb6),
|
||||
Key::Numpad7 => Some(0xffb7),
|
||||
Key::Numpad8 => Some(0xffb8),
|
||||
Key::Numpad9 => Some(0xffb9),
|
||||
Key::Raw(code) => Some(*code as u32),
|
||||
}
|
||||
}
|
|
@ -52,6 +52,10 @@ impl LinuxEventLoop {
|
|||
Self { rx }
|
||||
}
|
||||
|
||||
pub fn initialize(&self) {
|
||||
// NOOP on linux
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
// We don't run an event loop on Linux as there is no tray icon or application window needed.
|
||||
// Thad said, we still need a way to block this method, and thus we use a channel
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use espanso_detect::event::{InputEvent, Status};
|
||||
use espanso_inject::{get_injector, Injector, keys};
|
||||
use espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*};
|
||||
|
@ -42,58 +44,63 @@ fn main() {
|
|||
// notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
|
||||
// .to_string(),
|
||||
// });
|
||||
let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions {
|
||||
show_icon: true,
|
||||
icon_paths: &icon_paths,
|
||||
// let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions {
|
||||
// show_icon: true,
|
||||
// icon_paths: &icon_paths,
|
||||
// });
|
||||
let (remote, mut eventloop) = espanso_ui::linux::create(espanso_ui::linux::LinuxUIOptions {
|
||||
notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png".to_string(),
|
||||
});
|
||||
|
||||
eventloop.initialize();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let injector = get_injector(Default::default()).unwrap();
|
||||
//let mut source = espanso_detect::win32::Win32Source::new();
|
||||
//let mut source = espanso_detect::x11::X11Source::new();
|
||||
let mut source = espanso_detect::mac::CocoaSource::new();
|
||||
let mut source = espanso_detect::x11::X11Source::new();
|
||||
//let mut source = espanso_detect::mac::CocoaSource::new();
|
||||
source.initialize();
|
||||
source.eventloop(Box::new(move |event: InputEvent| {
|
||||
let injector = get_injector(Default::default());
|
||||
println!("ev {:?}", event);
|
||||
match event {
|
||||
InputEvent::Mouse(_) => {}
|
||||
InputEvent::Keyboard(evt) => {
|
||||
if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
|
||||
if evt.key == espanso_detect::event::Key::Escape && evt.status == Status::Released {
|
||||
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
|
||||
//remote.show_notification("Espanso is running!");
|
||||
injector.send_string("hey guys");
|
||||
//injector.send_key_combination(&[keys::Key::Meta, keys::Key::V], 2);
|
||||
injector.send_string("Hey guys! @", Default::default()).expect("error");
|
||||
//std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
//injector.send_key_combination(&[keys::Key::Control, keys::Key::V], Default::default()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
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();
|
||||
match event {
|
||||
TrayIconClick => {
|
||||
remote.show_context_menu(&menu);
|
||||
}
|
||||
ContextMenuClick(raw_id) => {
|
||||
//remote.update_tray_icon(TrayIcon::Disabled);
|
||||
remote.show_notification("Hello there!");
|
||||
println!("item {:?}", menu.get_item_id(raw_id));
|
||||
}
|
||||
}
|
||||
}));
|
||||
eventloop.run();
|
||||
// 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();
|
||||
// match event {
|
||||
// TrayIconClick => {
|
||||
// remote.show_context_menu(&menu);
|
||||
// }
|
||||
// ContextMenuClick(raw_id) => {
|
||||
// //remote.update_tray_icon(TrayIcon::Disabled);
|
||||
// remote.show_notification("Hello there!");
|
||||
// println!("item {:?}", menu.get_item_id(raw_id));
|
||||
// }
|
||||
// }
|
||||
// }));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user