Repository: espanso/espanso (language: rust)

Issue:
local date formats
Hi and thank you alot for espanso.

I am using espanso under Windows 10 and i would like to locale name of the day stored in %A variable into slovak language.

Is it possible?


Discussion / hints:
Hey @tiborepcek,
Thanks for the kind words. At the moment, the Date Extension is based on the [chrono](https://docs.rs/chrono/0.3.1/chrono/format/strftime/index.html) Rust crate, which doesn't seem to support localized names.
That said, it should be relatively easy to build a workaround using the [Script Extension](https://espanso.org/docs/matches/#script-extension). For example, if you have python installed you could add the match:

```yaml
  - trigger: ":sdate"
    replace: "{{output}}"
    vars:
      - name: output
        type: script
        params:
          args:
            - python
            - /home/freddy/Documents/dayoftheweek.py
```
Which will execute the `dayoftheweek.py` script, with the following content:

```python
import datetime
days = {
    0: "Pondelok",
    1: "Utorok",
    2: "Streda",
    3: "Štvrtok",
    4: "Piatok",
    5: "Sobota",
    6: "Nedeľa",
}
weekday = datetime.datetime.today().weekday()
print(days[weekday])
```
Thank you for your solution, it works. :)

But i also need a way without Python. Can i somehow do it?
You're welcome :)

> But i also need a way without Python. Can i somehow do it?
At the moment it is not possible, but I don't exclude the possibility to add this feature in the future. Keep following this issue if you want to stay updated.

Cheers :)

> Hey @tiborepcek,
> Thanks for the kind words. At the moment, the Date Extension is based on the [chrono](https://docs.rs/chrono/0.3.1/chrono/format/strftime/index.html) Rust crate, which doesn't seem to support localized names.
> That said, it should be relatively easy to build a workaround using the [Script Extension](https://espanso.org/docs/matches/#script-extension). For example, if you have python installed you could add the match:
> 
> ```yaml
>   - trigger: ":sdate"
>     replace: "{{output}}"
>     vars:
>       - name: output
>         type: script
>         params:
>           args:
>             - python
>             - /home/freddy/Documents/dayoftheweek.py
> ```
> 
> Which will execute the `dayoftheweek.py` script, with the following content:
> 
> ```python
> import datetime
> days = {
>     0: "Pondelok",
>     1: "Utorok",
>     2: "Streda",
>     3: "Štvrtok",
>     4: "Piatok",
>     5: "Sobota",
>     6: "Nedeľa",
> }
> weekday = datetime.datetime.today().weekday()
> print(days[weekday])
> ```

Hi federico,
great post and great answer.
What do i have to do to make this script run in a windows environment too without duplicate this script for a windows environment?

The thing one had to modify will probably be this part:

>           args:
>             - python
>             - /home/freddy/Documents/dayoftheweek.py
> ```

The Unix-Path should be replaced by a Windows specific Filepath in the case we start this script in a windows environment.

Regards.
Alexander
Hey @Aladin1001,
Your point is definitely valid, I never thought about the possibility of having cross-platform paths but I see how it can be useful.
I've opened an issue for that purpose: #265

Cheers :)
For anyone interested in helping the project, this is a great first issue!
In particular, we need a way to render strftime with localized strings in Rust, as done in this JS [library](https://github.com/samsonjs/strftime). If any of you could create a Rust crate with that functionality, we could bring the project on step forward :)
@federico-terzi  ho visto che la libreria ha delle localizzazioni "personalizzabili", nel senso che si possono ovviamente e giustamente aggiungere dei locale oltre a quelli già implementati. In RUST quale è la best practice per la gestione di questo tipo di situazioni?  
@ProfAndreaPollini better to switch to english :+1: 

I wonder if `const`-generics could be used to create the configurations, similar to what it's described here: https://www.reddit.com/r/rust/comments/fvciq3/state_machines_with_constgenerics/ 
Hey guys,
As far as I know, there are multiple possible solutions. One way to create configurations would be [static maps](https://github.com/sfackler/rust-phf) or simply a hash map wrapped in a [lazy_static](https://github.com/rust-lang-nursery/lazy-static.rs).

Each configuration could have a defined Struct, with all the required fields, such as:

```rust
struct Locale {
  days: Vec<String>,
  short_days: Vec<String>,
 ... ecc
}
```

The powerful thing about this approach is that we can make the struct Serializable/Deserializable through [serde_json](https://github.com/serde-rs/json), so that a user could just pass a JSON configuration in the same format as the JS library.
@federico-terzi just FYI, I'm working on an implementation directly in `chrono`: https://github.com/chronotope/chrono/pull/421
> @federico-terzi just FYI, I'm working on an implementation directly in `chrono`: [chronotope/chrono#421](https://github.com/chronotope/chrono/pull/421)

@ar3s3ru thanks for pointing that out, it looks super promising! Good job
Seems like locales are coming in the chrono library: https://github.com/chronotope/chrono/pull/453
Once those ones are stable, we'll most likely be able to add the functionality to espanso! So stay tuned for that
sorry to piggy back on this issue. but I'm trying to find a way to output _ordinal_ numbers (1st, 2nd, 3rd ...) but don't see a simple way to do it via https://docs.rs/chrono/0.3.1/chrono/format/strftime/index.html 

Am I blind here? Seems this should be part of chrono
@kylebradshaw Given that even the Unix `date` utility doesn't support them, it's pretty unlikely that it will be included in chrono... We might add it into espanso itself in the future, in the meanwhile, take a look at https://github.com/federico-terzi/espanso/issues/442#issuecomment-687097808
Is that implemented in v2?
TX in advance!
@jczanfona Not yet :) For anyone who would be interested in contributing to the project, any help here is greatly appreciated!
This JS library might be a great starting point: https://github.com/samsonjs/strftime

Cheers :)

Fix patch:
diff --git a/.github/scripts/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile
index f974dd63..50379afa 100644
--- a/.github/scripts/ubuntu/Dockerfile
+++ b/.github/scripts/ubuntu/Dockerfile
@@ -9,7 +9,7 @@ RUN apt-get update \
 ENV RUSTUP_HOME=/usr/local/rustup \
     CARGO_HOME=/usr/local/cargo \
     PATH=/usr/local/cargo/bin:$PATH \
-    RUST_VERSION=1.55.0
+    RUST_VERSION=1.57.0
 
 RUN set -eux; \
     dpkgArch="$(dpkg --print-architecture)"; \
diff --git a/Cargo.lock b/Cargo.lock
index a9e3bdf7..1dd561d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -187,9 +187,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.66"
+version = "1.0.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
 
 [[package]]
 name = "cfg-if"
@@ -212,6 +212,7 @@ dependencies = [
  "libc",
  "num-integer",
  "num-traits",
+ "pure-rust-locales",
  "time",
  "winapi 0.3.9",
 ]
@@ -399,6 +400,16 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "cstr_core"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08"
+dependencies = [
+ "cty",
+ "memchr",
+]
+
 [[package]]
 name = "ctor"
 version = "0.1.20"
@@ -409,6 +420,12 @@ dependencies = [
  "syn 1.0.67",
 ]
 
+[[package]]
+name = "cty"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
+
 [[package]]
 name = "dbus"
 version = "0.9.1"
@@ -572,7 +589,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "2.1.3-alpha"
+version = "2.1.4-beta"
 dependencies = [
  "anyhow",
  "caps",
@@ -851,6 +868,7 @@ dependencies = [
  "log",
  "rand 0.8.3",
  "regex",
+ "sys-locale",
  "thiserror",
 ]
 
@@ -2040,6 +2058,12 @@ dependencies = [
  "unicode-xid 0.2.1",
 ]
 
+[[package]]
+name = "pure-rust-locales"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45c49fc4f91f35bae654f85ebb3a44d60ac64f11b3166ffa609def390c732d8"
+
 [[package]]
 name = "quote"
 version = "0.3.15"
@@ -2577,6 +2601,19 @@ dependencies = [
  "unicode-xid 0.0.4",
 ]
 
+[[package]]
+name = "sys-locale"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91f89ebb59fa30d4f65fafc2d68e94f6975256fd87e812dd99cb6e020c8563df"
+dependencies = [
+ "cc",
+ "cstr_core",
+ "libc",
+ "web-sys",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "tempdir"
 version = "0.3.7"
diff --git a/espanso-clipboard/Cargo.toml b/espanso-clipboard/Cargo.toml
index b3896136..943588fb 100644
--- a/espanso-clipboard/Cargo.toml
+++ b/espanso-clipboard/Cargo.toml
@@ -28,4 +28,4 @@ widestring = "0.4.3"
 wait-timeout = { version = "0.2.0", optional = true }
 
 [build-dependencies]
-cc = "1.0.66"
\ No newline at end of file
+cc = "1.0.73"
\ No newline at end of file
diff --git a/espanso-clipboard/src/cocoa/native.mm b/espanso-clipboard/src/cocoa/native.mm
index 91ed16a7..a5bbf384 100644
--- a/espanso-clipboard/src/cocoa/native.mm
+++ b/espanso-clipboard/src/cocoa/native.mm
@@ -30,8 +30,6 @@ int32_t clipboard_get_text(char * buffer, int32_t buffer_size) {
       const char * text = [string UTF8String];
       strncpy(buffer, text, buffer_size);
 
-      [string release];
-
       return 1;
     }
   }
diff --git a/espanso-config/src/config/default.rs b/espanso-config/src/config/default.rs
index 09decba5..7966ba75 100644
--- a/espanso-config/src/config/default.rs
+++ b/espanso-config/src/config/default.rs
@@ -21,3 +21,5 @@ pub(crate) const DEFAULT_CLIPBOARD_THRESHOLD: usize = 100;
 pub(crate) const DEFAULT_PRE_PASTE_DELAY: usize = 100;
 pub(crate) const DEFAULT_SHORTCUT_EVENT_DELAY: usize = 10;
 pub(crate) const DEFAULT_RESTORE_CLIPBOARD_DELAY: usize = 300;
+pub(crate) const DEFAULT_POST_FORM_DELAY: usize = 200;
+pub(crate) const DEFAULT_POST_SEARCH_DELAY: usize = 200;
diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs
index 09923729..945841ed 100644
--- a/espanso-config/src/config/mod.rs
+++ b/espanso-config/src/config/mod.rs
@@ -150,6 +150,18 @@ pub trait Config: Send + Sync {
   // If false, avoid showing the SecureInput notification on macOS
   fn secure_input_notification(&self) -> bool;
 
+  // The number of milliseconds to wait after a form has been closed.
+  // This is useful to let the target application regain focus
+  // after a form has been closed, otherwise the injection might
+  // not be targeted to the right application.
+  fn post_form_delay(&self) -> usize;
+
+  // The number of milliseconds to wait after the search bar has been closed.
+  // This is useful to let the target application regain focus
+  // after the search bar has been closed, otherwise the injection might
+  // not be targeted to the right application.
+  fn post_search_delay(&self) -> usize;
+
   // If true, use the `xclip` command to implement the clipboard instead of
   // the built-in native module on X11.
   fn x11_use_xclip_backend(&self) -> bool;
@@ -188,6 +200,8 @@ pub trait Config: Send + Sync {
         toggle_key: {:?}
         auto_restart: {:?}
         restore_clipboard_delay: {:?} 
+        post_form_delay: {:?} 
+        post_search_delay: {:?} 
         backspace_limit: {}
         search_trigger: {:?}
         search_shortcut: {:?}
@@ -220,6 +234,8 @@ pub trait Config: Send + Sync {
       self.toggle_key(),
       self.auto_restart(),
       self.restore_clipboard_delay(),
+      self.post_form_delay(),
+      self.post_search_delay(),
       self.backspace_limit(),
       self.search_trigger(),
       self.search_shortcut(),
diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs
index 94f174d4..644b068d 100644
--- a/espanso-config/src/config/parse/mod.rs
+++ b/espanso-config/src/config/parse/mod.rs
@@ -44,6 +44,8 @@ pub(crate) struct ParsedConfig {
   pub show_notifications: Option<bool>,
   pub show_icon: Option<bool>,
   pub secure_input_notification: Option<bool>,
+  pub post_form_delay: Option<usize>,
+  pub post_search_delay: Option<usize>,
   pub win32_exclude_orphan_events: Option<bool>,
   pub win32_keyboard_layout_cache_interval: Option<i64>,
   pub x11_use_xclip_backend: Option<bool>,
diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs
index d6666625..64558db7 100644
--- a/espanso-config/src/config/parse/yaml.rs
+++ b/espanso-config/src/config/parse/yaml.rs
@@ -103,6 +103,12 @@ pub(crate) struct YAMLConfig {
   #[serde(default)]
   pub show_icon: Option<bool>,
 
+  #[serde(default)]
+  pub post_form_delay: Option<usize>,
+
+  #[serde(default)]
+  pub post_search_delay: Option<usize>,
+
   #[serde(default)]
   pub secure_input_notification: Option<bool>,
 
@@ -201,6 +207,8 @@ impl TryFrom<YAMLConfig> for ParsedConfig {
       pre_paste_delay: yaml_config.pre_paste_delay,
       restore_clipboard_delay: yaml_config.restore_clipboard_delay,
       paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay,
+      post_form_delay: yaml_config.post_form_delay,
+      post_search_delay: yaml_config.post_search_delay,
 
       win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
       win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
@@ -260,6 +268,8 @@ mod tests {
     show_icon: false
     show_notifications: false
     secure_input_notification: false
+    post_form_delay: 300
+    post_search_delay: 400
     win32_exclude_orphan_events: false
     win32_keyboard_layout_cache_interval: 300
     x11_use_xclip_backend: true
@@ -314,6 +324,8 @@ mod tests {
         show_icon: Some(false),
         show_notifications: Some(false),
         secure_input_notification: Some(false),
+        post_form_delay: Some(300),
+        post_search_delay: Some(400),
         win32_exclude_orphan_events: Some(false),
         win32_keyboard_layout_cache_interval: Some(300),
         x11_use_xclip_backend: Some(true),
diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs
index 518c1ff8..7b7dcc33 100644
--- a/espanso-config/src/config/resolve.rs
+++ b/espanso-config/src/config/resolve.rs
@@ -19,8 +19,8 @@
 
 use super::{
   default::{
-    DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY,
-    DEFAULT_SHORTCUT_EVENT_DELAY,
+    DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_POST_FORM_DELAY, DEFAULT_POST_SEARCH_DELAY,
+    DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY, DEFAULT_SHORTCUT_EVENT_DELAY,
   },
   parse::ParsedConfig,
   path::calculate_paths,
@@ -299,6 +299,20 @@ impl Config for ResolvedConfig {
     self.parsed.secure_input_notification.unwrap_or(true)
   }
 
+  fn post_form_delay(&self) -> usize {
+    self
+      .parsed
+      .post_form_delay
+      .unwrap_or(DEFAULT_POST_FORM_DELAY)
+  }
+
+  fn post_search_delay(&self) -> usize {
+    self
+      .parsed
+      .post_search_delay
+      .unwrap_or(DEFAULT_POST_SEARCH_DELAY)
+  }
+
   fn win32_exclude_orphan_events(&self) -> bool {
     self.parsed.win32_exclude_orphan_events.unwrap_or(true)
   }
@@ -398,6 +412,8 @@ impl ResolvedConfig {
       show_icon,
       show_notifications,
       secure_input_notification,
+      post_form_delay,
+      post_search_delay,
       win32_exclude_orphan_events,
       win32_keyboard_layout_cache_interval,
       x11_use_xclip_backend,
diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs
index b67e0f7e..24a94aa2 100644
--- a/espanso-config/src/legacy/mod.rs
+++ b/espanso-config/src/legacy/mod.rs
@@ -387,6 +387,14 @@ impl Config for LegacyInteropConfig {
     self.config.enable_active
   }
 
+  fn post_form_delay(&self) -> usize {
+    crate::config::default::DEFAULT_POST_FORM_DELAY
+  }
+
+  fn post_search_delay(&self) -> usize {
+    crate::config::default::DEFAULT_POST_SEARCH_DELAY
+  }
+
   fn win32_exclude_orphan_events(&self) -> bool {
     true
   }
diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml
index ae760fd3..b97c457e 100644
--- a/espanso-detect/Cargo.toml
+++ b/espanso-detect/Cargo.toml
@@ -27,7 +27,7 @@ scopeguard = "1.1.0"
 sctk = { package = "smithay-client-toolkit", version = "0.14.0", optional = true }
 
 [build-dependencies]
-cc = "1.0.66"
+cc = "1.0.73"
 
 [dev-dependencies]
 enum-as-inner = "0.3.3"
\ No newline at end of file
diff --git a/espanso-detect/src/layout/mod.rs b/espanso-detect/src/layout/mod.rs
index 02155296..83480df0 100644
--- a/espanso-detect/src/layout/mod.rs
+++ b/espanso-detect/src/layout/mod.rs
@@ -37,7 +37,6 @@ pub fn get_active_layout() -> Option<String> {
   if gnome::is_gnome() {
     gnome::get_active_layout()
   } else {
-    log::warn!("unable to determine the currently active layout, you might need to explicitly specify the layout in the config for espanso to work correctly.");
     None
   }
 }
diff --git a/espanso-engine/src/process/middleware/image_resolve.rs b/espanso-engine/src/process/middleware/image_resolve.rs
index 8a9569f2..7fb11510 100644
--- a/espanso-engine/src/process/middleware/image_resolve.rs
+++ b/espanso-engine/src/process/middleware/image_resolve.rs
@@ -47,7 +47,7 @@ impl<'a> Middleware for ImageResolverMiddleware<'a> {
     if let EventType::ImageRequested(m_event) = &event.etype {
       // On Windows, we have to replace the forward / with the backslash \ in the path
       let path = if cfg!(target_os = "windows") {
-        m_event.image_path.replace("/", "\\")
+        m_event.image_path.replace('/', "\\")
       } else {
         m_event.image_path.to_owned()
       };
diff --git a/espanso-info/Cargo.toml b/espanso-info/Cargo.toml
index 56aa9791..2a19659a 100644
--- a/espanso-info/Cargo.toml
+++ b/espanso-info/Cargo.toml
@@ -20,4 +20,4 @@ lazy_static = "1.4.0"
 widestring = "0.4.3"
 
 [build-dependencies]
-cc = "1.0.66"
\ No newline at end of file
+cc = "1.0.73"
\ No newline at end of file
diff --git a/espanso-info/src/cocoa/native.mm b/espanso-info/src/cocoa/native.mm
index 86d9403f..b4379529 100644
--- a/espanso-info/src/cocoa/native.mm
+++ b/espanso-info/src/cocoa/native.mm
@@ -23,29 +23,27 @@
 
 int32_t info_get_title(char *buffer, int32_t buffer_size)
 {
-  @autoreleasepool {
-    CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
-    int32_t result = 0;
-
-    if (windows) {
-      for (NSDictionary *window in (NSArray *)windows) {
-        NSNumber *ownerPid = window[(id) kCGWindowOwnerPID];
-
-        NSRunningApplication *currentApp = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]];
-
-        if ([currentApp isActive]) {
-          NSString *name = window[(id) kCGWindowName];
-          if (name.length > 0) {
-            const char * title = [name UTF8String];
-            snprintf(buffer, buffer_size, "%s", title);
-            result = 1;
-          }
-          break;
+  CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
+  int32_t result = 0;
+
+  if (windows) {
+    for (NSDictionary *window in (NSArray *)windows) {
+      NSNumber *ownerPid = window[(id) kCGWindowOwnerPID];
+
+      NSRunningApplication *currentApp = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]];
+
+      if ([currentApp isActive]) {
+        NSString *name = window[(id) kCGWindowName];
+        if (name.length > 0) {
+          const char * title = [name UTF8String];
+          snprintf(buffer, buffer_size, "%s", title);
+          result = 1;
         }
+        break;
       }
-
-      CFRelease(windows);
     }
+
+    CFRelease(windows);
   }
 
   return 0;
@@ -54,70 +52,64 @@ int32_t info_get_title(char *buffer, int32_t buffer_size)
 // Partially taken from: https://stackoverflow.com/questions/480866/get-the-title-of-the-current-active-window-document-in-mac-os-x/23451568#23451568
 int32_t info_get_title_fallback(char *buffer, int32_t buffer_size)
 {
-  @autoreleasepool {
-    // Get the process ID of the frontmost application.
-    NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
-    pid_t pid = [app processIdentifier];
-
-    AXUIElementRef appElem = AXUIElementCreateApplication(pid);
-    if (!appElem) {
-      return -1;
-    }
-
-    // Get the accessibility element corresponding to the frontmost window
-    // of the frontmost application.
-    AXUIElementRef window = NULL;
-    if (AXUIElementCopyAttributeValue(appElem, 
-          kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) {
-      CFRelease(appElem);
-      return -2;
-    }
+  // Get the process ID of the frontmost application.
+  NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
+  pid_t pid = [app processIdentifier];
 
-    // Finally, get the title of the frontmost window.
-    CFStringRef title = NULL;
-    AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute,
-                      (CFTypeRef*)&title);
+  AXUIElementRef appElem = AXUIElementCreateApplication(pid);
+  if (!appElem) {
+    return -1;
+  }
 
-    // At this point, we don't need window and appElem anymore.
-    CFRelease(window);
+  // Get the accessibility element corresponding to the frontmost window
+  // of the frontmost application.
+  AXUIElementRef window = NULL;
+  if (AXUIElementCopyAttributeValue(appElem, 
+        kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) {
     CFRelease(appElem);
+    return -2;
+  }
 
-    if (result != kAXErrorSuccess) {
-      // Failed to get the window title.
-      return -3;
-    }
+  // Finally, get the title of the frontmost window.
+  CFStringRef title = NULL;
+  AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute,
+                    (CFTypeRef*)&title);
 
-    if (CFStringGetCString(title, buffer, buffer_size, kCFStringEncodingUTF8)) {
-      CFRelease(title);
-      return 1;
-    } else {
-      return -4;
-    }
+  // At this point, we don't need window and appElem anymore.
+  CFRelease(window);
+  CFRelease(appElem);
+
+  if (result != kAXErrorSuccess) {
+    // Failed to get the window title.
+    return -3;
+  }
+
+  if (CFStringGetCString(title, buffer, buffer_size, kCFStringEncodingUTF8)) {
+    CFRelease(title);
+    return 1;
+  } else {
+    return -4;
   }
 }
 
 int32_t info_get_exec(char *buffer, int32_t buffer_size)
 {
-  @autoreleasepool {
-    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
-    NSString *bundlePath = [frontApp bundleURL].path;
-    const char * path = [bundlePath UTF8String];
+  NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
+  NSString *bundlePath = [frontApp bundleURL].path;
+  const char * path = [bundlePath UTF8String];
 
-    snprintf(buffer, buffer_size, "%s", path);
-  }
+  snprintf(buffer, buffer_size, "%s", path);
 
   return 1;
 }
 
 int32_t info_get_class(char *buffer, int32_t buffer_size)
 {
-  @autoreleasepool {
-    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
-    NSString *bundleId = frontApp.bundleIdentifier;
-    const char * bundle = [bundleId UTF8String];
+  NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
+  NSString *bundleId = frontApp.bundleIdentifier;
+  const char * bundle = [bundleId UTF8String];
 
-    snprintf(buffer, buffer_size, "%s", bundle);
-  }
+  snprintf(buffer, buffer_size, "%s", bundle);
 
   return 1;
 }
\ No newline at end of file
diff --git a/espanso-inject/Cargo.toml b/espanso-inject/Cargo.toml
index 5a0c9a0a..c7724be2 100644
--- a/espanso-inject/Cargo.toml
+++ b/espanso-inject/Cargo.toml
@@ -27,7 +27,7 @@ scopeguard = "1.1.0"
 itertools = "0.10.0"
 
 [build-dependencies]
-cc = "1.0.66"
+cc = "1.0.73"
 
 [dev-dependencies]
 enum-as-inner = "0.3.3"
\ No newline at end of file
diff --git a/espanso-mac-utils/Cargo.toml b/espanso-mac-utils/Cargo.toml
index 6172e9cf..cd47a4bf 100644
--- a/espanso-mac-utils/Cargo.toml
+++ b/espanso-mac-utils/Cargo.toml
@@ -14,4 +14,4 @@ lazy_static = "1.4.0"
 regex = "1.4.3"
 
 [build-dependencies]
-cc = "1.0.66"
\ No newline at end of file
+cc = "1.0.73"
\ No newline at end of file
diff --git a/espanso-modulo/Cargo.toml b/espanso-modulo/Cargo.toml
index a6f3ea5d..332c043d 100644
--- a/espanso-modulo/Cargo.toml
+++ b/espanso-modulo/Cargo.toml
@@ -15,7 +15,7 @@ lazy_static = "1.4.0"
 regex = "1.4.3"
 
 [build-dependencies]
-cc = "1.0.66"
+cc = "1.0.73"
 regex = "1.4.3"
 zip = "0.5.12"
 winres = "0.1.11"
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
index f3f1a21a..5c5570d2 100644
--- a/espanso-modulo/build.rs
+++ b/espanso-modulo/build.rs
@@ -49,7 +49,7 @@ fn build_native() {
       .expect("unable to extract wxWidgets source dir");
 
     // Compile wxWidgets
-    let tool = cc::windows_registry::find_tool("msvc", "msbuild")
+    let tool = cc::windows_registry::find_tool("msvc", "devenv")
       .expect("unable to locate MSVC compiler, did you install Visual Studio?");
     let mut vcvars_path = None;
     let mut current_root = tool.path();
diff --git a/espanso-modulo/src/sys/search/search.cpp b/espanso-modulo/src/sys/search/search.cpp
index a06f7963..6d4ac54e 100644
--- a/espanso-modulo/src/sys/search/search.cpp
+++ b/espanso-modulo/src/sys/search/search.cpp
@@ -41,7 +41,7 @@ const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxRESIZE_BORDER
 #endif
 #ifdef __LINUX__
 const int SEARCH_BAR_FONT_SIZE = 20;
-const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxBORDER_NONE;
+const long DEFAULT_STYLE = wxSTAY_ON_TOP;
 #endif
 
 const int HELP_TEXT_FONT_SIZE = 10;
diff --git a/espanso-render/Cargo.toml b/espanso-render/Cargo.toml
index 3940d0d0..2244e8a2 100644
--- a/espanso-render/Cargo.toml
+++ b/espanso-render/Cargo.toml
@@ -10,6 +10,7 @@ anyhow = "1.0.38"
 thiserror = "1.0.23"
 regex = "1.4.3"
 lazy_static = "1.4.0"
-chrono = "0.4.19"
+chrono = {version = "0.4.19", features=["unstable-locales"]}
 enum-as-inner = "0.3.3"
-rand = "0.8.3"
\ No newline at end of file
+rand = "0.8.3"
+sys-locale = "0.1.0"
\ No newline at end of file
diff --git a/espanso-render/src/extension/date.rs b/espanso-render/src/extension/date.rs
index 5cb08121..12f74c2e 100644
--- a/espanso-render/src/extension/date.rs
+++ b/espanso-render/src/extension/date.rs
@@ -17,22 +17,30 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use chrono::{DateTime, Duration, Local};
+use chrono::{DateTime, Duration, Local, Locale};
 
 use crate::{Extension, ExtensionOutput, ExtensionResult, Number, Params, Value};
 
-pub struct DateExtension {
+pub trait LocaleProvider {
+  fn get_system_locale(&self) -> String;
+}
+
+pub struct DateExtension<'a> {
   fixed_date: Option<DateTime<Local>>,
+  locale_provider: &'a dyn LocaleProvider,
 }
 
 #[allow(clippy::new_without_default)]
-impl DateExtension {
-  pub fn new() -> Self {
-    Self { fixed_date: None }
+impl<'a> DateExtension<'a> {
+  pub fn new(locale_provider: &'a dyn LocaleProvider) -> Self {
+    Self {
+      fixed_date: None,
+      locale_provider,
+    }
   }
 }
 
-impl Extension for DateExtension {
+impl<'a> Extension for DateExtension<'a> {
   fn name(&self) -> &str {
     "date"
   }
@@ -53,9 +61,14 @@ impl Extension for DateExtension {
     }
 
     let format = params.get("format");
+    let locale = params
+      .get("locale")
+      .and_then(|val| val.as_string())
+      .map(String::from)
+      .unwrap_or_else(|| self.locale_provider.get_system_locale());
 
     let date = if let Some(Value::String(format)) = format {
-      now.format(format).to_string()
+      DateExtension::format_date_with_locale_string(now, format, &locale)
     } else {
       now.to_rfc2822()
     };
@@ -64,7 +77,7 @@ impl Extension for DateExtension {
   }
 }
 
-impl DateExtension {
+impl<'a> DateExtension<'a> {
   fn get_date(&self) -> DateTime<Local> {
     if let Some(fixed_date) = self.fixed_date {
       fixed_date
@@ -72,6 +85,340 @@ impl DateExtension {
       Local::now()
     }
   }
+
+  fn format_date_with_locale(date: DateTime<Local>, format: &str, locale: Locale) -> String {
+    date.format_localized(format, locale).to_string()
+  }
+
+  fn format_date_with_locale_string(
+    date: DateTime<Local>,
+    format: &str,
+    locale_str: &str,
+  ) -> String {
+    let locale = convert_locale_string_to_locale(locale_str).unwrap_or(Locale::en_US);
+    Self::format_date_with_locale(date, format, locale)
+  }
+}
+
+fn convert_locale_string_to_locale(locale_str: &str) -> Option<Locale> {
+  match locale_str {
+    "aa-DJ" => Some(Locale::aa_DJ),
+    "aa-ER" => Some(Locale::aa_ER),
+    "aa-ET" => Some(Locale::aa_ET),
+    "af-ZA" => Some(Locale::af_ZA),
+    "agr-PE" => Some(Locale::agr_PE),
+    "ak-GH" => Some(Locale::ak_GH),
+    "am-ET" => Some(Locale::am_ET),
+    "an-ES" => Some(Locale::an_ES),
+    "anp-IN" => Some(Locale::anp_IN),
+    "ar-AE" => Some(Locale::ar_AE),
+    "ar-BH" => Some(Locale::ar_BH),
+    "ar-DZ" => Some(Locale::ar_DZ),
+    "ar-EG" => Some(Locale::ar_EG),
+    "ar-IN" => Some(Locale::ar_IN),
+    "ar-IQ" => Some(Locale::ar_IQ),
+    "ar-JO" => Some(Locale::ar_JO),
+    "ar-KW" => Some(Locale::ar_KW),
+    "ar-LB" => Some(Locale::ar_LB),
+    "ar-LY" => Some(Locale::ar_LY),
+    "ar-MA" => Some(Locale::ar_MA),
+    "ar-OM" => Some(Locale::ar_OM),
+    "ar-QA" => Some(Locale::ar_QA),
+    "ar-SA" => Some(Locale::ar_SA),
+    "ar-SD" => Some(Locale::ar_SD),
+    "ar-SS" => Some(Locale::ar_SS),
+    "ar-SY" => Some(Locale::ar_SY),
+    "ar-TN" => Some(Locale::ar_TN),
+    "ar-YE" => Some(Locale::ar_YE),
+    "as-IN" => Some(Locale::as_IN),
+    "ast-ES" => Some(Locale::ast_ES),
+    "ayc-PE" => Some(Locale::ayc_PE),
+    "az-AZ" => Some(Locale::az_AZ),
+    "az-IR" => Some(Locale::az_IR),
+    "be-BY" => Some(Locale::be_BY),
+    "bem-ZM" => Some(Locale::bem_ZM),
+    "ber-DZ" => Some(Locale::ber_DZ),
+    "ber-MA" => Some(Locale::ber_MA),
+    "bg-BG" => Some(Locale::bg_BG),
+    "bhb-IN" => Some(Locale::bhb_IN),
+    "bho-IN" => Some(Locale::bho_IN),
+    "bho-NP" => Some(Locale::bho_NP),
+    "bi-VU" => Some(Locale::bi_VU),
+    "bn-BD" => Some(Locale::bn_BD),
+    "bn-IN" => Some(Locale::bn_IN),
+    "bo-CN" => Some(Locale::bo_CN),
+    "bo-IN" => Some(Locale::bo_IN),
+    "br-FR" => Some(Locale::br_FR),
+    "brx-IN" => Some(Locale::brx_IN),
+    "bs-BA" => Some(Locale::bs_BA),
+    "byn-ER" => Some(Locale::byn_ER),
+    "ca-AD" => Some(Locale::ca_AD),
+    "ca-ES" => Some(Locale::ca_ES),
+    "ca-FR" => Some(Locale::ca_FR),
+    "ca-IT" => Some(Locale::ca_IT),
+    "ce-RU" => Some(Locale::ce_RU),
+    "chr-US" => Some(Locale::chr_US),
+    "cmn-TW" => Some(Locale::cmn_TW),
+    "crh-UA" => Some(Locale::crh_UA),
+    "cs-CZ" => Some(Locale::cs_CZ),
+    "csb-PL" => Some(Locale::csb_PL),
+    "cv-RU" => Some(Locale::cv_RU),
+    "cy-GB" => Some(Locale::cy_GB),
+    "da-DK" => Some(Locale::da_DK),
+    "de-AT" => Some(Locale::de_AT),
+    "de-BE" => Some(Locale::de_BE),
+    "de-CH" => Some(Locale::de_CH),
+    "de-DE" => Some(Locale::de_DE),
+    "de-IT" => Some(Locale::de_IT),
+    "de-LI" => Some(Locale::de_LI),
+    "de-LU" => Some(Locale::de_LU),
+    "doi-IN" => Some(Locale::doi_IN),
+    "dsb-DE" => Some(Locale::dsb_DE),
+    "dv-MV" => Some(Locale::dv_MV),
+    "dz-BT" => Some(Locale::dz_BT),
+    "el-CY" => Some(Locale::el_CY),
+    "el-GR" => Some(Locale::el_GR),
+    "en-AG" => Some(Locale::en_AG),
+    "en-AU" => Some(Locale::en_AU),
+    "en-BW" => Some(Locale::en_BW),
+    "en-CA" => Some(Locale::en_CA),
+    "en-DK" => Some(Locale::en_DK),
+    "en-GB" => Some(Locale::en_GB),
+    "en-HK" => Some(Locale::en_HK),
+    "en-IE" => Some(Locale::en_IE),
+    "en-IL" => Some(Locale::en_IL),
+    "en-IN" => Some(Locale::en_IN),
+    "en-NG" => Some(Locale::en_NG),
+    "en-NZ" => Some(Locale::en_NZ),
+    "en-PH" => Some(Locale::en_PH),
+    "en-SC" => Some(Locale::en_SC),
+    "en-SG" => Some(Locale::en_SG),
+    "en-US" => Some(Locale::en_US),
+    "en-ZA" => Some(Locale::en_ZA),
+    "en-ZM" => Some(Locale::en_ZM),
+    "en-ZW" => Some(Locale::en_ZW),
+    "eo" => Some(Locale::eo),
+    "es-AR" => Some(Locale::es_AR),
+    "es-BO" => Some(Locale::es_BO),
+    "es-CL" => Some(Locale::es_CL),
+    "es-CO" => Some(Locale::es_CO),
+    "es-CR" => Some(Locale::es_CR),
+    "es-CU" => Some(Locale::es_CU),
+    "es-DO" => Some(Locale::es_DO),
+    "es-EC" => Some(Locale::es_EC),
+    "es-ES" => Some(Locale::es_ES),
+    "es-GT" => Some(Locale::es_GT),
+    "es-HN" => Some(Locale::es_HN),
+    "es-MX" => Some(Locale::es_MX),
+    "es-NI" => Some(Locale::es_NI),
+    "es-PA" => Some(Locale::es_PA),
+    "es-PE" => Some(Locale::es_PE),
+    "es-PR" => Some(Locale::es_PR),
+    "es-PY" => Some(Locale::es_PY),
+    "es-SV" => Some(Locale::es_SV),
+    "es-US" => Some(Locale::es_US),
+    "es-UY" => Some(Locale::es_UY),
+    "es-VE" => Some(Locale::es_VE),
+    "et-EE" => Some(Locale::et_EE),
+    "eu-ES" => Some(Locale::eu_ES),
+    "fa-IR" => Some(Locale::fa_IR),
+    "ff-SN" => Some(Locale::ff_SN),
+    "fi-FI" => Some(Locale::fi_FI),
+    "fil-PH" => Some(Locale::fil_PH),
+    "fo-FO" => Some(Locale::fo_FO),
+    "fr-BE" => Some(Locale::fr_BE),
+    "fr-CA" => Some(Locale::fr_CA),
+    "fr-CH" => Some(Locale::fr_CH),
+    "fr-FR" => Some(Locale::fr_FR),
+    "fr-LU" => Some(Locale::fr_LU),
+    "fur-IT" => Some(Locale::fur_IT),
+    "fy-DE" => Some(Locale::fy_DE),
+    "fy-NL" => Some(Locale::fy_NL),
+    "ga-IE" => Some(Locale::ga_IE),
+    "gd-GB" => Some(Locale::gd_GB),
+    "gez-ER" => Some(Locale::gez_ER),
+    "gez-ET" => Some(Locale::gez_ET),
+    "gl-ES" => Some(Locale::gl_ES),
+    "gu-IN" => Some(Locale::gu_IN),
+    "gv-GB" => Some(Locale::gv_GB),
+    "ha-NG" => Some(Locale::ha_NG),
+    "hak-TW" => Some(Locale::hak_TW),
+    "he-IL" => Some(Locale::he_IL),
+    "hi-IN" => Some(Locale::hi_IN),
+    "hif-FJ" => Some(Locale::hif_FJ),
+    "hne-IN" => Some(Locale::hne_IN),
+    "hr-HR" => Some(Locale::hr_HR),
+    "hsb-DE" => Some(Locale::hsb_DE),
+    "ht-HT" => Some(Locale::ht_HT),
+    "hu-HU" => Some(Locale::hu_HU),
+    "hy-AM" => Some(Locale::hy_AM),
+    "ia-FR" => Some(Locale::ia_FR),
+    "id-ID" => Some(Locale::id_ID),
+    "ig-NG" => Some(Locale::ig_NG),
+    "ik-CA" => Some(Locale::ik_CA),
+    "is-IS" => Some(Locale::is_IS),
+    "it-CH" => Some(Locale::it_CH),
+    "it-IT" => Some(Locale::it_IT),
+    "iu-CA" => Some(Locale::iu_CA),
+    "ja-JP" => Some(Locale::ja_JP),
+    "ka-GE" => Some(Locale::ka_GE),
+    "kab-DZ" => Some(Locale::kab_DZ),
+    "kk-KZ" => Some(Locale::kk_KZ),
+    "kl-GL" => Some(Locale::kl_GL),
+    "km-KH" => Some(Locale::km_KH),
+    "kn-IN" => Some(Locale::kn_IN),
+    "ko-KR" => Some(Locale::ko_KR),
+    "kok-IN" => Some(Locale::kok_IN),
+    "ks-IN" => Some(Locale::ks_IN),
+    "ku-TR" => Some(Locale::ku_TR),
+    "kw-GB" => Some(Locale::kw_GB),
+    "ky-KG" => Some(Locale::ky_KG),
+    "lb-LU" => Some(Locale::lb_LU),
+    "lg-UG" => Some(Locale::lg_UG),
+    "li-BE" => Some(Locale::li_BE),
+    "li-NL" => Some(Locale::li_NL),
+    "lij-IT" => Some(Locale::lij_IT),
+    "ln-CD" => Some(Locale::ln_CD),
+    "lo-LA" => Some(Locale::lo_LA),
+    "lt-LT" => Some(Locale::lt_LT),
+    "lv-LV" => Some(Locale::lv_LV),
+    "lzh-TW" => Some(Locale::lzh_TW),
+    "mag-IN" => Some(Locale::mag_IN),
+    "mai-IN" => Some(Locale::mai_IN),
+    "mai-NP" => Some(Locale::mai_NP),
+    "mfe-MU" => Some(Locale::mfe_MU),
+    "mg-MG" => Some(Locale::mg_MG),
+    "mhr-RU" => Some(Locale::mhr_RU),
+    "mi-NZ" => Some(Locale::mi_NZ),
+    "miq-NI" => Some(Locale::miq_NI),
+    "mjw-IN" => Some(Locale::mjw_IN),
+    "mk-MK" => Some(Locale::mk_MK),
+    "ml-IN" => Some(Locale::ml_IN),
+    "mn-MN" => Some(Locale::mn_MN),
+    "mni-IN" => Some(Locale::mni_IN),
+    "mnw-MM" => Some(Locale::mnw_MM),
+    "mr-IN" => Some(Locale::mr_IN),
+    "ms-MY" => Some(Locale::ms_MY),
+    "mt-MT" => Some(Locale::mt_MT),
+    "my-MM" => Some(Locale::my_MM),
+    "nan-TW" => Some(Locale::nan_TW),
+    "nb-NO" => Some(Locale::nb_NO),
+    "nds-DE" => Some(Locale::nds_DE),
+    "nds-NL" => Some(Locale::nds_NL),
+    "ne-NP" => Some(Locale::ne_NP),
+    "nhn-MX" => Some(Locale::nhn_MX),
+    "niu-NU" => Some(Locale::niu_NU),
+    "niu-NZ" => Some(Locale::niu_NZ),
+    "nl-AW" => Some(Locale::nl_AW),
+    "nl-BE" => Some(Locale::nl_BE),
+    "nl-NL" => Some(Locale::nl_NL),
+    "nn-NO" => Some(Locale::nn_NO),
+    "nr-ZA" => Some(Locale::nr_ZA),
+    "nso-ZA" => Some(Locale::nso_ZA),
+    "oc-FR" => Some(Locale::oc_FR),
+    "om-ET" => Some(Locale::om_ET),
+    "om-KE" => Some(Locale::om_KE),
+    "or-IN" => Some(Locale::or_IN),
+    "os-RU" => Some(Locale::os_RU),
+    "pa-IN" => Some(Locale::pa_IN),
+    "pa-PK" => Some(Locale::pa_PK),
+    "pap-AW" => Some(Locale::pap_AW),
+    "pap-CW" => Some(Locale::pap_CW),
+    "pl-PL" => Some(Locale::pl_PL),
+    "ps-AF" => Some(Locale::ps_AF),
+    "pt-BR" => Some(Locale::pt_BR),
+    "pt-PT" => Some(Locale::pt_PT),
+    "quz-PE" => Some(Locale::quz_PE),
+    "raj-IN" => Some(Locale::raj_IN),
+    "ro-RO" => Some(Locale::ro_RO),
+    "ru-RU" => Some(Locale::ru_RU),
+    "ru-UA" => Some(Locale::ru_UA),
+    "rw-RW" => Some(Locale::rw_RW),
+    "sa-IN" => Some(Locale::sa_IN),
+    "sah-RU" => Some(Locale::sah_RU),
+    "sat-IN" => Some(Locale::sat_IN),
+    "sc-IT" => Some(Locale::sc_IT),
+    "sd-IN" => Some(Locale::sd_IN),
+    "se-NO" => Some(Locale::se_NO),
+    "sgs-LT" => Some(Locale::sgs_LT),
+    "shn-MM" => Some(Locale::shn_MM),
+    "shs-CA" => Some(Locale::shs_CA),
+    "si-LK" => Some(Locale::si_LK),
+    "sid-ET" => Some(Locale::sid_ET),
+    "sk-SK" => Some(Locale::sk_SK),
+    "sl-SI" => Some(Locale::sl_SI),
+    "sm-WS" => Some(Locale::sm_WS),
+    "so-DJ" => Some(Locale::so_DJ),
+    "so-ET" => Some(Locale::so_ET),
+    "so-KE" => Some(Locale::so_KE),
+    "so-SO" => Some(Locale::so_SO),
+    "sq-AL" => Some(Locale::sq_AL),
+    "sq-MK" => Some(Locale::sq_MK),
+    "sr-ME" => Some(Locale::sr_ME),
+    "sr-RS" => Some(Locale::sr_RS),
+    "ss-ZA" => Some(Locale::ss_ZA),
+    "st-ZA" => Some(Locale::st_ZA),
+    "sv-FI" => Some(Locale::sv_FI),
+    "sv-SE" => Some(Locale::sv_SE),
+    "sw-KE" => Some(Locale::sw_KE),
+    "sw-TZ" => Some(Locale::sw_TZ),
+    "szl-PL" => Some(Locale::szl_PL),
+    "ta-IN" => Some(Locale::ta_IN),
+    "ta-LK" => Some(Locale::ta_LK),
+    "tcy-IN" => Some(Locale::tcy_IN),
+    "te-IN" => Some(Locale::te_IN),
+    "tg-TJ" => Some(Locale::tg_TJ),
+    "th-TH" => Some(Locale::th_TH),
+    "the-NP" => Some(Locale::the_NP),
+    "ti-ER" => Some(Locale::ti_ER),
+    "ti-ET" => Some(Locale::ti_ET),
+    "tig-ER" => Some(Locale::tig_ER),
+    "tk-TM" => Some(Locale::tk_TM),
+    "tl-PH" => Some(Locale::tl_PH),
+    "tn-ZA" => Some(Locale::tn_ZA),
+    "to-TO" => Some(Locale::to_TO),
+    "tpi-PG" => Some(Locale::tpi_PG),
+    "tr-CY" => Some(Locale::tr_CY),
+    "tr-TR" => Some(Locale::tr_TR),
+    "ts-ZA" => Some(Locale::ts_ZA),
+    "tt-RU" => Some(Locale::tt_RU),
+    "ug-CN" => Some(Locale::ug_CN),
+    "uk-UA" => Some(Locale::uk_UA),
+    "unm-US" => Some(Locale::unm_US),
+    "ur-IN" => Some(Locale::ur_IN),
+    "ur-PK" => Some(Locale::ur_PK),
+    "uz-UZ" => Some(Locale::uz_UZ),
+    "ve-ZA" => Some(Locale::ve_ZA),
+    "vi-VN" => Some(Locale::vi_VN),
+    "wa-BE" => Some(Locale::wa_BE),
+    "wae-CH" => Some(Locale::wae_CH),
+    "wal-ET" => Some(Locale::wal_ET),
+    "wo-SN" => Some(Locale::wo_SN),
+    "xh-ZA" => Some(Locale::xh_ZA),
+    "yi-US" => Some(Locale::yi_US),
+    "yo-NG" => Some(Locale::yo_NG),
+    "yue-HK" => Some(Locale::yue_HK),
+    "yuw-PG" => Some(Locale::yuw_PG),
+    "zh-CN" => Some(Locale::zh_CN),
+    "zh-HK" => Some(Locale::zh_HK),
+    "zh-SG" => Some(Locale::zh_SG),
+    "zh-TW" => Some(Locale::zh_TW),
+    "zu-ZA" => Some(Locale::zu_ZA),
+    _ => None,
+  }
+}
+
+pub struct DefaultLocaleProvider {}
+impl LocaleProvider for DefaultLocaleProvider {
+  fn get_system_locale(&self) -> String {
+    sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))
+  }
+}
+#[allow(clippy::new_without_default)]
+impl DefaultLocaleProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
 }
 
 #[cfg(test)]
@@ -79,9 +426,30 @@ mod tests {
   use super::*;
   use chrono::offset::TimeZone;
 
+  struct MockLocaleProvider {
+    locale: String,
+  }
+  impl LocaleProvider for MockLocaleProvider {
+    fn get_system_locale(&self) -> String {
+      self.locale.clone()
+    }
+  }
+  impl MockLocaleProvider {
+    pub fn new() -> Self {
+      Self {
+        locale: "en-US".to_string(),
+      }
+    }
+
+    pub fn new_with_locale(locale: String) -> Self {
+      Self { locale }
+    }
+  }
+
   #[test]
   fn date_formatted_correctly() {
-    let mut extension = DateExtension::new();
+    let locale_provider = MockLocaleProvider::new();
+    let mut extension = DateExtension::new(&locale_provider);
     extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
 
     let param = vec![("format".to_string(), Value::String("%H:%M:%S".to_string()))]
@@ -98,7 +466,8 @@ mod tests {
 
   #[test]
   fn offset_works_correctly() {
-    let mut extension = DateExtension::new();
+    let locale_provider = MockLocaleProvider::new();
+    let mut extension = DateExtension::new(&locale_provider);
     extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
 
     let param = vec![
@@ -115,4 +484,61 @@ mod tests {
       ExtensionOutput::Single("10:10:11".to_string())
     );
   }
+
+  #[test]
+  fn default_locale_works_correctly() {
+    let locale_provider = MockLocaleProvider::new_with_locale("it-IT".to_string());
+    let mut extension = DateExtension::new(&locale_provider);
+    extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
+
+    let param = vec![("format".to_string(), Value::String("%A".to_string()))]
+      .into_iter()
+      .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("martedì".to_string())
+    );
+  }
+
+  #[test]
+  fn invalid_locale_should_default_to_en_us() {
+    let locale_provider = MockLocaleProvider::new_with_locale("invalid".to_string());
+    let mut extension = DateExtension::new(&locale_provider);
+    extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
+
+    let param = vec![("format".to_string(), Value::String("%A".to_string()))]
+      .into_iter()
+      .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("Tuesday".to_string())
+    );
+  }
+
+  #[test]
+  fn override_locale() {
+    let locale_provider = MockLocaleProvider::new();
+    let mut extension = DateExtension::new(&locale_provider);
+    extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
+
+    let param = vec![
+      ("format".to_string(), Value::String("%A".to_string())),
+      ("locale".to_string(), Value::String("it-IT".to_string())),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("martedì".to_string())
+    );
+  }
 }
diff --git a/espanso-render/src/extension/exec_util.rs b/espanso-render/src/extension/exec_util.rs
new file mode 100644
index 00000000..e602e720
--- /dev/null
+++ b/espanso-render/src/extension/exec_util.rs
@@ -0,0 +1,99 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2022 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/>.
+ */
+
+pub enum MacShell {
+  Bash,
+  Sh,
+  Zsh,
+}
+
+// Determine the PATH env variable value available inside a regular terminal session
+#[cfg(target_os = "macos")]
+pub fn determine_path_env_variable_override(explicit_shell: Option<MacShell>) -> Option<String> {
+  let shell: MacShell = explicit_shell.or_else(determine_default_macos_shell)?;
+
+  match shell {
+    MacShell::Bash => {
+      launch_command_and_get_output("bash", &["--login", "-c", "source ~/.bashrc; echo $PATH"])
+    }
+    MacShell::Sh => launch_command_and_get_output("sh", &["--login", "-c", "echo $PATH"]),
+    MacShell::Zsh => {
+      launch_command_and_get_output("zsh", &["--login", "-c", "source ~/.zshrc; echo $PATH"])
+    }
+  }
+}
+
+#[cfg(not(target_os = "macos"))]
+pub fn determine_path_env_variable_override(_: Option<MacShell>) -> Option<String> {
+  None
+}
+
+#[cfg(target_os = "macos")]
+pub fn determine_default_macos_shell() -> Option<MacShell> {
+  use regex::Regex;
+  use std::process::Command;
+
+  let output = Command::new("sh")
+    .args(&["--login", "-c", "dscl . -read ~/ UserShell"])
+    .output()
+    .ok()?;
+
+  lazy_static! {
+    static ref EXTRACT_SHELL_REGEX: Regex =
+      Regex::new(r"UserShell:\s(.*)$").expect("unable to generate regex to extract default shell");
+  }
+
+  if !output.status.success() {
+    return None;
+  }
+
+  let output_str = String::from_utf8_lossy(&output.stdout);
+  let captures = EXTRACT_SHELL_REGEX.captures(output_str.trim())?;
+
+  let shell = captures.get(1)?.as_str().trim();
+
+  if shell.ends_with("/bash") {
+    Some(MacShell::Bash)
+  } else if shell.ends_with("/zsh") {
+    Some(MacShell::Zsh)
+  } else if shell.ends_with("/sh") {
+    Some(MacShell::Sh)
+  } else {
+    None
+  }
+}
+
+#[cfg(not(target_os = "macos"))]
+pub fn determine_default_macos_shell() -> Option<MacShell> {
+  None
+}
+
+#[cfg(target_os = "macos")]
+fn launch_command_and_get_output(command: &str, args: &[&str]) -> Option<String> {
+  use std::process::Command;
+
+  let output = Command::new(command).args(args).output().ok()?;
+
+  if !output.status.success() {
+    return None;
+  }
+
+  let output_str = String::from_utf8_lossy(&output.stdout);
+  Some(output_str.to_string())
+}
diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs
index 4cc73569..1f620ef7 100644
--- a/espanso-render/src/extension/mod.rs
+++ b/espanso-render/src/extension/mod.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019-2021 Federico Terzi
+ * Copyright (C) 2019-2022 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
@@ -21,6 +21,7 @@ pub mod choice;
 pub mod clipboard;
 pub mod date;
 pub mod echo;
+mod exec_util;
 pub mod form;
 pub mod random;
 pub mod script;
diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs
index 4fe4250f..a060b7f6 100644
--- a/espanso-render/src/extension/shell.rs
+++ b/espanso-render/src/extension/shell.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019-2021 Federico Terzi
+ * Copyright (C) 2019-2022 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
@@ -24,7 +24,7 @@ use std::{
 };
 
 use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
-use log::{error, info};
+use log::{debug, error, info};
 use thiserror::Error;
 
 #[allow(clippy::upper_case_acronyms)]
@@ -35,10 +35,16 @@ pub enum Shell {
   WSL2,
   Bash,
   Sh,
+  Zsh,
 }
 
 impl Shell {
-  fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
+  fn execute_cmd(
+    &self,
+    cmd: &str,
+    vars: &HashMap<String, String>,
+    override_path_on_macos: bool,
+  ) -> std::io::Result<Output> {
     let mut is_wsl = false;
 
     let mut command = match self {
@@ -74,6 +80,11 @@ impl Shell {
         command.args(&["-c", cmd]);
         command
       }
+      Shell::Zsh => {
+        let mut command = Command::new("zsh");
+        command.args(&["-c", cmd]);
+        command
+      }
     };
 
     // Set the OS-specific flags
@@ -84,6 +95,27 @@ impl Shell {
       command.env(key, value);
     }
 
+    // If Espanso is executed as an app bundle on macOS, it doesn't inherit the PATH
+    // environment variables that are available inside a terminal, and this can be confusing for users.
+    // For example, one might use "jq" inside the terminal but then it throws an error with "command not found"
+    // if launched through the Espanso shell extension.
+    // For this reason, Espanso tries to obtain the same PATH value by spawning a login shell and extracting
+    // the PATH after the processing.
+    if cfg!(target_os = "macos") && override_path_on_macos {
+      let supported_mac_shell = match self {
+        Shell::Bash => Some(super::exec_util::MacShell::Bash),
+        Shell::Sh => Some(super::exec_util::MacShell::Sh),
+        Shell::Zsh => Some(super::exec_util::MacShell::Zsh),
+        _ => None,
+      };
+      if let Some(path_env_override) =
+        super::exec_util::determine_path_env_variable_override(supported_mac_shell)
+      {
+        debug!("overriding PATH env variable with: {}", path_env_override);
+        command.env("PATH", path_env_override);
+      }
+    }
+
     // In WSL environment, we have to specify which ENV variables
     // should be passed to linux.
     // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
@@ -110,6 +142,7 @@ impl Shell {
       "wsl2" => Some(Shell::WSL2),
       "bash" => Some(Shell::Bash),
       "sh" => Some(Shell::Sh),
+      "zsh" => Some(Shell::Zsh),
       _ => None,
     }
   }
@@ -120,7 +153,17 @@ impl Default for Shell {
     if cfg!(target_os = "windows") {
       Shell::Powershell
     } else if cfg!(target_os = "macos") {
-      Shell::Sh
+      lazy_static! {
+        static ref DEFAULT_MACOS_SHELL: Option<super::exec_util::MacShell> =
+          super::exec_util::determine_default_macos_shell();
+      }
+
+      match *DEFAULT_MACOS_SHELL {
+        Some(super::exec_util::MacShell::Bash) => Shell::Bash,
+        Some(super::exec_util::MacShell::Sh) => Shell::Sh,
+        Some(super::exec_util::MacShell::Zsh) => Shell::Zsh,
+        None => Shell::Sh,
+      }
     } else if cfg!(target_os = "linux") {
       Shell::Bash
     } else {
@@ -172,7 +215,13 @@ impl Extension for ShellExtension {
         self.config_path.to_string_lossy().to_string(),
       );
 
-      match shell.execute_cmd(cmd, &env_variables) {
+      let macos_override_path = params
+        .get("macos_override_path")
+        .and_then(|v| v.as_bool())
+        .copied()
+        .unwrap_or(true);
+
+      match shell.execute_cmd(cmd, &env_variables, macos_override_path) {
         Ok(output) => {
           let output_str = String::from_utf8_lossy(&output.stdout);
           let error_str = String::from_utf8_lossy(&output.stderr);
diff --git a/espanso-ui/Cargo.toml b/espanso-ui/Cargo.toml
index 1194758a..e1d5b3d0 100644
--- a/espanso-ui/Cargo.toml
+++ b/espanso-ui/Cargo.toml
@@ -31,4 +31,4 @@ notify-rust = "4.2.2"
 crossbeam = "0.8.0"
 
 [build-dependencies]
-cc = "1.0.66"
\ No newline at end of file
+cc = "1.0.73"
\ No newline at end of file
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index 5a5a7450..c5c573b7 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "2.1.3-alpha"
+version = "2.1.4-beta"
 authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
 license = "GPL-3.0"
 description = "Cross-platform Text Expander written in Rust"
@@ -81,7 +81,7 @@ section = "utility"
 license-file = ["../LICENSE", "1"]
 
 [package.metadata.deb.variants.wayland]
-depends = "$auto, systemd, libnotify-bin, libxkbcommon0, libwxgtk3.0-gtk3-0v5"
+depends = "$auto, systemd, libnotify-bin, libxkbcommon0, libwxgtk3.0-gtk3-0v5, wl-clipboard"
 # TODO: once this issue [1] is fixed, we should create a variant for 
 # wayland to automatically run the setcap script.
 # [1]: https://github.com/mmstick/cargo-deb/issues/151
diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs
index dd69176b..7371ffc3 100644
--- a/espanso/src/cli/worker/config.rs
+++ b/espanso/src/cli/worker/config.rs
@@ -197,3 +197,15 @@ impl<'a> espanso_engine::process::EnabledStatusProvider for ConfigManager<'a> {
     self.active().enable()
   }
 }
+
+impl<'a> crate::gui::modulo::form::ModuloFormUIOptionProvider for ConfigManager<'a> {
+  fn get_post_form_delay(&self) -> usize {
+    self.active().post_form_delay()
+  }
+}
+
+impl<'a> crate::gui::modulo::search::ModuloSearchUIOptionProvider for ConfigManager<'a> {
+  fn get_post_search_delay(&self) -> usize {
+    self.active().post_search_delay()
+  }
+}
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index 8581a626..d48269db 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -107,8 +107,10 @@ pub fn initialize_and_spawn(
       let default_config = &*config_manager.default();
 
       let modulo_manager = crate::gui::modulo::manager::ModuloManager::new();
-      let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager);
-      let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager);
+      let modulo_form_ui =
+        crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager, &config_manager);
+      let modulo_search_ui =
+        crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager, &config_manager);
       let modulo_text_ui = crate::gui::modulo::textview::ModuloTextUI::new(&modulo_manager);
 
       let context: Box<dyn Context> = Box::new(super::context::DefaultContext::new(
@@ -186,7 +188,8 @@ pub fn initialize_and_spawn(
       let clipboard_adapter = ClipboardAdapter::new(&*clipboard, &config_manager);
       let clipboard_extension =
         espanso_render::extension::clipboard::ClipboardExtension::new(&clipboard_adapter);
-      let date_extension = espanso_render::extension::date::DateExtension::new();
+      let locale_provider = espanso_render::extension::date::DefaultLocaleProvider::new();
+      let date_extension = espanso_render::extension::date::DateExtension::new(&locale_provider);
       let echo_extension = espanso_render::extension::echo::EchoExtension::new();
       // For backwards compatiblity purposes, the echo extension can also be called with "dummy" type
       let dummy_extension = espanso_render::extension::echo::EchoExtension::new_with_alias("dummy");
diff --git a/espanso/src/gui/modulo/form.rs b/espanso/src/gui/modulo/form.rs
index 6c0f778d..c483a135 100644
--- a/espanso/src/gui/modulo/form.rs
+++ b/espanso/src/gui/modulo/form.rs
@@ -20,18 +20,30 @@
 use serde::Serialize;
 use serde_json::{json, Map, Value};
 use std::collections::HashMap;
+use std::convert::TryInto;
 
 use crate::gui::{FormField, FormUI};
 
 use super::manager::ModuloManager;
 
+pub trait ModuloFormUIOptionProvider {
+  fn get_post_form_delay(&self) -> usize;
+}
+
 pub struct ModuloFormUI<'a> {
   manager: &'a ModuloManager,
+  option_provider: &'a dyn ModuloFormUIOptionProvider,
 }
 
 impl<'a> ModuloFormUI<'a> {
-  pub fn new(manager: &'a ModuloManager) -> Self {
-    Self { manager }
+  pub fn new(
+    manager: &'a ModuloManager,
+    option_provider: &'a dyn ModuloFormUIOptionProvider,
+  ) -> Self {
+    Self {
+      manager,
+      option_provider,
+    }
   }
 }
 
@@ -52,7 +64,7 @@ impl<'a> FormUI for ModuloFormUI<'a> {
       .manager
       .invoke(&["form", "-j", "-i", "-"], &json_config)?;
     let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
-    match json {
+    let result = match json {
       Ok(json) => {
         if json.is_empty() {
           Ok(None)
@@ -61,7 +73,16 @@ impl<'a> FormUI for ModuloFormUI<'a> {
         }
       }
       Err(error) => Err(error.into()),
+    };
+
+    let post_form_delay = self.option_provider.get_post_form_delay();
+    if post_form_delay > 0 {
+      std::thread::sleep(std::time::Duration::from_millis(
+        post_form_delay.try_into().unwrap(),
+      ));
     }
+
+    result
   }
 }
 
diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs
index 9f7ed516..a860e5db 100644
--- a/espanso/src/gui/modulo/manager.rs
+++ b/espanso/src/gui/modulo/manager.rs
@@ -105,6 +105,13 @@ impl ModuloManager {
                       error!("modulo reported an error: {}", error);
                     }
 
+                    if !child_output.status.success() {
+                      error!(
+                        "modulo exited with non-zero status code: {:?}",
+                        child_output.status.code()
+                      )
+                    }
+
                     if !output.trim().is_empty() {
                       Ok(output.to_string())
                     } else {
diff --git a/espanso/src/gui/modulo/search.rs b/espanso/src/gui/modulo/search.rs
index a55d525e..d8d0bac5 100644
--- a/espanso/src/gui/modulo/search.rs
+++ b/espanso/src/gui/modulo/search.rs
@@ -19,19 +19,30 @@
 
 use serde::Serialize;
 use serde_json::Value;
-use std::collections::HashMap;
+use std::{collections::HashMap, convert::TryInto};
 
 use crate::gui::{SearchItem, SearchUI};
 
 use super::manager::ModuloManager;
 
+pub trait ModuloSearchUIOptionProvider {
+  fn get_post_search_delay(&self) -> usize;
+}
+
 pub struct ModuloSearchUI<'a> {
   manager: &'a ModuloManager,
+  option_provider: &'a dyn ModuloSearchUIOptionProvider,
 }
 
 impl<'a> ModuloSearchUI<'a> {
-  pub fn new(manager: &'a ModuloManager) -> Self {
-    Self { manager }
+  pub fn new(
+    manager: &'a ModuloManager,
+    option_provider: &'a dyn ModuloSearchUIOptionProvider,
+  ) -> Self {
+    Self {
+      manager,
+      option_provider,
+    }
   }
 }
 
@@ -48,7 +59,7 @@ impl<'a> SearchUI for ModuloSearchUI<'a> {
       .manager
       .invoke(&["search", "-j", "-i", "-"], &json_config)?;
     let json: Result<HashMap<String, Value>, _> = serde_json::from_str(&output);
-    match json {
+    let result = match json {
       Ok(json) => {
         if let Some(Value::String(selected_id)) = json.get("selected") {
           Ok(Some(selected_id.clone()))
@@ -57,7 +68,16 @@ impl<'a> SearchUI for ModuloSearchUI<'a> {
         }
       }
       Err(error) => Err(error.into()),
+    };
+
+    let post_search_delay = self.option_provider.get_post_search_delay();
+    if post_search_delay > 0 {
+      std::thread::sleep(std::time::Duration::from_millis(
+        post_search_delay.try_into().unwrap(),
+      ));
     }
+
+    result
   }
 }
 
diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs
index e45567df..cd0a4922 100644
--- a/espanso/src/patch/patches/mod.rs
+++ b/espanso/src/patch/patches/mod.rs
@@ -48,6 +48,8 @@ generate_patchable_config!(
   backspace_limit -> usize,
   apply_patch -> bool,
   undo_backspace -> bool,
+  post_form_delay -> usize,
+  post_search_delay -> usize,
   win32_exclude_orphan_events -> bool,
   win32_keyboard_layout_cache_interval -> i64,
   x11_use_xclip_backend -> bool,
diff --git a/scripts/build_windows_resources.rs b/scripts/build_windows_resources.rs
index 0c11750d..6357b6e3 100644
--- a/scripts/build_windows_resources.rs
+++ b/scripts/build_windows_resources.rs
@@ -1,6 +1,6 @@
 //! ```cargo
 //! [dependencies]
-//! cc = "1.0.66"
+//! cc = "1.0.73"
 //! glob = "0.3.0"
 //! envmnt = "*"
 //! ```
@@ -26,7 +26,7 @@ fn main() {
 
   // First, we try to find the directory containing the various versions:
   // C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC\
-  let tool = cc::windows_registry::find_tool("msvc", "msbuild")
+  let tool = cc::windows_registry::find_tool("msvc", "devenv")
     .expect("unable to locate MSVC compiler, did you install Visual Studio?");
   let mut versions_dir = None;
   let mut current_root = tool.path();
@@ -54,14 +54,21 @@ fn main() {
     .next()
     .expect("unable to find vcruntime140_1.dll file")
     .expect("unable to extract path of vcruntime140_1.dll file");
-  
+
   // Copy the DLLs in the target directory
-  let parent_dir = target_file.parent().expect("unable to obtain directory containing DLLs");
-  for entry in glob::glob(&format!(r"{}\*.dll", parent_dir.to_string_lossy().to_string())).expect("unable to glob over DLLs") {
+  let parent_dir = target_file
+    .parent()
+    .expect("unable to obtain directory containing DLLs");
+  for entry in glob::glob(&format!(
+    r"{}\*.dll",
+    parent_dir.to_string_lossy().to_string()
+  ))
+  .expect("unable to glob over DLLs")
+  {
     let entry = entry.expect("unable to unwrap DLL entry");
     let filename = entry.file_name().expect("unable to obtain filename");
     std::fs::copy(&entry, target_dir.join(filename)).expect("unable to copy DLL");
-  } 
+  }
 
   // Copy the executable
   let exec_path = envmnt::get_or_panic("EXEC_PATH");
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 82b7b3ec..a57ec805 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
 name: espanso
-version: 2.1.3-alpha
+version: 2.1.4-beta
 summary: A Cross-platform Text Expander written in Rust
 description: |
   espanso is a Cross-platform, Text Expander written in Rust.


Test patch: