From 1b8ad38b33aaf0cffc578da31c1ebd44175b9298 Mon Sep 17 00:00:00 2001 From: Heiko Carrasco Date: Thu, 5 Mar 2020 16:52:17 +0100 Subject: [PATCH 01/18] Add support for kitty terminal --- native/liblinuxbridge/bridge.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index f353eac..1abff97 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -495,6 +495,8 @@ int32_t is_current_window_special() { return 1; }else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal return 1; + }else if (strstr(class_buffer, "kitty") != NULL) { // kitty terminal + return 1; } } From b523eadf6e5e07caf62bd652c98c23461548c96f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 7 Mar 2020 23:24:01 +0100 Subject: [PATCH 02/18] Reduce linux key injection delay --- native/liblinuxbridge/bridge.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index f353eac..5eeb78e 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -269,18 +269,18 @@ void event_callback(XPointer p, XRecordInterceptData *hook) } void send_string(const char * string) { - xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 12000); + xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000); } void delete_string(int32_t count) { for (int i = 0; i Date: Sun, 8 Mar 2020 00:23:26 +0100 Subject: [PATCH 03/18] Refactor anti self-injection mechanism --- src/config/mod.rs | 5 ----- src/context/linux.rs | 17 +++++++++++++++-- src/context/mod.rs | 7 ++++--- src/engine.rs | 30 +++++++++++------------------- src/main.rs | 14 ++++++++++---- 5 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index f841121..e1c0595 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -61,7 +61,6 @@ fn default_passive_arg_escape() -> char { '\\' } fn default_passive_key() -> KeyModifier { KeyModifier::OFF } fn default_enable_passive() -> bool { false } fn default_enable_active() -> bool { true } -fn default_action_noop_interval() -> u128 { 500 } fn default_backspace_limit() -> i32 { 3 } fn default_restore_clipboard_delay() -> i32 { 300 } fn default_exclude_default_entries() -> bool {false} @@ -130,9 +129,6 @@ pub struct Configs { #[serde(default = "default_enable_active")] pub enable_active: bool, - #[serde(default = "default_action_noop_interval")] - pub action_noop_interval: u128, - #[serde(default)] pub paste_shortcut: PasteShortcut, @@ -193,7 +189,6 @@ impl Configs { validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter()); validate_field!(result, self.passive_arg_escape, default_passive_arg_escape()); validate_field!(result, self.passive_key, default_passive_key()); - validate_field!(result, self.action_noop_interval, default_action_noop_interval()); validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay()); result diff --git a/src/context/linux.rs b/src/context/linux.rs index 9bed592..67185f3 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -26,14 +26,18 @@ use std::process::exit; use log::{debug, error, info}; use std::ffi::CStr; use std::{thread, time}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::sync::atomic::Ordering::Acquire; #[repr(C)] pub struct LinuxContext { - pub send_channel: Sender + pub send_channel: Sender, + is_injecting: Arc, } impl LinuxContext { - pub fn new(send_channel: Sender) -> Box { + pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { // Check if the X11 context is available let x11_available = unsafe { check_x11() @@ -46,6 +50,7 @@ impl LinuxContext { let context = Box::new(LinuxContext { send_channel, + is_injecting }); unsafe { @@ -85,6 +90,14 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, unsafe { let _self = _self as *mut LinuxContext; + // If espanso is currently injecting text, we should avoid processing + // external events, as it could happen that espanso reinterpret its + // own input. + if (*_self).is_injecting.load(Acquire) { + debug!("Input ignored while espanso is injecting text..."); + return; + } + if is_modifier == 0 { // Char event // Convert the received buffer to a string let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); diff --git a/src/context/mod.rs b/src/context/mod.rs index 782538f..ac9f9fe 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -30,7 +30,8 @@ use std::sync::mpsc::Sender; use crate::event::Event; use std::path::PathBuf; use std::fs::create_dir_all; -use std::sync::Once; +use std::sync::{Once, Arc}; +use std::sync::atomic::AtomicBool; pub trait Context { fn eventloop(&self); @@ -44,8 +45,8 @@ pub fn new(send_channel: Sender) -> Box { // LINUX IMPLEMENTATION #[cfg(target_os = "linux")] -pub fn new(send_channel: Sender) -> Box { - linux::LinuxContext::new(send_channel) +pub fn new(send_channel: Sender, is_injecting: Arc,) -> Box { + linux::LinuxContext::new(send_channel, is_injecting) } // WINDOWS IMPLEMENTATION diff --git a/src/engine.rs b/src/engine.rs index bb3a5ed..622f169 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -33,6 +33,9 @@ use std::collections::HashMap; use std::path::PathBuf; use regex::{Regex, Captures}; use std::time::SystemTime; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::{Relaxed, Release, Acquire, AcqRel, SeqCst}; pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> { @@ -41,29 +44,28 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager< config_manager: &'a M, ui_manager: &'a U, renderer: &'a R, + is_injecting: Arc, enabled: RefCell, last_action_time: RefCell, // Used to block espanso from re-interpreting it's own inputs - action_noop_interval: u128, } impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> Engine<'a, S, C, M, U, R> { pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C, config_manager: &'a M, ui_manager: &'a U, - renderer: &'a R) -> Engine<'a, S, C, M, U, R> { + renderer: &'a R, is_injecting: Arc) -> Engine<'a, S, C, M, U, R> { let enabled = RefCell::new(true); let last_action_time = RefCell::new(SystemTime::now()); - let action_noop_interval = config_manager.default_config().action_noop_interval; Engine{keyboard_manager, clipboard_manager, config_manager, ui_manager, renderer, + is_injecting, enabled, last_action_time, - action_noop_interval, } } @@ -139,11 +141,8 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa return; } - // avoid espanso reinterpreting its own actions - if self.check_last_action_and_set(self.action_noop_interval) { - debug!("Last action was too near, nooping the action."); - return; - } + // Block espanso from reinterpreting its own actions + self.is_injecting.store(true, Release); let char_count = if trailing_separator.is_none() { m.triggers[trigger_offset].chars().count() as i32 @@ -246,14 +245,12 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.clipboard_manager.set_clipboard(&previous_clipboard_content); } + + // Re-allow espanso to interpret actions + self.is_injecting.store(false, Release); } fn on_enable_update(&self, status: bool) { - // avoid espanso reinterpreting its own actions - if self.check_last_action_and_set(self.action_noop_interval) { - return; - } - let message = if status { "espanso enabled" }else{ @@ -269,11 +266,6 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } fn on_passive(&self) { - // avoid espanso reinterpreting its own actions - if self.check_last_action_and_set(self.action_noop_interval) { - return; - } - let config = self.config_manager.active_config(); if !config.enable_passive { diff --git a/src/main.rs b/src/main.rs index 78e8d0d..0505a6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ extern crate lazy_static; use std::thread; use std::fs::{File, OpenOptions}; use std::process::exit; -use std::sync::mpsc; +use std::sync::{mpsc, Arc}; use std::sync::mpsc::Receiver; use std::time::Duration; @@ -44,6 +44,7 @@ use crate::protocol::*; use std::io::{BufReader, BufRead}; use crate::package::default::DefaultPackageManager; use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult}; +use std::sync::atomic::AtomicBool; mod ui; mod edit; @@ -319,11 +320,15 @@ fn daemon_main(config_set: ConfigSet) { let (send_channel, receive_channel) = mpsc::channel(); - let context = context::new(send_channel.clone()); + // This atomic bool is used to "disable" espanso when espanding its own matches, otherwise + // we could reinterpret the characters we are injecting + let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false)); + + let context = context::new(send_channel.clone(), is_injecting.clone()); let config_set_copy = config_set.clone(); thread::Builder::new().name("daemon_background".to_string()).spawn(move || { - daemon_background(receive_channel, config_set_copy); + daemon_background(receive_channel, config_set_copy, is_injecting); }).expect("Unable to spawn daemon background thread"); let ipc_server = protocol::get_ipc_server(config_set, send_channel.clone()); @@ -333,7 +338,7 @@ fn daemon_main(config_set: ConfigSet) { } /// Background thread worker for the daemon -fn daemon_background(receive_channel: Receiver, config_set: ConfigSet) { +fn daemon_background(receive_channel: Receiver, config_set: ConfigSet, is_injecting: Arc) { let system_manager = system::get_manager(); let config_manager = RuntimeConfigManager::new(config_set, system_manager); @@ -354,6 +359,7 @@ fn daemon_background(receive_channel: Receiver, config_set: ConfigSet) { &config_manager, &ui_manager, &renderer, + is_injecting, ); let matcher = ScrollingMatcher::new(&config_manager, &engine); From fb032deb56e9fd9a12ac89aa84ed15063455f974 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 09:57:21 +0100 Subject: [PATCH 04/18] Refactor Windows context with new anti self-injection mechanism --- src/context/mod.rs | 6 +++--- src/context/windows.rs | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/context/mod.rs b/src/context/mod.rs index ac9f9fe..dec7eca 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -45,14 +45,14 @@ pub fn new(send_channel: Sender) -> Box { // LINUX IMPLEMENTATION #[cfg(target_os = "linux")] -pub fn new(send_channel: Sender, is_injecting: Arc,) -> Box { +pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { linux::LinuxContext::new(send_channel, is_injecting) } // WINDOWS IMPLEMENTATION #[cfg(target_os = "windows")] -pub fn new(send_channel: Sender) -> Box { - windows::WindowsContext::new(send_channel) +pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { + windows::WindowsContext::new(send_channel, is_injecting) } // espanso directories diff --git a/src/context/windows.rs b/src/context/windows.rs index c6ecf57..a0cf529 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -24,17 +24,21 @@ use crate::event::KeyModifier::*; use std::ffi::c_void; use std::{fs}; use widestring::{U16CString, U16CStr}; -use log::{info, error}; +use log::{info, error, debug}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::sync::atomic::Ordering::Acquire; const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp"); const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); pub struct WindowsContext { send_channel: Sender, + is_injecting: Arc, } impl WindowsContext { - pub fn new(send_channel: Sender) -> Box { + pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { // Initialize image resources let espanso_dir = super::get_data_dir(); @@ -68,6 +72,7 @@ impl WindowsContext { let context = Box::new(WindowsContext{ send_channel, + is_injecting, }); unsafe { @@ -106,6 +111,15 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32 is_modifier: i32, key_code: i32, is_key_down: i32) { unsafe { let _self = _self as *mut WindowsContext; + + // If espanso is currently injecting text, we should avoid processing + // external events, as it could happen that espanso reinterpret its + // own input. + if (*_self).is_injecting.load(Acquire) { + debug!("Input ignored while espanso is injecting text..."); + return; + } + if is_key_down != 0 { // KEY DOWN EVENT if is_modifier == 0 { // Char event // Convert the received buffer to a string From 4735c5846eaf7f796820733c9653b60ac5f58719 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 18:28:39 +0100 Subject: [PATCH 05/18] Fix a bug that prevented correct text injection on Linux when replacements contained characters common to the trigger. --- native/liblinuxbridge/bridge.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 5eeb78e..421af94 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -254,7 +254,7 @@ void event_callback(XPointer p, XRecordInterceptData *hook) switch (event_type) { case KeyPress: - //printf ("%d %d %s\n", key_code, res, buffer.data()); + //printf ("Press %d %d %s\n", key_code, res, buffer.data()); if (res > 0 && key_code != 22) { // Printable character, but not backspace keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code); }else{ // Modifier key @@ -268,7 +268,30 @@ void event_callback(XPointer p, XRecordInterceptData *hook) XRecordFreeData(hook); } +void release_all_keys() { + char keys[32]; + XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard + for (int i = 0; i<32; i++) { + // Only those that show a keypress should be changed + if (keys[i] != 0) { + for (int k = 0; k<8; k++) { + if ((keys[i] & (1 << k)) != 0) { // Bit by bit check + int key_code = i*8 + k; + XTestFakeKeyEvent(xdo_context->xdpy, key_code, false, CurrentTime); + } + } + } + } +} + void 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, 1000); } From 3221b41d012fad7627046668e3f1b39c423d9e1b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 18:37:26 +0100 Subject: [PATCH 06/18] Add ! and ? characters as word separators --- src/config/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index e1c0595..64b7389 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -51,7 +51,7 @@ fn default_conflict_check() -> bool{ true } fn default_ipc_server_port() -> i32 { 34982 } fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } -fn default_word_separators() -> Vec { vec![' ', ',', '.', '\r', '\n', 22u8 as char] } +fn default_word_separators() -> Vec { vec![' ', ',', '.', '?', '!', '\r', '\n', 22u8 as char] } fn default_toggle_interval() -> u32 { 230 } fn default_toggle_key() -> KeyModifier { KeyModifier::ALT } fn default_preserve_clipboard() -> bool {true} From 23095d4394cf4bbb44f3900571b413a3ff271b8b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 19:06:01 +0100 Subject: [PATCH 07/18] Add Auto backend to automatically switch between Inject and Clipboard on Linux based on match content --- src/config/mod.rs | 15 ++++++++++++++- src/engine.rs | 24 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 64b7389..180566e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -198,7 +198,20 @@ impl Configs { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum BackendType { Inject, - Clipboard + Clipboard, + + // On Linux systems there is a long standing issue with text injection (which + // in general is better than Clipboard copy/pasting) that prevents certain + // apps from correctly handling special characters (such as emojis or accented letters) + // when injected. For this reason, espanso initially defaulted on the Clipboard + // backend on Linux, as it was the most reliable (working in 99% of cases), + // even though it was less efficient and with a few inconveniences (for example, the + // previous clipboard content being overwritten). + // The Auto backend tries to take it a step further, by automatically determining + // when an injection is possible (only ascii characters in the replacement), and falling + // back to the Clipboard backend otherwise. + // Should only be used on Linux systems. + Auto } impl Default for BackendType { // The default backend varies based on the operating system. diff --git a/src/engine.rs b/src/engine.rs index 622f169..ac22cd1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -189,7 +189,25 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa None }; - match config.backend { + let backend = if config.backend == BackendType::Auto { + if cfg!(target_os = "linux") { + let all_ascii = target_string.chars().all(|c| c.is_ascii()); + if all_ascii { + debug!("All elements of the replacement are ascii, using Inject backend"); + &BackendType::Inject + }else{ + debug!("There are non-ascii characters, using Clipboard backend"); + &BackendType::Clipboard + } + }else{ + warn!("Using Auto backend is only supported on Linux, falling back to Inject backend."); + &BackendType::Inject + } + }else{ + &config.backend + }; + + match backend { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically // while on windows and macos, we need to emulate a Enter key press. @@ -217,6 +235,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.clipboard_manager.set_clipboard(&target_string); self.keyboard_manager.trigger_paste(&config.paste_shortcut); }, + _ => { + error!("Unsupported backend type evaluation."); + return; + } } if let Some(moves) = cursor_rewind { From 33cfb156dbdca80e58e9b5cc1e511ec97ab841a6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 19:10:26 +0100 Subject: [PATCH 08/18] Change way Inject backend handles multiline matches on Linux --- native/liblinuxbridge/bridge.cpp | 4 ++++ native/liblinuxbridge/bridge.h | 5 +++++ src/bridge/linux.rs | 1 + src/engine.rs | 21 +++++++-------------- src/keyboard/linux.rs | 4 +++- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 421af94..7aff24f 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -295,6 +295,10 @@ void send_string(const char * string) { xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000); } +void send_enter() { + xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000); +} + void delete_string(int32_t count) { for (int i = 0; i, U: UIMa match backend { BackendType::Inject => { - // Send the expected string. On linux, newlines are managed automatically - // while on windows and macos, we need to emulate a Enter key press. + // To handle newlines, substitute each "\n" char with an Enter key press. + let splits = target_string.split('\n'); - if cfg!(target_os = "linux") { - self.keyboard_manager.send_string(&target_string); - }else{ - // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.split('\n'); - - for (i, split) in splits.enumerate() { - if i > 0 { - self.keyboard_manager.send_enter(); - } - - self.keyboard_manager.send_string(split); + for (i, split) in splits.enumerate() { + if i > 0 { + self.keyboard_manager.send_enter(); } + + self.keyboard_manager.send_string(split); } }, BackendType::Clipboard => { diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 04c3c3a..03d55ba 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -35,7 +35,9 @@ impl super::KeyboardManager for LinuxKeyboardManager { } fn send_enter(&self) { - // On linux this is not needed, so NOOP + unsafe { + send_enter(); + } } fn trigger_paste(&self, shortcut: &PasteShortcut) { From 5273d8b8059870dadf0da0b4b801e76b906bbcb7 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 19:51:38 +0100 Subject: [PATCH 09/18] Add register special keys to improve word matches reliability --- native/liblinuxbridge/bridge.cpp | 5 ++++- native/liblinuxbridge/bridge.h | 2 +- src/context/linux.rs | 12 +++++++++--- src/event/mod.rs | 3 ++- src/matcher/mod.rs | 4 ++++ src/matcher/scrolling.rs | 7 +++++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 7aff24f..654a95f 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -137,7 +137,7 @@ int32_t initialize(void * _context_instance) { return -4; } record_range->device_events.first = KeyPress; - record_range->device_events.last = KeyRelease; + record_range->device_events.last = ButtonPress; // We want to get the keys from all clients XRecordClientSpec client_spec; @@ -261,6 +261,9 @@ void event_callback(XPointer p, XRecordInterceptData *hook) keypress_callback(context_instance, NULL, 0, 1, key_code); } break; + case ButtonPress: // Send also mouse button presses as "other events" + //printf ("Press button %d\n", key_code); + keypress_callback(context_instance, NULL, 0, 2, key_code); default: break; } diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index e976c71..876a4b3 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -48,7 +48,7 @@ extern "C" void cleanup(); * Called when a new keypress is made, the first argument is an char array, * while the second is the size of the array. */ -typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t is_modifier, int32_t key_code); +typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code); extern KeypressCallback keypress_callback; diff --git a/src/context/linux.rs b/src/context/linux.rs index 67185f3..95186b2 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -86,7 +86,7 @@ impl Drop for LinuxContext { // Native bridge code extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, - is_modifier: i32, key_code: i32) { + event_type: i32, key_code: i32) { unsafe { let _self = _self as *mut LinuxContext; @@ -98,7 +98,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, return; } - if is_modifier == 0 { // Char event + if event_type == 0 { // Char event // Convert the received buffer to a string let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); let char_str = c_str.to_str(); @@ -113,7 +113,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, debug!("Unable to receive char: {}",e); }, } - }else{ // Modifier event + }else if event_type == 1 { // Modifier event let modifier: Option = match key_code { 133 => Some(META), 50 => Some(SHIFT), @@ -126,7 +126,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, if let Some(modifier) = modifier { let event = Event::Key(KeyEvent::Modifier(modifier)); (*_self).send_channel.send(event).unwrap(); + }else{ // Not one of the default modifiers, send an "other" event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } + }else{ // Other type of event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } } } \ No newline at end of file diff --git a/src/event/mod.rs b/src/event/mod.rs index dc47691..8e81324 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -53,7 +53,8 @@ impl From for ActionType { #[derive(Debug, Clone)] pub enum KeyEvent { Char(String), - Modifier(KeyModifier) + Modifier(KeyModifier), + Other } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index e322355..4356c34 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -239,6 +239,7 @@ pub trait MatchReceiver { pub trait Matcher : KeyEventReceiver { fn handle_char(&self, c: &str); fn handle_modifier(&self, m: KeyModifier); + fn handle_other(&self); } impl KeyEventReceiver for M { @@ -250,6 +251,9 @@ impl KeyEventReceiver for M { KeyEvent::Modifier(m) => { self.handle_modifier(m); }, + KeyEvent::Other => { + self.handle_other(); + }, } } } diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 9b72b07..17b3da1 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -236,6 +236,13 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa current_set_queue.pop_back(); } } + + fn handle_other(&self) { + // When receiving "other" type of events, we mark them as valid separators. + // This dramatically improves the reliability of word matches + let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); + *was_previous_char_word_separator = true; + } } impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver for ScrollingMatcher<'a, R, M> { From 43a82872d36367fd13c36e192d0add4aad901f9a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 20:11:57 +0100 Subject: [PATCH 10/18] Add "other" event type on macOS to improve word matches reliability --- native/libmacbridge/AppDelegate.mm | 8 ++++++-- native/libmacbridge/bridge.h | 2 +- src/context/macos.rs | 33 +++++++++++++++++++++++------- src/context/mod.rs | 4 ++-- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/native/libmacbridge/AppDelegate.mm b/native/libmacbridge/AppDelegate.mm index 35e9177..29d1213 100644 --- a/native/libmacbridge/AppDelegate.mm +++ b/native/libmacbridge/AppDelegate.mm @@ -36,16 +36,20 @@ [myStatusItem.button setTarget:self]; // Setup key listener - [NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged) + [NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown) handler:^(NSEvent *event){ + if (event.type == NSEventTypeKeyDown && event.keyCode != 0x33) { // Send backspace as a modifier - const char * chars = [event.characters UTF8String]; + const char *chars = [event.characters UTF8String]; int len = event.characters.length; keypress_callback(context_instance, chars, len, 0, event.keyCode); //NSLog(@"keydown: %@, %d", event.characters, event.keyCode); + }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) { + // Send the mouse button clicks as "other" events, used to improve word matches reliability + keypress_callback(context_instance, NULL, 0, 2, event.buttonNumber); }else{ // Because this event is triggered for both the press and release of a modifier, trigger the callback // only on release diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index 3fc9f9d..dc74187 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -46,7 +46,7 @@ int32_t headless_eventloop(); * Called when a new keypress is made, the first argument is an char array, * while the second is the size of the array. */ -typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t is_modifier, int32_t key_code); +typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code); extern KeypressCallback keypress_callback; diff --git a/src/context/macos.rs b/src/context/macos.rs index 6c66ed3..b3f621f 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -24,17 +24,21 @@ use crate::event::{Event, KeyEvent, KeyModifier, ActionType}; use crate::event::KeyModifier::*; use std::ffi::{CString, CStr}; use std::fs; -use log::{info, error}; +use log::{info, error, debug}; use std::process::exit; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::sync::atomic::Ordering::Acquire; const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); pub struct MacContext { - pub send_channel: Sender + pub send_channel: Sender, + is_injecting: Arc, } impl MacContext { - pub fn new(send_channel: Sender) -> Box { + pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { // Check accessibility unsafe { let res = prompt_accessibility(); @@ -48,7 +52,8 @@ impl MacContext { } let context = Box::new(MacContext { - send_channel + send_channel, + is_injecting }); // Initialize the status icon path @@ -89,11 +94,19 @@ impl super::Context for MacContext { // Native bridge code extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, - is_modifier: i32, key_code: i32) { + event_type: i32, key_code: i32) { unsafe { let _self = _self as *mut MacContext; - if is_modifier == 0 { // Char event + // If espanso is currently injecting text, we should avoid processing + // external events, as it could happen that espanso reinterpret its + // own input. + if (*_self).is_injecting.load(Acquire) { + debug!("Input ignored while espanso is injecting text..."); + return; + } + + if event_type == 0 { // Char event // Convert the received buffer to a string let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); let char_str = c_str.to_str(); @@ -108,7 +121,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, error!("Unable to receive char: {}",e); }, } - }else{ // Modifier event + }else if event_type == 1 { // Modifier event let modifier: Option = match key_code { 0x37 => Some(META), 0x38 => Some(SHIFT), @@ -121,7 +134,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, if let Some(modifier) = modifier { let event = Event::Key(KeyEvent::Modifier(modifier)); (*_self).send_channel.send(event).unwrap(); + }else{ // Not one of the default modifiers, send an "other" event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } + }else{ // Other type of event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } } } diff --git a/src/context/mod.rs b/src/context/mod.rs index dec7eca..332ba4f 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -39,8 +39,8 @@ pub trait Context { // MAC IMPLEMENTATION #[cfg(target_os = "macos")] -pub fn new(send_channel: Sender) -> Box { - macos::MacContext::new(send_channel) +pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { + macos::MacContext::new(send_channel, is_injecting) } // LINUX IMPLEMENTATION From 69001c6546a55c57b9a270a6f4bfc7926461d9bf Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 20:17:47 +0100 Subject: [PATCH 11/18] Move the incompatible backend warning --- src/config/mod.rs | 5 +++++ src/engine.rs | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 180566e..8acf48d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -326,6 +326,11 @@ impl ConfigSet { let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME); let default = Configs::load_config(default_file.as_path())?; + // Check that a compatible backend is used, otherwise warn the user + if cfg!(not(target_os = "linux")) && default.backend == BackendType::Auto { + eprintln!("Warning: Using Auto backend is only supported on Linux, falling back to Inject backend."); + } + // Analyze which config files has to be loaded let mut target_files = Vec::new(); diff --git a/src/engine.rs b/src/engine.rs index 8f7d298..3d69476 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -200,7 +200,6 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa &BackendType::Clipboard } }else{ - warn!("Using Auto backend is only supported on Linux, falling back to Inject backend."); &BackendType::Inject } }else{ From 5712c7fd612b6bc4fe5438638695ba69a843a179 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 20:33:44 +0100 Subject: [PATCH 12/18] Add change to consider modifiers as word separators, which improves word match reliability --- src/matcher/scrolling.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 17b3da1..98df210 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -235,6 +235,10 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let mut current_set_queue = self.current_set_queue.borrow_mut(); current_set_queue.pop_back(); } + + // Consider modifiers as separators to improve word matches reliability + let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); + *was_previous_char_word_separator = true; } fn handle_other(&self) { From df1ae9db9e971c817176e2d688de0be836947307 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 20:37:40 +0100 Subject: [PATCH 13/18] Version bump 0.5.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 357886f..e91938c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.5.1" +version = "0.5.2" dependencies = [ "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index eae189e..cdad161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.5.1" +version = "0.5.2" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/snapcraft.yaml b/snapcraft.yaml index fff4d38..1ca478a 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.5.1 +version: 0.5.2 summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. From c20b4728ba7fb04edecf69bf12d179bb7c4ded1f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 8 Mar 2020 20:59:57 +0100 Subject: [PATCH 14/18] Add mouse detection on Windows to improve word matches --- native/libwinbridge/bridge.cpp | 15 +++++++++++++-- native/libwinbridge/bridge.h | 2 +- src/context/windows.rs | 13 ++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 0621a5a..7d6d3df 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -263,6 +263,12 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, is_key_down); } } + }else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches + { + if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) { + //std::cout << "mouse down" << std::endl; + keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0); + } } return 0; @@ -343,14 +349,19 @@ int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) { ); // Register raw inputs - RAWINPUTDEVICE Rid[1]; + RAWINPUTDEVICE Rid[2]; Rid[0].usUsagePage = 0x01; Rid[0].usUsage = 0x06; Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages Rid[0].hwndTarget = window; - if (RegisterRawInputDevices(Rid, 1, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. + Rid[1].usUsagePage = 0x01; + Rid[1].usUsage = 0x02; + Rid[1].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages + Rid[1].hwndTarget = window; + + if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. return -1; } diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index a5f1d89..40ed1e6 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -39,7 +39,7 @@ extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_pat * Called when a new keypress is made, the first argument is an int array, * while the second is the size of the array. */ -typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t is_modifier, int32_t key_code, int32_t is_key_down); +typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t is_key_down); extern KeypressCallback keypress_callback; /* diff --git a/src/context/windows.rs b/src/context/windows.rs index a0cf529..ab15201 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -108,7 +108,7 @@ impl super::Context for WindowsContext { // Native bridge code extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32, - is_modifier: i32, key_code: i32, is_key_down: i32) { + event_type: i32, key_code: i32, is_key_down: i32) { unsafe { let _self = _self as *mut WindowsContext; @@ -121,7 +121,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32 } if is_key_down != 0 { // KEY DOWN EVENT - if is_modifier == 0 { // Char event + if event_type == 0 { // Char event // Convert the received buffer to a string let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); let c_string = U16CStr::from_slice_with_nul(buffer); @@ -144,7 +144,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32 } } }else{ // KEY UP event - if is_modifier != 0 { // Modifier event + if event_type == 1 { // Modifier event let modifier: Option = match key_code { 0x5B | 0x5C => Some(META), 0x10 => Some(SHIFT), @@ -157,7 +157,14 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32 if let Some(modifier) = modifier { let event = Event::Key(KeyEvent::Modifier(modifier)); (*_self).send_channel.send(event).unwrap(); + }else{ // Not one of the default modifiers, send an "other" event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } + }else{ + // Other type of event + let event = Event::Key(KeyEvent::Other); + (*_self).send_channel.send(event).unwrap(); } } } From f11d130c45dceb389e9f79090cec881c9b560b87 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 10 Mar 2020 17:22:50 +0100 Subject: [PATCH 15/18] Implement specific key modifiers on Windows. See #117 --- native/libwinbridge/bridge.cpp | 29 +++++++- native/libwinbridge/bridge.h | 5 +- src/bridge/windows.rs | 2 +- src/context/windows.rs | 18 +++-- src/event/mod.rs | 126 +++++++++++++++++++++++++++++++++ src/matcher/scrolling.rs | 4 +- 6 files changed, 170 insertions(+), 14 deletions(-) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 7d6d3df..3a771b9 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -33,6 +33,7 @@ #pragma comment( lib, "gdiplus.lib" ) #include +#include // How many milliseconds must pass between keystrokes to refresh the keyboard layout const long refreshKeyboardLayoutInterval = 2000; @@ -258,16 +259,38 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR // We need to call the callback in two different ways based on the type of key // The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it. if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) { - keypress_callback(manager_instance, reinterpret_cast(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down); + keypress_callback(manager_instance, reinterpret_cast(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, 0, is_key_down); }else{ - keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, is_key_down); + //std::cout << raw->data.keyboard.MakeCode << " " << raw->data.keyboard.Flags << std::endl; + int variant = 0; + if (raw->data.keyboard.VKey == VK_SHIFT) { + // To discriminate between the left and right shift, we need to employ a workaround. + // See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput + if (raw->data.keyboard.MakeCode == 42) { // Left shift + variant = LEFT_VARIANT; + }if (raw->data.keyboard.MakeCode == 54) { // Right shift + variant = RIGHT_VARIANT; + } + }else{ + // Also the ALT and CTRL key are special cases + // Check out the previous Stackoverflow question for more information + if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU) { + if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0) { + variant = RIGHT_VARIANT; + }else{ + variant = LEFT_VARIANT; + } + } + } + + keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, variant, is_key_down); } } }else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches { if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) { //std::cout << "mouse down" << std::endl; - keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0); + keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0, 0); } } diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index 40ed1e6..f8084ad 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -35,11 +35,14 @@ extern void * manager_instance; */ extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path); +#define LEFT_VARIANT 1 +#define RIGHT_VARIANT 2 + /* * Called when a new keypress is made, the first argument is an int array, * while the second is the size of the array. */ -typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t is_key_down); +typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t is_key_down, int32_t variant); extern KeypressCallback keypress_callback; /* diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index f7a4459..f337638 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -51,7 +51,7 @@ extern { // KEYBOARD pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16, - i32, i32, i32, i32)); + i32, i32, i32, i32, i32)); pub fn eventloop(); pub fn send_string(string: *const u16); diff --git a/src/context/windows.rs b/src/context/windows.rs index ab15201..c797d1f 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -108,7 +108,7 @@ impl super::Context for WindowsContext { // Native bridge code extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32, - event_type: i32, key_code: i32, is_key_down: i32) { + event_type: i32, key_code: i32, variant: i32, is_key_down: i32) { unsafe { let _self = _self as *mut WindowsContext; @@ -145,12 +145,16 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32 } }else{ // KEY UP event if event_type == 1 { // Modifier event - let modifier: Option = match key_code { - 0x5B | 0x5C => Some(META), - 0x10 => Some(SHIFT), - 0x12 => Some(ALT), - 0x11 => Some(CTRL), - 0x08 => Some(BACKSPACE), + let modifier: Option = match (key_code, variant) { + (0x5B, _) => Some(LEFT_META), + (0x5C, _) => Some(RIGHT_META), + (0x10, 1) => Some(LEFT_SHIFT), + (0x10, 2) => Some(RIGHT_SHIFT), + (0x12, 1) => Some(LEFT_ALT), + (0x12, 2) => Some(RIGHT_ALT), + (0x11, 1) => Some(LEFT_CTRL), + (0x11, 2) => Some(RIGHT_CTRL), + (0x08, _) => Some(BACKSPACE), _ => None, }; diff --git a/src/event/mod.rs b/src/event/mod.rs index 8e81324..7d63a7f 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -57,6 +57,7 @@ pub enum KeyEvent { Other } +#[warn(non_camel_case_types)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum KeyModifier { CTRL, @@ -65,6 +66,70 @@ pub enum KeyModifier { META, BACKSPACE, OFF, + + // These are specific variants of the ones above. See issue: #117 + // https://github.com/federico-terzi/espanso/issues/117 + LEFT_CTRL, + RIGHT_CTRL, + LEFT_ALT, + RIGHT_ALT, + LEFT_META, + RIGHT_META, + LEFT_SHIFT, + RIGHT_SHIFT, +} + +impl KeyModifier { + /// This function is used to compare KeyModifiers, considering the relations between + /// the generic modifier and the specific left/right variant + /// For example, CTRL will match with CTRL, LEFT_CTRL and RIGHT_CTRL; + /// but LEFT_CTRL will only match will LEFT_CTRL + pub fn shallow_equals(current: &KeyModifier, config: &KeyModifier) -> bool { + use KeyModifier::*; + + match config { + KeyModifier::CTRL => { + current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL + }, + KeyModifier::SHIFT => { + current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT + }, + KeyModifier::ALT => { + current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT + }, + KeyModifier::META => { + current == &LEFT_META || current == &RIGHT_META || current == &META + }, + KeyModifier::BACKSPACE => { + current == &BACKSPACE + }, + KeyModifier::LEFT_CTRL => { + current == &LEFT_CTRL + }, + KeyModifier::RIGHT_CTRL => { + current == &RIGHT_CTRL + }, + KeyModifier::LEFT_ALT => { + current == &LEFT_ALT + }, + KeyModifier::RIGHT_ALT => { + current == &RIGHT_ALT + }, + KeyModifier::LEFT_META => { + current == &LEFT_META + }, + KeyModifier::RIGHT_META => { + current == &RIGHT_META + }, + KeyModifier::LEFT_SHIFT => { + current == &LEFT_SHIFT + }, + KeyModifier::RIGHT_SHIFT => { + current == &RIGHT_SHIFT + }, + _ => {false}, + } + } } // Receivers @@ -75,4 +140,65 @@ pub trait KeyEventReceiver { pub trait ActionEventReceiver { fn on_action_event(&self, e: ActionType); +} + +// TESTS + +#[cfg(test)] +mod tests { + use super::*; + use super::KeyModifier::*; + + #[test] + fn test_shallow_equals_ctrl() { + assert!(KeyModifier::shallow_equals(&CTRL, &CTRL)); + assert!(KeyModifier::shallow_equals(&LEFT_CTRL, &CTRL)); + assert!(KeyModifier::shallow_equals(&RIGHT_CTRL, &CTRL)); + + assert!(!KeyModifier::shallow_equals(&CTRL, &LEFT_CTRL)); + assert!(!KeyModifier::shallow_equals(&CTRL, &RIGHT_CTRL)); + } + + #[test] + fn test_shallow_equals_shift() { + assert!(KeyModifier::shallow_equals(&SHIFT, &SHIFT)); + assert!(KeyModifier::shallow_equals(&LEFT_SHIFT, &SHIFT)); + assert!(KeyModifier::shallow_equals(&RIGHT_SHIFT, &SHIFT)); + + assert!(!KeyModifier::shallow_equals(&SHIFT, &LEFT_SHIFT)); + assert!(!KeyModifier::shallow_equals(&SHIFT, &RIGHT_SHIFT)); + } + + #[test] + fn test_shallow_equals_alt() { + assert!(KeyModifier::shallow_equals(&ALT, &ALT)); + assert!(KeyModifier::shallow_equals(&LEFT_ALT, &ALT)); + assert!(KeyModifier::shallow_equals(&RIGHT_ALT, &ALT)); + + assert!(!KeyModifier::shallow_equals(&ALT, &LEFT_ALT)); + assert!(!KeyModifier::shallow_equals(&ALT, &RIGHT_ALT)); + } + + #[test] + fn test_shallow_equals_meta() { + assert!(KeyModifier::shallow_equals(&META, &META)); + assert!(KeyModifier::shallow_equals(&LEFT_META, &META)); + assert!(KeyModifier::shallow_equals(&RIGHT_META, &META)); + + assert!(!KeyModifier::shallow_equals(&META, &LEFT_META)); + assert!(!KeyModifier::shallow_equals(&META, &RIGHT_META)); + } + + #[test] + fn test_shallow_equals_backspace() { + assert!(KeyModifier::shallow_equals(&BACKSPACE, &BACKSPACE)); + } + + #[test] + fn test_shallow_equals_off() { + assert!(!KeyModifier::shallow_equals(&OFF, &CTRL)); + assert!(!KeyModifier::shallow_equals(&OFF, &ALT)); + assert!(!KeyModifier::shallow_equals(&OFF, &META)); + assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT)); + } } \ No newline at end of file diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 98df210..b1a24a8 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -212,7 +212,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa // TODO: at the moment, activating the passive key triggers the toggle key // study a mechanism to avoid this problem - if m == config.toggle_key { + if KeyModifier::shallow_equals(&m, &config.toggle_key) { check_interval(&self.toggle_press_time, u128::from(config.toggle_interval), || { self.toggle(); @@ -223,7 +223,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa self.current_set_queue.borrow_mut().clear(); } }); - }else if m == config.passive_key { + }else if KeyModifier::shallow_equals(&m, &config.passive_key) { check_interval(&self.passive_press_time, u128::from(config.toggle_interval), || { self.receiver.on_passive(); From 7764f80eabefc76e0939aaa1eb18576d90da82bc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 10 Mar 2020 18:07:50 +0100 Subject: [PATCH 16/18] Implement specific key modifiers on macOS. See #117 --- src/context/macos.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/context/macos.rs b/src/context/macos.rs index b3f621f..0f31d21 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -123,10 +123,14 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, } }else if event_type == 1 { // Modifier event let modifier: Option = match key_code { - 0x37 => Some(META), - 0x38 => Some(SHIFT), - 0x3A => Some(ALT), - 0x3B => Some(CTRL), + 0x37 => Some(LEFT_META), + 0x36 => Some(RIGHT_META), + 0x38 => Some(LEFT_SHIFT), + 0x3C => Some(RIGHT_SHIFT), + 0x3A => Some(LEFT_ALT), + 0x3D => Some(RIGHT_ALT), + 0x3B => Some(LEFT_CTRL), + 0x3E => Some(RIGHT_CTRL), 0x33 => Some(BACKSPACE), _ => None, }; From 1285bc20cfcf9c629440574b36946171a717bc12 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 10 Mar 2020 18:51:11 +0100 Subject: [PATCH 17/18] Implement more specific KeyModifiers on Linux. Fix #117 --- src/context/linux.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/context/linux.rs b/src/context/linux.rs index 95186b2..876db30 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -114,11 +114,16 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, }, } }else if event_type == 1 { // Modifier event + let modifier: Option = match key_code { - 133 => Some(META), - 50 => Some(SHIFT), - 64 => Some(ALT), - 37 => Some(CTRL), + 133 => Some(LEFT_META), + 134 => Some(RIGHT_META), + 50 => Some(LEFT_SHIFT), + 62 => Some(RIGHT_SHIFT), + 64 => Some(LEFT_ALT), + 108 => Some(RIGHT_ALT), + 37 => Some(LEFT_CTRL), + 105 => Some(RIGHT_CTRL), 22 => Some(BACKSPACE), _ => None, }; From 01d490bfaeb7f5db8168ecf33bde99dd65ea7438 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 10 Mar 2020 19:00:28 +0100 Subject: [PATCH 18/18] Fix wrong parameter order --- native/libwinbridge/bridge.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index f8084ad..304f10b 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -42,7 +42,7 @@ extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_pat * Called when a new keypress is made, the first argument is an int array, * while the second is the size of the array. */ -typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t is_key_down, int32_t variant); +typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t variant, int32_t is_key_down); extern KeypressCallback keypress_callback; /*