Add fast injection mode on Linux

This commit is contained in:
Federico Terzi 2020-04-18 19:31:24 +02:00
parent 83fd4cec99
commit 573b8ddcfd
12 changed files with 401 additions and 42 deletions

View File

@ -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 .)

View File

@ -18,6 +18,7 @@
*/
#include "bridge.h"
#include "fast_xdo.h"
#include <locale.h>
#include <stdio.h>
@ -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; i<count; i++) {
fast_send_event(xdo_context, focused, keycode, 1);
fast_send_event(xdo_context, focused, keycode, 0);
}
XFlush(xdo_context->xdpy);
}
void fast_send_enter() {
_fast_send_keycode_to_focused_window(XK_Return, 1);
}
void delete_string(int32_t count) {
for (int i = 0; i<count; i++) {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 1000);
}
}
void fast_delete_string(int32_t count) {
_fast_send_keycode_to_focused_window(XK_BackSpace, count);
}
void left_arrow(int32_t count) {
for (int i = 0; i<count; i++) {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 1000);
}
}
void fast_left_arrow(int32_t count) {
_fast_send_keycode_to_focused_window(XK_Left, count);
}
void trigger_paste() {
// Before sending the paste shortcut, trigger the press and release of the Shift key
// this is needed because for some triggers, for example ending with ":", the user

View File

@ -62,21 +62,41 @@ extern "C" void register_keypress_callback(KeypressCallback callback);
*/
extern "C" void send_string(const char * string);
/*
* Type the given string by simulating Key Presses using a faster inject method
*/
extern "C" void fast_send_string(const char * string);
/*
* Send the backspace keypress, *count* times.
*/
extern "C" void delete_string(int32_t count);
/*
* Send the backspace keypress, *count* times using a faster inject method
*/
extern "C" void fast_delete_string(int32_t count);
/*
* Send an Enter key press
*/
extern "C" void send_enter();
/*
* Send an Enter key press using a faster inject method
*/
extern "C" void fast_send_enter();
/*
* Send the left arrow keypress, *count* times.
*/
extern "C" void left_arrow(int32_t count);
/*
* Send the left arrow keypress, *count* times using a faster inject method
*/
extern "C" void fast_left_arrow(int32_t count);
/*
* Trigger normal paste ( Pressing CTRL+V )
*/

View File

@ -0,0 +1,247 @@
//
// 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.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include "fast_xdo.h"
extern "C" { // Needed to avoid C++ compiler name mangling
#include <xdo.h>
}
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);
}

View File

@ -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 <xdo.h>
}
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

View File

@ -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();
}

View File

@ -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<Match> { Vec::new() }
fn default_global_vars() -> Vec<MatchVariable> { 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<Match>,

View File

@ -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<String> = 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")

View File

@ -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 {
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 delete_string(&self, active_config: &Configs, count: i32) {
unsafe {
if active_config.fast_inject {
fast_delete_string(count);
}else{
delete_string(count)
}
}
}
fn move_cursor_left(&self, count: i32) {
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) {
fn trigger_copy(&self, _: &Configs) {
unsafe {
trigger_copy();
}

View File

@ -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);

View File

@ -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)]

View File

@ -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();
}