From 573b8ddcfd9909370b444fb2bbf0c19062576d41 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 18 Apr 2020 19:31:24 +0200 Subject: [PATCH] Add fast injection mode on Linux --- native/liblinuxbridge/CMakeLists.txt | 2 +- native/liblinuxbridge/bridge.cpp | 39 +++++ native/liblinuxbridge/bridge.h | 20 +++ native/liblinuxbridge/fast_xdo.cpp | 247 +++++++++++++++++++++++++++ native/liblinuxbridge/fast_xdo.h | 21 +++ src/bridge/linux.rs | 5 + src/config/mod.rs | 4 + src/engine.rs | 16 +- src/keyboard/linux.rs | 47 +++-- src/keyboard/macos.rs | 15 +- src/keyboard/mod.rs | 13 +- src/keyboard/windows.rs | 14 +- 12 files changed, 401 insertions(+), 42 deletions(-) create mode 100644 native/liblinuxbridge/fast_xdo.cpp create mode 100644 native/liblinuxbridge/fast_xdo.h diff --git a/native/liblinuxbridge/CMakeLists.txt b/native/liblinuxbridge/CMakeLists.txt index 4f44763..243fda2 100644 --- a/native/liblinuxbridge/CMakeLists.txt +++ b/native/liblinuxbridge/CMakeLists.txt @@ -4,6 +4,6 @@ project(liblinuxbridge) set (CMAKE_CXX_STANDARD 14) set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include") -add_library(linuxbridge STATIC bridge.cpp bridge.h) +add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h) install(TARGETS linuxbridge DESTINATION .) \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 3e8fc39..2934ca9 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -18,6 +18,7 @@ */ #include "bridge.h" +#include "fast_xdo.h" #include #include @@ -302,18 +303,56 @@ void send_enter() { xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000); } +void fast_send_string(const char * string) { + // It may happen that when an expansion is triggered, some keys are still pressed. + // This causes a problem if the expanded match contains that character, as the injection + // will not be able to register that keypress (as it is already pressed). + // To solve the problem, before an expansion we get which keys are currently pressed + // and inject a key_release event so that they can be further registered. + release_all_keys(); + + xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1); +} + +void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count) { + int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode); + + Window focused; + int revert_to; + XGetInputFocus(xdo_context->xdpy, &focused, &revert_to); + + for (int i = 0; ixdpy); +} + +void fast_send_enter() { + _fast_send_keycode_to_focused_window(XK_Return, 1); +} + void delete_string(int32_t count) { for (int i = 0; i +#include +#include + +#include "fast_xdo.h" + +extern "C" { // Needed to avoid C++ compiler name mangling +#include +} + +void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) { + xk->display = xdo->xdpy; + xk->subwindow = None; + xk->time = CurrentTime; + xk->same_screen = True; + + /* Should we set these at all? */ + xk->x = xk->y = xk->x_root = xk->y_root = 1; +} + +void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay) { + /* Properly ensure the modstate is set by finding a key + * that activates each bit in the modifier state */ + int mask = modstate | key->modmask; + int use_xtest = 0; + + /* Since key events have 'state' (shift, etc) in the event, we don't + * need to worry about key press ordering. */ + XKeyEvent xk; + fast_init_xkeyevent(xdo, &xk); + xk.window = window; + xk.keycode = key->code; + xk.state = mask | (key->group << 13); + xk.type = (is_press ? KeyPress : KeyRelease); + XSendEvent(xdo->xdpy, xk.window, True, KeyPressMask, (XEvent *)&xk); + + /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */ + XFlush(xdo->xdpy); + if (delay > 0) { + usleep(delay); + } +} + +int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys, + int nkeys, int pressed, int *modifier, useconds_t delay) { + int i = 0; + int modstate = 0; + int keymapchanged = 0; + + /* Find an unused keycode in case we need to bind unmapped keysyms */ + KeySym *keysyms = NULL; + int keysyms_per_keycode = 0; + int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */ + keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low, + xdo->keycode_high - xdo->keycode_low, + &keysyms_per_keycode); + + /* Find a keycode that is unused for scratchspace */ + for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) { + int j = 0; + int key_is_empty = 1; + for (j = 0; j < keysyms_per_keycode; j++) { + /*char *symname;*/ + int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j; + /*symname = XKeysymToString(keysyms[symindex]);*/ + if (keysyms[symindex] != 0) { + key_is_empty = 0; + } else { + break; + } + } + if (key_is_empty) { + scratch_keycode = i; + break; + } + } + XFree(keysyms); + + /* Allow passing NULL for modifier in case we don't care about knowing + * the modifier map state after we finish */ + if (modifier == NULL) + modifier = &modstate; + + for (i = 0; i < nkeys; i++) { + if (keys[i].needs_binding == 1) { + KeySym keysym_list[] = { keys[i].symbol }; + //_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + XSync(xdo->xdpy, False); + /* override the code in our current key to use the scratch_keycode */ + keys[i].code = scratch_keycode; + keymapchanged = 1; + } + + //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n", + //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier); + fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay); + + if (keys[i].needs_binding == 1) { + /* If we needed to make a new keymapping for this keystroke, we + * should sync with the server now, after the keypress, so that + * the next mapping or removal doesn't conflict. */ + XSync(xdo->xdpy, False); + } + + if (pressed) { + *modifier |= keys[i].modmask; + } else { + *modifier &= ~(keys[i].modmask); + } + } + + + if (keymapchanged) { + KeySym keysym_list[] = { 0 }; + //printf(xdo, "Reverting scratch keycode (sym %lu to %d)", + // keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + } + + /* Necessary? */ + XFlush(xdo->xdpy); + return XDO_SUCCESS; +} + +KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) { + int i = 0; + int len = xdo->charcodes_len; + + //printf("Finding symbol for key '%c'\n", key); + for (i = 0; i < len; i++) { + //printf(" => %c vs %c (%d)\n", + //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key)); + if (xdo->charcodes[i].key == key) { + //printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol); + return xdo->charcodes[i].symbol; + } + } + + if (key >= 0x100) key += 0x01000000; + if (XKeysymToString(key)) return key; + return NoSymbol; +} + +void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) { + int i = 0; + int len = xdo->charcodes_len; + + key->code = 0; + key->symbol = keysym; + key->group = 0; + key->modmask = 0; + key->needs_binding = 1; + + for (i = 0; i < len; i++) { + if (xdo->charcodes[i].symbol == keysym) { + key->code = xdo->charcodes[i].code; + key->group = xdo->charcodes[i].group; + key->modmask = xdo->charcodes[i].modmask; + key->needs_binding = 0; + return; + } + } +} + +void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) { + KeySym keysym = fast_keysym_from_char(xdo, key->key); + fast_charcodemap_from_keysym(xdo, key, keysym); +} + +/* XXX: Return proper code if errors found */ +int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) { + + /* Since we're doing down/up, the delay should be based on the number + * of keys pressed (including shift). Since up/down is two calls, + * divide by two. */ + delay /= 2; + + /* XXX: Add error handling */ + //int nkeys = strlen(string); + //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t)); + charcodemap_t key; + //int modifier = 0; + setlocale(LC_CTYPE,""); + mbstate_t ps = { 0 }; + ssize_t len; + while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) { + if (len == -1) { + fprintf(stderr, "Invalid multi-byte sequence encountered\n"); + return XDO_ERROR; + } + fast_charcodemap_from_char(xdo, &key); + if (key.code == 0 && key.symbol == NoSymbol) { + fprintf(stderr, "I don't what key produces '%lc', skipping.\n", + key.key); + continue; + } else { + //printf("Found key for %c\n", key.key); + //printf("code: %d\n", key.code); + //printf("sym: %s\n", XKeysymToString(key.symbol)); + } + + //printf(stderr, + //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n", + //key.key, key.code, key.symbol, key.group, key.modmask, + //(key.needs_binding == 1) ? "needs binding" : "ok"); + + //_xdo_send_key(xdo, window, keycode, modstate, True, delay); + //_xdo_send_key(xdo, window, keycode, modstate, False, delay); + fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2); + key.needs_binding = 0; + fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2); + + /* XXX: Flush here or at the end? or never? */ + //XFlush(xdo->xdpy); + } /* walk string generating a keysequence */ + + //free(keys); + return XDO_SUCCESS; +} + +void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) { + XKeyEvent xk; + xk.display = xdo->xdpy; + xk.window = window; + xk.root = XDefaultRootWindow(xdo->xdpy); + xk.subwindow = None; + xk.time = CurrentTime; + xk.x = 1; + xk.y = 1; + xk.x_root = 1; + xk.y_root = 1; + xk.same_screen = True; + xk.keycode = keycode; + xk.state = 0; + xk.type = (pressed ? KeyPress : KeyRelease); + + XEvent event; + event.xkey =xk; + + XSendEvent(xdo->xdpy, window, True, 0, &event); +} diff --git a/native/liblinuxbridge/fast_xdo.h b/native/liblinuxbridge/fast_xdo.h new file mode 100644 index 0000000..a7de8a4 --- /dev/null +++ b/native/liblinuxbridge/fast_xdo.h @@ -0,0 +1,21 @@ +// +// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT +// and modified to use XSendEvent instead of XTestFakeKeyEvent. + +#ifndef LIBLINUXBRIDGE_FAST_XDO_H +#define LIBLINUXBRIDGE_FAST_XDO_H + +extern "C" { // Needed to avoid C++ compiler name mangling +#include +} + +KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key); +void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key); +void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym); +void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk); +void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay); +int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay); +void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed); + +#endif //LIBLINUXBRIDGE_FAST_XDO_H diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index 94bdfa2..63ee91c 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -47,4 +47,9 @@ extern { pub fn trigger_alt_shift_ins_paste(); pub fn trigger_ctrl_alt_paste(); pub fn trigger_copy(); + + pub fn fast_send_string(string: *const c_char); + pub fn fast_delete_string(count: i32); + pub fn fast_left_arrow(count: i32); + pub fn fast_send_enter(); } \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 7816539..8356b5a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -68,6 +68,7 @@ fn default_secure_input_watcher_enabled() -> bool {true} fn default_secure_input_notification() -> bool {true} fn default_show_notifications() -> bool {true} fn default_show_icon() -> bool {true} +fn default_fast_inject() -> bool {false} fn default_secure_input_watcher_interval() -> i32 {5000} fn default_matches() -> Vec { Vec::new() } fn default_global_vars() -> Vec { Vec::new() } @@ -164,6 +165,9 @@ pub struct Configs { #[serde(default = "default_show_icon")] pub show_icon: bool, + #[serde(default = "default_fast_inject")] + pub fast_inject: bool, + #[serde(default = "default_matches")] pub matches: Vec, diff --git a/src/engine.rs b/src/engine.rs index 79730c7..fe0c694 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -129,7 +129,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator }; - self.keyboard_manager.delete_string(char_count); + self.keyboard_manager.delete_string(&config, char_count); let mut previous_clipboard_content : Option = None; @@ -194,10 +194,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa for (i, split) in splits.enumerate() { if i > 0 { - self.keyboard_manager.send_enter(); + self.keyboard_manager.send_enter(&config); } - self.keyboard_manager.send_string(split); + self.keyboard_manager.send_string(&config, split); } }, BackendType::Clipboard => { @@ -206,7 +206,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); self.clipboard_manager.set_clipboard(&target_string); - self.keyboard_manager.trigger_paste(&config.paste_shortcut); + self.keyboard_manager.trigger_paste(&config); }, _ => { error!("Unsupported backend type evaluation."); @@ -216,7 +216,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa if let Some(moves) = cursor_rewind { // Simulate left arrow key presses to bring the cursor into the desired position - self.keyboard_manager.move_cursor_left(moves); + self.keyboard_manager.move_cursor_left(&config, moves); } }, RenderResult::Image(image_path) => { @@ -225,7 +225,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); self.clipboard_manager.set_clipboard_image(&image_path); - self.keyboard_manager.trigger_paste(&config.paste_shortcut); + self.keyboard_manager.trigger_paste(&config); }, RenderResult::Error => { error!("Could not render match: {}", m.triggers[trigger_offset]); @@ -283,7 +283,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding // Trigger a copy shortcut to transfer the content of the selection to the clipboard - self.keyboard_manager.trigger_copy(); + self.keyboard_manager.trigger_copy(&config); // Sleep for a while, giving time to effectively copy the text std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding @@ -312,7 +312,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.clipboard_manager.set_clipboard(&payload); std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding - self.keyboard_manager.trigger_paste(&config.paste_shortcut); + self.keyboard_manager.trigger_paste(&config); }, _ => { warn!("Cannot expand passive match") diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 03d55ba..4a9fd2a 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -21,28 +21,39 @@ use std::ffi::CString; use crate::bridge::linux::*; use super::PasteShortcut; use log::error; +use crate::config::Configs; pub struct LinuxKeyboardManager { } impl super::KeyboardManager for LinuxKeyboardManager { - fn send_string(&self, s: &str) { + fn send_string(&self, active_config: &Configs, s: &str) { let res = CString::new(s); match res { - Ok(cstr) => unsafe { send_string(cstr.as_ptr()); } + Ok(cstr) => unsafe { + if active_config.fast_inject { + fast_send_string(cstr.as_ptr()); + }else{ + send_string(cstr.as_ptr()); + } + } Err(e) => panic!(e.to_string()) } } - fn send_enter(&self) { + fn send_enter(&self, active_config: &Configs) { unsafe { - send_enter(); + if active_config.fast_inject { + fast_send_enter(); + }else{ + send_enter(); + } } } - fn trigger_paste(&self, shortcut: &PasteShortcut) { + fn trigger_paste(&self, active_config: &Configs) { unsafe { - match shortcut { + match active_config.paste_shortcut { PasteShortcut::Default => { let is_special = is_current_window_special(); @@ -79,17 +90,27 @@ impl super::KeyboardManager for LinuxKeyboardManager { } } - fn delete_string(&self, count: i32) { - unsafe {delete_string(count)} - } - - fn move_cursor_left(&self, count: i32) { + fn delete_string(&self, active_config: &Configs, count: i32) { unsafe { - left_arrow(count); + if active_config.fast_inject { + fast_delete_string(count); + }else{ + delete_string(count) + } } } - fn trigger_copy(&self) { + fn move_cursor_left(&self, active_config: &Configs, count: i32) { + unsafe { + if active_config.fast_inject { + fast_left_arrow(count); + }else{ + left_arrow(count); + } + } + } + + fn trigger_copy(&self, _: &Configs) { unsafe { trigger_copy(); } diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index ccc4faf..03442fb 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -21,12 +21,13 @@ use std::ffi::CString; use crate::bridge::macos::*; use super::PasteShortcut; use log::error; +use crate::config::Configs; pub struct MacKeyboardManager { } impl super::KeyboardManager for MacKeyboardManager { - fn send_string(&self, s: &str) { + fn send_string(&self, _: &Configs, s: &str) { let res = CString::new(s); match res { Ok(cstr) => unsafe { send_string(cstr.as_ptr()); } @@ -34,16 +35,16 @@ impl super::KeyboardManager for MacKeyboardManager { } } - fn send_enter(&self) { + fn send_enter(&self, _: &Configs) { unsafe { // Send the kVK_Return key press send_vkey(0x24); } } - fn trigger_paste(&self, shortcut: &PasteShortcut) { + fn trigger_paste(&self, active_config: &Configs) { unsafe { - match shortcut { + match active_config.paste_shortcut { PasteShortcut::Default => { unsafe { trigger_paste(); @@ -56,17 +57,17 @@ impl super::KeyboardManager for MacKeyboardManager { } } - fn trigger_copy(&self) { + fn trigger_copy(&self, _: &Configs) { unsafe { trigger_copy(); } } - fn delete_string(&self, count: i32) { + fn delete_string(&self, _: &Configs, count: i32) { unsafe {delete_string(count)} } - fn move_cursor_left(&self, count: i32) { + fn move_cursor_left(&self, _: &Configs, count: i32) { unsafe { // Simulate the Left arrow count times send_multi_vkey(0x7B, count); diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 718b42f..304b201 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -18,6 +18,7 @@ */ use serde::{Serialize, Deserialize}; +use crate::config::Configs; #[cfg(target_os = "windows")] mod windows; @@ -29,12 +30,12 @@ mod linux; mod macos; pub trait KeyboardManager { - fn send_string(&self, s: &str); - fn send_enter(&self); - fn trigger_paste(&self, shortcut: &PasteShortcut); - fn delete_string(&self, count: i32); - fn move_cursor_left(&self, count: i32); - fn trigger_copy(&self); + fn send_string(&self, active_config: &Configs, s: &str); + fn send_enter(&self, active_config: &Configs); + fn trigger_paste(&self, active_config: &Configs); + fn delete_string(&self, active_config: &Configs, count: i32); + fn move_cursor_left(&self, active_config: &Configs, count: i32); + fn trigger_copy(&self, active_config: &Configs); } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs index c7ee10a..e1e0d09 100644 --- a/src/keyboard/windows.rs +++ b/src/keyboard/windows.rs @@ -26,7 +26,7 @@ pub struct WindowsKeyboardManager { } impl super::KeyboardManager for WindowsKeyboardManager { - fn send_string(&self, s: &str) { + fn send_string(&self, _: &Configs, s: &str) { let res = U16CString::from_str(s); match res { Ok(s) => { @@ -39,16 +39,16 @@ impl super::KeyboardManager for WindowsKeyboardManager { } - fn send_enter(&self) { + fn send_enter(&self, _: &Configs) { unsafe { // Send the VK_RETURN key press send_vkey(0x0D); } } - fn trigger_paste(&self, shortcut: &PasteShortcut) { + fn trigger_paste(&self, active_config: &Configs) { unsafe { - match shortcut { + match active_config.paste_shortcut { PasteShortcut::Default => { unsafe { trigger_paste(); @@ -61,20 +61,20 @@ impl super::KeyboardManager for WindowsKeyboardManager { } } - fn delete_string(&self, count: i32) { + fn delete_string(&self, _: &Configs, count: i32) { unsafe { delete_string(count) } } - fn move_cursor_left(&self, count: i32) { + fn move_cursor_left(&self, _: &Configs, count: i32) { unsafe { // Send the left arrow key multiple times send_multi_vkey(0x25, count) } } - fn trigger_copy(&self) { + fn trigger_copy(&self, _: &Configs) { unsafe { trigger_copy(); }