First draft of espanso inject x11 implementation

This commit is contained in:
Federico Terzi 2021-02-13 15:55:28 +01:00
parent e0bf94013d
commit ff6bfa20cb
9 changed files with 837 additions and 58 deletions

View File

@ -35,23 +35,23 @@ fn cc_config() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn cc_config() { fn cc_config() {
println!("cargo:rerun-if-changed=src/x11/native.cpp"); // println!("cargo:rerun-if-changed=src/x11/native.cpp");
println!("cargo:rerun-if-changed=src/x11/native.h"); // println!("cargo:rerun-if-changed=src/x11/native.h");
println!("cargo:rerun-if-changed=src/evdev/native.cpp"); // println!("cargo:rerun-if-changed=src/evdev/native.cpp");
println!("cargo:rerun-if-changed=src/evdev/native.h"); // println!("cargo:rerun-if-changed=src/evdev/native.h");
cc::Build::new() // cc::Build::new()
.cpp(true) // .cpp(true)
.include("src/x11/native.h") // .include("src/x11/native.h")
.file("src/x11/native.cpp") // .file("src/x11/native.cpp")
.compile("espansoinject"); // .compile("espansoinject");
cc::Build::new() // cc::Build::new()
.cpp(true) // .cpp(true)
.include("src/evdev/native.h") // .include("src/evdev/native.h")
.file("src/evdev/native.cpp") // .file("src/evdev/native.cpp")
.compile("espansoinjectevdev"); // .compile("espansoinjectevdev");
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
println!("cargo:rustc-link-lib=static=espansoinject"); // println!("cargo:rustc-link-lib=static=espansoinject");
println!("cargo:rustc-link-lib=static=espansoinjectevdev"); // println!("cargo:rustc-link-lib=static=espansoinjectevdev");
println!("cargo:rustc-link-lib=dylib=X11"); println!("cargo:rustc-link-lib=dylib=X11");
println!("cargo:rustc-link-lib=dylib=Xtst"); println!("cargo:rustc-link-lib=dylib=Xtst");
println!("cargo:rustc-link-lib=dylib=xkbcommon"); println!("cargo:rustc-link-lib=dylib=xkbcommon");

View 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

View File

@ -27,8 +27,8 @@ mod win32;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod x11; mod x11;
#[cfg(target_os = "linux")] //#[cfg(target_os = "linux")]
mod evdev; //mod evdev;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod mac; mod mac;
@ -37,19 +37,49 @@ mod mac;
extern crate lazy_static; extern crate lazy_static;
pub trait Injector { pub trait Injector {
fn send_string(&self, string: &str) -> Result<()>; fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()>;
fn send_keys(&self, keys: &[keys::Key], delay: i32) -> Result<()>; fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()>;
fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> 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)] #[allow(dead_code)]
pub struct InjectorOptions { pub struct InjectorCreationOptions {
// Only relevant in Linux systems // Only relevant in Linux systems
use_evdev: bool, use_evdev: bool,
} }
impl Default for InjectorOptions { impl Default for InjectorCreationOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
use_evdev: false, use_evdev: false,
@ -68,7 +98,8 @@ pub fn get_injector(_options: InjectorOptions) -> impl Injector {
} }
#[cfg(target_os = "linux")] #[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 // TODO: differenciate based on the options
x11::X11Injector::new()
} }

View 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

View 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);
}

View 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),
}

View 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),
}
}

View File

@ -52,6 +52,10 @@ impl LinuxEventLoop {
Self { rx } Self { rx }
} }
pub fn initialize(&self) {
// NOOP on linux
}
pub fn run(&self) { pub fn run(&self) {
// We don't run an event loop on Linux as there is no tray icon or application window needed. // 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 // Thad said, we still need a way to block this method, and thus we use a channel

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use espanso_detect::event::{InputEvent, Status}; use espanso_detect::event::{InputEvent, Status};
use espanso_inject::{get_injector, Injector, keys}; use espanso_inject::{get_injector, Injector, keys};
use espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*}; 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" // notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
// .to_string(), // .to_string(),
// }); // });
let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions { // let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions {
show_icon: true, // show_icon: true,
icon_paths: &icon_paths, // 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(); eventloop.initialize();
let handle = std::thread::spawn(move || { 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::win32::Win32Source::new();
//let mut source = espanso_detect::x11::X11Source::new(); let mut source = espanso_detect::x11::X11Source::new();
let mut source = espanso_detect::mac::CocoaSource::new(); //let mut source = espanso_detect::mac::CocoaSource::new();
source.initialize(); source.initialize();
source.eventloop(Box::new(move |event: InputEvent| { source.eventloop(Box::new(move |event: InputEvent| {
let injector = get_injector(Default::default());
println!("ev {:?}", event); println!("ev {:?}", event);
match event { match event {
InputEvent::Mouse(_) => {} InputEvent::Mouse(_) => {}
InputEvent::Keyboard(evt) => { 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.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
//remote.show_notification("Espanso is running!"); //remote.show_notification("Espanso is running!");
injector.send_string("hey guys"); injector.send_string("Hey guys! @", Default::default()).expect("error");
//injector.send_key_combination(&[keys::Key::Meta, keys::Key::V], 2); //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| { eventloop.run();
println!("ui {:?}", event); // eventloop.run(Box::new(move |event| {
let menu = Menu::from(vec![ // println!("ui {:?}", event);
MenuItem::Simple(SimpleMenuItem::new("open", "Open")), // let menu = Menu::from(vec![
MenuItem::Separator, // MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
MenuItem::Sub(SubMenuItem::new( // MenuItem::Separator,
"Sub", // MenuItem::Sub(SubMenuItem::new(
vec![ // "Sub",
MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")), // vec![
MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")), // MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
], // MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
)), // ],
]) // )),
.unwrap(); // ])
match event { // .unwrap();
TrayIconClick => { // match event {
remote.show_context_menu(&menu); // TrayIconClick => {
} // remote.show_context_menu(&menu);
ContextMenuClick(raw_id) => { // }
//remote.update_tray_icon(TrayIcon::Disabled); // ContextMenuClick(raw_id) => {
remote.show_notification("Hello there!"); // //remote.update_tray_icon(TrayIcon::Disabled);
println!("item {:?}", menu.get_item_id(raw_id)); // remote.show_notification("Hello there!");
} // println!("item {:?}", menu.get_item_id(raw_id));
} // }
})); // }
// }));
} }