Repository: espanso/espanso (language: rust)

Issue:
Add option to lower or disable the Windows keyboard layout cache invalidation interval
Tested on several machines - same behaviour/
When I switch to any input field triggers doesn't work right away, I have to type some random characters first and delete them, only then trigger words expands normally. 

Any advice how to diagnose/fix this ?
I use your extension in my work, with several colleagues and it's very important.
Thanks a lot for your work !
Wrong expansions when using Alacritty on Linux
OS: Ubuntu 20.04
Version: snap package, 0.7.3

When matching in my terminal (`alacritty`, with both `zsh` and `bash`), once in a while the replace text output comes in the wrong order. Examples I've seen:

`:espanso` -> `Hi t!here`
`:espanso` -> `!eHi ther`
`:espanso` -> `Hi thereespanso detectHi there there` (Note, `espanso detect` was the most recent command, so there was a scroll up in my shell history)

I am not able to reproduce the issue outside of the terminal (eg, in this text box in my browser it works right 100%).


Discussion / hints:
Hey @grenudi,

Thanks for reporting this issue! Could you post an example of these problematic triggers? I'm particularly curious to see if you are using the `word: true` option

Cheers :)

> Hey @grenudi,
> 
> Thanks for reporting this issue! Could you post an example of these problematic triggers? I'm particularly curious to see if you are using the `word: true` option
> 
> Cheers :)

For some reason it works like a charm when I try to record it... 

No, I do not use `word: true` option in any trigger, all my triggers start and end with same symbols, for example `-test-` , `.test.` , also option `backend: Clipboard` have no effect either. 

This doesn't depend on usage of  `type: "clipboard"` option as well, though it is used in the demo bellow, nor does it bound to `html`, nor `markdown`, nor `replace` options.

Video demo of  `-зак-` trigger
---

https://user-images.githubusercontent.com/17498075/131931355-d6857fde-2ec9-49c2-a0a9-6137955b9223.mp4

```
  - triggers: ["-закз-"]
    html: |+
      <p>
      <strong>Запрос закрыт согласно комментарию заявителя:</strong>
      <blockquote>{{cb}}</blockquote>
      {{clo}}
      </p>
    vars:
      - name: "cb"
        type: "clipboard"
      - name: clo
        type: match
        params:
          trigger: "-зак-"
```

All contents of file `cloacu--close-according-to-users-reply.yml`
---

```
parent: default

# Mnemonic   : XXX
# Description: XXX
# Data Source: XXX
# TODO: XXX

matches:
# ENGLISH
  # - triggers: ["  "]
  #   replace: |+

  # - triggers: [".."]
  #   markdown: |+

  - triggers: ["-cloacu-"]
    html: |+
      <p>
      <strong>[Closed according to requesters reply]</strong>
      <blockquote>{{cb}}</blockquote>
      {{zak}}
      </p>
    vars:
      - name: "cb"
        type: "clipboard"
      - name: zak
        type: match
        params:
          trigger: "-clo-"

# RUSSIAN
  # - triggers: ["  "]
  #   replace: |+

  # - triggers: [".."]
  #   markdown: |+

  - triggers: ["-закз-"]
    html: |+
      <p>
      <strong>Запрос закрыт согласно комментарию заявителя:</strong>
      <blockquote>{{cb}}</blockquote>
      {{clo}}
      </p>
    vars:
      - name: "cb"
        type: "clipboard"
      - name: clo
        type: match
        params:
          trigger: "-зак-"

# You can refer to the official documentation:
# https://espanso.org/docs/
```

FILE `default.yml`
---

```
# espanso configuration file

#### OPTIONS #### 
# https://espanso.org/docs/configuration/#options

backend: Clipboard

matches:
  # Testing
  - trigger: "  test"
    replace: "THIS IS TEST SO STFU test"

#YAML CHEATSHEET:
#                       >     |            "     '     >-     >+     |-     |+
# -------------------------|------|-----|-----|-----|------|------|------|------  
# Trailing spaces   | Kept | Kept |     |     |     | Kept | Kept | Kept | Kept
# Single newline => | _    | \n   | _   | _   | _   | _    |  _   | \n   | \n
# Double newline => | \n   | \n\n | \n  | \n  | \n  | \n   |  \n  | \n\n | \n\n
# Final newline  => | \n   | \n   |     |     |     |      |  \n  |      | \n
# Final dbl nl's => |      |      |     |     |     |      | Kept |      | Kept  
# In-line newlines  | No   | No   | No  | \n  | No  | No   | No   | No   | No
# Spaceless newlines| No   | No   | No  | \   | No  | No   | No   | No   | No 
# Single quote      | '    | '    | '   | '   | ''  | '    | '    | '    | '
# Double quote      | "    | "    | "   | \"  | "   | "    | "    | "    | "
# Backslash         | \    | \    | \   | \\  | \   | \    | \    | \    | \
# " #", ": "        | Ok   | Ok   | No  | Ok  | Ok  | Ok   | Ok   | Ok   | Ok
# Can start on same | No   | No   | Yes | Yes | Yes | No   | No   | No   | No
# line as key       |

tmp:
  filesharing: |+
    - Для данных задач вы можете использовать https://web.microsoftstream.com/ , при записи вебинара в тимс он автоматически сохраняется в MS Stream
    - В ручную можно делиться любыми файлами через https://nxcloud.softline.com/ (по умолчанию срок хранения 14 дней, продлевается по запросу или "перезаливом" файла).
    - Корпоративный OneDrive

    [
    Инструкция по работе с файлами: https://wiki.softline.com/pages/viewpage.action?pageId=693207842
    Инструкция по работе с OneDrive: https://wiki.softline.com/pages/viewpage.action?pageId=693207842
    ]
```

I think I've found the reason behind it.. This is due to keyboard layout switch, and at work I change layout many times per minute.
Here is a demo:

https://user-images.githubusercontent.com/17498075/131933320-43a398f9-3f93-415c-a722-2b685c06818a.mp4


Hey @grenudi,

Thank you so much for the detailed report! Indeed, there is a keyboard layout "cache" on Windows (see: https://github.com/federico-terzi/espanso/blob/master/native/libwinbridge/bridge.cpp#L47 and https://github.com/federico-terzi/espanso/blob/master/native/libwinbridge/bridge.cpp#L238) currently set to 2 seconds. This causes espanso to "miss" some characters if the layout change occurs soon before.

I'd say we might want to introduce a configurable option to either lower the cache invalidation time or disable it entirely. I'll convert this issue to a feature request!

Cheers :)

@federico-terzi  can you please advice how can I deal with it at the moment ? 
@grenudi The easiest way would be to wait for #594 to be released (hopefully by the end of September). At that point, we should be able to ship this feature as well.
Otherwise, you might try compiling espanso yourself after changing that value, though it's not trivial on Windows currently (the upcoming version will be much easier).

I'm really sorry I can't offer you anything sooner, but our limited resources are all dedicated to shipping the upcoming release :) After that, we'll move to "bug fixing" mode, including this one

Cheers :) 
Hey @YakBarber,

I'm sorry you experienced this problem! 
~~I've seen this happening when multiple processes are running at the same time. Could you please try running `killall espanso && espanso start` and see if that helps?~~

~~Keep in mind that 2 processes are expected in version 0.7.3, so don't worry if you see two processes running :)
(for future readers, the new v2 uses 3 processes by default)~~

Edit: I've just realised that this is unlikely to be the cause if it's happening only in the terminal, perhaps there is some interference with alacritty. Does that happen also with other terminals?

Cheers :)

I tried the same test in another terminal (Kitty) and got a different behavior. In Kitty it reliably acts as though the "shift key" wasn't added, meaning the output is: `hi there1`

EDIT: I also tried your struck comment about processes, and as you expected the problem persists.
@YakBarber Interesting, thank you for trying! I'll investigate :)
No Problem. I'm familiar with Rust and its toolchain, so if my testing anything deeper would be helpful, just let me know. If I can find some time to go spelunking I will, but it's unlikely.
Sounds good! Although It's likely we'll have to fiddle with Espanso parameters rather than Rust code :)

The way we'll probably handle it is by creating a specific patch for it and bundle it into Espanso itself 

Fix patch:
diff --git a/.github/scripts/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile
index 6fbba6e7..b3c7eee9 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
+RUN mkdir espanso && cargo install --force cargo-make --version 0.34.0
 
 COPY . espanso
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 519e97f1..b8810356 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,7 +35,7 @@ jobs:
           MACOSX_DEPLOYMENT_TARGET: "10.13"
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary
       - name: Build
@@ -60,7 +60,7 @@ jobs:
           cargo clippy -p espanso --features wayland -- -D warnings
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary --env NO_X11=true
       - name: Build
@@ -74,7 +74,7 @@ jobs:
         run: rustup update && rustup target add aarch64-apple-darwin
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Build
         run: |
           cargo make build-macos-arm-binary
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d41c8b91..9674a1f1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
           echo Using version ${{ needs.extract-version.outputs.espanso_version }}
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Test
         run: cargo make test-binary --profile release
       - name: Build
@@ -128,7 +128,7 @@ jobs:
           echo Using version ${{ needs.extract-version.outputs.espanso_version }}
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Test
         run: cargo make test-binary --profile release
         env:
@@ -168,7 +168,7 @@ jobs:
         run: rustup update && rustup target add aarch64-apple-darwin
       - name: Install cargo-make
         run: |
-          cargo install --force cargo-make
+          cargo install --force cargo-make --version 0.34.0
       - name: Build
         run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
       - name: Codesign executable
diff --git a/Cargo.lock b/Cargo.lock
index 0c06a1c8..716e52ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -572,7 +572,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "2.0.3-alpha"
+version = "2.0.4-alpha"
 dependencies = [
  "anyhow",
  "caps",
diff --git a/Compilation.md b/Compilation.md
index 8b936ed1..860ffac4 100644
--- a/Compilation.md
+++ b/Compilation.md
@@ -16,7 +16,7 @@ These are the basic tools required to build espanso:
 steps. You can install it by running:
 
 ```
-cargo install --force cargo-make
+cargo install --force cargo-make --version 0.34.0
 ```
 
 # Linux
diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs
index d3d12dac..dd889298 100644
--- a/espanso-config/src/config/mod.rs
+++ b/espanso-config/src/config/mod.rs
@@ -156,6 +156,12 @@ pub trait Config: Send + Sync {
   // Disabling this option might conflict with the undo feature.
   fn win32_exclude_orphan_events(&self) -> bool;
 
+  // The maximum interval (in milliseconds) for which a keyboard layout
+  // can be cached. If switching often between different layouts, you
+  // could lower this amount to avoid the "lost detection" effect described
+  // in this issue: https://github.com/federico-terzi/espanso/issues/745
+  fn win32_keyboard_layout_cache_interval(&self) -> i64;
+
   fn is_match<'a>(&self, app: &AppProperties<'a>) -> bool;
 
   fn pretty_dump(&self) -> String {
@@ -187,6 +193,9 @@ pub trait Config: Send + Sync {
         show_notifications: {:?}
         secure_input_notification: {:?}
 
+        win32_exclude_orphan_events: {:?}
+        win32_keyboard_layout_cache_interval: {:?}
+
         match_paths: {:#?}
       ", 
       self.label(),
@@ -215,6 +224,9 @@ pub trait Config: Send + Sync {
       self.show_notifications(),
       self.secure_input_notification(),
 
+      self.win32_exclude_orphan_events(),
+      self.win32_keyboard_layout_cache_interval(),
+
       self.match_paths(),
     }
   }
diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs
index 134afc0f..bd39ad4f 100644
--- a/espanso-config/src/config/parse/mod.rs
+++ b/espanso-config/src/config/parse/mod.rs
@@ -45,6 +45,7 @@ pub(crate) struct ParsedConfig {
   pub show_icon: Option<bool>,
   pub secure_input_notification: Option<bool>,
   pub win32_exclude_orphan_events: Option<bool>,
+  pub win32_keyboard_layout_cache_interval: Option<i64>,
 
   pub pre_paste_delay: Option<usize>,
   pub restore_clipboard_delay: Option<usize>,
diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs
index 81e884d1..824429c1 100644
--- a/espanso-config/src/config/parse/yaml.rs
+++ b/espanso-config/src/config/parse/yaml.rs
@@ -109,6 +109,9 @@ pub(crate) struct YAMLConfig {
   #[serde(default)]
   pub win32_exclude_orphan_events: Option<bool>,
 
+  #[serde(default)]
+  pub win32_keyboard_layout_cache_interval: Option<i64>,
+
   // Include/Exclude
   #[serde(default)]
   pub includes: Option<Vec<String>>,
@@ -197,6 +200,7 @@ impl TryFrom<YAMLConfig> for ParsedConfig {
       paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay,
 
       win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
+      win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
 
       use_standard_includes: yaml_config.use_standard_includes,
       includes: yaml_config.includes,
@@ -253,6 +257,7 @@ mod tests {
     show_notifications: false
     secure_input_notification: false
     win32_exclude_orphan_events: false
+    win32_keyboard_layout_cache_interval: 300
       
     use_standard_includes: true
     includes: ["test1"]
@@ -305,6 +310,7 @@ mod tests {
         show_notifications: Some(false),
         secure_input_notification: Some(false),
         win32_exclude_orphan_events: Some(false),
+        win32_keyboard_layout_cache_interval: Some(300),
 
         pre_paste_delay: Some(300),
         evdev_modifier_delay: Some(40),
diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs
index ea74240d..afcad9be 100644
--- a/espanso-config/src/config/resolve.rs
+++ b/espanso-config/src/config/resolve.rs
@@ -323,6 +323,13 @@ impl Config for ResolvedConfig {
   fn evdev_modifier_delay(&self) -> Option<usize> {
     self.parsed.evdev_modifier_delay
   }
+
+  fn win32_keyboard_layout_cache_interval(&self) -> i64 {
+    self
+      .parsed
+      .win32_keyboard_layout_cache_interval
+      .unwrap_or(2000)
+  }
 }
 
 impl ResolvedConfig {
@@ -405,6 +412,7 @@ impl ResolvedConfig {
       show_notifications,
       secure_input_notification,
       win32_exclude_orphan_events,
+      win32_keyboard_layout_cache_interval,
       includes,
       excludes,
       extra_includes,
diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs
index 4efbb569..674b08a9 100644
--- a/espanso-config/src/legacy/mod.rs
+++ b/espanso-config/src/legacy/mod.rs
@@ -394,6 +394,10 @@ impl Config for LegacyInteropConfig {
   fn evdev_modifier_delay(&self) -> Option<usize> {
     Some(10)
   }
+
+  fn win32_keyboard_layout_cache_interval(&self) -> i64 {
+    2000
+  }
 }
 
 struct LegacyMatchGroup {
diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs
index 2dd0b25f..818578dd 100644
--- a/espanso-detect/src/evdev/device.rs
+++ b/espanso-detect/src/evdev/device.rs
@@ -6,6 +6,7 @@ use libc::{input_event, size_t, ssize_t, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_R
 use log::trace;
 use scopeguard::ScopeGuard;
 use std::collections::HashMap;
+use std::os::raw::c_char;
 use std::os::unix::io::AsRawFd;
 use std::{
   ffi::{c_void, CStr},
@@ -160,16 +161,16 @@ impl Device {
     }
 
     // Extract the utf8 char
-    let mut buffer: [u8; 16] = [0; 16];
+    let mut buffer: [c_char; 16] = [0; 16];
     unsafe {
       xkb_state_key_get_utf8(
         self.get_state(),
         keycode,
-        buffer.as_mut_ptr() as *mut i8,
+        buffer.as_mut_ptr(),
         std::mem::size_of_val(&buffer),
       )
     };
-    let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) };
+    let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr()) };
     let content = content_raw.to_string_lossy().to_string();
 
     let event = RawKeyboardEvent {
diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs
index 393b40ec..6d210ed9 100644
--- a/espanso-detect/src/lib.rs
+++ b/espanso-detect/src/lib.rs
@@ -66,6 +66,12 @@ pub struct SourceCreationOptions {
   // those from espanso, but might need to be disabled when using some software-level keyboards.
   // Disabling this option might conflict with the undo feature.
   pub win32_exclude_orphan_events: bool,
+
+  // The maximum interval (in milliseconds) for which a keyboard layout
+  // can be cached. If switching often between different layouts, you
+  // could lower this amount to avoid the "lost detection" effect described
+  // in this issue: https://github.com/federico-terzi/espanso/issues/745
+  pub win32_keyboard_layout_cache_interval: i64,
 }
 
 // This struct identifies the keyboard layout that
@@ -87,6 +93,7 @@ impl Default for SourceCreationOptions {
       evdev_keyboard_rmlvo: None,
       hotkeys: Vec::new(),
       win32_exclude_orphan_events: true,
+      win32_keyboard_layout_cache_interval: 2000,
     }
   }
 }
@@ -97,6 +104,7 @@ pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
   Ok(Box::new(win32::Win32Source::new(
     &options.hotkeys,
     options.win32_exclude_orphan_events,
+    options.win32_keyboard_layout_cache_interval,
   )))
 }
 
diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs
index d25e5284..dbde5731 100644
--- a/espanso-detect/src/win32/mod.rs
+++ b/espanso-detect/src/win32/mod.rs
@@ -17,6 +17,7 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use std::os::raw::c_long;
 use std::{convert::TryInto, ffi::c_void};
 
 use lazycell::LazyCell;
@@ -79,10 +80,19 @@ pub struct RawHotKey {
   pub flags: u32,
 }
 
+#[repr(C)]
+pub struct InitOptions {
+  pub keyboard_layout_cache_interval: c_long,
+}
+
 #[allow(improper_ctypes)]
 #[link(name = "espansodetect", kind = "static")]
 extern "C" {
-  pub fn detect_initialize(_self: *const Win32Source, error_code: *mut i32) -> *mut c_void;
+  pub fn detect_initialize(
+    _self: *const Win32Source,
+    options: *const InitOptions,
+    error_code: *mut i32,
+  ) -> *mut c_void;
   pub fn detect_register_hotkey(window: *const c_void, hotkey: RawHotKey) -> i32;
 
   pub fn detect_eventloop(
@@ -99,24 +109,35 @@ pub struct Win32Source {
   hotkeys: Vec<HotKey>,
 
   exclude_orphan_events: bool,
+  keyboard_layout_cache_interval: i64,
 }
 
 #[allow(clippy::new_without_default)]
 impl Win32Source {
-  pub fn new(hotkeys: &[HotKey], exclude_orphan_events: bool) -> Win32Source {
+  pub fn new(
+    hotkeys: &[HotKey],
+    exclude_orphan_events: bool,
+    keyboard_layout_cache_interval: i64,
+  ) -> Win32Source {
     Self {
       handle: std::ptr::null_mut(),
       callback: LazyCell::new(),
       hotkeys: hotkeys.to_vec(),
       exclude_orphan_events,
+      keyboard_layout_cache_interval,
     }
   }
 }
 
 impl Source for Win32Source {
   fn initialize(&mut self) -> Result<()> {
+    let options = InitOptions {
+      keyboard_layout_cache_interval: self.keyboard_layout_cache_interval.try_into().unwrap(),
+    };
+
     let mut error_code = 0;
-    let handle = unsafe { detect_initialize(self as *const Win32Source, &mut error_code) };
+    let handle =
+      unsafe { detect_initialize(self as *const Win32Source, &options, &mut error_code) };
 
     if handle.is_null() {
       let error = match error_code {
diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp
index 1ac9d417..c54c3965 100644
--- a/espanso-detect/src/win32/native.cpp
+++ b/espanso-detect/src/win32/native.cpp
@@ -39,8 +39,6 @@
 #include <strsafe.h>
 #include <Windows.h>
 
-// How many milliseconds must pass between events before refreshing the keyboard layout
-const long DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL = 2000;
 const wchar_t *const DETECT_WINCLASS = L"EspansoDetect";
 const USHORT MOUSE_DOWN_FLAGS = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
                         RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN |
@@ -52,6 +50,8 @@ const USHORT MOUSE_UP_FLAGS = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP
 typedef struct {
   HKL current_keyboard_layout;
   DWORD last_key_press_tick;
+  // How many milliseconds must pass between events before refreshing the keyboard layout
+  long keyboard_layout_cache_interval;
 
   // Rust interop
   void * rust_instance;
@@ -130,7 +130,7 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
       DWORD currentTick = GetTickCount();
 
       // If enough time has passed between the last keypress and now, refresh the keyboard layout
-      if ((currentTick - variables->last_key_press_tick) > DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL)
+      if ((currentTick - variables->last_key_press_tick) > variables->keyboard_layout_cache_interval)
       {
 
         // Because keyboard layouts on windows are Window-specific, to get the current
@@ -269,7 +269,7 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
   }
 }
 
-void * detect_initialize(void *_self, int32_t *error_code)
+void * detect_initialize(void *_self, InitOptions * options, int32_t *error_code)
 {
   HWND window = NULL;
 
@@ -297,6 +297,7 @@ void * detect_initialize(void *_self, int32_t *error_code)
 
     // Initialize the default keyboard layout
     variables->current_keyboard_layout = GetKeyboardLayout(0);
+    variables->keyboard_layout_cache_interval = options->keyboard_layout_cache_interval;
 
     // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
     window = CreateWindowEx(
diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h
index a93b9f79..7e187d3a 100644
--- a/espanso-detect/src/win32/native.h
+++ b/espanso-detect/src/win32/native.h
@@ -76,9 +76,12 @@ typedef struct {
 
 typedef void (*EventCallback)(void * rust_istance, InputEvent data);
 
+typedef struct {
+  long keyboard_layout_cache_interval;
+} InitOptions;
 
 // Initialize the Raw Input API and the Window.
-extern "C" void * detect_initialize(void * rust_istance, int32_t *error_code);
+extern "C" void * detect_initialize(void * rust_istance, InitOptions * options, int32_t *error_code);
 
 // Register the given hotkey, return a non-zero code if successful
 extern "C" int32_t detect_register_hotkey(void * window, HotKey hotkey);
diff --git a/espanso-engine/src/process/middleware/disable.rs b/espanso-engine/src/process/middleware/disable.rs
index 77bd0e80..5b7e633d 100644
--- a/espanso-engine/src/process/middleware/disable.rs
+++ b/espanso-engine/src/process/middleware/disable.rs
@@ -64,18 +64,24 @@ impl Middleware for DisableMiddleware {
 
     match &event.etype {
       EventType::Keyboard(m_event) => {
-        if is_toggle_key(m_event, &self.options) {
+        if m_event.status == Status::Released {
           let mut last_toggle_press = self.last_toggle_press.borrow_mut();
-          if let Some(previous_press) = *last_toggle_press {
-            if previous_press.elapsed() < self.options.toggle_key_maximum_window {
-              *enabled = !*enabled;
-              *last_toggle_press = None;
-              has_status_changed = true;
+          if is_toggle_key(m_event, &self.options) {
+            if let Some(previous_press) = *last_toggle_press {
+              if previous_press.elapsed() < self.options.toggle_key_maximum_window {
+                *enabled = !*enabled;
+                *last_toggle_press = None;
+                has_status_changed = true;
+              } else {
+                *last_toggle_press = Some(Instant::now());
+              }
             } else {
               *last_toggle_press = Some(Instant::now());
             }
           } else {
-            *last_toggle_press = Some(Instant::now());
+            // If another key is pressed (not the toggle key), we should reset the window
+            // For more information, see: https://github.com/federico-terzi/espanso/issues/815
+            *last_toggle_press = None;
           }
         }
       }
@@ -115,10 +121,6 @@ impl Middleware for DisableMiddleware {
 }
 
 fn is_toggle_key(event: &KeyboardEvent, options: &DisableOptions) -> bool {
-  if event.status != Status::Released {
-    return false;
-  }
-
   if options
     .toggle_key
     .as_ref()
diff --git a/espanso-engine/src/process/middleware/markdown.rs b/espanso-engine/src/process/middleware/markdown.rs
index abebc54c..7deec855 100644
--- a/espanso-engine/src/process/middleware/markdown.rs
+++ b/espanso-engine/src/process/middleware/markdown.rs
@@ -17,6 +17,8 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use log::error;
+
 use super::super::Middleware;
 use crate::event::{effect::HtmlInjectRequest, Event, EventType};
 
@@ -37,23 +39,33 @@ impl Middleware for MarkdownMiddleware {
   fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
     if let EventType::MarkdownInject(m_event) = &event.etype {
       // Render the markdown into HTML
-      let html = markdown::to_html(&m_event.markdown);
-      let mut html = html.trim();
+      // NOTE: we wrap the `to_html` call between catch_unwind because if the markdown is malformed,
+      // the library panics. Ideally, the library would return a Result::Err in that case, but
+      // for now it doesn't, so we employ that workaround.
+      // See also: https://github.com/federico-terzi/espanso/issues/759
+      let html = std::panic::catch_unwind(|| markdown::to_html(&m_event.markdown));
+      if let Ok(html) = html {
+        let mut html = html.trim();
 
-      // Remove the surrounding paragraph
-      if html.starts_with("<p>") {
-        html = html.trim_start_matches("<p>");
-      }
-      if html.ends_with("</p>") {
-        html = html.trim_end_matches("</p>");
-      }
+        // Remove the surrounding paragraph
+        if html.starts_with("<p>") {
+          html = html.trim_start_matches("<p>");
+        }
+        if html.ends_with("</p>") {
+          html = html.trim_end_matches("</p>");
+        }
 
-      return Event::caused_by(
-        event.source_id,
-        EventType::HtmlInject(HtmlInjectRequest {
-          html: html.to_owned(),
-        }),
-      );
+        return Event::caused_by(
+          event.source_id,
+          EventType::HtmlInject(HtmlInjectRequest {
+            html: html.to_owned(),
+          }),
+        );
+      } else {
+        error!("unable to convert markdown to HTML, is it malformed?");
+
+        return Event::caused_by(event.source_id, EventType::NOOP);
+      }
     }
 
     event
diff --git a/espanso-engine/src/process/middleware/matcher.rs b/espanso-engine/src/process/middleware/matcher.rs
index 92f49ea5..d16276df 100644
--- a/espanso-engine/src/process/middleware/matcher.rs
+++ b/espanso-engine/src/process/middleware/matcher.rs
@@ -191,7 +191,7 @@ fn is_event_of_interest(event_type: &EventType) -> bool {
         // 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 let (Key::Other(raw_code), None) = (&keyboard_event.key, &keyboard_event.value) {
             if (65025..=65276).contains(raw_code) {
               return false;
             }
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
index 31646cab..86bc21d7 100644
--- a/espanso-modulo/build.rs
+++ b/espanso-modulo/build.rs
@@ -457,22 +457,5 @@ 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/Cargo.toml b/espanso/Cargo.toml
index 60cd2cff..a31cae3c 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "2.0.3-alpha"
+version = "2.0.4-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/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index 249db134..f68309ad 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -126,6 +126,8 @@ pub fn initialize_and_spawn(
           ),
           hotkeys: match_converter.get_hotkeys(),
           win32_exclude_orphan_events: default_config.win32_exclude_orphan_events(),
+          win32_keyboard_layout_cache_interval: default_config
+            .win32_keyboard_layout_cache_interval(),
         })
         .expect("failed to initialize detector module");
       let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs
index dd9fbe76..fb0510b4 100644
--- a/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs
@@ -55,55 +55,53 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
     let mut form_field = None;
 
     if let Value::Object(params) = field {
-      if let Some(Value::String(field_type)) = params.get("type") {
-        form_field = match field_type.as_str() {
-          "text" => Some(FormField::Text {
-            default: params
-              .get("default")
-              .and_then(|val| val.as_string())
-              .cloned(),
-            multiline: params
-              .get("multiline")
-              .and_then(|val| val.as_bool())
-              .cloned()
-              .unwrap_or(false),
-          }),
-          "choice" => Some(FormField::Choice {
-            default: params
-              .get("default")
-              .and_then(|val| val.as_string())
-              .cloned(),
-            values: params
-              .get("values")
-              .and_then(|val| val.as_array())
-              .map(|arr| {
-                arr
-                  .iter()
-                  .flat_map(|choice| choice.as_string())
-                  .cloned()
-                  .collect()
-              })
-              .unwrap_or_default(),
-          }),
-          "list" => Some(FormField::List {
-            default: params
-              .get("default")
-              .and_then(|val| val.as_string())
-              .cloned(),
-            values: params
-              .get("values")
-              .and_then(|val| val.as_array())
-              .map(|arr| {
-                arr
-                  .iter()
-                  .flat_map(|choice| choice.as_string())
-                  .cloned()
-                  .collect()
-              })
-              .unwrap_or_default(),
-          }),
-          _ => None,
-        }
+      form_field = match params.get("type") {
+        Some(Value::String(field_type)) if field_type == "choice" => Some(FormField::Choice {
+          default: params
+            .get("default")
+            .and_then(|val| val.as_string())
+            .cloned(),
+          values: params
+            .get("values")
+            .and_then(|val| val.as_array())
+            .map(|arr| {
+              arr
+                .iter()
+                .flat_map(|choice| choice.as_string())
+                .cloned()
+                .collect()
+            })
+            .unwrap_or_default(),
+        }),
+        Some(Value::String(field_type)) if field_type == "list" => Some(FormField::List {
+          default: params
+            .get("default")
+            .and_then(|val| val.as_string())
+            .cloned(),
+          values: params
+            .get("values")
+            .and_then(|val| val.as_array())
+            .map(|arr| {
+              arr
+                .iter()
+                .flat_map(|choice| choice.as_string())
+                .cloned()
+                .collect()
+            })
+            .unwrap_or_default(),
+        }),
+        // By default, it's considered type 'text'
+        _ => Some(FormField::Text {
+          default: params
+            .get("default")
+            .and_then(|val| val.as_string())
+            .cloned(),
+          multiline: params
+            .get("multiline")
+            .and_then(|val| val.as_bool())
+            .cloned()
+            .unwrap_or(false),
+        }),
       }
     }
 
diff --git a/espanso/src/patch/mod.rs b/espanso/src/patch/mod.rs
index e4991817..f1ce634c 100644
--- a/espanso/src/patch/mod.rs
+++ b/espanso/src/patch/mod.rs
@@ -55,6 +55,7 @@ fn get_builtin_patches() -> Vec<PatchDefinition> {
     patches::linux::urxvt_terminal_x11::patch(),
     patches::linux::xterm_terminal_x11::patch(),
     patches::linux::yakuake_terminal_x11::patch(),
+    patches::linux::virtualbox_x11::patch(),
   ];
 }
 
diff --git a/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs b/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs
index f02ea347..039d15ca 100644
--- a/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs
+++ b/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs
@@ -19,6 +19,8 @@
 
 use std::sync::Arc;
 
+use espanso_config::config::Backend;
+
 use crate::patch::patches::{PatchedConfig, Patches};
 use crate::patch::PatchDefinition;
 
@@ -33,6 +35,7 @@ pub fn patch() -> PatchDefinition {
         name,
         Patches {
           paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          backend: Some(Backend::Clipboard),
           ..Default::default()
         },
       ))
diff --git a/espanso/src/patch/patches/linux/mod.rs b/espanso/src/patch/patches/linux/mod.rs
index d145f160..3d420119 100644
--- a/espanso/src/patch/patches/linux/mod.rs
+++ b/espanso/src/patch/patches/linux/mod.rs
@@ -30,6 +30,7 @@ pub mod termite_terminal_x11;
 pub mod thunderbird_x11;
 pub mod tilix_terminal_x11;
 pub mod urxvt_terminal_x11;
+pub mod virtualbox_x11;
 pub mod xterm_terminal_x11;
 pub mod yakuake_terminal_x11;
 
diff --git a/espanso/src/patch/patches/linux/virtualbox_x11.rs b/espanso/src/patch/patches/linux/virtualbox_x11.rs
new file mode 100644
index 00000000..9db735fc
--- /dev/null
+++ b/espanso/src/patch/patches/linux/virtualbox_x11.rs
@@ -0,0 +1,46 @@
+/*
+ * 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 espanso_config::config::Backend;
+
+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 = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("VirtualBox Machine"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          backend: Some(Backend::Inject),
+          key_delay: Some(Some(10)),
+          inject_delay: Some(Some(15)),
+          disable_x11_fast_inject: Some(true),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs
index 5fddc89f..c6dd4f71 100644
--- a/espanso/src/patch/patches/mod.rs
+++ b/espanso/src/patch/patches/mod.rs
@@ -49,5 +49,6 @@ generate_patchable_config!(
   apply_patch -> bool,
   undo_backspace -> bool,
   win32_exclude_orphan_events -> bool,
+  win32_keyboard_layout_cache_interval -> i64,
   keyboard_layout -> Option<RMLVOConfig>
 );
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 51f22b01..92a5f118 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
 name: espanso
-version: 2.0.3-alpha
+version: 2.0.4-alpha
 summary: A Cross-platform Text Expander written in Rust
 description: |
   espanso is a Cross-platform, Text Expander written in Rust.


Test patch: