Merge pull request #241 from federico-terzi/dev

Version 0.5.5
This commit is contained in:
Federico Terzi 2020-04-18 21:19:29 +02:00 committed by GitHub
commit 2f5c48f2e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 569 additions and 66 deletions

2
Cargo.lock generated
View File

@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "espanso"
version = "0.5.4"
version = "0.5.5"
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)",

View File

@ -1,6 +1,6 @@
[package]
name = "espanso"
version = "0.5.4"
version = "0.5.5"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust"

46
SECURITY.md Normal file
View File

@ -0,0 +1,46 @@
# Security
Espanso has always been designed with a strong focus on security.
In the following section, there is an overview of the critical security
components.
If you have any doubt, don't hesitate to contact me.
## Architecture
In its most basic form, a text expander is composed of two parts:
* A **global key detector** that intercepts the keys pressed by the user,
in order to determine if a trigger was typed.
* A **key injection mechanism** that injects the
final text into the current application, a process known as *expansion*.
At this point, some of you may think that espanso is acting as a keylogger,
due to the *global key detector* we mentioned before. The good news is, **it's not!**
While espanso detects key presses as a keylogger would do,
**it doesn't log anything**. Moreover, to further reduce risks, espanso only
stores in memory the last 3 chars by default (you can change this amount by
setting the `backspace_limit` parameter in the config) and this is needed
to allow the user to correct wrongly typed triggers by pressing backspace,
up to 3 characters.
The matching part is implemented with an efficient [data structure](https://github.com/federico-terzi/espanso/blob/master/src/matcher/scrolling.rs)
that keeps track of the compatible matches in a "rolling" basis. So that in the worst case scenario,
the longest sequence of chars kept in memory would be equal to the longest trigger.
And of course, if you don't trust me you can examine all the code! That's
the wonderful thing about open source :)
### Implementation
The *global key detector* is implemented on top of various OS-dependent APIs, in particular:
* On Windows, it uses the [RawInput API](https://docs.microsoft.com/en-us/windows/win32/inputdev/raw-input).
* On macOS, it uses [addGlobalMonitorForEvents](https://developer.apple.com/documentation/appkit/nsevent/1535472-addglobalmonitorforevents).
* On Linux, it uses the [X Record Extension](https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html).
## Reporting Security Issues
To report a security issue, please email me at federicoterzi96[at]gmail.com

View File

@ -4,6 +4,6 @@ project(liblinuxbridge)
set (CMAKE_CXX_STANDARD 14)
set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include")
add_library(linuxbridge STATIC bridge.cpp bridge.h)
add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h)
install(TARGETS linuxbridge DESTINATION .)

View File

@ -18,6 +18,7 @@
*/
#include "bridge.h"
#include "fast_xdo.h"
#include <locale.h>
#include <stdio.h>
@ -302,18 +303,82 @@ void send_enter() {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000);
}
void fast_release_all_keys() {
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
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;
fast_send_event(xdo_context, focused, key_code, 0);
}
}
}
}
XFlush(xdo_context->xdpy);
}
void fast_send_string(const char * string) {
// It may happen that when an expansion is triggered, some keys are still pressed.
// This causes a problem if the expanded match contains that character, as the injection
// will not be able to register that keypress (as it is already pressed).
// To solve the problem, before an expansion we get which keys are currently pressed
// and inject a key_release event so that they can be further registered.
fast_release_all_keys();
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
fast_enter_text_window(xdo_context, focused, string, 1);
}
void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count) {
int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode);
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
for (int i = 0; i<count; i++) {
fast_send_event(xdo_context, focused, keycode, 1);
fast_send_event(xdo_context, focused, keycode, 0);
}
XFlush(xdo_context->xdpy);
}
void fast_send_enter() {
_fast_send_keycode_to_focused_window(XK_Return, 1);
}
void delete_string(int32_t count) {
for (int i = 0; i<count; i++) {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 1000);
}
}
void fast_delete_string(int32_t count) {
_fast_send_keycode_to_focused_window(XK_BackSpace, count);
}
void left_arrow(int32_t count) {
for (int i = 0; i<count; i++) {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 1000);
}
}
void fast_left_arrow(int32_t count) {
_fast_send_keycode_to_focused_window(XK_Left, count);
}
void trigger_paste() {
// Before sending the paste shortcut, trigger the press and release of the Shift key
// this is needed because for some triggers, for example ending with ":", the user

View File

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

View File

@ -0,0 +1,246 @@
//
// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
// and modified to use XSendEvent instead of XTestFakeKeyEvent.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include "fast_xdo.h"
extern "C" { // Needed to avoid C++ compiler name mangling
#include <xdo.h>
}
void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) {
xk->display = xdo->xdpy;
xk->subwindow = None;
xk->time = CurrentTime;
xk->same_screen = True;
/* Should we set these at all? */
xk->x = xk->y = xk->x_root = xk->y_root = 1;
}
void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
int modstate, int is_press, useconds_t delay) {
/* Properly ensure the modstate is set by finding a key
* that activates each bit in the modifier state */
int mask = modstate | key->modmask;
/* Since key events have 'state' (shift, etc) in the event, we don't
* need to worry about key press ordering. */
XKeyEvent xk;
fast_init_xkeyevent(xdo, &xk);
xk.window = window;
xk.keycode = key->code;
xk.state = mask | (key->group << 13);
xk.type = (is_press ? KeyPress : KeyRelease);
XSendEvent(xdo->xdpy, xk.window, True, 0, (XEvent *)&xk);
/* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */
XFlush(xdo->xdpy);
if (delay > 0) {
usleep(delay);
}
}
int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys,
int nkeys, int pressed, int *modifier, useconds_t delay) {
int i = 0;
int modstate = 0;
int keymapchanged = 0;
/* Find an unused keycode in case we need to bind unmapped keysyms */
KeySym *keysyms = NULL;
int keysyms_per_keycode = 0;
int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */
keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
xdo->keycode_high - xdo->keycode_low,
&keysyms_per_keycode);
/* Find a keycode that is unused for scratchspace */
for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) {
int j = 0;
int key_is_empty = 1;
for (j = 0; j < keysyms_per_keycode; j++) {
/*char *symname;*/
int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j;
/*symname = XKeysymToString(keysyms[symindex]);*/
if (keysyms[symindex] != 0) {
key_is_empty = 0;
} else {
break;
}
}
if (key_is_empty) {
scratch_keycode = i;
break;
}
}
XFree(keysyms);
/* Allow passing NULL for modifier in case we don't care about knowing
* the modifier map state after we finish */
if (modifier == NULL)
modifier = &modstate;
for (i = 0; i < nkeys; i++) {
if (keys[i].needs_binding == 1) {
KeySym keysym_list[] = { keys[i].symbol };
//_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode);
XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
XSync(xdo->xdpy, False);
/* override the code in our current key to use the scratch_keycode */
keys[i].code = scratch_keycode;
keymapchanged = 1;
}
//fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n",
//keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier);
fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay);
if (keys[i].needs_binding == 1) {
/* If we needed to make a new keymapping for this keystroke, we
* should sync with the server now, after the keypress, so that
* the next mapping or removal doesn't conflict. */
XSync(xdo->xdpy, False);
}
if (pressed) {
*modifier |= keys[i].modmask;
} else {
*modifier &= ~(keys[i].modmask);
}
}
if (keymapchanged) {
KeySym keysym_list[] = { 0 };
//printf(xdo, "Reverting scratch keycode (sym %lu to %d)",
// keys[i].symbol, scratch_keycode);
XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
}
/* Necessary? */
XFlush(xdo->xdpy);
return XDO_SUCCESS;
}
KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) {
int i = 0;
int len = xdo->charcodes_len;
//printf("Finding symbol for key '%c'\n", key);
for (i = 0; i < len; i++) {
//printf(" => %c vs %c (%d)\n",
//key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key));
if (xdo->charcodes[i].key == key) {
//printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol);
return xdo->charcodes[i].symbol;
}
}
if (key >= 0x100) key += 0x01000000;
if (XKeysymToString(key)) return key;
return NoSymbol;
}
void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) {
int i = 0;
int len = xdo->charcodes_len;
key->code = 0;
key->symbol = keysym;
key->group = 0;
key->modmask = 0;
key->needs_binding = 1;
for (i = 0; i < len; i++) {
if (xdo->charcodes[i].symbol == keysym) {
key->code = xdo->charcodes[i].code;
key->group = xdo->charcodes[i].group;
key->modmask = xdo->charcodes[i].modmask;
key->needs_binding = 0;
return;
}
}
}
void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) {
KeySym keysym = fast_keysym_from_char(xdo, key->key);
fast_charcodemap_from_keysym(xdo, key, keysym);
}
/* XXX: Return proper code if errors found */
int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) {
/* Since we're doing down/up, the delay should be based on the number
* of keys pressed (including shift). Since up/down is two calls,
* divide by two. */
delay /= 2;
/* XXX: Add error handling */
//int nkeys = strlen(string);
//charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t));
charcodemap_t key;
//int modifier = 0;
setlocale(LC_CTYPE,"");
mbstate_t ps = { 0 };
ssize_t len;
while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) {
if (len == -1) {
fprintf(stderr, "Invalid multi-byte sequence encountered\n");
return XDO_ERROR;
}
fast_charcodemap_from_char(xdo, &key);
if (key.code == 0 && key.symbol == NoSymbol) {
fprintf(stderr, "I don't what key produces '%lc', skipping.\n",
key.key);
continue;
} else {
//printf("Found key for %c\n", key.key);
//printf("code: %d\n", key.code);
//printf("sym: %s\n", XKeysymToString(key.symbol));
}
//printf(stderr,
//"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n",
//key.key, key.code, key.symbol, key.group, key.modmask,
//(key.needs_binding == 1) ? "needs binding" : "ok");
//_xdo_send_key(xdo, window, keycode, modstate, True, delay);
//_xdo_send_key(xdo, window, keycode, modstate, False, delay);
fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2);
key.needs_binding = 0;
fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2);
/* XXX: Flush here or at the end? or never? */
//XFlush(xdo->xdpy);
} /* walk string generating a keysequence */
//free(keys);
return XDO_SUCCESS;
}
void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) {
XKeyEvent xk;
xk.display = xdo->xdpy;
xk.window = window;
xk.root = XDefaultRootWindow(xdo->xdpy);
xk.subwindow = None;
xk.time = CurrentTime;
xk.x = 1;
xk.y = 1;
xk.x_root = 1;
xk.y_root = 1;
xk.same_screen = True;
xk.keycode = keycode;
xk.state = 0;
xk.type = (pressed ? KeyPress : KeyRelease);
XEvent event;
event.xkey =xk;
XSendEvent(xdo->xdpy, window, True, 0, &event);
}

View File

@ -0,0 +1,21 @@
//
// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
// and modified to use XSendEvent instead of XTestFakeKeyEvent.
#ifndef LIBLINUXBRIDGE_FAST_XDO_H
#define LIBLINUXBRIDGE_FAST_XDO_H
extern "C" { // Needed to avoid C++ compiler name mangling
#include <xdo.h>
}
KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key);
void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key);
void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym);
void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk);
void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
int modstate, int is_press, useconds_t delay);
int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed);
#endif //LIBLINUXBRIDGE_FAST_XDO_H

View File

@ -24,16 +24,18 @@
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Setup status icon
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
if (show_icon) {
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
NSString *nsIconPath = [NSString stringWithUTF8String:icon_path];
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
[statusImage setTemplate:YES];
NSString *nsIconPath = [NSString stringWithUTF8String:icon_path];
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
[statusImage setTemplate:YES];
[myStatusItem.button setImage:statusImage];
[myStatusItem setHighlightMode:YES];
[myStatusItem.button setAction:@selector(statusIconClick:)];
[myStatusItem.button setTarget:self];
[myStatusItem.button setImage:statusImage];
[myStatusItem setHighlightMode:YES];
[myStatusItem.button setAction:@selector(statusIconClick:)];
[myStatusItem.button setTarget:self];
}
// Setup key listener
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown)

View File

@ -26,11 +26,12 @@ extern "C" {
extern void * context_instance;
extern char * icon_path;
extern int32_t show_icon;
/*
* Initialize the AppDelegate and check for accessibility permissions
*/
int32_t initialize(void * context, const char * icon_path);
int32_t initialize(void * context, const char * icon_path, int32_t show_icon);
/*
* Start the event loop indefinitely. Blocking call.

View File

@ -33,15 +33,17 @@ extern "C" {
void * context_instance;
char * icon_path;
int32_t show_icon;
AppDelegate * delegate_ptr;
KeypressCallback keypress_callback;
IconClickCallback icon_click_callback;
ContextMenuClickCallback context_menu_click_callback;
int32_t initialize(void * context, const char * _icon_path) {
int32_t initialize(void * context, const char * _icon_path, int32_t _show_icon) {
context_instance = context;
icon_path = strdup(_icon_path);
show_icon = _show_icon;
AppDelegate *delegate = [[AppDelegate alloc] init];
delegate_ptr = delegate;

View File

@ -39,6 +39,7 @@
const long refreshKeyboardLayoutInterval = 2000;
void * manager_instance;
int32_t show_icon;
// Keyboard listening
@ -298,14 +299,17 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
}
default:
if (msg == WM_TASKBARCREATED) { // Explorer crashed, recreate the icon
Shell_NotifyIcon(NIM_ADD, &nid);
if (show_icon) {
Shell_NotifyIcon(NIM_ADD, &nid);
}
}
return DefWindowProc(window, msg, wp, lp);
}
}
int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) {
int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path, int32_t _show_icon) {
manager_instance = self;
show_icon = _show_icon;
// Load the images
g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
@ -440,7 +444,9 @@ int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) {
StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"espanso");
// Show the notification.
Shell_NotifyIcon(NIM_ADD, &nid);
if (show_icon) {
Shell_NotifyIcon(NIM_ADD, &nid);
}
}
}else{
// Something went wrong, error.

View File

@ -33,7 +33,7 @@ extern void * manager_instance;
* Initialize the Windows parameters
* return: 1 if OK, -1 otherwise.
*/
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, int32_t show_icon);
#define LEFT_VARIANT 1
#define RIGHT_VARIANT 2

View File

@ -47,4 +47,9 @@ extern {
pub fn trigger_alt_shift_ins_paste();
pub fn trigger_ctrl_alt_paste();
pub fn trigger_copy();
pub fn fast_send_string(string: *const c_char);
pub fn fast_delete_string(count: i32);
pub fn fast_left_arrow(count: i32);
pub fn fast_send_enter();
}

View File

@ -29,7 +29,7 @@ pub struct MacMenuItem {
#[allow(improper_ctypes)]
#[link(name="macbridge", kind="static")]
extern {
pub fn initialize(s: *const c_void, icon_path: *const c_char);
pub fn initialize(s: *const c_void, icon_path: *const c_char, show_icon: i32);
pub fn eventloop();
pub fn headless_eventloop();

View File

@ -30,7 +30,7 @@ pub struct WindowsMenuItem {
#[link(name="winbridge", kind="static")]
extern {
pub fn start_daemon_process() -> i32;
pub fn initialize(s: *const c_void, ico_path: *const u16, bmp_path: *const u16) -> i32;
pub fn initialize(s: *const c_void, ico_path: *const u16, bmp_path: *const u16, show_icon: i32) -> i32;
// SYSTEM
pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32;

View File

@ -66,6 +66,9 @@ fn default_restore_clipboard_delay() -> i32 { 300 }
fn default_exclude_default_entries() -> bool {false}
fn default_secure_input_watcher_enabled() -> bool {true}
fn default_secure_input_notification() -> bool {true}
fn default_show_notifications() -> bool {true}
fn default_show_icon() -> bool {true}
fn default_fast_inject() -> bool {false}
fn default_secure_input_watcher_interval() -> i32 {5000}
fn default_matches() -> Vec<Match> { Vec::new() }
fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
@ -156,6 +159,15 @@ pub struct Configs {
#[serde(default = "default_exclude_default_entries")]
pub exclude_default_entries: bool,
#[serde(default = "default_show_notifications")]
pub show_notifications: bool,
#[serde(default = "default_show_icon")]
pub show_icon: bool,
#[serde(default = "default_fast_inject")]
pub fast_inject: bool,
#[serde(default = "default_matches")]
pub matches: Vec<Match>,
@ -205,6 +217,8 @@ impl Configs {
validate_field!(result, self.secure_input_watcher_enabled, default_secure_input_watcher_enabled());
validate_field!(result, self.secure_input_watcher_interval, default_secure_input_watcher_interval());
validate_field!(result, self.secure_input_notification, default_secure_input_notification());
validate_field!(result, self.show_notifications, default_show_notifications());
validate_field!(result, self.show_icon, default_show_icon());
result
}
@ -243,7 +257,7 @@ impl Default for BackendType {
#[cfg(target_os = "linux")]
fn default() -> Self {
BackendType::Clipboard
BackendType::Auto
}
}
@ -267,6 +281,7 @@ impl Configs {
}
}
}else{
eprintln!("Error: Cannot load file {:?}", path);
Err(ConfigLoadError::FileNotFound)
}
}
@ -377,6 +392,11 @@ impl ConfigSet {
continue;
}
// Skip hidden files
if path.file_name().unwrap_or_default().to_str().unwrap_or_default().starts_with(".") {
continue;
}
let mut config = Configs::load_config(&path)?;
// Make sure the config does not contain reserved fields
@ -952,6 +972,32 @@ mod tests {
assert_eq!(config_set.specific.len(), 0);
}
#[test]
fn test_hidden_files_are_ignored() {
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
r###"
matches:
- trigger: ":lol"
replace: "LOL"
- trigger: ":yess"
replace: "Bob"
"###
);
create_user_config_file(data_dir.path(), ".specific.yml", r###"
name: specific1
exclude_default_entries: true
matches:
- trigger: "hello"
replace: "newstring"
"###);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 0);
}
#[test]
fn test_config_set_no_parent_configs_works_correctly() {
let (data_dir, package_dir) = create_temp_espanso_directories();

View File

@ -83,7 +83,13 @@ impl MacContext {
register_context_menu_click_callback(context_menu_click_callback);
let status_icon_path = CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
initialize(context_ptr, status_icon_path.as_ptr());
let show_icon = if config.show_icon {
1
}else{
0
};
initialize(context_ptr, status_icon_path.as_ptr(), show_icon);
}
context

View File

@ -87,8 +87,14 @@ impl WindowsContext {
let ico_file_c = U16CString::from_str(ico_icon).unwrap();
let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
let show_icon = if config.show_icon {
1
}else{
0
};
// Initialize the windows
let res = initialize(context_ptr, ico_file_c.as_ptr(), bmp_file_c.as_ptr());
let res = initialize(context_ptr, ico_file_c.as_ptr(), bmp_file_c.as_ptr(), show_icon);
if res != 1 {
panic!("Can't initialize Windows context")
}

View File

@ -129,7 +129,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
};
self.keyboard_manager.delete_string(char_count);
self.keyboard_manager.delete_string(&config, char_count);
let mut previous_clipboard_content : Option<String> = None;
@ -194,10 +194,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
for (i, split) in splits.enumerate() {
if i > 0 {
self.keyboard_manager.send_enter();
self.keyboard_manager.send_enter(&config);
}
self.keyboard_manager.send_string(split);
self.keyboard_manager.send_string(&config, split);
}
},
BackendType::Clipboard => {
@ -206,7 +206,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
self.clipboard_manager.set_clipboard(&target_string);
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
self.keyboard_manager.trigger_paste(&config);
},
_ => {
error!("Unsupported backend type evaluation.");
@ -216,7 +216,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
if let Some(moves) = cursor_rewind {
// Simulate left arrow key presses to bring the cursor into the desired position
self.keyboard_manager.move_cursor_left(moves);
self.keyboard_manager.move_cursor_left(&config, moves);
}
},
RenderResult::Image(image_path) => {
@ -225,7 +225,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
self.clipboard_manager.set_clipboard_image(&image_path);
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
self.keyboard_manager.trigger_paste(&config);
},
RenderResult::Error => {
error!("Could not render match: {}", m.triggers[trigger_offset]);
@ -257,7 +257,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
let mut enabled_ref = self.enabled.borrow_mut();
*enabled_ref = status;
self.ui_manager.notify(message);
let config = self.config_manager.default_config();
if config.show_notifications {
self.ui_manager.notify(message);
}
}
fn on_passive(&self) {
@ -279,7 +283,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
// Trigger a copy shortcut to transfer the content of the selection to the clipboard
self.keyboard_manager.trigger_copy();
self.keyboard_manager.trigger_copy(&config);
// Sleep for a while, giving time to effectively copy the text
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
@ -308,7 +312,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
self.clipboard_manager.set_clipboard(&payload);
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
self.keyboard_manager.trigger_paste(&config);
},
_ => {
warn!("Cannot expand passive match")
@ -351,7 +355,8 @@ impl <'a, S: KeyboardManager, C: ClipboardManager,
SystemEvent::SecureInputEnabled(app_name, path) => {
info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
if self.config_manager.default_config().secure_input_notification {
let config = self.config_manager.default_config();
if config.secure_input_notification && config.show_notifications {
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
}
},

View File

@ -21,28 +21,39 @@ use std::ffi::CString;
use crate::bridge::linux::*;
use super::PasteShortcut;
use log::error;
use crate::config::Configs;
pub struct LinuxKeyboardManager {
}
impl super::KeyboardManager for LinuxKeyboardManager {
fn send_string(&self, s: &str) {
fn send_string(&self, active_config: &Configs, s: &str) {
let res = CString::new(s);
match res {
Ok(cstr) => unsafe { send_string(cstr.as_ptr()); }
Ok(cstr) => unsafe {
if active_config.fast_inject {
fast_send_string(cstr.as_ptr());
}else{
send_string(cstr.as_ptr());
}
}
Err(e) => panic!(e.to_string())
}
}
fn send_enter(&self) {
fn send_enter(&self, active_config: &Configs) {
unsafe {
send_enter();
if active_config.fast_inject {
fast_send_enter();
}else{
send_enter();
}
}
}
fn trigger_paste(&self, shortcut: &PasteShortcut) {
fn trigger_paste(&self, active_config: &Configs) {
unsafe {
match shortcut {
match active_config.paste_shortcut {
PasteShortcut::Default => {
let is_special = is_current_window_special();
@ -79,17 +90,27 @@ impl super::KeyboardManager for LinuxKeyboardManager {
}
}
fn delete_string(&self, count: i32) {
unsafe {delete_string(count)}
}
fn move_cursor_left(&self, count: i32) {
fn delete_string(&self, active_config: &Configs, count: i32) {
unsafe {
left_arrow(count);
if active_config.fast_inject {
fast_delete_string(count);
}else{
delete_string(count)
}
}
}
fn trigger_copy(&self) {
fn move_cursor_left(&self, active_config: &Configs, count: i32) {
unsafe {
if active_config.fast_inject {
fast_left_arrow(count);
}else{
left_arrow(count);
}
}
}
fn trigger_copy(&self, _: &Configs) {
unsafe {
trigger_copy();
}

View File

@ -21,12 +21,13 @@ use std::ffi::CString;
use crate::bridge::macos::*;
use super::PasteShortcut;
use log::error;
use crate::config::Configs;
pub struct MacKeyboardManager {
}
impl super::KeyboardManager for MacKeyboardManager {
fn send_string(&self, s: &str) {
fn send_string(&self, _: &Configs, s: &str) {
let res = CString::new(s);
match res {
Ok(cstr) => unsafe { send_string(cstr.as_ptr()); }
@ -34,16 +35,16 @@ impl super::KeyboardManager for MacKeyboardManager {
}
}
fn send_enter(&self) {
fn send_enter(&self, _: &Configs) {
unsafe {
// Send the kVK_Return key press
send_vkey(0x24);
}
}
fn trigger_paste(&self, shortcut: &PasteShortcut) {
fn trigger_paste(&self, active_config: &Configs) {
unsafe {
match shortcut {
match active_config.paste_shortcut {
PasteShortcut::Default => {
unsafe {
trigger_paste();
@ -56,17 +57,17 @@ impl super::KeyboardManager for MacKeyboardManager {
}
}
fn trigger_copy(&self) {
fn trigger_copy(&self, _: &Configs) {
unsafe {
trigger_copy();
}
}
fn delete_string(&self, count: i32) {
fn delete_string(&self, _: &Configs, count: i32) {
unsafe {delete_string(count)}
}
fn move_cursor_left(&self, count: i32) {
fn move_cursor_left(&self, _: &Configs, count: i32) {
unsafe {
// Simulate the Left arrow count times
send_multi_vkey(0x7B, count);

View File

@ -18,6 +18,7 @@
*/
use serde::{Serialize, Deserialize};
use crate::config::Configs;
#[cfg(target_os = "windows")]
mod windows;
@ -29,12 +30,12 @@ mod linux;
mod macos;
pub trait KeyboardManager {
fn send_string(&self, s: &str);
fn send_enter(&self);
fn trigger_paste(&self, shortcut: &PasteShortcut);
fn delete_string(&self, count: i32);
fn move_cursor_left(&self, count: i32);
fn trigger_copy(&self);
fn send_string(&self, active_config: &Configs, s: &str);
fn send_enter(&self, active_config: &Configs);
fn trigger_paste(&self, active_config: &Configs);
fn delete_string(&self, active_config: &Configs, count: i32);
fn move_cursor_left(&self, active_config: &Configs, count: i32);
fn trigger_copy(&self, active_config: &Configs);
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@ -21,12 +21,13 @@ use widestring::{U16CString};
use crate::bridge::windows::*;
use super::PasteShortcut;
use log::error;
use crate::config::Configs;
pub struct WindowsKeyboardManager {
}
impl super::KeyboardManager for WindowsKeyboardManager {
fn send_string(&self, s: &str) {
fn send_string(&self, _: &Configs, s: &str) {
let res = U16CString::from_str(s);
match res {
Ok(s) => {
@ -39,16 +40,16 @@ impl super::KeyboardManager for WindowsKeyboardManager {
}
fn send_enter(&self) {
fn send_enter(&self, _: &Configs) {
unsafe {
// Send the VK_RETURN key press
send_vkey(0x0D);
}
}
fn trigger_paste(&self, shortcut: &PasteShortcut) {
fn trigger_paste(&self, active_config: &Configs) {
unsafe {
match shortcut {
match active_config.paste_shortcut {
PasteShortcut::Default => {
unsafe {
trigger_paste();
@ -61,20 +62,20 @@ impl super::KeyboardManager for WindowsKeyboardManager {
}
}
fn delete_string(&self, count: i32) {
fn delete_string(&self, _: &Configs, count: i32) {
unsafe {
delete_string(count)
}
}
fn move_cursor_left(&self, count: i32) {
fn move_cursor_left(&self, _: &Configs, count: i32) {
unsafe {
// Send the left arrow key multiple times
send_multi_vkey(0x25, count)
}
}
fn trigger_copy(&self) {
fn trigger_copy(&self, _: &Configs) {
unsafe {
trigger_copy();
}

View File

@ -350,7 +350,9 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet, is
let config_manager = RuntimeConfigManager::new(config_set, system_manager);
let ui_manager = ui::get_uimanager();
ui_manager.notify("espanso is running!");
if config_manager.default_config().show_notifications {
ui_manager.notify("espanso is running!");
}
let clipboard_manager = clipboard::get_manager();