commit
fa0158c197
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -370,7 +370,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
|
|
@ -137,7 +137,7 @@ int32_t initialize(void * _context_instance) {
|
||||||
return -4;
|
return -4;
|
||||||
}
|
}
|
||||||
record_range->device_events.first = KeyPress;
|
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
|
// We want to get the keys from all clients
|
||||||
XRecordClientSpec client_spec;
|
XRecordClientSpec client_spec;
|
||||||
|
@ -254,13 +254,16 @@ void event_callback(XPointer p, XRecordInterceptData *hook)
|
||||||
|
|
||||||
switch (event_type) {
|
switch (event_type) {
|
||||||
case KeyPress:
|
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
|
if (res > 0 && key_code != 22) { // Printable character, but not backspace
|
||||||
keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code);
|
keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code);
|
||||||
}else{ // Modifier key
|
}else{ // Modifier key
|
||||||
keypress_callback(context_instance, NULL, 0, 1, key_code);
|
keypress_callback(context_instance, NULL, 0, 1, key_code);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -268,19 +271,46 @@ void event_callback(XPointer p, XRecordInterceptData *hook)
|
||||||
XRecordFreeData(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) {
|
void send_string(const char * string) {
|
||||||
xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 12000);
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_enter() {
|
||||||
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void delete_string(int32_t count) {
|
void delete_string(int32_t count) {
|
||||||
for (int i = 0; i<count; i++) {
|
for (int i = 0; i<count; i++) {
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 8000);
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void left_arrow(int32_t count) {
|
void left_arrow(int32_t count) {
|
||||||
for (int i = 0; i<count; i++) {
|
for (int i = 0; i<count; i++) {
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 8000);
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,6 +525,8 @@ int32_t is_current_window_special() {
|
||||||
return 1;
|
return 1;
|
||||||
}else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal
|
}else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal
|
||||||
return 1;
|
return 1;
|
||||||
|
}else if (strstr(class_buffer, "kitty") != NULL) { // kitty terminal
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ extern "C" void cleanup();
|
||||||
* Called when a new keypress is made, the first argument is an char array,
|
* Called when a new keypress is made, the first argument is an char array,
|
||||||
* while the second is the size of the 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;
|
extern KeypressCallback keypress_callback;
|
||||||
|
|
||||||
|
@ -67,6 +67,11 @@ extern "C" void send_string(const char * string);
|
||||||
*/
|
*/
|
||||||
extern "C" void delete_string(int32_t count);
|
extern "C" void delete_string(int32_t count);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send an Enter key press
|
||||||
|
*/
|
||||||
|
extern "C" void send_enter();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Send the left arrow keypress, *count* times.
|
* Send the left arrow keypress, *count* times.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -36,16 +36,20 @@
|
||||||
[myStatusItem.button setTarget:self];
|
[myStatusItem.button setTarget:self];
|
||||||
|
|
||||||
// Setup key listener
|
// Setup key listener
|
||||||
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged)
|
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown)
|
||||||
handler:^(NSEvent *event){
|
handler:^(NSEvent *event){
|
||||||
|
|
||||||
if (event.type == NSEventTypeKeyDown
|
if (event.type == NSEventTypeKeyDown
|
||||||
&& event.keyCode != 0x33) { // Send backspace as a modifier
|
&& 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;
|
int len = event.characters.length;
|
||||||
|
|
||||||
keypress_callback(context_instance, chars, len, 0, event.keyCode);
|
keypress_callback(context_instance, chars, len, 0, event.keyCode);
|
||||||
//NSLog(@"keydown: %@, %d", event.characters, 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{
|
}else{
|
||||||
// Because this event is triggered for both the press and release of a modifier, trigger the callback
|
// Because this event is triggered for both the press and release of a modifier, trigger the callback
|
||||||
// only on release
|
// only on release
|
||||||
|
|
|
@ -46,7 +46,7 @@ int32_t headless_eventloop();
|
||||||
* Called when a new keypress is made, the first argument is an char array,
|
* Called when a new keypress is made, the first argument is an char array,
|
||||||
* while the second is the size of the 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;
|
extern KeypressCallback keypress_callback;
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
#pragma comment( lib, "gdiplus.lib" )
|
#pragma comment( lib, "gdiplus.lib" )
|
||||||
#include <gdiplus.h>
|
#include <gdiplus.h>
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
// How many milliseconds must pass between keystrokes to refresh the keyboard layout
|
// How many milliseconds must pass between keystrokes to refresh the keyboard layout
|
||||||
const long refreshKeyboardLayoutInterval = 2000;
|
const long refreshKeyboardLayoutInterval = 2000;
|
||||||
|
@ -258,11 +259,39 @@ 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
|
// 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.
|
// 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) {
|
if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) {
|
||||||
keypress_callback(manager_instance, reinterpret_cast<uint16_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down);
|
keypress_callback(manager_instance, reinterpret_cast<uint16_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, 0, is_key_down);
|
||||||
}else{
|
}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, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -343,14 +372,19 @@ int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register raw inputs
|
// Register raw inputs
|
||||||
RAWINPUTDEVICE Rid[1];
|
RAWINPUTDEVICE Rid[2];
|
||||||
|
|
||||||
Rid[0].usUsagePage = 0x01;
|
Rid[0].usUsagePage = 0x01;
|
||||||
Rid[0].usUsage = 0x06;
|
Rid[0].usUsage = 0x06;
|
||||||
Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages
|
Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages
|
||||||
Rid[0].hwndTarget = window;
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,14 @@ extern void * manager_instance;
|
||||||
*/
|
*/
|
||||||
extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path);
|
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,
|
* Called when a new keypress is made, the first argument is an int array,
|
||||||
* while the second is the size of the 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 variant, int32_t is_key_down);
|
||||||
extern KeypressCallback keypress_callback;
|
extern KeypressCallback keypress_callback;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 0.5.1
|
version: 0.5.2
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
|
@ -40,6 +40,7 @@ extern {
|
||||||
pub fn send_string(string: *const c_char);
|
pub fn send_string(string: *const c_char);
|
||||||
pub fn delete_string(count: i32);
|
pub fn delete_string(count: i32);
|
||||||
pub fn left_arrow(count: i32);
|
pub fn left_arrow(count: i32);
|
||||||
|
pub fn send_enter();
|
||||||
pub fn trigger_paste();
|
pub fn trigger_paste();
|
||||||
pub fn trigger_terminal_paste();
|
pub fn trigger_terminal_paste();
|
||||||
pub fn trigger_shift_ins_paste();
|
pub fn trigger_shift_ins_paste();
|
||||||
|
|
|
@ -51,7 +51,7 @@ extern {
|
||||||
|
|
||||||
// KEYBOARD
|
// KEYBOARD
|
||||||
pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16,
|
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 eventloop();
|
||||||
pub fn send_string(string: *const u16);
|
pub fn send_string(string: *const u16);
|
||||||
|
|
|
@ -51,7 +51,7 @@ fn default_conflict_check() -> bool{ true }
|
||||||
fn default_ipc_server_port() -> i32 { 34982 }
|
fn default_ipc_server_port() -> i32 { 34982 }
|
||||||
fn default_use_system_agent() -> bool { true }
|
fn default_use_system_agent() -> bool { true }
|
||||||
fn default_config_caching_interval() -> i32 { 800 }
|
fn default_config_caching_interval() -> i32 { 800 }
|
||||||
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
|
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '?', '!', '\r', '\n', 22u8 as char] }
|
||||||
fn default_toggle_interval() -> u32 { 230 }
|
fn default_toggle_interval() -> u32 { 230 }
|
||||||
fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
|
fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
|
||||||
fn default_preserve_clipboard() -> bool {true}
|
fn default_preserve_clipboard() -> bool {true}
|
||||||
|
@ -61,7 +61,6 @@ fn default_passive_arg_escape() -> char { '\\' }
|
||||||
fn default_passive_key() -> KeyModifier { KeyModifier::OFF }
|
fn default_passive_key() -> KeyModifier { KeyModifier::OFF }
|
||||||
fn default_enable_passive() -> bool { false }
|
fn default_enable_passive() -> bool { false }
|
||||||
fn default_enable_active() -> bool { true }
|
fn default_enable_active() -> bool { true }
|
||||||
fn default_action_noop_interval() -> u128 { 500 }
|
|
||||||
fn default_backspace_limit() -> i32 { 3 }
|
fn default_backspace_limit() -> i32 { 3 }
|
||||||
fn default_restore_clipboard_delay() -> i32 { 300 }
|
fn default_restore_clipboard_delay() -> i32 { 300 }
|
||||||
fn default_exclude_default_entries() -> bool {false}
|
fn default_exclude_default_entries() -> bool {false}
|
||||||
|
@ -130,9 +129,6 @@ pub struct Configs {
|
||||||
#[serde(default = "default_enable_active")]
|
#[serde(default = "default_enable_active")]
|
||||||
pub enable_active: bool,
|
pub enable_active: bool,
|
||||||
|
|
||||||
#[serde(default = "default_action_noop_interval")]
|
|
||||||
pub action_noop_interval: u128,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub paste_shortcut: PasteShortcut,
|
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_delimiter, default_passive_arg_delimiter());
|
||||||
validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
|
validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
|
||||||
validate_field!(result, self.passive_key, default_passive_key());
|
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());
|
validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay());
|
||||||
|
|
||||||
result
|
result
|
||||||
|
@ -203,7 +198,20 @@ impl Configs {
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum BackendType {
|
pub enum BackendType {
|
||||||
Inject,
|
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 {
|
impl Default for BackendType {
|
||||||
// The default backend varies based on the operating system.
|
// The default backend varies based on the operating system.
|
||||||
|
@ -318,6 +326,11 @@ impl ConfigSet {
|
||||||
let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME);
|
let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
let default = Configs::load_config(default_file.as_path())?;
|
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
|
// Analyze which config files has to be loaded
|
||||||
|
|
||||||
let mut target_files = Vec::new();
|
let mut target_files = Vec::new();
|
||||||
|
|
|
@ -26,14 +26,18 @@ use std::process::exit;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering::Acquire;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct LinuxContext {
|
pub struct LinuxContext {
|
||||||
pub send_channel: Sender<Event>
|
pub send_channel: Sender<Event>,
|
||||||
|
is_injecting: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinuxContext {
|
impl LinuxContext {
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<LinuxContext> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> {
|
||||||
// Check if the X11 context is available
|
// Check if the X11 context is available
|
||||||
let x11_available = unsafe {
|
let x11_available = unsafe {
|
||||||
check_x11()
|
check_x11()
|
||||||
|
@ -46,6 +50,7 @@ impl LinuxContext {
|
||||||
|
|
||||||
let context = Box::new(LinuxContext {
|
let context = Box::new(LinuxContext {
|
||||||
send_channel,
|
send_channel,
|
||||||
|
is_injecting
|
||||||
});
|
});
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -81,11 +86,19 @@ impl Drop for LinuxContext {
|
||||||
// Native bridge code
|
// Native bridge code
|
||||||
|
|
||||||
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
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 {
|
unsafe {
|
||||||
let _self = _self as *mut LinuxContext;
|
let _self = _self as *mut LinuxContext;
|
||||||
|
|
||||||
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
|
// Convert the received buffer to a string
|
||||||
let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
|
let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
|
||||||
let char_str = c_str.to_str();
|
let char_str = c_str.to_str();
|
||||||
|
@ -100,12 +113,17 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
||||||
debug!("Unable to receive char: {}",e);
|
debug!("Unable to receive char: {}",e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}else{ // Modifier event
|
}else if event_type == 1 { // Modifier event
|
||||||
|
|
||||||
let modifier: Option<KeyModifier> = match key_code {
|
let modifier: Option<KeyModifier> = match key_code {
|
||||||
133 => Some(META),
|
133 => Some(LEFT_META),
|
||||||
50 => Some(SHIFT),
|
134 => Some(RIGHT_META),
|
||||||
64 => Some(ALT),
|
50 => Some(LEFT_SHIFT),
|
||||||
37 => Some(CTRL),
|
62 => Some(RIGHT_SHIFT),
|
||||||
|
64 => Some(LEFT_ALT),
|
||||||
|
108 => Some(RIGHT_ALT),
|
||||||
|
37 => Some(LEFT_CTRL),
|
||||||
|
105 => Some(RIGHT_CTRL),
|
||||||
22 => Some(BACKSPACE),
|
22 => Some(BACKSPACE),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -113,7 +131,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
||||||
if let Some(modifier) = modifier {
|
if let Some(modifier) = modifier {
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
let event = Event::Key(KeyEvent::Modifier(modifier));
|
||||||
(*_self).send_channel.send(event).unwrap();
|
(*_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,17 +24,21 @@ use crate::event::{Event, KeyEvent, KeyModifier, ActionType};
|
||||||
use crate::event::KeyModifier::*;
|
use crate::event::KeyModifier::*;
|
||||||
use std::ffi::{CString, CStr};
|
use std::ffi::{CString, CStr};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use log::{info, error};
|
use log::{info, error, debug};
|
||||||
use std::process::exit;
|
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");
|
const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png");
|
||||||
|
|
||||||
pub struct MacContext {
|
pub struct MacContext {
|
||||||
pub send_channel: Sender<Event>
|
pub send_channel: Sender<Event>,
|
||||||
|
is_injecting: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacContext {
|
impl MacContext {
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<MacContext> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> {
|
||||||
// Check accessibility
|
// Check accessibility
|
||||||
unsafe {
|
unsafe {
|
||||||
let res = prompt_accessibility();
|
let res = prompt_accessibility();
|
||||||
|
@ -48,7 +52,8 @@ impl MacContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = Box::new(MacContext {
|
let context = Box::new(MacContext {
|
||||||
send_channel
|
send_channel,
|
||||||
|
is_injecting
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the status icon path
|
// Initialize the status icon path
|
||||||
|
@ -89,11 +94,19 @@ impl super::Context for MacContext {
|
||||||
// Native bridge code
|
// Native bridge code
|
||||||
|
|
||||||
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
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 {
|
unsafe {
|
||||||
let _self = _self as *mut MacContext;
|
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
|
// Convert the received buffer to a string
|
||||||
let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
|
let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
|
||||||
let char_str = c_str.to_str();
|
let char_str = c_str.to_str();
|
||||||
|
@ -108,12 +121,16 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
||||||
error!("Unable to receive char: {}",e);
|
error!("Unable to receive char: {}",e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}else{ // Modifier event
|
}else if event_type == 1 { // Modifier event
|
||||||
let modifier: Option<KeyModifier> = match key_code {
|
let modifier: Option<KeyModifier> = match key_code {
|
||||||
0x37 => Some(META),
|
0x37 => Some(LEFT_META),
|
||||||
0x38 => Some(SHIFT),
|
0x36 => Some(RIGHT_META),
|
||||||
0x3A => Some(ALT),
|
0x38 => Some(LEFT_SHIFT),
|
||||||
0x3B => Some(CTRL),
|
0x3C => Some(RIGHT_SHIFT),
|
||||||
|
0x3A => Some(LEFT_ALT),
|
||||||
|
0x3D => Some(RIGHT_ALT),
|
||||||
|
0x3B => Some(LEFT_CTRL),
|
||||||
|
0x3E => Some(RIGHT_CTRL),
|
||||||
0x33 => Some(BACKSPACE),
|
0x33 => Some(BACKSPACE),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -121,7 +138,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
||||||
if let Some(modifier) = modifier {
|
if let Some(modifier) = modifier {
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
let event = Event::Key(KeyEvent::Modifier(modifier));
|
||||||
(*_self).send_channel.send(event).unwrap();
|
(*_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ use std::sync::mpsc::Sender;
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
use std::sync::Once;
|
use std::sync::{Once, Arc};
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
pub trait Context {
|
pub trait Context {
|
||||||
fn eventloop(&self);
|
fn eventloop(&self);
|
||||||
|
@ -38,20 +39,20 @@ pub trait Context {
|
||||||
|
|
||||||
// MAC IMPLEMENTATION
|
// MAC IMPLEMENTATION
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
|
||||||
macos::MacContext::new(send_channel)
|
macos::MacContext::new(send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
// LINUX IMPLEMENTATION
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
|
||||||
linux::LinuxContext::new(send_channel)
|
linux::LinuxContext::new(send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
// WINDOWS IMPLEMENTATION
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
|
||||||
windows::WindowsContext::new(send_channel)
|
windows::WindowsContext::new(send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
// espanso directories
|
// espanso directories
|
||||||
|
|
|
@ -24,17 +24,21 @@ use crate::event::KeyModifier::*;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::{fs};
|
use std::{fs};
|
||||||
use widestring::{U16CString, U16CStr};
|
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 BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp");
|
||||||
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico");
|
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico");
|
||||||
|
|
||||||
pub struct WindowsContext {
|
pub struct WindowsContext {
|
||||||
send_channel: Sender<Event>,
|
send_channel: Sender<Event>,
|
||||||
|
is_injecting: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowsContext {
|
impl WindowsContext {
|
||||||
pub fn new(send_channel: Sender<Event>) -> Box<WindowsContext> {
|
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> {
|
||||||
// Initialize image resources
|
// Initialize image resources
|
||||||
|
|
||||||
let espanso_dir = super::get_data_dir();
|
let espanso_dir = super::get_data_dir();
|
||||||
|
@ -68,6 +72,7 @@ impl WindowsContext {
|
||||||
|
|
||||||
let context = Box::new(WindowsContext{
|
let context = Box::new(WindowsContext{
|
||||||
send_channel,
|
send_channel,
|
||||||
|
is_injecting,
|
||||||
});
|
});
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -103,11 +108,20 @@ impl super::Context for WindowsContext {
|
||||||
// Native bridge code
|
// Native bridge code
|
||||||
|
|
||||||
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32,
|
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, variant: i32, is_key_down: i32) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _self = _self as *mut WindowsContext;
|
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_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
|
// Convert the received buffer to a string
|
||||||
let buffer = std::slice::from_raw_parts(raw_buffer, len as usize);
|
let buffer = std::slice::from_raw_parts(raw_buffer, len as usize);
|
||||||
let c_string = U16CStr::from_slice_with_nul(buffer);
|
let c_string = U16CStr::from_slice_with_nul(buffer);
|
||||||
|
@ -130,20 +144,31 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{ // KEY UP event
|
}else{ // KEY UP event
|
||||||
if is_modifier != 0 { // Modifier event
|
if event_type == 1 { // Modifier event
|
||||||
let modifier: Option<KeyModifier> = match key_code {
|
let modifier: Option<KeyModifier> = match (key_code, variant) {
|
||||||
0x5B | 0x5C => Some(META),
|
(0x5B, _) => Some(LEFT_META),
|
||||||
0x10 => Some(SHIFT),
|
(0x5C, _) => Some(RIGHT_META),
|
||||||
0x12 => Some(ALT),
|
(0x10, 1) => Some(LEFT_SHIFT),
|
||||||
0x11 => Some(CTRL),
|
(0x10, 2) => Some(RIGHT_SHIFT),
|
||||||
0x08 => Some(BACKSPACE),
|
(0x12, 1) => Some(LEFT_ALT),
|
||||||
|
(0x12, 2) => Some(RIGHT_ALT),
|
||||||
|
(0x11, 1) => Some(LEFT_CTRL),
|
||||||
|
(0x11, 2) => Some(RIGHT_CTRL),
|
||||||
|
(0x08, _) => Some(BACKSPACE),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(modifier) = modifier {
|
if let Some(modifier) = modifier {
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
let event = Event::Key(KeyEvent::Modifier(modifier));
|
||||||
(*_self).send_channel.send(event).unwrap();
|
(*_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,9 @@ use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use regex::{Regex, Captures};
|
use regex::{Regex, Captures};
|
||||||
use std::time::SystemTime;
|
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>,
|
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
||||||
U: UIManager, R: Renderer> {
|
U: UIManager, R: Renderer> {
|
||||||
|
@ -41,29 +44,28 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<
|
||||||
config_manager: &'a M,
|
config_manager: &'a M,
|
||||||
ui_manager: &'a U,
|
ui_manager: &'a U,
|
||||||
renderer: &'a R,
|
renderer: &'a R,
|
||||||
|
is_injecting: Arc<AtomicBool>,
|
||||||
|
|
||||||
enabled: RefCell<bool>,
|
enabled: RefCell<bool>,
|
||||||
last_action_time: RefCell<SystemTime>, // Used to block espanso from re-interpreting it's own inputs
|
last_action_time: RefCell<SystemTime>, // 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>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||||
Engine<'a, S, C, M, U, R> {
|
Engine<'a, S, C, M, U, R> {
|
||||||
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
|
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
|
||||||
config_manager: &'a M, ui_manager: &'a U,
|
config_manager: &'a M, ui_manager: &'a U,
|
||||||
renderer: &'a R) -> Engine<'a, S, C, M, U, R> {
|
renderer: &'a R, is_injecting: Arc<AtomicBool>) -> Engine<'a, S, C, M, U, R> {
|
||||||
let enabled = RefCell::new(true);
|
let enabled = RefCell::new(true);
|
||||||
let last_action_time = RefCell::new(SystemTime::now());
|
let last_action_time = RefCell::new(SystemTime::now());
|
||||||
let action_noop_interval = config_manager.default_config().action_noop_interval;
|
|
||||||
|
|
||||||
Engine{keyboard_manager,
|
Engine{keyboard_manager,
|
||||||
clipboard_manager,
|
clipboard_manager,
|
||||||
config_manager,
|
config_manager,
|
||||||
ui_manager,
|
ui_manager,
|
||||||
renderer,
|
renderer,
|
||||||
|
is_injecting,
|
||||||
enabled,
|
enabled,
|
||||||
last_action_time,
|
last_action_time,
|
||||||
action_noop_interval,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +141,8 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid espanso reinterpreting its own actions
|
// Block espanso from reinterpreting its own actions
|
||||||
if self.check_last_action_and_set(self.action_noop_interval) {
|
self.is_injecting.store(true, Release);
|
||||||
debug!("Last action was too near, nooping the action.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let char_count = if trailing_separator.is_none() {
|
let char_count = if trailing_separator.is_none() {
|
||||||
m.triggers[trigger_offset].chars().count() as i32
|
m.triggers[trigger_offset].chars().count() as i32
|
||||||
|
@ -190,24 +189,34 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
match config.backend {
|
let backend = if config.backend == BackendType::Auto {
|
||||||
BackendType::Inject => {
|
if cfg!(target_os = "linux") {
|
||||||
// Send the expected string. On linux, newlines are managed automatically
|
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
||||||
// while on windows and macos, we need to emulate a Enter key press.
|
if all_ascii {
|
||||||
|
debug!("All elements of the replacement are ascii, using Inject backend");
|
||||||
if cfg!(target_os = "linux") {
|
&BackendType::Inject
|
||||||
self.keyboard_manager.send_string(&target_string);
|
|
||||||
}else{
|
}else{
|
||||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
debug!("There are non-ascii characters, using Clipboard backend");
|
||||||
let splits = target_string.split('\n');
|
&BackendType::Clipboard
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
&BackendType::Inject
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
&config.backend
|
||||||
|
};
|
||||||
|
|
||||||
for (i, split) in splits.enumerate() {
|
match backend {
|
||||||
if i > 0 {
|
BackendType::Inject => {
|
||||||
self.keyboard_manager.send_enter();
|
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||||
}
|
let splits = target_string.split('\n');
|
||||||
|
|
||||||
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 => {
|
BackendType::Clipboard => {
|
||||||
|
@ -218,6 +227,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
self.clipboard_manager.set_clipboard(&target_string);
|
self.clipboard_manager.set_clipboard(&target_string);
|
||||||
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||||
},
|
},
|
||||||
|
_ => {
|
||||||
|
error!("Unsupported backend type evaluation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(moves) = cursor_rewind {
|
if let Some(moves) = cursor_rewind {
|
||||||
|
@ -246,14 +259,12 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
|
|
||||||
self.clipboard_manager.set_clipboard(&previous_clipboard_content);
|
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) {
|
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 {
|
let message = if status {
|
||||||
"espanso enabled"
|
"espanso enabled"
|
||||||
}else{
|
}else{
|
||||||
|
@ -269,11 +280,6 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_passive(&self) {
|
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();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if !config.enable_passive {
|
if !config.enable_passive {
|
||||||
|
|
129
src/event/mod.rs
129
src/event/mod.rs
|
@ -53,9 +53,11 @@ impl From<i32> for ActionType {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum KeyEvent {
|
pub enum KeyEvent {
|
||||||
Char(String),
|
Char(String),
|
||||||
Modifier(KeyModifier)
|
Modifier(KeyModifier),
|
||||||
|
Other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[warn(non_camel_case_types)]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum KeyModifier {
|
pub enum KeyModifier {
|
||||||
CTRL,
|
CTRL,
|
||||||
|
@ -64,6 +66,70 @@ pub enum KeyModifier {
|
||||||
META,
|
META,
|
||||||
BACKSPACE,
|
BACKSPACE,
|
||||||
OFF,
|
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
|
// Receivers
|
||||||
|
@ -75,3 +141,64 @@ pub trait KeyEventReceiver {
|
||||||
pub trait ActionEventReceiver {
|
pub trait ActionEventReceiver {
|
||||||
fn on_action_event(&self, e: ActionType);
|
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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,9 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_enter(&self) {
|
fn send_enter(&self) {
|
||||||
// On linux this is not needed, so NOOP
|
unsafe {
|
||||||
|
send_enter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trigger_paste(&self, shortcut: &PasteShortcut) {
|
fn trigger_paste(&self, shortcut: &PasteShortcut) {
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -23,7 +23,7 @@ extern crate lazy_static;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::mpsc;
|
use std::sync::{mpsc, Arc};
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ use crate::protocol::*;
|
||||||
use std::io::{BufReader, BufRead};
|
use std::io::{BufReader, BufRead};
|
||||||
use crate::package::default::DefaultPackageManager;
|
use crate::package::default::DefaultPackageManager;
|
||||||
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
@ -319,11 +320,15 @@ fn daemon_main(config_set: ConfigSet) {
|
||||||
|
|
||||||
let (send_channel, receive_channel) = mpsc::channel();
|
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();
|
let config_set_copy = config_set.clone();
|
||||||
thread::Builder::new().name("daemon_background".to_string()).spawn(move || {
|
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");
|
}).expect("Unable to spawn daemon background thread");
|
||||||
|
|
||||||
let ipc_server = protocol::get_ipc_server(config_set, send_channel.clone());
|
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
|
/// Background thread worker for the daemon
|
||||||
fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
|
fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet, is_injecting: Arc<AtomicBool>) {
|
||||||
let system_manager = system::get_manager();
|
let system_manager = system::get_manager();
|
||||||
let config_manager = RuntimeConfigManager::new(config_set, system_manager);
|
let config_manager = RuntimeConfigManager::new(config_set, system_manager);
|
||||||
|
|
||||||
|
@ -354,6 +359,7 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
|
||||||
&config_manager,
|
&config_manager,
|
||||||
&ui_manager,
|
&ui_manager,
|
||||||
&renderer,
|
&renderer,
|
||||||
|
is_injecting,
|
||||||
);
|
);
|
||||||
|
|
||||||
let matcher = ScrollingMatcher::new(&config_manager, &engine);
|
let matcher = ScrollingMatcher::new(&config_manager, &engine);
|
||||||
|
|
|
@ -239,6 +239,7 @@ pub trait MatchReceiver {
|
||||||
pub trait Matcher : KeyEventReceiver {
|
pub trait Matcher : KeyEventReceiver {
|
||||||
fn handle_char(&self, c: &str);
|
fn handle_char(&self, c: &str);
|
||||||
fn handle_modifier(&self, m: KeyModifier);
|
fn handle_modifier(&self, m: KeyModifier);
|
||||||
|
fn handle_other(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <M: Matcher> KeyEventReceiver for M {
|
impl <M: Matcher> KeyEventReceiver for M {
|
||||||
|
@ -250,6 +251,9 @@ impl <M: Matcher> KeyEventReceiver for M {
|
||||||
KeyEvent::Modifier(m) => {
|
KeyEvent::Modifier(m) => {
|
||||||
self.handle_modifier(m);
|
self.handle_modifier(m);
|
||||||
},
|
},
|
||||||
|
KeyEvent::Other => {
|
||||||
|
self.handle_other();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// TODO: at the moment, activating the passive key triggers the toggle key
|
||||||
// study a mechanism to avoid this problem
|
// 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,
|
check_interval(&self.toggle_press_time,
|
||||||
u128::from(config.toggle_interval), || {
|
u128::from(config.toggle_interval), || {
|
||||||
self.toggle();
|
self.toggle();
|
||||||
|
@ -223,7 +223,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
self.current_set_queue.borrow_mut().clear();
|
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,
|
check_interval(&self.passive_press_time,
|
||||||
u128::from(config.toggle_interval), || {
|
u128::from(config.toggle_interval), || {
|
||||||
self.receiver.on_passive();
|
self.receiver.on_passive();
|
||||||
|
@ -235,6 +235,17 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
||||||
current_set_queue.pop_back();
|
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) {
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user