From 3e88af0139a2327541d02d464de79c604d1eb6d6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 18:05:54 +0200 Subject: [PATCH 01/25] fix(core): rename v2 resources to avoid reusing old assets --- espanso/src/icon.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/espanso/src/icon.rs b/espanso/src/icon.rs index b0d6e03..c5cf54c 100644 --- a/espanso/src/icon.rs +++ b/espanso/src/icon.rs @@ -69,25 +69,25 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result { Ok(IconPaths { form_icon: Some(extract_icon( WINDOWS_LOGO_ICO_BINARY, - &runtime_dir.join("form.ico"), + &runtime_dir.join("formv2.ico"), )?), search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?), wizard_icon: Some(extract_icon( WINDOWS_LOGO_ICO_BINARY, - &runtime_dir.join("wizard.ico"), + &runtime_dir.join("wizardv2.ico"), )?), tray_icon_normal: Some(extract_icon( WINDOWS_NORMAL_DARK_ICO_BINARY, - &runtime_dir.join("normal.ico"), + &runtime_dir.join("normalv2.ico"), )?), tray_icon_disabled: Some(extract_icon( WINDOWS_DISABLED_DARK_ICO_BINARY, - &runtime_dir.join("disabled.ico"), + &runtime_dir.join("disabledv2.ico"), )?), - logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?), + logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("iconv2.png"))?), logo_no_background: Some(extract_icon( LOGO_NO_BACKGROUND_BINARY, - &runtime_dir.join("icon_no_background.png"), + &runtime_dir.join("icon_no_backgroundv2.png"), )?), tray_explain_image: Some(extract_icon( WINDOWS_TRAY_EXPLAIN_IMAGE, @@ -100,17 +100,20 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result { #[cfg(target_os = "macos")] pub fn load_icon_paths(runtime_dir: &Path) -> Result { Ok(IconPaths { - search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?), - tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normal.png"))?), + search_icon: Some(extract_icon( + ICON_BINARY, + &runtime_dir.join("searchv2.png"), + )?), + tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normalv2.png"))?), tray_icon_disabled: Some(extract_icon( MAC_DISABLED_BINARY, - &runtime_dir.join("disabled.png"), + &runtime_dir.join("disabledv2.png"), )?), tray_icon_system_disabled: Some(extract_icon( MAC_SYSTEM_DISABLED_BINARY, - &runtime_dir.join("systemdisabled.png"), + &runtime_dir.join("systemdisabledv2.png"), )?), - logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?), + logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("iconv2.png"))?), logo_no_background: Some(extract_icon( LOGO_NO_BACKGROUND_BINARY, &runtime_dir.join("icon_no_background.png"), From 89aded62e06d25d0c91ae7cbbfc51d244716a115 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 18:06:40 +0200 Subject: [PATCH 02/25] fix(core): add support for older macOS versions. Fix #785 --- scripts/build_binary.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/build_binary.rs b/scripts/build_binary.rs index fa316ed..a2f98c4 100644 --- a/scripts/build_binary.rs +++ b/scripts/build_binary.rs @@ -67,6 +67,17 @@ fn main() { let mut cmd = Command::new("cargo"); cmd.args(&args); + // If compiling for macOS x86-64, set the minimum supported version + // to 10.13 + let is_macos = cfg!(target_os = "macos"); + let is_x86_arch = cfg!(target_arch = "x86_64"); + if is_macos + && (override_target_arch == "current" && is_x86_arch + || override_target_arch == "x86_64-apple-darwin") + { + cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.13"); + } + // Remove cargo/rust-specific env variables, as otherwise they mess up the // nested cargo build call. let all_vars = envmnt::vars(); From 6e4be125515892a173fedf51b2b8601b890ee0f4 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 19:28:08 +0200 Subject: [PATCH 03/25] chore(core): bump version --- Cargo.lock | 2 +- espanso/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 761f6f4..0c06a1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,7 @@ dependencies = [ [[package]] name = "espanso" -version = "2.0.2-alpha" +version = "2.0.3-alpha" dependencies = [ "anyhow", "caps", diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index afd0778..60cd2cf 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "2.0.2-alpha" +version = "2.0.3-alpha" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" From 48bd591bd69426fbcc702abf37687c284092c430 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 20:31:22 +0200 Subject: [PATCH 04/25] fix(detect): add flag to ToUnicodeEx call to prevent spurious random characters when pressing ALT+Arrow keys. Fix #552 --- espanso-detect/src/win32/native.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp index 673ccdb..c6c8301 100644 --- a/espanso-detect/src/win32/native.cpp +++ b/espanso-detect/src/win32/native.cpp @@ -156,9 +156,10 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w std::vector lpKeyState(256); if (GetKeyboardState(lpKeyState.data())) { - // This flag is needed to avoid chaning the keyboard state for some layouts. - // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 - UINT flags = 1 << 2; + // This flag is needed to avoid changing the keyboard state for some layouts. + // The 1 << 2 (setting bit 2) part is needed due to this issue: https://github.com/federico-terzi/espanso/issues/86 + // while the 1 (setting bit 0) part is needed due to this issue: https://github.com/federico-terzi/espanso/issues/552 + UINT flags = 1 << 2 | 1; int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout); From 45b20fd0679d81c84d8f3a9bbaeeb1922008ed55 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 21:19:05 +0200 Subject: [PATCH 05/25] fix(modulo): fix icon loading when path includes unicode chars. Fix #554 --- espanso-modulo/src/sys/form/form.cpp | 4 ++-- espanso-modulo/src/sys/search/search.cpp | 4 ++-- .../src/sys/troubleshooting/troubleshooting.cpp | 4 ++-- espanso-modulo/src/sys/welcome/welcome.cpp | 4 ++-- espanso-modulo/src/sys/wizard/wizard.cpp | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp index 120c053..722cd6b 100644 --- a/espanso-modulo/src/sys/form/form.cpp +++ b/espanso-modulo/src/sys/form/form.cpp @@ -102,8 +102,8 @@ enum bool FormApp::OnInit() { - FormFrame *frame = new FormFrame(formMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340) ); - setFrameIcon(formMetadata->iconPath, frame); + FormFrame *frame = new FormFrame(wxString::FromUTF8(formMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340) ); + setFrameIcon(wxString::FromUTF8(formMetadata->iconPath), frame); frame->Show( true ); Activate(frame); diff --git a/espanso-modulo/src/sys/search/search.cpp b/espanso-modulo/src/sys/search/search.cpp index b514e95..0e16d13 100644 --- a/espanso-modulo/src/sys/search/search.cpp +++ b/espanso-modulo/src/sys/search/search.cpp @@ -166,7 +166,7 @@ private: bool SearchApp::OnInit() { - SearchFrame *frame = new SearchFrame(searchMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340)); + SearchFrame *frame = new SearchFrame(wxString::FromUTF8(searchMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340)); frame->Show(true); SetupWindowStyle(frame); Activate(frame); @@ -198,7 +198,7 @@ SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize iconPanel = nullptr; if (searchMetadata->iconPath) { - wxString iconPath = wxString(searchMetadata->iconPath); + wxString iconPath = wxString::FromUTF8(searchMetadata->iconPath); if (wxFileExists(iconPath)) { wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG); diff --git a/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp b/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp index f34bd75..ace30b4 100644 --- a/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp +++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp @@ -81,7 +81,7 @@ public: if (error_set_metadata->errors[i].level == ERROR_METADATA_LEVEL_WARNING) { level = wxT("WARNING"); } - wxString error_text = wxString::Format(wxT("[%s] %s\n"), level, error_set_metadata->errors[i].message); + wxString error_text = wxString::Format(wxT("[%s] %s\n"), level, wxString::FromUTF8(error_set_metadata->errors[i].message)); errors_text.Append(error_text); } @@ -171,7 +171,7 @@ bool TroubleshootingApp::OnInit() if (troubleshooting_metadata->window_icon_path) { - setFrameIcon(troubleshooting_metadata->window_icon_path, frame); + setFrameIcon(wxString::FromUTF8(troubleshooting_metadata->window_icon_path), frame); } frame->Show(true); diff --git a/espanso-modulo/src/sys/welcome/welcome.cpp b/espanso-modulo/src/sys/welcome/welcome.cpp index d6c0201..26d85bf 100644 --- a/espanso-modulo/src/sys/welcome/welcome.cpp +++ b/espanso-modulo/src/sys/welcome/welcome.cpp @@ -54,7 +54,7 @@ DerivedWelcomeFrame::DerivedWelcomeFrame(wxWindow *parent) if (welcome_metadata->tray_image_path) { - wxBitmap trayBitmap = wxBitmap(welcome_metadata->tray_image_path, wxBITMAP_TYPE_PNG); + wxBitmap trayBitmap = wxBitmap(wxString::FromUTF8(welcome_metadata->tray_image_path), wxBITMAP_TYPE_PNG); this->tray_bitmap->SetBitmap(trayBitmap); #ifdef __WXOSX__ this->tray_info_label->SetLabel("You should see the espanso icon on the status bar:"); @@ -91,7 +91,7 @@ bool WelcomeApp::OnInit() if (welcome_metadata->window_icon_path) { - setFrameIcon(welcome_metadata->window_icon_path, frame); + setFrameIcon(wxString::FromUTF8(welcome_metadata->window_icon_path), frame); } frame->Show(true); diff --git a/espanso-modulo/src/sys/wizard/wizard.cpp b/espanso-modulo/src/sys/wizard/wizard.cpp index 5b9de10..a4b94cf 100644 --- a/espanso-modulo/src/sys/wizard/wizard.cpp +++ b/espanso-modulo/src/sys/wizard/wizard.cpp @@ -130,7 +130,7 @@ DerivedFrame::DerivedFrame(wxWindow *parent) if (wizard_metadata->welcome_image_path) { - wxBitmap welcomeBitmap = wxBitmap(wizard_metadata->welcome_image_path, wxBITMAP_TYPE_PNG); + wxBitmap welcomeBitmap = wxBitmap(wxString::FromUTF8(wizard_metadata->welcome_image_path), wxBITMAP_TYPE_PNG); this->welcome_image->SetBitmap(welcomeBitmap); } @@ -140,12 +140,12 @@ DerivedFrame::DerivedFrame(wxWindow *parent) if (wizard_metadata->accessibility_image_1_path) { - wxBitmap accessiblityImage1 = wxBitmap(wizard_metadata->accessibility_image_1_path, wxBITMAP_TYPE_PNG); + wxBitmap accessiblityImage1 = wxBitmap(wxString::FromUTF8(wizard_metadata->accessibility_image_1_path), wxBITMAP_TYPE_PNG); this->accessibility_image1->SetBitmap(accessiblityImage1); } if (wizard_metadata->accessibility_image_2_path) { - wxBitmap accessiblityImage2 = wxBitmap(wizard_metadata->accessibility_image_2_path, wxBITMAP_TYPE_PNG); + wxBitmap accessiblityImage2 = wxBitmap(wxString::FromUTF8(wizard_metadata->accessibility_image_2_path), wxBITMAP_TYPE_PNG); this->accessibility_image2->SetBitmap(accessiblityImage2); } @@ -380,7 +380,7 @@ bool WizardApp::OnInit() if (wizard_metadata->window_icon_path) { - setFrameIcon(wizard_metadata->window_icon_path, frame); + setFrameIcon(wxString::FromUTF8(wizard_metadata->window_icon_path), frame); } frame->Show(true); From cba4c41006f0e213f0f5305e0e05fb6e61e5f611 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 21:26:50 +0200 Subject: [PATCH 06/25] fix(modulo): fix cut button in welcome screen when using 125% text scaling on windows. Fix #777 --- espanso-modulo/src/sys/welcome/welcome.fbp | 2 +- espanso-modulo/src/sys/welcome/welcome_gui.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/espanso-modulo/src/sys/welcome/welcome.fbp b/espanso-modulo/src/sys/welcome/welcome.fbp index d9e8373..085445d 100644 --- a/espanso-modulo/src/sys/welcome/welcome.fbp +++ b/espanso-modulo/src/sys/welcome/welcome.fbp @@ -45,7 +45,7 @@ WelcomeFrame - 521,544 + 521,597 wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU ; ; forward_declare Espanso is running! diff --git a/espanso-modulo/src/sys/welcome/welcome_gui.h b/espanso-modulo/src/sys/welcome/welcome_gui.h index 44e21fe..003e7f6 100644 --- a/espanso-modulo/src/sys/welcome/welcome_gui.h +++ b/espanso-modulo/src/sys/welcome/welcome_gui.h @@ -54,7 +54,7 @@ class WelcomeFrame : public wxFrame public: - WelcomeFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso is running!"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 521,544 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL ); + WelcomeFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso is running!"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 521,597 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL ); ~WelcomeFrame(); From 12ba0b87556d4fa8f522141eaba06369ae2e7f3f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Oct 2021 22:17:19 +0200 Subject: [PATCH 07/25] fix(detect): add missing case that prevented some keys from being detected correctly on Windows. Fix #307 --- espanso-detect/src/win32/native.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp index c6c8301..3a6f4f5 100644 --- a/espanso-detect/src/win32/native.cpp +++ b/espanso-detect/src/win32/native.cpp @@ -116,7 +116,7 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w { // We only want KEY UP AND KEY DOWN events if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP && - raw->data.keyboard.Message != WM_SYSKEYDOWN) + raw->data.keyboard.Message != WM_SYSKEYDOWN && raw->data.keyboard.Message != WM_SYSKEYUP) { return 0; } From 806ac8112e3d787505f530baeba7ce860082f5b7 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 21:14:41 +0200 Subject: [PATCH 08/25] feat(mac-utils): add methods to start and stop headless eventloops --- espanso-mac-utils/src/ffi.rs | 2 ++ espanso-mac-utils/src/lib.rs | 14 ++++++++++++++ espanso-mac-utils/src/native.h | 4 ++++ espanso-mac-utils/src/native.mm | 12 ++++++++++++ 4 files changed, 32 insertions(+) diff --git a/espanso-mac-utils/src/ffi.rs b/espanso-mac-utils/src/ffi.rs index 190a673..407abb6 100644 --- a/espanso-mac-utils/src/ffi.rs +++ b/espanso-mac-utils/src/ffi.rs @@ -29,4 +29,6 @@ extern "C" { pub fn mac_utils_prompt_accessibility() -> i32; pub fn mac_utils_transition_to_foreground_app(); pub fn mac_utils_transition_to_background_app(); + pub fn mac_utils_start_headless_eventloop(); + pub fn mac_utils_exit_headless_eventloop(); } diff --git a/espanso-mac-utils/src/lib.rs b/espanso-mac-utils/src/lib.rs index de6c3b3..a338ae3 100644 --- a/espanso-mac-utils/src/lib.rs +++ b/espanso-mac-utils/src/lib.rs @@ -113,6 +113,20 @@ pub fn convert_to_background_app() { } } +#[cfg(target_os = "macos")] +pub fn start_headless_eventloop() { + unsafe { + ffi::mac_utils_start_headless_eventloop(); + } +} + +#[cfg(target_os = "macos")] +pub fn exit_headless_eventloop() { + unsafe { + ffi::mac_utils_exit_headless_eventloop(); + } +} + #[cfg(test)] #[cfg(target_os = "macos")] mod tests { diff --git a/espanso-mac-utils/src/native.h b/espanso-mac-utils/src/native.h index 984b540..b44ee48 100644 --- a/espanso-mac-utils/src/native.h +++ b/espanso-mac-utils/src/native.h @@ -40,4 +40,8 @@ extern "C" void mac_utils_transition_to_foreground_app(); // When called, convert the current process to a background app (hide the dock icon). extern "C" void mac_utils_transition_to_background_app(); +// Start and stop a "headless" eventloop to receive NSApplication events. +extern "C" void mac_utils_start_headless_eventloop(); +extern "C" void mac_utils_exit_headless_eventloop(); + #endif //ESPANSO_MAC_UTILS_H \ No newline at end of file diff --git a/espanso-mac-utils/src/native.mm b/espanso-mac-utils/src/native.mm index 75ff286..b17f034 100644 --- a/espanso-mac-utils/src/native.mm +++ b/espanso-mac-utils/src/native.mm @@ -88,4 +88,16 @@ void mac_utils_transition_to_foreground_app() { void mac_utils_transition_to_background_app() { ProcessSerialNumber psn = { 0, kCurrentProcess }; TransformProcessType(&psn, kProcessTransformToUIElementApplication); +} + +void mac_utils_start_headless_eventloop() { + NSApplication * application = [NSApplication sharedApplication]; + [NSApp run]; +} + +void mac_utils_exit_headless_eventloop() { + dispatch_async(dispatch_get_main_queue(), ^(void) { + [NSApp stop:nil]; + [NSApp abortModal]; + }); } \ No newline at end of file From 82c05d6615a2e0230526f8d1494e1536cc50ca2e Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 21:15:23 +0200 Subject: [PATCH 09/25] fix(core): add workaround to prevent launcher process from appearing 'not responding' on macOS --- espanso/src/cli/launcher/daemon.rs | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/espanso/src/cli/launcher/daemon.rs b/espanso/src/cli/launcher/daemon.rs index ee109da..98a6674 100644 --- a/espanso/src/cli/launcher/daemon.rs +++ b/espanso/src/cli/launcher/daemon.rs @@ -20,6 +20,7 @@ use std::process::Command; use anyhow::Result; +use std::process::ExitStatus; use thiserror::Error; use crate::cli::util::CommandExt; @@ -31,8 +32,7 @@ pub fn launch_daemon(paths_overrides: &PathsOverrides) -> Result<()> { command.args(&["daemon"]); command.with_paths_overrides(paths_overrides); - let mut child = command.spawn()?; - let result = child.wait()?; + let result = spawn_and_wait(command)?; if result.success() { Ok(()) @@ -46,3 +46,33 @@ pub enum DaemonError { #[error("unexpected error, 'espanso daemon' returned a non-zero exit code.")] NonZeroExitCode, } + +#[cfg(not(target_os = "macos"))] +fn spawn_and_wait(mut command: Command) -> Result { + let mut child = command.spawn()?; + Ok(child.wait()?) +} + +// On macOS, if we simply wait for the daemon process to terminate, the application will +// appear as "Not Responding" after a few seconds, even though it's working correctly. +// To avoid this undesirable behavior, we spawn an headless eventloop so that the +// launcher looks "alive", while waiting for the daemon +#[cfg(target_os = "macos")] +fn spawn_and_wait(mut command: Command) -> Result { + let mut child = command.spawn()?; + + let result = std::thread::Builder::new() + .name("daemon-monitor-thread".to_owned()) + .spawn(move || { + let results = child.wait(); + + espanso_mac_utils::exit_headless_eventloop(); + + results + })?; + + espanso_mac_utils::start_headless_eventloop(); + + let thread_result = result.join().expect("unable to join daemon-monitor-thread"); + Ok(thread_result?) +} From af70305ccc99a4058e6d5543ea5872a1abdc00bf Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 21:17:17 +0200 Subject: [PATCH 10/25] fix(core): fix edge case that prevented daemon from detecting a worker failure due to a signal. Fix #788 --- espanso/src/cli/daemon/mod.rs | 8 ++++++-- espanso/src/exit_code.rs | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/espanso/src/cli/daemon/mod.rs b/espanso/src/cli/daemon/mod.rs index 4543f17..1b1c2fe 100644 --- a/espanso/src/cli/daemon/mod.rs +++ b/espanso/src/cli/daemon/mod.rs @@ -32,8 +32,8 @@ use crate::{ common_flags::*, exit_code::{ DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR, - DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, - WORKER_SUCCESS, + DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_ERROR_EXIT_NO_CODE, + WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, WORKER_SUCCESS, }, ipc::{create_ipc_client_to_worker, IPCEvent}, lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock}, @@ -271,6 +271,10 @@ fn spawn_worker( .send(code) .expect("unable to forward worker exit code"); } + } else { + exit_notify + .send(WORKER_ERROR_EXIT_NO_CODE) + .expect("unable to forward worker exit code"); } } }) diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs index f99d1b0..01ade17 100644 --- a/espanso/src/exit_code.rs +++ b/espanso/src/exit_code.rs @@ -23,6 +23,7 @@ pub const WORKER_GENERAL_ERROR: i32 = 2; pub const WORKER_LEGACY_ALREADY_RUNNING: i32 = 3; pub const WORKER_EXIT_ALL_PROCESSES: i32 = 50; pub const WORKER_RESTART: i32 = 51; +pub const WORKER_ERROR_EXIT_NO_CODE: i32 = 90; pub const DAEMON_SUCCESS: i32 = 0; pub const DAEMON_ALREADY_RUNNING: i32 = 1; From 7987e01d72ea5eeee706f4d0dc8e773f23604002 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 22:03:30 +0200 Subject: [PATCH 11/25] fix(render): pprevent the shell extension from failing if stderr is not empty but process exits successfully. Fix #563 --- espanso-render/src/extension/shell.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs index a385a5b..3979acd 100644 --- a/espanso-render/src/extension/shell.rs +++ b/espanso-render/src/extension/shell.rs @@ -24,7 +24,7 @@ use std::{ }; use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value}; -use log::{info, warn}; +use log::{error, info}; use thiserror::Error; #[allow(clippy::upper_case_acronyms)] @@ -191,23 +191,15 @@ impl Extension for ShellExtension { info!("this debug information was shown because the 'debug' option is true."); } - let ignore_error = params - .get("ignore_error") - .and_then(|v| v.as_bool()) - .copied() - .unwrap_or(false); - - if !output.status.success() || !error_str.trim().is_empty() { - warn!( + if !output.status.success() { + error!( "shell command exited with code: {} and error: {}", output.status, error_str ); - if !ignore_error { - return ExtensionResult::Error( - ShellExtensionError::ExecutionError(error_str.to_string()).into(), - ); - } + return ExtensionResult::Error( + ShellExtensionError::ExecutionError(error_str.to_string()).into(), + ); } let trim = params From 379ab08cf1616de86842efada4cc2bc3d1346dcb Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 22:34:36 +0200 Subject: [PATCH 12/25] feat(detect): handle modifiers release event on macOS --- espanso-detect/src/event.rs | 5 +++++ espanso-detect/src/mac/mod.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs index 6eae8ad..bfd8914 100644 --- a/espanso-detect/src/event.rs +++ b/espanso-detect/src/event.rs @@ -25,6 +25,11 @@ pub enum InputEvent { Mouse(MouseEvent), Keyboard(KeyboardEvent), HotKey(HotKeyEvent), + + // Special event type only used on macOS + // This is sent after a global keyboard shortcut is released + // See https://github.com/federico-terzi/espanso/issues/791 + AllModifiersReleased, } #[derive(Debug, PartialEq)] diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs index d9bc5b0..5b93558 100644 --- a/espanso-detect/src/mac/mod.rs +++ b/espanso-detect/src/mac/mod.rs @@ -241,6 +241,13 @@ impl From for Option { INPUT_EVENT_TYPE_KEYBOARD => { let (key, variant) = key_code_to_key(raw.key_code); + // When a global keyboard shortcut is relased, the callback returns an event with keycode 0 + // and status 0. + // We need to handle it for this reason: https://github.com/federico-terzi/espanso/issues/791 + if raw.key_code == 0 && raw.status == 0 { + return Some(InputEvent::AllModifiersReleased); + } + let value = if raw.buffer_len > 0 { let raw_string_result = CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]); From 0b23a5cc3022acd6a5cc2d4c72880d279bd90c7f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 22:35:02 +0200 Subject: [PATCH 13/25] feat(engine): refactor funnel to allow skipping of events --- espanso-engine/src/funnel/default.rs | 7 +++++-- espanso-engine/src/funnel/mod.rs | 3 ++- espanso-engine/src/lib.rs | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/espanso-engine/src/funnel/default.rs b/espanso-engine/src/funnel/default.rs index d409bc5..cd32179 100644 --- a/espanso-engine/src/funnel/default.rs +++ b/espanso-engine/src/funnel/default.rs @@ -48,7 +48,10 @@ impl<'a> Funnel for DefaultFunnel<'a> { .expect("invalid source index returned by select operation"); // Receive (and convert) the event - let event = source.receive(op); - FunnelResult::Event(event) + if let Some(event) = source.receive(op) { + FunnelResult::Event(event) + } else { + FunnelResult::Skipped + } } } diff --git a/espanso-engine/src/funnel/mod.rs b/espanso-engine/src/funnel/mod.rs index d920310..5c2abbb 100644 --- a/espanso-engine/src/funnel/mod.rs +++ b/espanso-engine/src/funnel/mod.rs @@ -27,7 +27,7 @@ mod default; pub trait Source<'a> { fn register(&'a self, select: &mut Select<'a>) -> usize; - fn receive(&'a self, op: SelectedOperation) -> Event; + fn receive(&'a self, op: SelectedOperation) -> Option; } pub trait Funnel { @@ -36,6 +36,7 @@ pub trait Funnel { pub enum FunnelResult { Event(Event), + Skipped, EndOfStream, } diff --git a/espanso-engine/src/lib.rs b/espanso-engine/src/lib.rs index eb4fbd4..3ac31c8 100644 --- a/espanso-engine/src/lib.rs +++ b/espanso-engine/src/lib.rs @@ -68,6 +68,9 @@ impl<'a> Engine<'a> { debug!("end of stream received"); return ExitMode::Exit; } + FunnelResult::Skipped => { + // This event has been skipped, no need to handle it + } } } } From 44dd54e2e1c9ef0ff970c0267c7a52e8b360c707 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 22:36:25 +0200 Subject: [PATCH 14/25] fix(core): handle AllModifiersRelease event on macOS to prevent modifier state getting out-of-sync with shortcuts. Fix #791 --- espanso/src/cli/worker/engine/funnel/detect.rs | 15 ++++++++------- espanso/src/cli/worker/engine/funnel/exit.rs | 6 +++--- espanso/src/cli/worker/engine/funnel/mod.rs | 2 ++ espanso/src/cli/worker/engine/funnel/modifier.rs | 7 +++++++ .../src/cli/worker/engine/funnel/secure_input.rs | 11 +++++------ espanso/src/cli/worker/engine/funnel/ui.rs | 6 +++--- 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/espanso/src/cli/worker/engine/funnel/detect.rs b/espanso/src/cli/worker/engine/funnel/detect.rs index f0a074f..c587f30 100644 --- a/espanso/src/cli/worker/engine/funnel/detect.rs +++ b/espanso/src/cli/worker/engine/funnel/detect.rs @@ -37,12 +37,12 @@ impl<'a> funnel::Source<'a> for DetectSource { select.recv(&self.receiver) } - fn receive(&self, op: SelectedOperation) -> Event { + fn receive(&self, op: SelectedOperation) -> Option { let (input_event, source_id) = op .recv(&self.receiver) .expect("unable to select data from DetectSource receiver"); match input_event { - InputEvent::Keyboard(keyboard_event) => Event { + InputEvent::Keyboard(keyboard_event) => Some(Event { source_id, etype: EventType::Keyboard(KeyboardEvent { key: convert_to_engine_key(keyboard_event.key), @@ -50,20 +50,21 @@ impl<'a> funnel::Source<'a> for DetectSource { status: convert_to_engine_status(keyboard_event.status), variant: keyboard_event.variant.map(convert_to_engine_variant), }), - }, - InputEvent::Mouse(mouse_event) => Event { + }), + InputEvent::Mouse(mouse_event) => Some(Event { source_id, etype: EventType::Mouse(MouseEvent { status: convert_to_engine_status(mouse_event.status), button: convert_to_engine_mouse_button(mouse_event.button), }), - }, - InputEvent::HotKey(hotkey_event) => Event { + }), + InputEvent::HotKey(hotkey_event) => Some(Event { source_id, etype: EventType::HotKey(HotKeyEvent { hotkey_id: hotkey_event.hotkey_id, }), - }, + }), + InputEvent::AllModifiersReleased => None, } } } diff --git a/espanso/src/cli/worker/engine/funnel/exit.rs b/espanso/src/cli/worker/engine/funnel/exit.rs index ad1f540..f119e73 100644 --- a/espanso/src/cli/worker/engine/funnel/exit.rs +++ b/espanso/src/cli/worker/engine/funnel/exit.rs @@ -45,13 +45,13 @@ impl<'a> funnel::Source<'a> for ExitSource<'a> { select.recv(&self.exit_signal) } - fn receive(&self, op: SelectedOperation) -> Event { + fn receive(&self, op: SelectedOperation) -> Option { let mode = op .recv(&self.exit_signal) .expect("unable to select data from ExitSource receiver"); - Event { + Some(Event { source_id: self.sequencer.next_id(), etype: EventType::ExitRequested(mode), - } + }) } } diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs index 78bd38b..f1d699b 100644 --- a/espanso/src/cli/worker/engine/funnel/mod.rs +++ b/espanso/src/cli/worker/engine/funnel/mod.rs @@ -81,6 +81,8 @@ pub fn init_and_spawn( // Update the modifiers state if let Some((modifier, is_pressed)) = get_modifier_status(&event) { modifier_state_store_clone.update_state(modifier, is_pressed); + } else if let InputEvent::AllModifiersReleased = &event { + modifier_state_store_clone.clear_state(); } // Update the key state (if needed) diff --git a/espanso/src/cli/worker/engine/funnel/modifier.rs b/espanso/src/cli/worker/engine/funnel/modifier.rs index ba3229f..6545e73 100644 --- a/espanso/src/cli/worker/engine/funnel/modifier.rs +++ b/espanso/src/cli/worker/engine/funnel/modifier.rs @@ -91,6 +91,13 @@ impl ModifierStateStore { } } } + + pub fn clear_state(&self) { + let mut state = self.state.lock().expect("unable to obtain modifier state"); + for (_, status) in &mut state.modifiers { + status.release(); + } + } } struct ModifiersState { diff --git a/espanso/src/cli/worker/engine/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs index 527c80b..3e4da03 100644 --- a/espanso/src/cli/worker/engine/funnel/secure_input.rs +++ b/espanso/src/cli/worker/engine/funnel/secure_input.rs @@ -50,13 +50,13 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> { } } - fn receive(&self, op: SelectedOperation) -> Event { + fn receive(&self, op: SelectedOperation) -> Option { if cfg!(target_os = "macos") { let si_event = op .recv(&self.receiver) .expect("unable to select data from SecureInputSource receiver"); - Event { + Some(Event { source_id: self.sequencer.next_id(), etype: match si_event { SecureInputEvent::Disabled => EventType::SecureInputDisabled, @@ -64,13 +64,12 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> { EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path }) } }, - } + }) } else { - println!("noop"); - Event { + Some(Event { source_id: self.sequencer.next_id(), etype: EventType::NOOP, - } + }) } } } diff --git a/espanso/src/cli/worker/engine/funnel/ui.rs b/espanso/src/cli/worker/engine/funnel/ui.rs index ded79c5..e70cb97 100644 --- a/espanso/src/cli/worker/engine/funnel/ui.rs +++ b/espanso/src/cli/worker/engine/funnel/ui.rs @@ -46,12 +46,12 @@ impl<'a> funnel::Source<'a> for UISource<'a> { select.recv(&self.ui_receiver) } - fn receive(&self, op: SelectedOperation) -> Event { + fn receive(&self, op: SelectedOperation) -> Option { let ui_event = op .recv(&self.ui_receiver) .expect("unable to select data from UISource receiver"); - Event { + Some(Event { source_id: self.sequencer.next_id(), etype: match ui_event { UIEvent::TrayIconClick => EventType::TrayIconClicked, @@ -60,6 +60,6 @@ impl<'a> funnel::Source<'a> for UISource<'a> { } UIEvent::Heartbeat => EventType::Heartbeat, }, - } + }) } } From 93b6c8e75a5099dd18545b4cb24f2da75128b259 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 13 Oct 2021 22:38:35 +0200 Subject: [PATCH 15/25] fix(render): remove outdated test --- espanso-render/src/extension/shell.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs index 3979acd..4fe4250 100644 --- a/espanso-render/src/extension/shell.rs +++ b/espanso-render/src/extension/shell.rs @@ -380,24 +380,4 @@ mod tests { ExtensionResult::Error(_) )); } - - #[test] - #[cfg(not(target_os = "windows"))] - fn ignore_error() { - let extension = ShellExtension::new(&PathBuf::new()); - - let param = vec![ - ("cmd".to_string(), Value::String("exit 1".to_string())), - ("ignore_error".to_string(), Value::Bool(true)), - ] - .into_iter() - .collect::(); - assert_eq!( - extension - .calculate(&Default::default(), &Default::default(), ¶m) - .into_success() - .unwrap(), - ExtensionOutput::Single("".to_string()) - ); - } } From a257cf96e506b527265e96ba29f16c462bdf3a42 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 14 Oct 2021 19:37:47 +0200 Subject: [PATCH 16/25] fix(ci): add explicit macOS minimum version to CI to (hopefully) fix #785 --- .github/workflows/ci.yml | 2 ++ .github/workflows/release.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 579f0bd..739b4d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ jobs: run: | rustup component add clippy cargo clippy -- -D warnings + env: + MACOSX_DEPLOYMENT_TARGET: "10.13" - name: Install cargo-make run: | cargo install --force cargo-make diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee56d23..d41c8b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,8 +131,12 @@ jobs: cargo install --force cargo-make - name: Test run: cargo make test-binary --profile release + env: + MACOSX_DEPLOYMENT_TARGET: "10.13" - name: Build run: cargo make create-bundle --profile release + env: + MACOSX_DEPLOYMENT_TARGET: "10.13" - name: Create ZIP archive run: | ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-Intel.zip From 70bff10fd523f174f38796b938c0c3178a798c56 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 11:19:26 +0200 Subject: [PATCH 17/25] fix(engine): prevent events from stacking up when the search bar is open. Fix #781 --- espanso-engine/src/event/internal.rs | 8 +++ espanso-engine/src/event/mod.rs | 1 + espanso-engine/src/process/default.rs | 10 ++- .../{past_discard.rs => discard.rs} | 40 +++++++----- .../src/process/middleware/match_select.rs | 62 ++++++++++++++----- espanso-engine/src/process/middleware/mod.rs | 2 +- 6 files changed, 89 insertions(+), 34 deletions(-) rename espanso-engine/src/process/middleware/{past_discard.rs => discard.rs} (52%) diff --git a/espanso-engine/src/event/internal.rs b/espanso-engine/src/event/internal.rs index ca43c2e..eac56a3 100644 --- a/espanso-engine/src/event/internal.rs +++ b/espanso-engine/src/event/internal.rs @@ -85,6 +85,14 @@ pub struct DiscardPreviousEvent { pub minimum_source_id: u32, } +#[derive(Debug, Clone, PartialEq)] +pub struct DiscardBetweenEvent { + // All Events with a source_id between start_id (included) and end_id (excluded) + // will be discarded + pub start_id: u32, + pub end_id: u32, +} + #[derive(Debug, Clone, PartialEq)] pub struct SecureInputEnabledEvent { pub app_name: String, diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs index f43eb96..8769e0e 100644 --- a/espanso-engine/src/event/mod.rs +++ b/espanso-engine/src/event/mod.rs @@ -71,6 +71,7 @@ pub enum EventType { ImageResolved(internal::ImageResolvedEvent), MatchInjected, DiscardPrevious(internal::DiscardPreviousEvent), + DiscardBetween(internal::DiscardBetweenEvent), Undo(internal::UndoEvent), Disabled, diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs index c20fe6d..826f388 100644 --- a/espanso-engine/src/process/default.rs +++ b/espanso-engine/src/process/default.rs @@ -25,11 +25,11 @@ use super::{ cause::CauseCompensateMiddleware, cursor_hint::CursorHintMiddleware, delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, + discard::EventsDiscardMiddleware, markdown::MarkdownMiddleware, match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware, - past_discard::PastEventsDiscardMiddleware, render::RenderMiddleware, }, DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider, @@ -73,14 +73,18 @@ impl<'a> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), middleware: vec![ - Box::new(PastEventsDiscardMiddleware::new()), + Box::new(EventsDiscardMiddleware::new()), Box::new(DisableMiddleware::new(disable_options)), Box::new(IconStatusMiddleware::new()), Box::new(MatcherMiddleware::new(matchers, matcher_options_provider)), Box::new(SuppressMiddleware::new(enabled_status_provider)), Box::new(ContextMenuMiddleware::new()), Box::new(HotKeyMiddleware::new()), - Box::new(MatchSelectMiddleware::new(match_filter, match_selector)), + Box::new(MatchSelectMiddleware::new( + match_filter, + match_selector, + event_sequence_provider, + )), Box::new(CauseCompensateMiddleware::new()), Box::new(MultiplexMiddleware::new(multiplexer)), Box::new(RenderMiddleware::new(renderer)), diff --git a/espanso-engine/src/process/middleware/past_discard.rs b/espanso-engine/src/process/middleware/discard.rs similarity index 52% rename from espanso-engine/src/process/middleware/past_discard.rs rename to espanso-engine/src/process/middleware/discard.rs index 5b2b9b5..249588d 100644 --- a/espanso-engine/src/process/middleware/past_discard.rs +++ b/espanso-engine/src/process/middleware/discard.rs @@ -24,42 +24,54 @@ use log::trace; use super::super::Middleware; use crate::event::{Event, EventType, SourceId}; -/// This middleware discards all events that have a source_id smaller than its -/// configured threshold. This useful to discard past events that might have -/// been stuck in the event queue for too long. -pub struct PastEventsDiscardMiddleware { - source_id_threshold: RefCell, +/// This middleware discards all events that have a source_id between +/// the given maximum and minimum. +/// This useful to discard past events that might have been stuck in the +/// event queue for too long, or events generated while the search bar was open. +pub struct EventsDiscardMiddleware { + min_id_threshold: RefCell, + max_id_threshold: RefCell, } -impl PastEventsDiscardMiddleware { +impl EventsDiscardMiddleware { pub fn new() -> Self { Self { - source_id_threshold: RefCell::new(0), + min_id_threshold: RefCell::new(0), + max_id_threshold: RefCell::new(0), } } } -impl Middleware for PastEventsDiscardMiddleware { +impl Middleware for EventsDiscardMiddleware { fn name(&self) -> &'static str { - "past_discard" + "discard" } fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event { - let mut source_id_threshold = self.source_id_threshold.borrow_mut(); + let mut min_id_threshold = self.min_id_threshold.borrow_mut(); + let mut max_id_threshold = self.max_id_threshold.borrow_mut(); // Filter out previous events - if event.source_id < *source_id_threshold { + if event.source_id < *max_id_threshold && event.source_id >= *min_id_threshold { trace!("discarding previous event: {:?}", event); return Event::caused_by(event.source_id, EventType::NOOP); } - // Update the minimum threshold + // Update the thresholds if let EventType::DiscardPrevious(m_event) = &event.etype { trace!( - "updating minimum source id threshold for events to: {}", + "updating discard max_id_threshold threshold for events to: {}", m_event.minimum_source_id ); - *source_id_threshold = m_event.minimum_source_id; + *max_id_threshold = m_event.minimum_source_id; + } else if let EventType::DiscardBetween(m_event) = &event.etype { + trace!( + "updating discard thresholds for events to: max={} min={}", + m_event.end_id, + m_event.start_id + ); + *max_id_threshold = m_event.end_id; + *min_id_threshold = m_event.start_id; } event diff --git a/espanso-engine/src/process/middleware/match_select.rs b/espanso-engine/src/process/middleware/match_select.rs index 5fe0461..2467b19 100644 --- a/espanso-engine/src/process/middleware/match_select.rs +++ b/espanso-engine/src/process/middleware/match_select.rs @@ -20,7 +20,13 @@ use log::{debug, error}; use super::super::Middleware; -use crate::event::{internal::MatchSelectedEvent, Event, EventType}; +use crate::{ + event::{ + internal::{DiscardBetweenEvent, MatchSelectedEvent}, + Event, EventType, + }, + process::EventSequenceProvider, +}; pub trait MatchFilter { fn filter_active(&self, matches_ids: &[i32]) -> Vec; @@ -33,13 +39,19 @@ pub trait MatchSelector { pub struct MatchSelectMiddleware<'a> { match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector, + event_sequence_provider: &'a dyn EventSequenceProvider, } impl<'a> MatchSelectMiddleware<'a> { - pub fn new(match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector) -> Self { + pub fn new( + match_filter: &'a dyn MatchFilter, + match_selector: &'a dyn MatchSelector, + event_sequence_provider: &'a dyn EventSequenceProvider, + ) -> Self { Self { match_filter, match_selector, + event_sequence_provider, } } } @@ -49,7 +61,7 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> { "match_select" } - fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event { + fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event { if let EventType::MatchesDetected(m_event) = event.etype { let matches_ids: Vec = m_event.matches.iter().map(|m| m.id).collect(); @@ -75,22 +87,40 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> { } } _ => { + let start_event_id = self.event_sequence_provider.get_next_id(); + // Multiple matches, we need to ask the user which one to use - if let Some(selected_id) = self.match_selector.select(&valid_ids, m_event.is_search) { - let m = m_event.matches.into_iter().find(|m| m.id == selected_id); - if let Some(m) = m { - Event::caused_by( - event.source_id, - EventType::MatchSelected(MatchSelectedEvent { chosen: m }), - ) + let next_event = + if let Some(selected_id) = self.match_selector.select(&valid_ids, m_event.is_search) { + let m = m_event.matches.into_iter().find(|m| m.id == selected_id); + if let Some(m) = m { + Event::caused_by( + event.source_id, + EventType::MatchSelected(MatchSelectedEvent { chosen: m }), + ) + } else { + error!("MatchSelectMiddleware could not find the correspondent match"); + Event::caused_by(event.source_id, EventType::NOOP) + } } else { - error!("MatchSelectMiddleware could not find the correspondent match"); + debug!("MatchSelectMiddleware did not receive any match selection"); Event::caused_by(event.source_id, EventType::NOOP) - } - } else { - debug!("MatchSelectMiddleware did not receive any match selection"); - Event::caused_by(event.source_id, EventType::NOOP) - } + }; + + let end_event_id = self.event_sequence_provider.get_next_id(); + + // We want to prevent espanso from "stacking up" events while the search bar is open, + // therefore we filter out all events that were generated while the search bar was open. + // See also: https://github.com/federico-terzi/espanso/issues/781 + dispatch(Event::caused_by( + event.source_id, + EventType::DiscardBetween(DiscardBetweenEvent { + start_id: start_event_id, + end_id: end_event_id, + }), + )); + + next_event } }; } diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs index ec2bcc7..9ba98b3 100644 --- a/espanso-engine/src/process/middleware/mod.rs +++ b/espanso-engine/src/process/middleware/mod.rs @@ -23,6 +23,7 @@ pub mod context_menu; pub mod cursor_hint; pub mod delay_modifiers; pub mod disable; +pub mod discard; pub mod exit; pub mod hotkey; pub mod icon_status; @@ -31,7 +32,6 @@ pub mod markdown; pub mod match_select; pub mod matcher; pub mod multiplex; -pub mod past_discard; pub mod render; pub mod search; pub mod suppress; From 904f24a4384f6073874b2f4257d58c6a6f2b9858 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 12:08:32 +0200 Subject: [PATCH 18/25] fix(ci): attempt to fix linux CI that failed on apt install step --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 739b4d9..519e97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,8 @@ jobs: - name: Install Linux dependencies if: ${{ runner.os == 'Linux' }} run: | - sudo apt install libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev + sudo apt-get update + sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev - name: Check clippy run: | rustup component add clippy @@ -51,7 +52,8 @@ jobs: cargo fmt --all -- --check - name: Install Linux dependencies run: | - sudo apt install libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev + sudo apt-get update + sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev - name: Check clippy run: | rustup component add clippy From f9b256c1a3afd77e50ceed980df367fdc44b9755 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 12:22:28 +0200 Subject: [PATCH 19/25] fix(modulo): fix bug that prevented forms from being displayed on Linux --- espanso-modulo/build.rs | 17 +++++++++++++++++ espanso-modulo/src/sys/common/common.cpp | 5 ++--- espanso-modulo/src/sys/common/common.h | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs index 86bc21d..31646ca 100644 --- a/espanso-modulo/build.rs +++ b/espanso-modulo/build.rs @@ -457,5 +457,22 @@ fn build_native() { } fn main() { + println!("cargo:rerun-if-changed=src/x11/native/native.h"); + println!("cargo:rerun-if-changed=src/sys/interop/interop.h"); + println!("cargo:rerun-if-changed=src/sys/form/form.cpp"); + println!("cargo:rerun-if-changed=src/sys/common/mac.h"); + println!("cargo:rerun-if-changed=src/sys/common/mac.mm"); + println!("cargo:rerun-if-changed=src/sys/common/common.h"); + println!("cargo:rerun-if-changed=src/sys/common/common.cpp"); + println!("cargo:rerun-if-changed=src/sys/welcome/welcome_gui.h"); + println!("cargo:rerun-if-changed=src/sys/welcome/welcome_gui.cpp"); + println!("cargo:rerun-if-changed=src/sys/welcome/welcome.cpp"); + println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting_gui.h"); + println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting_gui.cpp"); + println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting.cpp"); + println!("cargo:rerun-if-changed=src/sys/search/search.cpp"); + println!("cargo:rerun-if-changed=src/sys/wizard/wizard.cpp"); + println!("cargo:rerun-if-changed=src/sys/wizard/wizard_gui.cpp"); + println!("cargo:rerun-if-changed=src/sys/wizard/wizard_gui.h"); build_native(); } diff --git a/espanso-modulo/src/sys/common/common.cpp b/espanso-modulo/src/sys/common/common.cpp index 53a4e16..47a2e69 100644 --- a/espanso-modulo/src/sys/common/common.cpp +++ b/espanso-modulo/src/sys/common/common.cpp @@ -26,9 +26,8 @@ #include "mac.h" #endif -void setFrameIcon(const char * iconPath, wxFrame * frame) { - if (iconPath) { - wxString iconPath(iconPath); +void setFrameIcon(wxString iconPath, wxFrame * frame) { + if (!iconPath.IsEmpty()) { wxBitmapType imgType = wxICON_DEFAULT_TYPE; #ifdef __WXMSW__ diff --git a/espanso-modulo/src/sys/common/common.h b/espanso-modulo/src/sys/common/common.h index 99b1d7a..c9a7d5d 100644 --- a/espanso-modulo/src/sys/common/common.h +++ b/espanso-modulo/src/sys/common/common.h @@ -27,7 +27,7 @@ #include #endif -void setFrameIcon(const char * iconPath, wxFrame * frame); +void setFrameIcon(wxString iconPath, wxFrame * frame); void Activate(wxFrame * frame); From 6f94ee3f38cec33188fae572314191cea6408eb1 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 13:23:27 +0200 Subject: [PATCH 20/25] fix(engine): filter out keyboard events while some modifiers are pressed. Fix #725 --- espanso-engine/src/process/default.rs | 11 +++-- .../src/process/middleware/matcher.rs | 49 +++++++++++++++++++ espanso-engine/src/process/mod.rs | 5 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs index 826f388..b0f85b8 100644 --- a/espanso-engine/src/process/default.rs +++ b/espanso-engine/src/process/default.rs @@ -33,8 +33,8 @@ use super::{ render::RenderMiddleware, }, DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider, - MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider, - Processor, Renderer, UndoEnabledProvider, + MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider, + Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider, }; use crate::{ event::{Event, EventType}, @@ -69,6 +69,7 @@ impl<'a> DefaultProcessor<'a> { match_provider: &'a dyn MatchProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider, enabled_status_provider: &'a dyn EnabledStatusProvider, + modifier_state_provider: &'a dyn ModifierStateProvider, ) -> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), @@ -76,7 +77,11 @@ impl<'a> DefaultProcessor<'a> { Box::new(EventsDiscardMiddleware::new()), Box::new(DisableMiddleware::new(disable_options)), Box::new(IconStatusMiddleware::new()), - Box::new(MatcherMiddleware::new(matchers, matcher_options_provider)), + Box::new(MatcherMiddleware::new( + matchers, + matcher_options_provider, + modifier_state_provider, + )), Box::new(SuppressMiddleware::new(enabled_status_provider)), Box::new(ContextMenuMiddleware::new()), Box::new(HotKeyMiddleware::new()), diff --git a/espanso-engine/src/process/middleware/matcher.rs b/espanso-engine/src/process/middleware/matcher.rs index 7b3e666..87d97ea 100644 --- a/espanso-engine/src/process/middleware/matcher.rs +++ b/espanso-engine/src/process/middleware/matcher.rs @@ -57,18 +57,32 @@ pub trait MatcherMiddlewareConfigProvider { fn max_history_size(&self) -> usize; } +pub trait ModifierStateProvider { + fn get_modifier_state(&self) -> ModifierState; +} + +#[derive(Debug, Clone)] +pub struct ModifierState { + pub is_ctrl_down: bool, + pub is_alt_down: bool, + pub is_meta_down: bool, +} + pub struct MatcherMiddleware<'a, State> { matchers: &'a [&'a dyn Matcher<'a, State>], matcher_states: RefCell>>, max_history_size: usize, + + modifier_status_provider: &'a dyn ModifierStateProvider, } impl<'a, State> MatcherMiddleware<'a, State> { pub fn new( matchers: &'a [&'a dyn Matcher<'a, State>], options_provider: &'a dyn MatcherMiddlewareConfigProvider, + modifier_status_provider: &'a dyn ModifierStateProvider, ) -> Self { let max_history_size = options_provider.max_history_size(); @@ -76,6 +90,7 @@ impl<'a, State> MatcherMiddleware<'a, State> { matchers, matcher_states: RefCell::new(VecDeque::new()), max_history_size, + modifier_status_provider, } } } @@ -101,6 +116,17 @@ impl<'a, State> Middleware for MatcherMiddleware<'a, State> { matcher_states.pop_back(); return event; } + + // We need to filter out some keyboard events if they are generated + // while some modifier keys are pressed, otherwise we could have + // wrong matches being detected. + // See: https://github.com/federico-terzi/espanso/issues/725 + if should_skip_key_event_due_to_modifier_press( + &self.modifier_status_provider.get_modifier_state(), + ) { + trace!("skipping keyboard event because incompatible modifiers are pressed"); + return event; + } } // Some keys (such as the arrow keys) and mouse clicks prevent espanso from building @@ -161,6 +187,17 @@ fn is_event_of_interest(event_type: &EventType) -> bool { // Skip non-press events false } else { + // Skip linux Keyboard (XKB) Extension function and modifier keys + // In hex, they have the byte 3 = 0xfe + // See list in "keysymdef.h" file + if cfg!(target_os = "linux") { + if let Key::Other(raw_code) = &keyboard_event.key { + if (65025..=65276).contains(raw_code) { + return false; + } + } + } + // Skip modifier keys !matches!( keyboard_event.key, @@ -205,4 +242,16 @@ fn is_invalidating_event(event_type: &EventType) -> bool { } } +fn should_skip_key_event_due_to_modifier_press(modifier_state: &ModifierState) -> bool { + if cfg!(target_os = "macos") { + modifier_state.is_meta_down + } else if cfg!(target_os = "windows") { + modifier_state.is_alt_down + } else if cfg!(target_os = "linux") { + modifier_state.is_alt_down || modifier_state.is_meta_down + } else { + unreachable!() + } +} + // TODO: test diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs index 91bce0d..1e295f9 100644 --- a/espanso-engine/src/process/mod.rs +++ b/espanso-engine/src/process/mod.rs @@ -39,7 +39,8 @@ pub use middleware::disable::DisableOptions; pub use middleware::image_resolve::PathProvider; pub use middleware::match_select::{MatchFilter, MatchSelector}; pub use middleware::matcher::{ - MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, + MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState, + ModifierStateProvider, }; pub use middleware::multiplex::Multiplexer; pub use middleware::render::{Renderer, RendererError}; @@ -63,6 +64,7 @@ pub fn default<'a, MatcherState>( match_provider: &'a dyn MatchProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider, enabled_status_provider: &'a dyn EnabledStatusProvider, + modifier_state_provider: &'a dyn ModifierStateProvider, ) -> impl Processor + 'a { default::DefaultProcessor::new( matchers, @@ -79,5 +81,6 @@ pub fn default<'a, MatcherState>( match_provider, undo_enabled_provider, enabled_status_provider, + modifier_state_provider, ) } From 789acc3d760ad1d4887b70cd822623faafa7d4b1 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 13:23:50 +0200 Subject: [PATCH 21/25] feat(core): pass necessary modifier state provider to engine --- .../src/cli/worker/engine/funnel/modifier.rs | 41 +++++++++++++++++++ espanso/src/cli/worker/engine/mod.rs | 1 + 2 files changed, 42 insertions(+) diff --git a/espanso/src/cli/worker/engine/funnel/modifier.rs b/espanso/src/cli/worker/engine/funnel/modifier.rs index 6545e73..5be0ff2 100644 --- a/espanso/src/cli/worker/engine/funnel/modifier.rs +++ b/espanso/src/cli/worker/engine/funnel/modifier.rs @@ -149,3 +149,44 @@ impl ModifierStatusProvider for ModifierStateStore { self.is_any_conflicting_modifier_pressed() } } + +impl espanso_engine::process::ModifierStateProvider for ModifierStateStore { + fn get_modifier_state(&self) -> espanso_engine::process::ModifierState { + let mut state = self.state.lock().expect("unable to obtain modifier state"); + + let mut is_ctrl_down = false; + let mut is_alt_down = false; + let mut is_meta_down = false; + + for (modifier, status) in &mut state.modifiers { + if status.is_outdated() { + warn!( + "detected outdated modifier records for {:?}, releasing the state", + modifier + ); + status.release(); + } + + if status.is_pressed() { + match modifier { + Modifier::Ctrl => { + is_ctrl_down = true; + } + Modifier::Alt => { + is_alt_down = true; + } + Modifier::Meta => { + is_meta_down = true; + } + _ => {} + } + } + } + + espanso_engine::process::ModifierState { + is_ctrl_down, + is_alt_down, + is_meta_down, + } + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 53e21b5..249db13 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -223,6 +223,7 @@ pub fn initialize_and_spawn( &combined_match_cache, &config_manager, &config_manager, + &modifier_state_store, ); let event_injector = EventInjectorAdapter::new(&*injector, &config_manager); From 3b74b5f558fa6f471df9fef16b0f9e3262fbb5ba Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 14:41:28 +0200 Subject: [PATCH 22/25] fix(engine): remove Alt from skipped modifiers on Windows, related to #725 --- espanso-engine/src/process/middleware/matcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/espanso-engine/src/process/middleware/matcher.rs b/espanso-engine/src/process/middleware/matcher.rs index 87d97ea..92f49ea 100644 --- a/espanso-engine/src/process/middleware/matcher.rs +++ b/espanso-engine/src/process/middleware/matcher.rs @@ -246,7 +246,7 @@ fn should_skip_key_event_due_to_modifier_press(modifier_state: &ModifierState) - if cfg!(target_os = "macos") { modifier_state.is_meta_down } else if cfg!(target_os = "windows") { - modifier_state.is_alt_down + false } else if cfg!(target_os = "linux") { modifier_state.is_alt_down || modifier_state.is_meta_down } else { From a584ee94ec7a7c89355e6b6bafb0492503506620 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 14:42:22 +0200 Subject: [PATCH 23/25] fix(detect): filter out values from keyboard events when Alt key is pressed, related to #725 --- espanso-detect/src/win32/native.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp index 3a6f4f5..1ac9d41 100644 --- a/espanso-detect/src/win32/native.cpp +++ b/espanso-detect/src/win32/native.cpp @@ -167,6 +167,15 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w if (result >= 1) { event.buffer_len = result; + + // Filter out the value if the key was pressed while the ALT key was down + // but not if AltGr is down (which is a shortcut to ALT+CTRL on some keyboards, such + // as the italian one). + // This is needed in conjunction with the fix for: https://github.com/federico-terzi/espanso/issues/725 + if ((lpKeyState[VK_MENU] & 0x80) != 0 && (lpKeyState[VK_CONTROL] & 0x80) == 0) { + memset(event.buffer, 0, sizeof(event.buffer)); + event.buffer_len = 0; + } } else { From 39655269c3284e89349a27ff8b02c19fc43df8c6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 16 Oct 2021 20:48:18 +0200 Subject: [PATCH 24/25] fix(core): disable undo_backspace when using slow inject mode on linux --- espanso/src/cli/worker/config.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs index c4808bb..408782a 100644 --- a/espanso/src/cli/worker/config.rs +++ b/espanso/src/cli/worker/config.rs @@ -168,6 +168,13 @@ impl<'a> espanso_engine::process::UndoEnabledProvider for ConfigManager<'a> { return false; } + // Because we cannot filter out espanso-generated events when using the X11 record injection + // method, we need to disable undo_backspace to avoid looping (espanso picks up its own + // injections, causing the program to misbehave) + if cfg!(target_os = "linux") && self.active().disable_x11_fast_inject() { + return false; + } + self.active().undo_backspace() } } From 72ef170a2cadf8df1e96b8deaa53d30c6bc566f5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 17 Oct 2021 13:05:09 +0200 Subject: [PATCH 25/25] feat(misc): update snapcraft configuration for v2. Fix #795 --- snapcraft.yaml | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/snapcraft.yaml b/snapcraft.yaml index 9723f54..51f22b0 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.7.3 +version: 2.0.3-alpha summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. @@ -21,13 +21,17 @@ description: | * Works with almost any program * Works with Emojis ¯\_(ツ)_/¯ * Works with Images + * Includes a powerful Search Bar * Date expansion support * Custom scripts support * Shell commands support + * Support Forms * App-specific configurations * Expandable with packages * Built-in package manager for espanso hub: https://hub.espanso.org/ * File based configuration + * Support Regex triggers + * Experimental Wayland support (currently not available through Snap, visit the website for more info). ## Get Started @@ -47,23 +51,35 @@ parts: source: . build-packages: - libssl-dev + - libdbus-1-dev + - libwxgtk3.0-gtk3-dev - pkg-config - - cmake + - libxkbcommon-dev - libxtst-dev - libx11-dev - - libxdo-dev stage-packages: - libx11-6 - libxau6 - libxcb1 - libxdmcp6 - - libxdo3 - libxext6 - libxinerama1 - libxkbcommon0 - libxtst6 - libnotify-bin - - xclip + - libdbus-1-3 + - libssl1.1 + - libwxbase3.0-0v5 + - libwxgtk3.0-0v5 + - libatk-bridge2.0-0 + - libatspi2.0-0 + - libcairo-gobject2 + - libepoxy0 + - libgtk-3-0 + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libwxgtk3.0-gtk3-0v5 apps: espanso: