Repository: espanso/espanso (language: rust)

Issue:
Port Espanso v2 on Snap

Will espanso 2.0 support older macOS versions?
Hi, @federico-terzi!

Is there any plan to support older macOS versions?

I currently use Mojave on a 5.1 MacPro. Will espanso compile on my Mac?

Thank you! 😊


Discussion / hints:

Hey @rflcrz,

Thank you for the feedback! I haven't tested Espanso on older versions of macOS and it's unlikely that we'll be able to "officially" support many versions with our current resources. That said, have you tried running the latest Espanso release? As far as I can tell, there is no explicit minimum version, so it might be working :)

If you want to try, you can download the new App bundle from here: https://github.com/federico-terzi/espanso/releases/download/v2.0.1/Espanso-Mac-Intel.zip

Let me know if that works

Cheers :)

This app bundle says that "The application requires macOS 11.0 or later."

I'm not a developer, but I can try to compile if there are build instructions. A simple "cargo build --release" didn't work for me.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 579f0bd8..519e97f1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,11 +25,14 @@ 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
           cargo clippy -- -D warnings
+        env:
+          MACOSX_DEPLOYMENT_TARGET: "10.13"
       - name: Install cargo-make
         run: |
           cargo install --force cargo-make
@@ -49,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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ee56d235..d41c8b91 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
diff --git a/Cargo.lock b/Cargo.lock
index 761f6f46..0c06a1c8 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-detect/src/event.rs b/espanso-detect/src/event.rs
index 6eae8ade..bfd89149 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 d9bc5b0f..5b93558e 100644
--- a/espanso-detect/src/mac/mod.rs
+++ b/espanso-detect/src/mac/mod.rs
@@ -241,6 +241,13 @@ impl From<RawInputEvent> for Option<InputEvent> {
       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)]);
diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp
index 673ccdb2..1ac9d417 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;
       }
@@ -156,9 +156,10 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
       std::vector<BYTE> 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<LPWSTR>(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout);
 
@@ -166,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
         {
diff --git a/espanso-engine/src/event/internal.rs b/espanso-engine/src/event/internal.rs
index ca43c2e9..eac56a30 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 f43eb96e..8769e0ef 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/funnel/default.rs b/espanso-engine/src/funnel/default.rs
index d409bc50..cd321793 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 d920310c..5c2abbba 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<Event>;
 }
 
 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 eb4fbd48..3ac31c86 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
+        }
       }
     }
   }
diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs
index c20fe6d8..b0f85b8e 100644
--- a/espanso-engine/src/process/default.rs
+++ b/espanso-engine/src/process/default.rs
@@ -25,16 +25,16 @@ 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,
-  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,18 +69,27 @@ 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(),
       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(MatcherMiddleware::new(
+          matchers,
+          matcher_options_provider,
+          modifier_state_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 5b2b9b58..249588dd 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<SourceId>,
+/// 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<SourceId>,
+  max_id_threshold: RefCell<SourceId>,
 }
 
-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 5fe0461e..2467b19a 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<i32>;
@@ -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<i32> = 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/matcher.rs b/espanso-engine/src/process/middleware/matcher.rs
index 7b3e6665..92f49ea5 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<VecDeque<Vec<State>>>,
 
   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") {
+    false
+  } 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/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs
index ec2bcc7c..9ba98b3b 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;
diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs
index 91bce0d0..1e295f93 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,
   )
 }
diff --git a/espanso-mac-utils/src/ffi.rs b/espanso-mac-utils/src/ffi.rs
index 190a6734..407abb69 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 de6c3b33..a338ae31 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 984b540a..b44ee48a 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 75ff286a..b17f034c 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
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
index 86bc21d7..31646cab 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 53a4e163..47a2e69e 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 99b1d7ab..c9a7d5df 100644
--- a/espanso-modulo/src/sys/common/common.h
+++ b/espanso-modulo/src/sys/common/common.h
@@ -27,7 +27,7 @@
     #include <wx/wx.h>
 #endif
 
-void setFrameIcon(const char * iconPath, wxFrame * frame);
+void setFrameIcon(wxString iconPath, wxFrame * frame);
 
 void Activate(wxFrame * frame);
 
diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp
index 120c053d..722cd6be 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 b514e951..0e16d134 100644
--- a/espanso-modulo/src/sys/search/search.cpp
+++ b/espanso-modulo/src/sys/search/search.cpp
@@ -166,7 +166,7 @@ class SearchFrame : public wxFrame
 
 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 f34bd754..ace30b4e 100644
--- a/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp
+++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp
@@ -81,7 +81,7 @@ class ErrorSetPanel : public wxPanel
       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 d6c02011..26d85bf9 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/welcome/welcome.fbp b/espanso-modulo/src/sys/welcome/welcome.fbp
index d9e83737..085445da 100644
--- a/espanso-modulo/src/sys/welcome/welcome.fbp
+++ b/espanso-modulo/src/sys/welcome/welcome.fbp
@@ -45,7 +45,7 @@
             <property name="minimum_size"></property>
             <property name="name">WelcomeFrame</property>
             <property name="pos"></property>
-            <property name="size">521,544</property>
+            <property name="size">521,597</property>
             <property name="style">wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU</property>
             <property name="subclass">; ; forward_declare</property>
             <property name="title">Espanso is running!</property>
diff --git a/espanso-modulo/src/sys/welcome/welcome_gui.h b/espanso-modulo/src/sys/welcome/welcome_gui.h
index 44e21fef..003e7f6d 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();
 
diff --git a/espanso-modulo/src/sys/wizard/wizard.cpp b/espanso-modulo/src/sys/wizard/wizard.cpp
index 5b9de108..a4b94cfd 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);
diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs
index a385a5b0..4fe4250f 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
@@ -388,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::<Params>();
-    assert_eq!(
-      extension
-        .calculate(&Default::default(), &Default::default(), &param)
-        .into_success()
-        .unwrap(),
-      ExtensionOutput::Single("".to_string())
-    );
-  }
 }
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index afd07786..60cd2cff 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 <federicoterzi96@gmail.com>"]
 license = "GPL-3.0"
 description = "Cross-platform Text Expander written in Rust"
diff --git a/espanso/src/cli/daemon/mod.rs b/espanso/src/cli/daemon/mod.rs
index 4543f172..1b1c2fed 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/cli/launcher/daemon.rs b/espanso/src/cli/launcher/daemon.rs
index ee109dac..98a6674f 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<ExitStatus> {
+  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<ExitStatus> {
+  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?)
+}
diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs
index c4808bba..408782a8 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()
   }
 }
diff --git a/espanso/src/cli/worker/engine/funnel/detect.rs b/espanso/src/cli/worker/engine/funnel/detect.rs
index f0a074f5..c587f307 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<Event> {
     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 ad1f5405..f119e731 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<Event> {
     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 78bd38b3..f1d699b8 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 ba3229f5..5be0ff26 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 {
@@ -142,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/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs
index 527c80b7..3e4da03e 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<Event> {
     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 ded79c5c..e70cb974 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<Event> {
     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,
       },
-    }
+    })
   }
 }
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index 53e21b5b..249db134 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);
diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs
index f99d1b0b..01ade17a 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;
diff --git a/espanso/src/icon.rs b/espanso/src/icon.rs
index b0d6e03b..c5cf54cd 100644
--- a/espanso/src/icon.rs
+++ b/espanso/src/icon.rs
@@ -69,25 +69,25 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
   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<IconPaths> {
 #[cfg(target_os = "macos")]
 pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
   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"),
diff --git a/scripts/build_binary.rs b/scripts/build_binary.rs
index fa316edb..a2f98c48 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();
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 9723f540..51f22b01 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:


Test patch: