diff --git a/Cargo.lock b/Cargo.lock index 47fbd70..c50176d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/Cargo.toml b/Cargo.toml index 95a12ba..8df4a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.5.4" +version = "0.5.5" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8e57bc6 --- /dev/null +++ b/SECURITY.md @@ -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 \ No newline at end of file diff --git a/native/liblinuxbridge/CMakeLists.txt b/native/liblinuxbridge/CMakeLists.txt index 4f44763..243fda2 100644 --- a/native/liblinuxbridge/CMakeLists.txt +++ b/native/liblinuxbridge/CMakeLists.txt @@ -4,6 +4,6 @@ project(liblinuxbridge) set (CMAKE_CXX_STANDARD 14) set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include") -add_library(linuxbridge STATIC bridge.cpp bridge.h) +add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h) install(TARGETS linuxbridge DESTINATION .) \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 3e8fc39..71706d9 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -18,6 +18,7 @@ */ #include "bridge.h" +#include "fast_xdo.h" #include #include @@ -302,18 +303,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; ixdpy); +} + +void fast_send_enter() { + _fast_send_keycode_to_focused_window(XK_Return, 1); +} + void delete_string(int32_t count) { for (int i = 0; i +#include +#include + +#include "fast_xdo.h" + +extern "C" { // Needed to avoid C++ compiler name mangling +#include +} + +void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) { + xk->display = xdo->xdpy; + xk->subwindow = None; + xk->time = CurrentTime; + xk->same_screen = True; + + /* Should we set these at all? */ + xk->x = xk->y = xk->x_root = xk->y_root = 1; +} + +void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay) { + /* Properly ensure the modstate is set by finding a key + * that activates each bit in the modifier state */ + int mask = modstate | key->modmask; + + /* 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); +} diff --git a/native/liblinuxbridge/fast_xdo.h b/native/liblinuxbridge/fast_xdo.h new file mode 100644 index 0000000..a7de8a4 --- /dev/null +++ b/native/liblinuxbridge/fast_xdo.h @@ -0,0 +1,21 @@ +// +// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT +// and modified to use XSendEvent instead of XTestFakeKeyEvent. + +#ifndef LIBLINUXBRIDGE_FAST_XDO_H +#define LIBLINUXBRIDGE_FAST_XDO_H + +extern "C" { // Needed to avoid C++ compiler name mangling +#include +} + +KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key); +void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key); +void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym); +void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk); +void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay); +int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay); +void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed); + +#endif //LIBLINUXBRIDGE_FAST_XDO_H diff --git a/native/libmacbridge/AppDelegate.mm b/native/libmacbridge/AppDelegate.mm index 29d1213..cd24e8b 100644 --- a/native/libmacbridge/AppDelegate.mm +++ b/native/libmacbridge/AppDelegate.mm @@ -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) diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index 03bb6bf..4a0f1d8 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -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. diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 1c59bd6..3e2d7e5 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -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; diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index dfb6d42..7011d4b 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -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. diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index 304f10b..bd07a78 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -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 diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index 94bdfa2..63ee91c 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -47,4 +47,9 @@ extern { pub fn trigger_alt_shift_ins_paste(); pub fn trigger_ctrl_alt_paste(); pub fn trigger_copy(); + + pub fn fast_send_string(string: *const c_char); + pub fn fast_delete_string(count: i32); + pub fn fast_left_arrow(count: i32); + pub fn fast_send_enter(); } \ No newline at end of file diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index 989786b..1cfb204 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -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(); diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index f337638..0513fd4 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -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; diff --git a/src/config/mod.rs b/src/config/mod.rs index d4d6589..c308ce6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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 { Vec::new() } fn default_global_vars() -> Vec { 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, @@ -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(); diff --git a/src/context/macos.rs b/src/context/macos.rs index 7b6ec44..7a71d25 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -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 diff --git a/src/context/windows.rs b/src/context/windows.rs index 0640d65..77c9be4 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -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") } diff --git a/src/engine.rs b/src/engine.rs index 0527533..fe0c694 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -129,7 +129,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator }; - self.keyboard_manager.delete_string(char_count); + self.keyboard_manager.delete_string(&config, char_count); let mut previous_clipboard_content : Option = None; @@ -194,10 +194,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa for (i, split) in splits.enumerate() { if i > 0 { - self.keyboard_manager.send_enter(); + self.keyboard_manager.send_enter(&config); } - self.keyboard_manager.send_string(split); + self.keyboard_manager.send_string(&config, split); } }, BackendType::Clipboard => { @@ -206,7 +206,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); self.clipboard_manager.set_clipboard(&target_string); - self.keyboard_manager.trigger_paste(&config.paste_shortcut); + self.keyboard_manager.trigger_paste(&config); }, _ => { error!("Unsupported backend type evaluation."); @@ -216,7 +216,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa if let Some(moves) = cursor_rewind { // Simulate left arrow key presses to bring the cursor into the desired position - self.keyboard_manager.move_cursor_left(moves); + self.keyboard_manager.move_cursor_left(&config, moves); } }, RenderResult::Image(image_path) => { @@ -225,7 +225,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); self.clipboard_manager.set_clipboard_image(&image_path); - self.keyboard_manager.trigger_paste(&config.paste_shortcut); + self.keyboard_manager.trigger_paste(&config); }, RenderResult::Error => { error!("Could not render match: {}", m.triggers[trigger_offset]); @@ -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); } }, diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 03d55ba..4a9fd2a 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -21,28 +21,39 @@ use std::ffi::CString; use crate::bridge::linux::*; use super::PasteShortcut; use log::error; +use crate::config::Configs; pub struct LinuxKeyboardManager { } impl super::KeyboardManager for LinuxKeyboardManager { - fn send_string(&self, s: &str) { + fn send_string(&self, active_config: &Configs, s: &str) { let res = CString::new(s); match res { - Ok(cstr) => unsafe { send_string(cstr.as_ptr()); } + Ok(cstr) => unsafe { + if active_config.fast_inject { + fast_send_string(cstr.as_ptr()); + }else{ + send_string(cstr.as_ptr()); + } + } Err(e) => panic!(e.to_string()) } } - fn send_enter(&self) { + fn send_enter(&self, active_config: &Configs) { unsafe { - send_enter(); + if active_config.fast_inject { + fast_send_enter(); + }else{ + send_enter(); + } } } - fn trigger_paste(&self, shortcut: &PasteShortcut) { + fn trigger_paste(&self, active_config: &Configs) { unsafe { - match shortcut { + match active_config.paste_shortcut { PasteShortcut::Default => { let is_special = is_current_window_special(); @@ -79,17 +90,27 @@ impl super::KeyboardManager for LinuxKeyboardManager { } } - fn delete_string(&self, count: i32) { - unsafe {delete_string(count)} - } - - fn move_cursor_left(&self, count: i32) { + fn delete_string(&self, active_config: &Configs, count: i32) { unsafe { - left_arrow(count); + if active_config.fast_inject { + fast_delete_string(count); + }else{ + delete_string(count) + } } } - fn trigger_copy(&self) { + fn move_cursor_left(&self, active_config: &Configs, count: i32) { + unsafe { + if active_config.fast_inject { + fast_left_arrow(count); + }else{ + left_arrow(count); + } + } + } + + fn trigger_copy(&self, _: &Configs) { unsafe { trigger_copy(); } diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index ccc4faf..03442fb 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -21,12 +21,13 @@ use std::ffi::CString; use crate::bridge::macos::*; use super::PasteShortcut; use log::error; +use crate::config::Configs; pub struct MacKeyboardManager { } impl super::KeyboardManager for MacKeyboardManager { - fn send_string(&self, s: &str) { + fn send_string(&self, _: &Configs, s: &str) { let res = CString::new(s); match res { Ok(cstr) => unsafe { send_string(cstr.as_ptr()); } @@ -34,16 +35,16 @@ impl super::KeyboardManager for MacKeyboardManager { } } - fn send_enter(&self) { + fn send_enter(&self, _: &Configs) { unsafe { // Send the kVK_Return key press send_vkey(0x24); } } - fn trigger_paste(&self, shortcut: &PasteShortcut) { + fn trigger_paste(&self, active_config: &Configs) { unsafe { - match shortcut { + match active_config.paste_shortcut { PasteShortcut::Default => { unsafe { trigger_paste(); @@ -56,17 +57,17 @@ impl super::KeyboardManager for MacKeyboardManager { } } - fn trigger_copy(&self) { + fn trigger_copy(&self, _: &Configs) { unsafe { trigger_copy(); } } - fn delete_string(&self, count: i32) { + fn delete_string(&self, _: &Configs, count: i32) { unsafe {delete_string(count)} } - fn move_cursor_left(&self, count: i32) { + fn move_cursor_left(&self, _: &Configs, count: i32) { unsafe { // Simulate the Left arrow count times send_multi_vkey(0x7B, count); diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 718b42f..304b201 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -18,6 +18,7 @@ */ use serde::{Serialize, Deserialize}; +use crate::config::Configs; #[cfg(target_os = "windows")] mod windows; @@ -29,12 +30,12 @@ mod linux; mod macos; pub trait KeyboardManager { - fn send_string(&self, s: &str); - fn send_enter(&self); - fn trigger_paste(&self, shortcut: &PasteShortcut); - fn delete_string(&self, count: i32); - fn move_cursor_left(&self, count: i32); - fn trigger_copy(&self); + fn send_string(&self, active_config: &Configs, s: &str); + fn send_enter(&self, active_config: &Configs); + fn trigger_paste(&self, active_config: &Configs); + fn delete_string(&self, active_config: &Configs, count: i32); + fn move_cursor_left(&self, active_config: &Configs, count: i32); + fn trigger_copy(&self, active_config: &Configs); } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs index c7ee10a..f8a9a61 100644 --- a/src/keyboard/windows.rs +++ b/src/keyboard/windows.rs @@ -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(); } diff --git a/src/main.rs b/src/main.rs index dc788fd..739976e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -350,7 +350,9 @@ fn daemon_background(receive_channel: Receiver, 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();