Repository: espanso/espanso (language: rust)

Issue:
Espanso's license is faulty
It's a quick fix, but take a look at https://github.com/federico-terzi/espanso/blob/master/LICENSE#L655
>\<program\>  Copyright (C) \<year\>  \<name of author\>

It is pretty clear what is missing

![image](https://user-images.githubusercontent.com/54688434/169499787-ec942cbd-b9d8-451a-8060-8ed71bd526e9.png)



Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 8c456495..b48a2daf 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -7,14 +7,6 @@ assignees: ''
 
 ---
 
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+Feature requests have been moved to discussions, please open it there :)
 
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
+https://github.com/espanso/espanso/discussions/categories/feature-requests-and-ideas
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f910d582..1cf4b63c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,12 @@
 
 name: CI
 
-on: [push, pull_request]
+on:
+  push:
+    branches:
+      - master
+      - dev
+  pull_request:
 
 env:
   CARGO_TERM_COLOR: always
@@ -16,8 +21,12 @@ jobs:
 
     runs-on: ${{ matrix.os }}
 
+    env:
+      WX_WIDGETS_BUILD_OUT_DIR: "${{github.workspace}}/wx-widgets-build"
+
     steps:
       - uses: actions/checkout@v2
+      - uses: Swatinem/rust-cache@v1
       - name: Check formatting
         run: |
           rustup component add rustfmt
@@ -27,26 +36,60 @@ jobs:
         run: |
           sudo apt-get update
           sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
+      - name: Install rust-script and cargo-make
+        run: |
+          cargo install rust-script --version "0.7.0"
+          cargo install cargo-make --version 0.34.0
+      # wxWidgets builds (internal to espanso-modulo) are by far the largest bottleneck
+      # in the current pipeline, so we cache the results for a faster compilation process
+      - name: "Cache wxWidgets builds"
+        if: ${{ runner.os != 'Linux' }}
+        uses: actions/cache@v3
+        with:
+          path: |
+            ${{env.WX_WIDGETS_BUILD_OUT_DIR}}
+          key: ${{ github.job }}-${{ runner.os }}-${{ hashFiles('espanso-modulo/build.rs') }}-${{ hashFiles('espanso-modulo/vendor/*') }}
+      - name: Build
+        run: |
+          cargo make build-binary
       - name: Check clippy
         run: |
           rustup component add clippy
           cargo clippy -- -D warnings
         env:
           MACOSX_DEPLOYMENT_TARGET: "10.13"
+
+  test:
+    strategy:
+      matrix:
+        os: [windows-latest, macos-latest, ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: Swatinem/rust-cache@v1
+      - name: Check formatting
+        run: |
+          rustup component add rustfmt
+          cargo fmt --all -- --check
+      - name: Install Linux dependencies
+        if: ${{ runner.os == 'Linux' }}
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
       - name: Install rust-script and cargo-make
         run: |
           cargo install rust-script --version "0.7.0"
-          cargo install --force cargo-make --version 0.34.0
+          cargo install cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary
-      - name: Build
-        run: |
-          cargo make build-binary
   
   build-wayland:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
+      - uses: Swatinem/rust-cache@v1
       - name: Check formatting
         run: |
           rustup component add rustfmt
@@ -55,29 +98,57 @@ jobs:
         run: |
           sudo apt-get update
           sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
+      - name: Install rust-script and cargo-make
+        run: |
+          cargo install rust-script --version "0.7.0"
+          cargo install cargo-make --version 0.34.0
+      - name: Build
+        run: cargo make build-binary --env NO_X11=true
       - name: Check clippy
         run: |
           rustup component add clippy
           cargo clippy -p espanso --features wayland -- -D warnings
+  
+  test-wayland:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: Swatinem/rust-cache@v1
+      - name: Check formatting
+        run: |
+          rustup component add rustfmt
+          cargo fmt --all -- --check
+      - name: Install Linux dependencies
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
       - name: Install rust-script and cargo-make
         run: |
           cargo install rust-script --version "0.7.0"
-          cargo install --force cargo-make --version 0.34.0
+          cargo install cargo-make --version 0.34.0
       - name: Run test suite
         run: cargo make test-binary --env NO_X11=true
-      - name: Build
-        run: cargo make build-binary --env NO_X11=true
   
   build-macos-arm:
     runs-on: macos-11
+    env:
+      WX_WIDGETS_BUILD_OUT_DIR: "${{github.workspace}}/wx-widgets-build"
     steps:
       - uses: actions/checkout@v2
       - name: Install target
         run: rustup update && rustup target add aarch64-apple-darwin
+      - uses: Swatinem/rust-cache@v1
       - name: Install rust-script and cargo-make
         run: |
           cargo install rust-script --version "0.7.0"
-          cargo install --force cargo-make --version 0.34.0
+          cargo install cargo-make --version 0.34.0
+      - name: "Cache wxWidgets builds"
+        if: ${{ runner.os != 'Linux' }}
+        uses: actions/cache@v3
+        with:
+          path: |
+            ${{env.WX_WIDGETS_BUILD_OUT_DIR}}
+          key: ${{ github.job }}-${{ runner.os }}-${{ hashFiles('espanso-modulo/build.rs') }}-${{ hashFiles('espanso-modulo/vendor/*') }}
       - name: Build
         run: |
           cargo make build-macos-arm-binary
@@ -85,5 +156,3 @@ jobs:
       #   uses: mxschmitt/action-tmate@v3
       #   with:
       #     limit-access-to-actor: true
-
-# TODO: add clippy check
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 8779c1a5..0a32ff42 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -264,12 +264,6 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d"
 
-[[package]]
-name = "const_fn"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
-
 [[package]]
 name = "const_format"
 version = "0.2.14"
@@ -332,9 +326,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80"
+checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-channel",
@@ -356,9 +350,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
@@ -367,12 +361,12 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.1"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
+checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
 dependencies = [
+ "autocfg",
  "cfg-if 1.0.0",
- "const_fn",
  "crossbeam-utils",
  "lazy_static",
  "memoffset",
@@ -381,9 +375,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.1"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
+checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-utils",
@@ -391,11 +385,10 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.1"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
 dependencies = [
- "autocfg",
  "cfg-if 1.0.0",
  "lazy_static",
 ]
@@ -589,7 +582,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "2.1.5-beta"
+version = "2.1.6-beta"
 dependencies = [
  "anyhow",
  "caps",
@@ -631,6 +624,7 @@ dependencies = [
  "serde_json",
  "serde_yaml",
  "simplelog",
+ "sysinfo",
  "tempdir",
  "thiserror",
  "widestring",
@@ -1402,9 +1396,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
 [[package]]
 name = "libc"
-version = "0.2.101"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
 
 [[package]]
 name = "libdbus-sys"
@@ -2198,6 +2192,30 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
 [[package]]
 name = "rdrand"
 version = "0.4.0"
@@ -2235,9 +2253,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.4.4"
+version = "1.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3"
+checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2614,6 +2632,21 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "sysinfo"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d80929a3b477bce3a64360ca82bfb361eacce1dcb7b1fb31e8e5e181e37c212"
+dependencies = [
+ "cfg-if 1.0.0",
+ "core-foundation-sys",
+ "libc",
+ "ntapi",
+ "once_cell",
+ "rayon",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "tempdir"
 version = "0.3.7"
diff --git a/LICENSE b/LICENSE
index f288702d..ee35e921 100644
--- a/LICENSE
+++ b/LICENSE
@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
 state the exclusion of warranty; and each file should have at least
 the "copyright" line and a pointer to where the full notice is found.
 
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
+    Espanso
+    Copyright (C) 2019-2022 Federico Terzi
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
   If the program does terminal interaction, make it output a short
 notice like this when it starts in an interactive mode:
 
-    <program>  Copyright (C) <year>  <name of author>
+    Espanso  Copyright (C) 2019-2022  Federico Terzi
     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
     This is free software, and you are welcome to redistribute it
     under certain conditions; type `show c' for details.
diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs
index 945841ed..ed1bb516 100644
--- a/espanso-config/src/config/mod.rs
+++ b/espanso-config/src/config/mod.rs
@@ -162,10 +162,22 @@ pub trait Config: Send + Sync {
   // not be targeted to the right application.
   fn post_search_delay(&self) -> usize;
 
+  // If enabled, Espanso emulates the Alt Code feature available on Windows
+  // (keeping ALT pressed and then typing a char code with the numpad).
+  // This feature is necessary on Windows because the mechanism used by Espanso
+  // to intercept keystrokes disables the Windows' native Alt code functionality
+  // as a side effect.
+  // Because many users relied on this feature, we try to bring it back by emulating it.
+  fn emulate_alt_codes(&self) -> bool;
+
   // 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;
 
+  // If true, use an alternative injection backend based on the `xdotool` library.
+  // This might improve the situation for certain locales/layouts on X11.
+  fn x11_use_xdotool_backend(&self) -> bool;
+
   // If true, filter out keyboard events without an explicit HID device source on Windows.
   // This is needed to filter out the software-generated events, including
   // those from espanso, but might need to be disabled when using some software-level keyboards.
@@ -212,6 +224,7 @@ pub trait Config: Send + Sync {
         secure_input_notification: {:?}
 
         x11_use_xclip_backend: {:?}
+        x11_use_xdotool_backend: {:?}
         win32_exclude_orphan_events: {:?}
         win32_keyboard_layout_cache_interval: {:?}
 
@@ -246,6 +259,7 @@ pub trait Config: Send + Sync {
       self.secure_input_notification(),
 
       self.x11_use_xclip_backend(),
+      self.x11_use_xdotool_backend(),
       self.win32_exclude_orphan_events(),
       self.win32_keyboard_layout_cache_interval(),
 
diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs
index 644b068d..c641684f 100644
--- a/espanso-config/src/config/parse/mod.rs
+++ b/espanso-config/src/config/parse/mod.rs
@@ -46,9 +46,11 @@ pub(crate) struct ParsedConfig {
   pub secure_input_notification: Option<bool>,
   pub post_form_delay: Option<usize>,
   pub post_search_delay: Option<usize>,
+  pub emulate_alt_codes: Option<bool>,
   pub win32_exclude_orphan_events: Option<bool>,
   pub win32_keyboard_layout_cache_interval: Option<i64>,
   pub x11_use_xclip_backend: Option<bool>,
+  pub x11_use_xdotool_backend: Option<bool>,
 
   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 64558db7..10e06f41 100644
--- a/espanso-config/src/config/parse/yaml.rs
+++ b/espanso-config/src/config/parse/yaml.rs
@@ -112,6 +112,9 @@ pub(crate) struct YAMLConfig {
   #[serde(default)]
   pub secure_input_notification: Option<bool>,
 
+  #[serde(default)]
+  pub emulate_alt_codes: Option<bool>,
+
   #[serde(default)]
   pub win32_exclude_orphan_events: Option<bool>,
 
@@ -121,6 +124,9 @@ pub(crate) struct YAMLConfig {
   #[serde(default)]
   pub x11_use_xclip_backend: Option<bool>,
 
+  #[serde(default)]
+  pub x11_use_xdotool_backend: Option<bool>,
+
   // Include/Exclude
   #[serde(default)]
   pub includes: Option<Vec<String>>,
@@ -210,9 +216,12 @@ impl TryFrom<YAMLConfig> for ParsedConfig {
       post_form_delay: yaml_config.post_form_delay,
       post_search_delay: yaml_config.post_search_delay,
 
+      emulate_alt_codes: yaml_config.emulate_alt_codes,
+
       win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
       win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
       x11_use_xclip_backend: yaml_config.x11_use_xclip_backend,
+      x11_use_xdotool_backend: yaml_config.x11_use_xdotool_backend,
 
       use_standard_includes: yaml_config.use_standard_includes,
       includes: yaml_config.includes,
@@ -270,9 +279,11 @@ mod tests {
     secure_input_notification: false
     post_form_delay: 300
     post_search_delay: 400
+    emulate_alt_codes: true
     win32_exclude_orphan_events: false
     win32_keyboard_layout_cache_interval: 300
     x11_use_xclip_backend: true
+    x11_use_xdotool_backend: true
       
     use_standard_includes: true
     includes: ["test1"]
@@ -324,11 +335,13 @@ mod tests {
         show_icon: Some(false),
         show_notifications: Some(false),
         secure_input_notification: Some(false),
+        emulate_alt_codes: Some(true),
         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),
+        x11_use_xdotool_backend: Some(true),
 
         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 7b7dcc33..76a9c282 100644
--- a/espanso-config/src/config/resolve.rs
+++ b/espanso-config/src/config/resolve.rs
@@ -299,6 +299,10 @@ impl Config for ResolvedConfig {
     self.parsed.secure_input_notification.unwrap_or(true)
   }
 
+  fn emulate_alt_codes(&self) -> bool {
+    self.parsed.emulate_alt_codes.unwrap_or(false)
+  }
+
   fn post_form_delay(&self) -> usize {
     self
       .parsed
@@ -331,6 +335,10 @@ impl Config for ResolvedConfig {
   fn x11_use_xclip_backend(&self) -> bool {
     self.parsed.x11_use_xclip_backend.unwrap_or(false)
   }
+
+  fn x11_use_xdotool_backend(&self) -> bool {
+    self.parsed.x11_use_xdotool_backend.unwrap_or(false)
+  }
 }
 
 impl ResolvedConfig {
@@ -412,11 +420,13 @@ impl ResolvedConfig {
       show_icon,
       show_notifications,
       secure_input_notification,
+      emulate_alt_codes,
       post_form_delay,
       post_search_delay,
       win32_exclude_orphan_events,
       win32_keyboard_layout_cache_interval,
       x11_use_xclip_backend,
+      x11_use_xdotool_backend,
       includes,
       excludes,
       extra_includes,
diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs
index 24a94aa2..33589de7 100644
--- a/espanso-config/src/legacy/mod.rs
+++ b/espanso-config/src/legacy/mod.rs
@@ -395,6 +395,10 @@ impl Config for LegacyInteropConfig {
     crate::config::default::DEFAULT_POST_SEARCH_DELAY
   }
 
+  fn emulate_alt_codes(&self) -> bool {
+    false
+  }
+
   fn win32_exclude_orphan_events(&self) -> bool {
     true
   }
@@ -410,6 +414,10 @@ impl Config for LegacyInteropConfig {
   fn x11_use_xclip_backend(&self) -> bool {
     false
   }
+
+  fn x11_use_xdotool_backend(&self) -> bool {
+    false
+  }
 }
 
 struct LegacyMatchGroup {
diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs
index 4f5f5c36..9c90ca35 100644
--- a/espanso-detect/src/evdev/mod.rs
+++ b/espanso-detect/src/evdev/mod.rs
@@ -379,6 +379,18 @@ fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
     0xFFD0 => (F19, None),
     0xFFD1 => (F20, None),
 
+    // Numpad
+    0xFFB0 => (Numpad0, None),
+    0xFFB1 => (Numpad1, None),
+    0xFFB2 => (Numpad2, None),
+    0xFFB3 => (Numpad3, None),
+    0xFFB4 => (Numpad4, None),
+    0xFFB5 => (Numpad5, None),
+    0xFFB6 => (Numpad6, None),
+    0xFFB7 => (Numpad7, None),
+    0xFFB8 => (Numpad8, None),
+    0xFFB9 => (Numpad9, None),
+
     // Other keys, includes the raw code provided by the operating system
     _ => (Other(key_sym), None),
   }
diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs
index bfd89149..30090c54 100644
--- a/espanso-detect/src/event.rs
+++ b/espanso-detect/src/event.rs
@@ -125,6 +125,18 @@ pub enum Key {
   F19,
   F20,
 
+  // Numpad keys
+  Numpad0,
+  Numpad1,
+  Numpad2,
+  Numpad3,
+  Numpad4,
+  Numpad5,
+  Numpad6,
+  Numpad7,
+  Numpad8,
+  Numpad9,
+
   // Other keys, includes the raw code provided by the operating system
   Other(i32),
 }
diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs
index 5f49ea01..88606a10 100644
--- a/espanso-detect/src/mac/mod.rs
+++ b/espanso-detect/src/mac/mod.rs
@@ -448,6 +448,18 @@ fn key_code_to_key(key_code: i32) -> (Key, Option<Variant>) {
     0x50 => (F19, None),
     0x5A => (F20, None),
 
+    // Numpad
+    0x52 => (Numpad0, None),
+    0x53 => (Numpad1, None),
+    0x54 => (Numpad2, None),
+    0x55 => (Numpad3, None),
+    0x56 => (Numpad4, None),
+    0x57 => (Numpad5, None),
+    0x58 => (Numpad6, None),
+    0x59 => (Numpad7, None),
+    0x5B => (Numpad8, None),
+    0x5C => (Numpad9, None),
+
     // Other keys, includes the raw code provided by the operating system
     _ => (Other(key_code), None),
   }
diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs
index dbde5731..c0f87385 100644
--- a/espanso-detect/src/win32/mod.rs
+++ b/espanso-detect/src/win32/mod.rs
@@ -395,6 +395,18 @@ fn key_code_to_key(key_code: i32) -> (Key, Option<Variant>) {
     0x82 => (F19, None),
     0x83 => (F20, None),
 
+    // Numpad
+    0x60 => (Numpad0, None),
+    0x61 => (Numpad1, None),
+    0x62 => (Numpad2, None),
+    0x63 => (Numpad3, None),
+    0x64 => (Numpad4, None),
+    0x65 => (Numpad5, None),
+    0x66 => (Numpad6, None),
+    0x67 => (Numpad7, None),
+    0x68 => (Numpad8, None),
+    0x69 => (Numpad9, None),
+
     // Other keys, includes the raw code provided by the operating system
     _ => (Other(key_code), None),
   }
diff --git a/espanso-detect/src/x11/mod.rs b/espanso-detect/src/x11/mod.rs
index 8665ee4f..9680d5ae 100644
--- a/espanso-detect/src/x11/mod.rs
+++ b/espanso-detect/src/x11/mod.rs
@@ -396,6 +396,18 @@ fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
     0xFFD0 => (F19, None),
     0xFFD1 => (F20, None),
 
+    // Numpad
+    0xFFB0 => (Numpad0, None),
+    0xFFB1 => (Numpad1, None),
+    0xFFB2 => (Numpad2, None),
+    0xFFB3 => (Numpad3, None),
+    0xFFB4 => (Numpad4, None),
+    0xFFB5 => (Numpad5, None),
+    0xFFB6 => (Numpad6, None),
+    0xFFB7 => (Numpad7, None),
+    0xFFB8 => (Numpad8, None),
+    0xFFB9 => (Numpad9, None),
+
     // Other keys, includes the raw code provided by the operating system
     _ => (Other(key_sym), None),
   }
diff --git a/espanso-engine/src/event/input.rs b/espanso-engine/src/event/input.rs
index ebe830e9..fc22cf5c 100644
--- a/espanso-engine/src/event/input.rs
+++ b/espanso-engine/src/event/input.rs
@@ -108,6 +108,18 @@ pub enum Key {
   F19,
   F20,
 
+  // Numpad keys
+  Numpad0,
+  Numpad1,
+  Numpad2,
+  Numpad3,
+  Numpad4,
+  Numpad5,
+  Numpad6,
+  Numpad7,
+  Numpad8,
+  Numpad9,
+
   // Other keys, includes the raw code provided by the operating system
   Other(i32),
 }
diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs
index 4bb8efdf..0c943409 100644
--- a/espanso-engine/src/process/default.rs
+++ b/espanso-engine/src/process/default.rs
@@ -22,6 +22,7 @@ use log::trace;
 use super::{
   middleware::{
     action::{ActionMiddleware, EventSequenceProvider},
+    alt_code_synthesizer::AltCodeSynthesizerMiddleware,
     cause::CauseCompensateMiddleware,
     cursor_hint::CursorHintMiddleware,
     delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
@@ -32,10 +33,10 @@ use super::{
     multiplex::MultiplexMiddleware,
     render::RenderMiddleware,
   },
-  DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
-  MatchResolver, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware,
-  ModifierStateProvider, Multiplexer, NotificationManager, PathProvider, Processor, Renderer,
-  UndoEnabledProvider,
+  AltCodeSynthEnabledProvider, DisableOptions, EnabledStatusProvider, MatchFilter,
+  MatchInfoProvider, MatchProvider, MatchResolver, MatchSelector, Matcher,
+  MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider, Multiplexer,
+  NotificationManager, PathProvider, Processor, Renderer, UndoEnabledProvider,
 };
 use crate::{
   event::{Event, EventType},
@@ -74,6 +75,7 @@ impl<'a> DefaultProcessor<'a> {
     modifier_state_provider: &'a dyn ModifierStateProvider,
     match_resolver: &'a dyn MatchResolver,
     notification_manager: &'a dyn NotificationManager,
+    alt_code_synth_enabled_provider: &'a dyn AltCodeSynthEnabledProvider,
   ) -> DefaultProcessor<'a> {
     Self {
       event_queue: VecDeque::new(),
@@ -81,6 +83,9 @@ impl<'a> DefaultProcessor<'a> {
         Box::new(EventsDiscardMiddleware::new()),
         Box::new(DisableMiddleware::new(disable_options)),
         Box::new(IconStatusMiddleware::new()),
+        Box::new(AltCodeSynthesizerMiddleware::new(
+          alt_code_synth_enabled_provider,
+        )),
         Box::new(MatcherMiddleware::new(
           matchers,
           matcher_options_provider,
diff --git a/espanso-engine/src/process/middleware/alt_code_synthesizer.rs b/espanso-engine/src/process/middleware/alt_code_synthesizer.rs
new file mode 100644
index 00000000..9cbd78f1
--- /dev/null
+++ b/espanso-engine/src/process/middleware/alt_code_synthesizer.rs
@@ -0,0 +1,675 @@
+/*
+ * 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/>.
+ */
+
+use std::cell::RefCell;
+
+use log::debug;
+
+use super::super::Middleware;
+use crate::event::{
+  effect::TextInjectRequest,
+  input::{Key, Status},
+  Event, EventType,
+};
+
+pub trait AltCodeSynthEnabledProvider {
+  fn is_alt_code_synthesizer_enabled(&self) -> bool;
+}
+
+pub struct AltCodeSynthesizerMiddleware<'a> {
+  code_buffer: RefCell<Option<String>>,
+  enabled_state_provider: &'a dyn AltCodeSynthEnabledProvider,
+}
+
+impl<'a> AltCodeSynthesizerMiddleware<'a> {
+  pub fn new(enabled_state_provider: &'a dyn AltCodeSynthEnabledProvider) -> Self {
+    Self {
+      code_buffer: RefCell::new(None),
+      enabled_state_provider,
+    }
+  }
+}
+
+impl<'a> Middleware for AltCodeSynthesizerMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "alt_code_synthesizer"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    match &event.etype {
+      EventType::Keyboard(keyboard_event)
+        if self
+          .enabled_state_provider
+          .is_alt_code_synthesizer_enabled() =>
+      {
+        let mut code_buffer = self.code_buffer.borrow_mut();
+
+        if keyboard_event.status == Status::Pressed {
+          if let Key::Alt = &keyboard_event.key {
+            *code_buffer = Some("".to_owned());
+          } else if let Some(buffer) = &mut *code_buffer {
+            match keyboard_event.key {
+              Key::Numpad0 => buffer.push('0'),
+              Key::Numpad1 => buffer.push('1'),
+              Key::Numpad2 => buffer.push('2'),
+              Key::Numpad3 => buffer.push('3'),
+              Key::Numpad4 => buffer.push('4'),
+              Key::Numpad5 => buffer.push('5'),
+              Key::Numpad6 => buffer.push('6'),
+              Key::Numpad7 => buffer.push('7'),
+              Key::Numpad8 => buffer.push('8'),
+              Key::Numpad9 => buffer.push('9'),
+              _ => {}
+            }
+          }
+        } else if keyboard_event.key == Key::Alt {
+          if let Some(codes) = &*code_buffer {
+            if let Some(target_char) = convert_buffer_into_char(codes) {
+              dispatch(Event::caused_by(
+                event.source_id,
+                EventType::TextInject(TextInjectRequest {
+                  text: target_char,
+                  ..Default::default()
+                }),
+              ));
+            }
+          }
+
+          *code_buffer = None;
+        }
+      }
+      _ => {}
+    }
+
+    event
+  }
+}
+
+fn convert_buffer_into_char(buffer: &str) -> Option<String> {
+  if buffer.is_empty() {
+    debug!("unable to generate ALT code as the buffer is empty");
+    return None;
+  }
+
+  if !buffer.chars().all(char::is_numeric) {
+    debug!("unable to generate ALT code as some of the buffer chars are not numeric");
+    return None;
+  }
+
+  // According to: https://en.wikipedia.org/wiki/Alt_code
+  // The conversion works as follow:
+  // * If the number is smaller than 256 and does not start with 0, then we use a patched version of the CP437 encoding
+  // * If the number is smaller than 256 and starts with 0 we use the CP1252 encoding
+  // * If the number is greater or equal than 256 we use the unicode encoding
+
+  let code = buffer.parse::<u32>().ok()?;
+  let unicode_code = if code >= 256 {
+    // Unicode
+    code
+  } else if buffer.starts_with('0') {
+    // CP1252
+    convert_cp1252_code_to_unicode(code)?
+  } else {
+    // CP437
+    convert_cp437_code_to_unicode(code)?
+  };
+
+  char::from_u32(unicode_code).map(|c| c.to_string())
+}
+
+// Taken from: https://altcodeunicode.com/
+fn convert_cp437_code_to_unicode(code: u32) -> Option<u32> {
+  match code {
+    0 => Some(0x0000),   // Control character - null (NUL)
+    1 => Some(0x263A),   // White smiling face, smiley face
+    2 => Some(0x263B),   // Black smiling face
+    3 => Some(0x2665),   // Black heart suit
+    4 => Some(0x2666),   // Black diamond suit
+    5 => Some(0x2663),   // Black club suit
+    6 => Some(0x2660),   // Black spade suit
+    7 => Some(0x2022),   // Bullet
+    8 => Some(0x25D8),   // Inverse bullet
+    9 => Some(0x25CB),   // White circle
+    10 => Some(0x25D9),  // Inverse white circle
+    11 => Some(0x2642),  // Male sign, mars, alchemical symbol for iron
+    12 => Some(0x2640),  // Female sign, venus, alchemical symbol for copper
+    13 => Some(0x266A),  // Eighth note, quaver
+    14 => Some(0x266B),  // Beamed eighth notes, barred eighth notes, beamed quavers
+    15 => Some(0x263C),  // White sun with rays
+    16 => Some(0x25BA),  // Black right-pointing pointer
+    17 => Some(0x25C4),  // Black left-pointing pointer
+    18 => Some(0x2195),  // Up down arrow
+    19 => Some(0x203C),  // Double exclamation mark
+    20 => Some(0x00B6),  // Pilcrow sign, paragraph sign
+    21 => Some(0x00A7),  // Section sign
+    22 => Some(0x25AC),  // Black rectangle
+    23 => Some(0x21A8),  // Up down arrow with base
+    24 => Some(0x2191),  // Upwards arrow
+    25 => Some(0x2193),  // Downwards arrow
+    26 => Some(0x2192),  // Rightwards arrow, Z notation total function
+    27 => Some(0x2190),  // Leftwards arrow
+    28 => Some(0x221F),  // Right angle
+    29 => Some(0x2194),  // Left right arrow, Z notation relation
+    30 => Some(0x25B2),  // Black up-pointing triangle
+    31 => Some(0x25BC),  // Black down-pointing triangle
+    32 => Some(0x0020),  // Space
+    33 => Some(0x0021),  // Exclamation mark, factorial
+    34 => Some(0x0022),  // Quotation mark
+    35 => Some(0x0023),  // Number sign, pound sign, hash, crosshatch, octothorpe
+    36 => Some(0x0024),  // Dollar sign, milréis, escudo
+    37 => Some(0x0025),  // Percent sign
+    38 => Some(0x0026),  // Ampersand
+    39 => Some(0x0027),  // Apostrophe
+    40 => Some(0x0028),  // Left parenthesis, opening parenthesis
+    41 => Some(0x0029),  // Right parenthesis, closing parenthesis
+    42 => Some(0x002A),  // Asterisk, star
+    43 => Some(0x002B),  // Plus sign
+    44 => Some(0x002C),  // Comma, decimal separator
+    45 => Some(0x002D),  // Hyphen, minus sign
+    46 => Some(0x002E),  // Full stop, period, dot, decimal point
+    47 => Some(0x002F),  // Solidus, slash, forward slash, virgule
+    48 => Some(0x0030),  // Digit zero
+    49 => Some(0x0031),  // Digit one
+    50 => Some(0x0032),  // Digit two
+    51 => Some(0x0033),  // Digit three
+    52 => Some(0x0034),  // Digit four
+    53 => Some(0x0035),  // Digit five
+    54 => Some(0x0036),  // Digit six
+    55 => Some(0x0037),  // Digit seven
+    56 => Some(0x0038),  // Digit eight
+    57 => Some(0x0039),  // Digit nine
+    58 => Some(0x003A),  // Colon
+    59 => Some(0x003B),  // Semicolon
+    60 => Some(0x003C),  // Less-than sign
+    61 => Some(0x003D),  // Equals sign
+    62 => Some(0x003E),  // Greater-than sign
+    63 => Some(0x003F),  // Question mark
+    64 => Some(0x0040),  // Commercial at, at sign
+    65 => Some(0x0041),  // Latin capital letter A
+    66 => Some(0x0042),  // Latin capital letter B
+    67 => Some(0x0043),  // Latin capital letter C
+    68 => Some(0x0044),  // Latin capital letter D
+    69 => Some(0x0045),  // Latin capital letter E
+    70 => Some(0x0046),  // Latin capital letter F
+    71 => Some(0x0047),  // Latin capital letter G
+    72 => Some(0x0048),  // Latin capital letter H
+    73 => Some(0x0049),  // Latin capital letter I
+    74 => Some(0x004A),  // Latin capital letter J
+    75 => Some(0x004B),  // Latin capital letter K
+    76 => Some(0x004C),  // Latin capital letter L
+    77 => Some(0x004D),  // Latin capital letter M
+    78 => Some(0x004E),  // Latin capital letter N
+    79 => Some(0x004F),  // Latin capital letter O
+    80 => Some(0x0050),  // Latin capital letter P
+    81 => Some(0x0051),  // Latin capital letter Q
+    82 => Some(0x0052),  // Latin capital letter R
+    83 => Some(0x0053),  // Latin capital letter S
+    84 => Some(0x0054),  // Latin capital letter T
+    85 => Some(0x0055),  // Latin capital letter U
+    86 => Some(0x0056),  // Latin capital letter V
+    87 => Some(0x0057),  // Latin capital letter W
+    88 => Some(0x0058),  // Latin capital letter X
+    89 => Some(0x0059),  // Latin capital letter Y
+    90 => Some(0x005A),  // Latin capital letter Z
+    91 => Some(0x005B),  // Left square bracket, opening square bracket
+    92 => Some(0x005C),  // Reverse solidus, back slash
+    93 => Some(0x005D),  // Right square bracket, closing square bracket
+    94 => Some(0x005E),  // Circumflex accent
+    95 => Some(0x005F),  // Low line, underscore
+    96 => Some(0x0060),  // Grave accent
+    97 => Some(0x0061),  // Latin small letter a
+    98 => Some(0x0062),  // Latin small letter b
+    99 => Some(0x0063),  // Latin small letter c
+    100 => Some(0x0064), // Latin small letter d
+    101 => Some(0x0065), // Latin small letter e
+    102 => Some(0x0066), // Latin small letter f
+    103 => Some(0x0067), // Latin small letter g
+    104 => Some(0x0068), // Latin small letter h
+    105 => Some(0x0069), // Latin small letter i
+    106 => Some(0x006A), // Latin small letter j
+    107 => Some(0x006B), // Latin small letter k
+    108 => Some(0x006C), // Latin small letter l
+    109 => Some(0x006D), // Latin small letter m
+    110 => Some(0x006E), // Latin small letter n
+    111 => Some(0x006F), // Latin small letter o
+    112 => Some(0x0070), // Latin small letter p
+    113 => Some(0x0071), // Latin small letter q
+    114 => Some(0x0072), // Latin small letter r
+    115 => Some(0x0073), // Latin small letter s
+    116 => Some(0x0074), // Latin small letter t
+    117 => Some(0x0075), // Latin small letter u
+    118 => Some(0x0076), // Latin small letter v
+    119 => Some(0x0077), // Latin small letter w
+    120 => Some(0x0078), // Latin small letter x
+    121 => Some(0x0079), // Latin small letter y
+    122 => Some(0x007A), // Latin small letter z
+    123 => Some(0x007B), // Left curly bracket, opening curly bracket, left brace
+    124 => Some(0x007C), // Vertical line, vertical bar
+    125 => Some(0x007D), // Right curly bracket, closing curly bracket, right brace
+    126 => Some(0x007E), // Tilde
+    127 => Some(0x2302), // House
+    128 => Some(0x00C7), // Latin capital letter C with cedilla
+    129 => Some(0x00FC), // Latin small letter u with diaeresis
+    130 => Some(0x00E9), // Latin small letter e with acute
+    131 => Some(0x00E2), // Latin small letter a with circumflex
+    132 => Some(0x00E4), // Latin small letter a with diaeresis
+    133 => Some(0x00E0), // Latin small letter a with grave
+    134 => Some(0x00E5), // Latin small letter a with ring above
+    135 => Some(0x00E7), // Latin small letter c with cedilla
+    136 => Some(0x00EA), // Latin small letter e with circumflex
+    137 => Some(0x00EB), // Latin small letter e with diaeresis
+    138 => Some(0x00E8), // Latin small letter e with grave
+    139 => Some(0x00EF), // Latin small letter i with diaeresis
+    140 => Some(0x00EE), // Latin small letter i with circumflex
+    141 => Some(0x00EC), // Latin small letter i with grave
+    142 => Some(0x00C4), // Latin capital letter A with diaeresis
+    143 => Some(0x00C5), // Latin capital letter A with ring above
+    144 => Some(0x00C9), // Latin capital letter E with acute
+    145 => Some(0x00E6), // Latin small letter ae, ash (from Old English æsc)
+    146 => Some(0x00C6), // Latin capital letter AE
+    147 => Some(0x00F4), // Latin small letter o with circumflex
+    148 => Some(0x00F6), // Latin small letter o with diaeresis
+    149 => Some(0x00F2), // Latin small letter o with grave
+    150 => Some(0x00FB), // Latin small letter u with circumflex
+    151 => Some(0x00F9), // Latin small letter u with grave
+    152 => Some(0x00FF), // Latin small letter y with diaeresis
+    153 => Some(0x00D6), // Latin capital letter O with diaeresis
+    154 => Some(0x00DC), // Latin capital letter U with diaeresis
+    155 => Some(0x00A2), // Cent sign
+    156 => Some(0x00A3), // Pound sign, pound sterling, Irish punt, lira sign
+    157 => Some(0x00A5), // Yen sign, yuan sign
+    158 => Some(0x20A7), // Peseta sign
+    159 => Some(0x0192), // Latin small letter f with hook, florin currency symbol, function symbol
+    160 => Some(0x00E1), // Latin small letter a with acute
+    161 => Some(0x00ED), // Latin small letter i with acute
+    162 => Some(0x00F3), // Latin small letter o with acute
+    163 => Some(0x00FA), // Latin small letter u with acute
+    164 => Some(0x00F1), // Latin small letter n with tilde, small letter enye
+    165 => Some(0x00D1), // Latin capital letter N with tilde, capital letter enye
+    166 => Some(0x00AA), // Feminine ordinal indicator
+    167 => Some(0x00BA), // Masculine ordinal indicator
+    168 => Some(0x00BF), // Inverted question mark, turned question mark
+    169 => Some(0x2310), // Reversed not sign, beginning of line
+    170 => Some(0x00AC), // Not sign, angled dash
+    171 => Some(0x00BD), // Vulgar fraction one half
+    172 => Some(0x00BC), // Vulgar fraction one quarter
+    173 => Some(0x00A1), // Inverted exclamation mark
+    174 => Some(0x00AB), // Left-pointing double angle quotation mark, left guillemet, chevrons (in typography)
+    175 => Some(0x00BB), // Right-pointing double angle quotation mark, right guillemet
+    176 => Some(0x2591), // Light shade
+    177 => Some(0x2592), // Medium shade, speckles fill, dotted fill
+    178 => Some(0x2593), // Dark shade
+    179 => Some(0x2502), // Box drawings light vertical
+    180 => Some(0x2524), // Box drawings light vertical and left
+    181 => Some(0x2561), // Box drawings vertical single and left double
+    182 => Some(0x2562), // Box drawings vertical double and left single
+    183 => Some(0x2556), // Box drawings down double and left single
+    184 => Some(0x2555), // Box drawings down single and left double
+    185 => Some(0x2563), // Box drawings double vertical and left
+    186 => Some(0x2551), // Box drawings double vertical
+    187 => Some(0x2557), // Box drawings double down and left
+    188 => Some(0x255D), // Box drawings double up and left
+    189 => Some(0x255C), // Box drawings up double and left single
+    190 => Some(0x255B), // Box drawings up single and left double
+    191 => Some(0x2510), // Box drawings light down and left
+    192 => Some(0x2514), // Box drawings light up and right
+    193 => Some(0x2534), // Box drawings light up and horizontal
+    194 => Some(0x252C), // Box drawings light down and horizontal
+    195 => Some(0x251C), // Box drawings light vertical and right
+    196 => Some(0x2500), // Box drawings light horizontal
+    197 => Some(0x253C), // Box drawings light vertical and horizontal
+    198 => Some(0x255E), // Box drawings vertical single and right double
+    199 => Some(0x255F), // Box drawings vertical double and right single
+    200 => Some(0x255A), // Box drawings double up and right
+    201 => Some(0x2554), // Box drawings double down and right
+    202 => Some(0x2569), // Box drawings double up and horizontal
+    203 => Some(0x2566), // Box drawings double down and horizontal
+    204 => Some(0x2560), // Box drawings double vertical and right
+    205 => Some(0x2550), // Box drawings double horizontal
+    206 => Some(0x256C), // Box drawings double vertical and horizontal
+    207 => Some(0x2567), // Box drawings up single and horizontal double
+    208 => Some(0x2568), // Box drawings up double and horizontal single
+    209 => Some(0x2564), // Box drawings down single and horizontal double
+    210 => Some(0x2565), // Box drawings down double and horizontal single
+    211 => Some(0x2559), // Box drawings up double and right single
+    212 => Some(0x2558), // Box drawings up single and right double
+    213 => Some(0x2552), // Box drawings down single and right double
+    214 => Some(0x2553), // Box drawings down double and right single
+    215 => Some(0x256B), // Box drawings vertical double and horizontal single
+    216 => Some(0x256A), // Box drawings vertical single and horizontal double
+    217 => Some(0x2518), // Box drawings light up and left
+    218 => Some(0x250C), // Box drawings light down and right
+    219 => Some(0x2588), // Full block, solid block
+    220 => Some(0x2584), // Lower half block
+    221 => Some(0x258C), // Left half block
+    222 => Some(0x2590), // Right half block
+    223 => Some(0x2580), // Upper half block
+    224 => Some(0x03B1), // Greek small letter alpha
+    225 => Some(0x00DF), // Latin small letter sharp s, eszett
+    226 => Some(0x0393), // Greek capital letter gamma
+    227 => Some(0x03C0), // Greek small letter pi
+    228 => Some(0x03A3), // Greek capital letter sigma
+    229 => Some(0x03C3), // Greek small letter sigma
+    230 => Some(0x00B5), // Micro sign
+    231 => Some(0x03A4), // Greek capital letter tau
+    232 => Some(0x03A6), // Greek capital letter phi
+    233 => Some(0x0398), // Greek capital letter theta
+    234 => Some(0x03A9), // Greek capital letter omega
+    235 => Some(0x03B4), // Greek small letter delta
+    236 => Some(0x221E), // Infinity
+    237 => Some(0x03C6), // Greek small letter phi
+    238 => Some(0x03B5), // Greek small letter epsilon
+    239 => Some(0x2229), // Intersection
+    240 => Some(0x2261), // Identical to
+    241 => Some(0x00B1), // Plus-minus sign
+    242 => Some(0x2265), // Greater-than or equal to
+    243 => Some(0x2264), // Less-than or equal to
+    244 => Some(0x2320), // Top half integral
+    245 => Some(0x2321), // Bottom half integral
+    246 => Some(0x00F7), // Division sign, obelus
+    247 => Some(0x2248), // Almost equal to, asymptotic to
+    248 => Some(0x00B0), // Degree sign
+    249 => Some(0x2219), // Bullet operator
+    250 => Some(0x00B7), // Middle dot, midpoint (in typography), interpunct, Georgian comma, Greek ano teleia
+    251 => Some(0x221A), // Square root, radical sign
+    252 => Some(0x207F), // Superscript Latin small letter n
+    253 => Some(0x00B2), // Superscript two, squared
+    254 => Some(0x25A0), // Black square
+    255 => Some(0x00A0), // No-break space, non-breaking space, nbsp
+    _ => None,
+  }
+}
+
+// Taken from here: https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT
+/*
+#
+#    Name:     cp1252 to Unicode table
+#    Unicode version: 2.0
+#    Table version: 2.01
+#    Table format:  Format A
+#    Date:          04/15/98
+#
+#    Contact:       Shawn.Steele@microsoft.com
+#
+#    General notes: none
+#
+#    Format: Three tab-separated columns
+#        Column #1 is the cp1252 code (in hex)
+#        Column #2 is the Unicode (in hex as 0xXXXX)
+#        Column #3 is the Unicode name (follows a comment sign, '#')
+#
+#    The entries are in cp1252 order
+#
+*/
+fn convert_cp1252_code_to_unicode(code: u32) -> Option<u32> {
+  match code {
+    0x00 => Some(0x0000), // #NULL
+    0x01 => Some(0x0001), // #START OF HEADING
+    0x02 => Some(0x0002), // #START OF TEXT
+    0x03 => Some(0x0003), // #END OF TEXT
+    0x04 => Some(0x0004), // #END OF TRANSMISSION
+    0x05 => Some(0x0005), // #ENQUIRY
+    0x06 => Some(0x0006), // #ACKNOWLEDGE
+    0x07 => Some(0x0007), // #BELL
+    0x08 => Some(0x0008), // #BACKSPACE
+    0x09 => Some(0x0009), // #HORIZONTAL TABULATION
+    0x0A => Some(0x000A), // #LINE FEED
+    0x0B => Some(0x000B), // #VERTICAL TABULATION
+    0x0C => Some(0x000C), // #FORM FEED
+    0x0D => Some(0x000D), // #CARRIAGE RETURN
+    0x0E => Some(0x000E), // #SHIFT OUT
+    0x0F => Some(0x000F), // #SHIFT IN
+    0x10 => Some(0x0010), // #DATA LINK ESCAPE
+    0x11 => Some(0x0011), // #DEVICE CONTROL ONE
+    0x12 => Some(0x0012), // #DEVICE CONTROL TWO
+    0x13 => Some(0x0013), // #DEVICE CONTROL THREE
+    0x14 => Some(0x0014), // #DEVICE CONTROL FOUR
+    0x15 => Some(0x0015), // #NEGATIVE ACKNOWLEDGE
+    0x16 => Some(0x0016), // #SYNCHRONOUS IDLE
+    0x17 => Some(0x0017), // #END OF TRANSMISSION BLOCK
+    0x18 => Some(0x0018), // #CANCEL
+    0x19 => Some(0x0019), // #END OF MEDIUM
+    0x1A => Some(0x001A), // #SUBSTITUTE
+    0x1B => Some(0x001B), // #ESCAPE
+    0x1C => Some(0x001C), // #FILE SEPARATOR
+    0x1D => Some(0x001D), // #GROUP SEPARATOR
+    0x1E => Some(0x001E), // #RECORD SEPARATOR
+    0x1F => Some(0x001F), // #UNIT SEPARATOR
+    0x20 => Some(0x0020), // #SPACE
+    0x21 => Some(0x0021), // #EXCLAMATION MARK
+    0x22 => Some(0x0022), // #QUOTATION MARK
+    0x23 => Some(0x0023), // #NUMBER SIGN
+    0x24 => Some(0x0024), // #DOLLAR SIGN
+    0x25 => Some(0x0025), // #PERCENT SIGN
+    0x26 => Some(0x0026), // #AMPERSAND
+    0x27 => Some(0x0027), // #APOSTROPHE
+    0x28 => Some(0x0028), // #LEFT PARENTHESIS
+    0x29 => Some(0x0029), // #RIGHT PARENTHESIS
+    0x2A => Some(0x002A), // #ASTERISK
+    0x2B => Some(0x002B), // #PLUS SIGN
+    0x2C => Some(0x002C), // #COMMA
+    0x2D => Some(0x002D), // #HYPHEN-MINUS
+    0x2E => Some(0x002E), // #FULL STOP
+    0x2F => Some(0x002F), // #SOLIDUS
+    0x30 => Some(0x0030), // #DIGIT ZERO
+    0x31 => Some(0x0031), // #DIGIT ONE
+    0x32 => Some(0x0032), // #DIGIT TWO
+    0x33 => Some(0x0033), // #DIGIT THREE
+    0x34 => Some(0x0034), // #DIGIT FOUR
+    0x35 => Some(0x0035), // #DIGIT FIVE
+    0x36 => Some(0x0036), // #DIGIT SIX
+    0x37 => Some(0x0037), // #DIGIT SEVEN
+    0x38 => Some(0x0038), // #DIGIT EIGHT
+    0x39 => Some(0x0039), // #DIGIT NINE
+    0x3A => Some(0x003A), // #COLON
+    0x3B => Some(0x003B), // #SEMICOLON
+    0x3C => Some(0x003C), // #LESS-THAN SIGN
+    0x3D => Some(0x003D), // #EQUALS SIGN
+    0x3E => Some(0x003E), // #GREATER-THAN SIGN
+    0x3F => Some(0x003F), // #QUESTION MARK
+    0x40 => Some(0x0040), // #COMMERCIAL AT
+    0x41 => Some(0x0041), // #LATIN CAPITAL LETTER A
+    0x42 => Some(0x0042), // #LATIN CAPITAL LETTER B
+    0x43 => Some(0x0043), // #LATIN CAPITAL LETTER C
+    0x44 => Some(0x0044), // #LATIN CAPITAL LETTER D
+    0x45 => Some(0x0045), // #LATIN CAPITAL LETTER E
+    0x46 => Some(0x0046), // #LATIN CAPITAL LETTER F
+    0x47 => Some(0x0047), // #LATIN CAPITAL LETTER G
+    0x48 => Some(0x0048), // #LATIN CAPITAL LETTER H
+    0x49 => Some(0x0049), // #LATIN CAPITAL LETTER I
+    0x4A => Some(0x004A), // #LATIN CAPITAL LETTER J
+    0x4B => Some(0x004B), // #LATIN CAPITAL LETTER K
+    0x4C => Some(0x004C), // #LATIN CAPITAL LETTER L
+    0x4D => Some(0x004D), // #LATIN CAPITAL LETTER M
+    0x4E => Some(0x004E), // #LATIN CAPITAL LETTER N
+    0x4F => Some(0x004F), // #LATIN CAPITAL LETTER O
+    0x50 => Some(0x0050), // #LATIN CAPITAL LETTER P
+    0x51 => Some(0x0051), // #LATIN CAPITAL LETTER Q
+    0x52 => Some(0x0052), // #LATIN CAPITAL LETTER R
+    0x53 => Some(0x0053), // #LATIN CAPITAL LETTER S
+    0x54 => Some(0x0054), // #LATIN CAPITAL LETTER T
+    0x55 => Some(0x0055), // #LATIN CAPITAL LETTER U
+    0x56 => Some(0x0056), // #LATIN CAPITAL LETTER V
+    0x57 => Some(0x0057), // #LATIN CAPITAL LETTER W
+    0x58 => Some(0x0058), // #LATIN CAPITAL LETTER X
+    0x59 => Some(0x0059), // #LATIN CAPITAL LETTER Y
+    0x5A => Some(0x005A), // #LATIN CAPITAL LETTER Z
+    0x5B => Some(0x005B), // #LEFT SQUARE BRACKET
+    0x5C => Some(0x005C), // #REVERSE SOLIDUS
+    0x5D => Some(0x005D), // #RIGHT SQUARE BRACKET
+    0x5E => Some(0x005E), // #CIRCUMFLEX ACCENT
+    0x5F => Some(0x005F), // #LOW LINE
+    0x60 => Some(0x0060), // #GRAVE ACCENT
+    0x61 => Some(0x0061), // #LATIN SMALL LETTER A
+    0x62 => Some(0x0062), // #LATIN SMALL LETTER B
+    0x63 => Some(0x0063), // #LATIN SMALL LETTER C
+    0x64 => Some(0x0064), // #LATIN SMALL LETTER D
+    0x65 => Some(0x0065), // #LATIN SMALL LETTER E
+    0x66 => Some(0x0066), // #LATIN SMALL LETTER F
+    0x67 => Some(0x0067), // #LATIN SMALL LETTER G
+    0x68 => Some(0x0068), // #LATIN SMALL LETTER H
+    0x69 => Some(0x0069), // #LATIN SMALL LETTER I
+    0x6A => Some(0x006A), // #LATIN SMALL LETTER J
+    0x6B => Some(0x006B), // #LATIN SMALL LETTER K
+    0x6C => Some(0x006C), // #LATIN SMALL LETTER L
+    0x6D => Some(0x006D), // #LATIN SMALL LETTER M
+    0x6E => Some(0x006E), // #LATIN SMALL LETTER N
+    0x6F => Some(0x006F), // #LATIN SMALL LETTER O
+    0x70 => Some(0x0070), // #LATIN SMALL LETTER P
+    0x71 => Some(0x0071), // #LATIN SMALL LETTER Q
+    0x72 => Some(0x0072), // #LATIN SMALL LETTER R
+    0x73 => Some(0x0073), // #LATIN SMALL LETTER S
+    0x74 => Some(0x0074), // #LATIN SMALL LETTER T
+    0x75 => Some(0x0075), // #LATIN SMALL LETTER U
+    0x76 => Some(0x0076), // #LATIN SMALL LETTER V
+    0x77 => Some(0x0077), // #LATIN SMALL LETTER W
+    0x78 => Some(0x0078), // #LATIN SMALL LETTER X
+    0x79 => Some(0x0079), // #LATIN SMALL LETTER Y
+    0x7A => Some(0x007A), // #LATIN SMALL LETTER Z
+    0x7B => Some(0x007B), // #LEFT CURLY BRACKET
+    0x7C => Some(0x007C), // #VERTICAL LINE
+    0x7D => Some(0x007D), // #RIGHT CURLY BRACKET
+    0x7E => Some(0x007E), // #TILDE
+    0x7F => Some(0x007F), // #DELETE
+    0x80 => Some(0x20AC), // #EURO SIGN
+    0x82 => Some(0x201A), // #SINGLE LOW-9 QUOTATION MARK
+    0x83 => Some(0x0192), // #LATIN SMALL LETTER F WITH HOOK
+    0x84 => Some(0x201E), // #DOUBLE LOW-9 QUOTATION MARK
+    0x85 => Some(0x2026), // #HORIZONTAL ELLIPSIS
+    0x86 => Some(0x2020), // #DAGGER
+    0x87 => Some(0x2021), // #DOUBLE DAGGER
+    0x88 => Some(0x02C6), // #MODIFIER LETTER CIRCUMFLEX ACCENT
+    0x89 => Some(0x2030), // #PER MILLE SIGN
+    0x8A => Some(0x0160), // #LATIN CAPITAL LETTER S WITH CARON
+    0x8B => Some(0x2039), // #SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+    0x8C => Some(0x0152), // #LATIN CAPITAL LIGATURE OE
+    0x8E => Some(0x017D), // #LATIN CAPITAL LETTER Z WITH CARON
+    0x91 => Some(0x2018), // #LEFT SINGLE QUOTATION MARK
+    0x92 => Some(0x2019), // #RIGHT SINGLE QUOTATION MARK
+    0x93 => Some(0x201C), // #LEFT DOUBLE QUOTATION MARK
+    0x94 => Some(0x201D), // #RIGHT DOUBLE QUOTATION MARK
+    0x95 => Some(0x2022), // #BULLET
+    0x96 => Some(0x2013), // #EN DASH
+    0x97 => Some(0x2014), // #EM DASH
+    0x98 => Some(0x02DC), // #SMALL TILDE
+    0x99 => Some(0x2122), // #TRADE MARK SIGN
+    0x9A => Some(0x0161), // #LATIN SMALL LETTER S WITH CARON
+    0x9B => Some(0x203A), // #SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+    0x9C => Some(0x0153), // #LATIN SMALL LIGATURE OE
+    0x9E => Some(0x017E), // #LATIN SMALL LETTER Z WITH CARON
+    0x9F => Some(0x0178), // #LATIN CAPITAL LETTER Y WITH DIAERESIS
+    0xA0 => Some(0x00A0), // #NO-BREAK SPACE
+    0xA1 => Some(0x00A1), // #INVERTED EXCLAMATION MARK
+    0xA2 => Some(0x00A2), // #CENT SIGN
+    0xA3 => Some(0x00A3), // #POUND SIGN
+    0xA4 => Some(0x00A4), // #CURRENCY SIGN
+    0xA5 => Some(0x00A5), // #YEN SIGN
+    0xA6 => Some(0x00A6), // #BROKEN BAR
+    0xA7 => Some(0x00A7), // #SECTION SIGN
+    0xA8 => Some(0x00A8), // #DIAERESIS
+    0xA9 => Some(0x00A9), // #COPYRIGHT SIGN
+    0xAA => Some(0x00AA), // #FEMININE ORDINAL INDICATOR
+    0xAB => Some(0x00AB), // #LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+    0xAC => Some(0x00AC), // #NOT SIGN
+    0xAD => Some(0x00AD), // #SOFT HYPHEN
+    0xAE => Some(0x00AE), // #REGISTERED SIGN
+    0xAF => Some(0x00AF), // #MACRON
+    0xB0 => Some(0x00B0), // #DEGREE SIGN
+    0xB1 => Some(0x00B1), // #PLUS-MINUS SIGN
+    0xB2 => Some(0x00B2), // #SUPERSCRIPT TWO
+    0xB3 => Some(0x00B3), // #SUPERSCRIPT THREE
+    0xB4 => Some(0x00B4), // #ACUTE ACCENT
+    0xB5 => Some(0x00B5), // #MICRO SIGN
+    0xB6 => Some(0x00B6), // #PILCROW SIGN
+    0xB7 => Some(0x00B7), // #MIDDLE DOT
+    0xB8 => Some(0x00B8), // #CEDILLA
+    0xB9 => Some(0x00B9), // #SUPERSCRIPT ONE
+    0xBA => Some(0x00BA), // #MASCULINE ORDINAL INDICATOR
+    0xBB => Some(0x00BB), // #RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+    0xBC => Some(0x00BC), // #VULGAR FRACTION ONE QUARTER
+    0xBD => Some(0x00BD), // #VULGAR FRACTION ONE HALF
+    0xBE => Some(0x00BE), // #VULGAR FRACTION THREE QUARTERS
+    0xBF => Some(0x00BF), // #INVERTED QUESTION MARK
+    0xC0 => Some(0x00C0), // #LATIN CAPITAL LETTER A WITH GRAVE
+    0xC1 => Some(0x00C1), // #LATIN CAPITAL LETTER A WITH ACUTE
+    0xC2 => Some(0x00C2), // #LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+    0xC3 => Some(0x00C3), // #LATIN CAPITAL LETTER A WITH TILDE
+    0xC4 => Some(0x00C4), // #LATIN CAPITAL LETTER A WITH DIAERESIS
+    0xC5 => Some(0x00C5), // #LATIN CAPITAL LETTER A WITH RING ABOVE
+    0xC6 => Some(0x00C6), // #LATIN CAPITAL LETTER AE
+    0xC7 => Some(0x00C7), // #LATIN CAPITAL LETTER C WITH CEDILLA
+    0xC8 => Some(0x00C8), // #LATIN CAPITAL LETTER E WITH GRAVE
+    0xC9 => Some(0x00C9), // #LATIN CAPITAL LETTER E WITH ACUTE
+    0xCA => Some(0x00CA), // #LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+    0xCB => Some(0x00CB), // #LATIN CAPITAL LETTER E WITH DIAERESIS
+    0xCC => Some(0x00CC), // #LATIN CAPITAL LETTER I WITH GRAVE
+    0xCD => Some(0x00CD), // #LATIN CAPITAL LETTER I WITH ACUTE
+    0xCE => Some(0x00CE), // #LATIN CAPITAL LETTER I WITH CIRCUMFLEX
+    0xCF => Some(0x00CF), // #LATIN CAPITAL LETTER I WITH DIAERESIS
+    0xD0 => Some(0x00D0), // #LATIN CAPITAL LETTER ETH
+    0xD1 => Some(0x00D1), // #LATIN CAPITAL LETTER N WITH TILDE
+    0xD2 => Some(0x00D2), // #LATIN CAPITAL LETTER O WITH GRAVE
+    0xD3 => Some(0x00D3), // #LATIN CAPITAL LETTER O WITH ACUTE
+    0xD4 => Some(0x00D4), // #LATIN CAPITAL LETTER O WITH CIRCUMFLEX
+    0xD5 => Some(0x00D5), // #LATIN CAPITAL LETTER O WITH TILDE
+    0xD6 => Some(0x00D6), // #LATIN CAPITAL LETTER O WITH DIAERESIS
+    0xD7 => Some(0x00D7), // #MULTIPLICATION SIGN
+    0xD8 => Some(0x00D8), // #LATIN CAPITAL LETTER O WITH STROKE
+    0xD9 => Some(0x00D9), // #LATIN CAPITAL LETTER U WITH GRAVE
+    0xDA => Some(0x00DA), // #LATIN CAPITAL LETTER U WITH ACUTE
+    0xDB => Some(0x00DB), // #LATIN CAPITAL LETTER U WITH CIRCUMFLEX
+    0xDC => Some(0x00DC), // #LATIN CAPITAL LETTER U WITH DIAERESIS
+    0xDD => Some(0x00DD), // #LATIN CAPITAL LETTER Y WITH ACUTE
+    0xDE => Some(0x00DE), // #LATIN CAPITAL LETTER THORN
+    0xDF => Some(0x00DF), // #LATIN SMALL LETTER SHARP S
+    0xE0 => Some(0x00E0), // #LATIN SMALL LETTER A WITH GRAVE
+    0xE1 => Some(0x00E1), // #LATIN SMALL LETTER A WITH ACUTE
+    0xE2 => Some(0x00E2), // #LATIN SMALL LETTER A WITH CIRCUMFLEX
+    0xE3 => Some(0x00E3), // #LATIN SMALL LETTER A WITH TILDE
+    0xE4 => Some(0x00E4), // #LATIN SMALL LETTER A WITH DIAERESIS
+    0xE5 => Some(0x00E5), // #LATIN SMALL LETTER A WITH RING ABOVE
+    0xE6 => Some(0x00E6), // #LATIN SMALL LETTER AE
+    0xE7 => Some(0x00E7), // #LATIN SMALL LETTER C WITH CEDILLA
+    0xE8 => Some(0x00E8), // #LATIN SMALL LETTER E WITH GRAVE
+    0xE9 => Some(0x00E9), // #LATIN SMALL LETTER E WITH ACUTE
+    0xEA => Some(0x00EA), // #LATIN SMALL LETTER E WITH CIRCUMFLEX
+    0xEB => Some(0x00EB), // #LATIN SMALL LETTER E WITH DIAERESIS
+    0xEC => Some(0x00EC), // #LATIN SMALL LETTER I WITH GRAVE
+    0xED => Some(0x00ED), // #LATIN SMALL LETTER I WITH ACUTE
+    0xEE => Some(0x00EE), // #LATIN SMALL LETTER I WITH CIRCUMFLEX
+    0xEF => Some(0x00EF), // #LATIN SMALL LETTER I WITH DIAERESIS
+    0xF0 => Some(0x00F0), // #LATIN SMALL LETTER ETH
+    0xF1 => Some(0x00F1), // #LATIN SMALL LETTER N WITH TILDE
+    0xF2 => Some(0x00F2), // #LATIN SMALL LETTER O WITH GRAVE
+    0xF3 => Some(0x00F3), // #LATIN SMALL LETTER O WITH ACUTE
+    0xF4 => Some(0x00F4), // #LATIN SMALL LETTER O WITH CIRCUMFLEX
+    0xF5 => Some(0x00F5), // #LATIN SMALL LETTER O WITH TILDE
+    0xF6 => Some(0x00F6), // #LATIN SMALL LETTER O WITH DIAERESIS
+    0xF7 => Some(0x00F7), // #DIVISION SIGN
+    0xF8 => Some(0x00F8), // #LATIN SMALL LETTER O WITH STROKE
+    0xF9 => Some(0x00F9), // #LATIN SMALL LETTER U WITH GRAVE
+    0xFA => Some(0x00FA), // #LATIN SMALL LETTER U WITH ACUTE
+    0xFB => Some(0x00FB), // #LATIN SMALL LETTER U WITH CIRCUMFLEX
+    0xFC => Some(0x00FC), // #LATIN SMALL LETTER U WITH DIAERESIS
+    0xFD => Some(0x00FD), // #LATIN SMALL LETTER Y WITH ACUTE
+    0xFE => Some(0x00FE), // #LATIN SMALL LETTER THORN
+    0xFF => Some(0x00FF), // #LATIN SMALL LETTER Y WITH DIAERESIS
+    _ => None,
+  }
+}
diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs
index d42e5c51..23961387 100644
--- a/espanso-engine/src/process/middleware/mod.rs
+++ b/espanso-engine/src/process/middleware/mod.rs
@@ -18,6 +18,7 @@
  */
 
 pub mod action;
+pub mod alt_code_synthesizer;
 pub mod cause;
 pub mod context_menu;
 pub mod cursor_hint;
diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs
index f55e5203..6430c3e5 100644
--- a/espanso-engine/src/process/mod.rs
+++ b/espanso-engine/src/process/mod.rs
@@ -34,6 +34,7 @@ pub trait Processor {
 // Dependency inversion entities
 
 pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
+pub use middleware::alt_code_synthesizer::AltCodeSynthEnabledProvider;
 pub use middleware::delay_modifiers::ModifierStatusProvider;
 pub use middleware::disable::DisableOptions;
 pub use middleware::image_resolve::PathProvider;
@@ -69,6 +70,7 @@ pub fn default<'a, MatcherState>(
   modifier_state_provider: &'a dyn ModifierStateProvider,
   match_resolver: &'a dyn MatchResolver,
   notification_manager: &'a dyn NotificationManager,
+  alt_code_synth_enabled_provider: &'a dyn AltCodeSynthEnabledProvider,
 ) -> impl Processor + 'a {
   default::DefaultProcessor::new(
     matchers,
@@ -88,5 +90,6 @@ pub fn default<'a, MatcherState>(
     modifier_state_provider,
     match_resolver,
     notification_manager,
+    alt_code_synth_enabled_provider,
   )
 }
diff --git a/espanso-inject/build.rs b/espanso-inject/build.rs
index 702678db..747f756c 100644
--- a/espanso-inject/build.rs
+++ b/espanso-inject/build.rs
@@ -37,6 +37,9 @@ fn cc_config() {
 fn cc_config() {
   println!("cargo:rerun-if-changed=src/evdev/native.h");
   println!("cargo:rerun-if-changed=src/evdev/native.c");
+  println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo.c");
+  println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo.h");
+  println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo_util.h");
   cc::Build::new()
     .include("src/evdev")
     .file("src/evdev/native.c")
@@ -47,6 +50,14 @@ fn cc_config() {
   println!("cargo:rustc-link-lib=dylib=xkbcommon");
 
   if cfg!(not(feature = "wayland")) {
+    cc::Build::new()
+      .cpp(false)
+      .include("src/x11/xdotool/vendor/xdo.h")
+      .include("src/x11/xdotool/vendor/xdo_util.h")
+      .file("src/x11/xdotool/vendor/xdo.c")
+      .compile("xdotoolvendor");
+
+    println!("cargo:rustc-link-lib=static=xdotoolvendor");
     println!("cargo:rustc-link-lib=dylib=X11");
     println!("cargo:rustc-link-lib=dylib=Xtst");
   }
diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs
index ebc086ea..e07ec7d5 100644
--- a/espanso-inject/src/lib.rs
+++ b/espanso-inject/src/lib.rs
@@ -61,6 +61,10 @@ pub struct InjectionOptions {
   // Used to set a modifier-specific delay.
   // NOTE: Only relevant on Wayland systems.
   pub evdev_modifier_delay: u32,
+
+  // If true, use the xdotool fallback to perform the expansions.
+  // NOTE: Only relevant on Linux-X11 systems.
+  pub x11_use_xdotool_fallback: bool,
 }
 
 impl Default for InjectionOptions {
@@ -84,6 +88,7 @@ impl Default for InjectionOptions {
       delay: default_delay,
       disable_fast_inject: false,
       evdev_modifier_delay: 10,
+      x11_use_xdotool_fallback: false,
     }
   }
 }
@@ -148,8 +153,8 @@ pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector
     info!("using EVDEVInjector");
     Ok(Box::new(evdev::EVDEVInjector::new(options)?))
   } else {
-    info!("using X11Injector");
-    Ok(Box::new(x11::X11Injector::new()?))
+    info!("using X11ProxyInjector");
+    Ok(Box::new(x11::X11ProxyInjector::new()?))
   }
 }
 
diff --git a/espanso-inject/src/x11/README.md b/espanso-inject/src/x11/README.md
deleted file mode 100644
index ac258d22..00000000
--- a/espanso-inject/src/x11/README.md
+++ /dev/null
@@ -1,53 +0,0 @@
-Same approach as evdev, but the lookup logic is:
-
-#include <locale.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <X11/Xlibint.h>
-#include <X11/Xlib.h>
-#include <X11/Xutil.h>
-#include <X11/cursorfont.h>
-#include <X11/keysymdef.h>
-#include <X11/keysym.h>
-#include <X11/extensions/record.h>
-#include <X11/extensions/XTest.h>
-#include <X11/XKBlib.h>
-#include <X11/Xatom.h>
-
-Display *data_disp = NULL;
-
-int main() {
-    data_disp = XOpenDisplay(NULL);
-
-    for (int code = 0; code<256; code++) {
-        for (int state = 0; state < 256; state++) {
-        XKeyEvent event;
-    event.display     = data_disp;
-    event.window      = XDefaultRootWindow(data_disp);
-    event.root        = XDefaultRootWindow(data_disp);
-    event.subwindow   = None;
-    event.time        = 0;
-    event.x           = 1;
-    event.y           = 1;
-    event.x_root      = 1;
-    event.y_root      = 1;
-    event.same_screen = True;
-    event.keycode     = code + 8;
-    event.state       = state;
-    event.type = KeyPress;
-
-    char buffer[10];
-    int res = XLookupString(&event, buffer, 9, NULL, NULL);
-
-    printf("hey %d %d %s\n", code, state, buffer);
-    }
-    
-    }
-    
-}
-
-
-This way, we get the state mask associated with a character, and we can pass it directly when injecting a character:
-https://github.com/federico-terzi/espanso/blob/master/native/liblinuxbridge/fast_xdo.cpp#L37
\ No newline at end of file
diff --git a/espanso-inject/src/x11/default/mod.rs b/espanso-inject/src/x11/default/mod.rs
new file mode 100644
index 00000000..2501f761
--- /dev/null
+++ b/espanso-inject/src/x11/default/mod.rs
@@ -0,0 +1,598 @@
+/*
+ * 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::{
+  collections::{HashMap, HashSet},
+  ffi::{CStr, CString},
+  os::raw::c_char,
+  slice,
+};
+
+use crate::x11::ffi::{
+  Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
+  XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
+  XSendEvent, XSync, XTestFakeKeyEvent,
+};
+use libc::c_void;
+use log::{debug, error};
+
+use crate::{linux::raw_keys::convert_to_sym_array, x11::ffi::Xutf8LookupString};
+use anyhow::{bail, Result};
+use thiserror::Error;
+
+use crate::{keys, InjectionOptions, Injector};
+
+use crate::x11::ffi::{
+  XCloseIM, XCreateIC, XDestroyIC, XFilterEvent, XFree, XIMPreeditNothing, XIMStatusNothing,
+  XNClientWindow_0, XNInputStyle_0, XOpenIM, XmbResetIC, XIC,
+};
+
+// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
+// keycode set (where ESC is 9).
+const EVDEV_OFFSET: u32 = 8;
+
+#[derive(Clone, Copy, Debug)]
+struct KeyPair {
+  // Keycode
+  code: u32,
+  // Modifier state which combined with the code produces the char
+  // This is a bit mask:
+  state: u32,
+}
+
+#[derive(Clone, Copy, Debug)]
+struct KeyRecord {
+  main: KeyPair,
+
+  // Under some keyboard layouts (de, es), a deadkey
+  // press might be needed to generate the right char
+  preceding_dead_key: Option<KeyPair>,
+}
+
+type CharMap = HashMap<String, KeyRecord>;
+type SymMap = HashMap<KeySym, KeyRecord>;
+
+pub struct X11DefaultInjector {
+  display: *mut Display,
+
+  char_map: CharMap,
+  sym_map: SymMap,
+}
+
+#[allow(clippy::new_without_default)]
+impl X11DefaultInjector {
+  pub fn new() -> Result<Self> {
+    // Necessary to properly handle non-ascii chars
+    let empty_string = CString::new("")?;
+    unsafe {
+      libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
+    }
+
+    let display = unsafe { crate::x11::ffi::XOpenDisplay(std::ptr::null()) };
+    if display.is_null() {
+      return Err(X11InjectorError::Init().into());
+    }
+
+    let (char_map, sym_map) = Self::generate_maps(display)?;
+
+    Ok(Self {
+      display,
+      char_map,
+      sym_map,
+    })
+  }
+
+  fn generate_maps(display: *mut Display) -> Result<(CharMap, SymMap)> {
+    debug!("generating key maps");
+
+    let mut char_map = HashMap::new();
+    let mut sym_map = HashMap::new();
+
+    let input_method = unsafe {
+      XOpenIM(
+        display,
+        std::ptr::null_mut(),
+        std::ptr::null_mut(),
+        std::ptr::null_mut(),
+      )
+    };
+    if input_method.is_null() {
+      bail!("could not open input method");
+    }
+    let _im_guard = scopeguard::guard((), |_| {
+      unsafe { XCloseIM(input_method) };
+    });
+
+    let input_context = unsafe {
+      XCreateIC(
+        input_method,
+        XNInputStyle_0.as_ptr(),
+        XIMPreeditNothing | XIMStatusNothing,
+        XNClientWindow_0.as_ptr(),
+        0,
+        std::ptr::null_mut(),
+      )
+    };
+    if input_context.is_null() {
+      bail!("could not open input context");
+    }
+    let _ic_guard = scopeguard::guard((), |_| {
+      unsafe { XDestroyIC(input_context) };
+    });
+
+    let deadkeys = Self::find_deadkeys(display, &input_context)?;
+
+    // Cycle through all state/code combinations to populate the reverse lookup tables
+    for key_code in 0..256u32 {
+      for modifier_state in 0..256u32 {
+        for dead_key in deadkeys.iter() {
+          let code_with_offset = key_code + EVDEV_OFFSET;
+
+          let preceding_dead_key = if let Some(dead_key) = dead_key {
+            let mut dead_key_event = XKeyEvent {
+              display,
+              keycode: dead_key.code,
+              state: dead_key.state,
+
+              // These might not even need to be filled
+              window: 0,
+              root: 0,
+              same_screen: 1,
+              time: 0,
+              type_: KeyPress,
+              x_root: 1,
+              y_root: 1,
+              x: 1,
+              y: 1,
+              subwindow: 0,
+              serial: 0,
+              send_event: 0,
+            };
+
+            unsafe { XFilterEvent(&mut dead_key_event, 0) };
+
+            Some(*dead_key)
+          } else {
+            None
+          };
+
+          let mut key_event = XKeyEvent {
+            display,
+            keycode: code_with_offset,
+            state: modifier_state,
+
+            // These might not even need to be filled
+            window: 0,
+            root: 0,
+            same_screen: 1,
+            time: 0,
+            type_: KeyPress,
+            x_root: 1,
+            y_root: 1,
+            x: 1,
+            y: 1,
+            subwindow: 0,
+            serial: 0,
+            send_event: 0,
+          };
+
+          unsafe { XFilterEvent(&mut key_event, 0) };
+          let mut sym: KeySym = 0;
+          let mut buffer: [c_char; 10] = [0; 10];
+
+          let result = unsafe {
+            Xutf8LookupString(
+              input_context,
+              &mut key_event,
+              buffer.as_mut_ptr(),
+              (buffer.len() - 1) as i32,
+              &mut sym,
+              std::ptr::null_mut(),
+            )
+          };
+
+          let key_record = KeyRecord {
+            main: KeyPair {
+              code: code_with_offset,
+              state: modifier_state,
+            },
+            preceding_dead_key,
+          };
+
+          // Keysym was found
+          if sym != 0 {
+            sym_map.entry(sym).or_insert(key_record);
+          };
+
+          // Char was found
+          if result > 0 {
+            let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+            let string = raw_string.to_string_lossy().to_string();
+            char_map.entry(string).or_insert(key_record);
+          };
+
+          // We need to reset the context state to prevent
+          // deadkeys effect to propagate to the next combination
+          let _reset = unsafe { XmbResetIC(input_context) };
+          unsafe { XFree(_reset as *mut c_void) };
+        }
+      }
+    }
+
+    debug!("Populated char_map with {} symbols", char_map.len());
+    debug!("Populated sym_map with {} symbols", sym_map.len());
+    debug!("Detected {} dead key combinations", deadkeys.len());
+
+    Ok((char_map, sym_map))
+  }
+
+  fn find_deadkeys(display: *mut Display, input_context: &XIC) -> Result<Vec<Option<KeyPair>>> {
+    let mut deadkeys = vec![None];
+    let mut seen_keysyms: HashSet<KeySym> = HashSet::new();
+
+    // Cycle through all state/code combinations to populate the reverse lookup tables
+    for key_code in 0..256u32 {
+      for modifier_state in 0..256u32 {
+        let code_with_offset = key_code + EVDEV_OFFSET;
+        let mut event = XKeyEvent {
+          display,
+          keycode: code_with_offset,
+          state: modifier_state,
+
+          // These might not even need to be filled
+          window: 0,
+          root: 0,
+          same_screen: 1,
+          time: 0,
+          type_: KeyPress,
+          x_root: 1,
+          y_root: 1,
+          x: 1,
+          y: 1,
+          subwindow: 0,
+          serial: 0,
+          send_event: 0,
+        };
+
+        let filter = unsafe { XFilterEvent(&mut event, 0) };
+        if filter == 1 {
+          let mut sym: KeySym = 0;
+          let mut buffer: [c_char; 10] = [0; 10];
+
+          unsafe {
+            Xutf8LookupString(
+              *input_context,
+              &mut event,
+              buffer.as_mut_ptr(),
+              (buffer.len() - 1) as i32,
+              &mut sym,
+              std::ptr::null_mut(),
+            )
+          };
+
+          if sym != 0 && !seen_keysyms.contains(&sym) {
+            let key_record = KeyPair {
+              code: code_with_offset,
+              state: modifier_state,
+            };
+            deadkeys.push(Some(key_record));
+            seen_keysyms.insert(sym);
+          }
+        }
+
+        let _reset = unsafe { XmbResetIC(*input_context) };
+        unsafe { XFree(_reset as *mut c_void) };
+      }
+    }
+
+    Ok(deadkeys)
+  }
+
+  fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
+    syms
+      .iter()
+      .map(|sym| {
+        self
+          .sym_map
+          .get(sym)
+          .cloned()
+          .ok_or_else(|| X11InjectorError::SymMapping(*sym).into())
+      })
+      .collect()
+  }
+
+  // This method was inspired by the wonderful xdotool by Jordan Sissel
+  // https://github.com/jordansissel/xdotool
+  fn get_modifier_codes(&self) -> Vec<Vec<KeyCode>> {
+    let modifiers_ptr = unsafe { XGetModifierMapping(self.display) };
+    let modifiers = unsafe { *modifiers_ptr };
+
+    let mut modifiers_codes = Vec::new();
+
+    for mod_index in 0..=7 {
+      let mut modifier_codes = Vec::new();
+      for mod_key in 0..modifiers.max_keypermod {
+        let modifier_map = unsafe {
+          slice::from_raw_parts(
+            modifiers.modifiermap,
+            (8 * modifiers.max_keypermod) as usize,
+          )
+        };
+        let keycode = modifier_map[(mod_index * modifiers.max_keypermod + mod_key) as usize];
+        if keycode != 0 {
+          modifier_codes.push(keycode);
+        }
+      }
+      modifiers_codes.push(modifier_codes);
+    }
+
+    unsafe { XFreeModifiermap(modifiers_ptr) };
+
+    modifiers_codes
+  }
+
+  fn render_key_combination(&self, original_records: &[KeyRecord]) -> Vec<KeyRecord> {
+    let modifiers_codes = self.get_modifier_codes();
+    let mut records = Vec::new();
+
+    let mut current_state = 0u32;
+    for record in original_records {
+      let mut current_record = *record;
+
+      // Render the state by applying the modifiers
+      for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
+        if modifier.contains(&(record.main.code as u8)) {
+          current_state |= 1 << mod_index;
+        }
+      }
+
+      current_record.main.state = current_state;
+      records.push(current_record);
+    }
+
+    records
+  }
+
+  fn get_focused_window(&self) -> Window {
+    let mut focused_window: Window = 0;
+    let mut revert_to = 0;
+    unsafe {
+      XGetInputFocus(self.display, &mut focused_window, &mut revert_to);
+    }
+    focused_window
+  }
+
+  fn send_key(&self, window: Window, record: &KeyPair, pressed: bool, delay_us: u32) {
+    let root_window = unsafe { XDefaultRootWindow(self.display) };
+    let mut event = XKeyEvent {
+      display: self.display,
+      keycode: record.code,
+      state: record.state,
+      window,
+      root: root_window,
+      same_screen: 1,
+      time: 0,
+      type_: if pressed { KeyPress } else { KeyRelease },
+      x_root: 1,
+      y_root: 1,
+      x: 1,
+      y: 1,
+      subwindow: 0,
+      serial: 0,
+      send_event: 0,
+    };
+    unsafe {
+      XSendEvent(self.display, window, 1, 0, &mut event);
+      XFlush(self.display);
+    }
+
+    if delay_us != 0 {
+      unsafe {
+        libc::usleep(delay_us);
+      }
+    }
+  }
+
+  fn xtest_send_modifiers(&self, modmask: u32, pressed: bool) {
+    let modifiers_codes = self.get_modifier_codes();
+    for (mod_index, modifier_codes) in modifiers_codes.into_iter().enumerate() {
+      if (modmask & (1 << mod_index)) != 0 {
+        for keycode in modifier_codes {
+          let is_press = if pressed { 1 } else { 0 };
+          unsafe {
+            XTestFakeKeyEvent(self.display, keycode as u32, is_press, 0);
+            XSync(self.display, 0);
+          }
+        }
+      }
+    }
+  }
+
+  fn xtest_send_key(&self, record: &KeyPair, pressed: bool, delay_us: u32) {
+    // If the key requires any modifier, we need to send those events
+    if record.state != 0 {
+      self.xtest_send_modifiers(record.state, pressed);
+    }
+
+    let is_press = if pressed { 1 } else { 0 };
+    unsafe {
+      XTestFakeKeyEvent(self.display, record.code, is_press, 0);
+      XSync(self.display, 0);
+      XFlush(self.display);
+    }
+
+    if delay_us != 0 {
+      unsafe {
+        libc::usleep(delay_us);
+      }
+    }
+  }
+
+  fn xtest_release_all_keys(&self) {
+    let mut keys: [u8; 32] = [0; 32];
+    unsafe {
+      XQueryKeymap(self.display, keys.as_mut_ptr());
+    }
+
+    #[allow(clippy::needless_range_loop)]
+    for i in 0..32 {
+      // Only those that are pressed should be changed
+      if keys[i] != 0 {
+        for k in 0..8 {
+          if (keys[i] & (1 << k)) != 0 {
+            let key_code = i * 8 + k;
+            unsafe {
+              XTestFakeKeyEvent(self.display, key_code as u32, 0, 0);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+impl Drop for X11DefaultInjector {
+  fn drop(&mut self) {
+    unsafe {
+      XCloseDisplay(self.display);
+    }
+  }
+}
+
+impl Injector for X11DefaultInjector {
+  fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> {
+    let focused_window = self.get_focused_window();
+
+    if options.disable_fast_inject {
+      self.xtest_release_all_keys();
+    }
+
+    // Compute all the key record sequence first to make sure a mapping is available
+    let records: Result<Vec<KeyRecord>> = string
+      .chars()
+      .map(|c| c.to_string())
+      .map(|char| {
+        self
+          .char_map
+          .get(&char)
+          .cloned()
+          .ok_or_else(|| X11InjectorError::CharMapping(char).into())
+      })
+      .collect();
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+
+    for record in records? {
+      if options.disable_fast_inject {
+        if let Some(deadkey) = &record.preceding_dead_key {
+          self.xtest_send_key(deadkey, true, delay_us);
+          self.xtest_send_key(deadkey, false, delay_us);
+        }
+
+        self.xtest_send_key(&record.main, true, delay_us);
+        self.xtest_send_key(&record.main, false, delay_us);
+      } else {
+        if let Some(deadkey) = &record.preceding_dead_key {
+          self.send_key(focused_window, deadkey, true, delay_us);
+          self.send_key(focused_window, deadkey, false, delay_us);
+        }
+
+        self.send_key(focused_window, &record.main, true, delay_us);
+        self.send_key(focused_window, &record.main, false, delay_us);
+      }
+    }
+
+    Ok(())
+  }
+
+  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let focused_window = self.get_focused_window();
+
+    // Compute all the key record sequence first to make sure a mapping is available
+    let syms = convert_to_sym_array(keys)?;
+    let records = self.convert_to_record_array(&syms)?;
+
+    if options.disable_fast_inject {
+      self.xtest_release_all_keys();
+    }
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+
+    for record in records {
+      if options.disable_fast_inject {
+        self.xtest_send_key(&record.main, true, delay_us);
+        self.xtest_send_key(&record.main, false, delay_us);
+      } else {
+        self.send_key(focused_window, &record.main, true, delay_us);
+        self.send_key(focused_window, &record.main, false, delay_us);
+      }
+    }
+
+    Ok(())
+  }
+
+  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let focused_window = self.get_focused_window();
+
+    // Compute all the key record sequence first to make sure a mapping is available
+    let syms = convert_to_sym_array(keys)?;
+    let records = self.convert_to_record_array(&syms)?;
+
+    // Render the correct modifier mask for the given sequence
+    let records = self.render_key_combination(&records);
+
+    if options.disable_fast_inject {
+      self.xtest_release_all_keys();
+    }
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+
+    // First press the keys
+    for record in records.iter() {
+      if options.disable_fast_inject {
+        self.xtest_send_key(&record.main, true, delay_us);
+      } else {
+        self.send_key(focused_window, &record.main, true, delay_us);
+      }
+    }
+
+    // Then release them
+    for record in records.iter().rev() {
+      if options.disable_fast_inject {
+        self.xtest_send_key(&record.main, false, delay_us);
+      } else {
+        self.send_key(focused_window, &record.main, false, delay_us);
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum X11InjectorError {
+  #[error("failed to initialize x11 display")]
+  Init(),
+
+  #[error("missing vkey mapping for char `{0}`")]
+  CharMapping(String),
+
+  #[error("missing record mapping for sym `{0}`")]
+  SymMapping(u64),
+}
diff --git a/espanso-inject/src/x11/ffi.rs b/espanso-inject/src/x11/ffi.rs
index e456b3a0..c5a6cf66 100644
--- a/espanso-inject/src/x11/ffi.rs
+++ b/espanso-inject/src/x11/ffi.rs
@@ -6,7 +6,7 @@ use std::{
   os::raw::{c_char, c_long, c_uint, c_ulong},
 };
 
-use libc::c_int;
+use libc::{c_int, c_uchar};
 
 pub enum Display {}
 pub type Window = u64;
@@ -123,4 +123,6 @@ extern "C" {
   pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int;
   pub fn XCloseIM(input_method: XIM) -> c_int;
   pub fn XFree(data: *mut c_void) -> c_int;
+  pub fn XKeycodeToKeysym(display: *mut Display, keycode: c_uchar, index: c_int) -> c_ulong;
+  pub fn XKeysymToString(keysym: c_ulong) -> *mut c_char;
 }
diff --git a/espanso-inject/src/x11/mod.rs b/espanso-inject/src/x11/mod.rs
index 84d4a6cc..c0a03179 100644
--- a/espanso-inject/src/x11/mod.rs
+++ b/espanso-inject/src/x11/mod.rs
@@ -17,584 +17,89 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-mod ffi;
-
-use std::{
-  collections::{HashMap, HashSet},
-  ffi::{CStr, CString},
-  os::raw::c_char,
-  slice,
-};
-
-use ffi::{
-  Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
-  XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
-  XSendEvent, XSync, XTestFakeKeyEvent,
-};
-use libc::c_void;
-use log::{debug, error};
-
-use crate::{linux::raw_keys::convert_to_sym_array, x11::ffi::Xutf8LookupString};
-use anyhow::{bail, Result};
-use thiserror::Error;
-
-use crate::{keys, InjectionOptions, Injector};
-
-use self::ffi::{
-  XCloseIM, XCreateIC, XDestroyIC, XFilterEvent, XFree, XIMPreeditNothing, XIMStatusNothing,
-  XNClientWindow_0, XNInputStyle_0, XOpenIM, XmbResetIC, XIC,
-};
-
-// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
-// keycode set (where ESC is 9).
-const EVDEV_OFFSET: u32 = 8;
-
-#[derive(Clone, Copy, Debug)]
-struct KeyPair {
-  // Keycode
-  code: u32,
-  // Modifier state which combined with the code produces the char
-  // This is a bit mask:
-  state: u32,
-}
+use crate::Injector;
 
-#[derive(Clone, Copy, Debug)]
-struct KeyRecord {
-  main: KeyPair,
+use anyhow::{bail, ensure, Result};
+use log::{error, warn};
 
-  // Under some keyboard layouts (de, es), a deadkey
-  // press might be needed to generate the right char
-  preceding_dead_key: Option<KeyPair>,
-}
-
-type CharMap = HashMap<String, KeyRecord>;
-type SymMap = HashMap<KeySym, KeyRecord>;
-
-pub struct X11Injector {
-  display: *mut Display,
+mod default;
+mod ffi;
+mod xdotool;
 
-  char_map: CharMap,
-  sym_map: SymMap,
+pub struct X11ProxyInjector {
+  default_injector: Option<default::X11DefaultInjector>,
+  xdotool_injector: Option<xdotool::X11XDOToolInjector>,
 }
 
-#[allow(clippy::new_without_default)]
-impl X11Injector {
+impl X11ProxyInjector {
   pub fn new() -> Result<Self> {
-    // Necessary to properly handle non-ascii chars
-    let empty_string = CString::new("")?;
-    unsafe {
-      libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
-    }
-
-    let display = unsafe { ffi::XOpenDisplay(std::ptr::null()) };
-    if display.is_null() {
-      return Err(X11InjectorError::Init().into());
-    }
-
-    let (char_map, sym_map) = Self::generate_maps(display)?;
-
-    Ok(Self {
-      display,
-      char_map,
-      sym_map,
-    })
-  }
-
-  fn generate_maps(display: *mut Display) -> Result<(CharMap, SymMap)> {
-    debug!("generating key maps");
-
-    let mut char_map = HashMap::new();
-    let mut sym_map = HashMap::new();
-
-    let input_method = unsafe {
-      XOpenIM(
-        display,
-        std::ptr::null_mut(),
-        std::ptr::null_mut(),
-        std::ptr::null_mut(),
-      )
-    };
-    if input_method.is_null() {
-      bail!("could not open input method");
-    }
-    let _im_guard = scopeguard::guard((), |_| {
-      unsafe { XCloseIM(input_method) };
-    });
-
-    let input_context = unsafe {
-      XCreateIC(
-        input_method,
-        XNInputStyle_0.as_ptr(),
-        XIMPreeditNothing | XIMStatusNothing,
-        XNClientWindow_0.as_ptr(),
-        0,
-        std::ptr::null_mut(),
-      )
-    };
-    if input_context.is_null() {
-      bail!("could not open input context");
-    }
-    let _ic_guard = scopeguard::guard((), |_| {
-      unsafe { XDestroyIC(input_context) };
-    });
-
-    let deadkeys = Self::find_deadkeys(display, &input_context)?;
-
-    // Cycle through all state/code combinations to populate the reverse lookup tables
-    for key_code in 0..256u32 {
-      for modifier_state in 0..256u32 {
-        for dead_key in deadkeys.iter() {
-          let code_with_offset = key_code + EVDEV_OFFSET;
-
-          let preceding_dead_key = if let Some(dead_key) = dead_key {
-            let mut dead_key_event = XKeyEvent {
-              display,
-              keycode: dead_key.code,
-              state: dead_key.state,
-
-              // These might not even need to be filled
-              window: 0,
-              root: 0,
-              same_screen: 1,
-              time: 0,
-              type_: KeyPress,
-              x_root: 1,
-              y_root: 1,
-              x: 1,
-              y: 1,
-              subwindow: 0,
-              serial: 0,
-              send_event: 0,
-            };
-
-            unsafe { XFilterEvent(&mut dead_key_event, 0) };
-
-            Some(*dead_key)
-          } else {
-            None
-          };
-
-          let mut key_event = XKeyEvent {
-            display,
-            keycode: code_with_offset,
-            state: modifier_state,
-
-            // These might not even need to be filled
-            window: 0,
-            root: 0,
-            same_screen: 1,
-            time: 0,
-            type_: KeyPress,
-            x_root: 1,
-            y_root: 1,
-            x: 1,
-            y: 1,
-            subwindow: 0,
-            serial: 0,
-            send_event: 0,
-          };
-
-          unsafe { XFilterEvent(&mut key_event, 0) };
-          let mut sym: KeySym = 0;
-          let mut buffer: [c_char; 10] = [0; 10];
-
-          let result = unsafe {
-            Xutf8LookupString(
-              input_context,
-              &mut key_event,
-              buffer.as_mut_ptr(),
-              (buffer.len() - 1) as i32,
-              &mut sym,
-              std::ptr::null_mut(),
-            )
-          };
-
-          let key_record = KeyRecord {
-            main: KeyPair {
-              code: code_with_offset,
-              state: modifier_state,
-            },
-            preceding_dead_key,
-          };
-
-          // Keysym was found
-          if sym != 0 {
-            sym_map.entry(sym).or_insert(key_record);
-          };
-
-          // Char was found
-          if result > 0 {
-            let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
-            let string = raw_string.to_string_lossy().to_string();
-            char_map.entry(string).or_insert(key_record);
-          };
-
-          // We need to reset the context state to prevent
-          // deadkeys effect to propagate to the next combination
-          let _reset = unsafe { XmbResetIC(input_context) };
-          unsafe { XFree(_reset as *mut c_void) };
-        }
+    let default_injector = match default::X11DefaultInjector::new() {
+      Ok(injector) => Some(injector),
+      Err(err) => {
+        error!("X11DefaultInjector could not be initialized: {:?}", err);
+        warn!("falling back to xdotool injector");
+        None
       }
-    }
-
-    debug!("Populated char_map with {} symbols", char_map.len());
-    debug!("Populated sym_map with {} symbols", sym_map.len());
-    debug!("Detected {} dead key combinations", deadkeys.len());
-
-    Ok((char_map, sym_map))
-  }
-
-  fn find_deadkeys(display: *mut Display, input_context: &XIC) -> Result<Vec<Option<KeyPair>>> {
-    let mut deadkeys = vec![None];
-    let mut seen_keysyms: HashSet<KeySym> = HashSet::new();
-
-    // Cycle through all state/code combinations to populate the reverse lookup tables
-    for key_code in 0..256u32 {
-      for modifier_state in 0..256u32 {
-        let code_with_offset = key_code + EVDEV_OFFSET;
-        let mut event = XKeyEvent {
-          display,
-          keycode: code_with_offset,
-          state: modifier_state,
-
-          // These might not even need to be filled
-          window: 0,
-          root: 0,
-          same_screen: 1,
-          time: 0,
-          type_: KeyPress,
-          x_root: 1,
-          y_root: 1,
-          x: 1,
-          y: 1,
-          subwindow: 0,
-          serial: 0,
-          send_event: 0,
-        };
-
-        let filter = unsafe { XFilterEvent(&mut event, 0) };
-        if filter == 1 {
-          let mut sym: KeySym = 0;
-          let mut buffer: [c_char; 10] = [0; 10];
-
-          unsafe {
-            Xutf8LookupString(
-              *input_context,
-              &mut event,
-              buffer.as_mut_ptr(),
-              (buffer.len() - 1) as i32,
-              &mut sym,
-              std::ptr::null_mut(),
-            )
-          };
-
-          if sym != 0 && !seen_keysyms.contains(&sym) {
-            let key_record = KeyPair {
-              code: code_with_offset,
-              state: modifier_state,
-            };
-            deadkeys.push(Some(key_record));
-            seen_keysyms.insert(sym);
-          }
-        }
-
-        let _reset = unsafe { XmbResetIC(*input_context) };
-        unsafe { XFree(_reset as *mut c_void) };
-      }
-    }
-
-    Ok(deadkeys)
-  }
-
-  fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
-    syms
-      .iter()
-      .map(|sym| {
-        self
-          .sym_map
-          .get(sym)
-          .cloned()
-          .ok_or_else(|| X11InjectorError::SymMapping(*sym).into())
-      })
-      .collect()
-  }
-
-  // This method was inspired by the wonderful xdotool by Jordan Sissel
-  // https://github.com/jordansissel/xdotool
-  fn get_modifier_codes(&self) -> Vec<Vec<KeyCode>> {
-    let modifiers_ptr = unsafe { XGetModifierMapping(self.display) };
-    let modifiers = unsafe { *modifiers_ptr };
-
-    let mut modifiers_codes = Vec::new();
-
-    for mod_index in 0..=7 {
-      let mut modifier_codes = Vec::new();
-      for mod_key in 0..modifiers.max_keypermod {
-        let modifier_map = unsafe {
-          slice::from_raw_parts(
-            modifiers.modifiermap,
-            (8 * modifiers.max_keypermod) as usize,
-          )
-        };
-        let keycode = modifier_map[(mod_index * modifiers.max_keypermod + mod_key) as usize];
-        if keycode != 0 {
-          modifier_codes.push(keycode);
-        }
-      }
-      modifiers_codes.push(modifier_codes);
-    }
-
-    unsafe { XFreeModifiermap(modifiers_ptr) };
-
-    modifiers_codes
-  }
-
-  fn render_key_combination(&self, original_records: &[KeyRecord]) -> Vec<KeyRecord> {
-    let modifiers_codes = self.get_modifier_codes();
-    let mut records = Vec::new();
-
-    let mut current_state = 0u32;
-    for record in original_records {
-      let mut current_record = *record;
+    };
 
-      // Render the state by applying the modifiers
-      for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
-        if modifier.contains(&(record.main.code as u8)) {
-          current_state |= 1 << mod_index;
-        }
+    let xdotool_injector = match xdotool::X11XDOToolInjector::new() {
+      Ok(injector) => Some(injector),
+      Err(err) => {
+        error!("X11XDOToolInjector could not be initialized: {:?}", err);
+        None
       }
-
-      current_record.main.state = current_state;
-      records.push(current_record);
-    }
-
-    records
-  }
-
-  fn get_focused_window(&self) -> Window {
-    let mut focused_window: Window = 0;
-    let mut revert_to = 0;
-    unsafe {
-      XGetInputFocus(self.display, &mut focused_window, &mut revert_to);
-    }
-    focused_window
-  }
-
-  fn send_key(&self, window: Window, record: &KeyPair, pressed: bool, delay_us: u32) {
-    let root_window = unsafe { XDefaultRootWindow(self.display) };
-    let mut event = XKeyEvent {
-      display: self.display,
-      keycode: record.code,
-      state: record.state,
-      window,
-      root: root_window,
-      same_screen: 1,
-      time: 0,
-      type_: if pressed { KeyPress } else { KeyRelease },
-      x_root: 1,
-      y_root: 1,
-      x: 1,
-      y: 1,
-      subwindow: 0,
-      serial: 0,
-      send_event: 0,
     };
-    unsafe {
-      XSendEvent(self.display, window, 1, 0, &mut event);
-      XFlush(self.display);
-    }
 
-    if delay_us != 0 {
-      unsafe {
-        libc::usleep(delay_us);
-      }
+    if default_injector.is_none() && xdotool_injector.is_none() {
+      bail!("unable to initialize injectors, neither the default or xdotool fallback could be initialized");
     }
-  }
 
-  fn xtest_send_modifiers(&self, modmask: u32, pressed: bool) {
-    let modifiers_codes = self.get_modifier_codes();
-    for (mod_index, modifier_codes) in modifiers_codes.into_iter().enumerate() {
-      if (modmask & (1 << mod_index)) != 0 {
-        for keycode in modifier_codes {
-          let is_press = if pressed { 1 } else { 0 };
-          unsafe {
-            XTestFakeKeyEvent(self.display, keycode as u32, is_press, 0);
-            XSync(self.display, 0);
-          }
-        }
-      }
-    }
+    Ok(X11ProxyInjector {
+      default_injector,
+      xdotool_injector,
+    })
   }
 
-  fn xtest_send_key(&self, record: &KeyPair, pressed: bool, delay_us: u32) {
-    // If the key requires any modifier, we need to send those events
-    if record.state != 0 {
-      self.xtest_send_modifiers(record.state, pressed);
-    }
-
-    let is_press = if pressed { 1 } else { 0 };
-    unsafe {
-      XTestFakeKeyEvent(self.display, record.code, is_press, 0);
-      XSync(self.display, 0);
-      XFlush(self.display);
-    }
-
-    if delay_us != 0 {
-      unsafe {
-        libc::usleep(delay_us);
+  fn get_active_injector(&self, options: &crate::InjectionOptions) -> Result<&dyn Injector> {
+    ensure!(
+      self.default_injector.is_some() || self.xdotool_injector.is_some(),
+      "unable to get active injector, neither default or xdotool fallback are available."
+    );
+
+    if options.x11_use_xdotool_fallback {
+      if let Some(xdotool_injector) = self.xdotool_injector.as_ref() {
+        return Ok(xdotool_injector);
+      } else if let Some(default_injector) = self.default_injector.as_ref() {
+        return Ok(default_injector);
       }
-    }
-  }
-
-  fn xtest_release_all_keys(&self) {
-    let mut keys: [u8; 32] = [0; 32];
-    unsafe {
-      XQueryKeymap(self.display, keys.as_mut_ptr());
+    } else if let Some(default_injector) = self.default_injector.as_ref() {
+      return Ok(default_injector);
+    } else if let Some(xdotool_injector) = self.xdotool_injector.as_ref() {
+      return Ok(xdotool_injector);
     }
 
-    #[allow(clippy::needless_range_loop)]
-    for i in 0..32 {
-      // Only those that are pressed should be changed
-      if keys[i] != 0 {
-        for k in 0..8 {
-          if (keys[i] & (1 << k)) != 0 {
-            let key_code = i * 8 + k;
-            unsafe {
-              XTestFakeKeyEvent(self.display, key_code as u32, 0, 0);
-            }
-          }
-        }
-      }
-    }
+    unreachable!()
   }
 }
 
-impl Drop for X11Injector {
-  fn drop(&mut self) {
-    unsafe {
-      XCloseDisplay(self.display);
-    }
+impl Injector for X11ProxyInjector {
+  fn send_string(&self, string: &str, options: crate::InjectionOptions) -> Result<()> {
+    self
+      .get_active_injector(&options)?
+      .send_string(string, options)
   }
-}
-
-impl Injector for X11Injector {
-  fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> {
-    let focused_window = self.get_focused_window();
-
-    if options.disable_fast_inject {
-      self.xtest_release_all_keys();
-    }
-
-    // Compute all the key record sequence first to make sure a mapping is available
-    let records: Result<Vec<KeyRecord>> = string
-      .chars()
-      .map(|c| c.to_string())
-      .map(|char| {
-        self
-          .char_map
-          .get(&char)
-          .cloned()
-          .ok_or_else(|| X11InjectorError::CharMapping(char).into())
-      })
-      .collect();
 
-    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
-
-    for record in records? {
-      if options.disable_fast_inject {
-        if let Some(deadkey) = &record.preceding_dead_key {
-          self.xtest_send_key(deadkey, true, delay_us);
-          self.xtest_send_key(deadkey, false, delay_us);
-        }
-
-        self.xtest_send_key(&record.main, true, delay_us);
-        self.xtest_send_key(&record.main, false, delay_us);
-      } else {
-        if let Some(deadkey) = &record.preceding_dead_key {
-          self.send_key(focused_window, deadkey, true, delay_us);
-          self.send_key(focused_window, deadkey, false, delay_us);
-        }
-
-        self.send_key(focused_window, &record.main, true, delay_us);
-        self.send_key(focused_window, &record.main, false, delay_us);
-      }
-    }
-
-    Ok(())
-  }
-
-  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
-    let focused_window = self.get_focused_window();
-
-    // Compute all the key record sequence first to make sure a mapping is available
-    let syms = convert_to_sym_array(keys)?;
-    let records = self.convert_to_record_array(&syms)?;
-
-    if options.disable_fast_inject {
-      self.xtest_release_all_keys();
-    }
-
-    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
-
-    for record in records {
-      if options.disable_fast_inject {
-        self.xtest_send_key(&record.main, true, delay_us);
-        self.xtest_send_key(&record.main, false, delay_us);
-      } else {
-        self.send_key(focused_window, &record.main, true, delay_us);
-        self.send_key(focused_window, &record.main, false, delay_us);
-      }
-    }
-
-    Ok(())
+  fn send_keys(&self, keys: &[crate::keys::Key], options: crate::InjectionOptions) -> Result<()> {
+    self.get_active_injector(&options)?.send_keys(keys, options)
   }
 
-  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
-    let focused_window = self.get_focused_window();
-
-    // Compute all the key record sequence first to make sure a mapping is available
-    let syms = convert_to_sym_array(keys)?;
-    let records = self.convert_to_record_array(&syms)?;
-
-    // Render the correct modifier mask for the given sequence
-    let records = self.render_key_combination(&records);
-
-    if options.disable_fast_inject {
-      self.xtest_release_all_keys();
-    }
-
-    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
-
-    // First press the keys
-    for record in records.iter() {
-      if options.disable_fast_inject {
-        self.xtest_send_key(&record.main, true, delay_us);
-      } else {
-        self.send_key(focused_window, &record.main, true, delay_us);
-      }
-    }
-
-    // Then release them
-    for record in records.iter().rev() {
-      if options.disable_fast_inject {
-        self.xtest_send_key(&record.main, false, delay_us);
-      } else {
-        self.send_key(focused_window, &record.main, false, delay_us);
-      }
-    }
-
-    Ok(())
+  fn send_key_combination(
+    &self,
+    keys: &[crate::keys::Key],
+    options: crate::InjectionOptions,
+  ) -> Result<()> {
+    self
+      .get_active_injector(&options)?
+      .send_key_combination(keys, options)
   }
 }
-
-#[derive(Error, Debug)]
-pub enum X11InjectorError {
-  #[error("failed to initialize x11 display")]
-  Init(),
-
-  #[error("missing vkey mapping for char `{0}`")]
-  CharMapping(String),
-
-  #[error("missing record mapping for sym `{0}`")]
-  SymMapping(u64),
-}
diff --git a/espanso-inject/src/x11/xdotool/README.md b/espanso-inject/src/x11/xdotool/README.md
new file mode 100644
index 00000000..a460e28e
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/README.md
@@ -0,0 +1,2 @@
+This is a fallback injection module that relies on the awesome xdotool project: 
+https://github.com/jordansissel/xdotool
\ No newline at end of file
diff --git a/espanso-inject/src/x11/xdotool/ffi.rs b/espanso-inject/src/x11/xdotool/ffi.rs
new file mode 100644
index 00000000..c3f8f17d
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/ffi.rs
@@ -0,0 +1,61 @@
+use libc::{c_char, c_int, c_long, useconds_t, wchar_t};
+
+use crate::x11::ffi::{Display, Window};
+
+#[repr(C)]
+pub struct charcodemap_t {
+  pub key: wchar_t,
+  pub code: c_char,
+  pub symbol: c_long,
+  pub group: c_int,
+  pub modmask: c_int,
+  pub needs_binding: c_int,
+}
+
+#[repr(C)]
+pub struct xdo_t {
+  pub xdpy: *mut Display,
+  pub display_name: *const c_char,
+  pub charcodes: *const charcodemap_t,
+  pub charcodes_len: c_int,
+  pub keycode_high: c_int,
+  pub keycode_low: c_int,
+  pub keysyms_per_keycode: c_int,
+  pub close_display_when_freed: c_int,
+  pub quiet: c_int,
+  pub debug: c_int,
+  pub features_mask: c_int,
+}
+
+pub const CURRENTWINDOW: u64 = 0;
+
+#[link(name = "xdotoolvendor", kind = "static")]
+extern "C" {
+  pub fn xdo_new(display: *const c_char) -> *mut xdo_t;
+  pub fn xdo_free(xdo: *const xdo_t);
+  pub fn xdo_enter_text_window(
+    xdo: *const xdo_t,
+    window: Window,
+    string: *const c_char,
+    delay: useconds_t,
+  );
+  pub fn xdo_send_keysequence_window(
+    xdo: *const xdo_t,
+    window: Window,
+    keysequence: *const c_char,
+    delay: useconds_t,
+  );
+  pub fn fast_send_event(xdo: *const xdo_t, window: Window, keycode: c_int, pressed: c_int);
+  pub fn fast_enter_text_window(
+    xdo: *const xdo_t,
+    window: Window,
+    string: *const c_char,
+    delay: useconds_t,
+  );
+  pub fn fast_send_keysequence_window(
+    xdo: *const xdo_t,
+    window: Window,
+    keysequence: *const c_char,
+    delay: useconds_t,
+  );
+}
diff --git a/espanso-inject/src/x11/xdotool/mod.rs b/espanso-inject/src/x11/xdotool/mod.rs
new file mode 100644
index 00000000..726b0530
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/mod.rs
@@ -0,0 +1,348 @@
+/*
+ * 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/>.
+ */
+
+use std::{
+  convert::TryInto,
+  ffi::{CStr, CString},
+};
+
+use crate::Injector;
+use anyhow::{bail, Context, Result};
+use log::debug;
+
+mod ffi;
+use self::ffi::{fast_send_keysequence_window, xdo_send_keysequence_window, xdo_t, CURRENTWINDOW};
+
+use super::ffi::{
+  Display, Window, XGetInputFocus, XKeycodeToKeysym, XKeysymToString, XQueryKeymap,
+  XTestFakeKeyEvent,
+};
+
+pub struct X11XDOToolInjector {
+  xdo: *const xdo_t,
+}
+
+impl X11XDOToolInjector {
+  pub fn new() -> Result<Self> {
+    let xdo = unsafe { ffi::xdo_new(std::ptr::null()) };
+    if xdo.is_null() {
+      bail!("unable to initialize xdo_t instance");
+    }
+
+    debug!("initialized xdo_t object");
+
+    Ok(Self { xdo })
+  }
+
+  fn xfake_release_all_keys(&self) {
+    let mut keys: [u8; 32] = [0; 32];
+    unsafe {
+      XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr());
+    }
+
+    #[allow(clippy::needless_range_loop)]
+    for i in 0..32 {
+      // Only those that are pressed should be changed
+      if keys[i] != 0 {
+        for k in 0..8 {
+          if (keys[i] & (1 << k)) != 0 {
+            let key_code = i * 8 + k;
+            unsafe {
+              XTestFakeKeyEvent((*self.xdo).xdpy, key_code as u32, 0, 0);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  fn get_focused_window(&self) -> Window {
+    let mut focused_window: Window = 0;
+    let mut revert_to = 0;
+    unsafe {
+      XGetInputFocus((*self.xdo).xdpy, &mut focused_window, &mut revert_to);
+    }
+    focused_window
+  }
+
+  fn xfake_send_string(
+    &self,
+    string: &str,
+    options: crate::InjectionOptions,
+  ) -> anyhow::Result<()> {
+    // It may happen that when an expansion is triggered, some keys are still pressed.
+    // This causes a problem if the expanded match contains that character, as the injection
+    // will not be able to register that keypress (as it is already pressed).
+    // To solve the problem, before an expansion we get which keys are currently pressed
+    // and inject a key_release event so that they can be further registered.
+    self.xfake_release_all_keys();
+
+    let c_string = CString::new(string).context("unable to create CString")?;
+    let delay = options.delay * 1000;
+
+    unsafe {
+      ffi::xdo_enter_text_window(
+        self.xdo,
+        CURRENTWINDOW,
+        c_string.as_ptr(),
+        delay.try_into().unwrap(),
+      );
+    }
+
+    Ok(())
+  }
+
+  fn fast_release_all_keys(&self) {
+    let mut keys: [u8; 32] = [0; 32];
+    unsafe {
+      XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr());
+    }
+
+    let focused_window = self.get_focused_window();
+
+    #[allow(clippy::needless_range_loop)]
+    for i in 0..32 {
+      // Only those that are pressed should be changed
+      if keys[i] != 0 {
+        for k in 0..8 {
+          if (keys[i] & (1 << k)) != 0 {
+            let key_code = i * 8 + k;
+            unsafe {
+              ffi::fast_send_event(self.xdo, focused_window, key_code.try_into().unwrap(), 0);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  fn fast_send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> {
+    // It may happen that when an expansion is triggered, some keys are still pressed.
+    // This causes a problem if the expanded match contains that character, as the injection
+    // will not be able to register that keypress (as it is already pressed).
+    // To solve the problem, before an expansion we get which keys are currently pressed
+    // and inject a key_release event so that they can be further registered.
+    self.fast_release_all_keys();
+
+    let c_string = CString::new(string).context("unable to create CString")?;
+    let delay = options.delay * 1000;
+
+    unsafe {
+      ffi::fast_enter_text_window(
+        self.xdo,
+        self.get_focused_window(),
+        c_string.as_ptr(),
+        delay.try_into().unwrap(),
+      );
+    }
+
+    Ok(())
+  }
+}
+
+impl Injector for X11XDOToolInjector {
+  fn send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> {
+    if options.disable_fast_inject {
+      self.xfake_send_string(string, options)
+    } else {
+      self.fast_send_string(string, options)
+    }
+  }
+
+  fn send_keys(
+    &self,
+    keys: &[crate::keys::Key],
+    options: crate::InjectionOptions,
+  ) -> anyhow::Result<()> {
+    let key_syms: Vec<String> = keys
+      .iter()
+      .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) })
+      .collect();
+
+    let delay = options.delay * 1000;
+
+    for key in key_syms {
+      let c_str = CString::new(key).context("unable to generate CString")?;
+
+      if options.disable_fast_inject {
+        unsafe {
+          xdo_send_keysequence_window(
+            self.xdo,
+            CURRENTWINDOW,
+            c_str.as_ptr(),
+            delay.try_into().unwrap(),
+          );
+        }
+      } else {
+        unsafe {
+          fast_send_keysequence_window(
+            self.xdo,
+            self.get_focused_window(),
+            c_str.as_ptr(),
+            delay.try_into().unwrap(),
+          );
+        }
+      }
+    }
+
+    Ok(())
+  }
+
+  fn send_key_combination(
+    &self,
+    keys: &[crate::keys::Key],
+    options: crate::InjectionOptions,
+  ) -> anyhow::Result<()> {
+    let key_syms: Vec<String> = keys
+      .iter()
+      .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) })
+      .collect();
+    let key_combination = key_syms.join("+");
+
+    let delay = options.delay * 1000;
+
+    let c_key_combination = CString::new(key_combination).context("unable to generate CString")?;
+
+    if options.disable_fast_inject {
+      unsafe {
+        xdo_send_keysequence_window(
+          self.xdo,
+          CURRENTWINDOW,
+          c_key_combination.as_ptr(),
+          delay.try_into().unwrap(),
+        );
+      }
+    } else {
+      unsafe {
+        fast_send_keysequence_window(
+          self.xdo,
+          self.get_focused_window(),
+          c_key_combination.as_ptr(),
+          delay.try_into().unwrap(),
+        );
+      }
+    }
+
+    Ok(())
+  }
+}
+
+impl Drop for X11XDOToolInjector {
+  fn drop(&mut self) {
+    unsafe { ffi::xdo_free(self.xdo) }
+  }
+}
+
+fn convert_key_to_keysym(display: *mut Display, key: &crate::keys::Key) -> Option<String> {
+  match key {
+    crate::keys::Key::Alt => Some("Alt_L".to_string()),
+    crate::keys::Key::CapsLock => Some("Caps_Lock".to_string()),
+    crate::keys::Key::Control => Some("Control_L".to_string()),
+    crate::keys::Key::Meta => Some("Meta_L".to_string()),
+    crate::keys::Key::NumLock => Some("Num_Lock".to_string()),
+    crate::keys::Key::Shift => Some("Shift_L".to_string()),
+    crate::keys::Key::Enter => Some("Return".to_string()),
+    crate::keys::Key::Tab => Some("Tab".to_string()),
+    crate::keys::Key::Space => Some("space".to_string()),
+    crate::keys::Key::ArrowDown => Some("downarrow".to_string()),
+    crate::keys::Key::ArrowLeft => Some("leftarrow".to_string()),
+    crate::keys::Key::ArrowRight => Some("rightarrow".to_string()),
+    crate::keys::Key::ArrowUp => Some("uparrow".to_string()),
+    crate::keys::Key::End => Some("End".to_string()),
+    crate::keys::Key::Home => Some("Home".to_string()),
+    crate::keys::Key::PageDown => Some("Page_Down".to_string()),
+    crate::keys::Key::PageUp => Some("Page_Up".to_string()),
+    crate::keys::Key::Escape => Some("Escape".to_string()),
+    crate::keys::Key::Backspace => Some("BackSpace".to_string()),
+    crate::keys::Key::Insert => Some("Insert".to_string()),
+    crate::keys::Key::Delete => Some("Delete".to_string()),
+    crate::keys::Key::F1 => Some("F1".to_string()),
+    crate::keys::Key::F2 => Some("F2".to_string()),
+    crate::keys::Key::F3 => Some("F3".to_string()),
+    crate::keys::Key::F4 => Some("F4".to_string()),
+    crate::keys::Key::F5 => Some("F5".to_string()),
+    crate::keys::Key::F6 => Some("F6".to_string()),
+    crate::keys::Key::F7 => Some("F7".to_string()),
+    crate::keys::Key::F8 => Some("F8".to_string()),
+    crate::keys::Key::F9 => Some("F9".to_string()),
+    crate::keys::Key::F10 => Some("F10".to_string()),
+    crate::keys::Key::F11 => Some("F11".to_string()),
+    crate::keys::Key::F12 => Some("F12".to_string()),
+    crate::keys::Key::F13 => Some("F13".to_string()),
+    crate::keys::Key::F14 => Some("F14".to_string()),
+    crate::keys::Key::F15 => Some("F15".to_string()),
+    crate::keys::Key::F16 => Some("F16".to_string()),
+    crate::keys::Key::F17 => Some("F17".to_string()),
+    crate::keys::Key::F18 => Some("F18".to_string()),
+    crate::keys::Key::F19 => Some("F19".to_string()),
+    crate::keys::Key::F20 => Some("F20".to_string()),
+    crate::keys::Key::A => Some("a".to_string()),
+    crate::keys::Key::B => Some("b".to_string()),
+    crate::keys::Key::C => Some("c".to_string()),
+    crate::keys::Key::D => Some("d".to_string()),
+    crate::keys::Key::E => Some("e".to_string()),
+    crate::keys::Key::F => Some("f".to_string()),
+    crate::keys::Key::G => Some("g".to_string()),
+    crate::keys::Key::H => Some("h".to_string()),
+    crate::keys::Key::I => Some("i".to_string()),
+    crate::keys::Key::J => Some("j".to_string()),
+    crate::keys::Key::K => Some("k".to_string()),
+    crate::keys::Key::L => Some("l".to_string()),
+    crate::keys::Key::M => Some("m".to_string()),
+    crate::keys::Key::N => Some("n".to_string()),
+    crate::keys::Key::O => Some("o".to_string()),
+    crate::keys::Key::P => Some("p".to_string()),
+    crate::keys::Key::Q => Some("q".to_string()),
+    crate::keys::Key::R => Some("r".to_string()),
+    crate::keys::Key::S => Some("s".to_string()),
+    crate::keys::Key::T => Some("t".to_string()),
+    crate::keys::Key::U => Some("u".to_string()),
+    crate::keys::Key::V => Some("v".to_string()),
+    crate::keys::Key::W => Some("w".to_string()),
+    crate::keys::Key::X => Some("x".to_string()),
+    crate::keys::Key::Y => Some("y".to_string()),
+    crate::keys::Key::Z => Some("z".to_string()),
+    crate::keys::Key::N0 => Some("0".to_string()),
+    crate::keys::Key::N1 => Some("1".to_string()),
+    crate::keys::Key::N2 => Some("2".to_string()),
+    crate::keys::Key::N3 => Some("3".to_string()),
+    crate::keys::Key::N4 => Some("4".to_string()),
+    crate::keys::Key::N5 => Some("5".to_string()),
+    crate::keys::Key::N6 => Some("6".to_string()),
+    crate::keys::Key::N7 => Some("7".to_string()),
+    crate::keys::Key::N8 => Some("8".to_string()),
+    crate::keys::Key::N9 => Some("9".to_string()),
+    crate::keys::Key::Numpad0 => Some("KP_0".to_string()),
+    crate::keys::Key::Numpad1 => Some("KP_1".to_string()),
+    crate::keys::Key::Numpad2 => Some("KP_2".to_string()),
+    crate::keys::Key::Numpad3 => Some("KP_3".to_string()),
+    crate::keys::Key::Numpad4 => Some("KP_4".to_string()),
+    crate::keys::Key::Numpad5 => Some("KP_5".to_string()),
+    crate::keys::Key::Numpad6 => Some("KP_6".to_string()),
+    crate::keys::Key::Numpad7 => Some("KP_7".to_string()),
+    crate::keys::Key::Numpad8 => Some("KP_8".to_string()),
+    crate::keys::Key::Numpad9 => Some("KP_9".to_string()),
+    crate::keys::Key::Raw(key_code) => unsafe {
+      let key_sym = XKeycodeToKeysym(display, (*key_code).try_into().unwrap(), 0);
+      let string = XKeysymToString(key_sym);
+      let c_str = CStr::from_ptr(string);
+      Some(c_str.to_string_lossy().to_string())
+    },
+  }
+}
diff --git a/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT b/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT
new file mode 100644
index 00000000..193aac51
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT
@@ -0,0 +1,24 @@
+Copyright (c) 2007, 2008, 2009: Jordan Sissel.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the Jordan Sissel nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY JORDAN SISSEL ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL JORDAN SISSEL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo.c b/espanso-inject/src/x11/xdotool/vendor/xdo.c
new file mode 100644
index 00000000..6b779f5b
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/vendor/xdo.c
@@ -0,0 +1,2266 @@
+/* xdo library
+ * - getwindowfocus contributed by Lee Pumphret
+ * - keysequence_{up,down} contributed by Magnus Boman
+ *
+ * See the following url for an explanation of how keymaps work in X11
+ * http://www.in-ulm.de/~mascheck/X11/xmodmap.html
+ */
+
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE 500
+#endif /* _XOPEN_SOURCE */
+
+#include <sys/select.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <regex.h>
+#include <ctype.h>
+#include <locale.h>
+#include <stdarg.h>
+
+#include <X11/Xlib.h>
+#include <X11/XKBlib.h>
+#include <X11/Xatom.h>
+#include <X11/Xresource.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XTest.h>
+#include <X11/keysym.h>
+#include <X11/cursorfont.h>
+
+#include <xkbcommon/xkbcommon.h>
+
+#include "xdo.h"
+#include "xdo_util.h"
+
+#define DEFAULT_DELAY 12
+
+/**
+ * The number of tries to check for a wait condition before aborting.
+ * TODO(sissel): Make this tunable at runtime?
+ */
+#define MAX_TRIES 500
+
+static void _xdo_populate_charcode_map(xdo_t *xdo);
+static int _xdo_has_xtest(const xdo_t *xdo);
+
+static KeySym _xdo_keysym_from_char(const xdo_t *xdo, wchar_t key);
+static void _xdo_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key);
+static void _xdo_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym);
+//static int _xdo_get_shiftcode_if_needed(const xdo_t *xdo, char key);
+
+static int _xdo_send_keysequence_window_to_keycode_list(const xdo_t *xdo, const char *keyseq,
+                                            charcodemap_t **keys, int *nkeys);
+static int _xdo_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq,
+                               int pressed, int *modifier, useconds_t delay);
+static int _xdo_ewmh_is_supported(const xdo_t *xdo, const char *feature);
+static void _xdo_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk);
+static void _xdo_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
+                          int modstate, int is_press, useconds_t delay);
+static void _xdo_send_modifier(const xdo_t *xdo, int modmask, int is_press);
+
+static int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode);
+static int _xdo_mousebutton(const xdo_t *xdo, Window window, int button, int is_press);
+
+static int _is_success(const char *funcname, int code, const xdo_t *xdo);
+static void _xdo_debug(const xdo_t *xdo, const char *format, ...);
+static void _xdo_eprintf(const xdo_t *xdo, int hushable, const char *format, ...);
+
+/* context-free functions */
+static wchar_t _keysym_to_char(KeySym keysym);
+
+/* Default to -1, initialize it when we need it */
+static Atom atom_NET_WM_PID = -1;
+static Atom atom_NET_WM_NAME = -1;
+static Atom atom_WM_NAME = -1;
+static Atom atom_STRING = -1;
+static Atom atom_UTF8_STRING = -1;
+
+xdo_t* xdo_new(const char *display_name) {
+  Display *xdpy;
+
+  if (display_name == NULL) {
+    display_name = XDisplayName(display_name);
+  }
+
+#define DISPLAY_HINT "Is there an Xorg or other X server running? You can try setting 'export DISPLAY=:0' and trying again."
+  if (display_name == NULL) {
+    fprintf(stderr, "Error: No DISPLAY environment variable is set. " DISPLAY_HINT "\n");
+    return NULL;
+  }
+
+  if (*display_name == '\0') {
+    fprintf(stderr, "Error: DISPLAY environment variable is empty. " DISPLAY_HINT "\n");
+    return NULL;
+  }
+
+  if ((xdpy = XOpenDisplay(display_name)) == NULL) {
+    return NULL;
+  }
+
+  return xdo_new_with_opened_display(xdpy, display_name, 1);
+}
+
+xdo_t* xdo_new_with_opened_display(Display *xdpy, const char *display,
+                                   int close_display_when_freed) {
+  xdo_t *xdo = NULL;
+
+  if (xdpy == NULL) {
+    /* Can't use _xdo_eprintf yet ... */
+    fprintf(stderr, "xdo_new: xdisplay I was given is a null pointer\n");
+    return NULL;
+  }
+
+  // This library and xdotool do not work correctly on Wayland/XWayland.
+  // Try to detect XWayland and warn the user about problems.
+  // TODO(sissel): This was disabled due to issue #346
+  //  -- xdotool works on XWayland for some operations, so it isn't helpful to refuse all usage on XWayland.
+  //if (appears_to_be_wayland(xdpy)) {
+    //fprintf(stderr, "The X server at %s appears to be XWayland. Unfortunately, XWayland does not correctly support the features used by libxdo and xdotool.\n", display);
+    //return NULL;
+  //}
+
+
+  /* XXX: Check for NULL here */
+  xdo = malloc(sizeof(xdo_t));
+  memset(xdo, 0, sizeof(xdo_t));
+
+  xdo->xdpy = xdpy;
+  xdo->close_display_when_freed = close_display_when_freed;
+
+  if (display == NULL) {
+    display = "unknown";
+  }
+
+  if (getenv("XDO_QUIET")) {
+    xdo->quiet = True;
+  }
+
+  if (_xdo_has_xtest(xdo)) {
+    xdo_enable_feature(xdo, XDO_FEATURE_XTEST);
+    _xdo_debug(xdo, "XTEST enabled.");
+  } else {
+    _xdo_eprintf(xdo, False, "Warning: XTEST extension unavailable on '%s'. Some"
+                " functionality may be disabled; See 'man xdotool' for more"
+                " info.", xdo->display_name);
+    xdo_disable_feature(xdo, XDO_FEATURE_XTEST);
+  }
+
+  _xdo_populate_charcode_map(xdo);
+  return xdo;
+}
+
+void xdo_free(xdo_t *xdo) {
+  if (xdo == NULL)
+    return;
+
+  free(xdo->display_name);
+  free(xdo->charcodes);
+  if (xdo->xdpy && xdo->close_display_when_freed)
+    XCloseDisplay(xdo->xdpy);
+
+  free(xdo);
+}
+
+int xdo_wait_for_window_map_state(const xdo_t *xdo, Window wid, int map_state) {
+  int tries = MAX_TRIES;
+  XWindowAttributes attr;
+  attr.map_state = IsUnmapped;
+  while (tries > 0 && attr.map_state != map_state) {
+    XGetWindowAttributes(xdo->xdpy, wid, &attr);
+    usleep(30000); /* TODO(sissel): Use exponential backoff up to 1 second */
+    tries--;
+  }
+  return 0;
+}
+
+int xdo_map_window(const xdo_t *xdo, Window wid) {
+  int ret = 0;
+  ret = XMapWindow(xdo->xdpy, wid);
+  XFlush(xdo->xdpy);
+  return _is_success("XMapWindow", ret == 0, xdo);
+}
+
+int xdo_unmap_window(const xdo_t *xdo, Window wid) {
+  int ret = 0;
+  ret = XUnmapWindow(xdo->xdpy, wid);
+  XFlush(xdo->xdpy);
+  return _is_success("XUnmapWindow", ret == 0, xdo);
+}
+
+int xdo_reparent_window(const xdo_t *xdo, Window wid_source, Window wid_target) {
+  int ret = 0;
+  ret = XReparentWindow(xdo->xdpy, wid_source, wid_target, 0, 0);
+  XFlush(xdo->xdpy);
+  return _is_success("XReparentWindow", ret == 0, xdo);
+}
+
+int xdo_get_window_location(const xdo_t *xdo, Window wid,
+                            int *x_ret, int *y_ret, Screen **screen_ret) {
+  int ret;
+  XWindowAttributes attr;
+  ret = XGetWindowAttributes(xdo->xdpy, wid, &attr);
+  if (ret != 0) {
+    int x, y;
+    Window unused_child;
+
+    /* The coordinates in attr are relative to the parent window.  If
+     * the parent window is the root window, then the coordinates are
+     * correct.  If the parent window isn't the root window --- which
+     * is likely --- then we translate them. */
+    Window parent;
+    Window root;
+    Window* children;
+    unsigned int nchildren;
+    XQueryTree(xdo->xdpy, wid, &root, &parent, &children, &nchildren);
+    if (children != NULL) {
+      XFree(children);
+    }
+    if (parent == attr.root) {
+      x = attr.x;
+      y = attr.y;
+    } else {
+      XTranslateCoordinates(xdo->xdpy, wid, attr.root,
+                            0, 0, &x, &y, &unused_child);
+    }
+
+    if (x_ret != NULL) {
+      *x_ret = x;
+    }
+
+    if (y_ret != NULL) {
+      *y_ret = y;
+    }
+
+    if (screen_ret != NULL) {
+      *screen_ret = attr.screen;
+    }
+  }
+  return _is_success("XGetWindowAttributes", ret == 0, xdo);
+}
+
+int xdo_get_window_size(const xdo_t *xdo, Window wid, unsigned int *width_ret,
+                        unsigned int *height_ret) {
+  int ret;
+  XWindowAttributes attr;
+  ret = XGetWindowAttributes(xdo->xdpy, wid, &attr);
+  if (ret != 0) {
+    if (width_ret != NULL) {
+      *width_ret = attr.width;
+    }
+
+    if (height_ret != NULL) {
+      *height_ret = attr.height;
+    }
+  }
+  return _is_success("XGetWindowAttributes", ret == 0, xdo);
+}
+
+int xdo_move_window(const xdo_t *xdo, Window wid, int x, int y) {
+  XWindowChanges wc;
+  int ret = 0;
+  wc.x = x;
+  wc.y = y;
+
+  ret = XConfigureWindow(xdo->xdpy, wid, CWX | CWY, &wc);
+  return _is_success("XConfigureWindow", ret == 0, xdo);
+}
+
+int xdo_translate_window_with_sizehint(const xdo_t *xdo, Window window,
+                                       unsigned int width, unsigned int height, 
+                                       unsigned int *width_ret, unsigned int *height_ret) {
+  XSizeHints hints;
+  long supplied_return;
+  XGetWMNormalHints(xdo->xdpy, window, &hints, &supplied_return);
+  if (supplied_return & PResizeInc) {
+    width *= hints.width_inc;
+    height *= hints.height_inc;
+  } else {
+    fprintf(stderr, "No size hints found for window %ld\n", window);
+    *width_ret = width;
+    *height_ret = width;
+  }
+
+  if (supplied_return & PBaseSize) {
+    width += hints.base_width;
+    height += hints.base_height;
+  }
+
+  if (width_ret != NULL) {
+    *width_ret = width;
+  }
+
+  if (height_ret != NULL) {
+    *height_ret = height;
+  }
+
+  return XDO_SUCCESS;
+}
+
+int xdo_set_window_size(const xdo_t *xdo, Window window, int width, int height, int flags) {
+  XWindowChanges wc;
+  int ret = 0;
+  int cw_flags = 0;
+
+  if (flags & SIZE_USEHINTS) {
+    flags |= SIZE_USEHINTS_X | SIZE_USEHINTS_Y;
+  }
+
+  wc.width = width;
+  wc.height = height;
+
+  if (flags & SIZE_USEHINTS_X) {
+    xdo_translate_window_with_sizehint(xdo, window, width, height, (unsigned int*)&wc.width,
+                                       NULL);
+  }
+
+  if (flags & SIZE_USEHINTS_Y) {
+    xdo_translate_window_with_sizehint(xdo, window, width, height, NULL,
+                                       (unsigned int*)&wc.height);
+  }
+
+  if (width > 0) {
+    cw_flags |= CWWidth;
+  }
+
+  if (height > 0) {
+    cw_flags |= CWHeight;
+  }
+
+  ret = XConfigureWindow(xdo->xdpy, window, cw_flags, &wc);
+  XFlush(xdo->xdpy);
+  return _is_success("XConfigureWindow", ret == 0, xdo);
+}
+
+int xdo_set_window_override_redirect(const xdo_t *xdo, Window wid,
+                                     int override_redirect) {
+  int ret;
+  XSetWindowAttributes wattr;
+  long mask = CWOverrideRedirect;
+  wattr.override_redirect = override_redirect;
+  ret = XChangeWindowAttributes(xdo->xdpy, wid, mask, &wattr);
+
+  return _is_success("XChangeWindowAttributes", ret == 0, xdo);
+}
+
+int xdo_set_window_class (const xdo_t *xdo, Window wid, const char *name,
+                         const char *_class) {
+  int ret = 0;
+  XClassHint *hint = XAllocClassHint();
+  XGetClassHint(xdo->xdpy, wid, hint);
+  if (name != NULL)
+    hint->res_name = (char*)name;
+
+  if(_class != NULL)
+    hint->res_class = (char*)_class;
+
+  ret = XSetClassHint(xdo->xdpy, wid, hint);
+  XFree(hint);
+  return _is_success("XSetClassHint", ret == 0, xdo);
+}
+
+int xdo_set_window_urgency (const xdo_t *xdo, Window wid, int urgency) {
+  int ret = 0;
+  XWMHints *hint = XGetWMHints(xdo->xdpy, wid);
+  if (hint == NULL)
+    hint = XAllocWMHints();
+
+  if (urgency)
+    hint->flags = hint->flags | XUrgencyHint;
+  else
+    hint->flags = hint->flags & ~XUrgencyHint;
+
+  ret = XSetWMHints(xdo->xdpy, wid, hint);
+  XFree(hint);
+  return _is_success("XSetWMHint", ret == 0, xdo);
+}
+
+int xdo_set_window_property(const xdo_t *xdo, Window wid, const char *property, const char *value) {
+  
+  char netwm_property[256] = "_NET_";
+  int ret = 0;
+  strcat(netwm_property, property);
+
+  // Change the property
+  ret = XChangeProperty(xdo->xdpy, wid, 
+                        XInternAtom(xdo->xdpy, property, False), 
+                        XInternAtom(xdo->xdpy, "STRING", False), 8, 
+                        PropModeReplace, (unsigned char*)value, strlen(value));
+  if (ret == 0) {
+    return _is_success("XChangeProperty", ret == 0, xdo);
+  }
+
+  // Change _NET_<property> just in case for simpler NETWM compliance?
+  ret = XChangeProperty(xdo->xdpy, wid, 
+                        XInternAtom(xdo->xdpy, netwm_property, False), 
+                        XInternAtom(xdo->xdpy, "STRING", False), 8, 
+                        PropModeReplace, (unsigned char*)value, strlen(value));
+  return _is_success("XChangeProperty", ret == 0, xdo);
+}
+
+int xdo_focus_window(const xdo_t *xdo, Window wid) {
+  int ret = 0;
+  ret = XSetInputFocus(xdo->xdpy, wid, RevertToParent, CurrentTime);
+  XFlush(xdo->xdpy);
+  return _is_success("XSetInputFocus", ret == 0, xdo);
+}
+
+int xdo_wait_for_window_size(const xdo_t *xdo, Window window,
+                             unsigned int width, unsigned int height,
+                             int flags, int to_or_from) {
+  unsigned int cur_width, cur_height;
+  /*unsigned int alt_width, alt_height;*/
+
+  //printf("Want: %udx%ud\n", width, height);
+  if (flags & SIZE_USEHINTS) {
+    xdo_translate_window_with_sizehint(xdo, window, width, height,
+                                       &width, &height);
+  } else {
+    unsigned int hint_width, hint_height;
+    /* TODO(sissel): fix compiler warning here, but it will require
+     * an ABI breakage by changing types... */
+    xdo_translate_window_with_sizehint(xdo, window, 1, 1,
+                                       &hint_width, &hint_height);
+    //printf("Hint: %dx%d\n", hint_width, hint_height);
+    /* Find the nearest multiple (rounded down) of the hint height. */
+    /*alt_width = (width - (width % hint_width));*/
+    /*alt_height = (height - (height % hint_height));*/
+    //printf("Alt: %udx%ud\n", alt_width, alt_height);
+  }
+
+  int tries = MAX_TRIES;
+  xdo_get_window_size(xdo, window, &cur_width,
+                      &cur_height);
+  //printf("Want: %udx%ud\n", width, height);
+  //printf("Alt: %udx%ud\n", alt_width, alt_height);
+  while (tries > 0 && (to_or_from == SIZE_TO
+         ? (cur_width != width && cur_height != height)
+         : (cur_width == width && cur_height == height))) {
+    xdo_get_window_size(xdo, window, (unsigned int *)&cur_width,
+                        (unsigned int *)&cur_height);
+    usleep(30000);
+    tries--;
+  }
+
+  return 0;
+}
+
+int xdo_wait_for_window_active(const xdo_t *xdo, Window window, int active) {
+  Window activewin = 0;
+  int ret = 0;
+  int tries = MAX_TRIES;
+
+  /* If active is true, wait until activewin is our window
+   * otherwise, wait until activewin is not our window */
+  while (tries > 0 && 
+         (active ? activewin != window : activewin == window)) {
+    ret = xdo_get_active_window(xdo, &activewin);
+    if (ret == XDO_ERROR) {
+      return ret;
+    }
+    usleep(30000);
+    tries--;
+  }
+
+  return 0;
+}
+
+int xdo_activate_window(const xdo_t *xdo, Window wid) {
+  int ret = 0;
+  long desktop = 0;
+  XEvent xev;
+  XWindowAttributes wattr;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_ACTIVE_WINDOW") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_ACTIVE_WINDOW, "
+            "so the attempt to activate the window was aborted.\n");
+    return XDO_ERROR;
+  }
+
+  /* If this window is on another desktop, let's go to that desktop first */
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == True
+      && _xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == True) {
+    xdo_get_desktop_for_window(xdo, wid, &desktop);
+    xdo_set_current_desktop(xdo, desktop);
+  }
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = wid;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_ACTIVE_WINDOW", False);
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = 2L; /* 2 == Message from a window pager */
+  xev.xclient.data.l[1] = CurrentTime;
+
+  XGetWindowAttributes(xdo->xdpy, wid, &wattr);
+  ret = XSendEvent(xdo->xdpy, wattr.screen->root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask,
+                   &xev);
+
+  /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise.
+   * Manpage says it will only generate BadWindow or BadValue errors */
+  return _is_success("XSendEvent[EWMH:_NET_ACTIVE_WINDOW]", ret == 0, xdo);
+}
+
+int xdo_set_number_of_desktops(const xdo_t *xdo, long ndesktops) {
+  /* XXX: This should support passing a screen number */
+  XEvent xev;
+  Window root;
+  int ret = 0;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_NUMBER_OF_DESKTOPS") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_NUMBER_OF_DESKTOPS, "
+            "so the attempt to change the number of desktops was aborted.\n");
+    return XDO_ERROR;
+  }
+
+  root = RootWindow(xdo->xdpy, 0);
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = root;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_NUMBER_OF_DESKTOPS", 
+                                         False);
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = ndesktops;
+
+  ret = XSendEvent(xdo->xdpy, root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask,
+                   &xev);
+
+  return _is_success("XSendEvent[EWMH:_NET_NUMBER_OF_DESKTOPS]", ret == 0, xdo);
+}
+
+int xdo_get_number_of_desktops(const xdo_t *xdo, long *ndesktops) {
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  Window root;
+  Atom request;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_NUMBER_OF_DESKTOPS") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_NUMBER_OF_DESKTOPS, "
+            "so the attempt to query the number of desktops was aborted.\n");
+    return XDO_ERROR;
+  }
+
+  request = XInternAtom(xdo->xdpy, "_NET_NUMBER_OF_DESKTOPS", False);
+  root = XDefaultRootWindow(xdo->xdpy);
+
+  data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size);
+
+  if (nitems > 0) {
+    *ndesktops = *((long*)data);
+  } else {
+    *ndesktops = 0;
+  }
+  free(data);
+
+  return _is_success("XGetWindowProperty[_NET_NUMBER_OF_DESKTOPS]",
+                     *ndesktops == 0, xdo);
+}
+
+int xdo_set_current_desktop(const xdo_t *xdo, long desktop) {
+  /* XXX: This should support passing a screen number */
+  XEvent xev;
+  Window root;
+  int ret = 0;
+
+  root = RootWindow(xdo->xdpy, 0);
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_CURRENT_DESKTOP, "
+            "so the attempt to change desktops was aborted.\n");
+    return XDO_ERROR;
+  }
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = root;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_CURRENT_DESKTOP", 
+                                         False);
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = desktop;
+  xev.xclient.data.l[1] = CurrentTime;
+
+  ret = XSendEvent(xdo->xdpy, root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask,
+                   &xev);
+
+  return _is_success("XSendEvent[EWMH:_NET_CURRENT_DESKTOP]", ret == 0, xdo);
+}
+
+int xdo_get_current_desktop(const xdo_t *xdo, long *desktop) {
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  Window root;
+
+  Atom request;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_CURRENT_DESKTOP, "
+            "so the query for the current desktop was aborted.\n");
+    return XDO_ERROR;
+  }
+
+  request = XInternAtom(xdo->xdpy, "_NET_CURRENT_DESKTOP", False);
+  root = XDefaultRootWindow(xdo->xdpy);
+
+  data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size);
+
+  if (nitems > 0) {
+    *desktop = *((long*)data);
+  } else {
+    *desktop = -1;
+  }
+  free(data);
+
+  return _is_success("XGetWindowProperty[_NET_CURRENT_DESKTOP]",
+                     *desktop == -1, xdo);
+}
+
+int xdo_set_desktop_for_window(const xdo_t *xdo, Window wid, long desktop) {
+  XEvent xev;
+  int ret = 0;
+  XWindowAttributes wattr;
+  XGetWindowAttributes(xdo->xdpy, wid, &wattr);
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_WM_DESKTOP, "
+            "so the attempt to change a window's desktop location was "
+            "aborted.\n");
+    return XDO_ERROR;
+  }
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = wid;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_WM_DESKTOP", 
+                                         False);
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = desktop;
+  xev.xclient.data.l[1] = 2; /* indicate we are messaging from a pager */
+
+  ret = XSendEvent(xdo->xdpy, wattr.screen->root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask,
+                   &xev);
+
+  return _is_success("XSendEvent[EWMH:_NET_WM_DESKTOP]", ret == 0, xdo);
+}
+
+int xdo_get_desktop_for_window(const xdo_t *xdo, Window wid, long *desktop) {
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  Atom request;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_WM_DESKTOP, "
+            "so the attempt to query a window's desktop location was "
+            "aborted.\n");
+    return XDO_ERROR;
+  }
+
+  request = XInternAtom(xdo->xdpy, "_NET_WM_DESKTOP", False);
+
+  data = xdo_get_window_property_by_atom(xdo, wid, request, &nitems, &type, &size);
+
+  if (nitems > 0) {
+    *desktop = *((long*)data);
+  } else {
+    *desktop = -1;
+  }
+  free(data);
+
+  return _is_success("XGetWindowProperty[_NET_WM_DESKTOP]",
+                     *desktop == -1, xdo);
+}
+
+int xdo_get_active_window(const xdo_t *xdo, Window *window_ret) {
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  Atom request;
+  Window root;
+
+  if (_xdo_ewmh_is_supported(xdo, "_NET_ACTIVE_WINDOW") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_ACTIVE_WINDOW, "
+            "so the attempt to query the active window aborted.\n");
+    return XDO_ERROR;
+  }
+
+  request = XInternAtom(xdo->xdpy, "_NET_ACTIVE_WINDOW", False);
+  root = XDefaultRootWindow(xdo->xdpy);
+  data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size);
+
+  if (nitems > 0) {
+    *window_ret = *((Window*)data);
+  } else {
+    *window_ret = 0;
+  }
+  free(data);
+
+  return _is_success("XGetWindowProperty[_NET_ACTIVE_WINDOW]",
+                     *window_ret == 0, xdo);
+}
+
+int xdo_select_window_with_click(const xdo_t *xdo, Window *window_ret) {
+  int screen_num;
+  Screen *screen;
+  xdo_get_mouse_location(xdo, NULL, NULL, &screen_num);
+
+  screen = ScreenOfDisplay(xdo->xdpy, screen_num);
+
+  /* Grab sync mode so we can ensure nothing changes while we figure
+   * out what the client window is.
+   * Also, everyone else who does 'select window' does it this way.
+   */
+  Cursor cursor = XCreateFontCursor(xdo->xdpy, XC_target);
+  int grab_ret = 0;
+  grab_ret = XGrabPointer(xdo->xdpy, screen->root, False, ButtonReleaseMask,
+               GrabModeSync, GrabModeAsync, screen->root, cursor, CurrentTime);
+  if (grab_ret == AlreadyGrabbed) {
+    fprintf(stderr, "Attempt to grab the mouse failed. Something already has"
+            " the mouse grabbed. This can happen if you are dragging something"
+            " or if there is a popup currently shown\n");
+    return XDO_ERROR;
+  }
+
+  XEvent e;
+  XAllowEvents(xdo->xdpy, SyncPointer, CurrentTime);
+  XWindowEvent(xdo->xdpy, screen->root, ButtonReleaseMask, &e);
+  XUngrabPointer(xdo->xdpy, CurrentTime);
+  XFreeCursor(xdo->xdpy, cursor);
+
+  if (e.xbutton.button != 1) {
+    fprintf(stderr, "window selection aborted with button %d\n", e.xbutton.button);
+    return XDO_ERROR;
+  }
+
+  /* If there is no subwindow, then we clicked on the root window */
+  if (e.xbutton.subwindow == 0) {
+    *window_ret = e.xbutton.root;
+  } else {
+     /* Random testing showed that 'root' always is the same as 'window'
+      * while 'subwindow' is the actual window we clicked on. Confusing... */
+     *window_ret = e.xbutton.subwindow;
+    _xdo_debug(xdo, "Click on window %lu foo", *window_ret);
+    xdo_find_window_client(xdo, *window_ret, window_ret, XDO_FIND_CHILDREN);
+  }
+  return XDO_SUCCESS;
+}
+
+/* XRaiseWindow is ignored in ion3 and Gnome2. Is it even useful? */
+int xdo_raise_window(const xdo_t *xdo, Window wid) {
+  int ret = 0;
+  ret = XRaiseWindow(xdo->xdpy, wid);
+  XFlush(xdo->xdpy);
+  return _is_success("XRaiseWindow", ret == 0, xdo);
+}
+
+int xdo_move_mouse(const xdo_t *xdo, int x, int y, int screen)  {
+  int ret = 0;
+
+  /* There is a bug (feature?) in XTestFakeMotionEvent that causes
+   * the screen number in the request to be ignored. The internets
+   * seem to recommend XWarpPointer instead, ie;
+   * https://bugzilla.redhat.com/show_bug.cgi?id=518803
+   */
+  Window screen_root = RootWindow(xdo->xdpy, screen);
+  ret = XWarpPointer(xdo->xdpy, None, screen_root, 0, 0, 0, 0, x, y);
+  XFlush(xdo->xdpy);
+  return _is_success("XWarpPointer", ret == 0, xdo);
+}
+
+int xdo_move_mouse_relative_to_window(const xdo_t *xdo, Window window, int x, int y) {
+  XWindowAttributes attr;
+  Window unused_child;
+  int root_x, root_y;
+
+  XGetWindowAttributes(xdo->xdpy, window, &attr);
+  XTranslateCoordinates(xdo->xdpy, window, attr.root,
+                        x, y, &root_x, &root_y, &unused_child);
+  return xdo_move_mouse(xdo, root_x, root_y, XScreenNumberOfScreen(attr.screen));
+}
+
+int xdo_move_mouse_relative(const xdo_t *xdo, int x, int y)  {
+  int ret = 0;
+  ret = XTestFakeRelativeMotionEvent(xdo->xdpy, x, y, CurrentTime);
+  XFlush(xdo->xdpy);
+  return _is_success("XTestFakeRelativeMotionEvent", ret == 0, xdo);
+}
+
+int _xdo_mousebutton(const xdo_t *xdo, Window window, int button, int is_press) {
+  int ret = 0;
+
+  if (window == CURRENTWINDOW) {
+    ret = XTestFakeButtonEvent(xdo->xdpy, button, is_press, CurrentTime);
+    XFlush(xdo->xdpy);
+    return _is_success("XTestFakeButtonEvent(down)", ret == 0, xdo);
+  } else {
+    /* Send to specific window */
+    int screen = 0;
+    XButtonEvent xbpe;
+    charcodemap_t *active_mod;
+    int active_mod_n;
+
+    xdo_get_mouse_location(xdo, &xbpe.x_root, &xbpe.y_root, &screen);
+    xdo_get_active_modifiers(xdo, &active_mod, &active_mod_n);
+
+    xbpe.window = window;
+    xbpe.button = button;
+    xbpe.display = xdo->xdpy;
+    xbpe.root = RootWindow(xdo->xdpy, screen);
+    xbpe.same_screen = True; /* Should we detect if window is on the same
+                                 screen as cursor? */
+    xbpe.state = xdo_get_input_state(xdo);
+
+    xbpe.subwindow = None;
+    xbpe.time = CurrentTime;
+    xbpe.type = (is_press ? ButtonPress : ButtonRelease);
+
+    /* Get the coordinates of the cursor relative to xbpe.window and also find what
+     * subwindow it might be on */
+    XTranslateCoordinates(xdo->xdpy, xbpe.root, xbpe.window, 
+                          xbpe.x_root, xbpe.y_root, &xbpe.x, &xbpe.y, &xbpe.subwindow);
+
+    /* Normal behavior of 'mouse up' is that the modifier mask includes
+     * 'ButtonNMotionMask' where N is the button being released. This works the
+     * same way with keys, too. */
+    if (!is_press) { /* is mouse up */
+      switch(button) {
+        case 1: xbpe.state |= Button1MotionMask; break;
+        case 2: xbpe.state |= Button2MotionMask; break;
+        case 3: xbpe.state |= Button3MotionMask; break;
+        case 4: xbpe.state |= Button4MotionMask; break;
+        case 5: xbpe.state |= Button5MotionMask; break;
+      }
+    }
+    ret = XSendEvent(xdo->xdpy, window, True, ButtonPressMask, (XEvent *)&xbpe);
+    XFlush(xdo->xdpy);
+    free(active_mod);
+    return _is_success("XSendEvent(mousedown)", ret == 0, xdo);
+  }
+}
+
+int xdo_mouse_up(const xdo_t *xdo, Window window, int button) {
+  return _xdo_mousebutton(xdo, window, button, False);
+}
+
+int xdo_mouse_down(const xdo_t *xdo, Window window, int button) {
+  return _xdo_mousebutton(xdo, window, button, True);
+}
+
+int xdo_get_mouse_location(const xdo_t *xdo, int *x_ret, int *y_ret,
+                           int *screen_num_ret) {
+  return xdo_get_mouse_location2(xdo, x_ret, y_ret, screen_num_ret, NULL);
+}
+
+int xdo_get_window_at_mouse(const xdo_t *xdo, Window *window_ret) {
+  return xdo_get_mouse_location2(xdo, NULL, NULL, NULL, window_ret);
+}
+
+int xdo_get_mouse_location2(const xdo_t *xdo, int *x_ret, int *y_ret,
+                            int *screen_num_ret, Window *window_ret) {
+  int ret = False;
+  int x = 0, y = 0, screen_num = 0;
+  int i = 0;
+  Window window = 0;
+  Window root = 0;
+  int dummy_int = 0;
+  unsigned int dummy_uint = 0;
+  int screencount = ScreenCount(xdo->xdpy);
+
+  for (i = 0; i < screencount; i++) {
+    Screen *screen = ScreenOfDisplay(xdo->xdpy, i);
+    ret = XQueryPointer(xdo->xdpy, RootWindowOfScreen(screen),
+                        &root, &window,
+                        &x, &y, &dummy_int, &dummy_int, &dummy_uint);
+    if (ret == True) {
+      screen_num = i;
+      break;
+    }
+  }
+
+  if (window_ret != NULL) {
+    /* Find the client window if we are not root. */
+    if (window != root && window != 0) {
+      int findret;
+      Window client = 0;
+
+      /* Search up the stack for a client window for this window */
+      findret = xdo_find_window_client(xdo, window, &client, XDO_FIND_PARENTS);
+      if (findret == XDO_ERROR) {
+        /* If no client found, search down the stack */
+        findret = xdo_find_window_client(xdo, window, &client, XDO_FIND_CHILDREN);
+      }
+      //fprintf(stderr, "%ld, %ld, %ld, %d\n", window, root, client, findret);
+      if (findret == XDO_SUCCESS) {
+        window = client;
+      }
+    } else {
+      window = root;
+    }
+  }
+  //printf("mouseloc root: %ld\n", root);
+  //printf("mouseloc window: %ld\n", window);
+
+  if (ret == True) {
+    if (x_ret != NULL) *x_ret = x;
+    if (y_ret != NULL) *y_ret = y;
+    if (screen_num_ret != NULL) *screen_num_ret = screen_num;
+    if (window_ret != NULL) *window_ret = window;
+  }
+
+  return _is_success("XQueryPointer", ret == False, xdo);
+}
+
+int xdo_click_window(const xdo_t *xdo, Window window, int button) {
+  int ret = 0;
+  ret = xdo_mouse_down(xdo, window, button);
+  if (ret != XDO_SUCCESS) {
+    fprintf(stderr, "xdo_mouse_down failed, aborting click.\n");
+    return ret;
+  }
+  usleep(DEFAULT_DELAY);
+  ret = xdo_mouse_up(xdo, window, button);
+  return ret;
+}
+
+int xdo_click_window_multiple(const xdo_t *xdo, Window window, int button,
+                       int repeat, useconds_t delay) {
+  int ret = 0;
+  while (repeat > 0) {
+    ret = xdo_click_window(xdo, window, button);
+    if (ret != XDO_SUCCESS) {
+      fprintf(stderr, "click failed with %d repeats remaining\n", repeat);
+      return ret;
+    }
+    repeat--;
+
+    /* Sleeping even after the last click is important, so that a call to xdo_set_active_modifiers()
+     * right after won't think that the button is still pressed. */
+    usleep(delay);
+  } /* while (repeat > 0) */
+  return ret;
+} /* int xdo_click_window_multiple */
+
+/* XXX: Return proper code if errors found */
+int xdo_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) {
+
+  /* Since we're doing down/up, the delay should be based on the number
+   * of keys pressed (including shift). Since up/down is two calls,
+   * divide by two. */
+  delay /= 2;
+
+  /* XXX: Add error handling */
+  //int nkeys = strlen(string);
+  //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t));
+  charcodemap_t key;
+  //int modifier = 0;
+  setlocale(LC_CTYPE,"");
+  mbstate_t ps = { 0 };
+  ssize_t len;
+  while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) {
+    if (len == -1) {
+      fprintf(stderr, "Invalid multi-byte sequence encountered\n");
+      return XDO_ERROR;
+    }
+    _xdo_charcodemap_from_char(xdo, &key);
+    if (key.code == 0 && key.symbol == NoSymbol) {
+      fprintf(stderr, "I don't know which key produces '%lc', skipping.\n",
+              key.key);
+      continue;
+    } else {
+      //printf("Found key for %c\n", key.key);
+      //printf("code: %d\n", key.code);
+      //printf("sym: %s\n", XKeysymToString(key.symbol));
+    }
+
+    //printf(stderr,
+            //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n",
+            //key.key, key.code, key.symbol, key.group, key.modmask,
+            //(key.needs_binding == 1) ? "needs binding" : "ok");
+
+    //_xdo_send_key(xdo, window, keycode, modstate, True, delay);
+    //_xdo_send_key(xdo, window, keycode, modstate, False, delay);
+    xdo_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2);
+    key.needs_binding = 0;
+    xdo_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2);
+
+    /* XXX: Flush here or at the end? or never? */
+    //XFlush(xdo->xdpy);
+  } /* walk string generating a keysequence */
+
+  //free(keys);
+  return XDO_SUCCESS;
+}
+
+int _xdo_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq,
+                        int pressed, int *modifier, useconds_t delay) {
+  int ret = 0;
+  charcodemap_t *keys = NULL;
+  int nkeys = 0;
+
+  if (_xdo_send_keysequence_window_to_keycode_list(xdo, keyseq, &keys, &nkeys) == False) {
+    fprintf(stderr, "Failure converting key sequence '%s' to keycodes\n", keyseq);
+    return 1;
+  }
+
+  ret = xdo_send_keysequence_window_list_do(xdo, window, keys, nkeys, pressed, modifier, delay);
+  free(keys);
+
+  return ret;
+}
+
+int xdo_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys, 
+                            int nkeys, int pressed, int *modifier, useconds_t delay) {
+  int i = 0;
+  int modstate = 0;
+  int keymapchanged = 0;
+
+  /* Find an unused keycode in case we need to bind unmapped keysyms */
+  KeySym *keysyms = NULL;
+  int keysyms_per_keycode = 0;
+  int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */
+  keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
+                                xdo->keycode_high - xdo->keycode_low,
+                                &keysyms_per_keycode);
+
+  /* Find a keycode that is unused for scratchspace */
+  for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) {
+    int j = 0;
+    int key_is_empty = 1;
+    for (j = 0; j < keysyms_per_keycode; j++) {
+      /*char *symname;*/
+      int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j;
+      /*symname = XKeysymToString(keysyms[symindex]);*/
+      if (keysyms[symindex] != 0) {
+        key_is_empty = 0;
+      } else {
+        break;
+      }
+    }
+    if (key_is_empty) {
+      scratch_keycode = i;
+      break;
+    }
+  }
+  XFree(keysyms);
+
+  /* Allow passing NULL for modifier in case we don't care about knowing
+   * the modifier map state after we finish */
+  if (modifier == NULL)
+    modifier = &modstate;
+
+  for (i = 0; i < nkeys; i++) {
+    if (keys[i].needs_binding == 1) {
+      KeySym keysym_list[] = { keys[i].symbol };
+      _xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode);
+      XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
+      XSync(xdo->xdpy, False);
+      /* override the code in our current key to use the scratch_keycode */
+      keys[i].code = scratch_keycode;
+      keymapchanged = 1;
+    }
+
+    //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n",
+            //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier);
+    _xdo_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay);
+
+    if (keys[i].needs_binding == 1) {
+      /* If we needed to make a new keymapping for this keystroke, we
+       * should sync with the server now, after the keypress, so that
+       * the next mapping or removal doesn't conflict. */
+      XSync(xdo->xdpy, False);
+    }
+
+    if (pressed) {
+      *modifier |= keys[i].modmask;
+    } else {
+      *modifier &= ~(keys[i].modmask);
+    }
+  }
+
+
+  if (keymapchanged) {
+    KeySym keysym_list[] = { 0 };
+    _xdo_debug(xdo, "Reverting scratch keycode (sym %lu to %d)",
+              keys[i].symbol, scratch_keycode);
+    XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
+  }
+
+  /* Necessary? */
+  XFlush(xdo->xdpy);
+  return XDO_SUCCESS;
+}
+
+  
+int xdo_send_keysequence_window_down(const xdo_t *xdo, Window window, const char *keyseq,
+                         useconds_t delay) {
+  return _xdo_send_keysequence_window_do(xdo, window, keyseq, True, NULL, delay);
+}
+
+int xdo_send_keysequence_window_up(const xdo_t *xdo, Window window, const char *keyseq,
+                       useconds_t delay) {
+  return _xdo_send_keysequence_window_do(xdo, window, keyseq, False, NULL, delay);
+}
+
+int xdo_send_keysequence_window(const xdo_t *xdo, Window window, const char *keyseq,
+                    useconds_t delay) {
+  int ret = 0;
+  int modifier = 0;
+  ret += _xdo_send_keysequence_window_do(xdo, window, keyseq, True, &modifier, delay / 2);
+  ret += _xdo_send_keysequence_window_do(xdo, window, keyseq, False, &modifier, delay / 2);
+  return ret;
+}
+
+/* Add by Lee Pumphret 2007-07-28
+ * Modified slightly by Jordan Sissel */
+int xdo_get_focused_window(const xdo_t *xdo, Window *window_ret) {
+  int ret = 0;
+  int unused_revert_ret;
+
+  ret = XGetInputFocus(xdo->xdpy, window_ret, &unused_revert_ret);
+
+  /* Xvfb with no window manager and given otherwise no input, with 
+   * a single client, will return the current focused window as '1'
+   * I think this is a bug, so let's alert the user. */
+  if (*window_ret == 1) {
+    fprintf(stderr, 
+            "XGetInputFocus returned the focused window of %ld. "
+            "This is likely a bug in the X server.\n", *window_ret);
+  }
+  return _is_success("XGetInputFocus", ret == 0, xdo);
+}
+
+int xdo_wait_for_window_focus(const xdo_t *xdo, Window window, int want_focus) {
+  Window focuswin = 0;
+  int ret;
+  int tries = MAX_TRIES;
+  ret = xdo_get_focused_window(xdo, &focuswin);
+  if (ret != 0) {
+    return ret;
+  }
+
+  while (tries > 0 && 
+         (want_focus ? focuswin != window : focuswin == window)) {
+    usleep(30000); /* TODO(sissel): Use exponential backoff up to 1 second */
+    ret = xdo_get_focused_window(xdo, &focuswin);
+    if (ret != 0) {
+      return ret;
+    }
+    tries--;
+  }
+  return 0;
+}
+
+/* Like xdo_get_focused_window, but return the first ancestor-or-self window
+ * having a property of WM_CLASS. This allows you to get the "real" or
+ * top-level-ish window having focus rather than something you may
+ * not expect to be the window having focused. */
+int xdo_get_focused_window_sane(const xdo_t *xdo, Window *window_ret) {
+  xdo_get_focused_window(xdo, window_ret);
+  xdo_find_window_client(xdo, *window_ret, window_ret, XDO_FIND_PARENTS);
+  return _is_success("xdo_get_focused_window_sane", *window_ret == 0, xdo);
+}
+
+int xdo_find_window_client(const xdo_t *xdo, Window window, Window *window_ret,
+                           int direction) {
+  /* for XQueryTree */
+  Window dummy, parent, *children = NULL;
+  unsigned int nchildren;
+  Atom atom_wmstate = XInternAtom(xdo->xdpy, "WM_STATE", False);
+
+  int done = False;
+  while (!done) {
+    if (window == 0) {
+      return XDO_ERROR;
+    }
+
+    long items;
+    _xdo_debug(xdo, "get_window_property on %lu", window);
+    xdo_get_window_property_by_atom(xdo, window, atom_wmstate, &items, NULL, NULL);
+
+    if (items == 0) {
+      /* This window doesn't have WM_STATE property, keep searching. */
+      _xdo_debug(xdo, "window %lu has no WM_STATE property, digging more.", window);
+      XQueryTree(xdo->xdpy, window, &dummy, &parent, &children, &nchildren);
+
+      if (direction == XDO_FIND_PARENTS) {
+        _xdo_debug(xdo, "searching parents");
+        /* Don't care about the children, but we still need to free them */
+        if (children != NULL)
+          XFree(children);
+        window = parent;
+      } else if (direction == XDO_FIND_CHILDREN) {
+        _xdo_debug(xdo, "searching %d children", nchildren);
+        unsigned int i = 0;
+        int ret;
+        done = True; /* recursion should end us */
+        for (i = 0; i < nchildren; i++) {
+          ret = xdo_find_window_client(xdo, children[i], &window, direction);
+          //fprintf(stderr, "findclient: %ld\n", window);
+          if (ret == XDO_SUCCESS) {
+            *window_ret = window;
+            break;
+          }
+        }
+        if (nchildren == 0) {
+          return XDO_ERROR;
+        }
+        if (children != NULL)
+          XFree(children);
+      } else {
+        fprintf(stderr, "Invalid find_client direction (%d)\n", direction);
+        *window_ret = 0;
+        if (children != NULL)
+          XFree(children);
+        return XDO_ERROR;
+      }
+    } else {
+      *window_ret = window;
+      done = True;
+    }
+  }
+  return XDO_SUCCESS;
+}
+
+/* Helper functions */
+static KeySym _xdo_keysym_from_char(const xdo_t *xdo, wchar_t key) {
+  int i = 0;
+  int len = xdo->charcodes_len;
+
+  //printf("Finding symbol for key '%c'\n", key);
+  for (i = 0; i < len; i++) {
+    //printf("  => %c vs %c (%d)\n",
+           //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key));
+    if (xdo->charcodes[i].key == key) {
+      //printf("  => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol);
+      return xdo->charcodes[i].symbol;
+    }
+  }
+
+  if (key >= 0x100) key += 0x01000000;
+  if (XKeysymToString(key)) return key;
+  return NoSymbol;
+}
+
+static void _xdo_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) {
+  KeySym keysym = _xdo_keysym_from_char(xdo, key->key);
+  _xdo_charcodemap_from_keysym(xdo, key, keysym);
+
+  /* If the character is an uppercase character within the Basic Latin or Latin-1 code block,
+   * then sending the capital character keycode will not work.
+   * We have to also send the shift modifier.
+   * There are only three ranges of capital letters to worry about */
+  if ((key->key >= 0x41 && key->key <= 0x5A) || (key->key >= 0xC0 && key->key <= 0xD6) || (key->key >= 0xD8 && key->key <= 0xDE)) {
+    key->modmask = ShiftMask;
+  }
+}
+
+static void _xdo_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) {
+  int i = 0;
+  int len = xdo->charcodes_len;
+
+  key->code = 0;
+  key->symbol = keysym;
+  key->group = 0;
+  key->modmask = 0;
+  key->needs_binding = 1;
+
+  for (i = 0; i < len; i++) {
+    if (xdo->charcodes[i].symbol == keysym) {
+      key->code = xdo->charcodes[i].code;
+      key->group = xdo->charcodes[i].group;
+      key->modmask = xdo->charcodes[i].modmask;
+      key->needs_binding = 0;
+      return;
+    }
+  }
+}
+
+static int _xdo_has_xtest(const xdo_t *xdo) {
+  int dummy;
+  return (XTestQueryExtension(xdo->xdpy, &dummy, &dummy, &dummy, &dummy) == True);
+}
+
+static void _xdo_populate_charcode_map(xdo_t *xdo) {
+  /* assert xdo->display is valid */
+  int keycodes_length = 0;
+  int idx = 0;
+  int keycode, group, groups, level, modmask, num_map;
+
+  XDisplayKeycodes(xdo->xdpy, &(xdo->keycode_low), &(xdo->keycode_high));
+  XModifierKeymap *modmap = XGetModifierMapping(xdo->xdpy);
+  KeySym *keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
+                                        xdo->keycode_high - xdo->keycode_low + 1,
+                                        &xdo->keysyms_per_keycode);
+  XFree(keysyms);
+
+  /* Add 2 to the size because the range [low, high] is inclusive */
+  /* Add 2 more for tab (\t) and newline (\n) */
+  keycodes_length = ((xdo->keycode_high - xdo->keycode_low) + 1)
+                     * xdo->keysyms_per_keycode;
+
+  xdo->charcodes = calloc(keycodes_length, sizeof(charcodemap_t));
+  XkbDescPtr desc = XkbGetMap(xdo->xdpy, XkbAllClientInfoMask, XkbUseCoreKbd);
+
+  for (keycode = xdo->keycode_low; keycode <= xdo->keycode_high; keycode++) {
+    groups = XkbKeyNumGroups(desc, keycode);
+    for (group = 0; group < groups; group++) {
+      XkbKeyTypePtr key_type = XkbKeyKeyType(desc, keycode, group);
+      for (level = 0; level < key_type->num_levels; level++) {
+        KeySym keysym = XkbKeycodeToKeysym(xdo->xdpy, keycode, group, level);
+        modmask = 0;
+
+        for (num_map = 0; num_map < key_type->map_count; num_map++) {
+          XkbKTMapEntryRec map = key_type->map[num_map];
+          if (map.active && map.level == level) {
+            modmask = map.mods.mask;
+            break;
+          }
+        }
+
+        xdo->charcodes[idx].key = _keysym_to_char(keysym);
+        xdo->charcodes[idx].code = keycode;
+        xdo->charcodes[idx].group = group;
+        xdo->charcodes[idx].modmask = modmask | _xdo_query_keycode_to_modifier(modmap, keycode);
+        xdo->charcodes[idx].symbol = keysym;
+
+        idx++;
+      }
+    }
+  }
+  xdo->charcodes_len = idx;
+  XkbFreeClientMap(desc, 0, 1);
+  XFreeModifiermap(modmap);
+}
+
+/* context-free functions */
+wchar_t _keysym_to_char(KeySym keysym) {
+  return (wchar_t)xkb_keysym_to_utf32(keysym);
+}
+
+int _xdo_send_keysequence_window_to_keycode_list(const xdo_t *xdo, const char *keyseq,
+                                     charcodemap_t **keys, int *nkeys) {
+  char *tokctx = NULL;
+  const char *tok = NULL;
+  char *keyseq_copy = NULL, *strptr = NULL;
+  int i = 0;
+
+  /* Array of keys to press, in order given by keyseq */
+  int keys_size = 10;
+
+  if (strcspn(keyseq, " \t\n.-[]{}\\|") != strlen(keyseq)) {
+    fprintf(stderr, "Error: Invalid key sequence '%s'\n", keyseq);
+    return False;
+  }
+
+  *nkeys = 0;
+  *keys = calloc(keys_size, sizeof(charcodemap_t));
+  keyseq_copy = strptr = strdup(keyseq);
+  while ((tok = strtok_r(strptr, "+", &tokctx)) != NULL) {
+    KeySym sym;
+    KeyCode key;
+
+    if (strptr != NULL)
+      strptr = NULL;
+
+    /* Check if 'tok' (string keysym) is an alias to another key */
+    /* symbol_map comes from xdo.util */
+    for (i = 0; symbol_map[i] != NULL; i+=2)
+      if (!strcasecmp(tok, symbol_map[i]))
+        tok = symbol_map[i + 1];
+
+    sym = XStringToKeysym(tok);
+    if (sym == NoSymbol) {
+      /* Accept a number as a explicit keycode */
+      if (isdigit(tok[0])) {
+        key = (unsigned int) atoi(tok);
+      } else {
+        fprintf(stderr, "(symbol) No such key name '%s'. Ignoring it.\n", tok);
+        continue;
+      }
+      (*keys)[*nkeys].code = key;
+      (*keys)[*nkeys].symbol = sym;
+      (*keys)[*nkeys].group = 0;
+      (*keys)[*nkeys].modmask = 0;
+      (*keys)[*nkeys].needs_binding = 0;
+      if (key == 0) {
+        //fprintf(stderr, "No such key '%s'. Ignoring it.\n", tok);
+        (*keys)[*nkeys].needs_binding = 1;
+      }
+    } else {
+      _xdo_charcodemap_from_keysym(xdo, &(*keys)[*nkeys], sym);
+    }
+
+    (*nkeys)++;
+    if (*nkeys == keys_size) {
+      keys_size *= 2;
+      *keys = realloc(*keys, keys_size * sizeof(KeyCode));
+    }
+  }
+
+  free(keyseq_copy);
+  return True;
+}
+
+int _is_success(const char *funcname, int code, const xdo_t *xdo) {
+  /* Nonzero is failure. */
+  if (code != 0 && !xdo->quiet)
+    fprintf(stderr, "%s failed (code=%d)\n", funcname, code);
+  return code;
+}
+
+int xdo_get_window_property(const xdo_t *xdo, Window window, const char *property,
+                            unsigned char **value, long *nitems, Atom *type, int *size) {
+    *value = xdo_get_window_property_by_atom(xdo, window, XInternAtom(xdo->xdpy, property, False), nitems, type, size);
+    if (*value == NULL) {
+        return XDO_ERROR;
+    }
+    return XDO_SUCCESS;
+}
+
+/* Arbitrary window property retrieval
+ * slightly modified version from xprop.c from Xorg */
+unsigned char *xdo_get_window_property_by_atom(const xdo_t *xdo, Window window, Atom atom,
+                                            long *nitems, Atom *type, int *size) {
+  Atom actual_type;
+  int actual_format;
+  unsigned long _nitems;
+  /*unsigned long nbytes;*/
+  unsigned long bytes_after; /* unused */
+  unsigned char *prop;
+  int status;
+
+  status = XGetWindowProperty(xdo->xdpy, window, atom, 0, (~0L),
+                              False, AnyPropertyType, &actual_type,
+                              &actual_format, &_nitems, &bytes_after,
+                              &prop);
+  if (status == BadWindow) {
+    fprintf(stderr, "window id # 0x%lx does not exists!", window);
+    return NULL;
+  } if (status != Success) {
+    fprintf(stderr, "XGetWindowProperty failed!");
+    return NULL;
+  }
+
+  /*
+   *if (actual_format == 32)
+   *  nbytes = sizeof(long);
+   *else if (actual_format == 16)
+   *  nbytes = sizeof(short);
+   *else if (actual_format == 8)
+   *  nbytes = 1;
+   *else if (actual_format == 0)
+   *  nbytes = 0;
+   */
+
+  if (nitems != NULL) {
+    *nitems = _nitems;
+  }
+
+  if (type != NULL) {
+    *type = actual_type;
+  }
+
+  if (size != NULL) {
+    *size = actual_format;
+  }
+  return prop;
+}
+
+int _xdo_ewmh_is_supported(const xdo_t *xdo, const char *feature) {
+  Atom type = 0;
+  long nitems = 0L;
+  int size = 0;
+  Atom *results = NULL;
+  long i = 0;
+
+  Window root;
+  Atom request;
+  Atom feature_atom;
+
+  request = XInternAtom(xdo->xdpy, "_NET_SUPPORTED", False);
+  feature_atom = XInternAtom(xdo->xdpy, feature, False);
+  root = XDefaultRootWindow(xdo->xdpy);
+
+  results = (Atom *) xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size);
+  for (i = 0L; i < nitems; i++) {
+    if (results[i] == feature_atom) {
+      free(results);
+      return True;
+    }
+  }
+  free(results);
+
+  return False;
+}
+
+void _xdo_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) {
+  xk->display = xdo->xdpy;
+  xk->subwindow = None;
+  xk->time = CurrentTime;
+  xk->same_screen = True;
+
+  /* Should we set these at all? */
+  xk->x = xk->y = xk->x_root = xk->y_root = 1;
+}
+
+void _xdo_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
+                          int modstate, int is_press, useconds_t delay) {
+  /* Properly ensure the modstate is set by finding a key
+   * that activates each bit in the modifier state */
+  int mask = modstate | key->modmask;
+  int use_xtest = 0;
+
+  if (window == CURRENTWINDOW) {
+    use_xtest = 1;
+  } else {
+    Window focuswin = 0;
+    xdo_get_focused_window(xdo, &focuswin);
+    if (focuswin == window) {
+      use_xtest = 1;
+    }
+  }
+  if (use_xtest) {
+    //printf("XTEST: Sending key %d %s\n", key->code, is_press ? "down" : "up");
+    XkbStateRec state;
+    XkbGetState(xdo->xdpy, XkbUseCoreKbd, &state);
+    int current_group = state.group;
+    XkbLockGroup(xdo->xdpy, XkbUseCoreKbd, key->group);
+    if (mask)
+      _xdo_send_modifier(xdo, mask, is_press);
+    //printf("XTEST: Sending key %d %s %x %d\n", key->code, is_press ? "down" : "up", key->modmask, key->group);
+    XTestFakeKeyEvent(xdo->xdpy, key->code, is_press, CurrentTime);
+    XkbLockGroup(xdo->xdpy, XkbUseCoreKbd, current_group);
+    XSync(xdo->xdpy, False);
+  } else {
+    /* Since key events have 'state' (shift, etc) in the event, we don't
+     * need to worry about key press ordering. */
+    XKeyEvent xk;
+    _xdo_init_xkeyevent(xdo, &xk);
+    xk.window = window;
+    xk.keycode = key->code;
+    xk.state = mask | (key->group << 13);
+    xk.type = (is_press ? KeyPress : KeyRelease);
+    XSendEvent(xdo->xdpy, xk.window, True, KeyPressMask, (XEvent *)&xk);
+  }
+
+  /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */
+  XFlush(xdo->xdpy);
+  if (delay > 0) {
+    usleep(delay);
+  }
+}
+
+int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode) {
+  int i = 0, j = 0;
+  int max = modmap->max_keypermod;
+
+  for (i = 0; i < 8; i++) { /* 8 modifier types, per XGetModifierMapping(3X) */
+    for (j = 0; j < max && modmap->modifiermap[(i * max) + j]; j++) {
+      if (keycode == modmap->modifiermap[(i * max) + j]) {
+        switch (i) {
+          case ShiftMapIndex: return ShiftMask; break;
+          case LockMapIndex: return LockMask; break;
+          case ControlMapIndex: return ControlMask; break;
+          case Mod1MapIndex: return Mod1Mask; break;
+          case Mod2MapIndex: return Mod2Mask; break;
+          case Mod3MapIndex: return Mod3Mask; break;
+          case Mod4MapIndex: return Mod4Mask; break;
+          case Mod5MapIndex: return Mod5Mask; break;
+        }
+      } /* end if */
+    } /* end loop j */
+  } /* end loop i */
+
+  /* No modifier found for this keycode, return no mask */
+  return 0;
+}
+
+void _xdo_send_modifier(const xdo_t *xdo, int modmask, int is_press) {
+  XModifierKeymap *modifiers = XGetModifierMapping(xdo->xdpy);
+  int mod_index, mod_key, keycode;
+
+  for (mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) {
+    if (modmask & (1 << mod_index)) {
+      for (mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) {
+        keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key];
+        if (keycode) {
+          XTestFakeKeyEvent(xdo->xdpy, keycode, is_press, CurrentTime);
+          XSync(xdo->xdpy, False);
+          break;
+        }
+      }
+    }
+  }
+
+  XFreeModifiermap(modifiers);
+}
+
+int xdo_get_active_modifiers(const xdo_t *xdo, charcodemap_t **keys,
+                                    int *nkeys) {
+  /* For each keyboard device, if an active key is a modifier,
+   * then add the keycode to the keycode list */
+
+  char keymap[32]; /* keycode map: 256 bits */
+  int keys_size = 10;
+  int keycode = 0;
+  int mod_index, mod_key;
+  XModifierKeymap *modifiers = XGetModifierMapping(xdo->xdpy);
+  *nkeys = 0;
+  *keys = malloc(keys_size * sizeof(charcodemap_t));
+
+  XQueryKeymap(xdo->xdpy, keymap);
+
+  for (mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) {
+    for (mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) {
+      keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key];
+      if (keycode && keymap[(keycode / 8)] & (1 << (keycode % 8))) {
+        /* This keycode is active and is a modifier, record it. */
+
+        /* Zero the charcodemap_t entry before using it.
+         * Fixes a bug reported by Hong-Leong Ong - where
+         * 'xdotool key --clearmodifiers ...' sometimes failed trying
+         * to clear modifiers that didn't exist since charcodemap_t's modmask was
+         * uninitialized */
+        memset(*keys + *nkeys, 0, sizeof(charcodemap_t));
+
+        (*keys)[*nkeys].code = keycode;
+        (*nkeys)++;
+
+        if (*nkeys == keys_size) {
+          keys_size *= 2;
+          *keys = realloc(keys, keys_size * sizeof(charcodemap_t));
+        }
+      }
+    }
+  } 
+
+  XFreeModifiermap(modifiers);
+
+  return XDO_SUCCESS;
+}
+
+unsigned int xdo_get_input_state(const xdo_t *xdo) {
+  Window root, dummy;
+  int root_x, root_y, win_x, win_y;
+  unsigned int mask;
+  root = DefaultRootWindow(xdo->xdpy);
+
+  XQueryPointer(xdo->xdpy, root, &dummy, &dummy,
+                &root_x, &root_y, &win_x, &win_y, &mask);
+
+  return mask;
+}
+
+const char **xdo_get_symbol_map(void) {
+  return symbol_map;
+}
+
+int xdo_clear_active_modifiers(const xdo_t *xdo, Window window, charcodemap_t *active_mods, int active_mods_n) {
+  int ret = 0;
+  unsigned int input_state = xdo_get_input_state(xdo);
+  xdo_send_keysequence_window_list_do(xdo, window, active_mods,
+                          active_mods_n, False, NULL, DEFAULT_DELAY);
+
+  if (input_state & Button1MotionMask)
+    ret = xdo_mouse_up(xdo, window, 1);
+  if (!ret && input_state & Button2MotionMask)
+    ret = xdo_mouse_up(xdo, window, 2);
+  if (!ret && input_state & Button3MotionMask)
+    ret = xdo_mouse_up(xdo, window, 3);
+  if (!ret && input_state & Button4MotionMask)
+    ret = xdo_mouse_up(xdo, window, 4);
+  if (!ret && input_state & Button5MotionMask)
+    ret = xdo_mouse_up(xdo, window, 5);
+  if (!ret && input_state & LockMask) {
+    /* explicitly use down+up here since xdo_send_keysequence_window alone will track the modifiers
+     * incurred by a key (like shift, or caps) and send them on the 'up' sequence.
+     * That seems to break things with Caps_Lock only, so let's be explicit here. */
+    ret = xdo_send_keysequence_window_down(xdo, window, "Caps_Lock", DEFAULT_DELAY);
+    ret += xdo_send_keysequence_window_up(xdo, window, "Caps_Lock", DEFAULT_DELAY);
+  }
+
+  XSync(xdo->xdpy, False);
+  return ret;
+}
+
+int xdo_set_active_modifiers(const xdo_t *xdo, Window window, charcodemap_t *active_mods, int active_mods_n) {
+  int ret = 0;
+  unsigned int input_state = xdo_get_input_state(xdo);
+  xdo_send_keysequence_window_list_do(xdo, window, active_mods,
+                          active_mods_n, True, NULL, DEFAULT_DELAY);
+  if (input_state & Button1MotionMask)
+    ret = xdo_mouse_down(xdo, window, 1);
+  if (!ret && input_state & Button2MotionMask)
+    ret = xdo_mouse_down(xdo, window, 2);
+  if (!ret && input_state & Button3MotionMask)
+    ret = xdo_mouse_down(xdo, window, 3);
+  if (!ret && input_state & Button4MotionMask)
+    ret = xdo_mouse_down(xdo, window, 4);
+  if (!ret && input_state & Button5MotionMask)
+    ret = xdo_mouse_down(xdo, window, 5);
+  if (!ret && input_state & LockMask) {
+    /* explicitly use down+up here since xdo_send_keysequence_window alone will track the modifiers
+     * incurred by a key (like shift, or caps) and send them on the 'up' sequence.
+     * That seems to break things with Caps_Lock only, so let's be explicit here. */
+    ret = xdo_send_keysequence_window_down(xdo, window, "Caps_Lock", DEFAULT_DELAY);
+    ret += xdo_send_keysequence_window_up(xdo, window, "Caps_Lock", DEFAULT_DELAY);
+  }
+
+  XSync(xdo->xdpy, False);
+  return ret;
+}
+
+int xdo_get_pid_window(const xdo_t *xdo, Window window) {
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  int window_pid = 0;
+
+  if (atom_NET_WM_PID == (Atom)-1) {
+    atom_NET_WM_PID = XInternAtom(xdo->xdpy, "_NET_WM_PID", False);
+  }
+
+  data = xdo_get_window_property_by_atom(xdo, window, atom_NET_WM_PID, &nitems, &type, &size);
+
+  if (nitems > 0) {
+    /* The data itself is unsigned long, but everyone uses int as pid values */
+    window_pid = (int) *((unsigned long *)data);
+  }
+  free(data);
+
+  return window_pid;
+}
+
+int xdo_wait_for_mouse_move_from(const xdo_t *xdo, int origin_x, int origin_y) {
+  int x, y;
+  int ret = 0;
+  int tries = MAX_TRIES;
+
+  ret = xdo_get_mouse_location(xdo, &x, &y, NULL);
+  while (tries > 0 && 
+         (x == origin_x && y == origin_y)) {
+    usleep(30000);
+    ret = xdo_get_mouse_location(xdo, &x, &y, NULL);
+    tries--;
+  }
+
+  return ret;
+}
+
+int xdo_wait_for_mouse_move_to(const xdo_t *xdo, int dest_x, int dest_y) {
+  int x, y;
+  int ret = 0;
+  int tries = MAX_TRIES;
+
+  ret = xdo_get_mouse_location(xdo, &x, &y, NULL);
+  while (tries > 0 && (x != dest_x && y != dest_y)) {
+    usleep(30000);
+    ret = xdo_get_mouse_location(xdo, &x, &y, NULL);
+    tries--;
+  }
+
+  return ret;
+}
+
+int xdo_get_desktop_viewport(const xdo_t *xdo, int *x_ret, int *y_ret) {
+  if (_xdo_ewmh_is_supported(xdo, "_NET_DESKTOP_VIEWPORT") == False) {
+    fprintf(stderr,
+            "Your windowmanager claims not to support _NET_DESKTOP_VIEWPORT, "
+            "so I cannot tell you the viewport position.\n");
+    return XDO_ERROR;
+  }
+
+  Atom type;
+  int size;
+  long nitems;
+  unsigned char *data;
+  Atom request = XInternAtom(xdo->xdpy, "_NET_DESKTOP_VIEWPORT", False);
+  Window root = RootWindow(xdo->xdpy, 0);
+  data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size);
+
+  if (type != XA_CARDINAL) {
+    fprintf(stderr, 
+            "Got unexpected type returned from _NET_DESKTOP_VIEWPORT."
+            " Expected CARDINAL, got %s\n",
+            XGetAtomName(xdo->xdpy, type));
+    free(data);
+    return XDO_ERROR;
+  }
+
+  if (nitems != 2) {
+    fprintf(stderr, "Expected 2 items for _NET_DESKTOP_VIEWPORT, got %ld\n",
+            nitems);
+    free(data);
+    return XDO_ERROR;
+  }
+
+  int *viewport_data = (int *)data;
+  *x_ret = viewport_data[0];
+  *y_ret = viewport_data[1];
+  free(data);
+
+  return XDO_SUCCESS;
+}
+
+int xdo_set_desktop_viewport(const xdo_t *xdo, int x, int y) {
+  XEvent xev;
+  int ret;
+  Window root = RootWindow(xdo->xdpy, 0);
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = root;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_DESKTOP_VIEWPORT",
+                                         False);
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = x;
+  xev.xclient.data.l[1] = y;
+
+  ret = XSendEvent(xdo->xdpy, root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask, &xev);
+
+  /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise.
+   * Manpage says it will only generate BadWindow or BadValue errors */
+  return _is_success("XSendEvent[EWMH:_NET_DESKTOP_VIEWPORT]", ret == 0, xdo);
+}
+
+int xdo_kill_window(const xdo_t *xdo, Window window) {
+  int ret;
+  ret = XKillClient(xdo->xdpy, window);
+  return _is_success("XKillClient", ret == 0, xdo);
+}
+
+int xdo_close_window(const xdo_t *xdo, Window window) {
+  int ret;
+  ret = XDestroyWindow(xdo->xdpy, window);
+  return _is_success("XDestroyWindow", ret == 0, xdo);
+}
+
+int xdo_quit_window(const xdo_t *xdo, Window window) {
+  XEvent xev;
+  int ret;
+  Window root = RootWindow(xdo->xdpy, 0);
+
+  memset(&xev, 0, sizeof(xev));
+  xev.type = ClientMessage;
+  xev.xclient.serial = 0;
+  xev.xclient.send_event = True;
+  xev.xclient.display = xdo->xdpy;
+  xev.xclient.window = window;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_CLOSE_WINDOW", False);
+  xev.xclient.format = 32;
+
+  ret = XSendEvent(xdo->xdpy, root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask,
+                   &xev);
+
+  /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise.
+   * Manpage says it will only generate BadWindow or BadValue errors */
+  return _is_success("XSendEvent[_NET_CLOSE_WINDOW]", ret == 0, xdo);
+}
+
+int xdo_get_window_name(const xdo_t *xdo, Window window, 
+                        unsigned char **name_ret, int *name_len_ret,
+                        int *name_type) {
+  if (atom_NET_WM_NAME == (Atom)-1) {
+    atom_NET_WM_NAME = XInternAtom(xdo->xdpy, "_NET_WM_NAME", False);
+  } 
+  if (atom_WM_NAME == (Atom)-1) {
+    atom_WM_NAME = XInternAtom(xdo->xdpy, "WM_NAME", False);
+  }
+  if (atom_STRING == (Atom)-1) {
+    atom_STRING = XInternAtom(xdo->xdpy, "STRING", False);
+  }
+  if (atom_UTF8_STRING == (Atom)-1) {
+    atom_UTF8_STRING = XInternAtom(xdo->xdpy, "UTF8_STRING", False);
+  }
+
+  Atom type;
+  int size;
+  long nitems;
+
+  /**
+   * http://standards.freedesktop.org/wm-spec/1.3/ar01s05.html
+   * Prefer _NET_WM_NAME if available, otherwise use WM_NAME
+   * If no WM_NAME, set name_ret to NULL and set len to 0
+   */
+
+  *name_ret = xdo_get_window_property_by_atom(xdo, window, atom_NET_WM_NAME, &nitems,
+                             &type, &size);
+  if (nitems == 0) {
+    *name_ret = xdo_get_window_property_by_atom(xdo, window, atom_WM_NAME, &nitems,
+                               &type, &size);
+  }
+  *name_len_ret = nitems;
+  *name_type = type;
+
+  return 0;
+}
+
+int xdo_get_window_classname(const xdo_t *xdo, Window window, unsigned char **class_ret) {
+  XClassHint classhint;
+  Status ret = XGetClassHint(xdo->xdpy, window, &classhint);
+
+  if (ret) {
+    XFree(classhint.res_name);
+    *class_ret = (unsigned char*) classhint.res_class;
+  } else {
+    *class_ret = NULL;
+  }
+  return _is_success("XGetClassHint[WM_CLASS]", ret == 0, xdo);
+}
+
+int xdo_window_state(xdo_t *xdo, Window window, unsigned long action, const char *property) {
+  int ret;
+  XEvent xev;
+  Window root = RootWindow(xdo->xdpy, 0);
+
+  memset(&xev, 0, sizeof(xev));
+  xev.xclient.type = ClientMessage;
+  xev.xclient.serial = 0;
+  xev.xclient.send_event = True;
+  xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_WM_STATE", False);
+  xev.xclient.window = window;
+  xev.xclient.format = 32;
+  xev.xclient.data.l[0] = action;
+  xev.xclient.data.l[1] = XInternAtom(xdo->xdpy, property, False);
+
+  ret = XSendEvent(xdo->xdpy, root, False,
+                   SubstructureNotifyMask | SubstructureRedirectMask, &xev);
+  return _is_success("XSendEvent[EWMH:_NET_WM_STATE]", ret == 0, xdo);
+}
+
+int xdo_minimize_window(const xdo_t *xdo, Window window) {
+  int ret;
+  int screen;
+
+  /* Get screen number */
+  XWindowAttributes attr;
+  XGetWindowAttributes(xdo->xdpy, window, &attr);
+  screen = XScreenNumberOfScreen(attr.screen);
+
+  /* Minimize it */
+  ret = XIconifyWindow(xdo->xdpy, window, screen);
+  return _is_success("XIconifyWindow", ret == 0, xdo);
+}
+
+void _xdo_debug(const xdo_t *xdo, const char *format, ...) {
+  va_list args;
+
+  va_start(args, format);
+  if (xdo->debug) {
+    vfprintf(stderr, format, args);
+    fprintf(stderr, "\n");
+  }
+} /* _xdo_debug */
+
+/* Used for printing things conditionally based on xdo->quiet */
+void _xdo_eprintf(const xdo_t *xdo, int hushable, const char *format, ...) {
+  va_list args;
+
+  va_start(args, format);
+  if (xdo->quiet == True && hushable) {
+    return;
+  }
+
+  vfprintf(stderr, format, args);
+  fprintf(stderr, "\n");
+} /* _xdo_eprintf */
+
+void xdo_enable_feature(xdo_t *xdo, int feature) {
+  xdo->features_mask |= (0 << feature);
+}
+
+void xdo_disable_feature(xdo_t *xdo, int feature) {
+  xdo->features_mask &= ~(1 << feature);
+}
+
+int xdo_has_feature(xdo_t *xdo, int feature) {
+  return (xdo->features_mask & (1 << feature));
+}
+
+// Espanso-specific variants
+void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) {
+    xk->display = xdo->xdpy;
+    xk->subwindow = None;
+    xk->time = CurrentTime;
+    xk->same_screen = True;
+
+    /* Should we set these at all? */
+    xk->x = xk->y = xk->x_root = xk->y_root = 1;
+}
+
+void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
+                   int modstate, int is_press, useconds_t delay) {
+    /* Properly ensure the modstate is set by finding a key
+     * that activates each bit in the modifier state */
+    int mask = modstate | key->modmask;
+
+    /* Since key events have 'state' (shift, etc) in the event, we don't
+         * need to worry about key press ordering. */
+    XKeyEvent xk;
+    fast_init_xkeyevent(xdo, &xk);
+    xk.window = window;
+    xk.keycode = key->code;
+    xk.state = mask | (key->group << 13);
+    xk.type = (is_press ? KeyPress : KeyRelease);
+    XSendEvent(xdo->xdpy, xk.window, True, 0, (XEvent *)&xk);
+
+    /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */
+    XFlush(xdo->xdpy);
+    if (delay > 0) {
+        usleep(delay);
+    }
+}
+
+int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys,
+                                        int nkeys, int pressed, int *modifier, useconds_t delay) {
+    int i = 0;
+    int modstate = 0;
+    int keymapchanged = 0;
+
+    /* Find an unused keycode in case we need to bind unmapped keysyms */
+    KeySym *keysyms = NULL;
+    int keysyms_per_keycode = 0;
+    int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */
+    keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
+                                  xdo->keycode_high - xdo->keycode_low,
+                                  &keysyms_per_keycode);
+
+    /* Find a keycode that is unused for scratchspace */
+    for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) {
+        int j = 0;
+        int key_is_empty = 1;
+        for (j = 0; j < keysyms_per_keycode; j++) {
+            /*char *symname;*/
+            int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j;
+            /*symname = XKeysymToString(keysyms[symindex]);*/
+            if (keysyms[symindex] != 0) {
+                key_is_empty = 0;
+            } else {
+                break;
+            }
+        }
+        if (key_is_empty) {
+            scratch_keycode = i;
+            break;
+        }
+    }
+    XFree(keysyms);
+
+    /* Allow passing NULL for modifier in case we don't care about knowing
+     * the modifier map state after we finish */
+    if (modifier == NULL)
+        modifier = &modstate;
+
+    for (i = 0; i < nkeys; i++) {
+        if (keys[i].needs_binding == 1) {
+            KeySym keysym_list[] = { keys[i].symbol };
+            //_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode);
+            XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
+            XSync(xdo->xdpy, False);
+            /* override the code in our current key to use the scratch_keycode */
+            keys[i].code = scratch_keycode;
+            keymapchanged = 1;
+        }
+
+        //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n",
+        //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier);
+        fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay);
+
+        if (keys[i].needs_binding == 1) {
+            /* If we needed to make a new keymapping for this keystroke, we
+             * should sync with the server now, after the keypress, so that
+             * the next mapping or removal doesn't conflict. */
+            XSync(xdo->xdpy, False);
+        }
+
+        if (pressed) {
+            *modifier |= keys[i].modmask;
+        } else {
+            *modifier &= ~(keys[i].modmask);
+        }
+    }
+
+
+    if (keymapchanged) {
+        KeySym keysym_list[] = { 0 };
+        //printf(xdo, "Reverting scratch keycode (sym %lu to %d)",
+        //           keys[i].symbol, scratch_keycode);
+        XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
+    }
+
+    /* Necessary? */
+    XFlush(xdo->xdpy);
+    return XDO_SUCCESS;
+}
+
+KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) {
+    int i = 0;
+    int len = xdo->charcodes_len;
+
+    //printf("Finding symbol for key '%c'\n", key);
+    for (i = 0; i < len; i++) {
+        //printf("  => %c vs %c (%d)\n",
+        //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key));
+        if (xdo->charcodes[i].key == key) {
+            //printf("  => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol);
+            return xdo->charcodes[i].symbol;
+        }
+    }
+
+    if (key >= 0x100) key += 0x01000000;
+    if (XKeysymToString(key)) return key;
+    return NoSymbol;
+}
+
+void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) {
+    int i = 0;
+    int len = xdo->charcodes_len;
+
+    key->code = 0;
+    key->symbol = keysym;
+    key->group = 0;
+    key->modmask = 0;
+    key->needs_binding = 1;
+
+    for (i = 0; i < len; i++) {
+        if (xdo->charcodes[i].symbol == keysym) {
+            key->code = xdo->charcodes[i].code;
+            key->group = xdo->charcodes[i].group;
+            key->modmask = xdo->charcodes[i].modmask;
+            key->needs_binding = 0;
+            return;
+        }
+    }
+}
+
+void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) {
+    KeySym keysym = fast_keysym_from_char(xdo, key->key);
+    fast_charcodemap_from_keysym(xdo, key, keysym);
+}
+
+/* XXX: Return proper code if errors found */
+int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) {
+
+    /* Since we're doing down/up, the delay should be based on the number
+     * of keys pressed (including shift). Since up/down is two calls,
+     * divide by two. */
+    delay /= 2;
+
+    /* XXX: Add error handling */
+    //int nkeys = strlen(string);
+    //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t));
+    charcodemap_t key;
+    //int modifier = 0;
+    setlocale(LC_CTYPE,"");
+    mbstate_t ps = { 0 };
+    ssize_t len;
+    while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) {
+        if (len == -1) {
+            fprintf(stderr, "Invalid multi-byte sequence encountered\n");
+            return XDO_ERROR;
+        }
+        fast_charcodemap_from_char(xdo, &key);
+        if (key.code == 0 && key.symbol == NoSymbol) {
+            fprintf(stderr, "I don't what key produces '%lc', skipping.\n",
+                    key.key);
+            continue;
+        } else {
+            //printf("Found key for %c\n", key.key);
+            //printf("code: %d\n", key.code);
+            //printf("sym: %s\n", XKeysymToString(key.symbol));
+        }
+
+        //printf(stderr,
+        //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n",
+        //key.key, key.code, key.symbol, key.group, key.modmask,
+        //(key.needs_binding == 1) ? "needs binding" : "ok");
+
+        //_xdo_send_key(xdo, window, keycode, modstate, True, delay);
+        //_xdo_send_key(xdo, window, keycode, modstate, False, delay);
+        fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2);
+        key.needs_binding = 0;
+        fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2);
+
+        XFlush(xdo->xdpy);
+    } /* walk string generating a keysequence */
+
+    //free(keys);
+    return XDO_SUCCESS;
+}
+
+void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) {
+    XKeyEvent xk;
+    xk.display = xdo->xdpy;
+    xk.window = window;
+    xk.root = XDefaultRootWindow(xdo->xdpy);
+    xk.subwindow = None;
+    xk.time = CurrentTime;
+    xk.x = 1;
+    xk.y = 1;
+    xk.x_root = 1;
+    xk.y_root = 1;
+    xk.same_screen = True;
+    xk.keycode = keycode;
+    xk.state = 0;
+    xk.type = (pressed ? KeyPress : KeyRelease);
+
+    XEvent event;
+    event.xkey =xk;
+
+    XSendEvent(xdo->xdpy, window, True, 0, &event);
+}
+
+int fast_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq,
+                        int pressed, int *modifier, useconds_t delay) {
+  int ret = 0;
+  charcodemap_t *keys = NULL;
+  int nkeys = 0;
+
+  if (_xdo_send_keysequence_window_to_keycode_list(xdo, keyseq, &keys, &nkeys) == False) {
+    fprintf(stderr, "Failure converting key sequence '%s' to keycodes\n", keyseq);
+    return 1;
+  }
+
+  ret = fast_send_keysequence_window_list_do(xdo, window, keys, nkeys, pressed, modifier, delay);
+  free(keys);
+
+  return ret;
+}
+
+int fast_send_keysequence_window(const xdo_t *xdo, Window window, const char *keyseq,
+                    useconds_t delay) {
+  int ret = 0;
+  int modifier = 0;
+  ret += fast_send_keysequence_window_do(xdo, window, keyseq, True, &modifier, delay / 2);
+  ret += fast_send_keysequence_window_do(xdo, window, keyseq, False, &modifier, delay / 2);
+  return ret;
+}
\ No newline at end of file
diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo.h b/espanso-inject/src/x11/xdotool/vendor/xdo.h
new file mode 100644
index 00000000..2290f636
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/vendor/xdo.h
@@ -0,0 +1,938 @@
+/**
+ * @file xdo.h
+ */
+#ifndef _XDO_H_
+#define _XDO_H_
+
+#ifndef __USE_XOPEN
+#define __USE_XOPEN
+#endif /* __USE_XOPEN */
+
+#include <sys/types.h>
+#include <X11/Xlib.h>
+#include <X11/X.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @mainpage
+ *
+ * libxdo helps you send fake mouse and keyboard input, search for windows,
+ * perform various window management tasks such as desktop changes, window
+ * movement, etc.
+ *
+ * For examples on libxdo usage, the xdotool source code is a good reference.
+ *
+ * @see xdo.h
+ * @see xdo_new
+ */
+
+/**
+ * When issuing a window size change, giving this flag will make the size
+ * change be relative to the size hints of the window.  For terminals, this
+ * generally means that the window size will be relative to the font size,
+ * allowing you to change window sizes based on character rows and columns
+ * instead of pixels.
+ */
+#define SIZE_USEHINTS (1L << 0)
+#define SIZE_USEHINTS_X (1L << 1)
+#define SIZE_USEHINTS_Y (1L << 2)
+
+/**
+ * CURRENTWINDOW is a special identify for xdo input faking (mouse and
+ * keyboard) functions like xdo_send_keysequence_window that indicate we should target the
+ * current window, not a specific window.
+ *
+ * Generally, this means we will use XTEST instead of XSendEvent when sending
+ * events.
+ */
+#define CURRENTWINDOW (0)
+
+/**
+ * @internal
+ * Map character to whatever information we need to be able to send
+ * this key (keycode, modifiers, group, etc)
+ */
+typedef struct charcodemap {
+  wchar_t key; /** the letter for this key, like 'a' */
+  KeyCode code; /** the keycode that this key is on */
+  KeySym symbol; /** the symbol representing this key */
+  int group; /** the keyboard group that has this key in it */
+  int modmask; /** the modifiers to apply when sending this key */
+   /** if this key need to be bound at runtime because it does not
+    * exist in the current keymap, this will be set to 1. */
+  int needs_binding;
+} charcodemap_t;
+
+typedef enum {
+  XDO_FEATURE_XTEST, /** Is XTest available? */
+} XDO_FEATURES;
+
+/**
+ * The main context.
+ */
+typedef struct xdo {
+
+  /** The Display for Xlib */
+  Display *xdpy;
+
+  /** The display name, if any. NULL if not specified. */
+  char *display_name;
+
+  /** @internal Array of known keys/characters */
+  charcodemap_t *charcodes;
+
+  /** @internal Length of charcodes array */
+  int charcodes_len;
+
+  /** @internal highest keycode value */
+  int keycode_high; /* highest and lowest keycodes */
+
+  /** @internal lowest keycode value */
+  int keycode_low;  /* used by this X server */
+  
+  /** @internal number of keysyms per keycode */
+  int keysyms_per_keycode;
+
+  /** Should we close the display when calling xdo_free? */
+  int close_display_when_freed;
+
+  /** Be extra quiet? (omits some error/message output) */
+  int quiet;
+
+  /** Enable debug output? */
+  int debug;
+
+  /** Feature flags, such as XDO_FEATURE_XTEST, etc... */
+  int features_mask;
+
+} xdo_t;
+
+
+/**
+ * Search only window title. DEPRECATED - Use SEARCH_NAME
+ * @see xdo_search_windows
+ */
+#define SEARCH_TITLE (1UL << 0)
+
+/**
+ * Search only window class.
+ * @see xdo_search_windows
+ */
+#define SEARCH_CLASS (1UL << 1)
+
+/**
+ * Search only window name.
+ * @see xdo_search_windows
+ */
+#define SEARCH_NAME (1UL << 2)
+
+/**
+ * Search only window pid.
+ * @see xdo_search_windows
+ */
+#define SEARCH_PID  (1UL << 3)
+
+/**
+ * Search only visible windows.
+ * @see xdo_search_windows
+ */
+#define SEARCH_ONLYVISIBLE  (1UL << 4)
+
+/**
+ * Search only a specific screen. 
+ * @see xdo_search.screen
+ * @see xdo_search_windows
+ */
+#define SEARCH_SCREEN  (1UL << 5)
+
+/**
+ * Search only window class name.
+ * @see xdo_search
+ */
+#define SEARCH_CLASSNAME (1UL << 6)
+
+/**
+ * Search a specific desktop
+ * @see xdo_search.screen
+ * @see xdo_search_windows
+ */
+#define SEARCH_DESKTOP (1UL << 7)
+
+/**
+ * Search only window role.
+ * @see xdo_search
+ */
+#define SEARCH_ROLE (1UL << 8)
+
+/**
+ * The window search query structure.
+ *
+ * @see xdo_search_windows
+ */
+typedef struct xdo_search {
+  const char *title;        /** pattern to test against a window title */
+  const char *winclass;     /** pattern to test against a window class */
+  const char *winclassname; /** pattern to test against a window class */
+  const char *winname;      /** pattern to test against a window name */
+  const char *winrole;      /** pattern to test against a window role */
+  int pid;            /** window pid (From window atom _NET_WM_PID) */
+  long max_depth;     /** depth of search. 1 means only toplevel windows */
+  int only_visible;   /** boolean; set true to search only visible windows */
+  int screen;         /** what screen to search, if any. If none given, search 
+                         all screens */
+
+  /** Should the tests be 'and' or 'or' ? If 'and', any failure will skip the
+   * window. If 'or', any success will keep the window in search results. */
+  enum { SEARCH_ANY, SEARCH_ALL } require;
+  
+  /** bitmask of things you are searching for, such as SEARCH_NAME, etc.
+   * @see SEARCH_NAME, SEARCH_CLASS, SEARCH_PID, SEARCH_CLASSNAME, etc
+   */
+  unsigned int searchmask; 
+
+  /** What desktop to search, if any. If none given, search all screens. */
+  long desktop;
+
+  /** How many results to return? If 0, return all. */
+  unsigned int limit;
+} xdo_search_t;
+
+#define XDO_ERROR 1
+#define XDO_SUCCESS 0
+
+/**
+ * Create a new xdo_t instance.
+ *
+ * @param display the string display name, such as ":0". If null, uses the
+ * environment variable DISPLAY just like XOpenDisplay(NULL).
+ *
+ * @return Pointer to a new xdo_t or NULL on failure
+ */
+xdo_t* xdo_new(const char *display);
+
+/**
+ * Create a new xdo_t instance with an existing X11 Display instance.
+ *
+ * @param xdpy the Display pointer given by a previous XOpenDisplay()
+ * @param display the string display name
+ * @param close_display_when_freed If true, we will close the display when
+ * xdo_free is called. Otherwise, we leave it open.
+ */
+xdo_t* xdo_new_with_opened_display(Display *xdpy, const char *display,
+                                   int close_display_when_freed);
+
+/**
+ * Return a string representing the version of this library
+ */
+const char *xdo_version(void);
+
+/**
+ * Free and destroy an xdo_t instance.
+ *
+ * If close_display_when_freed is set, then we will also close the Display.
+ */
+void xdo_free(xdo_t *xdo);
+
+/**
+ * Move the mouse to a specific location.
+ *
+ * @param x the target X coordinate on the screen in pixels.
+ * @param y the target Y coordinate on the screen in pixels.
+ * @param screen the screen (number) you want to move on.
+ */
+int xdo_move_mouse(const xdo_t *xdo, int x, int y, int screen);
+
+/**
+ * Move the mouse to a specific location relative to the top-left corner
+ * of a window.
+ *
+ * @param x the target X coordinate on the screen in pixels.
+ * @param y the target Y coordinate on the screen in pixels.
+ */
+int xdo_move_mouse_relative_to_window(const xdo_t *xdo, Window window, int x, int y);
+
+/**
+ * Move the mouse relative to it's current position.
+ *
+ * @param x the distance in pixels to move on the X axis.
+ * @param y the distance in pixels to move on the Y axis.
+ */
+int xdo_move_mouse_relative(const xdo_t *xdo, int x, int y);
+
+/**
+ * Send a mouse press (aka mouse down) for a given button at the current mouse
+ * location.
+ *
+ * @param window The window you want to send the event to or CURRENTWINDOW
+ * @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
+ *    right, 4 is wheel up, 5 is wheel down.
+ */
+int xdo_mouse_down(const xdo_t *xdo, Window window, int button);
+
+/**
+ * Send a mouse release (aka mouse up) for a given button at the current mouse
+ * location.
+ *
+ * @param window The window you want to send the event to or CURRENTWINDOW
+ * @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
+ *    right, 4 is wheel up, 5 is wheel down.
+ */
+int xdo_mouse_up(const xdo_t *xdo, Window window, int button);
+
+/**
+ * Get the current mouse location (coordinates and screen number).
+ *
+ * @param x integer pointer where the X coordinate will be stored
+ * @param y integer pointer where the Y coordinate will be stored
+ * @param screen_num integer pointer where the screen number will be stored
+ */
+int xdo_get_mouse_location(const xdo_t *xdo, int *x, int *y, int *screen_num);
+
+/**
+ * Get the window the mouse is currently over
+ *
+ * @param window_ret Window pointer where the window will be stored.
+ */
+int xdo_get_window_at_mouse(const xdo_t *xdo, Window *window_ret);
+
+/**
+ * Get all mouse location-related data.
+ *
+ * If null is passed for any parameter, we simply do not store it.
+ * Useful if you only want the 'y' coordinate, for example.
+ *
+ * @param x integer pointer where the X coordinate will be stored
+ * @param y integer pointer where the Y coordinate will be stored
+ * @param screen_num integer pointer where the screen number will be stored
+ * @param window Window pointer where the window/client the mouse is over
+ *   will be stored.
+ */
+int xdo_get_mouse_location2(const xdo_t *xdo, int *x_ret, int *y_ret,
+                            int *screen_num_ret, Window *window_ret);
+
+/**
+ * Wait for the mouse to move from a location. This function will block
+ * until the condition has been satisfied.
+ *
+ * @param origin_x the X position you expect the mouse to move from
+ * @param origin_y the Y position you expect the mouse to move from
+ */
+int xdo_wait_for_mouse_move_from(const xdo_t *xdo, int origin_x, int origin_y);
+
+/**
+ * Wait for the mouse to move to a location. This function will block
+ * until the condition has been satisfied.
+ *
+ * @param dest_x the X position you expect the mouse to move to
+ * @param dest_y the Y position you expect the mouse to move to
+ */
+int xdo_wait_for_mouse_move_to(const xdo_t *xdo, int dest_x, int dest_y);
+
+/**
+ * Send a click for a specific mouse button at the current mouse location.
+ *
+ * @param window The window you want to send the event to or CURRENTWINDOW
+ * @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
+ *    right, 4 is wheel up, 5 is wheel down.
+ */
+int xdo_click_window(const xdo_t *xdo, Window window, int button);
+
+/**
+ * Send a one or more clicks for a specific mouse button at the current mouse
+ * location.
+ *
+ * @param window The window you want to send the event to or CURRENTWINDOW
+ * @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
+ *    right, 4 is wheel up, 5 is wheel down.
+ */
+int xdo_click_window_multiple(const xdo_t *xdo, Window window, int button,
+                       int repeat, useconds_t delay);
+
+/**
+ * Type a string to the specified window.
+ *
+ * If you want to send a specific key or key sequence, such as "alt+l", you
+ * want instead xdo_send_keysequence_window(...).
+ *
+ * @param window The window you want to send keystrokes to or CURRENTWINDOW
+ * @param string The string to type, like "Hello world!"
+ * @param delay The delay between keystrokes in microseconds. 12000 is a decent
+ *    choice if you don't have other plans.
+ */
+int xdo_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
+
+/**
+ * Send a keysequence to the specified window.
+ *
+ * This allows you to send keysequences by symbol name. Any combination
+ * of X11 KeySym names separated by '+' are valid. Single KeySym names
+ * are valid, too.
+ *
+ * Examples:
+ *   "l"
+ *   "semicolon"
+ *   "alt+Return"
+ *   "Alt_L+Tab"
+ *
+ * If you want to type a string, such as "Hello world." you want to instead
+ * use xdo_enter_text_window.
+ *
+ * @param window The window you want to send the keysequence to or
+ *   CURRENTWINDOW
+ * @param keysequence The string keysequence to send.
+ * @param delay The delay between keystrokes in microseconds.
+ */
+int xdo_send_keysequence_window(const xdo_t *xdo, Window window,
+                    const char *keysequence, useconds_t delay);
+
+/**
+ * Send key release (up) events for the given key sequence.
+ *
+ * @see xdo_send_keysequence_window
+ */
+int xdo_send_keysequence_window_up(const xdo_t *xdo, Window window,
+                       const char *keysequence, useconds_t delay);
+
+/**
+ * Send key press (down) events for the given key sequence.
+ *
+ * @see xdo_send_keysequence_window
+ */
+int xdo_send_keysequence_window_down(const xdo_t *xdo, Window window,
+                         const char *keysequence, useconds_t delay);
+                         
+/**
+ * Send a series of keystrokes.
+ *
+ * @param window The window to send events to or CURRENTWINDOW
+ * @param keys The array of charcodemap_t entities to send.
+ * @param nkeys The length of the keys parameter
+ * @param pressed 1 for key press, 0 for key release.
+ * @param modifier Pointer to integer to record the modifiers activated by
+ *   the keys being pressed. If NULL, we don't save the modifiers.
+ * @param delay The delay between keystrokes in microseconds.
+ */
+int xdo_send_keysequence_window_list_do(const xdo_t *xdo, Window window,
+                            charcodemap_t *keys, int nkeys,
+                            int pressed, int *modifier, useconds_t delay);
+
+
+/**
+ * Wait for a window to have a specific map state.
+ *
+ * State possibilities:
+ *   IsUnmapped - window is not displayed.
+ *   IsViewable - window is mapped and shown (though may be clipped by windows
+ *     on top of it)
+ *   IsUnviewable - window is mapped but a parent window is unmapped.
+ *
+ * @param wid the window you want to wait for.
+ * @param map_state the state to wait for.
+ */
+int xdo_wait_for_window_map_state(const xdo_t *xdo, Window wid, int map_state);
+
+#define SIZE_TO 0
+#define SIZE_FROM 1
+int xdo_wait_for_window_size(const xdo_t *xdo, Window window, unsigned int width,
+                             unsigned int height, int flags, int to_or_from);
+
+
+/**
+ * Move a window to a specific location.
+ *
+ * The top left corner of the window will be moved to the x,y coordinate.
+ *
+ * @param wid the window to move
+ * @param x the X coordinate to move to.
+ * @param y the Y coordinate to move to.
+ */
+int xdo_move_window(const xdo_t *xdo, Window wid, int x, int y);
+
+/**
+ * Apply a window's sizing hints (if any) to a given width and height.
+ *
+ * This function wraps XGetWMNormalHints() and applies any 
+ * resize increment and base size to your given width and height values.
+ *
+ * @param window the window to use
+ * @param width the unit width you want to translate
+ * @param height the unit height you want to translate
+ * @param width_ret the return location of the translated width
+ * @param height_ret the return location of the translated height
+ */
+int xdo_translate_window_with_sizehint(const xdo_t *xdo, Window window,
+                                       unsigned int width, unsigned int height,
+                                       unsigned int *width_ret, unsigned int *height_ret);
+
+/**
+ * Change the window size.
+ *
+ * @param wid the window to resize
+ * @param w the new desired width
+ * @param h the new desired height
+ * @param flags if 0, use pixels for units. If SIZE_USEHINTS, then
+ *   the units will be relative to the window size hints.
+ */
+int xdo_set_window_size(const xdo_t *xdo, Window wid, int w, int h, int flags);
+
+/**
+ * Change a window property.
+ *
+ * Example properties you can change are WM_NAME, WM_ICON_NAME, etc.
+ *
+ * @param wid The window to change a property of.
+ * @param property the string name of the property.
+ * @param value the string value of the property.
+ */
+int xdo_set_window_property(const xdo_t *xdo, Window wid, const char *property,
+                        const char *value);
+
+/**
+ * Change the window's classname and or class.
+ *
+ * @param name The new class name. If NULL, no change.
+ * @param _class The new class. If NULL, no change.
+ */
+int xdo_set_window_class(const xdo_t *xdo, Window wid, const char *name,
+                        const char *_class);
+
+/**
+ * Sets the urgency hint for a window.
+ */
+int xdo_set_window_urgency (const xdo_t *xdo, Window wid, int urgency);
+
+/**
+ * Set the override_redirect value for a window. This generally means
+ * whether or not a window manager will manage this window.
+ *
+ * If you set it to 1, the window manager will usually not draw borders on the
+ * window, etc. If you set it to 0, the window manager will see it like a
+ * normal application window.
+ *
+ */
+int xdo_set_window_override_redirect(const xdo_t *xdo, Window wid,
+                                     int override_redirect);
+
+/**
+ * Focus a window.
+ *
+ * @see xdo_activate_window
+ * @param wid the window to focus.
+ */
+int xdo_focus_window(const xdo_t *xdo, Window wid);
+
+/**
+ * Raise a window to the top of the window stack. This is also sometimes
+ * termed as bringing the window forward.
+ *
+ * @param wid The window to raise.
+ */
+int xdo_raise_window(const xdo_t *xdo, Window wid);
+
+/**
+ * Get the window currently having focus.
+ *
+ * @param window_ret Pointer to a window where the currently-focused window
+ *   will be stored.
+ */
+int xdo_get_focused_window(const xdo_t *xdo, Window *window_ret);
+
+/**
+ * Wait for a window to have or lose focus.
+ *
+ * @param window The window to wait on
+ * @param want_focus If 1, wait for focus. If 0, wait for loss of focus.
+ */
+int xdo_wait_for_window_focus(const xdo_t *xdo, Window window, int want_focus);
+
+/**
+ * Get the PID owning a window. Not all applications support this.
+ * It looks at the _NET_WM_PID property of the window.
+ *
+ * @param window the window to query.
+ * @return the process id or 0 if no pid found.
+ */
+int xdo_get_pid_window(const xdo_t *xdo, Window window);
+
+/**
+ * Like xdo_get_focused_window, but return the first ancestor-or-self window *
+ * having a property of WM_CLASS. This allows you to get the "real" or
+ * top-level-ish window having focus rather than something you may not expect
+ * to be the window having focused.
+ *
+ * @param window_ret Pointer to a window where the currently-focused window
+ *   will be stored.
+ */
+int xdo_get_focused_window_sane(const xdo_t *xdo, Window *window_ret);
+
+/**
+ * Activate a window. This is generally a better choice than xdo_focus_window
+ * for a variety of reasons, but it requires window manager support:
+ *   - If the window is on another desktop, that desktop is switched to.
+ *   - It moves the window forward rather than simply focusing it
+ *
+ * Requires your window manager to support this.
+ * Uses _NET_ACTIVE_WINDOW from the EWMH spec.
+ *
+ * @param wid the window to activate
+ */
+int xdo_activate_window(const xdo_t *xdo, Window wid);
+
+/**
+ * Wait for a window to be active or not active.
+ *
+ * Requires your window manager to support this.
+ * Uses _NET_ACTIVE_WINDOW from the EWMH spec.
+ *
+ * @param window the window to wait on
+ * @param active If 1, wait for active. If 0, wait for inactive.
+ */
+int xdo_wait_for_window_active(const xdo_t *xdo, Window window, int active);
+
+/**
+ * Map a window. This mostly means to make the window visible if it is
+ * not currently mapped.
+ *
+ * @param wid the window to map.
+ */
+int xdo_map_window(const xdo_t *xdo, Window wid);
+
+/**
+ * Unmap a window
+ *
+ * @param wid the window to unmap
+ */
+int xdo_unmap_window(const xdo_t *xdo, Window wid);
+
+/**
+ * Minimize a window.
+ */
+int xdo_minimize_window(const xdo_t *xdo, Window wid);
+
+#define _NET_WM_STATE_REMOVE        0    /* remove/unset property */
+#define _NET_WM_STATE_ADD           1    /* add/set property */
+#define _NET_WM_STATE_TOGGLE        2    /* toggle property  */
+
+/**
+ * Get window classname
+ * @param window the window
+ * @param class_ret Pointer to the window classname WM_CLASS
+ */
+int xdo_get_window_classname(const xdo_t *xdo, Window window, unsigned char **class_ret);
+
+/**
+ * Change window state
+ * @param action the _NET_WM_STATE action
+ */
+int xdo_window_state(xdo_t *xdo, Window window, unsigned long action, const char *property);
+
+/** 
+ * Reparents a window
+ *
+ * @param wid_source the window to reparent
+ * @param wid_target the new parent window
+ */
+int xdo_reparent_window(const xdo_t *xdo, Window wid_source, Window wid_target);
+
+/**
+ * Get a window's location.
+ *
+ * @param wid the window to query
+ * @param x_ret pointer to int where the X location is stored. If NULL, X is
+ *   ignored.
+ * @param y_ret pointer to int where the Y location is stored. If NULL, X is
+ *   ignored.
+ * @param screen_ret Pointer to Screen* where the Screen* the window on is
+ *   stored. If NULL, this parameter is ignored.
+ */
+int xdo_get_window_location(const xdo_t *xdo, Window wid,
+                            int *x_ret, int *y_ret, Screen **screen_ret);
+
+/**
+ * Get a window's size.
+ *
+ * @param wid the window to query
+ * @param width_ret pointer to unsigned int where the width is stored.
+ * @param height_ret pointer to unsigned int where the height is stored.
+ */
+int xdo_get_window_size(const xdo_t *xdo, Window wid, unsigned int *width_ret,
+                        unsigned int *height_ret);
+
+/* pager-like behaviors */
+
+/**
+ * Get the currently-active window.
+ * Requires your window manager to support this.
+ * Uses _NET_ACTIVE_WINDOW from the EWMH spec.
+ *
+ * @param window_ret Pointer to Window where the active window is stored.
+ */
+int xdo_get_active_window(const xdo_t *xdo, Window *window_ret);
+
+/**
+ * Get a window ID by clicking on it. This function blocks until a selection
+ * is made.
+ *
+ * @param window_ret Pointer to Window where the selected window is stored.
+ */
+int xdo_select_window_with_click(const xdo_t *xdo, Window *window_ret);
+
+/**
+ * Set the number of desktops.
+ * Uses _NET_NUMBER_OF_DESKTOPS of the EWMH spec.
+ *
+ * @param ndesktops the new number of desktops to set.
+ */
+int xdo_set_number_of_desktops(const xdo_t *xdo, long ndesktops);
+
+/**
+ * Get the current number of desktops.
+ * Uses _NET_NUMBER_OF_DESKTOPS of the EWMH spec.
+ *
+ * @param ndesktops pointer to long where the current number of desktops is
+ *   stored
+ */
+int xdo_get_number_of_desktops(const xdo_t *xdo, long *ndesktops);
+
+/**
+ * Switch to another desktop.
+ * Uses _NET_CURRENT_DESKTOP of the EWMH spec.
+ *
+ * @param desktop The desktop number to switch to.
+ */
+int xdo_set_current_desktop(const xdo_t *xdo, long desktop);
+
+/**
+ * Get the current desktop.
+ * Uses _NET_CURRENT_DESKTOP of the EWMH spec.
+ *
+ * @param desktop pointer to long where the current desktop number is stored.
+ */
+int xdo_get_current_desktop(const xdo_t *xdo, long *desktop);
+
+/**
+ * Move a window to another desktop
+ * Uses _NET_WM_DESKTOP of the EWMH spec.
+ *
+ * @param wid the window to move
+ * @param desktop the desktop destination for the window
+ */
+int xdo_set_desktop_for_window(const xdo_t *xdo, Window wid, long desktop);
+
+/**
+ * Get the desktop a window is on.
+ * Uses _NET_WM_DESKTOP of the EWMH spec.
+ *
+ * If your desktop does not support _NET_WM_DESKTOP, then '*desktop' remains
+ * unmodified.
+ *
+ * @param wid the window to query
+ * @param deskto pointer to long where the desktop of the window is stored
+ */
+int xdo_get_desktop_for_window(const xdo_t *xdo, Window wid, long *desktop);
+
+/**
+ * Search for windows.
+ *
+ * @param search the search query.
+ * @param windowlist_ret the list of matching windows to return
+ * @param nwindows_ret the number of windows (length of windowlist_ret)
+ * @see xdo_search_t
+ */
+int xdo_search_windows(const xdo_t *xdo, const xdo_search_t *search,
+                      Window **windowlist_ret, unsigned int *nwindows_ret);
+
+/**
+ * Generic property fetch.
+ *
+ * @param window the window to query
+ * @param atom the Atom to request
+ * @param nitems the number of items 
+ * @param type the type of the return
+ * @param size the size of the type
+ * @return data consisting of 'nitems' items of size 'size' and type 'type'
+ *   will need to be cast to the type before using.
+ */
+unsigned char *xdo_get_window_property_by_atom(const xdo_t *xdo, Window window, Atom atom,
+                              long *nitems, Atom *type, int *size);
+
+/**
+ * Get property of window by name of atom.
+ *
+ * @param window the window to query
+ * @param property the name of the atom
+ * @param nitems the number of items 
+ * @param type the type of the return
+ * @param size the size of the type
+ * @return data consisting of 'nitems' items of size 'size' and type 'type'
+ *   will need to be cast to the type before using.
+ */
+int xdo_get_window_property(const xdo_t *xdo, Window window, const char *property,
+                            unsigned char **value, long *nitems, Atom *type, int *size);
+
+/**
+ * Get the current input state. This is a mask value containing any of the
+ * following: ShiftMask, LockMask, ControlMask, Mod1Mask, Mod2Mask, Mod3Mask,
+ * Mod4Mask, or Mod5Mask.
+ *
+ * @return the input mask
+ */
+unsigned int xdo_get_input_state(const xdo_t *xdo);
+
+/**
+ * If you need the symbol map, use this method.
+ *
+ * The symbol map is an array of string pairs mapping common tokens to X Keysym
+ * strings, such as "alt" to "Alt_L"
+ *
+ * @returns array of strings.
+ */
+const char **xdo_get_symbol_map(void);
+
+/* active modifiers stuff */
+
+/**
+ * Get a list of active keys. Uses XQueryKeymap.
+ *
+ * @param keys Pointer to the array of charcodemap_t that will be allocated
+ *    by this function.
+ * @param nkeys Pointer to integer where the number of keys will be stored.
+ */
+int xdo_get_active_modifiers(const xdo_t *xdo, charcodemap_t **keys,
+                             int *nkeys);
+
+/**
+ * Send any events necessary to clear the active modifiers.
+ * For example, if you are holding 'alt' when xdo_get_active_modifiers is 
+ * called, then this method will send a key-up for 'alt'
+ */
+int xdo_clear_active_modifiers(const xdo_t *xdo, Window window,
+                               charcodemap_t *active_mods,
+                               int active_mods_n);
+
+/**
+ * Send any events necessary to make these modifiers active.
+ * This is useful if you just cleared the active modifiers and then wish
+ * to restore them after.
+ */
+int xdo_set_active_modifiers(const xdo_t *xdo, Window window,
+                             charcodemap_t *active_mods,
+                             int active_mods_n);
+
+/**
+ * Get the position of the current viewport.
+ *
+ * This is only relevant if your window manager supports
+ * _NET_DESKTOP_VIEWPORT 
+ */
+int xdo_get_desktop_viewport(const xdo_t *xdo, int *x_ret, int *y_ret);
+
+/**
+ * Set the position of the current viewport.
+ *
+ * This is only relevant if your window manager supports
+ * _NET_DESKTOP_VIEWPORT
+ */
+int xdo_set_desktop_viewport(const xdo_t *xdo, int x, int y);
+
+/**
+ * Kill a window and the client owning it.
+ *
+ */
+int xdo_kill_window(const xdo_t *xdo, Window window);
+
+/**
+ * Close a window without trying to kill the client.
+ *
+ */
+int xdo_close_window(const xdo_t *xdo, Window window);
+
+/**
+ * Request that a window close, gracefully.
+ *
+ */
+int xdo_quit_window(const xdo_t *xdo, Window window);
+
+/**
+ * Find a client window that is a parent of the window given
+ */
+#define XDO_FIND_PARENTS (0)
+
+/**
+ * Find a client window that is a child of the window given
+ */
+#define XDO_FIND_CHILDREN (1)
+
+/**
+ * Find a client window (child) in a given window. Useful if you get the
+ * window manager's decorator window rather than the client window.
+ */
+int xdo_find_window_client(const xdo_t *xdo, Window window, Window *window_ret,
+                           int direction);
+
+/**
+ * Get a window's name, if any.
+ *
+ * @param window window to get the name of.
+ * @param name_ret character pointer pointer where the address of the window name will be stored.
+ * @param name_len_ret integer pointer where the length of the window name will be stored.
+ * @param name_type integer pointer where the type (atom) of the window name will be stored.
+ */
+int xdo_get_window_name(const xdo_t *xdo, Window window, 
+                        unsigned char **name_ret, int *name_len_ret,
+                        int *name_type);
+
+/**
+ * Disable an xdo feature.
+ *
+ * This function is mainly used by libxdo itself, however, you may find it useful
+ * in your own applications.
+ * 
+ * @see XDO_FEATURES
+ */
+void xdo_disable_feature(xdo_t *xdo, int feature);
+
+/**
+ * Enable an xdo feature.
+ *
+ * This function is mainly used by libxdo itself, however, you may find it useful
+ * in your own applications.
+ * 
+ * @see XDO_FEATURES
+ */
+void xdo_enable_feature(xdo_t *xdo, int feature);
+
+/**
+ * Check if a feature is enabled.
+ *
+ * This function is mainly used by libxdo itself, however, you may find it useful
+ * in your own applications.
+ * 
+ * @see XDO_FEATURES
+ */
+int xdo_has_feature(xdo_t *xdo, int feature);
+
+// Espanso-specific variants
+
+KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key);
+void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key);
+void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym);
+void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk);
+void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
+                          int modstate, int is_press, useconds_t delay);
+int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
+void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed);
+int fast_send_keysequence_window(const xdo_t *xdo, Window window,
+                    const char *keysequence, useconds_t delay);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* ifndef _XDO_H_ */
+
diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo_util.h b/espanso-inject/src/x11/xdotool/vendor/xdo_util.h
new file mode 100644
index 00000000..3cfe956e
--- /dev/null
+++ b/espanso-inject/src/x11/xdotool/vendor/xdo_util.h
@@ -0,0 +1,24 @@
+/* xdo utility pieces 
+ *
+ * $Id$
+ */
+
+#ifndef _XDO_UTIL_H_
+#define _XDO_UTIL_H_
+
+#include "xdo.h"
+
+/* human to Keysym string mapping */
+static const char *symbol_map[] = {
+  "alt", "Alt_L",
+  "ctrl", "Control_L",
+  "control", "Control_L",
+  "meta", "Meta_L",
+  "super", "Super_L",
+  "shift", "Shift_L",
+  "enter", "Return",
+  "return", "Return",
+  NULL, NULL,
+};
+
+#endif /* ifndef _XDO_UTIL_H_ */
diff --git a/espanso-migrate/src/convert.rs b/espanso-migrate/src/convert.rs
index 4e9b4c5a..16edc7e0 100644
--- a/espanso-migrate/src/convert.rs
+++ b/espanso-migrate/src/convert.rs
@@ -335,7 +335,7 @@ fn map_field_if_present(
 }
 
 // This is needed to convert the old form's {{control}} syntax to the new [[control]] one.
-fn apply_form_syntax_patch(matches: &mut Vec<Yaml>) {
+fn apply_form_syntax_patch(matches: &mut [Yaml]) {
   matches.iter_mut().for_each(|m| {
     if let Yaml::Hash(fields) = m {
       if let Some(Yaml::String(form_option)) = fields.get_mut(&Yaml::String("form".to_string())) {
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
index 5c5570d2..b8b889a1 100644
--- a/espanso-modulo/build.rs
+++ b/espanso-modulo/build.rs
@@ -25,6 +25,9 @@ use std::path::Path;
 #[cfg(not(target_os = "linux"))]
 const WX_WIDGETS_ARCHIVE_NAME: &str = "wxWidgets-3.1.5.zip";
 
+#[cfg(not(target_os = "linux"))]
+const WX_WIDGETS_BUILD_OUT_DIR_ENV_NAME: &str = "WX_WIDGETS_BUILD_OUT_DIR";
+
 #[cfg(target_os = "windows")]
 fn build_native() {
   use std::process::Command;
@@ -36,7 +39,17 @@ fn build_native() {
     panic!("could not find wxWidgets archive!");
   }
 
-  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
+  let out_dir = if let Ok(out_path) = std::env::var(WX_WIDGETS_BUILD_OUT_DIR_ENV_NAME) {
+    println!(
+      "detected wxWidgets build output directory override: {}",
+      out_path
+    );
+    let path = PathBuf::from(out_path);
+    std::fs::create_dir_all(&path).expect("unable to create wxWidgets out dir");
+    path
+  } else {
+    PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"))
+  };
   let out_wx_dir = out_dir.join("wx");
 
   if !out_wx_dir.is_dir() {
@@ -154,8 +167,19 @@ fn build_native() {
     panic!("could not find wxWidgets archive!");
   }
 
-  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
+  let out_dir = if let Ok(out_path) = std::env::var(WX_WIDGETS_BUILD_OUT_DIR_ENV_NAME) {
+    println!(
+      "detected wxWidgets build output directory override: {}",
+      out_path
+    );
+    let path = PathBuf::from(out_path);
+    std::fs::create_dir_all(&path).expect("unable to create wxWidgets out dir");
+    path
+  } else {
+    PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"))
+  };
   let out_wx_dir = out_dir.join("wx");
+  println!("wxWidgets will be compiled into: {}", out_wx_dir.display());
 
   let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
     .expect("unable to read target arch")
diff --git a/espanso-package/src/manifest.rs b/espanso-package/src/manifest.rs
index 172a3a1b..b6ff60fd 100644
--- a/espanso-package/src/manifest.rs
+++ b/espanso-package/src/manifest.rs
@@ -19,7 +19,7 @@
 
 use std::path::Path;
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use serde::{Deserialize, Serialize};
 
 #[derive(Debug, PartialEq, Serialize, Deserialize)]
@@ -34,7 +34,13 @@ pub struct Manifest {
 impl Manifest {
   pub fn parse(manifest_path: &Path) -> Result<Self> {
     let manifest_str = std::fs::read_to_string(manifest_path)?;
-    Ok(serde_yaml::from_str(&manifest_str)?)
+
+    serde_yaml::from_str(&manifest_str).with_context(|| {
+      format!(
+        "Failed manifest parsing for path: {}",
+        manifest_path.display()
+      )
+    })
   }
 }
 
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index f628756e..3a8c2708 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "2.1.5-beta"
+version = "2.1.6-beta"
 authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
 license = "GPL-3.0"
 description = "Cross-platform Text Expander written in Rust"
@@ -56,6 +56,7 @@ colored = "2.0.0"
 tempdir = "0.3.7"
 notify = "4.0.17"
 opener = "0.5.0"
+sysinfo = "0.24.5"
 
 [target.'cfg(windows)'.dependencies]
 named_pipe = "0.4.1"
diff --git a/espanso/src/cli/migrate.rs b/espanso/src/cli/migrate.rs
index 22ddb8ff..a5a5cc69 100644
--- a/espanso/src/cli/migrate.rs
+++ b/espanso/src/cli/migrate.rs
@@ -177,9 +177,8 @@ fn find_available_backup_dir() -> PathBuf {
       "".to_string()
     };
 
-    let target_backup_dir = dirs::document_dir()
-      .expect("unable to generate backup directory")
-      .join(format!("espanso-migrate-backup{}", num));
+    let target_backup_dir =
+      find_backup_dir_location().join(format!("espanso-migrate-backup{}", num));
 
     if !target_backup_dir.is_dir() {
       return target_backup_dir;
@@ -189,6 +188,22 @@ fn find_available_backup_dir() -> PathBuf {
   panic!("could not generate valid backup directory");
 }
 
+fn find_backup_dir_location() -> PathBuf {
+  if let Some(documents_dir) = dirs::document_dir() {
+    return documents_dir;
+  }
+
+  if let Some(home_dir) = dirs::home_dir() {
+    return home_dir;
+  }
+
+  if let Ok(current_dir) = std::env::current_dir() {
+    return current_dir;
+  }
+
+  panic!("unable to generate suitable backup location directory");
+}
+
 fn error_print_and_log(msg: &str) {
   error!("{}", msg);
   eprintln!("{}", msg);
diff --git a/espanso/src/cli/service/linux.rs b/espanso/src/cli/service/linux.rs
index 55ce07e1..53ec82e0 100644
--- a/espanso/src/cli/service/linux.rs
+++ b/espanso/src/cli/service/linux.rs
@@ -29,6 +29,7 @@ use crate::{error_eprintln, info_println, warn_eprintln};
 
 const LINUX_SERVICE_NAME: &str = "espanso";
 const LINUX_SERVICE_CONTENT: &str = include_str!("../../res/linux/systemd.service");
+#[allow(clippy::transmute_bytes_to_str)]
 const LINUX_SERVICE_FILENAME: &str = formatcp!("{}.service", LINUX_SERVICE_NAME);
 
 pub fn register() -> Result<()> {
diff --git a/espanso/src/cli/service/mod.rs b/espanso/src/cli/service/mod.rs
index b9dc7fc0..fe6a9214 100644
--- a/espanso/src/cli/service/mod.rs
+++ b/espanso/src/cli/service/mod.rs
@@ -152,6 +152,14 @@ fn start_main(paths: &Paths, _paths_overrides: &PathsOverrides, args: &ArgMatche
   }
 
   error_eprintln!("unable to start service: timed out");
+
+  error_eprintln!(
+    "Hint: sometimes this happens because another Espanso process is left running for some reason."
+  );
+  error_eprintln!(
+    "      Please try running 'espanso restart' or manually killing all Espanso processes, then try again."
+  );
+
   SERVICE_TIMED_OUT
 }
 
diff --git a/espanso/src/cli/service/stop.rs b/espanso/src/cli/service/stop.rs
index 4a39efa3..9024416c 100644
--- a/espanso/src/cli/service/stop.rs
+++ b/espanso/src/cli/service/stop.rs
@@ -19,14 +19,18 @@
 
 use anyhow::{Error, Result};
 use log::error;
+use std::process::Command;
 use std::{path::Path, time::Instant};
+use sysinfo::{PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt};
 use thiserror::Error;
 
 use espanso_ipc::IPCClient;
 
+use crate::info_println;
 use crate::{
   ipc::{create_ipc_client_to_worker, IPCEvent},
   lock::acquire_worker_lock,
+  warn_eprintln,
 };
 
 pub fn terminate_worker(runtime_dir: &Path) -> Result<()> {
@@ -46,14 +50,18 @@ pub fn terminate_worker(runtime_dir: &Path) -> Result<()> {
     }
   }
 
-  let now = Instant::now();
-  while now.elapsed() < std::time::Duration::from_secs(3) {
-    let lock_file = acquire_worker_lock(runtime_dir);
-    if lock_file.is_some() {
-      return Ok(());
-    }
+  if wait_for_worker_to_be_stopped(runtime_dir) {
+    return Ok(());
+  }
 
-    std::thread::sleep(std::time::Duration::from_millis(200));
+  warn_eprintln!(
+    "unable to gracefully terminate espanso (timed-out), trying to force the termination..."
+  );
+
+  forcefully_terminate_espanso();
+
+  if wait_for_worker_to_be_stopped(runtime_dir) {
+    return Ok(());
   }
 
   Err(StopError::WorkerTimedOut.into())
@@ -67,3 +75,47 @@ pub enum StopError {
   #[error("ipc error: `{0}`")]
   IPCError(Error),
 }
+
+fn wait_for_worker_to_be_stopped(runtime_dir: &Path) -> bool {
+  let now = Instant::now();
+  while now.elapsed() < std::time::Duration::from_secs(3) {
+    let lock_file = acquire_worker_lock(runtime_dir);
+    if lock_file.is_some() {
+      return true;
+    }
+
+    std::thread::sleep(std::time::Duration::from_millis(200));
+  }
+
+  false
+}
+
+fn forcefully_terminate_espanso() {
+  let mut sys =
+    System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new()));
+  sys.refresh_processes_specifics(ProcessRefreshKind::new());
+
+  let target_process_names = if cfg!(target_os = "windows") {
+    vec!["espanso.exe", "espansod.exe"]
+  } else {
+    vec!["espanso"]
+  };
+
+  let current_pid = std::process::id();
+
+  // We want to terminate all Espanso processes except this one
+  for (pid, process) in sys.processes() {
+    if target_process_names.contains(&process.name()) && pid.as_u32() != current_pid {
+      let str_pid = pid.as_u32().to_string();
+      info_println!("killing espanso process with PID: {}", str_pid);
+
+      if cfg!(target_os = "windows") {
+        let _ = Command::new("taskkill")
+          .args(&["/pid", &str_pid, "/f"])
+          .output();
+      } else {
+        let _ = Command::new("kill").args(&["-9", &str_pid]).output();
+      }
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs
index 7371ffc3..8960173d 100644
--- a/espanso/src/cli/worker/config.rs
+++ b/espanso/src/cli/worker/config.rs
@@ -143,6 +143,7 @@ impl<'a> super::engine::dispatch::executor::clipboard_injector::ClipboardParamsP
       restore_clipboard: active.preserve_clipboard(),
       restore_clipboard_delay: active.restore_clipboard_delay(),
       x11_use_xclip_backend: active.x11_use_xclip_backend(),
+      x11_use_xdotool_backend: active.x11_use_xdotool_backend(),
     }
   }
 }
@@ -164,6 +165,7 @@ impl<'a> super::engine::dispatch::executor::InjectParamsProvider for ConfigManag
       inject_delay: active.inject_delay(),
       key_delay: active.key_delay(),
       evdev_modifier_delay: active.evdev_modifier_delay(),
+      x11_use_xdotool_backend: active.x11_use_xdotool_backend(),
     }
   }
 }
@@ -209,3 +211,9 @@ impl<'a> crate::gui::modulo::search::ModuloSearchUIOptionProvider for ConfigMana
     self.active().post_search_delay()
   }
 }
+
+impl<'a> espanso_engine::process::AltCodeSynthEnabledProvider for ConfigManager<'a> {
+  fn is_alt_code_synthesizer_enabled(&self) -> bool {
+    self.active().emulate_alt_codes()
+  }
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
index bf7ac6c8..f170b47e 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
@@ -40,6 +40,7 @@ pub struct ClipboardParams {
   pub restore_clipboard: bool,
   pub restore_clipboard_delay: usize,
   pub x11_use_xclip_backend: bool,
+  pub x11_use_xdotool_backend: bool,
 }
 
 pub struct ClipboardInjectorAdapter<'a> {
@@ -95,6 +96,7 @@ impl<'a> ClipboardInjectorAdapter<'a> {
       InjectionOptions {
         delay: params.paste_shortcut_event_delay as i32,
         disable_fast_inject: params.disable_x11_fast_inject,
+        x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
         ..Default::default()
       },
     )?;
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs
index 66b70084..5741b30e 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs
@@ -67,6 +67,7 @@ impl<'a> TextInjector for EventInjectorAdapter<'a> {
         })
         .try_into()
         .unwrap(),
+      x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
     };
 
     // We don't use the lines() method because it skips emtpy lines, which is not what we want.
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs
index 38f19a54..669d0307 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs
@@ -59,6 +59,7 @@ impl<'a> KeyInjector for KeyInjectorAdapter<'a> {
         })
         .try_into()
         .unwrap(),
+      x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
     };
 
     let converted_keys: Vec<_> = keys.iter().map(convert_to_inject_key).collect();
@@ -107,6 +108,16 @@ fn convert_to_inject_key(key: &espanso_engine::event::input::Key) -> espanso_inj
     espanso_engine::event::input::Key::F18 => espanso_inject::keys::Key::F18,
     espanso_engine::event::input::Key::F19 => espanso_inject::keys::Key::F19,
     espanso_engine::event::input::Key::F20 => espanso_inject::keys::Key::F20,
+    espanso_engine::event::input::Key::Numpad0 => espanso_inject::keys::Key::Numpad0,
+    espanso_engine::event::input::Key::Numpad1 => espanso_inject::keys::Key::Numpad1,
+    espanso_engine::event::input::Key::Numpad2 => espanso_inject::keys::Key::Numpad2,
+    espanso_engine::event::input::Key::Numpad3 => espanso_inject::keys::Key::Numpad3,
+    espanso_engine::event::input::Key::Numpad4 => espanso_inject::keys::Key::Numpad4,
+    espanso_engine::event::input::Key::Numpad5 => espanso_inject::keys::Key::Numpad5,
+    espanso_engine::event::input::Key::Numpad6 => espanso_inject::keys::Key::Numpad6,
+    espanso_engine::event::input::Key::Numpad7 => espanso_inject::keys::Key::Numpad7,
+    espanso_engine::event::input::Key::Numpad8 => espanso_inject::keys::Key::Numpad8,
+    espanso_engine::event::input::Key::Numpad9 => espanso_inject::keys::Key::Numpad9,
     espanso_engine::event::input::Key::Other(raw) => espanso_inject::keys::Key::Raw(*raw),
   }
 }
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
index ff3dc19f..49d518d5 100644
--- a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
+++ b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
@@ -34,4 +34,5 @@ pub struct InjectParams {
   pub key_delay: Option<usize>,
   pub disable_x11_fast_inject: bool,
   pub evdev_modifier_delay: Option<usize>,
+  pub x11_use_xdotool_backend: bool,
 }
diff --git a/espanso/src/cli/worker/engine/funnel/detect.rs b/espanso/src/cli/worker/engine/funnel/detect.rs
index c587f307..8e3e1e2a 100644
--- a/espanso/src/cli/worker/engine/funnel/detect.rs
+++ b/espanso/src/cli/worker/engine/funnel/detect.rs
@@ -110,6 +110,16 @@ pub fn convert_to_engine_key(key: espanso_detect::event::Key) -> Key {
     espanso_detect::event::Key::F18 => Key::F18,
     espanso_detect::event::Key::F19 => Key::F19,
     espanso_detect::event::Key::F20 => Key::F20,
+    espanso_detect::event::Key::Numpad0 => Key::Numpad0,
+    espanso_detect::event::Key::Numpad1 => Key::Numpad1,
+    espanso_detect::event::Key::Numpad2 => Key::Numpad2,
+    espanso_detect::event::Key::Numpad3 => Key::Numpad3,
+    espanso_detect::event::Key::Numpad4 => Key::Numpad4,
+    espanso_detect::event::Key::Numpad5 => Key::Numpad5,
+    espanso_detect::event::Key::Numpad6 => Key::Numpad6,
+    espanso_detect::event::Key::Numpad7 => Key::Numpad7,
+    espanso_detect::event::Key::Numpad8 => Key::Numpad8,
+    espanso_detect::event::Key::Numpad9 => Key::Numpad9,
     espanso_detect::event::Key::Other(code) => Key::Other(code),
   }
 }
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index d48269db..a30262f6 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -243,6 +243,7 @@ pub fn initialize_and_spawn(
         &modifier_state_store,
         &combined_match_cache,
         &notification_manager,
+        &config_manager,
       );
 
       let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);
diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs
index 9eda6385..58b2bd72 100644
--- a/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs
+++ b/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs
@@ -98,5 +98,6 @@ pub fn convert_to_match_key(key: Key) -> espanso_match::event::Key {
     Key::F19 => espanso_match::event::Key::F19,
     Key::F20 => espanso_match::event::Key::F20,
     Key::Other(_) => espanso_match::event::Key::Other,
+    _ => espanso_match::event::Key::Other,
   }
 }
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index 708cd8de..7db6bf20 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -36,6 +36,7 @@ use simplelog::{
 use crate::{
   cli::{LogMode, PathsOverrides},
   config::load_config,
+  util::log_system_info,
 };
 
 mod capabilities;
@@ -586,6 +587,7 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
       info!("reading configs from: {:?}", paths.config);
       info!("reading packages from: {:?}", paths.packages);
       info!("using runtime dir: {:?}", paths.runtime);
+      log_system_info();
 
       if handler.requires_config {
         let config_result =
diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs
index cd0a4922..30c29eb5 100644
--- a/espanso/src/patch/patches/mod.rs
+++ b/espanso/src/patch/patches/mod.rs
@@ -50,8 +50,10 @@ generate_patchable_config!(
   undo_backspace -> bool,
   post_form_delay -> usize,
   post_search_delay -> usize,
+  emulate_alt_codes -> bool,
   win32_exclude_orphan_events -> bool,
   win32_keyboard_layout_cache_interval -> i64,
   x11_use_xclip_backend -> bool,
+  x11_use_xdotool_backend -> bool,
   keyboard_layout -> Option<RMLVOConfig>
 );
diff --git a/espanso/src/util.rs b/espanso/src/util.rs
index 1e4a22f2..42602f5d 100644
--- a/espanso/src/util.rs
+++ b/espanso/src/util.rs
@@ -17,7 +17,9 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use log::info;
 use std::process::Command;
+use sysinfo::{System, SystemExt};
 
 #[cfg(target_os = "windows")]
 pub fn set_command_flags(command: &mut Command) {
@@ -43,3 +45,13 @@ pub fn attach_console() {
 pub fn attach_console() {
   // Not necessary on Linux and macOS
 }
+
+pub fn log_system_info() {
+  let sys = System::new();
+  info!(
+    "system info: {} v{} - kernel: {}",
+    sys.name().unwrap_or_default(),
+    sys.os_version().unwrap_or_default(),
+    sys.kernel_version().unwrap_or_default()
+  );
+}
diff --git a/snapcraft.yaml b/snapcraft.yaml
index dcfb4b88..3003c5de 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
 name: espanso
-version: 2.1.5-beta
+version: 2.1.6-beta
 summary: A Cross-platform Text Expander written in Rust
 description: |
   espanso is a Cross-platform, Text Expander written in Rust.


Test patch: