Repository: espanso/espanso (language: rust)

Issue:
Disable toggle_key by default
Given the large number of bug reports received over the past two years, it's clear that setting the `toggle_key` to ALT by default was not a good design choice, I'm sorry about that!

To improve the situation, the next Espanso version (2.1.2), will ship with the toggle_key disabled by default.

If you want to keep using it, you can explicitly add it to your "$CONFIG/config/default.yml` as explained here: https://espanso.org/docs/next/configuration/options/#customizing-the-toggle-key

Cheers :)




Discussion / hints:

Fix patch:
diff --git a/.github/scripts/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile
index b3c7eee9..f974dd63 100644
--- a/.github/scripts/ubuntu/Dockerfile
+++ b/.github/scripts/ubuntu/Dockerfile
@@ -31,7 +31,7 @@ RUN set -eux; \
     cargo --version; \
     rustc --version;
 
-RUN mkdir espanso && cargo install --force cargo-make --version 0.34.0
+RUN mkdir espanso && cargo install rust-script --version "0.7.0" && cargo install --force cargo-make --version 0.34.0
 
 COPY . espanso
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b8810356..f910d582 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,8 +33,9 @@ jobs:
           cargo clippy -- -D warnings
         env:
           MACOSX_DEPLOYMENT_TARGET: "10.13"
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary
@@ -58,8 +59,9 @@ jobs:
         run: |
           rustup component add clippy
           cargo clippy -p espanso --features wayland -- -D warnings
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary --env NO_X11=true
@@ -72,8 +74,9 @@ jobs:
       - uses: actions/checkout@v2
       - name: Install target
         run: rustup update && rustup target add aarch64-apple-darwin
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Build
         run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9674a1f1..974040b8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -59,8 +59,9 @@ jobs:
       - name: Print target version
         run: |
           echo Using version ${{ needs.extract-version.outputs.espanso_version }}
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Test
         run: cargo make test-binary --profile release
@@ -126,8 +127,9 @@ jobs:
       - name: Print target version
         run: |
           echo Using version ${{ needs.extract-version.outputs.espanso_version }}
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Test
         run: cargo make test-binary --profile release
@@ -166,8 +168,9 @@ jobs:
           echo Using version ${{ needs.extract-version.outputs.espanso_version }}
       - name: Install rust target
         run: rustup update && rustup target add aarch64-apple-darwin
-      - name: Install cargo-make
+      - name: Install rust-script and cargo-make
         run: |
+          cargo install rust-script --version "0.7.0"
           cargo install --force cargo-make --version 0.34.0
       - name: Build
         run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
diff --git a/Cargo.lock b/Cargo.lock
index 0ba00ea8..193d7ecf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -572,7 +572,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "2.1.1-alpha"
+version = "2.1.2-alpha"
 dependencies = [
  "anyhow",
  "caps",
diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs
index 7416ce4b..68dfecfc 100644
--- a/espanso-clipboard/src/lib.rs
+++ b/espanso-clipboard/src/lib.rs
@@ -49,18 +49,11 @@ pub trait Clipboard {
 }
 
 #[allow(dead_code)]
+#[derive(Default)]
 pub struct ClipboardOperationOptions {
   pub use_xclip_backend: bool,
 }
 
-impl Default for ClipboardOperationOptions {
-  fn default() -> Self {
-    Self {
-      use_xclip_backend: false,
-    }
-  }
-}
-
 #[allow(dead_code)]
 pub struct ClipboardOptions {
   // Wayland-only
diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs
index f566a415..d6f27b6b 100644
--- a/espanso-clipboard/src/wayland/fallback/mod.rs
+++ b/espanso-clipboard/src/wayland/fallback/mod.rs
@@ -135,7 +135,7 @@ impl Clipboard for WaylandFallbackClipboard {
     file.read_to_end(&mut data)?;
 
     self.invoke_command_with_timeout(
-      &mut Command::new("wl-copy").arg("--type").arg("image/png"),
+      Command::new("wl-copy").arg("--type").arg("image/png"),
       &data,
       "wl-copy",
     )
@@ -148,7 +148,7 @@ impl Clipboard for WaylandFallbackClipboard {
     _: &ClipboardOperationOptions,
   ) -> anyhow::Result<()> {
     self.invoke_command_with_timeout(
-      &mut Command::new("wl-copy").arg("--type").arg("text/html"),
+      Command::new("wl-copy").arg("--type").arg("text/html"),
       html.as_bytes(),
       "wl-copy",
     )
diff --git a/espanso-clipboard/src/x11/xclip/mod.rs b/espanso-clipboard/src/x11/xclip/mod.rs
index 1136dff4..f2873863 100644
--- a/espanso-clipboard/src/x11/xclip/mod.rs
+++ b/espanso-clipboard/src/x11/xclip/mod.rs
@@ -30,7 +30,7 @@ pub struct XClipClipboard {
 
 impl XClipClipboard {
   pub fn new() -> Self {
-    let command = Command::new("xclipz").arg("-h").output();
+    let command = Command::new("xclip").arg("-h").output();
     let is_xclip_available = command
       .map(|output| output.status.success())
       .unwrap_or(false);
diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs
index 87380850..548c4337 100644
--- a/espanso-config/src/config/resolve.rs
+++ b/espanso-config/src/config/resolve.rs
@@ -37,7 +37,7 @@ use thiserror::Error;
 
 const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
 pub(crate) struct ResolvedConfig {
   parsed: ParsedConfig,
 
@@ -52,20 +52,6 @@ pub(crate) struct ResolvedConfig {
   filter_exec: Option<Regex>,
 }
 
-impl Default for ResolvedConfig {
-  fn default() -> Self {
-    Self {
-      parsed: Default::default(),
-      source_path: None,
-      id: 0,
-      match_paths: Vec::new(),
-      filter_title: None,
-      filter_class: None,
-      filter_exec: None,
-    }
-  }
-}
-
 impl Config for ResolvedConfig {
   fn id(&self) -> i32 {
     self.id
@@ -202,13 +188,10 @@ impl Config for ResolvedConfig {
       Some("left_shift") => Some(ToggleKey::LeftShift),
       Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
       Some("off") => None,
-      None => Some(ToggleKey::Alt),
+      None => None,
       err => {
-        error!(
-          "invalid toggle_key specified {:?}, falling back to ALT",
-          err
-        );
-        Some(ToggleKey::Alt)
+        error!("invalid toggle_key specified {:?}", err);
+        None
       }
     }
   }
diff --git a/espanso-config/src/legacy/config.rs b/espanso-config/src/legacy/config.rs
index 87ff50c0..a873744e 100644
--- a/espanso-config/src/legacy/config.rs
+++ b/espanso-config/src/legacy/config.rs
@@ -701,18 +701,15 @@ impl LegacyConfigSet {
     let mut sorted_triggers: Vec<String> = default
       .matches
       .iter()
-      .flat_map(|t| triggers_for_match(t))
+      .flat_map(triggers_for_match)
       .collect();
     sorted_triggers.sort();
 
     let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
 
     for s in specific.iter() {
-      let mut specific_triggers: Vec<String> = s
-        .matches
-        .iter()
-        .flat_map(|t| triggers_for_match(t))
-        .collect();
+      let mut specific_triggers: Vec<String> =
+        s.matches.iter().flat_map(triggers_for_match).collect();
       specific_triggers.sort();
       has_conflicts |= Self::list_has_conflicts(&specific_triggers);
     }
diff --git a/espanso-config/src/matches/group/mod.rs b/espanso-config/src/matches/group/mod.rs
index 3d61c239..895a183f 100644
--- a/espanso-config/src/matches/group/mod.rs
+++ b/espanso-config/src/matches/group/mod.rs
@@ -27,23 +27,13 @@ use super::{Match, Variable};
 pub(crate) mod loader;
 mod path;
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Default)]
 pub(crate) struct MatchGroup {
   pub imports: Vec<String>,
   pub global_vars: Vec<Variable>,
   pub matches: Vec<Match>,
 }
 
-impl Default for MatchGroup {
-  fn default() -> Self {
-    Self {
-      imports: Vec::new(),
-      global_vars: Vec::new(),
-      matches: Vec::new(),
-    }
-  }
-}
-
 impl MatchGroup {
   // TODO: test
   pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {
diff --git a/espanso-config/src/matches/mod.rs b/espanso-config/src/matches/mod.rs
index 8d2a5bbd..33fd8e43 100644
--- a/espanso-config/src/matches/mod.rs
+++ b/espanso-config/src/matches/mod.rs
@@ -132,19 +132,11 @@ pub enum UpperCasingStyle {
   CapitalizeWords,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
 pub struct RegexCause {
   pub regex: String,
 }
 
-impl Default for RegexCause {
-  fn default() -> Self {
-    Self {
-      regex: String::new(),
-    }
-  }
-}
-
 // Effects
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
@@ -186,19 +178,11 @@ impl Default for TextEffect {
   }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
 pub struct ImageEffect {
   pub path: String,
 }
 
-impl Default for ImageEffect {
-  fn default() -> Self {
-    Self {
-      path: String::new(),
-    }
-  }
-}
-
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Variable {
   pub id: StructId,
diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs
index 64162163..ebc086ea 100644
--- a/espanso-inject/src/lib.rs
+++ b/espanso-inject/src/lib.rs
@@ -89,6 +89,7 @@ impl Default for InjectionOptions {
 }
 
 #[allow(dead_code)]
+#[derive(Default)]
 pub struct InjectorCreationOptions {
   // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
   pub use_evdev: bool,
@@ -128,18 +129,6 @@ pub trait KeyboardStateProvider {
   fn is_key_pressed(&self, code: u32) -> bool;
 }
 
-impl Default for InjectorCreationOptions {
-  fn default() -> Self {
-    Self {
-      use_evdev: false,
-      evdev_modifiers: None,
-      evdev_max_modifier_combination_len: None,
-      evdev_keyboard_rmlvo: None,
-      keyboard_state_provider: None,
-    }
-  }
-}
-
 #[cfg(target_os = "windows")]
 pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
   info!("using Win32Injector");
diff --git a/espanso-match/src/regex/mod.rs b/espanso-match/src/regex/mod.rs
index 560f513d..b05e5ca8 100644
--- a/espanso-match/src/regex/mod.rs
+++ b/espanso-match/src/regex/mod.rs
@@ -40,19 +40,11 @@ impl<Id> RegexMatch<Id> {
   }
 }
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct RegexMatcherState {
   buffer: String,
 }
 
-impl Default for RegexMatcherState {
-  fn default() -> Self {
-    Self {
-      buffer: String::new(),
-    }
-  }
-}
-
 pub struct RegexMatcherOptions {
   pub max_buffer_size: usize,
 }
diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs
index 136bed31..44cbd991 100644
--- a/espanso-match/src/rolling/matcher.rs
+++ b/espanso-match/src/rolling/matcher.rs
@@ -50,20 +50,12 @@ struct RollingMatcherStatePath<'a, Id> {
   events: Vec<(Event, IsWordSeparator)>,
 }
 
+#[derive(Default)]
 pub struct RollingMatcherOptions {
   pub char_word_separators: Vec<String>,
   pub key_word_separators: Vec<Key>,
 }
 
-impl Default for RollingMatcherOptions {
-  fn default() -> Self {
-    Self {
-      char_word_separators: Vec::new(),
-      key_word_separators: Vec::new(),
-    }
-  }
-}
-
 pub struct RollingMatcher<Id> {
   char_word_separators: Vec<String>,
   key_word_separators: Vec<Key>,
diff --git a/espanso-match/src/rolling/mod.rs b/espanso-match/src/rolling/mod.rs
index 57d60b7e..ec2db0ef 100644
--- a/espanso-match/src/rolling/mod.rs
+++ b/espanso-match/src/rolling/mod.rs
@@ -72,22 +72,13 @@ impl<Id> RollingMatch<Id> {
   }
 }
 
+#[derive(Default)]
 pub struct StringMatchOptions {
   pub case_insensitive: bool,
   pub left_word: bool,
   pub right_word: bool,
 }
 
-impl Default for StringMatchOptions {
-  fn default() -> Self {
-    Self {
-      case_insensitive: false,
-      left_word: false,
-      right_word: false,
-    }
-  }
-}
-
 #[cfg(test)]
 mod tests {
   use super::*;
diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp
index 34813144..8ff4bec0 100644
--- a/espanso-modulo/src/sys/form/form.cpp
+++ b/espanso-modulo/src/sys/form/form.cpp
@@ -96,6 +96,7 @@ class FormFrame: public wxFrame
     void Submit();
     void OnSubmitBtn(wxCommandEvent& event);
     void OnCharHook(wxKeyEvent& event);
+    void OnListBoxEvent(wxCommandEvent& event);
     void UpdateHelpText();
     void HandleNormalFocus(wxFocusEvent& event);
     void HandleMultilineFocus(wxFocusEvent& event);
@@ -141,7 +142,6 @@ FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& si
 
     Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
     Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY);
-    // TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
 
     this->SetClientSize(panel->GetBestSize());
     this->CentreOnScreen();
@@ -218,6 +218,11 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
                 }
 
                 ((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
+                // ListBoxes prevent the global CHAR_HOOK handler from handling the Return key
+                // correctly, so we need to handle the double click event too (which is triggered
+                // when the enter key is pressed).
+                // See: https://github.com/federico-terzi/espanso/issues/857
+                ((wxListBox*)choice)->Bind(wxEVT_LISTBOX_DCLICK, &FormFrame::OnListBoxEvent, this, wxID_ANY);
                 
                 // Create the field wrapper
                 std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
@@ -311,6 +316,10 @@ void FormFrame::OnCharHook(wxKeyEvent& event) {
     }
 }
 
+void FormFrame::OnListBoxEvent(wxCommandEvent& event) {
+    Submit();
+}
+
 extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
     // Setup high DPI support on Windows
     #ifdef __WXMSW__
diff --git a/espanso-render/src/extension/choice.rs b/espanso-render/src/extension/choice.rs
new file mode 100644
index 00000000..b043680d
--- /dev/null
+++ b/espanso-render/src/extension/choice.rs
@@ -0,0 +1,132 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::error;
+use thiserror::Error;
+
+use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
+
+pub trait ChoiceSelector {
+  fn show(&self, choices: &[Choice]) -> ChoiceSelectorResult;
+}
+
+#[derive(Debug, Clone)]
+pub struct Choice<'a> {
+  pub label: &'a str,
+  pub id: &'a str,
+}
+
+pub enum ChoiceSelectorResult {
+  Success(String),
+  Aborted,
+  Error(anyhow::Error),
+}
+
+pub struct ChoiceExtension<'a> {
+  selector: &'a dyn ChoiceSelector,
+}
+
+#[allow(clippy::new_without_default)]
+impl<'a> ChoiceExtension<'a> {
+  pub fn new(selector: &'a dyn ChoiceSelector) -> Self {
+    Self { selector }
+  }
+}
+
+impl<'a> Extension for ChoiceExtension<'a> {
+  fn name(&self) -> &str {
+    "choice"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    _: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    let choices: Vec<Choice> = if let Some(Value::String(values)) = params.get("values") {
+      values
+        .lines()
+        .filter_map(|line| {
+          let trimmed_line = line.trim();
+          if !trimmed_line.is_empty() {
+            Some(trimmed_line)
+          } else {
+            None
+          }
+        })
+        .map(|line| Choice {
+          label: line,
+          id: line,
+        })
+        .collect()
+    } else if let Some(Value::Array(values)) = params.get("values") {
+      let choices: Result<Vec<Choice>> = values
+        .iter()
+        .map(|value| match value {
+          Value::String(string) => Ok(Choice {
+            id: string,
+            label: string,
+          }),
+          Value::Object(fields) => Ok(Choice {
+            id: fields
+              .get("id")
+              .and_then(|val| val.as_string())
+              .ok_or(ChoiceError::InvalidObjectValue)?,
+            label: fields
+              .get("label")
+              .and_then(|val| val.as_string())
+              .ok_or(ChoiceError::InvalidObjectValue)?,
+          }),
+          _ => Err(ChoiceError::InvalidValueType.into()),
+        })
+        .collect();
+
+      match choices {
+        Ok(choices) => choices,
+        Err(err) => {
+          return crate::ExtensionResult::Error(err);
+        }
+      }
+    } else {
+      return crate::ExtensionResult::Error(ChoiceError::MissingValues.into());
+    };
+
+    match self.selector.show(&choices) {
+      ChoiceSelectorResult::Success(choice_id) => {
+        ExtensionResult::Success(ExtensionOutput::Single(choice_id))
+      }
+      ChoiceSelectorResult::Aborted => ExtensionResult::Aborted,
+      ChoiceSelectorResult::Error(error) => ExtensionResult::Error(error),
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ChoiceError {
+  #[error("missing values parameter")]
+  MissingValues,
+
+  #[error("values contain object items, but they are missing either the 'id' or 'label' fields")]
+  InvalidObjectValue,
+
+  #[error("values contain an invalid item type. items can only be strings or objects")]
+  InvalidValueType,
+}
diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs
index 35bae77b..4cc73569 100644
--- a/espanso-render/src/extension/mod.rs
+++ b/espanso-render/src/extension/mod.rs
@@ -17,6 +17,7 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+pub mod choice;
 pub mod clipboard;
 pub mod date;
 pub mod echo;
diff --git a/espanso-render/src/lib.rs b/espanso-render/src/lib.rs
index 0517f9f5..29296ce8 100644
--- a/espanso-render/src/lib.rs
+++ b/espanso-render/src/lib.rs
@@ -42,20 +42,12 @@ pub enum RenderResult {
   Error(anyhow::Error),
 }
 
+#[derive(Default)]
 pub struct Context<'a> {
   pub global_vars: Vec<&'a Variable>,
   pub templates: Vec<&'a Template>,
 }
 
-impl<'a> Default for Context<'a> {
-  fn default() -> Self {
-    Self {
-      global_vars: Vec::new(),
-      templates: Vec::new(),
-    }
-  }
-}
-
 #[derive(Debug, Clone, PartialEq)]
 pub struct RenderOptions {
   pub casing_style: CasingStyle,
@@ -77,23 +69,13 @@ pub enum CasingStyle {
   Uppercase,
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Default)]
 pub struct Template {
   pub ids: Vec<String>,
   pub body: String,
   pub vars: Vec<Variable>,
 }
 
-impl Default for Template {
-  fn default() -> Self {
-    Self {
-      ids: Vec::new(),
-      body: "".to_string(),
-      vars: Vec::new(),
-    }
-  }
-}
-
 #[derive(Debug, Clone, PartialEq)]
 pub struct Variable {
   pub name: String,
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index 4716d8c2..65def50f 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "2.1.1-alpha"
+version = "2.1.2-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/mod.rs b/espanso/src/cli/mod.rs
index f7a37684..258b5a1b 100644
--- a/espanso/src/cli/mod.rs
+++ b/espanso/src/cli/mod.rs
@@ -74,6 +74,7 @@ pub enum LogMode {
   CleanAndAppend,
 }
 
+#[derive(Default)]
 pub struct CliModuleArgs {
   pub config_store: Option<Box<dyn ConfigStore>>,
   pub match_store: Option<Box<dyn MatchStore>>,
@@ -84,20 +85,6 @@ pub struct CliModuleArgs {
   pub cli_args: Option<ArgMatches<'static>>,
 }
 
-impl Default for CliModuleArgs {
-  fn default() -> Self {
-    Self {
-      config_store: None,
-      match_store: None,
-      is_legacy_config: false,
-      non_fatal_errors: Vec::new(),
-      paths: None,
-      paths_overrides: None,
-      cli_args: None,
-    }
-  }
-}
-
 pub struct PathsOverrides {
   pub config: Option<PathBuf>,
   pub runtime: Option<PathBuf>,
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
index 163245b2..bf7ac6c8 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
@@ -81,6 +81,11 @@ impl<'a> ClipboardInjectorAdapter<'a> {
       custom_combination
     } else if cfg!(target_os = "macos") {
       vec![Key::Meta, Key::V]
+    } else if cfg!(target_os = "linux") && cfg!(feature = "wayland") {
+      // Because on Wayland we currently don't have app-specific configs (and therefore no patches)
+      // we switch to the more supported SHIFT+INSERT combination
+      // See: https://github.com/federico-terzi/espanso/issues/899
+      vec![Key::Shift, Key::Insert]
     } else {
       vec![Key::Control, Key::V]
     };
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs
index 01b87111..b9829641 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs
@@ -58,11 +58,7 @@ fn convert_to_ui_menu_item(
     espanso_engine::event::ui::MenuItem::Sub(sub) => {
       espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem {
         label: sub.label.clone(),
-        items: sub
-          .items
-          .iter()
-          .map(|item| convert_to_ui_menu_item(item))
-          .collect(),
+        items: sub.items.iter().map(convert_to_ui_menu_item).collect(),
       })
     }
     espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator,
diff --git a/espanso/src/cli/worker/engine/funnel/key_state.rs b/espanso/src/cli/worker/engine/funnel/key_state.rs
index f3522199..8b50a977 100644
--- a/espanso/src/cli/worker/engine/funnel/key_state.rs
+++ b/espanso/src/cli/worker/engine/funnel/key_state.rs
@@ -76,18 +76,11 @@ impl KeyStateStore {
   }
 }
 
+#[derive(Default)]
 struct KeyState {
   keys: HashMap<u32, KeyStatus>,
 }
 
-impl Default for KeyState {
-  fn default() -> Self {
-    Self {
-      keys: HashMap::new(),
-    }
-  }
-}
-
 struct KeyStatus {
   pressed_at: Option<Instant>,
 }
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index 513bff59..8581a626 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -49,7 +49,9 @@ use crate::{
         },
         multiplex::MultiplexAdapter,
         render::{
-          extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter},
+          extension::{
+            choice::ChoiceSelectorAdapter, clipboard::ClipboardAdapter, form::FormProviderAdapter,
+          },
           RendererAdapter,
         },
       },
@@ -198,6 +200,9 @@ pub fn initialize_and_spawn(
       let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config);
       let form_adapter = FormProviderAdapter::new(&modulo_form_ui);
       let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter);
+      let choice_adapter = ChoiceSelectorAdapter::new(&modulo_search_ui);
+      let choice_extension =
+        espanso_render::extension::choice::ChoiceExtension::new(&choice_adapter);
       let renderer = espanso_render::create(vec![
         &clipboard_extension,
         &date_extension,
@@ -207,6 +212,7 @@ pub fn initialize_and_spawn(
         &script_extension,
         &shell_extension,
         &form_extension,
+        &choice_extension,
       ]);
       let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer);
       let path_provider = PathProviderAdapter::new(&paths);
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs
new file mode 100644
index 00000000..d2f245d2
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs
@@ -0,0 +1,55 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_render::extension::choice::{ChoiceSelector, ChoiceSelectorResult};
+
+use crate::gui::{SearchItem, SearchUI};
+
+pub struct ChoiceSelectorAdapter<'a> {
+  search_ui: &'a dyn SearchUI,
+}
+
+impl<'a> ChoiceSelectorAdapter<'a> {
+  pub fn new(search_ui: &'a dyn SearchUI) -> Self {
+    Self { search_ui }
+  }
+}
+
+impl<'a> ChoiceSelector for ChoiceSelectorAdapter<'a> {
+  fn show(&self, choices: &[espanso_render::extension::choice::Choice]) -> ChoiceSelectorResult {
+    let items = convert_items(choices);
+    match self.search_ui.show(&items, None) {
+      Ok(Some(choice)) => ChoiceSelectorResult::Success(choice),
+      Ok(None) => ChoiceSelectorResult::Aborted,
+      Err(err) => ChoiceSelectorResult::Error(err),
+    }
+  }
+}
+
+fn convert_items(choices: &[espanso_render::extension::choice::Choice]) -> Vec<SearchItem> {
+  choices
+    .iter()
+    .map(|choice| SearchItem {
+      id: choice.id.to_string(),
+      label: choice.label.to_string(),
+      tag: None,
+      is_builtin: false,
+    })
+    .collect()
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs
index d7cafacc..e2497b57 100644
--- a/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs
@@ -17,5 +17,6 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+pub mod choice;
 pub mod clipboard;
 pub mod form;
diff --git a/espanso/src/patch/mod.rs b/espanso/src/patch/mod.rs
index fa6d065c..8bb70555 100644
--- a/espanso/src/patch/mod.rs
+++ b/espanso/src/patch/mod.rs
@@ -31,6 +31,7 @@ pub fn patch_store(store: Box<dyn ConfigStore>) -> Box<dyn ConfigStore> {
 fn get_builtin_patches() -> Vec<PatchDefinition> {
   #[cfg(target_os = "windows")]
   return vec![
+    patches::win::brave::patch(),
     patches::win::onenote_for_windows_10::patch(),
     patches::win::vscode_win::patch(),
   ];
diff --git a/espanso/src/patch/patches/win/brave.rs b/espanso/src/patch/patches/win/brave.rs
new file mode 100644
index 00000000..2e57be82
--- /dev/null
+++ b/espanso/src/patch/patches/win/brave.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "windows"),
+    should_patch: |app| app.exec.unwrap_or_default().contains("brave.exe"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          pre_paste_delay: Some(400),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/win/mod.rs b/espanso/src/patch/patches/win/mod.rs
index 63fb8629..a6762b9d 100644
--- a/espanso/src/patch/patches/win/mod.rs
+++ b/espanso/src/patch/patches/win/mod.rs
@@ -17,5 +17,6 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+pub mod brave;
 pub mod onenote_for_windows_10;
 pub mod vscode_win;
diff --git a/packager.py b/packager.py
deleted file mode 100644
index 6a944a1a..00000000
--- a/packager.py
+++ /dev/null
@@ -1,254 +0,0 @@
-import subprocess
-import sys
-import os
-import platform
-import hashlib
-import click
-import shutil
-import toml
-import hashlib
-import glob
-import urllib.request
-from dataclasses import dataclass
-
-PACKAGER_TARGET_DIR = "target/packager"
-
-@dataclass
-class PackageInfo:
-    name: str
-    version: str
-    description: str
-    publisher: str
-    url: str
-    modulo_version: str
-
-@click.group()
-def cli():
-    pass
-
-@cli.command()
-@click.option('--skipcargo', default=False, is_flag=True, help="Skip cargo release build")
-def build(skipcargo):
-    """Build espanso distribution"""
-    # Check operating system
-    TARGET_OS = "macos"
-    if platform.system() == "Windows":
-        TARGET_OS = "windows"
-    elif platform.system() == "Linux":
-        TARGET_OS = "linux"
-
-    print("Detected OS:", TARGET_OS)
-
-    print("Loading info from Cargo.toml")
-    cargo_info = toml.load("Cargo.toml")
-    package_info = PackageInfo(cargo_info["package"]["name"],
-                               cargo_info["package"]["version"],
-                               cargo_info["package"]["description"],
-                               cargo_info["package"]["authors"][0],
-                               cargo_info["package"]["homepage"],
-                               cargo_info["modulo"]["version"])
-    print(package_info)
-
-    if not skipcargo:
-        print("Building release version...")
-        subprocess.run(["cargo", "build", "--release"])
-    else:
-        print("Skipping build")
-
-    if TARGET_OS == "windows":
-        build_windows(package_info)
-    elif TARGET_OS == "macos":
-        build_mac(package_info)
-
-def calculate_sha256(file):
-    with open(file, "rb") as f:
-        b = f.read() # read entire file as bytes
-        readable_hash = hashlib.sha256(b).hexdigest()
-        return readable_hash
-
-def build_windows(package_info):
-    print("Starting packaging process for Windows...")
-
-    # Check Inno Setup
-    try:
-        subprocess.run(["iscc"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-    except FileNotFoundError:
-        raise Exception("Could not find Inno Setup compiler. Please install it from here: http://www.jrsoftware.org/isdl.php")
-
-    print("Clearing target dirs")
-
-    # Clearing previous build directory
-    if os.path.isdir(PACKAGER_TARGET_DIR):
-        print("Cleaning packager temp directory...")
-        shutil.rmtree(PACKAGER_TARGET_DIR)
-
-    TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
-    os.makedirs(TARGET_DIR, exist_ok=True)
-
-    modulo_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe".format(package_info.modulo_version)
-    modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe.sha256.txt".format(package_info.modulo_version)
-    print("Pulling modulo depencency from:", modulo_url)
-    modulo_target_file = os.path.join(TARGET_DIR, "modulo.exe")
-    urllib.request.urlretrieve(modulo_url, modulo_target_file)
-    print("Pulling SHA signature from:", modulo_sha_url)
-    modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
-    urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
-    print("Checking signatures...")
-    expected_sha = None
-    with open(modulo_sha_file, "r") as sha_f:
-        expected_sha = sha_f.read()
-    actual_sha = calculate_sha256(modulo_target_file)
-    if actual_sha != expected_sha:
-        raise Exception("Modulo SHA256 is not matching")
-
-    print("Gathering CRT DLLs...")
-    msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
-    print("Found Redists: ", msvc_dirs)
-
-    print("Determining best redist...")
-
-    if len(msvc_dirs) == 0:
-        raise Exception("Cannot find redistributable dlls")
-
-    msvc_dir = None
-
-    for curr_dir in msvc_dirs:
-        dll_files = glob.glob(curr_dir + "\\x64\\*CRT\\*.dll")
-        print("Found dlls", dll_files, "in", curr_dir)
-        if any("vcruntime140_1.dll" in x.lower() for x in dll_files):
-            msvc_dir = curr_dir
-            break
-
-    if msvc_dir is None:
-        raise Exception("Cannot find redist with VCRUNTIME140_1.dll")
-
-    print("Using: ", msvc_dir)
-
-    dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")
-
-    print("Found DLLs:")
-    include_list = []
-    for dll in dll_files:
-        print("Including: "+dll)
-        include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
-
-    print("Including modulo")
-    include_list.append("Source: \""+os.path.abspath(modulo_target_file)+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
-
-    include = "\r\n".join(include_list)
-
-    INSTALLER_NAME = f"espanso-win-installer"
-
-    # Inno setup
-    shutil.copy("packager/win/modpath.iss", os.path.join(TARGET_DIR, "modpath.iss"))
-
-    print("Processing inno setup template")
-    with open("packager/win/setupscript.iss", "r") as iss_script:
-        content = iss_script.read()
-
-        # Replace variables
-        content = content.replace("{{{app_name}}}", package_info.name)
-        content = content.replace("{{{app_version}}}", package_info.version)
-        content = content.replace("{{{app_publisher}}}", package_info.publisher)
-        content = content.replace("{{{app_url}}}", package_info.url)
-        content = content.replace("{{{app_license}}}",  os.path.abspath("LICENSE"))
-        content = content.replace("{{{app_icon}}}",  os.path.abspath("packager/win/icon.ico"))
-        content = content.replace("{{{executable_path}}}",  os.path.abspath("target/release/espanso.exe"))
-        content = content.replace("{{{output_dir}}}",  os.path.abspath(TARGET_DIR))
-        content = content.replace("{{{output_name}}}",  INSTALLER_NAME)
-        content = content.replace("{{{dll_include}}}",  include)
-
-        with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
-            output_script.write(content)
-
-    print("Compiling installer with Inno setup")
-    subprocess.run(["iscc", os.path.abspath(os.path.join(TARGET_DIR, "setupscript.iss"))])
-
-    print("Calculating the SHA256")
-    sha256_hash = hashlib.sha256()
-    with open(os.path.abspath(os.path.join(TARGET_DIR, INSTALLER_NAME+".exe")),"rb") as f:
-        # Read and update hash string value in blocks of 4K
-        for byte_block in iter(lambda: f.read(4096),b""):
-            sha256_hash.update(byte_block)
-
-        hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-win-installer-sha256.txt"))
-        with open(hash_file, "w") as hf:
-            hf.write(sha256_hash.hexdigest())
-
-
-def build_mac(package_info):
-    print("Starting packaging process for MacOS...")
-
-    print("Clearing target dirs")
-
-    # Clearing previous build directory
-    if os.path.isdir(PACKAGER_TARGET_DIR):
-        print("Cleaning packager temp directory...")
-        shutil.rmtree(PACKAGER_TARGET_DIR)
-
-    TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "mac")
-    os.makedirs(TARGET_DIR, exist_ok=True)
-
-    print("Compressing release to archive...")
-    target_name = f"espanso-mac.tar.gz"
-    archive_target = os.path.abspath(os.path.join(TARGET_DIR, target_name))
-    subprocess.run(["tar",
-                    "-C", os.path.abspath("target/release"),
-                    "-cvf",
-                    archive_target,
-                    "espanso",
-                    ])
-    print(f"Created archive: {archive_target}")
-
-    print("Calculating the SHA256")
-    sha256_hash = hashlib.sha256()
-    with open(archive_target,"rb") as f:
-        # Read and update hash string value in blocks of 4K
-        for byte_block in iter(lambda: f.read(4096),b""):
-            sha256_hash.update(byte_block)
-
-        hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-mac-sha256.txt"))
-        with open(hash_file, "w") as hf:
-            hf.write(sha256_hash.hexdigest())
-
-    modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-mac.sha256.txt".format(package_info.modulo_version)
-    print("Pulling SHA signature from:", modulo_sha_url)
-    modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
-    urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
-    modulo_sha = None
-    with open(modulo_sha_file, "r") as sha_f:
-        modulo_sha = sha_f.read()
-    if modulo_sha is None:
-        raise Exception("Cannot determine modulo SHA")
-
-    print("Processing Homebrew formula template")
-    with open("packager/mac/espanso.rb", "r") as formula_template:
-        content = formula_template.read()
-
-        # Replace variables
-        content = content.replace("{{{app_desc}}}", package_info.description)
-        content = content.replace("{{{app_url}}}", package_info.url)
-        content = content.replace("{{{app_version}}}", package_info.version)
-        content = content.replace("{{{modulo_version}}}", package_info.modulo_version)
-        content = content.replace("{{{modulo_sha}}}", modulo_sha)
-
-        # Calculate hash
-        with open(archive_target, "rb") as f:
-            bytes = f.read()
-            readable_hash = hashlib.sha256(bytes).hexdigest()
-            content = content.replace("{{{release_hash}}}", readable_hash)
-
-        with open(os.path.join(TARGET_DIR, "espanso.rb"), "w") as output_script:
-            output_script.write(content)
-
-    print("Done!")
-
-
-if __name__ == '__main__':
-    print("[[ espanso packager ]]")
-
-    # Check python version 3
-    if sys.version_info[0] < 3:
-        raise Exception("Must be using Python 3")
-
-    cli()
\ No newline at end of file
diff --git a/scripts/create_app_image.sh b/scripts/create_app_image.sh
index 4779363a..f3a31c42 100644
--- a/scripts/create_app_image.sh
+++ b/scripts/create_app_image.sh
@@ -8,7 +8,7 @@ BASE_DIR=$(pwd)
 
 mkdir -p $TOOL_DIR
 
-if ls $TOOL_DIR/*.AppImage 1> /dev/null 2>&1; then
+if ls $TOOL_DIR/linuxdeploy*.AppImage 1> /dev/null 2>&1; then
   echo "Skipping download of linuxdeploy"
 else
   echo "Downloading linuxdeploy tool"
@@ -16,12 +16,36 @@ else
   chmod +x $TOOL_DIR/linuxdeploy*.AppImage
 fi
 
+if ls $TOOL_DIR/appimagetool*.AppImage 1> /dev/null 2>&1; then
+  echo "Skipping download of appimagetool"
+else
+  echo "Downloading appimagetool"
+  wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -P "$TOOL_DIR"
+  chmod +x $TOOL_DIR/appimagetool*.AppImage
+fi
+
 rm -Rf "$TARGET_DIR"
 mkdir -p $OUTPUT_DIR
 mkdir -p $BUILD_DIR
 
 echo Building AppImage into $OUTPUT_DIR
 pushd $OUTPUT_DIR
-$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" -i "$BASE_DIR/espanso/src/res/linux/icon.png" --appdir $BUILD_DIR --output appimage
+$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" \
+  -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" \
+  -i "$BASE_DIR/espanso/src/res/linux/icon.png" \
+  --appdir $BUILD_DIR \
+  --output appimage
 chmod +x ./Espanso*.AppImage
+
+# Apply a workaround to fix this issue: https://github.com/federico-terzi/espanso/issues/900
+# See: https://github.com/project-slippi/Ishiiruka/issues/323#issuecomment-977415376
+
+echo "Applying patch for libgmodule"
+
+./Espanso*.AppImage --appimage-extract
+rm -Rf ./Espanso*.AppImage
+rm -Rf squashfs-root/usr/lib/libgmodule*
+$TOOL_DIR/appimagetool*.AppImage --appimage-extract-and-run -v squashfs-root
+rm -Rf squashfs-root
+
 popd
\ No newline at end of file
diff --git a/snap/hooks/remove b/snap/hooks/remove
new file mode 100755
index 00000000..5f85fecc
--- /dev/null
+++ b/snap/hooks/remove
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+echo "Stopping Espanso..."
+killall espanso
+
+# Here I've also tried to unregister the Systemd service, but couldn't manage to. 
+# The remove hook is run as Root, but the systemd service is registered as a user.
+# I couldn't find any (working) way to unregister a user systemd service as root.
+# I've also tried these solutions: https://unix.stackexchange.com/questions/552922/stop-systemd-user-services-as-root-user
+# but none seemed to work in this case.
+# If you manage to find a way to do so, feel free to open an issue so that
+# we can improve the hook! Thanks
\ No newline at end of file
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 8f5437cc..c69662a5 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
 name: espanso
-version: 2.1.1-alpha
+version: 2.1.2-alpha
 summary: A Cross-platform Text Expander written in Rust
 description: |
   espanso is a Cross-platform, Text Expander written in Rust.


Test patch: