Repository: espanso/espanso (language: rust)

Issue:
App-specific includes are not working in v2
I'm using the "includes" command to use matches only in a specific application (Firefox) and I have the following configurations:

**/config/firefox.yml:**
filter_title: "Mozilla Firefox"
extra_includes:
  - "../match/_firefox.yml"

**/match/_firefox.yml:**
matches:
  - trigger: ":test"
    replace: This should work only in Firefox!

But it's not working, while on the other hand it was working just fine on the previous version. Did I do something wrong?

Thank you very much for this great work.


Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..c8b241f2
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,1 @@
+target/*
\ No newline at end of file
diff --git a/ci/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile
similarity index 87%
rename from ci/ubuntu/Dockerfile
rename to .github/scripts/ubuntu/Dockerfile
index 2fdb41b9..6fbba6e7 100644
--- a/ci/ubuntu/Dockerfile
+++ b/.github/scripts/ubuntu/Dockerfile
@@ -3,12 +3,13 @@ FROM ubuntu:18.04
 RUN apt-get update \
     && apt-get install -y libssl-dev \
     libxdo-dev libxtst-dev libx11-dev \
-    wget git cmake build-essential pkg-config
+    libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev \
+    wget git file build-essential pkg-config
 
 ENV RUSTUP_HOME=/usr/local/rustup \
     CARGO_HOME=/usr/local/cargo \
     PATH=/usr/local/cargo/bin:$PATH \
-    RUST_VERSION=1.41.0
+    RUST_VERSION=1.55.0
 
 RUN set -eux; \
     dpkgArch="$(dpkg --print-architecture)"; \
@@ -30,9 +31,8 @@ RUN set -eux; \
     cargo --version; \
     rustc --version;
 
-RUN mkdir espanso
+RUN mkdir espanso && cargo install --force cargo-make
 
 COPY . espanso
 
-RUN cd espanso \
-    && cargo install cargo-deb
+RUN cd espanso
diff --git a/.github/scripts/ubuntu/build_appimage.sh b/.github/scripts/ubuntu/build_appimage.sh
new file mode 100755
index 00000000..ed49379e
--- /dev/null
+++ b/.github/scripts/ubuntu/build_appimage.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -e
+
+echo "Testing espanso..."
+cd espanso
+cargo make test-binary --profile release
+
+echo "Building espanso and creating AppImage"
+cargo make create-app-image --profile release
+
+cd ..
+cp espanso/target/linux/AppImage/out/Espanso-*.AppImage Espanso-X11.AppImage
+sha256sum Espanso-X11.AppImage > Espanso-X11.AppImage.sha256.txt
+ls -la
+
+echo "Copying to mounted volume"
+cp Espanso-X11* /shared
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..579f0bd8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,82 @@
+# Huge thanks to Alacritty, as their configuration served as a starting point for this one!
+# See: https://github.com/alacritty/alacritty
+
+name: CI
+
+on: [push, pull_request]
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  build:
+    strategy:
+      matrix:
+        os: [windows-latest, macos-latest, ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Check formatting
+        run: |
+          rustup component add rustfmt
+          cargo fmt --all -- --check
+      - name: Install Linux dependencies
+        if: ${{ runner.os == 'Linux' }}
+        run: |
+          sudo apt install libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
+      - name: Check clippy
+        run: |
+          rustup component add clippy
+          cargo clippy -- -D warnings
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - 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
+      - name: Check formatting
+        run: |
+          rustup component add rustfmt
+          cargo fmt --all -- --check
+      - name: Install Linux dependencies
+        run: |
+          sudo apt install libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
+      - name: Check clippy
+        run: |
+          rustup component add clippy
+          cargo clippy -p espanso --features wayland -- -D warnings
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - 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
+    steps:
+      - uses: actions/checkout@v2
+      - name: Install target
+        run: rustup update && rustup target add aarch64-apple-darwin
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - name: Build
+        run: |
+          cargo make build-macos-arm-binary
+      # - name: Setup tmate session
+      #   uses: mxschmitt/action-tmate@v3
+      #   with:
+      #     limit-access-to-actor: true
+
+# TODO: add clippy check
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..ee56d235
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,199 @@
+# Huge thanks to Alacritty, as their configuration served as a starting point for this one!
+# See: https://github.com/alacritty/alacritty
+
+name: Release
+
+on:
+  push:
+    branches:
+      - master
+      - dev
+
+env:
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+  CARGO_TERM_COLOR: always
+
+jobs:
+  extract-version:
+    name: extract-version
+    runs-on: ubuntu-latest
+    outputs:
+      espanso_version: ${{ steps.version.outputs.version }}
+    steps:
+      - uses: actions/checkout@v2
+      - name: "Extract version"
+        id: "version"
+        run: |
+          ESPANSO_VERSION=$(cat espanso/Cargo.toml | grep version | head -1 | awk -F '"' '{ print $2 }')
+          echo version: $ESPANSO_VERSION
+          echo "::set-output name=version::v$ESPANSO_VERSION"
+
+  create-release:
+    name: create-release
+    needs: ["extract-version"]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Create new release (only on master)
+        if: ${{ github.ref == 'refs/heads/master' }}
+        run: |
+          COMMIT_HASH=$(git rev-list --max-count=1 HEAD)
+          echo "Creating release: ${{ needs.extract-version.outputs.espanso_version }}"
+          echo "for hash: $COMMIT_HASH"
+          gh release create ${{ needs.extract-version.outputs.espanso_version }} \
+            -t ${{ needs.extract-version.outputs.espanso_version }} \
+            --notes "Automatically released by CI" \
+            --prerelease \
+            --target $COMMIT_HASH
+
+  windows:
+    needs: ["extract-version", "create-release"]
+    runs-on: windows-latest
+
+    defaults:
+      run:
+        shell: bash
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Print target version
+        run: |
+          echo Using version ${{ needs.extract-version.outputs.espanso_version }}
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - name: Test
+        run: cargo make test-binary --profile release
+      - name: Build
+        run: cargo make build-windows-all --profile release
+      - name: Create portable mode archive
+        shell: powershell
+        run: |
+          Rename-Item target/windows/portable espanso-portable
+          Compress-Archive target/windows/espanso-portable target/windows/Espanso-Win-Portable-x86_64.zip
+      - name: Calculate hashes
+        shell: powershell
+        run: |
+          Get-FileHash target/windows/Espanso-Win-Portable-x86_64.zip -Algorithm SHA256 | select-object -ExpandProperty Hash > target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
+          Get-FileHash target/windows/installer/Espanso-Win-Installer-x86_64.exe -Algorithm SHA256 | select-object -ExpandProperty Hash > target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt
+      - uses: actions/upload-artifact@v2
+        name: "Upload artifacts"
+        with:
+          name: Windows Artifacts
+          path: |
+            target/windows/installer/Espanso-Win-Installer-x86_64.exe
+            target/windows/Espanso-Win-Portable-x86_64.zip
+            target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt
+            target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
+      - name: Upload artifacts to Github Releases (if master)
+        if: ${{ github.ref == 'refs/heads/master' }}
+        run: |
+          gh release upload ${{ needs.extract-version.outputs.espanso_version }} target/windows/installer/Espanso-Win-Installer-x86_64.exe target/windows/Espanso-Win-Portable-x86_64.zip target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
+  
+  linux-x11:
+    needs: ["extract-version", "create-release"]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Print target version
+        run: |
+          echo Using version ${{ needs.extract-version.outputs.espanso_version }}
+      - name: Build docker image
+        run: |
+          sudo docker build -t espanso-ubuntu . -f .github/scripts/ubuntu/Dockerfile
+      - name: Build AppImage
+        run: |
+          sudo docker run --rm -v "$(pwd):/shared" espanso-ubuntu espanso/.github/scripts/ubuntu/build_appimage.sh
+      - uses: actions/upload-artifact@v2
+        name: "Upload artifacts"
+        with:
+          name: Linux X11 Artifacts
+          path: |
+            Espanso-X11.AppImage
+            Espanso-X11.AppImage.sha256.txt
+      - name: Upload artifacts to Github Releases (if master)
+        if: ${{ github.ref == 'refs/heads/master' }}
+        run: |
+          gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-X11.AppImage Espanso-X11.AppImage.sha256.txt
+
+  macos-intel:
+    needs: ["extract-version", "create-release"]
+    runs-on: macos-11
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Print target version
+        run: |
+          echo Using version ${{ needs.extract-version.outputs.espanso_version }}
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - name: Test
+        run: cargo make test-binary --profile release
+      - name: Build
+        run: cargo make create-bundle --profile release
+      - name: Create ZIP archive
+        run: |
+          ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-Intel.zip
+      - name: Calculate hashes
+        run: |
+          shasum -a 256 Espanso-Mac-Intel.zip > Espanso-Mac-Intel.zip.sha256.txt
+      - uses: actions/upload-artifact@v2
+        name: "Upload artifacts"
+        with:
+          name: Mac Intel Artifacts
+          path: |
+            Espanso-Mac-Intel.zip
+            Espanso-Mac-Intel.zip.sha256.txt
+      - name: Upload artifacts to Github Releases (if master)
+        if: ${{ github.ref == 'refs/heads/master' }}
+        run: |
+          gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-Mac-Intel.zip Espanso-Mac-Intel.zip.sha256.txt
+  
+  macos-m1:
+    needs: ["extract-version", "create-release"]
+    runs-on: macos-11
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Print target version
+        run: |
+          echo Using version ${{ needs.extract-version.outputs.espanso_version }}
+      - name: Install rust target
+        run: rustup update && rustup target add aarch64-apple-darwin
+      - name: Install cargo-make
+        run: |
+          cargo install --force cargo-make
+      - name: Build
+        run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
+      - name: Codesign executable
+        env: 
+          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+          MACOS_CI_KEYCHAIN_PWD: ${{ secrets.MACOS_CI_KEYCHAIN_PWD }}
+        run: |
+          echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
+          security create-keychain -p $MACOS_CI_KEYCHAIN_PWD buildespanso.keychain 
+          security default-keychain -s buildespanso.keychain
+          security unlock-keychain -p $MACOS_CI_KEYCHAIN_PWD buildespanso.keychain
+          security import certificate.p12 -k buildespanso.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
+          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CI_KEYCHAIN_PWD buildespanso.keychain
+          /usr/bin/codesign --force -s "Espanso CI Self-Signed" target/mac/Espanso.app -v
+      - name: Create ZIP archive
+        run: |
+          ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-M1.zip
+      - name: Calculate hashes
+        run: |
+          shasum -a 256 Espanso-Mac-M1.zip > Espanso-Mac-M1.zip.sha256.txt
+      - uses: actions/upload-artifact@v2
+        name: "Upload artifacts"
+        with:
+          name: Mac M1 Artifacts
+          path: |
+            Espanso-Mac-M1.zip
+            Espanso-Mac-M1.zip.sha256.txt
+      - name: Upload artifacts to Github Releases (if master)
+        if: ${{ github.ref == 'refs/heads/master' }}
+        run: |
+          gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-Mac-M1.zip Espanso-Mac-M1.zip.sha256.txt
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 62d4f6e4..493e4f2f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,16 +1,18 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
-name = "adler32"
-version = "1.0.3"
+name = "adler"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.6"
+version = "0.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
 dependencies = [
  "memchr",
 ]
@@ -25,84 +27,72 @@ dependencies = [
 ]
 
 [[package]]
-name = "arc-swap"
-version = "0.4.7"
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
+checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
 
 [[package]]
 name = "arrayref"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
 
 [[package]]
 name = "arrayvec"
-version = "0.4.11"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
-dependencies = [
- "nodrop",
-]
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "atty"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
+ "hermit-abi",
  "libc",
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "autocfg"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875"
-
-[[package]]
-name = "backtrace"
-version = "0.3.37"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5180c5a20655b14a819b652fd2378fa5f1697b6c9ddad3e695c2f9cedf6df4e2"
-dependencies = [
- "backtrace-sys",
- "cfg-if",
- "libc",
- "rustc-demangle",
-]
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
-name = "backtrace-sys"
-version = "0.1.31"
+name = "base64"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b"
-dependencies = [
- "cc",
- "libc",
-]
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
 [[package]]
-name = "base64"
-version = "0.10.1"
+name = "bitflags"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
-dependencies = [
- "byteorder",
-]
+checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
 
 [[package]]
 name = "bitflags"
-version = "1.1.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
 [[package]]
 name = "blake2b_simd"
-version = "0.5.7"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf775a81bb2d464e20ff170ac20316c7b08a43d11dbc72f0f82e8e8d3d6d0499"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
 dependencies = [
  "arrayref",
  "arrayvec",
@@ -110,27 +100,54 @@ dependencies = [
 ]
 
 [[package]]
-name = "byteorder"
-version = "1.3.2"
+name = "block"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
 [[package]]
-name = "bytes"
-version = "0.4.12"
+name = "block-buffer"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
 dependencies = [
- "byteorder",
- "either",
- "iovec",
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
 [[package]]
 name = "bzip2"
-version = "0.3.3"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
+checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
 dependencies = [
  "bzip2-sys",
  "libc",
@@ -138,57 +155,76 @@ dependencies = [
 
 [[package]]
 name = "bzip2-sys"
-version = "0.1.7"
+version = "0.1.11+1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6584aa36f5ad4c9247f5323b0a42f37802b37a836f0ad87084d7a33961abe25f"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
 dependencies = [
  "cc",
  "libc",
+ "pkg-config",
 ]
 
 [[package]]
-name = "c2-chacha"
-version = "0.2.2"
+name = "calloop"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101"
+checksum = "9d0a1340115d6bd81e1066469091596a339f68878a2ce3c2f39e546607d22131"
 dependencies = [
- "lazy_static",
- "ppv-lite86",
+ "log",
+ "nix 0.19.1",
+]
+
+[[package]]
+name = "caps"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c088f2dddef283f86b023ab1ebe2301c653326834996458b2f48d29b804e9540"
+dependencies = [
+ "errno",
+ "libc",
+ "thiserror",
 ]
 
 [[package]]
 name = "cc"
-version = "1.0.45"
+version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be"
+checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
 
 [[package]]
 name = "cfg-if"
-version = "0.1.9"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.9"
+version = "0.4.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
 dependencies = [
  "libc",
  "num-integer",
  "num-traits",
  "time",
+ "winapi 0.3.9",
 ]
 
 [[package]]
 name = "clap"
-version = "2.33.0"
+version = "2.33.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 dependencies = [
- "ansi_term",
+ "ansi_term 0.11.0",
  "atty",
- "bitflags",
+ "bitflags 1.2.1",
  "strsim",
  "textwrap",
  "unicode-width",
@@ -196,90 +232,74 @@ dependencies = [
 ]
 
 [[package]]
-name = "clicolors-control"
-version = "1.0.1"
+name = "colored"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e"
+checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
 dependencies = [
  "atty",
  "lazy_static",
- "libc",
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "cloudabi"
-version = "0.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "cmake"
-version = "0.1.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62"
-dependencies = [
- "cc",
-]
-
 [[package]]
 name = "console"
-version = "0.9.0"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62828f51cfa18f8c31d3d55a43c6ce6af3f87f754cba9fbba7ff38089b9f5612"
+checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
 dependencies = [
- "clicolors-control",
  "encode_unicode",
  "lazy_static",
  "libc",
  "regex",
- "termios",
+ "terminal_size",
  "unicode-width",
  "winapi 0.3.9",
 ]
 
 [[package]]
-name = "constant_time_eq"
-version = "0.1.4"
+name = "const-sha1"
+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 = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120"
+checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
 
 [[package]]
-name = "cookie"
-version = "0.12.0"
+name = "const_format"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5"
+checksum = "c75ea7d6aeb2ebd1ee24f7b7e1b23242ef5a56b3a693733b99bfbe5ef31d0306"
 dependencies = [
- "time",
- "url 1.7.2",
+ "const_format_proc_macros",
 ]
 
 [[package]]
-name = "cookie_store"
-version = "0.7.0"
+name = "const_format_proc_macros"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c"
+checksum = "29c36c619c422113552db4eb28cddba8faa757e33f758cc3415bd2885977b591"
 dependencies = [
- "cookie",
- "failure",
- "idna 0.1.5",
- "log",
- "publicsuffix",
- "serde",
- "serde_json",
- "time",
- "try_from",
- "url 1.7.2",
+ "proc-macro2",
+ "quote 1.0.9",
+ "unicode-xid 0.2.1",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
 [[package]]
 name = "core-foundation"
-version = "0.6.4"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -287,106 +307,220 @@ dependencies = [
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.6.2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
 
 [[package]]
 name = "crc32fast"
-version = "1.2.0"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
+checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.7.1"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71"
+checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
 dependencies = [
+ "cfg-if 1.0.0",
  "crossbeam-epoch",
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.7.2"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9"
+checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
 dependencies = [
- "arrayvec",
- "cfg-if",
+ "cfg-if 1.0.0",
+ "const_fn",
  "crossbeam-utils",
  "lazy_static",
  "memoffset",
- "scopeguard 1.0.0",
+ "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.1.2"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
+checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
 dependencies = [
+ "cfg-if 1.0.0",
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.6.6"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
 dependencies = [
- "cfg-if",
+ "autocfg",
+ "cfg-if 1.0.0",
  "lazy_static",
 ]
 
+[[package]]
+name = "ctor"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
+dependencies = [
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b1334c0161ddfccd239ac81b188d62015b049c986c5cd0b7f9447cf2c54f4a3"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+]
+
 [[package]]
 name = "dialoguer"
-version = "0.4.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "116f66c4e7b19af0d52857aa4ff710cc3b4781d9c16616e31540bc55ec57ba8c"
+checksum = "c9dd058f8b65922819fabb4a41e7d1964e56344042c26efbccd465202c23fa0c"
 dependencies = [
  "console",
  "lazy_static",
  "tempfile",
+ "zeroize",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
+
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dirs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi 0.3.9",
 ]
 
 [[package]]
 name = "dirs"
-version = "2.0.2"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
+checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
 dependencies = [
- "cfg-if",
  "dirs-sys",
 ]
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
+checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
 dependencies = [
- "cfg-if",
  "libc",
  "redox_users",
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "dlib"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "downcast"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d"
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
+
 [[package]]
 name = "dtoa"
-version = "0.4.4"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
+
+[[package]]
+name = "dunce"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
+checksum = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4"
 
 [[package]]
 name = "either"
-version = "1.5.3"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "encode_unicode"
@@ -396,107 +530,386 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.20"
+version = "0.8.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87240518927716f79692c2ed85bfe6e98196d18c6401ec75355760233a7e12e9"
+checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
-name = "error-chain"
-version = "0.12.1"
+name = "enum-as-inner"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9"
+checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
 dependencies = [
- "backtrace",
- "version_check",
+ "heck",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067"
+dependencies = [
+ "gcc",
+ "libc",
 ]
 
 [[package]]
 name = "espanso"
-version = "0.7.3"
+version = "2.0.1"
 dependencies = [
- "backtrace",
- "chrono",
+ "anyhow",
+ "caps",
  "clap",
- "cmake",
+ "colored",
+ "const_format",
+ "crossbeam",
  "dialoguer",
- "dirs",
+ "dirs 3.0.1",
+ "enum-as-inner",
+ "espanso-clipboard",
+ "espanso-config",
+ "espanso-detect",
+ "espanso-engine",
+ "espanso-info",
+ "espanso-inject",
+ "espanso-ipc",
+ "espanso-kvs",
+ "espanso-mac-utils",
+ "espanso-match",
+ "espanso-migrate",
+ "espanso-modulo",
+ "espanso-package",
+ "espanso-path",
+ "espanso-render",
+ "espanso-ui",
  "fs2",
- "html2text",
+ "fs_extra",
  "lazy_static",
  "libc",
  "log",
  "log-panics",
- "markdown",
+ "maplit",
  "named_pipe",
  "notify",
- "rand 0.7.2",
+ "opener",
  "regex",
- "reqwest",
  "serde",
  "serde_json",
  "serde_yaml",
- "signal-hook",
  "simplelog",
- "tempfile",
- "walkdir",
+ "tempdir",
+ "thiserror",
  "widestring",
  "winapi 0.3.9",
- "zip",
+ "winreg 0.9.0",
 ]
 
 [[package]]
-name = "failure"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
+name = "espanso-clipboard"
+version = "0.1.0"
 dependencies = [
- "backtrace",
- "failure_derive",
+ "anyhow",
+ "cc",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "thiserror",
+ "wait-timeout",
+ "widestring",
 ]
 
 [[package]]
-name = "failure_derive"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
+name = "espanso-config"
+version = "0.1.0"
 dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "syn 0.15.44",
- "synstructure",
-]
-
-[[package]]
-name = "filetime"
-version = "0.2.9"
+ "anyhow",
+ "dunce",
+ "enum-as-inner",
+ "glob",
+ "indoc",
+ "lazy_static",
+ "log",
+ "mockall",
+ "ordered-float",
+ "regex",
+ "serde",
+ "serde_yaml",
+ "tempdir",
+ "tempfile",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "espanso-detect"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "enum-as-inner",
+ "lazy_static",
+ "lazycell",
+ "libc",
+ "log",
+ "regex",
+ "scopeguard",
+ "smithay-client-toolkit",
+ "thiserror",
+ "widestring",
+]
+
+[[package]]
+name = "espanso-engine"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "crossbeam",
+ "html2text",
+ "log",
+ "markdown",
+ "tempdir",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-info"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "thiserror",
+ "widestring",
+]
+
+[[package]]
+name = "espanso-inject"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "enum-as-inner",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "libc",
+ "log",
+ "regex",
+ "scopeguard",
+ "thiserror",
+ "widestring",
+]
+
+[[package]]
+name = "espanso-ipc"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "crossbeam",
+ "log",
+ "named_pipe",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-kvs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "log",
+ "serde",
+ "serde_json",
+ "tempdir",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-mac-utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "regex",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-match"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "log",
+ "regex",
+ "thiserror",
+ "unicase",
+]
+
+[[package]]
+name = "espanso-migrate"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "fs_extra",
+ "glob",
+ "include_dir",
+ "lazy_static",
+ "log",
+ "path-slash",
+ "pretty_assertions",
+ "regex",
+ "tempdir",
+ "tempfile",
+ "test-case",
+ "thiserror",
+ "walkdir",
+ "yaml-rust 0.4.5 (git+https://github.com/federico-terzi/yaml-rust)",
+]
+
+[[package]]
+name = "espanso-modulo"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "glob",
+ "lazy_static",
+ "log",
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "winres",
+ "zip",
+]
+
+[[package]]
+name = "espanso-package"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs_extra",
+ "glob",
+ "hex",
+ "lazy_static",
+ "log",
+ "natord",
+ "regex",
+ "reqwest",
+ "scopeguard",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "sha2",
+ "tempdir",
+ "thiserror",
+ "zip",
+]
+
+[[package]]
+name = "espanso-path"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "dirs 3.0.1",
+ "log",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-render"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "enum-as-inner",
+ "lazy_static",
+ "log",
+ "rand 0.8.3",
+ "regex",
+ "thiserror",
+]
+
+[[package]]
+name = "espanso-ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cc",
+ "crossbeam",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "notify-rust",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "widestring",
+ "winrt-notification 0.3.1",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f59efc38004c988e4201d11d263b8171f49a2e7ec0bdbb71773433f271504a5e"
+checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.2.5",
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "flate2"
-version = "1.0.11"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2adaffba6388640136149e18ed080b77a78611c1e1d6de75aedcdf78df5d4682"
+checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
 dependencies = [
+ "cfg-if 1.0.0",
  "crc32fast",
  "libc",
  "miniz_oxide",
 ]
 
+[[package]]
+name = "float-cmp"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "fnv"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "foreign-types"
@@ -513,6 +926,22 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "fragile"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2"
+
 [[package]]
 name = "fs2"
 version = "0.4.3"
@@ -523,13 +952,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "fs_extra"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
+
 [[package]]
 name = "fsevent"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "fsevent-sys",
 ]
 
@@ -554,7 +989,7 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "fuchsia-zircon-sys",
 ]
 
@@ -575,50 +1010,147 @@ dependencies = [
 ]
 
 [[package]]
-name = "futures"
-version = "0.1.29"
+name = "futures-channel"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef"
+checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+dependencies = [
+ "futures-core",
+]
 
 [[package]]
-name = "futures-cpupool"
-version = "0.1.8"
+name = "futures-core"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
+checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+
+[[package]]
+name = "futures-task"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+
+[[package]]
+name = "futures-util"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
 dependencies = [
- "futures",
- "num_cpus",
+ "autocfg",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gcc"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
 ]
 
 [[package]]
 name = "getrandom"
-version = "0.1.12"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
- "wasi",
+ "wasi 0.10.0+wasi-snapshot-preview1",
 ]
 
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
 [[package]]
 name = "h2"
-version = "0.1.26"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462"
+checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472"
 dependencies = [
- "byteorder",
  "bytes",
  "fnv",
- "futures",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
  "http",
  "indexmap",
- "log",
  "slab",
- "string",
- "tokio-io",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
 ]
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "html2text"
 version = "0.2.1"
@@ -639,16 +1171,16 @@ dependencies = [
  "log",
  "mac",
  "markup5ever",
- "proc-macro2 1.0.24",
- "quote 1.0.2",
- "syn 1.0.53",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
 ]
 
 [[package]]
 name = "http"
-version = "0.1.18"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4"
+checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
 dependencies = [
  "bytes",
  "fnv",
@@ -657,70 +1189,69 @@ dependencies = [
 
 [[package]]
 name = "http-body"
-version = "0.1.0"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d"
+checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
 dependencies = [
  "bytes",
- "futures",
  "http",
- "tokio-buf",
+ "pin-project-lite",
 ]
 
 [[package]]
 name = "httparse"
-version = "1.3.4"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
 
 [[package]]
 name = "hyper"
-version = "0.12.35"
+version = "0.14.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6"
+checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd"
 dependencies = [
  "bytes",
- "futures",
- "futures-cpupool",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
  "h2",
  "http",
  "http-body",
  "httparse",
- "iovec",
+ "httpdate",
  "itoa",
- "log",
- "net2",
- "rustc_version",
- "time",
+ "pin-project-lite",
+ "socket2",
  "tokio",
- "tokio-buf",
- "tokio-executor",
- "tokio-io",
- "tokio-reactor",
- "tokio-tcp",
- "tokio-threadpool",
- "tokio-timer",
+ "tower-service",
+ "tracing",
  "want",
 ]
 
 [[package]]
 name = "hyper-tls"
-version = "0.3.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
 dependencies = [
  "bytes",
- "futures",
  "hyper",
  "native-tls",
- "tokio-io",
+ "tokio",
+ "tokio-native-tls",
 ]
 
 [[package]]
 name = "idna"
-version = "0.1.5"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
 dependencies = [
  "matches",
  "unicode-bidi",
@@ -728,57 +1259,106 @@ dependencies = [
 ]
 
 [[package]]
-name = "idna"
-version = "0.2.0"
+name = "include_dir"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
+checksum = "23d58bdeb22b1c4691106c084b1063781904c35d0f22eda2a283598968eac61a"
 dependencies = [
- "matches",
- "unicode-bidi",
- "unicode-normalization",
+ "glob",
+ "include_dir_impl",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "include_dir_impl"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327869970574819d24d1dca25c891856144d29159ab797fa9dc725c5c3f57215"
+dependencies = [
+ "anyhow",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
 ]
 
 [[package]]
 name = "indexmap"
-version = "1.2.0"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3"
+checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136"
+dependencies = [
+ "unindent",
+]
 
 [[package]]
 name = "inotify"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8"
+checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "inotify-sys",
  "libc",
 ]
 
 [[package]]
 name = "inotify-sys"
-version = "0.1.3"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "iovec"
-version = "0.1.2"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
 dependencies = [
  "libc",
- "winapi 0.2.8",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itertools"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
+dependencies = [
+ "either",
 ]
 
 [[package]]
 name = "itoa"
-version = "0.4.4"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "js-sys"
+version = "0.3.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
+checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d"
+dependencies = [
+ "wasm-bindgen",
+]
 
 [[package]]
 name = "kernel32-sys"
@@ -798,39 +1378,48 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "lazycell"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
 [[package]]
 name = "libc"
-version = "0.2.62"
+version = "0.2.101"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba"
+checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
 
 [[package]]
-name = "linked-hash-map"
-version = "0.5.3"
+name = "libdbus-sys"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
+checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0"
+dependencies = [
+ "pkg-config",
+]
 
 [[package]]
-name = "lock_api"
-version = "0.1.5"
+name = "libloading"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
+checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
 dependencies = [
- "owning_ref",
- "scopeguard 0.3.3",
+ "cfg-if 1.0.0",
+ "winapi 0.3.9",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
 [[package]]
 name = "log"
-version = "0.4.8"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -839,7 +1428,6 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef"
 dependencies = [
- "backtrace",
  "log",
 ]
 
@@ -849,6 +1437,33 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
 
+[[package]]
+name = "mac-notification-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76"
+dependencies = [
+ "cc",
+ "chrono",
+ "dirs 1.0.5",
+ "objc-foundation",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
 [[package]]
 name = "markdown"
 version = "0.3.0"
@@ -862,16 +1477,13 @@ dependencies = [
 
 [[package]]
 name = "markup5ever"
-version = "0.10.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
 dependencies = [
  "log",
  "phf",
  "phf_codegen",
- "serde",
- "serde_derive",
- "serde_json",
  "string_cache",
  "string_cache_codegen",
  "tendril",
@@ -891,68 +1503,82 @@ dependencies = [
 
 [[package]]
 name = "matches"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
 
 [[package]]
 name = "memchr"
-version = "2.2.1"
+version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
 [[package]]
-name = "memoffset"
-version = "0.5.1"
+name = "memmap2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f"
+checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4"
 dependencies = [
- "rustc_version",
+ "libc",
 ]
 
 [[package]]
-name = "mime"
-version = "0.3.14"
+name = "memoffset"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd1d63acd1b78403cc0c325605908475dd9b9a3acbf65ed8bcab97e27014afcf"
+checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
+dependencies = [
+ "autocfg",
+]
 
 [[package]]
-name = "mime_guess"
-version = "2.0.1"
+name = "mime"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599"
-dependencies = [
- "mime",
- "unicase",
-]
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.3.2"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7108aff85b876d06f22503dcce091e29f76733b2bfdd91eebce81f5e68203a10"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
 dependencies = [
- "adler32",
+ "adler",
+ "autocfg",
 ]
 
 [[package]]
 name = "mio"
-version = "0.6.19"
+version = "0.6.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
 dependencies = [
+ "cfg-if 0.1.10",
  "fuchsia-zircon",
  "fuchsia-zircon-sys",
  "iovec",
  "kernel32-sys",
  "libc",
  "log",
- "miow",
+ "miow 0.2.2",
  "net2",
  "slab",
  "winapi 0.2.8",
 ]
 
+[[package]]
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow 0.3.7",
+ "ntapi",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "mio-extras"
 version = "2.0.6"
@@ -961,15 +1587,15 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
 dependencies = [
  "lazycell",
  "log",
- "mio",
+ "mio 0.6.23",
  "slab",
 ]
 
 [[package]]
 name = "miow"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
 dependencies = [
  "kernel32-sys",
  "net2",
@@ -977,6 +1603,42 @@ dependencies = [
  "ws2_32-sys",
 ]
 
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "mockall"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d614ad23f9bb59119b8b5670a85c7ba92c5e9adf4385c81ea00c51c8be33d5"
+dependencies = [
+ "cfg-if 1.0.0",
+ "downcast",
+ "fragile",
+ "lazy_static",
+ "mockall_derive",
+ "predicates",
+ "predicates-tree",
+]
+
+[[package]]
+name = "mockall_derive"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd4234635bca06fc96c7368d038061e0aae1b00a764dc817e900dc974e3deea"
+dependencies = [
+ "cfg-if 1.0.0",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
 [[package]]
 name = "named_pipe"
 version = "0.4.1"
@@ -988,9 +1650,9 @@ dependencies = [
 
 [[package]]
 name = "native-tls"
-version = "0.2.3"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
 dependencies = [
  "lazy_static",
  "libc",
@@ -1004,13 +1666,19 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "natord"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
+
 [[package]]
 name = "net2"
-version = "0.2.33"
+version = "0.2.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
 dependencies = [
- "cfg-if",
+ "cfg-if 0.1.10",
  "libc",
  "winapi 0.3.9",
 ]
@@ -1022,34 +1690,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
 
 [[package]]
-name = "nodrop"
-version = "0.1.13"
+name = "nix"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
+dependencies = [
+ "bitflags 1.2.1",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
+dependencies = [
+ "bitflags 1.2.1",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
+dependencies = [
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
 
 [[package]]
 name = "notify"
-version = "4.0.15"
+version = "4.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd"
+checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "filetime",
  "fsevent",
  "fsevent-sys",
  "inotify",
  "libc",
- "mio",
+ "mio 0.6.23",
  "mio-extras",
  "walkdir",
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "notify-rust"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a3a5dd7b4b415b112ce0fae1988f3e6dee90a96918bf3950b5f2289b19a04b"
+dependencies = [
+ "dbus",
+ "mac-notification-sys",
+ "winrt-notification 0.2.2",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "num-integer"
-version = "0.1.41"
+version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
 dependencies = [
  "autocfg",
  "num-traits",
@@ -1057,47 +1779,99 @@ dependencies = [
 
 [[package]]
 name = "num-traits"
-version = "0.2.8"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
 dependencies = [
  "autocfg",
 ]
 
 [[package]]
 name = "num_cpus"
-version = "1.10.1"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
 dependencies = [
+ "hermit-abi",
  "libc",
 ]
 
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "opener"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952"
+dependencies = [
+ "bstr",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "openssl"
-version = "0.10.24"
+version = "0.10.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8152bb5a9b5b721538462336e3bef9a539f892715e5037fda0f984577311af15"
+checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
 dependencies = [
- "bitflags",
- "cfg-if",
+ "bitflags 1.2.1",
+ "cfg-if 1.0.0",
  "foreign-types",
- "lazy_static",
  "libc",
+ "once_cell",
  "openssl-sys",
 ]
 
 [[package]]
 name = "openssl-probe"
-version = "0.1.2"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.49"
+version = "0.9.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fad9e54bd23bd4cbbe48fdc08a1b8091707ac869ef8508edea2fec77dcc884"
+checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
 dependencies = [
  "autocfg",
  "cc",
@@ -1107,42 +1881,28 @@ dependencies = [
 ]
 
 [[package]]
-name = "owning_ref"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
-dependencies = [
- "stable_deref_trait",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.7.1"
+name = "ordered-float"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
+checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218"
 dependencies = [
- "lock_api",
- "parking_lot_core",
+ "num-traits",
 ]
 
 [[package]]
-name = "parking_lot_core"
-version = "0.4.0"
+name = "output_vt100"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
+checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
 dependencies = [
- "libc",
- "rand 0.6.5",
- "rustc_version",
- "smallvec",
  "winapi 0.3.9",
 ]
 
 [[package]]
-name = "percent-encoding"
-version = "1.0.1"
+name = "path-slash"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
+checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619"
 
 [[package]]
 name = "percent-encoding"
@@ -1176,7 +1936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
 dependencies = [
  "phf_shared",
- "rand 0.7.2",
+ "rand 0.7.3",
 ]
 
 [[package]]
@@ -1188,6 +1948,18 @@ dependencies = [
  "siphasher",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "pipeline"
 version = "0.5.0"
@@ -1196,21 +1968,15 @@ checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0"
 
 [[package]]
 name = "pkg-config"
-version = "0.3.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea"
-
-[[package]]
-name = "podio"
-version = "0.1.6"
+version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.5"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
 [[package]]
 name = "precomputed-hash"
@@ -1219,107 +1985,135 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
 [[package]]
-name = "proc-macro2"
-version = "0.4.30"
+name = "predicates"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df"
 dependencies = [
- "unicode-xid 0.1.0",
+ "difference",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
 ]
 
 [[package]]
-name = "proc-macro2"
-version = "1.0.24"
+name = "predicates-core"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
-dependencies = [
- "unicode-xid 0.2.0",
-]
+checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
 
 [[package]]
-name = "publicsuffix"
-version = "1.5.3"
+name = "predicates-tree"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bf259a81de2b2eb9850ec990ec78e6a25319715584fd7652b9b26f96fcb1510"
+checksum = "15f553275e5721409451eb85e15fd9a860a6e5ab4496eb215987502b5f5391f2"
 dependencies = [
- "error-chain",
- "idna 0.2.0",
- "lazy_static",
- "regex",
- "url 2.1.0",
+ "predicates-core",
+ "treeline",
 ]
 
 [[package]]
-name = "quote"
-version = "0.6.13"
+name = "pretty_assertions"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b"
 dependencies = [
- "proc-macro2 0.4.30",
+ "ansi_term 0.12.1",
+ "ctor",
+ "diff",
+ "output_vt100",
 ]
 
 [[package]]
-name = "quote"
-version = "1.0.2"
+name = "proc-macro-hack"
+version = "0.5.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
-dependencies = [
- "proc-macro2 1.0.24",
-]
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
 [[package]]
-name = "rand"
-version = "0.6.5"
+name = "proc-macro2"
+version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
 dependencies = [
- "autocfg",
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "quote"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
  "libc",
- "rand_chacha 0.1.1",
- "rand_core 0.4.2",
- "rand_hc 0.1.0",
- "rand_isaac",
- "rand_jitter",
- "rand_os",
- "rand_pcg 0.1.2",
- "rand_xorshift",
+ "rand_core 0.3.1",
+ "rdrand",
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "rand"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
 dependencies = [
- "getrandom",
+ "getrandom 0.1.16",
  "libc",
- "rand_chacha 0.2.1",
+ "rand_chacha 0.2.2",
  "rand_core 0.5.1",
  "rand_hc 0.2.0",
- "rand_pcg 0.2.1",
+ "rand_pcg",
 ]
 
 [[package]]
-name = "rand_chacha"
-version = "0.1.1"
+name = "rand"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
 dependencies = [
- "autocfg",
- "rand_core 0.3.1",
+ "libc",
+ "rand_chacha 0.3.0",
+ "rand_core 0.6.2",
+ "rand_hc 0.3.0",
 ]
 
 [[package]]
 name = "rand_chacha"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
 dependencies = [
- "c2-chacha",
+ "ppv-lite86",
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.2",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.3.1"
@@ -1341,16 +2135,16 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
 dependencies = [
- "getrandom",
+ "getrandom 0.1.16",
 ]
 
 [[package]]
-name = "rand_hc"
-version = "0.1.0"
+name = "rand_core"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
 dependencies = [
- "rand_core 0.3.1",
+ "getrandom 0.2.2",
 ]
 
 [[package]]
@@ -1363,47 +2157,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "rand_isaac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
-dependencies = [
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "rand_jitter"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
-dependencies = [
- "libc",
- "rand_core 0.4.2",
- "winapi 0.3.9",
-]
-
-[[package]]
-name = "rand_os"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
-dependencies = [
- "cloudabi",
- "fuchsia-cprng",
- "libc",
- "rand_core 0.4.2",
- "rdrand",
- "winapi 0.3.9",
-]
-
-[[package]]
-name = "rand_pcg"
-version = "0.1.2"
+name = "rand_hc"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
 dependencies = [
- "autocfg",
- "rand_core 0.4.2",
+ "rand_core 0.6.2",
 ]
 
 [[package]]
@@ -1415,15 +2174,6 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
-[[package]]
-name = "rand_xorshift"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
-dependencies = [
- "rand_core 0.3.1",
-]
-
 [[package]]
 name = "rdrand"
 version = "0.4.0"
@@ -1435,152 +2185,152 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.1.56"
+version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
+dependencies = [
+ "bitflags 1.2.1",
+]
 
 [[package]]
 name = "redox_users"
-version = "0.3.1"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
 dependencies = [
- "failure",
- "rand_os",
- "redox_syscall",
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
  "rust-argon2",
 ]
 
 [[package]]
 name = "regex"
-version = "1.3.1"
+version = "1.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
+checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3"
 dependencies = [
  "aho-corasick",
  "memchr",
  "regex-syntax",
- "thread_local",
 ]
 
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
 [[package]]
 name = "regex-syntax"
-version = "0.6.12"
+version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
+checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
 
 [[package]]
 name = "remove_dir_all"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
 dependencies = [
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "reqwest"
-version = "0.9.20"
+version = "0.11.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6d896143a583047512e59ac54a215cb203c29cc941917343edea3be8df9c78"
+checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
 dependencies = [
  "base64",
  "bytes",
- "cookie",
- "cookie_store",
  "encoding_rs",
- "flate2",
- "futures",
+ "futures-core",
+ "futures-util",
  "http",
+ "http-body",
  "hyper",
  "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
  "log",
  "mime",
- "mime_guess",
  "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
  "serde",
- "serde_json",
  "serde_urlencoded",
- "time",
  "tokio",
- "tokio-executor",
- "tokio-io",
- "tokio-threadpool",
- "tokio-timer",
- "url 1.7.2",
- "uuid",
- "winreg",
+ "tokio-native-tls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg 0.7.0",
 ]
 
 [[package]]
 name = "rust-argon2"
-version = "0.5.1"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
 dependencies = [
  "base64",
  "blake2b_simd",
+ "constant_time_eq",
  "crossbeam-utils",
 ]
 
-[[package]]
-name = "rustc-demangle"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
-
-[[package]]
-name = "rustc_version"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
-dependencies = [
- "semver",
-]
-
 [[package]]
 name = "ryu"
-version = "1.0.0"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
 [[package]]
 name = "same-file"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
 dependencies = [
  "winapi-util",
 ]
 
 [[package]]
 name = "schannel"
-version = "0.1.16"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
 dependencies = [
  "lazy_static",
  "winapi 0.3.9",
 ]
 
 [[package]]
-name = "scopeguard"
-version = "0.3.3"
+name = "scoped-tls"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
+checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
 
 [[package]]
 name = "scopeguard"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "security-framework"
-version = "0.3.1"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eee63d0f4a9ec776eeb30e220f0bc1e092c3ad744b2a379e3993070364d3adc2"
+checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
 dependencies = [
+ "bitflags 1.2.1",
  "core-foundation",
  "core-foundation-sys",
  "libc",
@@ -1589,53 +2339,39 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "0.3.1"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9636f8989cbf61385ae4824b98c1aaa54c994d7d8b41f11c601ed799f0549a56"
+checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
 dependencies = [
  "core-foundation-sys",
+ "libc",
 ]
 
-[[package]]
-name = "semver"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
-dependencies = [
- "semver-parser",
-]
-
-[[package]]
-name = "semver-parser"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-
 [[package]]
 name = "serde"
-version = "1.0.117"
+version = "1.0.123"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
+checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.117"
+version = "1.0.123"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
+checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
 dependencies = [
- "proc-macro2 1.0.24",
- "quote 1.0.2",
- "syn 1.0.53",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.60"
+version = "1.0.62"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
+checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
 dependencies = [
  "itoa",
  "ryu",
@@ -1644,97 +2380,109 @@ dependencies = [
 
 [[package]]
 name = "serde_urlencoded"
-version = "0.5.5"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
 dependencies = [
- "dtoa",
+ "form_urlencoded",
  "itoa",
+ "ryu",
  "serde",
- "url 1.7.2",
 ]
 
 [[package]]
 name = "serde_yaml"
-version = "0.8.14"
+version = "0.8.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7"
+checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
 dependencies = [
  "dtoa",
  "linked-hash-map",
  "serde",
- "yaml-rust",
-]
-
-[[package]]
-name = "signal-hook"
-version = "0.1.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6"
-dependencies = [
- "libc",
- "signal-hook-registry",
+ "yaml-rust 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "signal-hook-registry"
-version = "1.2.0"
+name = "sha2"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
+checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3"
 dependencies = [
- "arc-swap",
- "libc",
+ "block-buffer",
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
 ]
 
 [[package]]
 name = "simplelog"
-version = "0.7.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebbe8c881061cce7ee205784634eda7a61922925e7cc2833188467d3a560e027"
+checksum = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
 dependencies = [
  "chrono",
  "log",
- "term",
+ "termcolor",
 ]
 
 [[package]]
 name = "siphasher"
-version = "0.3.3"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"
+checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1"
 
 [[package]]
 name = "slab"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
 
 [[package]]
 name = "smallvec"
-version = "0.6.10"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
 
 [[package]]
-name = "stable_deref_trait"
-version = "1.1.1"
+name = "smithay-client-toolkit"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
+checksum = "ec783683499a2cfc85b6df3d04f83b1907b5cbd98a1aed44667dbdf1eac4e64c"
+dependencies = [
+ "bitflags 1.2.1",
+ "calloop",
+ "dlib",
+ "lazy_static",
+ "log",
+ "memmap2",
+ "nix 0.20.0",
+ "wayland-client",
+ "wayland-cursor",
+ "wayland-protocols",
+]
 
 [[package]]
-name = "string"
-version = "0.2.1"
+name = "socket2"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
+checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
 dependencies = [
- "bytes",
+ "libc",
+ "winapi 0.3.9",
 ]
 
+[[package]]
+name = "squote"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fccf17fd09e2455ea796d2ad267b64fa2c5cbd8701b2a93b555d2aa73449f7d"
+
 [[package]]
 name = "string_cache"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a"
+checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
 dependencies = [
  "lazy_static",
  "new_debug_unreachable",
@@ -1751,455 +2499,760 @@ checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
 dependencies = [
  "phf_generator",
  "phf_shared",
- "proc-macro2 1.0.24",
- "quote 1.0.2",
+ "proc-macro2",
+ "quote 1.0.9",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "strum"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da"
+
+[[package]]
+name = "strum"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
+dependencies = [
+ "strum_macros 0.20.1",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8"
+dependencies = [
+ "quote 0.3.15",
+ "syn 0.11.11",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
+[[package]]
+name = "syn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
+dependencies = [
+ "quote 0.3.15",
+ "synom",
+ "unicode-xid 0.0.4",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.9",
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "synom"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
+dependencies = [
+ "unicode-xid 0.0.4",
+]
+
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "rand 0.8.3",
+ "redox_syscall 0.2.5",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "test-case"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956044ef122917dde830c19dec5f76d0670329fde4104836d62ebcb14f4865f1"
+dependencies = [
+ "cfg-if 1.0.0",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+ "version_check",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio 0.7.13",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+dependencies = [
+ "cfg-if 1.0.0",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "treeline"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
 ]
 
 [[package]]
-name = "strsim"
-version = "0.8.0"
+name = "unicode-segmentation"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
 
 [[package]]
-name = "syn"
-version = "0.15.44"
+name = "unicode-width"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
-dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "unicode-xid 0.1.0",
-]
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
 
 [[package]]
-name = "syn"
-version = "1.0.53"
+name = "unicode-xid"
+version = "0.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
-dependencies = [
- "proc-macro2 1.0.24",
- "quote 1.0.2",
- "unicode-xid 0.2.0",
-]
+checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
 
 [[package]]
-name = "synstructure"
-version = "0.10.2"
+name = "unicode-xid"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f"
-dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "syn 0.15.44",
- "unicode-xid 0.1.0",
-]
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
 [[package]]
-name = "tempfile"
-version = "3.1.0"
+name = "unindent"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
-dependencies = [
- "cfg-if",
- "libc",
- "rand 0.7.2",
- "redox_syscall",
- "remove_dir_all",
- "winapi 0.3.9",
-]
+checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
 
 [[package]]
-name = "tendril"
-version = "0.4.1"
+name = "url"
+version = "2.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
 dependencies = [
- "futf",
- "mac",
- "utf-8",
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
 ]
 
 [[package]]
-name = "term"
-version = "0.6.1"
+name = "utf-8"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5"
-dependencies = [
- "dirs",
- "winapi 0.3.9",
-]
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
 [[package]]
-name = "termios"
-version = "0.3.1"
+name = "vcpkg"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625"
-dependencies = [
- "libc",
-]
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
-name = "textwrap"
-version = "0.11.0"
+name = "vec_map"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
 [[package]]
-name = "thread_local"
-version = "0.3.6"
+name = "version_check"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
-dependencies = [
- "lazy_static",
-]
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
 
 [[package]]
-name = "time"
-version = "0.1.42"
+name = "wait-timeout"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
 dependencies = [
  "libc",
- "redox_syscall",
- "winapi 0.3.9",
 ]
 
 [[package]]
-name = "tokio"
-version = "0.1.22"
+name = "walkdir"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
 dependencies = [
- "bytes",
- "futures",
- "mio",
- "num_cpus",
- "tokio-current-thread",
- "tokio-executor",
- "tokio-io",
- "tokio-reactor",
- "tokio-tcp",
- "tokio-threadpool",
- "tokio-timer",
+ "same-file",
+ "winapi 0.3.9",
+ "winapi-util",
 ]
 
 [[package]]
-name = "tokio-buf"
-version = "0.1.1"
+name = "want"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
 dependencies = [
- "bytes",
- "either",
- "futures",
+ "log",
+ "try-lock",
 ]
 
 [[package]]
-name = "tokio-current-thread"
-version = "0.1.6"
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443"
-dependencies = [
- "futures",
- "tokio-executor",
-]
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 
 [[package]]
-name = "tokio-executor"
-version = "0.1.8"
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f27ee0e6db01c5f0b2973824547ce7e637b2ed79b891a9677b0de9bd532b6ac"
-dependencies = [
- "crossbeam-utils",
- "futures",
-]
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
 [[package]]
-name = "tokio-io"
-version = "0.1.12"
+name = "wasm-bindgen"
+version = "0.2.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926"
+checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
 dependencies = [
- "bytes",
- "futures",
- "log",
+ "cfg-if 1.0.0",
+ "serde",
+ "serde_json",
+ "wasm-bindgen-macro",
 ]
 
 [[package]]
-name = "tokio-reactor"
-version = "0.1.9"
+name = "wasm-bindgen-backend"
+version = "0.2.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6af16bfac7e112bea8b0442542161bfc41cbfa4466b580bdda7d18cb88b911ce"
+checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041"
 dependencies = [
- "crossbeam-utils",
- "futures",
+ "bumpalo",
  "lazy_static",
  "log",
- "mio",
- "num_cpus",
- "parking_lot",
- "slab",
- "tokio-executor",
- "tokio-io",
- "tokio-sync",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+ "wasm-bindgen-shared",
 ]
 
 [[package]]
-name = "tokio-sync"
-version = "0.1.6"
+name = "wasm-bindgen-futures"
+version = "0.4.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2162248ff317e2bc713b261f242b69dbb838b85248ed20bb21df56d60ea4cae7"
+checksum = "95fded345a6559c2cfee778d562300c581f7d4ff3edb9b0d230d69800d213972"
 dependencies = [
- "fnv",
- "futures",
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
 ]
 
 [[package]]
-name = "tokio-tcp"
-version = "0.1.3"
+name = "wasm-bindgen-macro"
+version = "0.2.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119"
+checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef"
 dependencies = [
- "bytes",
- "futures",
- "iovec",
- "mio",
- "tokio-io",
- "tokio-reactor",
+ "quote 1.0.9",
+ "wasm-bindgen-macro-support",
 ]
 
 [[package]]
-name = "tokio-threadpool"
-version = "0.1.15"
+name = "wasm-bindgen-macro-support"
+version = "0.2.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ca01319dea1e376a001e8dc192d42ebde6dd532532a5bad988ac37db365b19"
+checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad"
 dependencies = [
- "crossbeam-deque",
- "crossbeam-queue",
- "crossbeam-utils",
- "futures",
- "log",
- "num_cpus",
- "rand 0.6.5",
- "slab",
- "tokio-executor",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
 ]
 
 [[package]]
-name = "tokio-timer"
-version = "0.2.11"
+name = "wasm-bindgen-shared"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
+
+[[package]]
+name = "wayland-client"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e"
+checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355"
 dependencies = [
- "crossbeam-utils",
- "futures",
- "slab",
- "tokio-executor",
+ "bitflags 1.2.1",
+ "downcast-rs",
+ "libc",
+ "nix 0.20.0",
+ "scoped-tls",
+ "wayland-commons",
+ "wayland-scanner",
+ "wayland-sys",
 ]
 
 [[package]]
-name = "try-lock"
-version = "0.2.2"
+name = "wayland-commons"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
+checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda"
+dependencies = [
+ "nix 0.20.0",
+ "once_cell",
+ "smallvec",
+ "wayland-sys",
+]
 
 [[package]]
-name = "try_from"
-version = "0.3.2"
+name = "wayland-cursor"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b"
+checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a"
 dependencies = [
- "cfg-if",
+ "nix 0.20.0",
+ "wayland-client",
+ "xcursor",
 ]
 
 [[package]]
-name = "unicase"
-version = "2.5.1"
+name = "wayland-protocols"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150"
+checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f"
 dependencies = [
- "version_check",
+ "bitflags 1.2.1",
+ "wayland-client",
+ "wayland-commons",
+ "wayland-scanner",
 ]
 
 [[package]]
-name = "unicode-bidi"
-version = "0.3.4"
+name = "wayland-scanner"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1"
 dependencies = [
- "matches",
+ "proc-macro2",
+ "quote 1.0.9",
+ "xml-rs 0.8.3",
 ]
 
 [[package]]
-name = "unicode-normalization"
-version = "0.1.8"
+name = "wayland-sys"
+version = "0.28.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426"
+checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8"
 dependencies = [
- "smallvec",
+ "dlib",
+ "lazy_static",
+ "pkg-config",
 ]
 
 [[package]]
-name = "unicode-width"
-version = "0.1.6"
+name = "web-sys"
+version = "0.3.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
+checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
 
 [[package]]
-name = "unicode-xid"
-version = "0.1.0"
+name = "widestring"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
 
 [[package]]
-name = "unicode-xid"
-version = "0.2.0"
+name = "winapi"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
 
 [[package]]
-name = "url"
-version = "1.7.2"
+name = "winapi"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
 dependencies = [
- "idna 0.1.5",
- "matches",
- "percent-encoding 1.0.1",
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
 ]
 
 [[package]]
-name = "url"
-version = "2.1.0"
+name = "winapi-build"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61"
-dependencies = [
- "idna 0.2.0",
- "matches",
- "percent-encoding 2.1.0",
-]
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 
 [[package]]
-name = "utf-8"
-version = "0.7.5"
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
-name = "uuid"
-version = "0.7.4"
+name = "winapi-util"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
 dependencies = [
- "rand 0.6.5",
+ "winapi 0.3.9",
 ]
 
 [[package]]
-name = "vcpkg"
-version = "0.2.7"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
-name = "vec_map"
-version = "0.8.1"
+name = "windows"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
+checksum = "426842497696b65fbfc575691d94ef65befb248ed1a8c4361e293c724e7ebe61"
+dependencies = [
+ "const-sha1",
+ "windows_gen",
+ "windows_macros",
+ "windows_winmd",
+]
 
 [[package]]
-name = "version_check"
-version = "0.1.5"
+name = "windows_gen"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+checksum = "77ac8f0f06b647f42ee5459a8e1ffe41795647582c5926ec3fa363a91aad7d77"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.9",
+ "squote",
+ "syn 1.0.67",
+ "windows_gen_macros",
+ "windows_winmd",
+]
 
 [[package]]
-name = "walkdir"
-version = "2.2.9"
+name = "windows_gen_macros"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e"
+checksum = "23eac2169a20173b890c496f9e0e1149a92ef29fe4ba96026b72eec363b993f9"
 dependencies = [
- "same-file",
- "winapi 0.3.9",
- "winapi-util",
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
 ]
 
 [[package]]
-name = "want"
-version = "0.2.0"
+name = "windows_macros"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230"
+checksum = "9edc57c944eec106c7823b425ab0fd9f90163489e50a4df747f65fcf9030e1fb"
 dependencies = [
- "futures",
- "log",
- "try-lock",
+ "proc-macro2",
+ "quote 1.0.9",
+ "squote",
+ "syn 1.0.67",
+ "windows_gen",
+ "windows_winmd",
 ]
 
 [[package]]
-name = "wasi"
-version = "0.7.0"
+name = "windows_winmd"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
+checksum = "16d44527d04c9713312ed598f5d6ce3c453754dbfc03ddc376615be4415ffc88"
+dependencies = [
+ "windows_winmd_macros",
+]
 
 [[package]]
-name = "widestring"
-version = "0.4.0"
+name = "windows_winmd_macros"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
+checksum = "2353f43f512938450614a176abf2b6cb31ac3b84fd71c88470fee571303e3f36"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
 
 [[package]]
-name = "winapi"
-version = "0.2.8"
+name = "winreg"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi 0.3.9",
+]
 
 [[package]]
-name = "winapi"
-version = "0.3.9"
+name = "winreg"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+checksum = "16cdb3898397cf7f624c294948669beafaeebc5577d5ec53d0afb76633593597"
 dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
+ "winapi 0.3.9",
 ]
 
 [[package]]
-name = "winapi-build"
-version = "0.1.1"
+name = "winres"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc"
+dependencies = [
+ "toml",
+]
 
 [[package]]
-name = "winapi-i686-pc-windows-gnu"
+name = "winrt"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-util"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
+checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d"
 dependencies = [
  "winapi 0.3.9",
 ]
 
 [[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
+name = "winrt-notification"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+checksum = "6c31a65da50d792c6f9bd2e3216249566c4fb1d2d34f9b7d2d66d2e93f62a242"
+dependencies = [
+ "strum 0.8.0",
+ "strum_macros 0.8.0",
+ "winapi 0.3.9",
+ "winrt",
+ "xml-rs 0.6.1",
+]
 
 [[package]]
-name = "winreg"
-version = "0.6.2"
+name = "winrt-notification"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
+checksum = "e5ee9845acda665033013f93baec4f71ac0e60a391b530a5a83bdb966c1807ca"
 dependencies = [
- "winapi 0.3.9",
+ "strum 0.20.0",
+ "windows",
+ "xml-rs 0.8.3",
 ]
 
 [[package]]
@@ -2212,6 +3265,30 @@ dependencies = [
  "winapi-build",
 ]
 
+[[package]]
+name = "xcursor"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621"
+dependencies = [
+ "bitflags 0.9.1",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
+
 [[package]]
 name = "xml5ever"
 version = "0.16.1"
@@ -2226,22 +3303,37 @@ dependencies = [
 
 [[package]]
 name = "yaml-rust"
-version = "0.4.3"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "git+https://github.com/federico-terzi/yaml-rust#b1a195252fcdabf743f68d03f4d84d151a5a3f62"
 dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
+
 [[package]]
 name = "zip"
-version = "0.5.3"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c21bb410afa2bd823a047f5bda3adb62f51074ac7e06263b2c97ecdd47e9fc6"
+checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
 dependencies = [
+ "byteorder",
  "bzip2",
  "crc32fast",
  "flate2",
- "podio",
+ "thiserror",
  "time",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index f64e3059..f302d450 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,55 +1,21 @@
-[package]
-name = "espanso"
-version = "0.7.3"
-authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
-license = "GPL-3.0"
-description = "Cross-platform Text Expander written in Rust"
-readme = "README.md"
-homepage = "https://github.com/federico-terzi/espanso"
-edition = "2018"
-build="build.rs"
+[workspace]
 
-[modulo]
-version = "0.1.1"
-
-[dependencies]
-widestring = "0.4.0"
-serde = { version = "1.0.117", features = ["derive"] }
-serde_yaml = "0.8"
-dirs = "2.0.2"
-clap = "2.33.0"
-regex = "1.3.1"
-log = "0.4.8"
-simplelog = "0.7.1"
-fs2 = "0.4.3"
-serde_json = "1.0.60"
-log-panics = {version = "2.0.0", features = ["with-backtrace"]}
-backtrace = "0.3.37"
-chrono = "0.4.9"
-lazy_static = "1.4.0"
-walkdir = "2.2.9"
-reqwest = "0.9.20"
-tempfile = "3.1.0"
-dialoguer = "0.4.0"
-rand = "0.7.2"
-zip = "0.5.3"
-notify = "4.0.13"
-markdown = "0.3.0"
-html2text = "0.2.1"
-
-[target.'cfg(unix)'.dependencies]
-libc = "0.2.62"
-signal-hook = "0.1.15"
-
-[target.'cfg(windows)'.dependencies]
-named_pipe = "0.4.1"
-winapi = { version = "0.3.9", features = ["wincon"] }
-
-[build-dependencies]
-cmake = "0.1.31"
-
-[package.metadata.deb]
-maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
-depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
-section = "utility"
-license-file = ["LICENSE", "1"]
\ No newline at end of file
+members = [
+  "espanso",
+  "espanso-detect",
+  "espanso-ui",
+  "espanso-inject",
+  "espanso-ipc",
+  "espanso-config",
+  "espanso-match",
+  "espanso-clipboard",
+  "espanso-render",
+  "espanso-info",
+  "espanso-path",
+  "espanso-modulo",
+  "espanso-migrate",
+  "espanso-mac-utils",
+  "espanso-kvs",
+  "espanso-engine",
+  "espanso-package",
+]
\ No newline at end of file
diff --git a/Compilation.md b/Compilation.md
new file mode 100644
index 00000000..8b936ed1
--- /dev/null
+++ b/Compilation.md
@@ -0,0 +1,67 @@
+# Compilation
+
+This document tries to explain the various steps needed to build espanso. (Work in progress).
+
+# Prerequisites
+
+These are the basic tools required to build espanso:
+
+* A recent Rust compiler. You can install it following these instructions: https://www.rust-lang.org/tools/install
+* A C/C++ compiler. There are multiple of them depending on the platform, but espanso officially supports the following:
+  * On Windows, you should use the MSVC compiler. The easiest way to install it is by downloading Visual Studio and checking "Desktop development with C++" in the installer: https://visualstudio.microsoft.com/
+  * On macOS, you should use the official build tools that come with Xcode. If you don't want to install Xcode, you should be able to download only the build tools by executing `xcode-select —install` and following the instructions.
+  * On Linux, you should use the default C/C++ compiler (it's usually GCC). On Ubuntu/Debian systems, you can install them with `sudo apt install build-essential`
+
+* Espanso heavily relies on [cargo make](https://github.com/sagiegurari/cargo-make) for the various packaging
+steps. You can install it by running:
+
+```
+cargo install --force cargo-make
+```
+
+# Linux
+
+Espanso on Linux comes in two different flavors: one for X11 and one for Wayland. 
+If you don't know which one to choose, follow these steps to determine which one you are running: https://unix.stackexchange.com/a/325972
+
+## Compiling for X11
+
+### Necessary packages
+
+If compiling on Ubuntu X11:
+* `sudo apt install libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev`
+
+### AppImage
+
+The AppImage is a convenient format to distribute Linux applications, as besides the binary, 
+it also bundles all the required libraries.
+
+You can create the AppImage by running (this will work on X11 systems):
+
+```
+cargo make create-app-image --profile release
+```
+
+You will find the resulting AppImage in the `target/linux/AppImage/out` folder.
+
+### Binary
+
+TODO
+
+## Compiling on Wayland
+
+TODO
+
+## Advanced
+
+Espanso offers a few flags that might be necessary if you want to further tune the resulting binary.
+
+### Disabling modulo (GUI features)
+
+Espanso includes a component known as _modulo_, which handles most of the graphical-related parts of the tool.
+For example, the Search bar or Forms are handled by it.
+
+If you don't want them, you can pass the `--env NO_MODULO=true` flag to any of the previous `cargo make` commands
+to remove support for it.
+
+Keep in mind that espanso was designed with modulo as a first class citizen, so the experience might be far from perfect without it.
diff --git a/Makefile.toml b/Makefile.toml
new file mode 100644
index 00000000..2cd02ba1
--- /dev/null
+++ b/Makefile.toml
@@ -0,0 +1,105 @@
+[config]
+default_to_workspace = false
+
+[env]
+DEBUG = true 
+RELEASE = false
+NO_X11 = false
+NO_MODULO = false
+EXEC_PATH = "target/debug/espanso"
+BUILD_ARCH = "current"
+
+[env.release]
+DEBUG = false
+RELEASE = true
+EXEC_PATH = "target/release/espanso"
+
+# Build variants
+
+# This one was written in Rust instead of bash because it has to run on Windows as well
+[tasks.build-binary]
+script_runner = "@rust"
+script = { file = "scripts/build_binary.rs" }
+
+[tasks.run-binary]
+command = "${EXEC_PATH}"
+args = ["${@}"]
+dependencies = ["build-binary"]
+
+[tasks.test-binary]
+script_runner = "@rust"
+script = { file = "scripts/test_binary.rs" }
+
+# Windows
+
+[tasks.build-windows-resources]
+script_runner = "@rust"
+script = { file = "scripts/build_windows_resources.rs" }
+dependencies = ["build-binary"]
+
+[tasks.build-windows-portable]
+script_runner = "@rust"
+script = { file = "scripts/build_windows_portable.rs" }
+dependencies = ["build-windows-resources"]
+
+[tasks.build-windows-installer]
+script_runner = "@rust"
+script = { file = "scripts/build_windows_installer.rs" }
+dependencies = ["build-windows-resources"]
+
+[tasks.build-windows-all]
+dependencies = ["build-windows-portable", "build-windows-installer"]
+
+# macOS
+
+[tasks.build-macos-arm-binary]
+env = { "BUILD_ARCH" = "aarch64-apple-darwin" }
+run_task = [
+  { name = "build-binary" }
+]
+
+[tasks.build-macos-x86-binary]
+env = { "BUILD_ARCH" = "x86_64-apple-darwin" }
+run_task = [
+  { name = "build-binary" }
+]
+
+[tasks.build-universal-binary]
+script = { file = "scripts/join_universal_binary.sh"}
+dependencies=["build-macos-arm-binary", "build-macos-x86-binary"]
+
+[tasks.create-bundle]
+script = { file = "scripts/create_bundle.sh" }
+dependencies=["build-binary"]
+
+[tasks.create-universal-bundle]
+env = { "EXEC_PATH" = "target/universal/espanso" }
+script = { file = "scripts/create_bundle.sh" }
+dependencies=["build-universal-binary"]
+
+[tasks.run-bundle]
+command="target/mac/Espanso.app/Contents/MacOS/espanso"
+args=["${@}"]
+dependencies=["create-bundle"]
+
+# Linux
+
+[tasks.create-app-image]
+script = { file = "scripts/create_app_image.sh" }
+dependencies=["build-binary"]
+
+[tasks.run-app-image]
+args=["${@}"]
+script='''
+#!/usr/bin/env bash
+set -e
+echo Launching AppImage with args: "$@"
+./target/linux/AppImage/out/Espanso-*.AppImage "$@"
+'''
+dependencies=["create-app-image"]
+
+# Test runs
+
+[tasks.test-output]
+command = "cargo"
+args = ["test", "--workspace", "--exclude", "espanso-modulo", "--exclude", "espanso-ipc", "--no-default-features", "--", "--nocapture"]
\ No newline at end of file
diff --git a/README.md b/README.md
index d696434e..7a23f29b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-![espanso](images/titlebar.png)
+![espanso](images/logo_extended.png)
 
 > A cross-platform Text Expander written in Rust
 
@@ -6,7 +6,6 @@
 ![Language](https://img.shields.io/badge/language-rust-orange)
 ![Platforms](https://img.shields.io/badge/platforms-Windows%2C%20macOS%20and%20Linux-blue)
 ![License](https://img.shields.io/github/license/federico-terzi/espanso)
-[![Build Status](https://dev.azure.com/freddy6896/espanso/_apis/build/status/federico-terzi.espanso?branchName=master)](https://dev.azure.com/freddy6896/espanso/_build/latest?definitionId=1&branchName=master)
 
 ![example](images/example.gif)
 
@@ -30,6 +29,7 @@ ___
 * Works with almost **any program**
 * Works with **Emojis** 😄
 * Works with **Images**
+* Includes a powerful **Search Bar** 🔎
 * **Date** expansion support
 * **Custom scripts** support
 * **Shell commands** support
@@ -38,6 +38,8 @@ ___
 * Expandable with **packages**
 * Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
 * File based configuration
+* Support Regex triggers
+* Experimental Wayland support
 
 ## Get Started
 
@@ -60,20 +62,17 @@ please consider making a small donation, it really helps :)
 
 ## Contributors
 
-Many people helped the project along the way, thanks to all of you. In particular, I want to thank: 
+Many people helped the project along the way, thank you to all of you!
 
-* [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru
-* [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester
-* [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester
-* [Timo Runge](http://timorunge.com/) - MacOS contributor
-* [NickSeagull](http://nickseagull.github.io/) - Contributor
-* [matt-h](https://github.com/matt-h) - Contributor
+<a href="https://github.com/federico-terzi/espanso/graphs/contributors">
+  <img src="https://contrib.rocks/image?repo=federico-terzi/espanso" />
+</a>
 
 ## Remarks
 
 * Thanks to [libxdo](https://github.com/jordansissel/xdotool) and [xclip](https://github.com/astrand/xclip), used to implement the Linux port.
-* Thanks to the ModifyPath
-  script, used by espanso to improve the Windows installer.
+* Thanks to [libxkbcommon](https://xkbcommon.org/) and [wl-clipboard](https://github.com/bugaevc/wl-clipboard), used to implement the Wayland port.
+* Thanks to [wxWidgets](https://www.wxwidgets.org/) for providing a powerful cross-platform GUI library.
 
 ## License
 
diff --git a/SECURITY.md b/SECURITY.md
index 8e57bc62..8f1c0e3f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,8 @@
+> TODO: this document is relative to version 1 and will be updated soon for changes introduced in version 2
+>
+> Despite significant architectural differences, the following points are still a good approximation
+> of the internals.
+
 # Security
 
 Espanso has always been designed with a strong focus on security. 
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 70e8c3b7..00000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-# Starter pipeline
-# Start with a minimal pipeline that you can customize to build and deploy your code.
-# Add steps that build, run tests, deploy, and more:
-# https://aka.ms/yaml
-
-trigger:
-- master
-
-jobs:
-  - job: Linux
-    pool:
-      vmImage: 'ubuntu-latest'
-    steps:
-      - script: |
-          sudo apt -y update
-          sudo apt install -y libxtst-dev libx11-dev libxdo3 libxdo-dev
-        displayName: Install library dependencies
-      - template: ci/test.yml
-      - template: ci/build-linux.yml
-      - template: ci/deploy.yml
-
-  - job: UbuntuDEB
-    pool:
-      vmImage: 'ubuntu-latest'
-    steps:
-      - script: |
-          sudo docker build -t espanso-ubuntu . -f ci/ubuntu/Dockerfile
-          sudo docker run --rm -v "$(pwd):/shared" espanso-ubuntu espanso/ci/ubuntu/build_deb.sh
-        displayName: Setting up docker
-      - template: ci/deploy.yml
-
-  - job: macOS
-    pool:
-      vmImage: 'macOS-10.14'
-    steps:
-      - template: ci/test.yml
-      - template: ci/build-macos.yml
-      - template: ci/deploy.yml
-      - template: ci/publish-homebrew.yml
-
-  - job: Windows
-    pool:
-      vmImage: 'windows-2019'
-    steps:
-      - template: ci/test.yml
-      - template: ci/build-win.yml
-      - template: ci/deploy.yml
\ No newline at end of file
diff --git a/build.rs b/build.rs
deleted file mode 100644
index 433fb3e3..00000000
--- a/build.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-extern crate cmake;
-use cmake::Config;
-use std::path::PathBuf;
-
-/* OS SPECIFIC CONFIGS */
-
-#[cfg(target_os = "windows")]
-fn get_config() -> PathBuf {
-    Config::new("native/libwinbridge").build()
-}
-
-#[cfg(target_os = "linux")]
-fn get_config() -> PathBuf {
-    Config::new("native/liblinuxbridge").build()
-}
-
-#[cfg(target_os = "macos")]
-fn get_config() -> PathBuf {
-    Config::new("native/libmacbridge").build()
-}
-
-/*
-    OS CUSTOM CARGO CONFIG LINES
-    Note: this is where linked libraries should be specified.
-*/
-
-#[cfg(target_os = "windows")]
-fn print_config() {
-    println!("cargo:rustc-link-lib=static=winbridge");
-    println!("cargo:rustc-link-lib=dylib=user32");
-    #[cfg(target_env = "gnu")]
-    println!("cargo:rustc-link-lib=dylib=gdiplus");
-    #[cfg(target_env = "gnu")]
-    println!("cargo:rustc-link-lib=dylib=stdc++");
-}
-
-#[cfg(target_os = "linux")]
-fn print_config() {
-    println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
-    println!("cargo:rustc-link-lib=static=linuxbridge");
-    println!("cargo:rustc-link-lib=dylib=X11");
-    println!("cargo:rustc-link-lib=dylib=Xtst");
-    println!("cargo:rustc-link-lib=dylib=xdo");
-}
-
-#[cfg(target_os = "macos")]
-fn print_config() {
-    println!("cargo:rustc-link-lib=dylib=c++");
-    println!("cargo:rustc-link-lib=static=macbridge");
-    println!("cargo:rustc-link-lib=framework=Cocoa");
-    println!("cargo:rustc-link-lib=framework=IOKit");
-}
-
-fn main() {
-    let dst = get_config();
-
-    println!("cargo:rustc-link-search=native={}", dst.display());
-    print_config();
-}
diff --git a/ci/build-linux.yml b/ci/build-linux.yml
deleted file mode 100644
index 2e43ae50..00000000
--- a/ci/build-linux.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-steps:
-  - script: |
-      cargo build --release
-      cd target/release/
-      tar czf "espanso-linux.tar.gz" espanso
-      cd ../..
-      cp target/release/espanso-*.gz .
-      sha256sum espanso-*.gz | awk '{ print $1 }' > espanso-linux-sha256.txt
-      ls -la
-    displayName: "Cargo build and packaging for Linux"
-
diff --git a/ci/build-macos.yml b/ci/build-macos.yml
deleted file mode 100644
index 4135ab49..00000000
--- a/ci/build-macos.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.7'
-      addToPath: true
-
-  - script: |
-      python --version
-      python -m pip install toml click
-    displayName: Installing python dependencies
-
-  - script: |
-      set -e
-      python packager.py build
-      cp target/packager/mac/espanso-*.gz .
-      cp target/packager/mac/espanso-*.txt .
-      cp target/packager/mac/espanso.rb .
-      ls -la
-    displayName: "Cargo build and packaging for MacOS"
\ No newline at end of file
diff --git a/ci/build-win.yml b/ci/build-win.yml
deleted file mode 100644
index 93aa37eb..00000000
--- a/ci/build-win.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.7'
-      addToPath: true
-
-  - script: |
-      python --version
-      python -m pip install toml click
-    displayName: Installing python dependencies
-
-  - script: |
-      python packager.py build
-      copy "target\\packager\\win\\espanso-win-installer.exe" "espanso-win-installer.exe"
-      copy "target\\packager\\win\\espanso-win-installer-sha256.txt" "espanso-win-installer-sha256.txt"
-      dir
-    displayName: "Build and packaging for Windows"
-
diff --git a/ci/deploy.yml b/ci/deploy.yml
deleted file mode 100644
index 7da24953..00000000
--- a/ci/deploy.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-parameters:
-  github:
-    isPreRelease: false
-    repositoryName: '$(Build.Repository.Name)'
-    gitHubConnection: "MyGithubConnection"
-  dependsOn: []
-  displayName: "Release to github"
-
-steps:
-  - script: |
-      VER=$(cat Cargo.toml| grep version -m 1 | awk -F '"' '{ print $2 }')
-      echo '##vso[task.setvariable variable=vers]'v$VER
-    condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
-    displayName: Obtain version from Cargo.toml on Unix
-
-  - powershell: |
-      Select-String -Path "Cargo.toml" -Pattern "version" | Select-Object -First 1 -outvariable v
-      $vv = [regex]::match($v, '"([^"]+)"').Groups[1].Value
-      echo "##vso[task.setvariable variable=vers]v$vv"
-    condition: eq(variables['Agent.OS'], 'Windows_NT')
-    displayName: Obtain version from Cargo.toml on Windows
-
-  - task: GitHubRelease@0
-    displayName: Create GitHub release
-    inputs:
-      gitHubConnection: ${{ parameters.github.gitHubConnection }}
-      tagSource: manual
-      title: '$(vers)'
-      tag: '$(vers)'
-      assetUploadMode: replace
-      action: edit
-      assets: 'espanso-*'
-      addChangeLog: false
-      repositoryName: ${{ parameters.github.repositoryName }}
-      isPreRelease: ${{ parameters.github.isPreRelease }}
-    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
\ No newline at end of file
diff --git a/ci/install-rust.yml b/ci/install-rust.yml
deleted file mode 100644
index 163eda8c..00000000
--- a/ci/install-rust.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-# defaults for any parameters that aren't specified
-parameters:
-  rust_version: stable
-
-steps:
-  # Linux and macOS.
-  - script: |
-      set -e
-      curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN
-      echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin"
-    env:
-      RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
-    displayName: "Install rust (*nix)"
-    condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
-  # Windows.
-  - script: |
-      curl -sSf -o rustup-init.exe https://win.rustup.rs
-      rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc
-      set PATH=%PATH%;%USERPROFILE%\.cargo\bin
-      echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
-    env:
-      RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
-    displayName: "Install rust (windows)"
-    condition: eq(variables['Agent.OS'], 'Windows_NT')
-  # Install additional components:
-  - ${{ each component in parameters.components }}:
-      - script: rustup component add ${{ component }}
-
-  # All platforms.
-  - script: |
-      rustup -V
-      rustup component list --installed
-      rustc -Vv
-      cargo -V
-    displayName: Query rust and cargo versions
\ No newline at end of file
diff --git a/ci/publish-homebrew.yml b/ci/publish-homebrew.yml
deleted file mode 100644
index 5936281b..00000000
--- a/ci/publish-homebrew.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-steps:
-  - task: InstallSSHKey@0
-    inputs:
-      knownHostsEntry: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
-      sshPublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsB9zcHN84/T5URAsfIpb52HnJl2kUK7WWXyV9pFXaO6yz722JxzVq56J3TTrcUCDhM3DKSGKivB6n/tmLw4mefcY3t7kh8puAtaNrNnB4TWqVPFHZtnpYuYslp1rM92r7Bz1FHfVfsDZxqSWlGU/lp0gNEEgXbr2PCExbCh3TGTsKePARhMAtPEvyEZk1+8uA/HvUTjhuDp7P+BbejAsqtgVF0QoEvqDE5af8DZY6+i1cHRgwBYgSnOus8FHsZUGMyAJQtb+dD7imGw/nzokPJzbmQJwQetyhp52CfThpAm12EFtIU43imb8nndlVAmsIHF6czbmI5LP3U0UcTLct freddy@freddy-Z97M-DS3H"
-      sshKeySecureFile: "azuressh"
-
-  - script: |
-      set -ex
-      cat ~/.ssh/known_hosts
-      git config --global user.email "federicoterzi96@gmail.com"
-      git config --global user.email "Federico Terzi"
-      VER=$(cat Cargo.toml| grep version -m 1 | awk -F '"' '{ print $2 }')
-      git clone git@github.com:federico-terzi/homebrew-espanso.git
-      rm homebrew-espanso/Formula/espanso.rb
-      cp espanso.rb homebrew-espanso/Formula/espanso.rb
-      cd homebrew-espanso
-      git add -A
-      git commit -m "Update to version: $VER"
-      git push
-    displayName: "Publishing to Homebrew"
-    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
\ No newline at end of file
diff --git a/ci/ubuntu/build_deb.sh b/ci/ubuntu/build_deb.sh
deleted file mode 100755
index 7a298083..00000000
--- a/ci/ubuntu/build_deb.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-
-echo "Testing espanso..."
-cd espanso
-cargo test --release
-
-echo "Building espanso and packaging deb"
-cargo deb
-
-cd ..
-cp espanso/target/debian/espanso*.deb espanso-debian-amd64.deb
-sha256sum espanso-debian-amd64.deb > espanso-debian-amd64-sha256.txt
-ls -la
-
-echo "Copying to mounted volume"
-cp espanso-debian-* /shared
diff --git a/espanso-clipboard/Cargo.toml b/espanso-clipboard/Cargo.toml
new file mode 100644
index 00000000..b3896136
--- /dev/null
+++ b/espanso-clipboard/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "espanso-clipboard"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[features]
+# If the wayland feature is enabled, all X11 dependencies will be dropped
+# and wayland support will be enabled
+wayland = ["wait-timeout"]
+
+# If enabled, avoid linking with the gdiplus library on Windows, which
+# might conflict with wxWidgets
+avoid-gdi = []
+
+[dependencies]
+log = "0.4.14"
+lazycell = "1.3.0"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+lazy_static = "1.4.0"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+
+[target.'cfg(target_os = "linux")'.dependencies]
+wait-timeout = { version = "0.2.0", optional = true }
+
+[build-dependencies]
+cc = "1.0.66"
\ No newline at end of file
diff --git a/espanso-clipboard/build.rs b/espanso-clipboard/build.rs
new file mode 100644
index 00000000..90c0bfc4
--- /dev/null
+++ b/espanso-clipboard/build.rs
@@ -0,0 +1,82 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/win32/native.cpp");
+  println!("cargo:rerun-if-changed=src/win32/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/win32/native.h")
+    .file("src/win32/native.cpp")
+    .compile("espansoclipboard");
+
+  println!("cargo:rustc-link-lib=static=espansoclipboard");
+  println!("cargo:rustc-link-lib=dylib=user32");
+  println!("cargo:rustc-link-lib=dylib=gdi32");
+
+  if cfg!(not(feature = "avoid-gdi")) {
+    println!("cargo:rustc-link-lib=dylib=gdiplus");
+  }
+
+  #[cfg(target_env = "gnu")]
+  println!("cargo:rustc-link-lib=dylib=stdc++");
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+  if cfg!(not(feature = "wayland")) {
+    println!("cargo:rerun-if-changed=src/x11/native/native.h");
+    println!("cargo:rerun-if-changed=src/x11/native/native.c");
+    cc::Build::new()
+      .cpp(true)
+      .include("src/x11/native/clip")
+      .include("src/x11/native")
+      .file("src/x11/native/clip/clip.cpp")
+      .file("src/x11/native/clip/clip_x11.cpp")
+      .file("src/x11/native/clip/image.cpp")
+      .file("src/x11/native/native.cpp")
+      .compile("espansoclipboardx11");
+
+    println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
+    println!("cargo:rustc-link-lib=static=espansoclipboardx11");
+    println!("cargo:rustc-link-lib=dylib=xcb");
+    println!("cargo:rustc-link-lib=dylib=stdc++");
+  } else {
+    // Nothing to compile on wayland
+  }
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/cocoa/native.mm");
+  println!("cargo:rerun-if-changed=src/cocoa/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/cocoa/native.h")
+    .file("src/cocoa/native.mm")
+    .compile("espansoclipboard");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansoclipboard");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-clipboard/src/cocoa/ffi.rs b/espanso-clipboard/src/cocoa/ffi.rs
new file mode 100644
index 00000000..9528786f
--- /dev/null
+++ b/espanso-clipboard/src/cocoa/ffi.rs
@@ -0,0 +1,28 @@
+/*
+ * 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::os::raw::c_char;
+
+#[link(name = "espansoclipboard", kind = "static")]
+extern "C" {
+  pub fn clipboard_get_text(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn clipboard_set_text(text: *const c_char) -> i32;
+  pub fn clipboard_set_image(image_path: *const c_char) -> i32;
+  pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const c_char) -> i32;
+}
diff --git a/espanso-clipboard/src/cocoa/mod.rs b/espanso-clipboard/src/cocoa/mod.rs
new file mode 100644
index 00000000..d8425921
--- /dev/null
+++ b/espanso-clipboard/src/cocoa/mod.rs
@@ -0,0 +1,103 @@
+/*
+ * 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/>.
+ */
+
+mod ffi;
+
+use std::{
+  ffi::{CStr, CString},
+  path::PathBuf,
+};
+
+use crate::Clipboard;
+use anyhow::Result;
+use log::error;
+use thiserror::Error;
+
+pub struct CocoaClipboard {}
+
+impl CocoaClipboard {
+  pub fn new() -> Result<Self> {
+    Ok(Self {})
+  }
+}
+
+impl Clipboard for CocoaClipboard {
+  fn get_text(&self) -> Option<String> {
+    let mut buffer: [i8; 2048] = [0; 2048];
+    let native_result =
+      unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
+    if native_result > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      Some(string.to_string_lossy().to_string())
+    } else {
+      None
+    }
+  }
+
+  fn set_text(&self, text: &str) -> anyhow::Result<()> {
+    let string = CString::new(text)?;
+    let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(CocoaClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> {
+    if !image_path.exists() || !image_path.is_file() {
+      return Err(CocoaClipboardError::ImageNotFound(image_path.to_path_buf()).into());
+    }
+
+    let path = CString::new(image_path.to_string_lossy().to_string())?;
+    let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) };
+
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(CocoaClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> {
+    let html_string = CString::new(html)?;
+    let fallback_string = CString::new(fallback_text.unwrap_or_default())?;
+    let fallback_ptr = if fallback_text.is_some() {
+      fallback_string.as_ptr()
+    } else {
+      std::ptr::null()
+    };
+
+    let native_result = unsafe { ffi::clipboard_set_html(html_string.as_ptr(), fallback_ptr) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(CocoaClipboardError::SetOperationFailed().into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum CocoaClipboardError {
+  #[error("clipboard set operation failed")]
+  SetOperationFailed(),
+
+  #[error("image not found: `{0}`")]
+  ImageNotFound(PathBuf),
+}
diff --git a/espanso-clipboard/src/cocoa/native.h b/espanso-clipboard/src/cocoa/native.h
new file mode 100644
index 00000000..f22c0e80
--- /dev/null
+++ b/espanso-clipboard/src/cocoa/native.h
@@ -0,0 +1,30 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_CLIPBOARD_H
+#define ESPANSO_CLIPBOARD_H
+
+#include <stdint.h>
+
+extern "C" int32_t clipboard_get_text(char * buffer, int32_t buffer_size);
+extern "C" int32_t clipboard_set_text(char * text);
+extern "C" int32_t clipboard_set_image(char * image_path);
+extern "C" int32_t clipboard_set_html(char * html, char * fallback_text);
+
+#endif //ESPANSO_CLIPBOARD_H
\ No newline at end of file
diff --git a/espanso-clipboard/src/cocoa/native.mm b/espanso-clipboard/src/cocoa/native.mm
new file mode 100644
index 00000000..b60f56d1
--- /dev/null
+++ b/espanso-clipboard/src/cocoa/native.mm
@@ -0,0 +1,87 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+#include <string.h>
+
+int32_t clipboard_get_text(char * buffer, int32_t buffer_size) {
+  NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+  for (id element in pasteboard.pasteboardItems) {
+    NSString *string = [element stringForType: NSPasteboardTypeString];
+    if (string != NULL) {
+      const char * text = [string UTF8String];
+      strncpy(buffer, text, buffer_size);
+
+      [string release];
+
+      return 1;
+    }
+  }
+  return 0;
+}
+
+int32_t clipboard_set_text(char * text) {
+  NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+  NSArray *array = @[NSPasteboardTypeString];
+  [pasteboard declareTypes:array owner:nil];
+
+  NSString *nsText = [NSString stringWithUTF8String:text];
+  [pasteboard setString:nsText forType:NSPasteboardTypeString];
+}
+
+int32_t clipboard_set_image(char * image_path) {
+  NSString *pathString = [NSString stringWithUTF8String:image_path];
+  NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString];
+  int result = 0;
+
+  if (image != nil) {
+      NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+      [pasteboard clearContents];
+      NSArray *copiedObjects = [NSArray arrayWithObject:image];
+      [pasteboard writeObjects:copiedObjects];
+      result = 1;
+  }
+  [image release];
+
+  return result;
+}
+
+int32_t clipboard_set_html(char * html, char * fallback_text) {
+  NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+  NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString];
+  [pasteboard declareTypes:array owner:nil];
+
+  NSString *nsHtml = [NSString stringWithUTF8String:html];
+  NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil];
+  NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil];
+
+  NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length])
+                                  documentAttributes:nil];
+
+  [pasteboard setData:rtf forType:NSRTFPboardType];
+
+  if (fallback_text) {
+    NSString *nsText = [NSString stringWithUTF8String:fallback_text];
+    [pasteboard setString:nsText forType:NSPasteboardTypeString];
+  }
+  
+  return 1;
+}
\ No newline at end of file
diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs
new file mode 100644
index 00000000..077728b5
--- /dev/null
+++ b/espanso-clipboard/src/lib.rs
@@ -0,0 +1,97 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use log::info;
+
+#[cfg(target_os = "windows")]
+mod win32;
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+mod x11;
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+mod wayland;
+
+#[cfg(target_os = "macos")]
+mod cocoa;
+
+pub trait Clipboard {
+  fn get_text(&self) -> Option<String>;
+  fn set_text(&self, text: &str) -> Result<()>;
+  fn set_image(&self, image_path: &Path) -> Result<()>;
+  fn set_html(&self, html: &str, fallback_text: Option<&str>) -> Result<()>;
+}
+
+#[allow(dead_code)]
+pub struct ClipboardOptions {
+  // Wayland-only
+  // The number of milliseconds the wl-clipboard commands are allowed
+  // to run before triggering a time-out event.
+  wayland_command_timeout_ms: u64,
+}
+
+impl Default for ClipboardOptions {
+  fn default() -> Self {
+    Self {
+      wayland_command_timeout_ms: 2000,
+    }
+  }
+}
+
+#[cfg(target_os = "windows")]
+pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
+  info!("using Win32Clipboard");
+  Ok(Box::new(win32::Win32Clipboard::new()?))
+}
+
+#[cfg(target_os = "macos")]
+pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
+  info!("using CocoaClipboard");
+  Ok(Box::new(cocoa::CocoaClipboard::new()?))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
+  info!("using X11NativeClipboard");
+  Ok(Box::new(x11::native::X11NativeClipboard::new()?))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+pub fn get_clipboard(options: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
+  // TODO: On some Wayland compositors (currently sway), the "wlr-data-control" protocol
+  // could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
+  // Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8
+  //
+  // We could even decide the correct implementation at runtime by checking if the
+  // required protocol is available, if so use the efficient implementation
+  // instead of the fallback one, which calls the wl-copy and wl-paste binaries, and is thus
+  // less efficient
+
+  info!("using WaylandFallbackClipboard");
+  Ok(Box::new(wayland::fallback::WaylandFallbackClipboard::new(
+    options,
+  )?))
+}
diff --git a/espanso-clipboard/src/wayland/README.md b/espanso-clipboard/src/wayland/README.md
new file mode 100644
index 00000000..84dd2478
--- /dev/null
+++ b/espanso-clipboard/src/wayland/README.md
@@ -0,0 +1,33 @@
+# Notes on Wayland and clipboard support
+
+### Running espanso as another user
+
+When running espanso as another user, we need to set up a couple of permissions
+in order to enable the clipboard tools to correctly connect to the Wayland desktop.
+
+In particular, we need to add the `espanso` user to the same group as the current user
+so that it can access the `/run/user/X` directory (with X depending on the user).
+
+```
+# Find the current user wayland dir with
+echo $XDG_RUNTIME_DIR  # in my case output: /run/user/1000
+
+ls -la /run/user/1000
+
+# Now add the `espanso` user to the current user group
+sudo usermod -a -G freddy espanso
+
+# Give permissions to the group
+chmod g+rwx /run/user/1000
+
+# Give write permission to the wayland socket
+chmod g+w /run/user/1000/wayland-0
+```
+
+Now the clipboard should work as expected
+
+## Better implementation
+
+On some Wayland compositors (currently sway), the "wlr-data-control" protocol could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
+
+Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8
\ No newline at end of file
diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs
new file mode 100644
index 00000000..157bdd59
--- /dev/null
+++ b/espanso-clipboard/src/wayland/fallback/mod.rs
@@ -0,0 +1,201 @@
+/*
+ * 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::{
+  io::{Read, Write},
+  os::unix::net::UnixStream,
+  path::PathBuf,
+  process::Stdio,
+};
+
+use crate::{Clipboard, ClipboardOptions};
+use anyhow::Result;
+use log::error;
+use std::process::Command;
+use thiserror::Error;
+use wait_timeout::ChildExt;
+
+pub(crate) struct WaylandFallbackClipboard {
+  command_timeout: u64,
+}
+
+impl WaylandFallbackClipboard {
+  pub fn new(options: ClipboardOptions) -> Result<Self> {
+    // Make sure wl-paste and wl-copy are available
+    if Command::new("wl-paste").arg("--version").output().is_err() {
+      error!("unable to call 'wl-paste' binary, please install the wl-clipboard package.");
+      return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
+    }
+    if Command::new("wl-copy").arg("--version").output().is_err() {
+      error!("unable to call 'wl-copy' binary, please install the wl-clipboard package.");
+      return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
+    }
+
+    // Try to connect to the wayland display
+    let wayland_socket = if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
+      PathBuf::from(runtime_dir).join("wayland-0")
+    } else {
+      error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard");
+      return Err(WaylandFallbackClipboardError::MissingEnvVariable().into());
+    };
+    if UnixStream::connect(wayland_socket).is_err() {
+      error!("failed to connect to Wayland display");
+      return Err(WaylandFallbackClipboardError::ConnectionFailed().into());
+    }
+
+    Ok(Self {
+      command_timeout: options.wayland_command_timeout_ms,
+    })
+  }
+}
+
+impl Clipboard for WaylandFallbackClipboard {
+  fn get_text(&self) -> Option<String> {
+    let timeout = std::time::Duration::from_millis(self.command_timeout);
+    match Command::new("wl-paste")
+      .arg("--no-newline")
+      .stdout(Stdio::piped())
+      .spawn()
+    {
+      Ok(mut child) => match child.wait_timeout(timeout) {
+        Ok(status_code) => {
+          if let Some(status) = status_code {
+            if status.success() {
+              if let Some(mut io) = child.stdout {
+                let mut output = Vec::new();
+                io.read_to_end(&mut output).ok()?;
+                Some(String::from_utf8_lossy(&output).to_string())
+              } else {
+                None
+              }
+            } else {
+              error!("error, wl-paste exited with non-zero exit code");
+              None
+            }
+          } else {
+            error!("error, wl-paste has timed-out, killing the process");
+            if child.kill().is_err() {
+              error!("unable to kill wl-paste");
+            }
+            None
+          }
+        }
+        Err(err) => {
+          error!("error while executing 'wl-paste': {}", err);
+          None
+        }
+      },
+      Err(err) => {
+        error!("could not invoke 'wl-paste': {}", err);
+        None
+      }
+    }
+  }
+
+  fn set_text(&self, text: &str) -> anyhow::Result<()> {
+    self.invoke_command_with_timeout(&mut Command::new("wl-copy"), text.as_bytes(), "wl-copy")
+  }
+
+  fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> {
+    if !image_path.exists() || !image_path.is_file() {
+      return Err(WaylandFallbackClipboardError::ImageNotFound(image_path.to_path_buf()).into());
+    }
+
+    // Load the image data
+    let mut file = std::fs::File::open(image_path)?;
+    let mut data = Vec::new();
+    file.read_to_end(&mut data)?;
+
+    self.invoke_command_with_timeout(
+      &mut Command::new("wl-copy").arg("--type").arg("image/png"),
+      &data,
+      "wl-copy",
+    )
+  }
+
+  fn set_html(&self, html: &str, _fallback_text: Option<&str>) -> anyhow::Result<()> {
+    self.invoke_command_with_timeout(
+      &mut Command::new("wl-copy").arg("--type").arg("text/html"),
+      html.as_bytes(),
+      "wl-copy",
+    )
+  }
+}
+
+impl WaylandFallbackClipboard {
+  fn invoke_command_with_timeout(
+    &self,
+    command: &mut Command,
+    data: &[u8],
+    name: &str,
+  ) -> Result<()> {
+    let timeout = std::time::Duration::from_millis(self.command_timeout);
+    match command.stdin(Stdio::piped()).spawn() {
+      Ok(mut child) => {
+        if let Some(stdin) = child.stdin.as_mut() {
+          stdin.write_all(data)?;
+        }
+        match child.wait_timeout(timeout) {
+          Ok(status_code) => {
+            if let Some(status) = status_code {
+              if status.success() {
+                Ok(())
+              } else {
+                error!("error, {} exited with non-zero exit code", name);
+                Err(WaylandFallbackClipboardError::SetOperationFailed().into())
+              }
+            } else {
+              error!("error, {} has timed-out, killing the process", name);
+              if child.kill().is_err() {
+                error!("unable to kill {}", name);
+              }
+              Err(WaylandFallbackClipboardError::SetOperationFailed().into())
+            }
+          }
+          Err(err) => {
+            error!("error while executing '{}': {}", name, err);
+            Err(WaylandFallbackClipboardError::SetOperationFailed().into())
+          }
+        }
+      }
+      Err(err) => {
+        error!("could not invoke '{}': {}", name, err);
+        Err(WaylandFallbackClipboardError::SetOperationFailed().into())
+      }
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub(crate) enum WaylandFallbackClipboardError {
+  #[error("wl-clipboard binaries are missing")]
+  MissingWLClipboard(),
+
+  #[error("missing XDG_RUNTIME_DIR env variable")]
+  MissingEnvVariable(),
+
+  #[error("can't connect to Wayland display")]
+  ConnectionFailed(),
+
+  #[error("clipboard set operation failed")]
+  SetOperationFailed(),
+
+  #[error("image not found: `{0}`")]
+  ImageNotFound(PathBuf),
+}
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h b/espanso-clipboard/src/wayland/mod.rs
similarity index 80%
rename from other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h
rename to espanso-clipboard/src/wayland/mod.rs
index dbe09dff..68603c4a 100644
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h
+++ b/espanso-clipboard/src/wayland/mod.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -17,10 +17,4 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-#import <Cocoa/Cocoa.h>
-
-@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate>
-
-
-@end
-
+pub(crate) mod fallback;
diff --git a/espanso-clipboard/src/win32/ffi.rs b/espanso-clipboard/src/win32/ffi.rs
new file mode 100644
index 00000000..601d8a80
--- /dev/null
+++ b/espanso-clipboard/src/win32/ffi.rs
@@ -0,0 +1,28 @@
+/*
+ * 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::os::raw::c_char;
+
+#[link(name = "espansoclipboard", kind = "static")]
+extern "C" {
+  pub fn clipboard_get_text(buffer: *mut u16, buffer_size: i32) -> i32;
+  pub fn clipboard_set_text(text: *const u16) -> i32;
+  pub fn clipboard_set_image(image_path: *const u16) -> i32;
+  pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const u16) -> i32;
+}
diff --git a/espanso-clipboard/src/win32/mod.rs b/espanso-clipboard/src/win32/mod.rs
new file mode 100644
index 00000000..c4a31433
--- /dev/null
+++ b/espanso-clipboard/src/win32/mod.rs
@@ -0,0 +1,147 @@
+/*
+ * 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/>.
+ */
+
+mod ffi;
+
+use std::{ffi::CString, path::PathBuf};
+
+use crate::Clipboard;
+use anyhow::Result;
+use log::error;
+use thiserror::Error;
+use widestring::{U16CStr, U16CString};
+
+pub struct Win32Clipboard {}
+
+impl Win32Clipboard {
+  pub fn new() -> Result<Self> {
+    Ok(Self {})
+  }
+}
+
+impl Clipboard for Win32Clipboard {
+  fn get_text(&self) -> Option<String> {
+    let mut buffer: [u16; 2048] = [0; 2048];
+    let native_result =
+      unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
+    if native_result > 0 {
+      let string = unsafe { U16CStr::from_ptr_str(buffer.as_ptr()) };
+      Some(string.to_string_lossy())
+    } else {
+      None
+    }
+  }
+
+  fn set_text(&self, text: &str) -> anyhow::Result<()> {
+    let string = U16CString::from_str(text)?;
+    let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(Win32ClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> {
+    if !image_path.exists() || !image_path.is_file() {
+      return Err(Win32ClipboardError::ImageNotFound(image_path.to_path_buf()).into());
+    }
+
+    let path = U16CString::from_os_str(image_path.as_os_str())?;
+    let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) };
+
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(Win32ClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> {
+    let html_descriptor = generate_html_descriptor(html);
+    let html_string = CString::new(html_descriptor)?;
+    let fallback_string = U16CString::from_str(fallback_text.unwrap_or_default())?;
+    let fallback_ptr = if fallback_text.is_some() {
+      fallback_string.as_ptr()
+    } else {
+      std::ptr::null()
+    };
+
+    let native_result = unsafe { ffi::clipboard_set_html(html_string.as_ptr(), fallback_ptr) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(Win32ClipboardError::SetOperationFailed().into())
+    }
+  }
+}
+
+fn generate_html_descriptor(html: &str) -> String {
+  // In order to set the HTML clipboard, we have to create a prefix with a specific format
+  // For more information, look here:
+  // https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
+  // https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
+  let content = format!("<!--StartFragment-->{}<!--EndFragment-->", html);
+
+  let tokens = vec![
+    "Version:0.9",
+    "StartHTML:<<STR*#>",
+    "EndHTML:<<END*#>",
+    "StartFragment:<<SFG#*>",
+    "EndFragment:<<EFG#*>",
+    "<html>",
+    "<body>",
+    &content,
+    "</body>",
+    "</html>",
+  ];
+
+  let mut render = tokens.join("\r\n");
+
+  // Now replace the placeholders with the actual positions
+  render = render.replace(
+    "<<STR*#>",
+    &format!("{:0>8}", render.find("<html>").unwrap_or_default()),
+  );
+  render = render.replace("<<END*#>", &format!("{:0>8}", render.len()));
+  render = render.replace(
+    "<<SFG#*>",
+    &format!(
+      "{:0>8}",
+      render.find("<!--StartFragment-->").unwrap_or_default() + "<!--StartFragment-->".len()
+    ),
+  );
+  render = render.replace(
+    "<<EFG#*>",
+    &format!(
+      "{:0>8}",
+      render.find("<!--EndFragment-->").unwrap_or_default()
+    ),
+  );
+  render
+}
+
+#[derive(Error, Debug)]
+pub enum Win32ClipboardError {
+  #[error("clipboard set operation failed")]
+  SetOperationFailed(),
+
+  #[error("image not found: `{0}`")]
+  ImageNotFound(PathBuf),
+}
diff --git a/espanso-clipboard/src/win32/native.cpp b/espanso-clipboard/src/win32/native.cpp
new file mode 100644
index 00000000..af8d1d8d
--- /dev/null
+++ b/espanso-clipboard/src/win32/native.cpp
@@ -0,0 +1,184 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <iostream>
+#include <stdio.h>
+#include <string>
+#include <vector>
+#include <memory>
+#include <array>
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include <windows.h>
+#include <winuser.h>
+#include <strsafe.h>
+
+#include <gdiplus.h>
+
+#include <Windows.h>
+
+int32_t clipboard_get_text(wchar_t *buffer, int32_t buffer_size)
+{
+  int32_t result = 0;
+
+  if (OpenClipboard(NULL))
+  {
+    HANDLE hData;
+    if (hData = GetClipboardData(CF_UNICODETEXT))
+    {
+      HGLOBAL hMem;
+      if (hMem = GlobalLock(hData))
+      {
+        GlobalUnlock(hMem);
+        wcsncpy(buffer, (wchar_t *)hMem, buffer_size);
+        if (wcsnlen_s(buffer, buffer_size) > 0)
+        {
+          result = 1;
+        }
+      }
+    }
+
+    CloseClipboard();
+  }
+
+  return result;
+}
+
+int32_t clipboard_set_text(wchar_t *text)
+{
+  int32_t result = 0;
+  const size_t len = wcslen(text) + 1;
+
+  if (OpenClipboard(NULL))
+  {
+    EmptyClipboard();
+
+    HGLOBAL hMem;
+    if (hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t)))
+    {
+      memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t));
+      GlobalUnlock(hMem);
+
+      if (SetClipboardData(CF_UNICODETEXT, hMem))
+      {
+        result = 1;
+      }
+    }
+
+    CloseClipboard();
+  }
+
+  return result;
+}
+
+int32_t clipboard_set_image(wchar_t *path)
+{
+  int32_t result = 0;
+
+  Gdiplus::GdiplusStartupInput gdiplusStartupInput;
+  ULONG_PTR gdiplusToken;
+  Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
+
+  Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path);
+  if (gdibmp)
+  {
+    HBITMAP hbitmap;
+    gdibmp->GetHBITMAP(0, &hbitmap);
+    if (OpenClipboard(NULL))
+    {
+      EmptyClipboard();
+      DIBSECTION ds;
+      if (GetObject(hbitmap, sizeof(DIBSECTION), &ds))
+      {
+        HDC hdc = GetDC(HWND_DESKTOP);
+        //create compatible bitmap (get DDB from DIB)
+        HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT,
+                                             ds.dsBm.bmBits, (BITMAPINFO *)&ds.dsBmih, DIB_RGB_COLORS);
+        ReleaseDC(HWND_DESKTOP, hdc);
+        SetClipboardData(CF_BITMAP, hbitmap_ddb);
+        DeleteObject(hbitmap_ddb);
+        result = 1;
+      }
+      CloseClipboard();
+    }
+
+    DeleteObject(hbitmap);
+    delete gdibmp;
+  }
+
+  Gdiplus::GdiplusShutdown(gdiplusToken);
+
+  return result;
+}
+
+// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
+int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text) {
+  // Get clipboard id for HTML format
+  static int cfid = 0;
+  if(!cfid) {
+      cfid = RegisterClipboardFormat(L"HTML Format");
+  }
+
+  int32_t result = 0;
+  
+  const size_t html_len = strlen(html_descriptor) + 1;  
+  const size_t fallback_len = (fallback_text != nullptr) ? wcslen(fallback_text) + 1 : 0;
+
+  if (OpenClipboard(NULL))
+  {
+    EmptyClipboard();
+
+    // First copy the HTML
+    HGLOBAL hMem;
+    if (hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char)))
+    {
+      memcpy(GlobalLock(hMem), html_descriptor, html_len * sizeof(char));
+      GlobalUnlock(hMem);
+
+      if (SetClipboardData(cfid, hMem))
+      {
+        result = 1;
+      }
+    }
+
+    // Then try to set the fallback text, if present.
+    if (fallback_len > 0) {
+      if (hMem = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t)))
+      {
+        memcpy(GlobalLock(hMem), fallback_text, fallback_len * sizeof(wchar_t));
+        GlobalUnlock(hMem);
+
+        SetClipboardData(CF_UNICODETEXT, hMem);
+      }
+    }
+
+    CloseClipboard();
+  }
+
+  return result;
+}
\ No newline at end of file
diff --git a/espanso-clipboard/src/win32/native.h b/espanso-clipboard/src/win32/native.h
new file mode 100644
index 00000000..ce49cea3
--- /dev/null
+++ b/espanso-clipboard/src/win32/native.h
@@ -0,0 +1,30 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_CLIPBOARD_H
+#define ESPANSO_CLIPBOARD_H
+
+#include <stdint.h>
+
+extern "C" int32_t clipboard_get_text(wchar_t * buffer, int32_t buffer_size);
+extern "C" int32_t clipboard_set_text(wchar_t * text);
+extern "C" int32_t clipboard_set_image(wchar_t * image);
+extern "C" int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text);
+
+#endif //ESPANSO_CLIPBOARD_H
\ No newline at end of file
diff --git a/espanso-clipboard/src/x11/mod.rs b/espanso-clipboard/src/x11/mod.rs
new file mode 100644
index 00000000..cccb648a
--- /dev/null
+++ b/espanso-clipboard/src/x11/mod.rs
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+pub(crate) mod native;
diff --git a/espanso-clipboard/src/x11/native/README.md b/espanso-clipboard/src/x11/native/README.md
new file mode 100644
index 00000000..af2b7591
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/README.md
@@ -0,0 +1,4 @@
+The X11NativeClipboard modules uses the wonderful [clip](https://github.com/dacap/clip) library
+by David Capello to manipulate the clipboard.
+
+At the time of writing, the library is MIT licensed.
\ No newline at end of file
diff --git a/espanso-clipboard/src/x11/native/clip/LICENSE.txt b/espanso-clipboard/src/x11/native/clip/LICENSE.txt
new file mode 100644
index 00000000..8b436bd0
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2015-2020 David Capello
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/espanso-clipboard/src/x11/native/clip/clip.cpp b/espanso-clipboard/src/x11/native/clip/clip.cpp
new file mode 100644
index 00000000..bd221fc6
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip.cpp
@@ -0,0 +1,174 @@
+// Clip Library
+// Copyright (c) 2015-2018 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#include "clip.h"
+#include "clip_lock_impl.h"
+
+#include <vector>
+#include <stdexcept>
+
+namespace clip {
+
+namespace {
+
+void default_error_handler(ErrorCode code) {
+  static const char* err[] = {
+    "Cannot lock clipboard",
+    "Image format is not supported"
+  };
+  throw std::runtime_error(err[static_cast<int>(code)]);
+}
+
+} // anonymous namespace
+
+error_handler g_error_handler = default_error_handler;
+
+lock::lock(void* native_window_handle)
+  : p(new impl(native_window_handle)) {
+}
+
+lock::~lock() = default;
+
+bool lock::locked() const {
+  return p->locked();
+}
+
+bool lock::clear() {
+  return p->clear();
+}
+
+bool lock::is_convertible(format f) const {
+  return p->is_convertible(f);
+}
+
+bool lock::set_data(format f, const char* buf, size_t length) {
+  return p->set_data(f, buf, length);
+}
+
+bool lock::get_data(format f, char* buf, size_t len) const {
+  return p->get_data(f, buf, len);
+}
+
+size_t lock::get_data_length(format f) const {
+  return p->get_data_length(f);
+}
+
+bool lock::set_image(const image& img) {
+  return p->set_image(img);
+}
+
+bool lock::get_image(image& img) const {
+  return p->get_image(img);
+}
+
+bool lock::get_image_spec(image_spec& spec) const {
+  return p->get_image_spec(spec);
+}
+
+format empty_format() { return 0; }
+format text_format()  { return 1; }
+format image_format() { return 2; }
+
+bool has(format f) {
+  lock l;
+  if (l.locked())
+    return l.is_convertible(f);
+  else
+    return false;
+}
+
+bool clear() {
+  lock l;
+  if (l.locked())
+    return l.clear();
+  else
+    return false;
+}
+
+bool set_text(const std::string& value) {
+  lock l;
+  if (l.locked()) {
+    l.clear();
+    return l.set_data(text_format(), value.c_str(), value.size());
+  }
+  else
+    return false;
+}
+
+bool get_text(std::string& value) {
+  lock l;
+  if (!l.locked())
+    return false;
+
+  format f = text_format();
+  if (!l.is_convertible(f))
+    return false;
+
+  size_t len = l.get_data_length(f);
+  if (len > 0) {
+    std::vector<char> buf(len);
+    l.get_data(f, &buf[0], len);
+    value = &buf[0];
+    return true;
+  }
+  else {
+    value.clear();
+    return true;
+  }
+}
+
+bool set_image(const image& img) {
+  lock l;
+  if (l.locked()) {
+    l.clear();
+    return l.set_image(img);
+  }
+  else
+    return false;
+}
+
+bool get_image(image& img) {
+  lock l;
+  if (!l.locked())
+    return false;
+
+  format f = image_format();
+  if (!l.is_convertible(f))
+    return false;
+
+  return l.get_image(img);
+}
+
+bool get_image_spec(image_spec& spec) {
+  lock l;
+  if (!l.locked())
+    return false;
+
+  format f = image_format();
+  if (!l.is_convertible(f))
+    return false;
+
+  return l.get_image_spec(spec);
+}
+
+void set_error_handler(error_handler handler) {
+  g_error_handler = handler;
+}
+
+error_handler get_error_handler() {
+  return g_error_handler;
+}
+
+#ifdef HAVE_XCB_XLIB_H
+static int g_x11_timeout = 1000;
+void set_x11_wait_timeout(int msecs) { g_x11_timeout = msecs; }
+int get_x11_wait_timeout() { return g_x11_timeout; }
+#else
+void set_x11_wait_timeout(int) { }
+int get_x11_wait_timeout() { return 1000; }
+#endif
+
+} // namespace clip
diff --git a/espanso-clipboard/src/x11/native/clip/clip.h b/espanso-clipboard/src/x11/native/clip/clip.h
new file mode 100644
index 00000000..b74f275c
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip.h
@@ -0,0 +1,178 @@
+// Clip Library
+// Copyright (c) 2015-2018 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef CLIP_H_INCLUDED
+#define CLIP_H_INCLUDED
+#pragma once
+
+#include <cassert>
+#include <memory>
+#include <string>
+
+namespace clip {
+
+  // ======================================================================
+  // Low-level API to lock the clipboard/pasteboard and modify it
+  // ======================================================================
+
+  // Clipboard format identifier.
+  typedef size_t format;
+
+  class image;
+  struct image_spec;
+
+  class lock {
+  public:
+    // You can give your current HWND as the "native_window_handle."
+    // Windows clipboard functions use this handle to open/close
+    // (lock/unlock) the clipboard. From the MSDN documentation we
+    // need this handler so SetClipboardData() doesn't fail after a
+    // EmptyClipboard() call. Anyway it looks to work just fine if we
+    // call OpenClipboard() with a null HWND.
+    lock(void* native_window_handle = nullptr);
+    ~lock();
+
+    // Returns true if we've locked the clipboard successfully in
+    // lock() constructor.
+    bool locked() const;
+
+    // Clears the clipboard content. If you don't clear the content,
+    // previous clipboard content (in unknown formats) could persist
+    // after the unlock.
+    bool clear();
+
+    // Returns true if the clipboard can be converted to the given
+    // format.
+    bool is_convertible(format f) const;
+    bool set_data(format f, const char* buf, size_t len);
+    bool get_data(format f, char* buf, size_t len) const;
+    size_t get_data_length(format f) const;
+
+    // For images
+    bool set_image(const image& image);
+    bool get_image(image& image) const;
+    bool get_image_spec(image_spec& spec) const;
+
+  private:
+    class impl;
+    std::unique_ptr<impl> p;
+  };
+
+  format register_format(const std::string& name);
+
+  // This format is when the clipboard has no content.
+  format empty_format();
+
+  // When the clipboard has UTF8 text.
+  format text_format();
+
+  // When the clipboard has an image.
+  format image_format();
+
+  // Returns true if the clipboard has content of the given type.
+  bool has(format f);
+
+  // Clears the clipboard content.
+  bool clear();
+
+  // ======================================================================
+  // Error handling
+  // ======================================================================
+
+  enum class ErrorCode {
+    CannotLock,
+    ImageNotSupported,
+  };
+
+  typedef void (*error_handler)(ErrorCode code);
+
+  void set_error_handler(error_handler f);
+  error_handler get_error_handler();
+
+  // ======================================================================
+  // Text
+  // ======================================================================
+
+  // High-level API to put/get UTF8 text in/from the clipboard. These
+  // functions returns false in case of error.
+  bool set_text(const std::string& value);
+  bool get_text(std::string& value);
+
+  // ======================================================================
+  // Image
+  // ======================================================================
+
+  struct image_spec {
+    unsigned long width = 0;
+    unsigned long height = 0;
+    unsigned long bits_per_pixel = 0;
+    unsigned long bytes_per_row = 0;
+    unsigned long red_mask = 0;
+    unsigned long green_mask = 0;
+    unsigned long blue_mask = 0;
+    unsigned long alpha_mask = 0;
+    unsigned long red_shift = 0;
+    unsigned long green_shift = 0;
+    unsigned long blue_shift = 0;
+    unsigned long alpha_shift = 0;
+  };
+
+  // The image data must contain straight RGB values
+  // (non-premultiplied by alpha). The image retrieved from the
+  // clipboard will be non-premultiplied too. Basically you will be
+  // always dealing with straight alpha images.
+  //
+  // Details: Windows expects premultiplied images on its clipboard
+  // content, so the library code make the proper conversion
+  // automatically. macOS handles straight alpha directly, so there is
+  // no conversion at all. Linux/X11 images are transferred in
+  // image/png format which are specified in straight alpha.
+  class image {
+  public:
+    image();
+    image(const image_spec& spec);
+    image(const void* data, const image_spec& spec);
+    image(const image& image);
+    image(image&& image);
+    ~image();
+
+    image& operator=(const image& image);
+    image& operator=(image&& image);
+
+    char* data() const { return m_data; }
+    const image_spec& spec() const { return m_spec; }
+
+    bool is_valid() const { return m_data != nullptr; }
+    void reset();
+
+  private:
+    void copy_image(const image& image);
+    void move_image(image&& image);
+
+    bool m_own_data;
+    char* m_data;
+    image_spec m_spec;
+  };
+
+  // High-level API to set/get an image in/from the clipboard. These
+  // functions returns false in case of error.
+  bool set_image(const image& img);
+  bool get_image(image& img);
+  bool get_image_spec(image_spec& spec);
+
+  // ======================================================================
+  // Platform-specific
+  // ======================================================================
+
+  // Only for X11: Sets the time (in milliseconds) that we must wait
+  // for the selection/clipboard owner to receive the content. This
+  // value is 1000 (one second) by default.
+  void set_x11_wait_timeout(int msecs);
+  int get_x11_wait_timeout();
+
+} // namespace clip
+
+#endif // CLIP_H_INCLUDED
diff --git a/espanso-clipboard/src/x11/native/clip/clip_common.h b/espanso-clipboard/src/x11/native/clip/clip_common.h
new file mode 100644
index 00000000..ebe0d72f
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip_common.h
@@ -0,0 +1,76 @@
+// Clip Library
+// Copyright (C) 2020 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef CLIP_COMMON_H_INCLUDED
+#define CLIP_COMMON_H_INCLUDED
+#pragma once
+
+namespace clip {
+namespace details {
+
+inline void divide_rgb_by_alpha(image& img,
+                                bool hasAlphaGreaterThanZero = false) {
+  const image_spec& spec = img.spec();
+
+  bool hasValidPremultipliedAlpha = true;
+
+  for (unsigned long y=0; y<spec.height; ++y) {
+    const uint32_t* dst = (uint32_t*)(img.data()+y*spec.bytes_per_row);
+    for (unsigned long x=0; x<spec.width; ++x, ++dst) {
+      const uint32_t c = *dst;
+      const int r = ((c & spec.red_mask  ) >> spec.red_shift  );
+      const int g = ((c & spec.green_mask) >> spec.green_shift);
+      const int b = ((c & spec.blue_mask ) >> spec.blue_shift );
+      const int a = ((c & spec.alpha_mask) >> spec.alpha_shift);
+
+      if (a > 0)
+        hasAlphaGreaterThanZero = true;
+      if (r > a || g > a || b > a)
+        hasValidPremultipliedAlpha = false;
+    }
+  }
+
+  for (unsigned long y=0; y<spec.height; ++y) {
+    uint32_t* dst = (uint32_t*)(img.data()+y*spec.bytes_per_row);
+    for (unsigned long x=0; x<spec.width; ++x, ++dst) {
+      const uint32_t c = *dst;
+      int r = ((c & spec.red_mask  ) >> spec.red_shift  );
+      int g = ((c & spec.green_mask) >> spec.green_shift);
+      int b = ((c & spec.blue_mask ) >> spec.blue_shift );
+      int a = ((c & spec.alpha_mask) >> spec.alpha_shift);
+
+      // If all alpha values = 0, we make the image opaque.
+      if (!hasAlphaGreaterThanZero) {
+        a = 255;
+
+        // We cannot change the image spec (e.g. spec.alpha_mask=0) to
+        // make the image opaque, because the "spec" of the image is
+        // read-only. The image spec used by the client is the one
+        // returned by get_image_spec().
+      }
+      // If there is alpha information and it's pre-multiplied alpha
+      else if (hasValidPremultipliedAlpha) {
+        if (a > 0) {
+          // Convert it to straight alpha
+          r = r * 255 / a;
+          g = g * 255 / a;
+          b = b * 255 / a;
+        }
+      }
+
+      *dst =
+        (r << spec.red_shift  ) |
+        (g << spec.green_shift) |
+        (b << spec.blue_shift ) |
+        (a << spec.alpha_shift);
+    }
+  }
+}
+
+} // namespace details
+} // namespace clip
+
+#endif // CLIP_H_INCLUDED
diff --git a/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h b/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h
new file mode 100644
index 00000000..3f08af7f
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h
@@ -0,0 +1,33 @@
+// Clip Library
+// Copyright (c) 2015-2018 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef CLIP_LOCK_IMPL_H_INCLUDED
+#define CLIP_LOCK_IMPL_H_INCLUDED
+
+namespace clip {
+
+class lock::impl {
+public:
+  impl(void* native_window_handle);
+  ~impl();
+
+  bool locked() const { return m_locked; }
+  bool clear();
+  bool is_convertible(format f) const;
+  bool set_data(format f, const char* buf, size_t len);
+  bool get_data(format f, char* buf, size_t len) const;
+  size_t get_data_length(format f) const;
+  bool set_image(const image& image);
+  bool get_image(image& image) const;
+  bool get_image_spec(image_spec& spec) const;
+
+private:
+  bool m_locked;
+};
+
+} // namespace clip
+
+#endif
diff --git a/espanso-clipboard/src/x11/native/clip/clip_x11.cpp b/espanso-clipboard/src/x11/native/clip/clip_x11.cpp
new file mode 100644
index 00000000..ecb37ad4
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip_x11.cpp
@@ -0,0 +1,1088 @@
+// Clip Library
+// Copyright (c) 2018-2019 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#include "clip.h"
+#include "clip_lock_impl.h"
+
+#include <xcb/xcb.h>
+
+#include <algorithm>
+#include <cassert>
+#include <condition_variable>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+#ifdef HAVE_PNG_H
+  #include "clip_x11_png.h"
+#endif
+
+#define CLIP_SUPPORT_SAVE_TARGETS 1
+
+namespace clip {
+
+namespace {
+
+enum CommonAtom {
+  ATOM,
+  INCR,
+  TARGETS,
+  CLIPBOARD,
+#ifdef HAVE_PNG_H
+  MIME_IMAGE_PNG,
+#endif
+#ifdef CLIP_SUPPORT_SAVE_TARGETS
+  ATOM_PAIR,
+  SAVE_TARGETS,
+  MULTIPLE,
+  CLIPBOARD_MANAGER,
+#endif
+};
+
+const char* kCommonAtomNames[] = {
+  "ATOM",
+  "INCR",
+  "TARGETS",
+  "CLIPBOARD",
+#ifdef HAVE_PNG_H
+  "image/png",
+#endif
+#ifdef CLIP_SUPPORT_SAVE_TARGETS
+  "ATOM_PAIR",
+  "SAVE_TARGETS",
+  "MULTIPLE",
+  "CLIPBOARD_MANAGER",
+#endif
+};
+
+const int kBaseForCustomFormats = 100;
+
+class Manager {
+public:
+  typedef std::shared_ptr<std::vector<uint8_t>> buffer_ptr;
+  typedef std::vector<xcb_atom_t> atoms;
+  typedef std::function<bool()> notify_callback;
+
+  Manager()
+    : m_lock(m_mutex, std::defer_lock)
+    , m_connection(xcb_connect(nullptr, nullptr))
+    , m_window(0)
+    , m_incr_process(false) {
+    if (!m_connection)
+      return;
+
+    const xcb_setup_t* setup = xcb_get_setup(m_connection);
+    if (!setup)
+      return;
+
+    xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data;
+    if (!screen)
+      return;
+
+    uint32_t event_mask =
+      // Just in case that some program reports SelectionNotify events
+      // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
+      XCB_EVENT_MASK_PROPERTY_CHANGE |
+      // To receive DestroyNotify event and stop the message loop.
+      XCB_EVENT_MASK_STRUCTURE_NOTIFY;
+
+    m_window = xcb_generate_id(m_connection);
+    xcb_create_window(m_connection, 0,
+                      m_window,
+                      screen->root,
+                      0, 0, 1, 1, 0,
+                      XCB_WINDOW_CLASS_INPUT_OUTPUT,
+                      screen->root_visual,
+                      XCB_CW_EVENT_MASK,
+                      &event_mask);
+
+    m_thread = std::thread(
+      [this]{
+        process_x11_events();
+      });
+  }
+
+  ~Manager() {
+#ifdef CLIP_SUPPORT_SAVE_TARGETS
+    if (!m_data.empty() &&
+        m_window &&
+        m_window == get_x11_selection_owner()) {
+      // Check if there is a CLIPBOARD_MANAGER running to save all
+      // targets before we exit.
+      xcb_window_t x11_clipboard_manager = 0;
+      xcb_get_selection_owner_cookie_t cookie =
+        xcb_get_selection_owner(m_connection,
+                                get_atom(CLIPBOARD_MANAGER));
+
+      xcb_get_selection_owner_reply_t* reply =
+        xcb_get_selection_owner_reply(m_connection, cookie, nullptr);
+      if (reply) {
+        x11_clipboard_manager = reply->owner;
+        free(reply);
+      }
+
+      if (x11_clipboard_manager) {
+        // Start the SAVE_TARGETS mechanism so the X11
+        // CLIPBOARD_MANAGER will save our clipboard data
+        // from now on.
+        get_data_from_selection_owner(
+          { get_atom(SAVE_TARGETS) },
+          [this]() -> bool { return true; },
+          get_atom(CLIPBOARD_MANAGER));
+      }
+    }
+#endif
+
+    if (m_window) {
+      xcb_destroy_window(m_connection, m_window);
+      xcb_flush(m_connection);
+    }
+
+    if (m_thread.joinable())
+      m_thread.join();
+
+    if (m_connection)
+      xcb_disconnect(m_connection);
+  }
+
+  bool try_lock() {
+    bool res = m_lock.try_lock();
+    if (!res) {
+      for (int i=0; i<5 && !res; ++i) {
+        res = m_lock.try_lock();
+        std::this_thread::sleep_for(std::chrono::milliseconds(20));
+      }
+    }
+    return res;
+  }
+
+  void unlock() {
+    m_lock.unlock();
+  }
+
+  // Clear our data
+  void clear_data() {
+    m_data.clear();
+    m_image.reset();
+  }
+
+  void clear() {
+    clear_data();
+
+    // Clear the clipboard data from the selection owner
+    const xcb_window_t owner = get_x11_selection_owner();
+    if (m_window != owner) {
+      xcb_selection_clear_event_t event;
+      event.response_type = XCB_SELECTION_CLEAR;
+      event.pad0          = 0;
+      event.sequence      = 0;
+      event.time          = XCB_CURRENT_TIME;
+      event.owner         = owner;
+      event.selection     = get_atom(CLIPBOARD);
+
+      xcb_send_event(m_connection, false,
+                     owner,
+                     XCB_EVENT_MASK_NO_EVENT,
+                     (const char*)&event);
+
+      xcb_flush(m_connection);
+    }
+  }
+
+  bool is_convertible(format f) const {
+    const atoms atoms = get_format_atoms(f);
+    const xcb_window_t owner = get_x11_selection_owner();
+
+    // If we are the owner, we just can check the m_data map
+    if (owner == m_window) {
+      for (xcb_atom_t atom : atoms) {
+        auto it = m_data.find(atom);
+        if (it != m_data.end())
+          return true;
+      }
+    }
+    // Ask to the selection owner the available formats/atoms/targets.
+    else if (owner) {
+      return
+        get_data_from_selection_owner(
+          { get_atom(TARGETS) },
+          [this, &atoms]() -> bool {
+            assert(m_reply_data);
+            if (!m_reply_data)
+              return false;
+
+            const xcb_atom_t* sel_atoms = (const xcb_atom_t*)&(*m_reply_data)[0];
+            int sel_natoms = m_reply_data->size() / sizeof(xcb_atom_t);
+            auto atoms_begin = atoms.begin();
+            auto atoms_end = atoms.end();
+            for (int i=0; i<sel_natoms; ++i) {
+              if (std::find(atoms_begin,
+                            atoms_end,
+                            sel_atoms[i]) != atoms_end) {
+                return true;
+              }
+            }
+            return false;
+          });
+    }
+
+    return false;
+  }
+
+  bool set_data(format f, const char* buf, size_t len) {
+    if (!set_x11_selection_owner())
+      return false;
+
+    const atoms atoms = get_format_atoms(f);
+    if (atoms.empty())
+      return false;
+
+    buffer_ptr shared_data_buf = std::make_shared<std::vector<uint8_t>>(len);
+    std::copy(buf,
+              buf+len,
+              shared_data_buf->begin());
+    for (xcb_atom_t atom : atoms)
+      m_data[atom] = shared_data_buf;
+
+    return true;
+  }
+
+  bool get_data(format f, char* buf, size_t len) const {
+    const atoms atoms = get_format_atoms(f);
+    const xcb_window_t owner = get_x11_selection_owner();
+    if (owner == m_window) {
+      for (xcb_atom_t atom : atoms) {
+        auto it = m_data.find(atom);
+        if (it != m_data.end()) {
+          size_t n = std::min(len, it->second->size());
+          std::copy(it->second->begin(),
+                    it->second->begin()+n,
+                    buf);
+
+          if (f == text_format()) {
+            // Add an extra null char
+            if (n < len)
+              buf[n] = 0;
+          }
+
+          return true;
+        }
+      }
+    }
+    else if (owner) {
+      if (get_data_from_selection_owner(
+            atoms,
+            [this, buf, len, f]() -> bool {
+              size_t n = std::min(len, m_reply_data->size());
+              std::copy(m_reply_data->begin(),
+                        m_reply_data->begin()+n,
+                        buf);
+
+              if (f == text_format()) {
+                if (n < len)
+                  buf[n] = 0; // Include a null character
+              }
+
+              return true;
+            })) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  size_t get_data_length(format f) const {
+    size_t len = 0;
+    const atoms atoms = get_format_atoms(f);
+    const xcb_window_t owner = get_x11_selection_owner();
+    if (owner == m_window) {
+      for (xcb_atom_t atom : atoms) {
+        auto it = m_data.find(atom);
+        if (it != m_data.end()) {
+          len = it->second->size();
+          break;
+        }
+      }
+    }
+    else if (owner) {
+      if (!get_data_from_selection_owner(
+            atoms,
+            [this, &len]() -> bool {
+              len = m_reply_data->size();
+              return true;
+            })) {
+        // Error getting data length
+        return 0;
+      }
+    }
+    if (f == text_format() && len > 0) {
+      ++len; // Add an extra byte for the null char
+    }
+    return len;
+  }
+
+  bool set_image(const image& image) {
+    if (!set_x11_selection_owner())
+      return false;
+
+    m_image = image;
+
+#ifdef HAVE_PNG_H
+    // Put a nullptr in the m_data for image/png format and then we'll
+    // encode the png data when the image is requested in this format.
+    m_data[get_atom(MIME_IMAGE_PNG)] = buffer_ptr();
+#endif
+
+    return true;
+  }
+
+  bool get_image(image& output_img) const {
+    const xcb_window_t owner = get_x11_selection_owner();
+    if (owner == m_window) {
+      if (m_image.is_valid()) {
+        output_img = m_image;
+        return true;
+      }
+    }
+#ifdef HAVE_PNG_H
+    else if (owner &&
+             get_data_from_selection_owner(
+               { get_atom(MIME_IMAGE_PNG) },
+               [this, &output_img]() -> bool {
+                 return x11::read_png(&(*m_reply_data)[0],
+                                      m_reply_data->size(),
+                                      &output_img, nullptr);
+               })) {
+      return true;
+    }
+#endif
+    return false;
+  }
+
+  bool get_image_spec(image_spec& spec) const {
+    const xcb_window_t owner = get_x11_selection_owner();
+    if (owner == m_window) {
+      if (m_image.is_valid()) {
+        spec = m_image.spec();
+        return true;
+      }
+    }
+#ifdef HAVE_PNG_H
+    else if (owner &&
+             get_data_from_selection_owner(
+               { get_atom(MIME_IMAGE_PNG) },
+               [this, &spec]() -> bool {
+                 return x11::read_png(&(*m_reply_data)[0],
+                                      m_reply_data->size(),
+                                      nullptr, &spec);
+               })) {
+      return true;
+    }
+#endif
+    return false;
+  }
+
+  format register_format(const std::string& name) {
+    xcb_atom_t atom = get_atom(name.c_str());
+    m_custom_formats.push_back(atom);
+    return (format)(m_custom_formats.size()-1) + kBaseForCustomFormats;
+  }
+
+private:
+
+  void process_x11_events() {
+    bool stop = false;
+    xcb_generic_event_t* event;
+    while (!stop && (event = xcb_wait_for_event(m_connection))) {
+      int type = (event->response_type & ~0x80);
+
+      switch (type) {
+
+        case XCB_DESTROY_NOTIFY:
+          // To stop the message loop we can just destroy the window
+          stop = true;
+          break;
+
+        // Someone else has new content in the clipboard, so is
+        // notifying us that we should delete our data now.
+        case XCB_SELECTION_CLEAR:
+          handle_selection_clear_event(
+            (xcb_selection_clear_event_t*)event);
+          break;
+
+          // Someone is requesting the clipboard content from us.
+        case XCB_SELECTION_REQUEST:
+          handle_selection_request_event(
+            (xcb_selection_request_event_t*)event);
+          break;
+
+          // We've requested the clipboard content and this is the
+          // answer.
+        case XCB_SELECTION_NOTIFY:
+          handle_selection_notify_event(
+            (xcb_selection_notify_event_t*)event);
+          break;
+
+        case XCB_PROPERTY_NOTIFY:
+          handle_property_notify_event(
+            (xcb_property_notify_event_t*)event);
+          break;
+
+      }
+
+      free(event);
+    }
+  }
+
+  void handle_selection_clear_event(xcb_selection_clear_event_t* event) {
+    if (event->selection == get_atom(CLIPBOARD)) {
+      std::lock_guard<std::mutex> lock(m_mutex);
+      clear_data(); // Clear our clipboard data
+    }
+  }
+
+  void handle_selection_request_event(xcb_selection_request_event_t* event) {
+    std::lock_guard<std::mutex> lock(m_mutex);
+
+    if (event->target == get_atom(TARGETS)) {
+      atoms targets;
+      targets.push_back(get_atom(TARGETS));
+#ifdef CLIP_SUPPORT_SAVE_TARGETS
+      targets.push_back(get_atom(SAVE_TARGETS));
+      targets.push_back(get_atom(MULTIPLE));
+#endif
+      for (const auto& it : m_data)
+        targets.push_back(it.first);
+
+      // Set the "property" of "requestor" with the clipboard
+      // formats ("targets", atoms) that we provide.
+      xcb_change_property(
+        m_connection,
+        XCB_PROP_MODE_REPLACE,
+        event->requestor,
+        event->property,
+        get_atom(ATOM),
+        8*sizeof(xcb_atom_t),
+        targets.size(),
+        &targets[0]);
+    }
+#ifdef CLIP_SUPPORT_SAVE_TARGETS
+    else if (event->target == get_atom(SAVE_TARGETS)) {
+      // Do nothing
+    }
+    else if (event->target == get_atom(MULTIPLE)) {
+      xcb_get_property_reply_t* reply =
+        get_and_delete_property(event->requestor,
+                                event->property,
+                                get_atom(ATOM_PAIR),
+                                false);
+      if (reply) {
+        for (xcb_atom_t
+               *ptr=(xcb_atom_t*)xcb_get_property_value(reply),
+               *end=ptr + (xcb_get_property_value_length(reply)/sizeof(xcb_atom_t));
+             ptr<end; ) {
+          xcb_atom_t target = *ptr++;
+          xcb_atom_t property = *ptr++;
+
+          if (!set_requestor_property_with_clipboard_content(
+                event->requestor,
+                property,
+                target)) {
+            xcb_change_property(
+              m_connection,
+              XCB_PROP_MODE_REPLACE,
+              event->requestor,
+              event->property,
+              XCB_ATOM_NONE, 0, 0, nullptr);
+          }
+        }
+
+        free(reply);
+      }
+    }
+#endif // CLIP_SUPPORT_SAVE_TARGETS
+    else {
+      if (!set_requestor_property_with_clipboard_content(
+            event->requestor,
+            event->property,
+            event->target)) {
+        return;
+      }
+    }
+
+    // Notify the "requestor" that we've already updated the property.
+    xcb_selection_notify_event_t notify;
+    notify.response_type = XCB_SELECTION_NOTIFY;
+    notify.pad0          = 0;
+    notify.sequence      = 0;
+    notify.time          = event->time;
+    notify.requestor     = event->requestor;
+    notify.selection     = event->selection;
+    notify.target        = event->target;
+    notify.property      = event->property;
+
+    xcb_send_event(m_connection, false,
+                   event->requestor,
+                   XCB_EVENT_MASK_NO_EVENT, // SelectionNotify events go without mask
+                   (const char*)&notify);
+
+    xcb_flush(m_connection);
+  }
+
+  bool set_requestor_property_with_clipboard_content(const xcb_atom_t requestor,
+                                                     const xcb_atom_t property,
+                                                     const xcb_atom_t target) {
+    auto it = m_data.find(target);
+    if (it == m_data.end()) {
+      // Nothing to do (unsupported target)
+      return false;
+    }
+
+    // This can be null of the data was set from an image but we
+    // didn't encode the image yet (e.g. to image/png format).
+    if (!it->second) {
+      encode_data_on_demand(*it);
+
+      // Return nothing, the given "target" cannot be constructed
+      // (maybe by some encoding error).
+      if (!it->second)
+        return false;
+    }
+
+    // Set the "property" of "requestor" with the
+    // clipboard content in the requested format ("target").
+    xcb_change_property(
+      m_connection,
+      XCB_PROP_MODE_REPLACE,
+      requestor,
+      property,
+      target,
+      8,
+      it->second->size(),
+      &(*it->second)[0]);
+    return true;
+  }
+
+  void handle_selection_notify_event(xcb_selection_notify_event_t* event) {
+    assert(event->requestor == m_window);
+
+    if (event->target == get_atom(TARGETS))
+      m_target_atom = get_atom(ATOM);
+    else
+      m_target_atom = event->target;
+
+    xcb_get_property_reply_t* reply =
+      get_and_delete_property(event->requestor,
+                              event->property,
+                              m_target_atom);
+    if (reply) {
+      // In this case, We're going to receive the clipboard content in
+      // chunks of data with several PropertyNotify events.
+      if (reply->type == get_atom(INCR)) {
+        free(reply);
+
+        reply = get_and_delete_property(event->requestor,
+                                        event->property,
+                                        get_atom(INCR));
+        if (reply) {
+          if (xcb_get_property_value_length(reply) == 4) {
+            uint32_t n = *(uint32_t*)xcb_get_property_value(reply);
+            m_reply_data = std::make_shared<std::vector<uint8_t>>(n);
+            m_reply_offset = 0;
+            m_incr_process = true;
+            m_incr_received = true;
+          }
+          free(reply);
+        }
+      }
+      else {
+        // Simple case, the whole clipboard content in just one reply
+        // (without the INCR method).
+        m_reply_data.reset();
+        m_reply_offset = 0;
+        copy_reply_data(reply);
+
+        call_callback(reply);
+
+        free(reply);
+      }
+    }
+  }
+
+  void handle_property_notify_event(xcb_property_notify_event_t* event) {
+    if (m_incr_process &&
+        event->state == XCB_PROPERTY_NEW_VALUE &&
+        event->atom == get_atom(CLIPBOARD)) {
+      xcb_get_property_reply_t* reply =
+        get_and_delete_property(event->window,
+                                event->atom,
+                                m_target_atom);
+      if (reply) {
+        m_incr_received = true;
+
+        // When the length is 0 it means that the content was
+        // completely sent by the selection owner.
+        if (xcb_get_property_value_length(reply) > 0) {
+          copy_reply_data(reply);
+        }
+        else {
+          // Now that m_reply_data has the complete clipboard content,
+          // we can call the m_callback.
+          call_callback(reply);
+          m_incr_process = false;
+        }
+        free(reply);
+      }
+    }
+  }
+
+  xcb_get_property_reply_t* get_and_delete_property(xcb_window_t window,
+                                                    xcb_atom_t property,
+                                                    xcb_atom_t atom,
+                                                    bool delete_prop = true) {
+    xcb_get_property_cookie_t cookie =
+      xcb_get_property(m_connection,
+                       delete_prop,
+                       window,
+                       property,
+                       atom,
+                       0, 0x1fffffff); // 0x1fffffff = INT32_MAX / 4
+
+    xcb_generic_error_t* err = nullptr;
+    xcb_get_property_reply_t* reply =
+      xcb_get_property_reply(m_connection, cookie, &err);
+    if (err) {
+      free(err);
+    }
+    return reply;
+  }
+
+  // Concatenates the new data received in "reply" into "m_reply_data"
+  // buffer.
+  void copy_reply_data(xcb_get_property_reply_t* reply) {
+    const uint8_t* src = (const uint8_t*)xcb_get_property_value(reply);
+    // n = length of "src" in bytes
+    size_t n = xcb_get_property_value_length(reply);
+
+    size_t req = m_reply_offset+n;
+    if (!m_reply_data) {
+      m_reply_data = std::make_shared<std::vector<uint8_t>>(req);
+    }
+    // The "m_reply_data" size can be smaller because the size
+    // specified in INCR property is just a lower bound.
+    else if (req > m_reply_data->size()) {
+      m_reply_data->resize(req);
+    }
+
+    std::copy(src, src+n, m_reply_data->begin()+m_reply_offset);
+    m_reply_offset += n;
+  }
+
+  // Calls the current m_callback() to handle the clipboard content
+  // received from the owner.
+  void call_callback(__attribute__((unused)) xcb_get_property_reply_t* reply) {
+    m_callback_result = false;
+    if (m_callback)
+      m_callback_result = m_callback();
+
+    m_cv.notify_one();
+
+    m_reply_data.reset();
+  }
+
+  bool get_data_from_selection_owner(const atoms& atoms,
+                                     const notify_callback&& callback,
+                                     xcb_atom_t selection = 0) const {
+    if (!selection)
+      selection = get_atom(CLIPBOARD);
+
+    // Put the callback on "m_callback" so we can call it on
+    // SelectionNotify event.
+    m_callback = std::move(callback);
+
+    // Clear data if we are not the selection owner.
+    if (m_window != get_x11_selection_owner())
+      m_data.clear();
+
+    // Ask to the selection owner for its content on each known
+    // text format/atom.
+    for (xcb_atom_t atom : atoms) {
+      xcb_convert_selection(m_connection,
+                            m_window, // Send us the result
+                            selection, // Clipboard selection
+                            atom, // The clipboard format that we're requesting
+                            get_atom(CLIPBOARD), // Leave result in this window's property
+                            XCB_CURRENT_TIME);
+
+      xcb_flush(m_connection);
+
+      // We use the "m_incr_received" to wait several timeouts in case
+      // that we've received the INCR SelectionNotify or
+      // PropertyNotify events.
+      do {
+        m_incr_received = false;
+
+        // Wait a response for 100 milliseconds
+        std::cv_status status =
+          m_cv.wait_for(m_lock,
+                        std::chrono::milliseconds(get_x11_wait_timeout()));
+        if (status == std::cv_status::no_timeout) {
+          // If the condition variable was notified, it means that the
+          // callback was called correctly.
+          return m_callback_result;
+        }
+      } while (m_incr_received);
+    }
+
+    // Reset callback
+    m_callback = notify_callback();
+    return false;
+  }
+
+  atoms get_atoms(const char** names,
+                  const int n) const {
+    atoms result(n, 0);
+    std::vector<xcb_intern_atom_cookie_t> cookies(n);
+
+    for (int i=0; i<n; ++i) {
+      auto it = m_atoms.find(names[i]);
+      if (it != m_atoms.end())
+        result[i] = it->second;
+      else
+        cookies[i] = xcb_intern_atom(
+          m_connection, 0,
+          std::strlen(names[i]), names[i]);
+    }
+
+    for (int i=0; i<n; ++i) {
+      if (result[i] == 0) {
+        xcb_intern_atom_reply_t* reply =
+          xcb_intern_atom_reply(m_connection,
+                                cookies[i],
+                                nullptr);
+        if (reply) {
+          result[i] = m_atoms[names[i]] = reply->atom;
+          free(reply);
+        }
+      }
+    }
+
+    return result;
+  }
+
+  xcb_atom_t get_atom(const char* name) const {
+    auto it = m_atoms.find(name);
+    if (it != m_atoms.end())
+      return it->second;
+
+    xcb_atom_t result = 0;
+    xcb_intern_atom_cookie_t cookie =
+      xcb_intern_atom(m_connection, 0,
+                      std::strlen(name), name);
+
+    xcb_intern_atom_reply_t* reply =
+      xcb_intern_atom_reply(m_connection,
+                            cookie,
+                            nullptr);
+    if (reply) {
+      result = m_atoms[name] = reply->atom;
+      free(reply);
+    }
+    return result;
+  }
+
+  xcb_atom_t get_atom(CommonAtom i) const {
+    if (m_common_atoms.empty()) {
+      m_common_atoms =
+        get_atoms(kCommonAtomNames,
+                  sizeof(kCommonAtomNames) / sizeof(kCommonAtomNames[0]));
+    }
+    return m_common_atoms[i];
+  }
+
+  const atoms& get_text_format_atoms() const {
+    if (m_text_atoms.empty()) {
+      const char* names[] = {
+        // Prefer utf-8 formats first
+        "UTF8_STRING",
+        "text/plain;charset=utf-8",
+        "text/plain;charset=UTF-8",
+        // ANSI C strings?
+        "STRING",
+        "TEXT",
+        "text/plain",
+      };
+      m_text_atoms = get_atoms(names, sizeof(names) / sizeof(names[0]));
+    }
+    return m_text_atoms;
+  }
+
+  const atoms& get_image_format_atoms() const {
+    if (m_image_atoms.empty()) {
+#ifdef HAVE_PNG_H
+      m_image_atoms.push_back(get_atom(MIME_IMAGE_PNG));
+#endif
+    }
+    return m_image_atoms;
+  }
+
+  atoms get_format_atoms(const format f) const {
+    atoms atoms;
+    if (f == text_format()) {
+      atoms = get_text_format_atoms();
+    }
+    else if (f == image_format()) {
+      atoms = get_image_format_atoms();
+    }
+    else {
+      xcb_atom_t atom = get_format_atom(f);
+      if (atom)
+        atoms.push_back(atom);
+    }
+    return atoms;
+  }
+
+#if !defined(NDEBUG)
+  // This can be used to print debugging messages.
+  std::string get_atom_name(xcb_atom_t atom) const {
+    std::string result;
+    xcb_get_atom_name_cookie_t cookie =
+      xcb_get_atom_name(m_connection, atom);
+    xcb_generic_error_t* err = nullptr;
+    xcb_get_atom_name_reply_t* reply =
+      xcb_get_atom_name_reply(m_connection, cookie, &err);
+    if (err) {
+      free(err);
+    }
+    if (reply) {
+      int len = xcb_get_atom_name_name_length(reply);
+      if (len > 0) {
+        result.resize(len);
+        char* name = xcb_get_atom_name_name(reply);
+        if (name)
+          std::copy(name, name+len, result.begin());
+      }
+      free(reply);
+    }
+    return result;
+  }
+#endif
+
+  bool set_x11_selection_owner() const {
+    xcb_void_cookie_t cookie =
+      xcb_set_selection_owner_checked(m_connection,
+                                      m_window,
+                                      get_atom(CLIPBOARD),
+                                      XCB_CURRENT_TIME);
+    xcb_generic_error_t* err =
+      xcb_request_check(m_connection,
+                        cookie);
+    if (err) {
+      free(err);
+      return false;
+    }
+    return true;
+  }
+
+  xcb_window_t get_x11_selection_owner() const {
+    xcb_window_t result = 0;
+    xcb_get_selection_owner_cookie_t cookie =
+      xcb_get_selection_owner(m_connection,
+                              get_atom(CLIPBOARD));
+
+    xcb_get_selection_owner_reply_t* reply =
+      xcb_get_selection_owner_reply(m_connection, cookie, nullptr);
+    if (reply) {
+      result = reply->owner;
+      free(reply);
+    }
+    return result;
+  }
+
+  xcb_atom_t get_format_atom(const format f) const {
+    int i = f - kBaseForCustomFormats;
+    if (i >= 0 && i < int(m_custom_formats.size()))
+      return m_custom_formats[i];
+    else
+      return 0;
+  }
+
+  void encode_data_on_demand(__attribute__((unused)) std::pair<const xcb_atom_t, buffer_ptr>& e) {
+#ifdef HAVE_PNG_H
+    if (e.first == get_atom(MIME_IMAGE_PNG)) {
+      assert(m_image.is_valid());
+      if (!m_image.is_valid())
+        return;
+
+      std::vector<uint8_t> output;
+      if (x11::write_png(m_image, output)) {
+        e.second =
+          std::make_shared<std::vector<uint8_t>>(
+            std::move(output));
+      }
+    }
+#endif
+  }
+
+  // Access to the whole Manager
+  std::mutex m_mutex;
+
+  // Lock used in the main thread using the Manager (i.e. by lock::impl)
+  mutable std::unique_lock<std::mutex> m_lock;
+
+  // Connection to X11 server
+  xcb_connection_t* m_connection;
+
+  // Temporal background window used to own the clipboard and process
+  // all events related about the clipboard in a background thread
+  xcb_window_t m_window;
+
+  // Used to wait/notify the arrival of the SelectionNotify event when
+  // we requested the clipboard content from other selection owner.
+  mutable std::condition_variable m_cv;
+
+  // Thread used to run a background message loop to wait X11 events
+  // about clipboard. The X11 selection owner will be a hidden window
+  // created by us just for the clipboard purpose/communication.
+  std::thread m_thread;
+
+  // Internal callback used when a SelectionNotify is received (or the
+  // whole data content is received by the INCR method). So this
+  // callback can use the notification by different purposes (e.g. get
+  // the data length only, or get/process the data content, etc.).
+  mutable notify_callback m_callback;
+
+  // Result returned by the m_callback. Used as return value in the
+  // get_data_from_selection_owner() function. For example, if the
+  // callback must read a "image/png" file from the clipboard data and
+  // fails, the callback can return false and finally the get_image()
+  // will return false (i.e. there is data, but it's not a valid image
+  // format).
+  bool m_callback_result;
+
+  // Cache of known atoms
+  mutable std::map<std::string, xcb_atom_t> m_atoms;
+
+  // Cache of common used atoms by us
+  mutable atoms m_common_atoms;
+
+  // Cache of atoms related to text or image content
+  mutable atoms m_text_atoms;
+  mutable atoms m_image_atoms;
+
+  // Actual clipboard data generated by us (when we "copy" content in
+  // the clipboard, it means that we own the X11 "CLIPBOARD"
+  // selection, and in case of SelectionRequest events, we've to
+  // return the data stored in this "m_data" field)
+  mutable std::map<xcb_atom_t, buffer_ptr> m_data;
+
+  // Copied image in the clipboard. As we have to transfer the image
+  // in some specific format (e.g. image/png) we want to keep a copy
+  // of the image and make the conversion when the clipboard data is
+  // requested by other process.
+  mutable image m_image;
+
+  // True if we have received an INCR notification so we're going to
+  // process several PropertyNotify to concatenate all data chunks.
+  bool m_incr_process;
+
+  // Variable used to wait more time if we've received an INCR
+  // notification, which means that we're going to receive large
+  // amounts of data from the selection owner.
+  mutable bool m_incr_received;
+
+  // Target/selection format used in the SelectionNotify. Used in the
+  // INCR method to get data from the same property in the same format
+  // (target) on each PropertyNotify.
+  xcb_atom_t m_target_atom;
+
+  // Each time we receive data from the selection owner, we put that
+  // data in this buffer. If we get the data with the INCR method,
+  // we'll concatenate chunks of data in this buffer to complete the
+  // whole clipboard content.
+  buffer_ptr m_reply_data;
+
+  // Used to concatenate chunks of data in "m_reply_data" from several
+  // PropertyNotify when we are getting the selection owner data with
+  // the INCR method.
+  size_t m_reply_offset;
+
+  // List of user-defined formats/atoms.
+  std::vector<xcb_atom_t> m_custom_formats;
+};
+
+Manager* manager = nullptr;
+
+void delete_manager_atexit() {
+  if (manager) {
+    delete manager;
+    manager = nullptr;
+  }
+}
+
+Manager* get_manager() {
+  if (!manager) {
+    manager = new Manager;
+    std::atexit(delete_manager_atexit);
+  }
+  return manager;
+}
+
+} // anonymous namespace
+
+lock::impl::impl(void*) : m_locked(false) {
+  m_locked = get_manager()->try_lock();
+}
+
+lock::impl::~impl() {
+  if (m_locked)
+    manager->unlock();
+}
+
+bool lock::impl::clear() {
+  manager->clear();
+  return true;
+}
+
+bool lock::impl::is_convertible(format f) const {
+  return manager->is_convertible(f);
+}
+
+bool lock::impl::set_data(format f, const char* buf, size_t len) {
+  return manager->set_data(f, buf, len);
+}
+
+bool lock::impl::get_data(format f, char* buf, size_t len) const {
+  return manager->get_data(f, buf, len);
+}
+
+size_t lock::impl::get_data_length(format f) const {
+  return manager->get_data_length(f);
+}
+
+bool lock::impl::set_image(const image& image) {
+  return manager->set_image(image);
+}
+
+bool lock::impl::get_image(image& output_img) const {
+  return manager->get_image(output_img);
+}
+
+bool lock::impl::get_image_spec(image_spec& spec) const {
+  return manager->get_image_spec(spec);
+}
+
+format register_format(const std::string& name) {
+  return get_manager()->register_format(name);
+}
+
+} // namespace clip
diff --git a/espanso-clipboard/src/x11/native/clip/clip_x11_png.h b/espanso-clipboard/src/x11/native/clip/clip_x11_png.h
new file mode 100644
index 00000000..e1ad8b54
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/clip_x11_png.h
@@ -0,0 +1,225 @@
+// Clip Library
+// Copyright (c) 2018 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#include "clip.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "png.h"
+
+namespace clip {
+namespace x11 {
+
+//////////////////////////////////////////////////////////////////////
+// Functions to convert clip::image into png data to store it in the
+// clipboard.
+
+void write_data_fn(png_structp png, png_bytep buf, png_size_t len) {
+  std::vector<uint8_t>& output = *(std::vector<uint8_t>*)png_get_io_ptr(png);
+  const size_t i = output.size();
+  output.resize(i+len);
+  std::copy(buf, buf+len, output.begin()+i);
+}
+
+bool write_png(const image& image,
+               std::vector<uint8_t>& output) {
+  png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
+                                            nullptr, nullptr, nullptr);
+  if (!png)
+    return false;
+
+  png_infop info = png_create_info_struct(png);
+  if (!info) {
+    png_destroy_write_struct(&png, nullptr);
+    return false;
+  }
+
+  if (setjmp(png_jmpbuf(png))) {
+    png_destroy_write_struct(&png, &info);
+    return false;
+  }
+
+  png_set_write_fn(png,
+                   (png_voidp)&output,
+                   write_data_fn,
+                   nullptr);    // No need for a flush function
+
+  const image_spec& spec = image.spec();
+  int color_type = (spec.alpha_mask ?
+                    PNG_COLOR_TYPE_RGB_ALPHA:
+                    PNG_COLOR_TYPE_RGB);
+
+  png_set_IHDR(png, info,
+               spec.width, spec.height, 8, color_type,
+               PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+  png_write_info(png, info);
+  png_set_packing(png);
+
+  png_bytep row =
+    (png_bytep)png_malloc(png, png_get_rowbytes(png, info));
+
+  for (png_uint_32 y=0; y<spec.height; ++y) {
+    const uint32_t* src =
+      (const uint32_t*)(((const uint8_t*)image.data())
+                        + y*spec.bytes_per_row);
+    uint8_t* dst = row;
+    unsigned int x, c;
+
+    for (x=0; x<spec.width; x++) {
+      c = *(src++);
+      *(dst++) = (c & spec.red_mask  ) >> spec.red_shift;
+      *(dst++) = (c & spec.green_mask) >> spec.green_shift;
+      *(dst++) = (c & spec.blue_mask ) >> spec.blue_shift;
+      if (color_type == PNG_COLOR_TYPE_RGB_ALPHA)
+        *(dst++) = (c & spec.alpha_mask) >> spec.alpha_shift;
+    }
+
+    png_write_rows(png, &row, 1);
+  }
+
+  png_free(png, row);
+  png_write_end(png, info);
+  png_destroy_write_struct(&png, &info);
+  return true;
+}
+
+//////////////////////////////////////////////////////////////////////
+// Functions to convert png data stored in the clipboard to a
+// clip::image.
+
+struct read_png_io {
+  const uint8_t* buf;
+  size_t len;
+  size_t pos;
+};
+
+void read_data_fn(png_structp png, png_bytep buf, png_size_t len) {
+  read_png_io& io = *(read_png_io*)png_get_io_ptr(png);
+  if (io.pos < io.len) {
+    size_t n = std::min(len, io.len-io.pos);
+    if (n > 0) {
+      std::copy(io.buf+io.pos,
+                io.buf+io.pos+n,
+                buf);
+      io.pos += n;
+    }
+  }
+}
+
+bool read_png(const uint8_t* buf,
+              const size_t len,
+              image* output_image,
+              image_spec* output_spec) {
+  png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
+                                           nullptr, nullptr, nullptr);
+  if (!png)
+    return false;
+
+  png_infop info = png_create_info_struct(png);
+  if (!info) {
+    png_destroy_read_struct(&png, nullptr, nullptr);
+    return false;
+  }
+
+  if (setjmp(png_jmpbuf(png))) {
+    png_destroy_read_struct(&png, &info, nullptr);
+    return false;
+  }
+
+  read_png_io io = { buf, len, 0 };
+  png_set_read_fn(png, (png_voidp)&io, read_data_fn);
+
+  png_read_info(png, info);
+
+  png_uint_32 width, height;
+  int bit_depth, color_type, interlace_type;
+  png_get_IHDR(png, info, &width, &height,
+               &bit_depth, &color_type,
+               &interlace_type,
+               nullptr, nullptr);
+
+  image_spec spec;
+  spec.width = width;
+  spec.height = height;
+  spec.bits_per_pixel = 32;
+  spec.bytes_per_row = png_get_rowbytes(png, info);
+
+  spec.red_mask    = 0x000000ff;
+  spec.green_mask  = 0x0000ff00;
+  spec.blue_mask   = 0x00ff0000;
+  spec.red_shift   = 0;
+  spec.green_shift = 8;
+  spec.blue_shift  = 16;
+
+  if (color_type == PNG_COLOR_TYPE_RGB_ALPHA ||
+      color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
+    spec.alpha_mask = 0xff000000;
+    spec.alpha_shift = 24;
+  }
+  else {
+    spec.alpha_mask = 0;
+    spec.alpha_shift = 0;
+  }
+
+  if (output_spec)
+    *output_spec = spec;
+
+  if (output_image &&
+      width > 0 &&
+      height > 0) {
+    image img(spec);
+
+    // We want RGB 24-bit or RGBA 32-bit as a result
+    png_set_strip_16(png); // Down to 8-bit
+    png_set_packing(png);  // Use one byte if color depth < 8-bit
+    png_set_expand_gray_1_2_4_to_8(png);
+    png_set_palette_to_rgb(png);
+    png_set_gray_to_rgb(png);
+    png_set_tRNS_to_alpha(png);
+
+    int number_passes = png_set_interlace_handling(png);
+    png_read_update_info(png, info);
+
+    png_bytepp rows = (png_bytepp)png_malloc(png, sizeof(png_bytep)*height);
+    png_uint_32 y;
+    for (y=0; y<height; ++y)
+      rows[y] = (png_bytep)png_malloc(png, spec.bytes_per_row);
+
+    for (int pass=0; pass<number_passes; ++pass)
+      for (y=0; y<height; ++y)
+        png_read_rows(png, rows+y, nullptr, 1);
+
+    for (y=0; y<height; ++y) {
+      const uint8_t* src = rows[y];
+      uint32_t* dst = (uint32_t*)(img.data() + y*spec.bytes_per_row);
+      unsigned int x, r, g, b, a = 255;
+
+      for (x=0; x<width; x++) {
+        r = *(src++);
+        g = *(src++);
+        b = *(src++);
+        if (spec.alpha_mask)
+          a = *(src++);
+        *(dst++) =
+          (r << spec.red_shift) |
+          (g << spec.green_shift) |
+          (b << spec.blue_shift) |
+          (a << spec.alpha_shift);
+      }
+      png_free(png, rows[y]);
+    }
+    png_free(png, rows);
+
+    std::swap(*output_image, img);
+  }
+
+  png_destroy_read_struct(&png, &info, nullptr);
+  return true;
+}
+
+} // namespace x11
+} // namespace clip
diff --git a/espanso-clipboard/src/x11/native/clip/image.cpp b/espanso-clipboard/src/x11/native/clip/image.cpp
new file mode 100644
index 00000000..9e2aa451
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/clip/image.cpp
@@ -0,0 +1,83 @@
+// Clip Library
+// Copyright (c) 2015-2018 David Capello
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#include "clip.h"
+
+namespace clip {
+
+image::image()
+  : m_own_data(false),
+    m_data(nullptr)
+{
+}
+
+image::image(const image_spec& spec)
+  : m_own_data(true),
+    m_data(new char[spec.bytes_per_row*spec.height]),
+    m_spec(spec) {
+}
+
+image::image(const void* data, const image_spec& spec)
+  : m_own_data(false),
+    m_data((char*)data),
+    m_spec(spec) {
+}
+
+image::image(const image& image)
+  : m_own_data(false),
+    m_data(nullptr),
+    m_spec(image.m_spec) {
+  copy_image(image);
+}
+
+image::image(image&& image)
+  : m_own_data(false),
+    m_data(nullptr) {
+  move_image(std::move(image));
+}
+
+image::~image() {
+  reset();
+}
+
+image& image::operator=(const image& image) {
+  copy_image(image);
+  return *this;
+}
+
+image& image::operator=(image&& image) {
+  move_image(std::move(image));
+  return *this;
+}
+
+void image::reset() {
+  if (m_own_data) {
+    delete[] m_data;
+    m_own_data = false;
+    m_data = nullptr;
+  }
+}
+
+void image::copy_image(const image& image) {
+  reset();
+
+  m_spec = image.spec();
+  std::size_t n = m_spec.bytes_per_row*m_spec.height;
+
+  m_own_data = true;
+  m_data = new char[n];
+  std::copy(image.data(),
+            image.data()+n,
+            m_data);
+}
+
+void image::move_image(image&& image) {
+  std::swap(m_own_data, image.m_own_data);
+  std::swap(m_data, image.m_data);
+  std::swap(m_spec, image.m_spec);
+}
+
+} // namespace clip
diff --git a/espanso-clipboard/src/x11/native/ffi.rs b/espanso-clipboard/src/x11/native/ffi.rs
new file mode 100644
index 00000000..005449aa
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/ffi.rs
@@ -0,0 +1,28 @@
+/*
+ * 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::os::raw::c_char;
+
+#[link(name = "espansoclipboardx11", kind = "static")]
+extern "C" {
+  pub fn clipboard_x11_get_text(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn clipboard_x11_set_text(text: *const c_char) -> i32;
+  pub fn clipboard_x11_set_html(html: *const c_char, fallback_text: *const c_char) -> i32;
+  pub fn clipboard_x11_set_image(buffer: *const u8, buffer_size: i32) -> i32;
+}
diff --git a/espanso-clipboard/src/x11/native/mod.rs b/espanso-clipboard/src/x11/native/mod.rs
new file mode 100644
index 00000000..de5a1f1b
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/mod.rs
@@ -0,0 +1,108 @@
+/*
+ * 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::{
+  ffi::{CStr, CString},
+  io::Read,
+  path::PathBuf,
+};
+
+use crate::Clipboard;
+use anyhow::Result;
+use std::os::raw::c_char;
+use thiserror::Error;
+
+mod ffi;
+
+pub struct X11NativeClipboard {}
+
+impl X11NativeClipboard {
+  pub fn new() -> Result<Self> {
+    Ok(Self {})
+  }
+}
+
+impl Clipboard for X11NativeClipboard {
+  fn get_text(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    let native_result =
+      unsafe { ffi::clipboard_x11_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
+    if native_result > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      Some(string.to_string_lossy().to_string())
+    } else {
+      None
+    }
+  }
+
+  fn set_text(&self, text: &str) -> anyhow::Result<()> {
+    let string = CString::new(text)?;
+    let native_result = unsafe { ffi::clipboard_x11_set_text(string.as_ptr()) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(X11NativeClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> {
+    if !image_path.exists() || !image_path.is_file() {
+      return Err(X11NativeClipboardError::ImageNotFound(image_path.to_path_buf()).into());
+    }
+
+    // Load the image data
+    let mut file = std::fs::File::open(image_path)?;
+    let mut data = Vec::new();
+    file.read_to_end(&mut data)?;
+
+    let native_result = unsafe { ffi::clipboard_x11_set_image(data.as_ptr(), data.len() as i32) };
+
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(X11NativeClipboardError::SetOperationFailed().into())
+    }
+  }
+
+  fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> {
+    let html_string = CString::new(html)?;
+    let fallback_string = CString::new(fallback_text.unwrap_or_default())?;
+    let fallback_ptr = if fallback_text.is_some() {
+      fallback_string.as_ptr()
+    } else {
+      std::ptr::null()
+    };
+
+    let native_result = unsafe { ffi::clipboard_x11_set_html(html_string.as_ptr(), fallback_ptr) };
+    if native_result > 0 {
+      Ok(())
+    } else {
+      Err(X11NativeClipboardError::SetOperationFailed().into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum X11NativeClipboardError {
+  #[error("clipboard set operation failed")]
+  SetOperationFailed(),
+
+  #[error("image not found: `{0}`")]
+  ImageNotFound(PathBuf),
+}
diff --git a/espanso-clipboard/src/x11/native/native.cpp b/espanso-clipboard/src/x11/native/native.cpp
new file mode 100644
index 00000000..44c2c23a
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/native.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include "clip/clip.h"
+#include "string.h"
+#include <iostream>
+
+clip::format html_format = clip::register_format("text/html");
+clip::format png_format = clip::register_format("image/png");
+
+int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size) {
+  std::string value;
+  if (!clip::get_text(value)) {
+    return 0;
+  }
+
+  if (value.length() == 0) {
+    return 0;
+  }
+
+  strncpy(buffer, value.c_str(), buffer_size - 1);
+  return 1;
+}
+
+int32_t clipboard_x11_set_text(char * text) {
+  if (!clip::set_text(text)) {
+    return 0;
+  } else {
+    return 1;
+  }
+}
+
+int32_t clipboard_x11_set_html(char * html, char * fallback_text) {
+  clip::lock l;
+  if (!l.clear()) {
+    return 0;
+  }
+  if (!l.set_data(html_format, html, strlen(html))) {
+    return 0;
+  }
+  if (fallback_text) {
+    // Best effort to set the fallback
+    l.set_data(clip::text_format(), fallback_text, strlen(fallback_text));
+  }
+  return 1;
+}
+
+int32_t clipboard_x11_set_image(char * buffer, int32_t size) {
+  clip::lock l;
+  if (!l.clear()) {
+    return 0;
+  }
+
+  if (!l.set_data(png_format, buffer, size)) {
+    return 0;
+  }
+
+  return 1;
+}
\ No newline at end of file
diff --git a/espanso-clipboard/src/x11/native/native.h b/espanso-clipboard/src/x11/native/native.h
new file mode 100644
index 00000000..204a161c
--- /dev/null
+++ b/espanso-clipboard/src/x11/native/native.h
@@ -0,0 +1,31 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_X11_CLIPBOARD_H
+#define ESPANSO_X11_CLIPBOARD_H
+
+#include <stdint.h>
+
+extern "C" int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size);
+
+extern "C" int32_t clipboard_x11_set_text(char * text);
+extern "C" int32_t clipboard_x11_set_html(char * html, char * fallback_text);
+extern "C" int32_t clipboard_x11_set_image(char * buffer, int32_t buffer_size);
+
+#endif //ESPANSO_X11_CLIPBOARD_H
\ No newline at end of file
diff --git a/espanso-config/Cargo.toml b/espanso-config/Cargo.toml
new file mode 100644
index 00000000..b00cc9c7
--- /dev/null
+++ b/espanso-config/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "espanso-config"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+serde = { version = "1.0.123", features = ["derive"] }
+serde_yaml = "0.8.17"
+glob = "0.3.0"
+regex = "1.4.3"
+lazy_static = "1.4.0"
+dunce = "1.0.1"
+walkdir = "2.3.1"
+enum-as-inner = "0.3.3"
+ordered-float = "2.0"
+indoc = "1.0.3"
+
+[dev-dependencies]
+tempdir = "0.3.7"
+tempfile = "3.2.0"
+mockall = "0.9.1"
\ No newline at end of file
diff --git a/espanso-config/src/config/default.rs b/espanso-config/src/config/default.rs
new file mode 100644
index 00000000..09decba5
--- /dev/null
+++ b/espanso-config/src/config/default.rs
@@ -0,0 +1,23 @@
+/*
+ * 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/>.
+ */
+
+pub(crate) const DEFAULT_CLIPBOARD_THRESHOLD: usize = 100;
+pub(crate) const DEFAULT_PRE_PASTE_DELAY: usize = 100;
+pub(crate) const DEFAULT_SHORTCUT_EVENT_DELAY: usize = 10;
+pub(crate) const DEFAULT_RESTORE_CLIPBOARD_DELAY: usize = 300;
diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs
new file mode 100644
index 00000000..d3d12dac
--- /dev/null
+++ b/espanso-config/src/config/mod.rs
@@ -0,0 +1,297 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use indoc::formatdoc;
+use std::sync::Arc;
+use std::{collections::HashSet, path::Path};
+use thiserror::Error;
+
+pub(crate) mod default;
+mod parse;
+mod path;
+mod resolve;
+pub(crate) mod store;
+mod util;
+
+#[cfg(test)]
+use mockall::{automock, predicate::*};
+
+use crate::error::NonFatalErrorSet;
+#[cfg_attr(test, automock)]
+pub trait Config: Send + Sync {
+  fn id(&self) -> i32;
+  fn label(&self) -> &str;
+  fn match_paths(&self) -> &[String];
+
+  // The mechanism used to perform the injection. Espanso can either
+  // inject text by simulating keypresses (Inject backend) or
+  // by using the clipboard (Clipboard backend). Both of them have pros
+  // and cons, so the "Auto" backend is used by default to automatically
+  // choose the most appropriate one based on the situation.
+  // If for whatever reason the Auto backend is not appropriate, you
+  // can change this option to override it.
+  fn backend(&self) -> Backend;
+
+  // If false, espanso will be disabled for the current configuration.
+  // This option can be used to selectively disable espanso when
+  // using a specific application (by creating an app-specific config).
+  fn enable(&self) -> bool;
+
+  // Number of chars after which a match is injected with the clipboard
+  // backend instead of the default one. This is done for efficiency
+  // reasons, as injecting a long match through separate events becomes
+  // slow for long strings.
+  fn clipboard_threshold(&self) -> usize;
+
+  // Delay (in ms) that espanso should wait to trigger the paste shortcut
+  // after copying the content in the clipboard. This is needed because
+  // if we trigger a "paste" shortcut before the content is actually
+  // copied in the clipboard, the operation will fail.
+  fn pre_paste_delay(&self) -> usize;
+
+  // Number of milliseconds between keystrokes when simulating the Paste shortcut
+  // For example: CTRL + (wait 5ms) + V + (wait 5ms) + release V + (wait 5ms) + release CTRL
+  // This is needed as sometimes (for example on macOS), without a delay some keystrokes
+  // were not registered correctly
+  fn paste_shortcut_event_delay(&self) -> usize;
+
+  // Customize the keyboard shortcut used to paste an expansion.
+  // This should follow this format: CTRL+SHIFT+V
+  fn paste_shortcut(&self) -> Option<String>;
+
+  // NOTE: This is only relevant on Linux under X11 environments
+  // Switch to a slower (but sometimes more supported) way of injecting
+  // key events based on XTestFakeKeyEvent instead of XSendEvent.
+  // From my experiements, disabling fast inject becomes particularly slow when
+  // using the Gnome desktop environment.
+  fn disable_x11_fast_inject(&self) -> bool;
+
+  // Defines the key that disables/enables espanso when double pressed
+  fn toggle_key(&self) -> Option<ToggleKey>;
+
+  // If true, instructs the daemon process to restart the worker (and refresh
+  // the configuration) after a configuration file change is detected on disk.
+  fn auto_restart(&self) -> bool;
+
+  // If true, espanso will attempt to preserve the previous clipboard content
+  // after an expansion has taken place (when using the Clipboard backend).
+  fn preserve_clipboard(&self) -> bool;
+
+  // The number of milliseconds to wait before restoring the previous clipboard
+  // content after an expansion. This is needed as without this delay, sometimes
+  // the target application detects the previous clipboard content instead of
+  // the expansion content.
+  fn restore_clipboard_delay(&self) -> usize;
+
+  // Number of milliseconds between text injection events. Increase if the target
+  // application is missing some characters.
+  fn inject_delay(&self) -> Option<usize>;
+
+  // Number of milliseconds between key injection events. Increase if the target
+  // application is missing some key events.
+  fn key_delay(&self) -> Option<usize>;
+
+  // Extra delay to apply when injecting modifiers under the EVDEV backend.
+  // This is useful on Wayland if espanso is injecting seemingly random
+  // cased letters, for example "Hi theRE1" instead of "Hi there!".
+  // Increase if necessary, decrease to speed up the injection.
+  fn evdev_modifier_delay(&self) -> Option<usize>;
+
+  // Chars that when pressed mark the start and end of a word.
+  // Examples of this are . or ,
+  fn word_separators(&self) -> Vec<String>;
+
+  // Maximum number of backspace presses espanso keeps track of.
+  // For example, this is needed to correctly expand even if typos
+  // are typed.
+  fn backspace_limit(&self) -> usize;
+
+  // If false, avoid applying the built-in patches to the current config.
+  fn apply_patch(&self) -> bool;
+
+  // On Wayland, overrides the auto-detected keyboard configuration (RMLVO)
+  // which is used both for the detection and injection process.
+  fn keyboard_layout(&self) -> Option<RMLVOConfig>;
+
+  // Trigger used to show the Search UI
+  fn search_trigger(&self) -> Option<String>;
+
+  // Hotkey used to trigger the Search UI
+  fn search_shortcut(&self) -> Option<String>;
+
+  // When enabled, espanso automatically "reverts" an expansion if the user
+  // presses the Backspace key afterwards.
+  fn undo_backspace(&self) -> bool;
+
+  // If false, disable all notifications
+  fn show_notifications(&self) -> bool;
+
+  // If false, avoid showing the espanso icon on the system's tray bar
+  // Note: currently not working on Linux
+  fn show_icon(&self) -> bool;
+
+  // If false, avoid showing the SecureInput notification on macOS
+  fn secure_input_notification(&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.
+  // Disabling this option might conflict with the undo feature.
+  fn win32_exclude_orphan_events(&self) -> bool;
+
+  fn is_match<'a>(&self, app: &AppProperties<'a>) -> bool;
+
+  fn pretty_dump(&self) -> String {
+    formatdoc! {"
+        [espanso config: {:?}]
+
+        backend: {:?}
+        enable: {:?}
+        paste_shortcut: {:?}
+        inject_delay: {:?}
+        key_delay: {:?}
+        apply_patch: {:?}
+        word_separators: {:?}
+        
+        preserve_clipboard: {:?}
+        clipboard_threshold: {:?}
+        disable_x11_fast_inject: {}
+        pre_paste_delay: {}
+        paste_shortcut_event_delay: {}
+        toggle_key: {:?}
+        auto_restart: {:?}
+        restore_clipboard_delay: {:?} 
+        backspace_limit: {}
+        search_trigger: {:?}
+        search_shortcut: {:?}
+        keyboard_layout: {:?}
+
+        show_icon: {:?}
+        show_notifications: {:?}
+        secure_input_notification: {:?}
+
+        match_paths: {:#?}
+      ", 
+      self.label(),
+      self.backend(),
+      self.enable(),
+      self.paste_shortcut(),
+      self.inject_delay(),
+      self.key_delay(),
+      self.apply_patch(),
+      self.word_separators(),
+
+      self.preserve_clipboard(),
+      self.clipboard_threshold(),
+      self.disable_x11_fast_inject(),
+      self.pre_paste_delay(),
+      self.paste_shortcut_event_delay(),
+      self.toggle_key(),
+      self.auto_restart(),
+      self.restore_clipboard_delay(),
+      self.backspace_limit(),
+      self.search_trigger(),
+      self.search_shortcut(),
+      self.keyboard_layout(),
+
+      self.show_icon(),
+      self.show_notifications(),
+      self.secure_input_notification(),
+
+      self.match_paths(),
+    }
+  }
+}
+
+pub trait ConfigStore: Send {
+  fn default(&self) -> Arc<dyn Config>;
+  fn active<'a>(&'a self, app: &AppProperties) -> Arc<dyn Config>;
+  fn configs(&self) -> Vec<Arc<dyn Config>>;
+
+  fn get_all_match_paths(&self) -> HashSet<String>;
+}
+
+pub struct AppProperties<'a> {
+  pub title: Option<&'a str>,
+  pub class: Option<&'a str>,
+  pub exec: Option<&'a str>,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum Backend {
+  Inject,
+  Clipboard,
+  Auto,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum ToggleKey {
+  Ctrl,
+  Meta,
+  Alt,
+  Shift,
+  RightCtrl,
+  RightAlt,
+  RightShift,
+  RightMeta,
+  LeftCtrl,
+  LeftAlt,
+  LeftShift,
+  LeftMeta,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct RMLVOConfig {
+  pub rules: Option<String>,
+  pub model: Option<String>,
+  pub layout: Option<String>,
+  pub variant: Option<String>,
+  pub options: Option<String>,
+}
+
+impl std::fmt::Display for RMLVOConfig {
+  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+    write!(
+      f,
+      "[R={}, M={}, L={}, V={}, O={}]",
+      self.rules.as_deref().unwrap_or_default(),
+      self.model.as_deref().unwrap_or_default(),
+      self.layout.as_deref().unwrap_or_default(),
+      self.variant.as_deref().unwrap_or_default(),
+      self.options.as_deref().unwrap_or_default(),
+    )
+  }
+}
+
+pub fn load_store(config_dir: &Path) -> Result<(impl ConfigStore, Vec<NonFatalErrorSet>)> {
+  store::DefaultConfigStore::load(config_dir)
+}
+
+#[derive(Error, Debug)]
+pub enum ConfigStoreError {
+  #[error("invalid config directory")]
+  InvalidConfigDir(),
+
+  #[error("missing default.yml config")]
+  MissingDefault(),
+
+  #[error("io error")]
+  IOError(#[from] std::io::Error),
+}
diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs
new file mode 100644
index 00000000..134afc0f
--- /dev/null
+++ b/espanso-config/src/config/parse/mod.rs
@@ -0,0 +1,85 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::{collections::BTreeMap, convert::TryInto, path::Path};
+use thiserror::Error;
+
+mod yaml;
+
+#[derive(Debug, Clone, PartialEq, Default)]
+pub(crate) struct ParsedConfig {
+  pub label: Option<String>,
+
+  pub backend: Option<String>,
+  pub enable: Option<bool>,
+  pub clipboard_threshold: Option<usize>,
+  pub auto_restart: Option<bool>,
+  pub preserve_clipboard: Option<bool>,
+  pub toggle_key: Option<String>,
+  pub paste_shortcut: Option<String>,
+  pub disable_x11_fast_inject: Option<bool>,
+  pub word_separators: Option<Vec<String>>,
+  pub backspace_limit: Option<usize>,
+  pub apply_patch: Option<bool>,
+  pub search_trigger: Option<String>,
+  pub search_shortcut: Option<String>,
+  pub undo_backspace: Option<bool>,
+  pub show_notifications: Option<bool>,
+  pub show_icon: Option<bool>,
+  pub secure_input_notification: Option<bool>,
+  pub win32_exclude_orphan_events: Option<bool>,
+
+  pub pre_paste_delay: Option<usize>,
+  pub restore_clipboard_delay: Option<usize>,
+  pub paste_shortcut_event_delay: Option<usize>,
+  pub inject_delay: Option<usize>,
+  pub key_delay: Option<usize>,
+  pub keyboard_layout: Option<BTreeMap<String, String>>,
+  pub evdev_modifier_delay: Option<usize>,
+
+  // Includes
+  pub includes: Option<Vec<String>>,
+  pub excludes: Option<Vec<String>>,
+  pub extra_includes: Option<Vec<String>>,
+  pub extra_excludes: Option<Vec<String>>,
+  pub use_standard_includes: Option<bool>,
+
+  // Filters
+  pub filter_title: Option<String>,
+  pub filter_class: Option<String>,
+  pub filter_exec: Option<String>,
+  pub filter_os: Option<String>,
+}
+
+impl ParsedConfig {
+  pub fn load(path: &Path) -> Result<Self> {
+    let content = std::fs::read_to_string(path)?;
+    match yaml::YAMLConfig::parse_from_str(&content) {
+      Ok(config) => Ok(config.try_into()?),
+      Err(err) => Err(ParsedConfigError::LoadFailed(err).into()),
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ParsedConfigError {
+  #[error("can't load config `{0}`")]
+  LoadFailed(#[from] anyhow::Error),
+}
diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs
new file mode 100644
index 00000000..81e884d1
--- /dev/null
+++ b/espanso-config/src/config/parse/yaml.rs
@@ -0,0 +1,328 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use serde_yaml::Mapping;
+use std::convert::TryFrom;
+
+use crate::util::is_yaml_empty;
+
+use super::ParsedConfig;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub(crate) struct YAMLConfig {
+  #[serde(default)]
+  pub label: Option<String>,
+
+  #[serde(default)]
+  pub backend: Option<String>,
+
+  #[serde(default)]
+  pub enable: Option<bool>,
+
+  #[serde(default)]
+  pub clipboard_threshold: Option<usize>,
+
+  #[serde(default)]
+  pub pre_paste_delay: Option<usize>,
+
+  #[serde(default)]
+  pub toggle_key: Option<String>,
+
+  #[serde(default)]
+  pub auto_restart: Option<bool>,
+
+  #[serde(default)]
+  pub preserve_clipboard: Option<bool>,
+
+  #[serde(default)]
+  pub restore_clipboard_delay: Option<usize>,
+
+  #[serde(default)]
+  pub paste_shortcut_event_delay: Option<usize>,
+
+  #[serde(default)]
+  pub paste_shortcut: Option<String>,
+
+  #[serde(default)]
+  pub disable_x11_fast_inject: Option<bool>,
+
+  #[serde(default)]
+  pub inject_delay: Option<usize>,
+
+  #[serde(default)]
+  pub key_delay: Option<usize>,
+
+  #[serde(default)]
+  pub backspace_delay: Option<usize>,
+
+  #[serde(default)]
+  pub evdev_modifier_delay: Option<usize>,
+
+  #[serde(default)]
+  pub word_separators: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub backspace_limit: Option<usize>,
+
+  #[serde(default)]
+  pub apply_patch: Option<bool>,
+
+  #[serde(default)]
+  pub keyboard_layout: Option<Mapping>,
+
+  #[serde(default)]
+  pub search_trigger: Option<String>,
+
+  #[serde(default)]
+  pub search_shortcut: Option<String>,
+
+  #[serde(default)]
+  pub undo_backspace: Option<bool>,
+
+  #[serde(default)]
+  pub show_notifications: Option<bool>,
+
+  #[serde(default)]
+  pub show_icon: Option<bool>,
+
+  #[serde(default)]
+  pub secure_input_notification: Option<bool>,
+
+  #[serde(default)]
+  pub win32_exclude_orphan_events: Option<bool>,
+
+  // Include/Exclude
+  #[serde(default)]
+  pub includes: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub excludes: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub extra_includes: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub extra_excludes: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub use_standard_includes: Option<bool>,
+
+  // Filters
+  #[serde(default)]
+  pub filter_title: Option<String>,
+
+  #[serde(default)]
+  pub filter_class: Option<String>,
+
+  #[serde(default)]
+  pub filter_exec: Option<String>,
+
+  #[serde(default)]
+  pub filter_os: Option<String>,
+}
+
+impl YAMLConfig {
+  pub fn parse_from_str(yaml: &str) -> Result<Self> {
+    // Because an empty string is not valid YAML but we want to support it anyway
+    if is_yaml_empty(yaml) {
+      return Ok(serde_yaml::from_str(
+        "arbitrary_field_that_will_not_block_the_parser: true",
+      )?);
+    }
+
+    Ok(serde_yaml::from_str(yaml)?)
+  }
+}
+
+impl TryFrom<YAMLConfig> for ParsedConfig {
+  type Error = anyhow::Error;
+
+  fn try_from(yaml_config: YAMLConfig) -> Result<Self, Self::Error> {
+    Ok(Self {
+      label: yaml_config.label,
+      backend: yaml_config.backend,
+      enable: yaml_config.enable,
+      clipboard_threshold: yaml_config.clipboard_threshold,
+      auto_restart: yaml_config.auto_restart,
+      toggle_key: yaml_config.toggle_key,
+      preserve_clipboard: yaml_config.preserve_clipboard,
+      paste_shortcut: yaml_config.paste_shortcut,
+      disable_x11_fast_inject: yaml_config.disable_x11_fast_inject,
+      inject_delay: yaml_config.inject_delay,
+      key_delay: yaml_config.key_delay.or(yaml_config.backspace_delay),
+      evdev_modifier_delay: yaml_config.evdev_modifier_delay,
+      word_separators: yaml_config.word_separators,
+      backspace_limit: yaml_config.backspace_limit,
+      apply_patch: yaml_config.apply_patch,
+      keyboard_layout: yaml_config.keyboard_layout.map(|mapping| {
+        mapping
+          .into_iter()
+          .filter_map(|(key, value)| {
+            if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
+              Some((key.to_string(), value.to_string()))
+            } else {
+              None
+            }
+          })
+          .collect()
+      }),
+      search_trigger: yaml_config.search_trigger,
+      search_shortcut: yaml_config.search_shortcut,
+      undo_backspace: yaml_config.undo_backspace,
+
+      show_icon: yaml_config.show_icon,
+      show_notifications: yaml_config.show_notifications,
+      secure_input_notification: yaml_config.secure_input_notification,
+
+      pre_paste_delay: yaml_config.pre_paste_delay,
+      restore_clipboard_delay: yaml_config.restore_clipboard_delay,
+      paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay,
+
+      win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
+
+      use_standard_includes: yaml_config.use_standard_includes,
+      includes: yaml_config.includes,
+      extra_includes: yaml_config.extra_includes,
+      excludes: yaml_config.excludes,
+      extra_excludes: yaml_config.extra_excludes,
+
+      filter_class: yaml_config.filter_class,
+      filter_exec: yaml_config.filter_exec,
+      filter_os: yaml_config.filter_os,
+      filter_title: yaml_config.filter_title,
+    })
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use std::{collections::BTreeMap, convert::TryInto};
+
+  #[test]
+  fn conversion_to_parsed_config_works_correctly() {
+    let config = YAMLConfig::parse_from_str(
+      r#"
+    label: "test"
+    backend: clipboard
+    enable: false
+    clipboard_threshold: 200
+    pre_paste_delay: 300
+    toggle_key: CTRL
+    auto_restart: false
+    preserve_clipboard: false
+    restore_clipboard_delay: 400
+    paste_shortcut: CTRL+ALT+V
+    paste_shortcut_event_delay: 10
+    disable_x11_fast_inject: true
+    inject_delay: 10
+    key_delay: 20
+    backspace_delay: 30
+    evdev_modifier_delay: 40
+    word_separators: ["'", "."]
+    backspace_limit: 10
+    apply_patch: false
+    keyboard_layout:
+      rules: test_rule
+      model: test_model
+      layout: test_layout
+      variant: test_variant
+      options: test_options
+    search_trigger: "search"
+    search_shortcut: "CTRL+SPACE"
+    undo_backspace: false
+    show_icon: false
+    show_notifications: false
+    secure_input_notification: false
+    win32_exclude_orphan_events: false
+      
+    use_standard_includes: true
+    includes: ["test1"]
+    extra_includes: ["test2"]
+    excludes: ["test3"]
+    extra_excludes: ["test4"]
+    
+    filter_class: "test5"
+    filter_exec: "test6"
+    filter_os: "test7"
+    filter_title: "test8"
+    "#,
+    )
+    .unwrap();
+    let parsed_config: ParsedConfig = config.try_into().unwrap();
+
+    let keyboard_layout: BTreeMap<String, String> = vec![
+      ("rules".to_string(), "test_rule".to_string()),
+      ("model".to_string(), "test_model".to_string()),
+      ("layout".to_string(), "test_layout".to_string()),
+      ("variant".to_string(), "test_variant".to_string()),
+      ("options".to_string(), "test_options".to_string()),
+    ]
+    .into_iter()
+    .collect();
+
+    assert_eq!(
+      parsed_config,
+      ParsedConfig {
+        label: Some("test".to_string()),
+
+        backend: Some("clipboard".to_string()),
+        enable: Some(false),
+        clipboard_threshold: Some(200),
+        auto_restart: Some(false),
+        preserve_clipboard: Some(false),
+        restore_clipboard_delay: Some(400),
+        paste_shortcut: Some("CTRL+ALT+V".to_string()),
+        paste_shortcut_event_delay: Some(10),
+        disable_x11_fast_inject: Some(true),
+        inject_delay: Some(10),
+        key_delay: Some(20),
+        backspace_limit: Some(10),
+        apply_patch: Some(false),
+        keyboard_layout: Some(keyboard_layout),
+        search_trigger: Some("search".to_owned()),
+        search_shortcut: Some("CTRL+SPACE".to_owned()),
+        undo_backspace: Some(false),
+        show_icon: Some(false),
+        show_notifications: Some(false),
+        secure_input_notification: Some(false),
+        win32_exclude_orphan_events: Some(false),
+
+        pre_paste_delay: Some(300),
+        evdev_modifier_delay: Some(40),
+
+        toggle_key: Some("CTRL".to_string()),
+        word_separators: Some(vec!["'".to_owned(), ".".to_owned()]),
+
+        use_standard_includes: Some(true),
+        includes: Some(vec!["test1".to_string()]),
+        extra_includes: Some(vec!["test2".to_string()]),
+        excludes: Some(vec!["test3".to_string()]),
+        extra_excludes: Some(vec!["test4".to_string()]),
+
+        filter_class: Some("test5".to_string()),
+        filter_exec: Some("test6".to_string()),
+        filter_os: Some("test7".to_string()),
+        filter_title: Some("test8".to_string()),
+      }
+    )
+  }
+}
diff --git a/espanso-config/src/config/path.rs b/espanso-config/src/config/path.rs
new file mode 100644
index 00000000..fa8a099d
--- /dev/null
+++ b/espanso-config/src/config/path.rs
@@ -0,0 +1,182 @@
+/*
+ * 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::HashSet, path::Path};
+
+use glob::glob;
+use log::error;
+use regex::Regex;
+
+lazy_static! {
+  static ref ABSOLUTE_PATH: Regex = Regex::new(r"(?m)^([a-zA-Z]:\\|/).*$").unwrap();
+}
+
+pub fn calculate_paths<'a>(
+  base_dir: &Path,
+  glob_patterns: impl Iterator<Item = &'a String>,
+) -> HashSet<String> {
+  let mut path_set = HashSet::new();
+  for glob_pattern in glob_patterns {
+    // Handle relative and absolute paths appropriately
+    let pattern = if ABSOLUTE_PATH.is_match(glob_pattern) {
+      glob_pattern.clone()
+    } else {
+      format!("{}/{}", base_dir.to_string_lossy(), glob_pattern)
+    };
+
+    let entries = glob(&pattern);
+    match entries {
+      Ok(paths) => {
+        for path in paths {
+          match path {
+            Ok(path) => {
+              // Canonicalize the path
+              match dunce::canonicalize(&path) {
+                Ok(canonical_path) => {
+                  path_set.insert(canonical_path.to_string_lossy().to_string());
+                }
+                Err(err) => {
+                  error!(
+                    "unable to canonicalize path from glob: {:?}, with error: {}",
+                    path, err
+                  );
+                }
+              }
+            }
+            Err(err) => error!(
+              "glob error when processing pattern: {}, with error: {}",
+              glob_pattern, err
+            ),
+          }
+        }
+      }
+      Err(err) => {
+        error!(
+          "unable to calculate glob from pattern: {}, with error: {}",
+          glob_pattern, err
+        );
+      }
+    }
+  }
+
+  path_set
+}
+
+#[cfg(test)]
+pub mod tests {
+  use super::*;
+  use crate::util::tests::use_test_directory;
+  use std::fs::create_dir_all;
+
+  #[test]
+  fn calculate_paths_relative_paths() {
+    use_test_directory(|base, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let result = calculate_paths(
+        base,
+        vec![
+          "**/*.yml".to_string(),
+          "match/sub/*.yml".to_string(),
+          // Invalid path
+          "invalid".to_string(),
+        ]
+        .iter(),
+      );
+
+      let mut expected = HashSet::new();
+      expected.insert(base_file.to_string_lossy().to_string());
+      expected.insert(another_file.to_string_lossy().to_string());
+      expected.insert(under_file.to_string_lossy().to_string());
+      expected.insert(sub_file.to_string_lossy().to_string());
+
+      assert_eq!(result, expected);
+    });
+  }
+
+  #[test]
+  fn calculate_paths_relative_with_parent_modifier() {
+    use_test_directory(|base, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let result = calculate_paths(base, vec!["match/sub/../sub/*.yml".to_string()].iter());
+
+      let mut expected = HashSet::new();
+      expected.insert(sub_file.to_string_lossy().to_string());
+
+      assert_eq!(result, expected);
+    });
+  }
+
+  #[test]
+  fn calculate_paths_absolute_paths() {
+    use_test_directory(|base, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let result = calculate_paths(
+        base,
+        vec![
+          format!("{}/**/*.yml", base.to_string_lossy()),
+          format!("{}/match/sub/*.yml", base.to_string_lossy()),
+          // Invalid path
+          "invalid".to_string(),
+        ]
+        .iter(),
+      );
+
+      let mut expected = HashSet::new();
+      expected.insert(base_file.to_string_lossy().to_string());
+      expected.insert(another_file.to_string_lossy().to_string());
+      expected.insert(under_file.to_string_lossy().to_string());
+      expected.insert(sub_file.to_string_lossy().to_string());
+
+      assert_eq!(result, expected);
+    });
+  }
+}
diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs
new file mode 100644
index 00000000..ea74240d
--- /dev/null
+++ b/espanso-config/src/config/resolve.rs
@@ -0,0 +1,962 @@
+/*
+ * 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 super::{
+  default::{
+    DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY,
+    DEFAULT_SHORTCUT_EVENT_DELAY,
+  },
+  parse::ParsedConfig,
+  path::calculate_paths,
+  util::os_matches,
+  AppProperties, Backend, Config, RMLVOConfig, ToggleKey,
+};
+use crate::{counter::next_id, merge};
+use anyhow::Result;
+use log::error;
+use regex::Regex;
+use std::path::PathBuf;
+use std::{collections::HashSet, path::Path};
+use thiserror::Error;
+
+const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
+
+#[derive(Debug, Clone)]
+pub(crate) struct ResolvedConfig {
+  parsed: ParsedConfig,
+
+  source_path: Option<PathBuf>,
+
+  // Generated properties
+  id: i32,
+  match_paths: Vec<String>,
+
+  filter_title: Option<Regex>,
+  filter_class: Option<Regex>,
+  filter_exec: Option<Regex>,
+}
+
+impl Default for ResolvedConfig {
+  fn default() -> Self {
+    Self {
+      parsed: Default::default(),
+      source_path: None,
+      id: 0,
+      match_paths: Vec::new(),
+      filter_title: None,
+      filter_class: None,
+      filter_exec: None,
+    }
+  }
+}
+
+impl Config for ResolvedConfig {
+  fn id(&self) -> i32 {
+    self.id
+  }
+
+  fn label(&self) -> &str {
+    if let Some(label) = self.parsed.label.as_deref() {
+      return label;
+    }
+
+    if let Some(source_path) = self.source_path.as_ref() {
+      if let Some(source_path) = source_path.to_str() {
+        return source_path;
+      }
+    }
+
+    "none"
+  }
+
+  fn match_paths(&self) -> &[String] {
+    &self.match_paths
+  }
+
+  fn is_match(&self, app: &AppProperties) -> bool {
+    if self.parsed.filter_os.is_none()
+      && self.parsed.filter_title.is_none()
+      && self.parsed.filter_exec.is_none()
+      && self.parsed.filter_class.is_none()
+    {
+      return false;
+    }
+
+    let is_os_match = if let Some(filter_os) = self.parsed.filter_os.as_deref() {
+      os_matches(filter_os)
+    } else {
+      true
+    };
+
+    let is_title_match = if let Some(title_regex) = self.filter_title.as_ref() {
+      if let Some(title) = app.title {
+        title_regex.is_match(title)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    let is_exec_match = if let Some(exec_regex) = self.filter_exec.as_ref() {
+      if let Some(exec) = app.exec {
+        exec_regex.is_match(exec)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    let is_class_match = if let Some(class_regex) = self.filter_class.as_ref() {
+      if let Some(class) = app.class {
+        class_regex.is_match(class)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    // All the filters that have been specified must be true to define a match
+    is_os_match && is_exec_match && is_title_match && is_class_match
+  }
+
+  fn backend(&self) -> Backend {
+    // TODO: test
+    match self
+      .parsed
+      .backend
+      .as_deref()
+      .map(|b| b.to_lowercase())
+      .as_deref()
+    {
+      Some("clipboard") => Backend::Clipboard,
+      Some("inject") => Backend::Inject,
+      Some("auto") => Backend::Auto,
+      None => Backend::Auto,
+      err => {
+        error!("invalid backend specified {:?}, falling back to Auto", err);
+        Backend::Auto
+      }
+    }
+  }
+
+  fn enable(&self) -> bool {
+    self.parsed.enable.unwrap_or(true)
+  }
+
+  fn clipboard_threshold(&self) -> usize {
+    self
+      .parsed
+      .clipboard_threshold
+      .unwrap_or(DEFAULT_CLIPBOARD_THRESHOLD)
+  }
+
+  fn auto_restart(&self) -> bool {
+    self.parsed.auto_restart.unwrap_or(true)
+  }
+
+  fn pre_paste_delay(&self) -> usize {
+    self
+      .parsed
+      .pre_paste_delay
+      .unwrap_or(DEFAULT_PRE_PASTE_DELAY)
+  }
+
+  fn toggle_key(&self) -> Option<ToggleKey> {
+    // TODO: test
+    match self
+      .parsed
+      .toggle_key
+      .as_deref()
+      .map(|key| key.to_lowercase())
+      .as_deref()
+    {
+      Some("ctrl") => Some(ToggleKey::Ctrl),
+      Some("alt") => Some(ToggleKey::Alt),
+      Some("shift") => Some(ToggleKey::Shift),
+      Some("meta") | Some("cmd") => Some(ToggleKey::Meta),
+      Some("right_ctrl") => Some(ToggleKey::RightCtrl),
+      Some("right_alt") => Some(ToggleKey::RightAlt),
+      Some("right_shift") => Some(ToggleKey::RightShift),
+      Some("right_meta") | Some("right_cmd") => Some(ToggleKey::RightMeta),
+      Some("left_ctrl") => Some(ToggleKey::LeftCtrl),
+      Some("left_alt") => Some(ToggleKey::LeftAlt),
+      Some("left_shift") => Some(ToggleKey::LeftShift),
+      Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
+      Some("off") => None,
+      None => Some(ToggleKey::Alt),
+      err => {
+        error!(
+          "invalid toggle_key specified {:?}, falling back to ALT",
+          err
+        );
+        Some(ToggleKey::Alt)
+      }
+    }
+  }
+
+  fn preserve_clipboard(&self) -> bool {
+    self.parsed.preserve_clipboard.unwrap_or(true)
+  }
+
+  fn restore_clipboard_delay(&self) -> usize {
+    self
+      .parsed
+      .restore_clipboard_delay
+      .unwrap_or(DEFAULT_RESTORE_CLIPBOARD_DELAY)
+  }
+
+  fn paste_shortcut_event_delay(&self) -> usize {
+    self
+      .parsed
+      .paste_shortcut_event_delay
+      .unwrap_or(DEFAULT_SHORTCUT_EVENT_DELAY)
+  }
+
+  fn paste_shortcut(&self) -> Option<String> {
+    self.parsed.paste_shortcut.clone()
+  }
+
+  fn disable_x11_fast_inject(&self) -> bool {
+    self.parsed.disable_x11_fast_inject.unwrap_or(false)
+  }
+
+  fn inject_delay(&self) -> Option<usize> {
+    self.parsed.inject_delay
+  }
+
+  fn key_delay(&self) -> Option<usize> {
+    self.parsed.key_delay
+  }
+
+  fn word_separators(&self) -> Vec<String> {
+    self.parsed.word_separators.clone().unwrap_or_else(|| {
+      vec![
+        " ".to_string(),
+        ",".to_string(),
+        ".".to_string(),
+        "?".to_string(),
+        "!".to_string(),
+        "\r".to_string(),
+        "\n".to_string(),
+        (22u8 as char).to_string(),
+      ]
+    })
+  }
+
+  fn backspace_limit(&self) -> usize {
+    self.parsed.backspace_limit.unwrap_or(5)
+  }
+
+  fn apply_patch(&self) -> bool {
+    self.parsed.apply_patch.unwrap_or(true)
+  }
+
+  fn keyboard_layout(&self) -> Option<RMLVOConfig> {
+    self
+      .parsed
+      .keyboard_layout
+      .as_ref()
+      .map(|layout| RMLVOConfig {
+        rules: layout.get("rules").map(String::from),
+        model: layout.get("model").map(String::from),
+        layout: layout.get("layout").map(String::from),
+        variant: layout.get("variant").map(String::from),
+        options: layout.get("options").map(String::from),
+      })
+  }
+
+  fn search_trigger(&self) -> Option<String> {
+    match self.parsed.search_trigger.as_deref() {
+      Some("OFF") | Some("off") => None,
+      Some(x) => Some(x.to_string()),
+      None => Some("jkj".to_string()),
+    }
+  }
+
+  fn search_shortcut(&self) -> Option<String> {
+    match self.parsed.search_shortcut.as_deref() {
+      Some("OFF") | Some("off") => None,
+      Some(x) => Some(x.to_string()),
+      None => Some("ALT+SPACE".to_string()),
+    }
+  }
+
+  fn undo_backspace(&self) -> bool {
+    self.parsed.undo_backspace.unwrap_or(true)
+  }
+
+  fn show_icon(&self) -> bool {
+    self.parsed.show_icon.unwrap_or(true)
+  }
+
+  fn show_notifications(&self) -> bool {
+    self.parsed.show_notifications.unwrap_or(true)
+  }
+
+  fn secure_input_notification(&self) -> bool {
+    self.parsed.secure_input_notification.unwrap_or(true)
+  }
+
+  fn win32_exclude_orphan_events(&self) -> bool {
+    self.parsed.win32_exclude_orphan_events.unwrap_or(true)
+  }
+
+  fn evdev_modifier_delay(&self) -> Option<usize> {
+    self.parsed.evdev_modifier_delay
+  }
+}
+
+impl ResolvedConfig {
+  pub fn load(path: &Path, parent: Option<&Self>) -> Result<Self> {
+    let mut config = ParsedConfig::load(path)?;
+
+    // Merge with parent config if present
+    if let Some(parent) = parent {
+      Self::merge_parsed(&mut config, &parent.parsed);
+    }
+
+    // Extract the base directory
+    let base_dir = path
+      .parent()
+      .ok_or_else(ResolveError::ParentResolveFailed)?;
+
+    let match_paths = Self::generate_match_paths(&config, base_dir)
+      .into_iter()
+      .collect();
+
+    let filter_title = if let Some(filter_title) = config.filter_title.as_deref() {
+      Some(Regex::new(filter_title)?)
+    } else {
+      None
+    };
+
+    let filter_class = if let Some(filter_class) = config.filter_class.as_deref() {
+      Some(Regex::new(filter_class)?)
+    } else {
+      None
+    };
+
+    let filter_exec = if let Some(filter_exec) = config.filter_exec.as_deref() {
+      Some(Regex::new(filter_exec)?)
+    } else {
+      None
+    };
+
+    Ok(Self {
+      parsed: config,
+      source_path: Some(path.to_owned()),
+      id: next_id(),
+      match_paths,
+      filter_title,
+      filter_class,
+      filter_exec,
+    })
+  }
+
+  fn merge_parsed(child: &mut ParsedConfig, parent: &ParsedConfig) {
+    // Override the None fields with the parent's value
+    merge!(
+      ParsedConfig,
+      child,
+      parent,
+      // Fields
+      label,
+      backend,
+      enable,
+      clipboard_threshold,
+      auto_restart,
+      pre_paste_delay,
+      preserve_clipboard,
+      restore_clipboard_delay,
+      paste_shortcut,
+      apply_patch,
+      paste_shortcut_event_delay,
+      disable_x11_fast_inject,
+      toggle_key,
+      inject_delay,
+      key_delay,
+      evdev_modifier_delay,
+      word_separators,
+      backspace_limit,
+      keyboard_layout,
+      search_trigger,
+      search_shortcut,
+      undo_backspace,
+      show_icon,
+      show_notifications,
+      secure_input_notification,
+      win32_exclude_orphan_events,
+      includes,
+      excludes,
+      extra_includes,
+      extra_excludes,
+      use_standard_includes,
+      filter_title,
+      filter_class,
+      filter_exec,
+      filter_os
+    );
+  }
+
+  fn aggregate_includes(config: &ParsedConfig) -> HashSet<String> {
+    let mut includes = HashSet::new();
+
+    if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() {
+      STANDARD_INCLUDES.iter().for_each(|include| {
+        includes.insert(include.to_string());
+      })
+    }
+
+    if let Some(yaml_includes) = config.includes.as_ref() {
+      yaml_includes.iter().for_each(|include| {
+        includes.insert(include.to_string());
+      })
+    }
+
+    if let Some(extra_includes) = config.extra_includes.as_ref() {
+      extra_includes.iter().for_each(|include| {
+        includes.insert(include.to_string());
+      })
+    }
+
+    includes
+  }
+
+  fn aggregate_excludes(config: &ParsedConfig) -> HashSet<String> {
+    let mut excludes = HashSet::new();
+
+    if let Some(yaml_excludes) = config.excludes.as_ref() {
+      yaml_excludes.iter().for_each(|exclude| {
+        excludes.insert(exclude.to_string());
+      })
+    }
+
+    if let Some(extra_excludes) = config.extra_excludes.as_ref() {
+      extra_excludes.iter().for_each(|exclude| {
+        excludes.insert(exclude.to_string());
+      })
+    }
+
+    excludes
+  }
+
+  fn generate_match_paths(config: &ParsedConfig, base_dir: &Path) -> HashSet<String> {
+    let includes = Self::aggregate_includes(config);
+    let excludes = Self::aggregate_excludes(config);
+
+    // Extract the paths
+    let exclude_paths = calculate_paths(base_dir, excludes.iter());
+    let include_paths = calculate_paths(base_dir, includes.iter());
+
+    include_paths
+      .difference(&exclude_paths)
+      .cloned()
+      .collect::<HashSet<_>>()
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ResolveError {
+  #[error("unable to resolve parent path")]
+  ParentResolveFailed(),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::util::tests::use_test_directory;
+  use std::fs::create_dir_all;
+
+  #[test]
+  fn aggregate_includes_empty_config() {
+    assert_eq!(
+      ResolvedConfig::aggregate_includes(&ParsedConfig {
+        ..Default::default()
+      }),
+      vec!["../match/**/[!_]*.yml".to_string(),]
+        .iter()
+        .cloned()
+        .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_includes_no_standard() {
+    assert_eq!(
+      ResolvedConfig::aggregate_includes(&ParsedConfig {
+        use_standard_includes: Some(false),
+        ..Default::default()
+      }),
+      HashSet::new()
+    );
+  }
+
+  #[test]
+  fn aggregate_includes_custom_includes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_includes(&ParsedConfig {
+        includes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec![
+        "../match/**/[!_]*.yml".to_string(),
+        "custom/*.yml".to_string()
+      ]
+      .iter()
+      .cloned()
+      .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_includes_extra_includes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_includes(&ParsedConfig {
+        extra_includes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec![
+        "../match/**/[!_]*.yml".to_string(),
+        "custom/*.yml".to_string()
+      ]
+      .iter()
+      .cloned()
+      .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_includes_includes_and_extra_includes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_includes(&ParsedConfig {
+        includes: Some(vec!["sub/*.yml".to_string()]),
+        extra_includes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec![
+        "../match/**/[!_]*.yml".to_string(),
+        "custom/*.yml".to_string(),
+        "sub/*.yml".to_string()
+      ]
+      .iter()
+      .cloned()
+      .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_excludes_empty_config() {
+    assert_eq!(
+      ResolvedConfig::aggregate_excludes(&ParsedConfig {
+        ..Default::default()
+      })
+      .len(),
+      0
+    );
+  }
+
+  #[test]
+  fn aggregate_excludes_no_standard() {
+    assert_eq!(
+      ResolvedConfig::aggregate_excludes(&ParsedConfig {
+        use_standard_includes: Some(false),
+        ..Default::default()
+      }),
+      HashSet::new()
+    );
+  }
+
+  #[test]
+  fn aggregate_excludes_custom_excludes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_excludes(&ParsedConfig {
+        excludes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec!["custom/*.yml".to_string()]
+        .iter()
+        .cloned()
+        .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_excludes_extra_excludes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_excludes(&ParsedConfig {
+        extra_excludes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec!["custom/*.yml".to_string()]
+        .iter()
+        .cloned()
+        .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn aggregate_excludes_excludes_and_extra_excludes() {
+    assert_eq!(
+      ResolvedConfig::aggregate_excludes(&ParsedConfig {
+        excludes: Some(vec!["sub/*.yml".to_string()]),
+        extra_excludes: Some(vec!["custom/*.yml".to_string()]),
+        ..Default::default()
+      }),
+      vec!["custom/*.yml".to_string(), "sub/*.yml".to_string()]
+        .iter()
+        .cloned()
+        .collect::<HashSet<_>>()
+    );
+  }
+
+  #[test]
+  fn merge_parent_field_parent_fallback() {
+    let parent = ParsedConfig {
+      use_standard_includes: Some(false),
+      ..Default::default()
+    };
+    let mut child = ParsedConfig {
+      ..Default::default()
+    };
+    assert_eq!(child.use_standard_includes, None);
+
+    ResolvedConfig::merge_parsed(&mut child, &parent);
+    assert_eq!(child.use_standard_includes, Some(false));
+  }
+
+  #[test]
+  fn merge_parent_field_child_overwrite_parent() {
+    let parent = ParsedConfig {
+      use_standard_includes: Some(true),
+      ..Default::default()
+    };
+    let mut child = ParsedConfig {
+      use_standard_includes: Some(false),
+      ..Default::default()
+    };
+    assert_eq!(child.use_standard_includes, Some(false));
+
+    ResolvedConfig::merge_parsed(&mut child, &parent);
+    assert_eq!(child.use_standard_includes, Some(false));
+  }
+
+  #[test]
+  fn match_paths_generated_correctly() {
+    use_test_directory(|_, match_dir, config_dir| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, "").unwrap();
+
+      let config = ResolvedConfig::load(&config_file, None).unwrap();
+
+      let mut expected = vec![
+        base_file.to_string_lossy().to_string(),
+        another_file.to_string_lossy().to_string(),
+        sub_file.to_string_lossy().to_string(),
+      ];
+      expected.sort();
+
+      let mut result = config.match_paths().to_vec();
+      result.sort();
+
+      assert_eq!(result, expected.as_slice());
+    });
+  }
+
+  #[test]
+  fn match_paths_generated_correctly_with_child_config() {
+    use_test_directory(|_, match_dir, config_dir| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("another.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+      let sub_under_file = sub_dir.join("_sub.yml");
+      std::fs::write(&sub_under_file, "test").unwrap();
+
+      // Configs
+
+      let parent_file = config_dir.join("parent.yml");
+      std::fs::write(
+        &parent_file,
+        r#"
+      excludes: ['../**/another.yml']
+      "#,
+      )
+      .unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(
+        &config_file,
+        r#"
+      use_standard_includes: false
+      excludes: []
+      includes: ["../match/sub/*.yml"]
+      "#,
+      )
+      .unwrap();
+
+      let parent = ResolvedConfig::load(&parent_file, None).unwrap();
+      let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
+
+      let mut expected = vec![
+        sub_file.to_string_lossy().to_string(),
+        sub_under_file.to_string_lossy().to_string(),
+      ];
+      expected.sort();
+
+      let mut result = child.match_paths().to_vec();
+      result.sort();
+      assert_eq!(result, expected.as_slice());
+
+      let expected = vec![base_file.to_string_lossy().to_string()];
+
+      assert_eq!(parent.match_paths(), expected.as_slice());
+    });
+  }
+
+  #[test]
+  fn match_paths_generated_correctly_with_underscore_files() {
+    use_test_directory(|_, match_dir, config_dir| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(&under_file, "test").unwrap();
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, "extra_includes: ['../match/_sub.yml']").unwrap();
+
+      let config = ResolvedConfig::load(&config_file, None).unwrap();
+
+      let mut expected = vec![
+        base_file.to_string_lossy().to_string(),
+        another_file.to_string_lossy().to_string(),
+        sub_file.to_string_lossy().to_string(),
+        under_file.to_string_lossy().to_string(),
+      ];
+      expected.sort();
+
+      let mut result = config.match_paths().to_vec();
+      result.sort();
+
+      assert_eq!(result, expected.as_slice());
+    });
+  }
+
+  fn test_filter_is_match(config: &str, app: &AppProperties) -> bool {
+    let mut result = false;
+    let result_ref = &mut result;
+    use_test_directory(move |_, _, config_dir| {
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, config).unwrap();
+
+      let config = ResolvedConfig::load(&config_file, None).unwrap();
+
+      *result_ref = config.is_match(app)
+    });
+    result
+  }
+
+  #[test]
+  fn is_match_no_filters() {
+    assert!(!test_filter_is_match(
+      "",
+      &AppProperties {
+        title: Some("Google"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+  }
+
+  #[test]
+  fn is_match_filter_title() {
+    assert!(test_filter_is_match(
+      "filter_title: Google",
+      &AppProperties {
+        title: Some("Google Mail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_title: Google",
+      &AppProperties {
+        title: Some("Yahoo"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_title: Google",
+      &AppProperties {
+        title: None,
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+  }
+
+  #[test]
+  fn is_match_filter_class() {
+    assert!(test_filter_is_match(
+      "filter_class: Chrome",
+      &AppProperties {
+        title: Some("Google Mail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_class: Chrome",
+      &AppProperties {
+        title: Some("Yahoo"),
+        class: Some("Another"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_class: Chrome",
+      &AppProperties {
+        title: Some("google"),
+        class: None,
+        exec: Some("chrome.exe"),
+      },
+    ));
+  }
+
+  #[test]
+  fn is_match_filter_exec() {
+    assert!(test_filter_is_match(
+      "filter_exec: chrome.exe",
+      &AppProperties {
+        title: Some("Google Mail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_exec: chrome.exe",
+      &AppProperties {
+        title: Some("Yahoo"),
+        class: Some("Another"),
+        exec: Some("zoom.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      "filter_exec: chrome.exe",
+      &AppProperties {
+        title: Some("google"),
+        class: Some("Chrome"),
+        exec: None,
+      },
+    ));
+  }
+
+  #[test]
+  fn is_match_filter_os() {
+    let (current, another) = if cfg!(target_os = "windows") {
+      ("windows", "macos")
+    } else if cfg!(target_os = "macos") {
+      ("macos", "windows")
+    } else if cfg!(target_os = "linux") {
+      ("linux", "macos")
+    } else {
+      ("invalid", "invalid")
+    };
+
+    assert!(test_filter_is_match(
+      &format!("filter_os: {}", current),
+      &AppProperties {
+        title: Some("Google Mail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      &format!("filter_os: {}", another),
+      &AppProperties {
+        title: Some("Google Mail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+  }
+
+  #[test]
+  fn is_match_multiple_filters() {
+    assert!(test_filter_is_match(
+      r#"
+      filter_exec: chrome.exe
+      filter_title: "Youtube"
+      "#,
+      &AppProperties {
+        title: Some("Youtube - Broadcast Yourself"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+
+    assert!(!test_filter_is_match(
+      r#"
+      filter_exec: chrome.exe
+      filter_title: "Youtube"
+      "#,
+      &AppProperties {
+        title: Some("Gmail"),
+        class: Some("Chrome"),
+        exec: Some("chrome.exe"),
+      },
+    ));
+  }
+}
diff --git a/espanso-config/src/config/store.rs b/espanso-config/src/config/store.rs
new file mode 100644
index 00000000..e4e67156
--- /dev/null
+++ b/espanso-config/src/config/store.rs
@@ -0,0 +1,196 @@
+/*
+ * 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 crate::error::NonFatalErrorSet;
+
+use super::{resolve::ResolvedConfig, Config, ConfigStore, ConfigStoreError};
+use anyhow::{Context, Result};
+use log::{debug, error};
+use std::sync::Arc;
+use std::{collections::HashSet, path::Path};
+
+pub(crate) struct DefaultConfigStore {
+  default: Arc<dyn Config>,
+  customs: Vec<Arc<dyn Config>>,
+}
+
+impl ConfigStore for DefaultConfigStore {
+  fn default(&self) -> Arc<dyn super::Config> {
+    Arc::clone(&self.default)
+  }
+
+  fn active<'a>(&'a self, app: &super::AppProperties) -> Arc<dyn super::Config> {
+    // Find a custom config that matches or fallback to the default one
+    for custom in self.customs.iter() {
+      if custom.is_match(app) {
+        return Arc::clone(custom);
+      }
+    }
+    Arc::clone(&self.default)
+  }
+
+  fn configs(&self) -> Vec<Arc<dyn Config>> {
+    let mut configs = vec![Arc::clone(&self.default)];
+
+    for custom in self.customs.iter() {
+      configs.push(Arc::clone(custom));
+    }
+
+    configs
+  }
+
+  // TODO: test
+  fn get_all_match_paths(&self) -> HashSet<String> {
+    let mut paths = HashSet::new();
+
+    paths.extend(self.default().match_paths().iter().cloned());
+    for custom in self.customs.iter() {
+      paths.extend(custom.match_paths().iter().cloned());
+    }
+
+    paths
+  }
+}
+
+impl DefaultConfigStore {
+  pub fn load(config_dir: &Path) -> Result<(Self, Vec<NonFatalErrorSet>)> {
+    if !config_dir.is_dir() {
+      return Err(ConfigStoreError::InvalidConfigDir().into());
+    }
+
+    // First get the default.yml file
+    let default_file = config_dir.join("default.yml");
+    if !default_file.exists() || !default_file.is_file() {
+      return Err(ConfigStoreError::MissingDefault().into());
+    }
+
+    let mut non_fatal_errors = Vec::new();
+
+    let default = ResolvedConfig::load(&default_file, None)
+      .context("failed to load default.yml configuration")?;
+    debug!("loaded default config at path: {:?}", default_file);
+
+    // Then the others
+    let mut customs: Vec<Arc<dyn Config>> = Vec::new();
+    for entry in std::fs::read_dir(config_dir).map_err(ConfigStoreError::IOError)? {
+      let entry = entry?;
+      let config_file = entry.path();
+      let extension = config_file
+        .extension()
+        .unwrap_or_default()
+        .to_string_lossy()
+        .to_lowercase();
+
+      // Additional config files are loaded best-effort
+      if config_file.is_file()
+        && config_file != default_file
+        && (extension == "yml" || extension == "yaml")
+      {
+        match ResolvedConfig::load(&config_file, Some(&default)) {
+          Ok(config) => {
+            customs.push(Arc::new(config));
+            debug!("loaded config at path: {:?}", config_file);
+          }
+          Err(err) => {
+            error!(
+              "unable to load config at path: {:?}, with error: {}",
+              config_file, err
+            );
+            non_fatal_errors.push(NonFatalErrorSet::single_error(&config_file, err));
+          }
+        }
+      }
+    }
+
+    Ok((
+      Self {
+        default: Arc::new(default),
+        customs,
+      },
+      non_fatal_errors,
+    ))
+  }
+
+  pub fn from_configs(default: Arc<dyn Config>, customs: Vec<Arc<dyn Config>>) -> Result<Self> {
+    Ok(Self { default, customs })
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::config::MockConfig;
+
+  pub fn new_mock(label: &'static str, is_match: bool) -> MockConfig {
+    let label = label.to_owned();
+    let mut mock = MockConfig::new();
+    mock.expect_id().return_const(0);
+    mock.expect_label().return_const(label);
+    mock.expect_is_match().return_const(is_match);
+    mock
+  }
+
+  #[test]
+  fn config_store_selects_correctly() {
+    let default = new_mock("default", false);
+    let custom1 = new_mock("custom1", false);
+    let custom2 = new_mock("custom2", true);
+
+    let store = DefaultConfigStore {
+      default: Arc::new(default),
+      customs: vec![Arc::new(custom1), Arc::new(custom2)],
+    };
+
+    assert_eq!(store.default().label(), "default");
+    assert_eq!(
+      store
+        .active(&crate::config::AppProperties {
+          title: None,
+          class: None,
+          exec: None,
+        })
+        .label(),
+      "custom2"
+    );
+  }
+
+  #[test]
+  fn config_store_active_fallback_to_default_if_no_match() {
+    let default = new_mock("default", false);
+    let custom1 = new_mock("custom1", false);
+    let custom2 = new_mock("custom2", false);
+
+    let store = DefaultConfigStore {
+      default: Arc::new(default),
+      customs: vec![Arc::new(custom1), Arc::new(custom2)],
+    };
+
+    assert_eq!(store.default().label(), "default");
+    assert_eq!(
+      store
+        .active(&crate::config::AppProperties {
+          title: None,
+          class: None,
+          exec: None,
+        })
+        .label(),
+      "default"
+    );
+  }
+}
diff --git a/espanso-config/src/config/util.rs b/espanso-config/src/config/util.rs
new file mode 100644
index 00000000..0492d72f
--- /dev/null
+++ b/espanso-config/src/config/util.rs
@@ -0,0 +1,80 @@
+/*
+ * 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/>.
+ */
+
+#[macro_export]
+macro_rules! merge {
+  ( $t:ident, $child:expr, $parent:expr, $( $x:ident ),* ) => {
+    {
+      $(
+        if $child.$x.is_none() {
+          $child.$x = $parent.$x.clone();
+        }
+      )*
+
+      // Build a temporary object to verify that all fields
+      // are being used at compile time
+      $t {
+        $(
+          $x: None,
+        )*
+      };
+    }
+  };
+}
+
+pub fn os_matches(os: &str) -> bool {
+  match os {
+    "macos" => cfg!(target_os = "macos"),
+    "windows" => cfg!(target_os = "windows"),
+    "linux" => cfg!(target_os = "linux"),
+    _ => false,
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  #[cfg(target_os = "linux")]
+  fn os_matches_linux() {
+    assert!(os_matches("linux"));
+    assert!(!os_matches("windows"));
+    assert!(!os_matches("macos"));
+    assert!(!os_matches("invalid"));
+  }
+
+  #[test]
+  #[cfg(target_os = "macos")]
+  fn os_matches_macos() {
+    assert!(os_matches("macos"));
+    assert!(!os_matches("windows"));
+    assert!(!os_matches("linux"));
+    assert!(!os_matches("invalid"));
+  }
+
+  #[test]
+  #[cfg(target_os = "windows")]
+  fn os_matches_windows() {
+    assert!(os_matches("windows"));
+    assert!(!os_matches("macos"));
+    assert!(!os_matches("linux"));
+    assert!(!os_matches("invalid"));
+  }
+}
diff --git a/espanso-config/src/counter.rs b/espanso-config/src/counter.rs
new file mode 100644
index 00000000..565bd041
--- /dev/null
+++ b/espanso-config/src/counter.rs
@@ -0,0 +1,34 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::atomic::{AtomicI32, Ordering};
+
+thread_local! {
+  // TODO: if thread local, we probably don't need an atomic
+  static STRUCT_COUNTER: AtomicI32 = AtomicI32::new(0);
+}
+
+pub type StructId = i32;
+
+/// Some structs need a unique id.
+/// In order to generate it, we use an atomic static variable
+/// that is incremented for each struct.
+pub fn next_id() -> StructId {
+  STRUCT_COUNTER.with(|count| count.fetch_add(1, Ordering::SeqCst))
+}
diff --git a/espanso-config/src/error.rs b/espanso-config/src/error.rs
new file mode 100644
index 00000000..d376013c
--- /dev/null
+++ b/espanso-config/src/error.rs
@@ -0,0 +1,71 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Error;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug)]
+pub struct NonFatalErrorSet {
+  pub file: PathBuf,
+  pub errors: Vec<ErrorRecord>,
+}
+
+impl NonFatalErrorSet {
+  pub fn new(file: &Path, non_fatal_errors: Vec<ErrorRecord>) -> Self {
+    Self {
+      file: file.to_owned(),
+      errors: non_fatal_errors,
+    }
+  }
+
+  pub fn single_error(file: &Path, error: Error) -> Self {
+    Self {
+      file: file.to_owned(),
+      errors: vec![ErrorRecord::error(error)],
+    }
+  }
+}
+
+#[derive(Debug)]
+pub struct ErrorRecord {
+  pub level: ErrorLevel,
+  pub error: Error,
+}
+
+impl ErrorRecord {
+  pub fn error(error: Error) -> Self {
+    Self {
+      level: ErrorLevel::Error,
+      error,
+    }
+  }
+
+  pub fn warn(error: Error) -> Self {
+    Self {
+      level: ErrorLevel::Warning,
+      error,
+    }
+  }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum ErrorLevel {
+  Error,
+  Warning,
+}
diff --git a/espanso-config/src/legacy/config.rs b/espanso-config/src/legacy/config.rs
new file mode 100644
index 00000000..87ff50c0
--- /dev/null
+++ b/espanso-config/src/legacy/config.rs
@@ -0,0 +1,1871 @@
+// This file is taken from the old version of espanso, and used to load
+// the "legacy" config format
+
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019 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 super::model::{KeyModifier, PasteShortcut};
+use log::error;
+use serde::{Deserialize, Serialize};
+use serde_yaml::Value;
+use std::collections::{HashMap, HashSet};
+use std::error::Error;
+use std::fmt;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use walkdir::{DirEntry, WalkDir};
+
+pub const DEFAULT_CONFIG_FILE_NAME: &str = "default.yml";
+pub const USER_CONFIGS_FOLDER_NAME: &str = "user";
+
+// Default values for primitives
+fn default_name() -> String {
+  "default".to_owned()
+}
+fn default_parent() -> String {
+  "self".to_owned()
+}
+fn default_filter_title() -> String {
+  "".to_owned()
+}
+fn default_filter_class() -> String {
+  "".to_owned()
+}
+fn default_filter_exec() -> String {
+  "".to_owned()
+}
+fn default_log_level() -> i32 {
+  0
+}
+fn default_conflict_check() -> bool {
+  false
+}
+fn default_ipc_server_port() -> i32 {
+  34982
+}
+fn default_worker_ipc_server_port() -> i32 {
+  34983
+}
+fn default_use_system_agent() -> bool {
+  true
+}
+fn default_config_caching_interval() -> i32 {
+  800
+}
+fn default_word_separators() -> Vec<char> {
+  vec![' ', ',', '.', '?', '!', '\r', '\n', 22u8 as char]
+}
+fn default_toggle_interval() -> u32 {
+  230
+}
+fn default_toggle_key() -> KeyModifier {
+  KeyModifier::ALT
+}
+fn default_preserve_clipboard() -> bool {
+  true
+}
+fn default_passive_match_regex() -> String {
+  "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned()
+}
+fn default_passive_arg_delimiter() -> char {
+  '/'
+}
+fn default_passive_arg_escape() -> char {
+  '\\'
+}
+fn default_passive_delay() -> u64 {
+  100
+}
+fn default_passive_key() -> KeyModifier {
+  KeyModifier::OFF
+}
+fn default_enable_passive() -> bool {
+  false
+}
+fn default_enable_active() -> bool {
+  true
+}
+fn default_backspace_limit() -> i32 {
+  3
+}
+fn default_backspace_delay() -> i32 {
+  0
+}
+fn default_inject_delay() -> i32 {
+  0
+}
+fn default_restore_clipboard_delay() -> i32 {
+  300
+}
+fn default_exclude_default_entries() -> bool {
+  false
+}
+fn default_secure_input_watcher_enabled() -> bool {
+  true
+}
+fn default_secure_input_notification() -> bool {
+  true
+}
+fn default_show_notifications() -> bool {
+  true
+}
+fn default_auto_restart() -> bool {
+  true
+}
+fn default_undo_backspace() -> bool {
+  true
+}
+fn default_show_icon() -> bool {
+  true
+}
+fn default_fast_inject() -> bool {
+  true
+}
+fn default_secure_input_watcher_interval() -> i32 {
+  5000
+}
+fn default_matches() -> Vec<Value> {
+  Vec::new()
+}
+fn default_global_vars() -> Vec<Value> {
+  Vec::new()
+}
+fn default_modulo_path() -> Option<String> {
+  None
+}
+fn default_post_inject_delay() -> u64 {
+  100
+}
+fn default_wait_for_modifiers_release() -> bool {
+  false
+}
+fn default_search_trigger() -> Option<String> {
+  Some("jkj".to_string())
+}
+fn default_search_shortcut() -> Option<String> {
+  Some("ALT+SPACE".to_string())
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct LegacyConfig {
+  #[serde(default = "default_name")]
+  pub name: String,
+
+  #[serde(default = "default_parent")]
+  pub parent: String,
+
+  #[serde(default = "default_filter_title")]
+  pub filter_title: String,
+
+  #[serde(default = "default_filter_class")]
+  pub filter_class: String,
+
+  #[serde(default = "default_filter_exec")]
+  pub filter_exec: String,
+
+  #[serde(default = "default_log_level")]
+  pub log_level: i32,
+
+  #[serde(default = "default_conflict_check")]
+  pub conflict_check: bool,
+
+  #[serde(default = "default_ipc_server_port")]
+  pub ipc_server_port: i32,
+
+  #[serde(default = "default_worker_ipc_server_port")]
+  pub worker_ipc_server_port: i32,
+
+  #[serde(default = "default_use_system_agent")]
+  pub use_system_agent: bool,
+
+  #[serde(default = "default_config_caching_interval")]
+  pub config_caching_interval: i32,
+
+  #[serde(default = "default_word_separators")]
+  pub word_separators: Vec<char>,
+
+  #[serde(default = "default_toggle_key")]
+  pub toggle_key: KeyModifier,
+
+  #[serde(default = "default_toggle_interval")]
+  pub toggle_interval: u32,
+
+  #[serde(default = "default_preserve_clipboard")]
+  pub preserve_clipboard: bool,
+
+  #[serde(default = "default_passive_match_regex")]
+  pub passive_match_regex: String,
+
+  #[serde(default = "default_passive_arg_delimiter")]
+  pub passive_arg_delimiter: char,
+
+  #[serde(default = "default_passive_arg_escape")]
+  pub passive_arg_escape: char,
+
+  #[serde(default = "default_passive_key")]
+  pub passive_key: KeyModifier,
+
+  #[serde(default = "default_passive_delay")]
+  pub passive_delay: u64,
+
+  #[serde(default = "default_enable_passive")]
+  pub enable_passive: bool,
+
+  #[serde(default = "default_enable_active")]
+  pub enable_active: bool,
+
+  #[serde(default = "default_undo_backspace")]
+  pub undo_backspace: bool,
+
+  #[serde(default)]
+  pub paste_shortcut: PasteShortcut,
+
+  #[serde(default = "default_backspace_limit")]
+  pub backspace_limit: i32,
+
+  #[serde(default = "default_restore_clipboard_delay")]
+  pub restore_clipboard_delay: i32,
+
+  #[serde(default = "default_secure_input_watcher_enabled")]
+  pub secure_input_watcher_enabled: bool,
+
+  #[serde(default = "default_secure_input_watcher_interval")]
+  pub secure_input_watcher_interval: i32,
+
+  #[serde(default = "default_post_inject_delay")]
+  pub post_inject_delay: u64,
+
+  #[serde(default = "default_secure_input_notification")]
+  pub secure_input_notification: bool,
+
+  #[serde(default)]
+  pub backend: BackendType,
+
+  #[serde(default = "default_exclude_default_entries")]
+  pub exclude_default_entries: bool,
+
+  #[serde(default = "default_show_notifications")]
+  pub show_notifications: bool,
+
+  #[serde(default = "default_show_icon")]
+  pub show_icon: bool,
+
+  #[serde(default = "default_fast_inject")]
+  pub fast_inject: bool,
+
+  #[serde(default = "default_backspace_delay")]
+  pub backspace_delay: i32,
+
+  #[serde(default = "default_inject_delay")]
+  pub inject_delay: i32,
+
+  #[serde(default = "default_auto_restart")]
+  pub auto_restart: bool,
+
+  #[serde(default = "default_matches")]
+  pub matches: Vec<Value>,
+
+  #[serde(default = "default_global_vars")]
+  pub global_vars: Vec<Value>,
+
+  #[serde(default = "default_modulo_path")]
+  pub modulo_path: Option<String>,
+
+  #[serde(default = "default_search_trigger")]
+  pub search_trigger: Option<String>,
+
+  #[serde(default = "default_search_shortcut")]
+  pub search_shortcut: Option<String>,
+
+  #[serde(default = "default_wait_for_modifiers_release")]
+  pub wait_for_modifiers_release: bool,
+}
+
+// Macro used to validate config fields
+#[macro_export]
+macro_rules! validate_field {
+    ($result:expr, $field:expr, $def_value:expr) => {
+        if $field != $def_value {
+            let mut field_name = stringify!($field);
+            if field_name.starts_with("self.") {
+                field_name = &field_name[5..];  // Remove the 'self.' prefix
+            }
+            error!("Validation error, parameter '{}' is reserved and can be only used in the default.yml config file", field_name);
+            $result = false;
+        }
+    };
+}
+
+impl LegacyConfig {
+  /*
+   * Validate the Config instance.
+   * It makes sure that user defined config instances do not define
+   * attributes reserved to the default config.
+   */
+  fn validate_user_defined_config(&self) -> bool {
+    let mut result = true;
+
+    validate_field!(
+      result,
+      self.config_caching_interval,
+      default_config_caching_interval()
+    );
+    validate_field!(result, self.log_level, default_log_level());
+    validate_field!(result, self.conflict_check, default_conflict_check());
+    validate_field!(result, self.toggle_key, default_toggle_key());
+    validate_field!(result, self.toggle_interval, default_toggle_interval());
+    validate_field!(result, self.backspace_limit, default_backspace_limit());
+    validate_field!(result, self.ipc_server_port, default_ipc_server_port());
+    validate_field!(result, self.use_system_agent, default_use_system_agent());
+    validate_field!(
+      result,
+      self.preserve_clipboard,
+      default_preserve_clipboard()
+    );
+    validate_field!(
+      result,
+      self.passive_match_regex,
+      default_passive_match_regex()
+    );
+    validate_field!(
+      result,
+      self.passive_arg_delimiter,
+      default_passive_arg_delimiter()
+    );
+    validate_field!(
+      result,
+      self.passive_arg_escape,
+      default_passive_arg_escape()
+    );
+    validate_field!(result, self.passive_key, default_passive_key());
+    validate_field!(
+      result,
+      self.restore_clipboard_delay,
+      default_restore_clipboard_delay()
+    );
+    validate_field!(
+      result,
+      self.secure_input_watcher_enabled,
+      default_secure_input_watcher_enabled()
+    );
+    validate_field!(
+      result,
+      self.secure_input_watcher_interval,
+      default_secure_input_watcher_interval()
+    );
+    validate_field!(
+      result,
+      self.secure_input_notification,
+      default_secure_input_notification()
+    );
+    validate_field!(
+      result,
+      self.show_notifications,
+      default_show_notifications()
+    );
+    validate_field!(result, self.show_icon, default_show_icon());
+
+    result
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub enum BackendType {
+  Inject,
+  Clipboard,
+  Auto,
+}
+impl Default for BackendType {
+  fn default() -> Self {
+    BackendType::Auto
+  }
+}
+
+impl LegacyConfig {
+  fn load_config(path: &Path) -> Result<LegacyConfig, ConfigLoadError> {
+    let file_res = File::open(path);
+    if let Ok(mut file) = file_res {
+      let mut contents = String::new();
+      let res = file.read_to_string(&mut contents);
+
+      if res.is_err() {
+        return Err(ConfigLoadError::UnableToReadFile);
+      }
+
+      let config_res = serde_yaml::from_str(&contents);
+
+      match config_res {
+        Ok(config) => Ok(config),
+        Err(e) => Err(ConfigLoadError::InvalidYAML(path.to_owned(), e.to_string())),
+      }
+    } else {
+      eprintln!("Error: Cannot load file {:?}", path);
+      Err(ConfigLoadError::FileNotFound)
+    }
+  }
+
+  fn merge_overwrite(&mut self, new_config: LegacyConfig) {
+    // Merge matches
+    let mut merged_matches = new_config.matches;
+    let mut match_trigger_set = HashSet::new();
+    merged_matches.iter().for_each(|m| {
+      match_trigger_set.extend(triggers_for_match(m));
+    });
+    let parent_matches: Vec<Value> = self
+      .matches
+      .iter()
+      .filter(|&m| {
+        !triggers_for_match(m)
+          .iter()
+          .any(|trigger| match_trigger_set.contains(trigger))
+      })
+      .cloned()
+      .collect();
+
+    merged_matches.extend(parent_matches);
+    self.matches = merged_matches;
+
+    // Merge global variables
+    let mut merged_global_vars = new_config.global_vars;
+    let mut vars_name_set = HashSet::new();
+    merged_global_vars.iter().for_each(|m| {
+      vars_name_set.insert(name_for_global_var(m));
+    });
+    let parent_vars: Vec<Value> = self
+      .global_vars
+      .iter()
+      .filter(|&m| !vars_name_set.contains(&name_for_global_var(m)))
+      .cloned()
+      .collect();
+
+    merged_global_vars.extend(parent_vars);
+    self.global_vars = merged_global_vars;
+  }
+
+  fn merge_no_overwrite(&mut self, default: &LegacyConfig) {
+    // Merge matches
+    let mut match_trigger_set = HashSet::new();
+    self.matches.iter().for_each(|m| {
+      match_trigger_set.extend(triggers_for_match(m));
+    });
+    let default_matches: Vec<Value> = default
+      .matches
+      .iter()
+      .filter(|&m| {
+        !triggers_for_match(m)
+          .iter()
+          .any(|trigger| match_trigger_set.contains(trigger))
+      })
+      .cloned()
+      .collect();
+
+    self.matches.extend(default_matches);
+
+    // Merge global variables
+    let mut vars_name_set = HashSet::new();
+    self.global_vars.iter().for_each(|m| {
+      vars_name_set.insert(name_for_global_var(m));
+    });
+    let default_vars: Vec<Value> = default
+      .global_vars
+      .iter()
+      .filter(|&m| !vars_name_set.contains(&name_for_global_var(m)))
+      .cloned()
+      .collect();
+
+    self.global_vars.extend(default_vars);
+  }
+}
+
+fn triggers_for_match(m: &Value) -> Vec<String> {
+  if let Some(triggers) = m.get("triggers").and_then(|v| v.as_sequence()) {
+    triggers
+      .iter()
+      .filter_map(|v| v.as_str().map(|s| s.to_string()))
+      .collect()
+  } else if let Some(trigger) = m.get("trigger").and_then(|v| v.as_str()) {
+    vec![trigger.to_string()]
+  } else {
+    vec![]
+  }
+}
+
+#[allow(dead_code)]
+fn replace_for_match(m: &Value) -> String {
+  m.get("replace")
+    .and_then(|v| v.as_str())
+    .expect("match is missing replace field")
+    .to_string()
+}
+
+fn name_for_global_var(v: &Value) -> String {
+  v.get("name")
+    .and_then(|v| v.as_str())
+    .expect("global var is missing name field")
+    .to_string()
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct LegacyConfigSet {
+  pub default: LegacyConfig,
+  pub specific: Vec<LegacyConfig>,
+}
+
+impl LegacyConfigSet {
+  pub fn load(config_dir: &Path, package_dir: &Path) -> Result<LegacyConfigSet, ConfigLoadError> {
+    if !config_dir.is_dir() {
+      return Err(ConfigLoadError::InvalidConfigDirectory);
+    }
+
+    // Load default configuration
+    let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME);
+    let default = LegacyConfig::load_config(default_file.as_path())?;
+
+    // Analyze which config files have to be loaded
+
+    let mut target_files = Vec::new();
+
+    let specific_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME);
+    if specific_dir.exists() {
+      let dir_entry = WalkDir::new(specific_dir);
+      target_files.extend(dir_entry);
+    }
+
+    let package_files = if package_dir.exists() {
+      let dir_entry = WalkDir::new(package_dir);
+      dir_entry.into_iter().collect()
+    } else {
+      Vec::new()
+    };
+
+    // Load the user defined config files
+
+    let mut name_set = HashSet::new();
+    let mut children_map: HashMap<String, Vec<LegacyConfig>> = HashMap::new();
+    let mut package_map: HashMap<String, Vec<LegacyConfig>> = HashMap::new();
+    let mut root_configs = vec![default];
+
+    let mut file_loader = |entry: walkdir::Result<DirEntry>,
+                           dest_map: &mut HashMap<String, Vec<LegacyConfig>>|
+     -> Result<(), ConfigLoadError> {
+      match entry {
+        Ok(entry) => {
+          let path = entry.path();
+
+          // Skip non-yaml config files
+          if path
+            .extension()
+            .unwrap_or_default()
+            .to_str()
+            .unwrap_or_default()
+            != "yml"
+          {
+            return Ok(());
+          }
+
+          // Skip hidden files
+          if path
+            .file_name()
+            .unwrap_or_default()
+            .to_str()
+            .unwrap_or_default()
+            .starts_with('.')
+          {
+            return Ok(());
+          }
+
+          let mut config = LegacyConfig::load_config(path)?;
+
+          // Make sure the config does not contain reserved fields
+          if !config.validate_user_defined_config() {
+            return Err(ConfigLoadError::InvalidParameter(path.to_owned()));
+          }
+
+          // No name specified, defaulting to the path name
+          if config.name == "default" {
+            config.name = path.to_str().unwrap_or_default().to_owned();
+          }
+
+          if name_set.contains(&config.name) {
+            return Err(ConfigLoadError::NameDuplicate(path.to_owned()));
+          }
+
+          name_set.insert(config.name.clone());
+
+          if config.parent == "self" {
+            // No parent, root config
+            root_configs.push(config);
+          } else {
+            // Children config
+            let children_vec = dest_map.entry(config.parent.clone()).or_default();
+            children_vec.push(config);
+          }
+        }
+        Err(e) => {
+          eprintln!("Warning: Unable to read config file: {}", e);
+        }
+      }
+
+      Ok(())
+    };
+
+    // Load the default and user specific configs
+    for entry in target_files {
+      file_loader(entry, &mut children_map)?;
+    }
+
+    // Load the package related configs
+    for entry in package_files {
+      file_loader(entry, &mut package_map)?;
+    }
+
+    // Merge the children config files
+    let mut configs_without_packages = Vec::new();
+    for root_config in root_configs {
+      let config = LegacyConfigSet::reduce_configs(root_config, &children_map, true);
+      configs_without_packages.push(config);
+    }
+
+    // Merge package files
+    // Note: we need two different steps as the packages have a lower priority
+    //       than configs.
+    let mut configs = Vec::new();
+    for root_config in configs_without_packages {
+      let config = LegacyConfigSet::reduce_configs(root_config, &package_map, false);
+      configs.push(config);
+    }
+
+    // Separate default from specific
+    let default = configs.get(0).unwrap().clone();
+    let mut specific = (&configs[1..]).to_vec();
+
+    // Add default entries to specific configs when needed
+    for config in specific.iter_mut() {
+      if !config.exclude_default_entries {
+        config.merge_no_overwrite(&default);
+      }
+    }
+
+    // Check if some triggers are conflicting with each other
+    // For more information, see: https://github.com/federico-terzi/espanso/issues/135
+    if default.conflict_check {
+      let has_conflicts = Self::has_conflicts(&default, &specific);
+      if has_conflicts {
+        eprintln!("Warning: some triggers had conflicts and may not behave as intended");
+        eprintln!("To turn off this check, add \"conflict_check: false\" in the configuration");
+      }
+    }
+
+    Ok(LegacyConfigSet { default, specific })
+  }
+
+  fn reduce_configs(
+    target: LegacyConfig,
+    children_map: &HashMap<String, Vec<LegacyConfig>>,
+    higher_priority: bool,
+  ) -> LegacyConfig {
+    if children_map.contains_key(&target.name) {
+      let mut target = target;
+      for children in children_map.get(&target.name).unwrap() {
+        let children = Self::reduce_configs(children.clone(), children_map, higher_priority);
+        if higher_priority {
+          target.merge_overwrite(children);
+        } else {
+          target.merge_no_overwrite(&children);
+        }
+      }
+      target
+    } else {
+      target
+    }
+  }
+
+  fn has_conflicts(default: &LegacyConfig, specific: &[LegacyConfig]) -> bool {
+    let mut sorted_triggers: Vec<String> = default
+      .matches
+      .iter()
+      .flat_map(|t| triggers_for_match(t))
+      .collect();
+    sorted_triggers.sort();
+
+    let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
+
+    for s in specific.iter() {
+      let mut specific_triggers: Vec<String> = s
+        .matches
+        .iter()
+        .flat_map(|t| triggers_for_match(t))
+        .collect();
+      specific_triggers.sort();
+      has_conflicts |= Self::list_has_conflicts(&specific_triggers);
+    }
+
+    has_conflicts
+  }
+
+  fn list_has_conflicts(sorted_list: &[String]) -> bool {
+    if sorted_list.len() <= 1 {
+      return false;
+    }
+
+    let mut has_conflicts = false;
+
+    for (i, item) in sorted_list.iter().skip(1).enumerate() {
+      let previous = &sorted_list[i];
+      if item.starts_with(previous) {
+        has_conflicts = true;
+        eprintln!(
+          "Warning: trigger '{}' is conflicting with '{}' and may not behave as intended",
+          item, previous
+        );
+      }
+    }
+
+    has_conflicts
+  }
+}
+
+// Error handling
+#[derive(Debug, PartialEq)]
+#[allow(dead_code)]
+pub enum ConfigLoadError {
+  FileNotFound,
+  UnableToReadFile,
+  InvalidYAML(PathBuf, String),
+  InvalidConfigDirectory,
+  InvalidParameter(PathBuf),
+  NameDuplicate(PathBuf),
+  UnableToCreateDefaultConfig,
+}
+
+impl fmt::Display for ConfigLoadError {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    match self {
+            ConfigLoadError::FileNotFound =>  write!(f, "File not found"),
+            ConfigLoadError::UnableToReadFile =>  write!(f, "Unable to read config file"),
+            ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e),
+            ConfigLoadError::InvalidConfigDirectory =>  write!(f, "Invalid config directory"),
+            ConfigLoadError::InvalidParameter(path) =>  write!(f, "Invalid parameter in '{}', use of reserved parameters in used defined configs is not permitted", path.to_str().unwrap_or_default()),
+            ConfigLoadError::NameDuplicate(path) =>  write!(f, "Found duplicate 'name' in '{}', please use different names", path.to_str().unwrap_or_default()),
+            ConfigLoadError::UnableToCreateDefaultConfig =>  write!(f, "Could not generate default config file"),
+        }
+  }
+}
+
+impl Error for ConfigLoadError {
+  fn description(&self) -> &str {
+    match self {
+      ConfigLoadError::FileNotFound => "File not found",
+      ConfigLoadError::UnableToReadFile => "Unable to read config file",
+      ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax",
+      ConfigLoadError::InvalidConfigDirectory => "Invalid config directory",
+      ConfigLoadError::InvalidParameter(_) => {
+        "Invalid parameter, use of reserved parameters in user defined configs is not permitted"
+      }
+      ConfigLoadError::NameDuplicate(_) => {
+        "Found duplicate 'name' in some configurations, please use different names"
+      }
+      ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file",
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use std::fs;
+  use std::fs::create_dir_all;
+  use std::io::Write;
+  use tempfile::{NamedTempFile, TempDir};
+
+  const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("res/test/default.yml");
+  const TEST_WORKING_CONFIG_FILE: &str = include_str!("res/test/working_config.yml");
+  const TEST_CONFIG_FILE_WITH_BAD_YAML: &str = include_str!("res/test/config_with_bad_yaml.yml");
+
+  // Test Configs
+
+  fn create_tmp_file(string: &str) -> NamedTempFile {
+    let file = NamedTempFile::new().unwrap();
+    file.as_file().write_all(string.as_bytes()).unwrap();
+    file
+  }
+
+  #[test]
+  fn test_config_file_not_found() {
+    let config = LegacyConfig::load_config(Path::new("invalid/path"));
+    assert!(config.is_err());
+    assert_eq!(config.unwrap_err(), ConfigLoadError::FileNotFound);
+  }
+
+  #[test]
+  fn test_config_file_with_bad_yaml_syntax() {
+    let broken_config_file = create_tmp_file(TEST_CONFIG_FILE_WITH_BAD_YAML);
+    let config = LegacyConfig::load_config(broken_config_file.path());
+    match config {
+      Ok(_) => unreachable!(),
+      Err(e) => match e {
+        ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, broken_config_file.path().to_owned()),
+        _ => unreachable!(),
+      },
+    }
+  }
+
+  #[test]
+  fn test_validate_field_macro() {
+    let mut result = true;
+
+    validate_field!(result, 3, 3);
+    assert!(result);
+
+    validate_field!(result, 10, 3);
+    assert!(!result);
+
+    validate_field!(result, 3, 3);
+    assert!(!result);
+  }
+
+  #[test]
+  fn test_user_defined_config_does_not_have_reserved_fields() {
+    let working_config_file = create_tmp_file(
+      r###"
+
+        backend: Clipboard
+
+        "###,
+    );
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(config.unwrap().validate_user_defined_config());
+  }
+
+  #[test]
+  fn test_user_defined_config_has_reserved_fields_config_caching_interval() {
+    let working_config_file = create_tmp_file(
+      r###"
+
+        # This should not happen in an app-specific config
+        config_caching_interval: 100
+
+        "###,
+    );
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(!config.unwrap().validate_user_defined_config());
+  }
+
+  #[test]
+  fn test_user_defined_config_has_reserved_fields_toggle_key() {
+    let working_config_file = create_tmp_file(
+      r###"
+
+        # This should not happen in an app-specific config
+        toggle_key: CTRL
+
+        "###,
+    );
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(!config.unwrap().validate_user_defined_config());
+  }
+
+  #[test]
+  fn test_user_defined_config_has_reserved_fields_toggle_interval() {
+    let working_config_file = create_tmp_file(
+      r###"
+
+        # This should not happen in an app-specific config
+        toggle_interval: 1000
+
+        "###,
+    );
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(!config.unwrap().validate_user_defined_config());
+  }
+
+  #[test]
+  fn test_user_defined_config_has_reserved_fields_backspace_limit() {
+    let working_config_file = create_tmp_file(
+      r###"
+
+        # This should not happen in an app-specific config
+        backspace_limit: 10
+
+        "###,
+    );
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(!config.unwrap().validate_user_defined_config());
+  }
+
+  #[test]
+  fn test_config_loaded_correctly() {
+    let working_config_file = create_tmp_file(TEST_WORKING_CONFIG_FILE);
+    let config = LegacyConfig::load_config(working_config_file.path());
+    assert!(config.is_ok());
+  }
+
+  // Test ConfigSet
+
+  pub fn create_temp_espanso_directories() -> (TempDir, TempDir) {
+    create_temp_espanso_directories_with_default_content(DEFAULT_CONFIG_FILE_CONTENT)
+  }
+
+  pub fn create_temp_espanso_directories_with_default_content(
+    default_content: &str,
+  ) -> (TempDir, TempDir) {
+    let data_dir = TempDir::new().expect("unable to create data directory");
+    let package_dir = TempDir::new().expect("unable to create package directory");
+
+    let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
+    fs::write(default_path, default_content).unwrap();
+
+    (data_dir, package_dir)
+  }
+
+  pub fn create_temp_file_in_dir(tmp_dir: &Path, name: &str, content: &str) -> PathBuf {
+    let user_defined_path = tmp_dir.join(name);
+    let user_defined_path_copy = user_defined_path.clone();
+    fs::write(user_defined_path, content).unwrap();
+
+    user_defined_path_copy
+  }
+
+  pub fn create_user_config_file(tmp_dir: &Path, name: &str, content: &str) -> PathBuf {
+    let user_config_dir = tmp_dir.join(USER_CONFIGS_FOLDER_NAME);
+    if !user_config_dir.exists() {
+      create_dir_all(&user_config_dir).unwrap();
+    }
+
+    create_temp_file_in_dir(&user_config_dir, name, content)
+  }
+
+  pub fn create_package_file(
+    package_data_dir: &Path,
+    package_name: &str,
+    filename: &str,
+    content: &str,
+  ) -> PathBuf {
+    let package_dir = package_data_dir.join(package_name);
+    if !package_dir.exists() {
+      create_dir_all(&package_dir).unwrap();
+    }
+
+    create_temp_file_in_dir(&package_dir, filename, content)
+  }
+
+  #[test]
+  fn test_config_set_default_content_should_work_correctly() {
+    let (data_dir, package_dir) = create_temp_espanso_directories();
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    assert!(config_set.is_ok());
+  }
+
+  #[test]
+  fn test_config_set_load_fail_bad_directory() {
+    let config_set = LegacyConfigSet::load(Path::new("invalid/path"), Path::new("invalid/path"));
+    assert!(config_set.is_err());
+    assert_eq!(
+      config_set.unwrap_err(),
+      ConfigLoadError::InvalidConfigDirectory
+    );
+  }
+
+  #[test]
+  fn test_config_set_missing_default_file() {
+    let data_dir = TempDir::new().expect("unable to create temp directory");
+    let package_dir = TempDir::new().expect("unable to create package directory");
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    assert!(config_set.is_err());
+    assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound);
+  }
+
+  #[test]
+  fn test_config_set_invalid_yaml_syntax() {
+    let (data_dir, package_dir) =
+      create_temp_espanso_directories_with_default_content(TEST_CONFIG_FILE_WITH_BAD_YAML);
+    let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    match config_set {
+      Ok(_) => unreachable!(),
+      Err(e) => match e {
+        ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path),
+        _ => unreachable!(),
+      },
+    }
+  }
+
+  #[test]
+  fn test_config_set_specific_file_with_reserved_fields() {
+    let (data_dir, package_dir) = create_temp_espanso_directories();
+
+    let user_defined_path = create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        config_caching_interval: 10000
+        "###,
+    );
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    assert!(config_set.is_err());
+    assert_eq!(
+      config_set.unwrap_err(),
+      ConfigLoadError::InvalidParameter(user_defined_path)
+    )
+  }
+
+  #[test]
+  fn test_config_set_specific_file_missing_name_auto_generated() {
+    let (data_dir, package_dir) = create_temp_espanso_directories();
+
+    let user_defined_path = create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        backend: Clipboard
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    assert!(config_set.is_ok());
+    assert_eq!(
+      config_set.unwrap().specific[0].name,
+      user_defined_path.to_str().unwrap_or_default()
+    )
+  }
+
+  #[test]
+  fn test_config_set_specific_file_duplicate_name() {
+    let (data_dir, package_dir) = create_temp_espanso_directories();
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        name: specific1
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path());
+    assert!(config_set.is_err());
+    assert!(matches!(
+      &config_set.unwrap_err(),
+      &ConfigLoadError::NameDuplicate(_)
+    ))
+  }
+
+  #[test]
+  fn test_user_defined_config_set_merge_with_parent_matches() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ":lol"
+              replace: "LOL"
+            - trigger: ":yess"
+              replace: "Bob"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific1.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: "hello"
+              replace: "newstring"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.default.matches.len(), 2);
+    assert_eq!(config_set.specific[0].matches.len(), 3);
+
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| triggers_for_match(x)[0] == "hello"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| triggers_for_match(x)[0] == ":lol"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| triggers_for_match(x)[0] == ":yess"));
+  }
+
+  #[test]
+  fn test_user_defined_config_set_merge_with_parent_matches_child_priority() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ":lol"
+              replace: "LOL"
+            - trigger: ":yess"
+              replace: "Bob"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: ":lol"
+              replace: "newstring"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.default.matches.len(), 2);
+    assert_eq!(config_set.specific[0].matches.len(), 2);
+
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| { triggers_for_match(x)[0] == ":lol" && replace_for_match(x) == "newstring" }));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| triggers_for_match(x)[0] == ":yess"));
+  }
+
+  #[test]
+  fn test_user_defined_config_set_exclude_merge_with_parent_matches() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ":lol"
+              replace: "LOL"
+            - trigger: ":yess"
+              replace: "Bob"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        name: specific1
+
+        exclude_default_entries: true
+
+        matches:
+            - trigger: "hello"
+              replace: "newstring"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.default.matches.len(), 2);
+    assert_eq!(config_set.specific[0].matches.len(), 1);
+
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|x| { triggers_for_match(x)[0] == "hello" && replace_for_match(x) == "newstring" }));
+  }
+
+  #[test]
+  fn test_only_yaml_files_are_loaded_from_config() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+            matches:
+                - trigger: ":lol"
+                  replace: "LOL"
+                - trigger: ":yess"
+                  replace: "Bob"
+            "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.zzz",
+      r###"
+        name: specific1
+
+        exclude_default_entries: true
+
+        matches:
+            - trigger: "hello"
+              replace: "newstring"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+  }
+
+  #[test]
+  fn test_hidden_files_are_ignored() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+            matches:
+                - trigger: ":lol"
+                  replace: "LOL"
+                - trigger: ":yess"
+                  replace: "Bob"
+            "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      ".specific.yml",
+      r###"
+        name: specific1
+
+        exclude_default_entries: true
+
+        matches:
+            - trigger: "hello"
+              replace: "newstring"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+  }
+
+  #[test]
+  fn test_config_set_no_parent_configs_works_correctly() {
+    let (data_dir, package_dir) = create_temp_espanso_directories();
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        name: specific2
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 2);
+  }
+
+  #[test]
+  fn test_config_set_default_parent_works_correctly() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        parent: default
+
+        matches:
+            - trigger: "hello"
+              replace: "world"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.matches.len(), 2);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hello"));
+  }
+
+  #[test]
+  fn test_config_set_no_parent_should_not_merge() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        matches:
+            - trigger: "hello"
+              replace: "world"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 1);
+    assert_eq!(config_set.default.matches.len(), 1);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(!config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hello"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hello"));
+  }
+
+  #[test]
+  fn test_config_set_default_nested_parent_works_correctly() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: custom1
+        parent: default
+
+        matches:
+            - trigger: "hello"
+              replace: "world"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        parent: custom1
+
+        matches:
+            - trigger: "super"
+              replace: "mario"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.matches.len(), 3);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hello"));
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "super"));
+  }
+
+  #[test]
+  fn test_config_set_parent_merge_children_priority_should_be_higher() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        parent: default
+
+        matches:
+            - trigger: "hasta"
+              replace: "world"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.matches.len(), 1);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| { triggers_for_match(m)[0] == "hasta" && replace_for_match(m) == "world" }));
+  }
+
+  #[test]
+  fn test_config_set_package_configs_default_merge() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_package_file(
+      package_dir.path(),
+      "package1",
+      "package.yml",
+      r###"
+        parent: default
+
+        matches:
+            - trigger: "harry"
+              replace: "potter"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.matches.len(), 2);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "harry"));
+  }
+
+  #[test]
+  fn test_config_set_package_configs_lower_priority_than_user() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_package_file(
+      package_dir.path(),
+      "package1",
+      "package.yml",
+      r###"
+        parent: default
+
+        matches:
+            - trigger: "hasta"
+              replace: "potter"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.matches.len(), 1);
+    assert_eq!(
+      triggers_for_match(&config_set.default.matches[0])[0],
+      "hasta"
+    );
+    assert_eq!(
+      replace_for_match(&config_set.default.matches[0]),
+      "Hasta la vista"
+    );
+  }
+
+  #[test]
+  fn test_config_set_package_configs_without_merge() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_package_file(
+      package_dir.path(),
+      "package1",
+      "package.yml",
+      r###"
+        matches:
+            - trigger: "harry"
+              replace: "potter"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 1);
+    assert_eq!(config_set.default.matches.len(), 1);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "harry"));
+  }
+
+  #[test]
+  fn test_config_set_package_configs_multiple_files() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: hasta
+              replace: Hasta la vista
+        "###,
+    );
+
+    create_package_file(
+      package_dir.path(),
+      "package1",
+      "package.yml",
+      r###"
+        name: package1
+
+        matches:
+            - trigger: "harry"
+              replace: "potter"
+        "###,
+    );
+
+    create_package_file(
+      package_dir.path(),
+      "package1",
+      "addon.yml",
+      r###"
+        parent: package1
+
+        matches:
+            - trigger: "ron"
+              replace: "weasley"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 1);
+    assert_eq!(config_set.default.matches.len(), 1);
+    assert!(config_set
+      .default
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "hasta"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "harry"));
+    assert!(config_set.specific[0]
+      .matches
+      .iter()
+      .any(|m| triggers_for_match(m)[0] == "ron"));
+  }
+
+  #[test]
+  fn test_list_has_conflict_no_conflict() {
+    assert!(!LegacyConfigSet::list_has_conflicts(&[
+      ":ab".to_owned(),
+      ":bc".to_owned()
+    ]));
+  }
+
+  #[test]
+  fn test_list_has_conflict_conflict() {
+    let mut list = vec!["ac".to_owned(), "ab".to_owned(), "abc".to_owned()];
+    list.sort();
+    assert!(LegacyConfigSet::list_has_conflicts(&list));
+  }
+
+  #[test]
+  fn test_has_conflict_no_conflict() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ac
+              replace: Hasta la vista
+            - trigger: bc
+              replace: Jon
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: "hello"
+              replace: "world"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert!(!LegacyConfigSet::has_conflicts(
+      &config_set.default,
+      &config_set.specific
+    ),);
+  }
+
+  #[test]
+  fn test_has_conflict_conflict_in_default() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ac
+              replace: Hasta la vista
+            - trigger: bc
+              replace: Jon
+            - trigger: acb
+              replace: Error
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: "hello"
+              replace: "world"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert!(LegacyConfigSet::has_conflicts(
+      &config_set.default,
+      &config_set.specific
+    ),);
+  }
+
+  #[test]
+  fn test_has_conflict_conflict_in_specific_and_default() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ac
+              replace: Hasta la vista
+            - trigger: bc
+              replace: Jon
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: "bcd"
+              replace: "Conflict"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert!(LegacyConfigSet::has_conflicts(
+      &config_set.default,
+      &config_set.specific
+    ),);
+  }
+
+  #[test]
+  fn test_has_conflict_no_conflict_in_specific_and_specific() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        matches:
+            - trigger: ac
+              replace: Hasta la vista
+            - trigger: bc
+              replace: Jon
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+        name: specific1
+
+        matches:
+            - trigger: "bad"
+              replace: "Conflict"
+        "###,
+    );
+    create_user_config_file(
+      data_dir.path(),
+      "specific2.yml",
+      r###"
+        name: specific2
+
+        matches:
+            - trigger: "badass"
+              replace: "Conflict"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert!(!LegacyConfigSet::has_conflicts(
+      &config_set.default,
+      &config_set.specific
+    ),);
+  }
+
+  #[test]
+  fn test_config_set_specific_inherits_default_global_vars() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 1);
+    assert_eq!(config_set.default.global_vars.len(), 1);
+    assert_eq!(config_set.specific[0].global_vars.len(), 2);
+    assert!(config_set.specific[0]
+      .global_vars
+      .iter()
+      .any(|m| name_for_global_var(m) == "testvar"));
+    assert!(config_set.specific[0]
+      .global_vars
+      .iter()
+      .any(|m| name_for_global_var(m) == "specificvar"));
+  }
+
+  #[test]
+  fn test_config_set_default_get_variables_from_specific() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+         parent: default
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 0);
+    assert_eq!(config_set.default.global_vars.len(), 2);
+    assert!(config_set
+      .default
+      .global_vars
+      .iter()
+      .any(|m| name_for_global_var(m) == "testvar"));
+    assert!(config_set
+      .default
+      .global_vars
+      .iter()
+      .any(|m| name_for_global_var(m) == "specificvar"));
+  }
+
+  #[test]
+  fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() {
+    let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
+      r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    create_user_config_file(
+      data_dir.path(),
+      "specific.yml",
+      r###"
+         exclude_default_entries: true
+
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###,
+    );
+
+    let config_set = LegacyConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+    assert_eq!(config_set.specific.len(), 1);
+    assert_eq!(config_set.default.global_vars.len(), 1);
+    assert_eq!(config_set.specific[0].global_vars.len(), 1);
+    assert!(config_set.specific[0]
+      .global_vars
+      .iter()
+      .any(|m| name_for_global_var(m) == "specificvar"));
+  }
+}
diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs
new file mode 100644
index 00000000..4efbb569
--- /dev/null
+++ b/espanso-config/src/legacy/mod.rs
@@ -0,0 +1,690 @@
+/*
+ * This file is part of espanso.
+ *
+ * C title: (), class: (), exec: ()opyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::warn;
+use regex::Regex;
+use std::{collections::HashMap, path::Path, sync::Arc};
+
+use self::config::LegacyConfig;
+use crate::matches::{
+  group::loader::yaml::{
+    parse::{YAMLMatch, YAMLVariable},
+    try_convert_into_match, try_convert_into_variable,
+  },
+  MatchEffect,
+};
+use crate::{config::store::DefaultConfigStore, counter::StructId};
+use crate::{
+  config::Config,
+  config::{AppProperties, ConfigStore},
+  counter::next_id,
+  matches::{
+    store::{MatchSet, MatchStore},
+    Match, Variable,
+  },
+};
+use std::convert::TryInto;
+
+mod config;
+mod model;
+
+pub fn load(
+  base_dir: &Path,
+  package_dir: &Path,
+) -> Result<(Box<dyn ConfigStore>, Box<dyn MatchStore>)> {
+  let config_set = config::LegacyConfigSet::load(base_dir, package_dir)?;
+
+  let mut match_deduplicate_map = HashMap::new();
+  let mut var_deduplicate_map = HashMap::new();
+
+  let (default_config, mut default_match_group) = split_config(config_set.default);
+  deduplicate_ids(
+    &mut default_match_group,
+    &mut match_deduplicate_map,
+    &mut var_deduplicate_map,
+  );
+
+  let mut match_groups = HashMap::new();
+  match_groups.insert("default".to_string(), default_match_group);
+
+  let mut custom_configs: Vec<Arc<dyn Config>> = Vec::new();
+  for custom in config_set.specific {
+    let (custom_config, mut custom_match_group) = split_config(custom);
+    deduplicate_ids(
+      &mut custom_match_group,
+      &mut match_deduplicate_map,
+      &mut var_deduplicate_map,
+    );
+
+    match_groups.insert(custom_config.name.clone(), custom_match_group);
+    custom_configs.push(Arc::new(custom_config));
+  }
+
+  let config_store = DefaultConfigStore::from_configs(Arc::new(default_config), custom_configs)?;
+  let match_store = LegacyMatchStore::new(match_groups);
+
+  Ok((Box::new(config_store), Box::new(match_store)))
+}
+
+fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup) {
+  let global_vars = config
+    .global_vars
+    .iter()
+    .filter_map(|var| {
+      let var: YAMLVariable = serde_yaml::from_value(var.clone()).ok()?;
+      let (var, warnings) = try_convert_into_variable(var).ok()?;
+      warnings.into_iter().for_each(|warning| {
+        warn!("{}", warning);
+      });
+      Some(var)
+    })
+    .collect();
+
+  let matches = config
+    .matches
+    .iter()
+    .filter_map(|var| {
+      let m: YAMLMatch = serde_yaml::from_value(var.clone()).ok()?;
+      let (m, warnings) = try_convert_into_match(m).ok()?;
+      warnings.into_iter().for_each(|warning| {
+        warn!("{}", warning);
+      });
+      Some(m)
+    })
+    .collect();
+
+  let match_group = LegacyMatchGroup {
+    matches,
+    global_vars,
+  };
+
+  let config: LegacyInteropConfig = config.into();
+  (config, match_group)
+}
+
+/// Due to the way the legacy configs are loaded (matches are copied multiple times in the various configs)
+/// we need to deduplicate the ids of those matches (and global vars).
+fn deduplicate_ids(
+  match_group: &mut LegacyMatchGroup,
+  match_map: &mut HashMap<Match, StructId>,
+  var_map: &mut HashMap<Variable, StructId>,
+) {
+  deduplicate_vars(&mut match_group.global_vars, var_map);
+  deduplicate_matches(&mut match_group.matches, match_map, var_map);
+}
+
+fn deduplicate_matches(
+  matches: &mut [Match],
+  match_map: &mut HashMap<Match, StructId>,
+  var_map: &mut HashMap<Variable, StructId>,
+) {
+  for m in matches.iter_mut() {
+    // Deduplicate variables first
+    if let MatchEffect::Text(effect) = &mut m.effect {
+      deduplicate_vars(&mut effect.vars, var_map);
+    }
+
+    let mut m_without_id = m.clone();
+    m_without_id.id = 0;
+    if let Some(id) = match_map.get(&m_without_id) {
+      m.id = *id;
+    } else {
+      match_map.insert(m_without_id, m.id);
+    }
+  }
+}
+
+// TODO: test case of matches with inner variables
+fn deduplicate_vars(vars: &mut [Variable], var_map: &mut HashMap<Variable, StructId>) {
+  for v in vars.iter_mut() {
+    let mut v_without_id = v.clone();
+    v_without_id.id = 0;
+    if let Some(id) = var_map.get(&v_without_id) {
+      v.id = *id;
+    } else {
+      var_map.insert(v_without_id, v.id);
+    }
+  }
+}
+
+struct LegacyInteropConfig {
+  pub name: String,
+  match_paths: Vec<String>,
+
+  id: i32,
+
+  config: LegacyConfig,
+
+  filter_title: Option<Regex>,
+  filter_class: Option<Regex>,
+  filter_exec: Option<Regex>,
+}
+
+impl From<config::LegacyConfig> for LegacyInteropConfig {
+  fn from(config: config::LegacyConfig) -> Self {
+    Self {
+      id: next_id(),
+      config: config.clone(),
+      name: config.name.clone(),
+      match_paths: vec![config.name],
+      filter_title: if !config.filter_title.is_empty() {
+        Regex::new(&config.filter_title).ok()
+      } else {
+        None
+      },
+      filter_class: if !config.filter_class.is_empty() {
+        Regex::new(&config.filter_class).ok()
+      } else {
+        None
+      },
+      filter_exec: if !config.filter_exec.is_empty() {
+        Regex::new(&config.filter_exec).ok()
+      } else {
+        None
+      },
+    }
+  }
+}
+
+impl Config for LegacyInteropConfig {
+  fn id(&self) -> i32 {
+    self.id
+  }
+
+  fn label(&self) -> &str {
+    &self.config.name
+  }
+
+  fn backend(&self) -> crate::config::Backend {
+    match self.config.backend {
+      config::BackendType::Inject => crate::config::Backend::Inject,
+      config::BackendType::Clipboard => crate::config::Backend::Clipboard,
+      config::BackendType::Auto => crate::config::Backend::Auto,
+    }
+  }
+
+  fn auto_restart(&self) -> bool {
+    self.config.auto_restart
+  }
+
+  fn match_paths(&self) -> &[String] {
+    &self.match_paths
+  }
+
+  fn is_match(&self, app: &AppProperties) -> bool {
+    if self.filter_title.is_none() && self.filter_exec.is_none() && self.filter_class.is_none() {
+      return false;
+    }
+
+    let is_title_match = if let Some(title_regex) = self.filter_title.as_ref() {
+      if let Some(title) = app.title {
+        title_regex.is_match(title)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    let is_exec_match = if let Some(exec_regex) = self.filter_exec.as_ref() {
+      if let Some(exec) = app.exec {
+        exec_regex.is_match(exec)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    let is_class_match = if let Some(class_regex) = self.filter_class.as_ref() {
+      if let Some(class) = app.class {
+        class_regex.is_match(class)
+      } else {
+        false
+      }
+    } else {
+      true
+    };
+
+    // All the filters that have been specified must be true to define a match
+    is_exec_match && is_title_match && is_class_match
+  }
+
+  fn clipboard_threshold(&self) -> usize {
+    crate::config::default::DEFAULT_CLIPBOARD_THRESHOLD
+  }
+
+  fn pre_paste_delay(&self) -> usize {
+    crate::config::default::DEFAULT_PRE_PASTE_DELAY
+  }
+
+  fn toggle_key(&self) -> Option<crate::config::ToggleKey> {
+    match self.config.toggle_key {
+      model::KeyModifier::CTRL => Some(crate::config::ToggleKey::Ctrl),
+      model::KeyModifier::SHIFT => Some(crate::config::ToggleKey::Shift),
+      model::KeyModifier::ALT => Some(crate::config::ToggleKey::Alt),
+      model::KeyModifier::META => Some(crate::config::ToggleKey::Meta),
+      model::KeyModifier::BACKSPACE => None,
+      model::KeyModifier::OFF => None,
+      model::KeyModifier::LEFT_CTRL => Some(crate::config::ToggleKey::LeftCtrl),
+      model::KeyModifier::RIGHT_CTRL => Some(crate::config::ToggleKey::RightCtrl),
+      model::KeyModifier::LEFT_ALT => Some(crate::config::ToggleKey::LeftAlt),
+      model::KeyModifier::RIGHT_ALT => Some(crate::config::ToggleKey::RightAlt),
+      model::KeyModifier::LEFT_META => Some(crate::config::ToggleKey::LeftMeta),
+      model::KeyModifier::RIGHT_META => Some(crate::config::ToggleKey::RightMeta),
+      model::KeyModifier::LEFT_SHIFT => Some(crate::config::ToggleKey::LeftShift),
+      model::KeyModifier::RIGHT_SHIFT => Some(crate::config::ToggleKey::RightShift),
+      model::KeyModifier::CAPS_LOCK => None,
+    }
+  }
+
+  fn preserve_clipboard(&self) -> bool {
+    self.config.preserve_clipboard
+  }
+
+  fn restore_clipboard_delay(&self) -> usize {
+    self.config.restore_clipboard_delay.try_into().unwrap()
+  }
+
+  fn paste_shortcut_event_delay(&self) -> usize {
+    crate::config::default::DEFAULT_SHORTCUT_EVENT_DELAY
+  }
+
+  fn paste_shortcut(&self) -> Option<String> {
+    match self.config.paste_shortcut {
+      model::PasteShortcut::Default => None,
+      model::PasteShortcut::CtrlV => Some("CTRL+V".to_string()),
+      model::PasteShortcut::CtrlShiftV => Some("CTRL+SHIFT+V".to_string()),
+      model::PasteShortcut::ShiftInsert => Some("SHIFT+INSERT".to_string()),
+      model::PasteShortcut::CtrlAltV => Some("CTRL+ALT+V".to_string()),
+      model::PasteShortcut::MetaV => Some("META+V".to_string()),
+    }
+  }
+
+  fn disable_x11_fast_inject(&self) -> bool {
+    !self.config.fast_inject
+  }
+
+  fn inject_delay(&self) -> Option<usize> {
+    if self.config.inject_delay == 0 {
+      None
+    } else {
+      Some(self.config.inject_delay.try_into().unwrap())
+    }
+  }
+
+  fn key_delay(&self) -> Option<usize> {
+    if self.config.backspace_delay == 0 {
+      None
+    } else {
+      Some(self.config.backspace_delay.try_into().unwrap())
+    }
+  }
+
+  fn word_separators(&self) -> Vec<String> {
+    self
+      .config
+      .word_separators
+      .iter()
+      .map(|c| String::from(*c))
+      .collect()
+  }
+
+  fn backspace_limit(&self) -> usize {
+    self.config.backspace_limit.try_into().unwrap()
+  }
+
+  fn apply_patch(&self) -> bool {
+    true
+  }
+
+  fn keyboard_layout(&self) -> Option<crate::config::RMLVOConfig> {
+    None
+  }
+
+  fn search_trigger(&self) -> Option<String> {
+    self.config.search_trigger.clone()
+  }
+
+  fn search_shortcut(&self) -> Option<String> {
+    self.config.search_shortcut.clone()
+  }
+
+  fn undo_backspace(&self) -> bool {
+    self.config.undo_backspace
+  }
+
+  fn show_icon(&self) -> bool {
+    self.config.show_icon
+  }
+
+  fn show_notifications(&self) -> bool {
+    self.config.show_notifications
+  }
+
+  fn secure_input_notification(&self) -> bool {
+    self.config.secure_input_notification
+  }
+
+  fn enable(&self) -> bool {
+    self.config.enable_active
+  }
+
+  fn win32_exclude_orphan_events(&self) -> bool {
+    true
+  }
+
+  fn evdev_modifier_delay(&self) -> Option<usize> {
+    Some(10)
+  }
+}
+
+struct LegacyMatchGroup {
+  matches: Vec<Match>,
+  global_vars: Vec<Variable>,
+}
+
+struct LegacyMatchStore {
+  groups: HashMap<String, LegacyMatchGroup>,
+}
+
+impl LegacyMatchStore {
+  pub fn new(groups: HashMap<String, LegacyMatchGroup>) -> Self {
+    Self { groups }
+  }
+}
+
+impl MatchStore for LegacyMatchStore {
+  fn query(&self, paths: &[String]) -> MatchSet {
+    let group = if !paths.is_empty() {
+      self.groups.get(&paths[0])
+    } else {
+      None
+    };
+
+    if let Some(group) = group {
+      MatchSet {
+        matches: group.matches.iter().collect(),
+        global_vars: group.global_vars.iter().collect(),
+      }
+    } else {
+      MatchSet {
+        matches: Vec::new(),
+        global_vars: Vec::new(),
+      }
+    }
+  }
+
+  fn loaded_paths(&self) -> Vec<String> {
+    self.groups.keys().cloned().collect()
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use std::{fs::create_dir_all, path::Path};
+  use tempdir::TempDir;
+
+  pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
+    let dir = TempDir::new("tempconfig").unwrap();
+    let user_dir = dir.path().join("user");
+    create_dir_all(&user_dir).unwrap();
+
+    let package_dir = TempDir::new("tempconfig").unwrap();
+
+    callback(
+      &dunce::canonicalize(&dir.path()).unwrap(),
+      &dunce::canonicalize(&user_dir).unwrap(),
+      &dunce::canonicalize(&package_dir.path()).unwrap(),
+    );
+  }
+
+  #[test]
+  fn load_legacy_works_correctly() {
+    use_test_directory(|base, user, packages| {
+      std::fs::write(
+        base.join("default.yml"),
+        r#"
+      backend: Clipboard
+
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      std::fs::write(
+        user.join("specific.yml"),
+        r#"
+      name: specific
+      parent: default
+
+      matches:
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      std::fs::write(
+        user.join("separate.yml"),
+        r#"
+      name: separate
+      filter_title: "Google"
+
+      matches:
+        - trigger: "eren"
+          replace: "mikasa"
+      "#,
+      )
+      .unwrap();
+
+      let (config_store, match_store) = load(base, packages).unwrap();
+
+      let default_config = config_store.default();
+      assert_eq!(default_config.match_paths().len(), 1);
+
+      let active_config = config_store.active(&AppProperties {
+        title: Some("Google"),
+        class: None,
+        exec: None,
+      });
+      assert_eq!(active_config.match_paths().len(), 1);
+
+      let default_fallback = config_store.active(&AppProperties {
+        title: Some("Yahoo"),
+        class: None,
+        exec: None,
+      });
+      assert_eq!(default_fallback.match_paths().len(), 1);
+
+      assert_eq!(
+        match_store
+          .query(default_config.match_paths())
+          .matches
+          .len(),
+        2
+      );
+      assert_eq!(
+        match_store
+          .query(default_config.match_paths())
+          .global_vars
+          .len(),
+        1
+      );
+
+      assert_eq!(
+        match_store.query(active_config.match_paths()).matches.len(),
+        3
+      );
+      assert_eq!(
+        match_store
+          .query(active_config.match_paths())
+          .global_vars
+          .len(),
+        1
+      );
+
+      assert_eq!(
+        match_store
+          .query(default_fallback.match_paths())
+          .matches
+          .len(),
+        2
+      );
+      assert_eq!(
+        match_store
+          .query(default_fallback.match_paths())
+          .global_vars
+          .len(),
+        1
+      );
+    });
+  }
+
+  #[test]
+  fn load_legacy_deduplicates_ids_correctly() {
+    use_test_directory(|base, user, packages| {
+      std::fs::write(
+        base.join("default.yml"),
+        r#"
+      backend: Clipboard
+
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+        
+        - trigger: "withvars"
+          replace: "{{output}}"
+          vars:
+            - name: "output"
+              type: "echo"
+              params:
+                echo: "test"
+      "#,
+      )
+      .unwrap();
+
+      std::fs::write(
+        user.join("specific.yml"),
+        r#"
+      name: specific
+      filter_title: "Google"
+      "#,
+      )
+      .unwrap();
+
+      let (config_store, match_store) = load(base, packages).unwrap();
+
+      let default_config = config_store.default();
+      let active_config = config_store.active(&AppProperties {
+        title: Some("Google"),
+        class: None,
+        exec: None,
+      });
+
+      for (i, m) in match_store
+        .query(default_config.match_paths())
+        .matches
+        .into_iter()
+        .enumerate()
+      {
+        assert_eq!(
+          m.id,
+          match_store
+            .query(active_config.match_paths())
+            .matches
+            .get(i)
+            .unwrap()
+            .id
+        );
+      }
+
+      assert_eq!(
+        match_store
+          .query(default_config.match_paths())
+          .global_vars
+          .first()
+          .unwrap()
+          .id,
+        match_store
+          .query(active_config.match_paths())
+          .global_vars
+          .first()
+          .unwrap()
+          .id,
+      );
+    });
+  }
+
+  #[test]
+  fn load_legacy_with_packages() {
+    use_test_directory(|base, _, packages| {
+      std::fs::write(
+        base.join("default.yml"),
+        r#"
+      backend: Clipboard
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      create_dir_all(packages.join("test-package")).unwrap();
+      std::fs::write(
+        packages.join("test-package").join("package.yml"),
+        r#"
+      name: test-package 
+      parent: default
+
+      matches:
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let (config_store, match_store) = load(base, packages).unwrap();
+
+      let default_config = config_store.default();
+      assert_eq!(default_config.match_paths().len(), 1);
+
+      assert_eq!(
+        match_store
+          .query(default_config.match_paths())
+          .matches
+          .len(),
+        2
+      );
+    });
+  }
+}
diff --git a/espanso-config/src/legacy/model.rs b/espanso-config/src/legacy/model.rs
new file mode 100644
index 00000000..4427debf
--- /dev/null
+++ b/espanso-config/src/legacy/model.rs
@@ -0,0 +1,62 @@
+/*
+ * 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 serde::{Deserialize, Serialize};
+
+#[allow(non_camel_case_types)]
+#[allow(clippy::upper_case_acronyms)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub enum KeyModifier {
+  CTRL,
+  SHIFT,
+  ALT,
+  META,
+  BACKSPACE,
+  OFF,
+
+  // These are specific variants of the ones above. See issue: #117
+  // https://github.com/federico-terzi/espanso/issues/117
+  LEFT_CTRL,
+  RIGHT_CTRL,
+  LEFT_ALT,
+  RIGHT_ALT,
+  LEFT_META,
+  RIGHT_META,
+  LEFT_SHIFT,
+  RIGHT_SHIFT,
+
+  // Special cases, should not be used in config
+  CAPS_LOCK,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub enum PasteShortcut {
+  Default,     // Default one for the current system
+  CtrlV,       // Classic Ctrl+V shortcut
+  CtrlShiftV,  // Could be used to paste without formatting in many applications
+  ShiftInsert, // Often used in Linux systems
+  CtrlAltV,    // Used in some Linux terminals (urxvt)
+  MetaV,       // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
+}
+
+impl Default for PasteShortcut {
+  fn default() -> Self {
+    PasteShortcut::Default
+  }
+}
diff --git a/espanso-config/src/lib.rs b/espanso-config/src/lib.rs
new file mode 100644
index 00000000..458d2d65
--- /dev/null
+++ b/espanso-config/src/lib.rs
@@ -0,0 +1,319 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use config::ConfigStore;
+use matches::store::MatchStore;
+use std::path::Path;
+use thiserror::Error;
+
+#[macro_use]
+extern crate lazy_static;
+
+pub mod config;
+mod counter;
+pub mod error;
+mod legacy;
+pub mod matches;
+mod util;
+
+#[allow(clippy::type_complexity)]
+pub fn load(
+  base_path: &Path,
+) -> Result<(
+  Box<dyn ConfigStore>,
+  Box<dyn MatchStore>,
+  Vec<error::NonFatalErrorSet>,
+)> {
+  let config_dir = base_path.join("config");
+  if !config_dir.exists() || !config_dir.is_dir() {
+    return Err(ConfigError::MissingConfigDir().into());
+  }
+
+  let (config_store, non_fatal_config_errors) = config::load_store(&config_dir)?;
+  let root_paths = config_store.get_all_match_paths();
+
+  let (match_store, non_fatal_match_errors) =
+    matches::store::load(&root_paths.into_iter().collect::<Vec<String>>());
+
+  let mut non_fatal_errors = Vec::new();
+  non_fatal_errors.extend(non_fatal_config_errors.into_iter());
+  non_fatal_errors.extend(non_fatal_match_errors.into_iter());
+
+  Ok((
+    Box::new(config_store),
+    Box::new(match_store),
+    non_fatal_errors,
+  ))
+}
+
+pub fn load_legacy(
+  config_dir: &Path,
+  package_dir: &Path,
+) -> Result<(Box<dyn ConfigStore>, Box<dyn MatchStore>)> {
+  legacy::load(config_dir, package_dir)
+}
+
+pub fn is_legacy_config(base_dir: &Path) -> bool {
+  base_dir.join("user").is_dir() && base_dir.join("default.yml").is_file()
+}
+
+#[derive(Error, Debug)]
+pub enum ConfigError {
+  #[error("missing config directory")]
+  MissingConfigDir(),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::util::tests::use_test_directory;
+  use config::AppProperties;
+
+  #[test]
+  fn load_works_correctly() {
+    use_test_directory(|base, match_dir, config_dir| {
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "_sub.yml"
+
+      matches:
+        - trigger: "hello2"
+          replace: "world2"
+      "#,
+      )
+      .unwrap();
+
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(
+        &under_file,
+        r#"
+      matches:
+        - trigger: "hello3"
+          replace: "world3"
+      "#,
+      )
+      .unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, "").unwrap();
+
+      let custom_config_file = config_dir.join("custom.yml");
+      std::fs::write(
+        &custom_config_file,
+        r#"
+      filter_title: "Chrome"
+
+      use_standard_includes: false
+      includes: ["../match/another.yml"]
+      "#,
+      )
+      .unwrap();
+
+      let (config_store, match_store, errors) = load(base).unwrap();
+
+      assert_eq!(errors.len(), 0);
+      assert_eq!(config_store.default().match_paths().len(), 2);
+      assert_eq!(
+        config_store
+          .active(&AppProperties {
+            title: Some("Google Chrome"),
+            class: None,
+            exec: None,
+          })
+          .match_paths()
+          .len(),
+        1
+      );
+
+      assert_eq!(
+        match_store
+          .query(config_store.default().match_paths())
+          .matches
+          .len(),
+        3
+      );
+      assert_eq!(
+        match_store
+          .query(
+            config_store
+              .active(&AppProperties {
+                title: Some("Chrome"),
+                class: None,
+                exec: None,
+              })
+              .match_paths()
+          )
+          .matches
+          .len(),
+        2
+      );
+    });
+  }
+
+  #[test]
+  fn load_non_fatal_errors() {
+    use_test_directory(|base, match_dir, config_dir| {
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      matches:
+        - "invalid"invalid
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "_sub.yml"
+
+      matches:
+        - trigger: "hello2"
+          replace: "world2"
+      "#,
+      )
+      .unwrap();
+
+      let under_file = match_dir.join("_sub.yml");
+      std::fs::write(
+        &under_file,
+        r#"
+      matches:
+        - trigger: "hello3"
+          replace: "world3"invalid
+      "#,
+      )
+      .unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, r#""#).unwrap();
+
+      let custom_config_file = config_dir.join("custom.yml");
+      std::fs::write(
+        &custom_config_file,
+        r#"
+      filter_title: "Chrome"
+      "
+
+      use_standard_includes: false
+      includes: ["../match/another.yml"]
+      "#,
+      )
+      .unwrap();
+
+      let (config_store, match_store, errors) = load(base).unwrap();
+
+      assert_eq!(errors.len(), 3);
+      // It shouldn't have loaded the "config.yml" one because of the YAML error
+      assert_eq!(config_store.configs().len(), 1);
+      // It shouldn't load "base.yml" and "_sub.yml" due to YAML errors
+      assert_eq!(match_store.loaded_paths().len(), 1);
+    });
+  }
+
+  #[test]
+  fn load_non_fatal_match_errors() {
+    use_test_directory(|base, match_dir, config_dir| {
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world"
+        - trigger: "invalid because there is no action field"
+      "#,
+      )
+      .unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(&config_file, r#""#).unwrap();
+
+      let (config_store, match_store, errors) = load(base).unwrap();
+
+      assert_eq!(errors.len(), 1);
+      assert_eq!(errors[0].file, base_file);
+      assert_eq!(errors[0].errors.len(), 1);
+
+      assert_eq!(
+        match_store
+          .query(config_store.default().match_paths())
+          .matches
+          .len(),
+        1
+      );
+    });
+  }
+
+  #[test]
+  fn load_fatal_errors() {
+    use_test_directory(|base, match_dir, config_dir| {
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      matches:
+        - trigger: hello
+          replace: world
+      "#,
+      )
+      .unwrap();
+
+      let config_file = config_dir.join("default.yml");
+      std::fs::write(
+        &config_file,
+        r#"
+      invalid
+
+      "
+      "#,
+      )
+      .unwrap();
+
+      // A syntax error in the default.yml file cannot be handled gracefully
+      assert!(load(base).is_err());
+    });
+  }
+
+  #[test]
+  fn load_without_valid_config_dir() {
+    use_test_directory(|_, match_dir, _| {
+      // To correcly load the configs, the "load" method looks for the "config" directory
+      assert!(load(match_dir).is_err());
+    });
+  }
+}
diff --git a/espanso-config/src/matches/group/loader/mod.rs b/espanso-config/src/matches/group/loader/mod.rs
new file mode 100644
index 00000000..ed32c667
--- /dev/null
+++ b/espanso-config/src/matches/group/loader/mod.rs
@@ -0,0 +1,179 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::path::Path;
+use thiserror::Error;
+
+use crate::error::NonFatalErrorSet;
+
+use self::yaml::YAMLImporter;
+
+use super::MatchGroup;
+
+pub(crate) mod yaml;
+
+trait Importer {
+  fn is_supported(&self, extension: &str) -> bool;
+  fn load_group(&self, path: &Path) -> Result<(MatchGroup, Option<NonFatalErrorSet>)>;
+}
+
+lazy_static! {
+  static ref IMPORTERS: Vec<Box<dyn Importer + Sync + Send>> = vec![Box::new(YAMLImporter::new()),];
+}
+
+pub(crate) fn load_match_group(path: &Path) -> Result<(MatchGroup, Option<NonFatalErrorSet>)> {
+  if let Some(extension) = path.extension() {
+    let extension = extension.to_string_lossy().to_lowercase();
+
+    let importer = IMPORTERS
+      .iter()
+      .find(|importer| importer.is_supported(&extension));
+
+    match importer {
+      Some(importer) => match importer.load_group(path) {
+        Ok((group, non_fatal_error_set)) => Ok((group, non_fatal_error_set)),
+        Err(err) => Err(LoadError::ParsingError(err).into()),
+      },
+      None => Err(LoadError::InvalidFormat.into()),
+    }
+  } else {
+    Err(LoadError::MissingExtension.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum LoadError {
+  #[error("missing extension in match group file")]
+  MissingExtension,
+
+  #[error("invalid match group format")]
+  InvalidFormat,
+
+  #[error(transparent)]
+  ParsingError(anyhow::Error),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::util::tests::use_test_directory;
+
+  #[test]
+  fn load_group_invalid_format() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base.invalid");
+      std::fs::write(&file, "test").unwrap();
+
+      assert!(matches!(
+        load_match_group(&file)
+          .unwrap_err()
+          .downcast::<LoadError>()
+          .unwrap(),
+        LoadError::InvalidFormat
+      ));
+    });
+  }
+
+  #[test]
+  fn load_group_missing_extension() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base");
+      std::fs::write(&file, "test").unwrap();
+
+      assert!(matches!(
+        load_match_group(&file)
+          .unwrap_err()
+          .downcast::<LoadError>()
+          .unwrap(),
+        LoadError::MissingExtension
+      ));
+    });
+  }
+
+  #[test]
+  fn load_group_parsing_error() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base.yml");
+      std::fs::write(&file, "test").unwrap();
+
+      assert!(matches!(
+        load_match_group(&file)
+          .unwrap_err()
+          .downcast::<LoadError>()
+          .unwrap(),
+        LoadError::ParsingError(_)
+      ));
+    });
+  }
+
+  #[test]
+  fn load_group_yaml_format() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base.yml");
+      std::fs::write(
+        &file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
+    });
+  }
+
+  #[test]
+  fn load_group_yaml_format_2() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base.yaml");
+      std::fs::write(
+        &file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
+    });
+  }
+
+  #[test]
+  fn load_group_yaml_format_casing() {
+    use_test_directory(|_, match_dir, _| {
+      let file = match_dir.join("base.YML");
+      std::fs::write(
+        &file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
+    });
+  }
+}
diff --git a/espanso-config/src/matches/group/loader/yaml/mod.rs b/espanso-config/src/matches/group/loader/yaml/mod.rs
new file mode 100644
index 00000000..1d3a61c3
--- /dev/null
+++ b/espanso-config/src/matches/group/loader/yaml/mod.rs
@@ -0,0 +1,720 @@
+/*
+ * 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 crate::{
+  counter::next_id,
+  error::{ErrorRecord, NonFatalErrorSet},
+  matches::{
+    group::{path::resolve_imports, MatchGroup},
+    ImageEffect, Match, Params, RegexCause, TextFormat, TextInjectMode, UpperCasingStyle, Value,
+    Variable,
+  },
+};
+use anyhow::{anyhow, bail, Context, Result};
+use parse::YAMLMatchGroup;
+use regex::{Captures, Regex};
+
+use self::{
+  parse::{YAMLMatch, YAMLVariable},
+  util::convert_params,
+};
+use crate::matches::{MatchCause, MatchEffect, TextEffect, TriggerCause};
+
+use super::Importer;
+
+pub(crate) mod parse;
+mod util;
+
+lazy_static! {
+  static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
+}
+
+// Create an alias to make the meaning more explicit
+type Warning = anyhow::Error;
+
+pub(crate) struct YAMLImporter {}
+
+impl YAMLImporter {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Importer for YAMLImporter {
+  fn is_supported(&self, extension: &str) -> bool {
+    extension == "yaml" || extension == "yml"
+  }
+
+  fn load_group(
+    &self,
+    path: &std::path::Path,
+  ) -> anyhow::Result<(crate::matches::group::MatchGroup, Option<NonFatalErrorSet>)> {
+    let yaml_group =
+      YAMLMatchGroup::parse_from_file(path).context("failed to parse YAML match group")?;
+
+    let mut non_fatal_errors = Vec::new();
+
+    let mut global_vars = Vec::new();
+    for yaml_global_var in yaml_group.global_vars.as_ref().cloned().unwrap_or_default() {
+      match try_convert_into_variable(yaml_global_var) {
+        Ok((var, warnings)) => {
+          global_vars.push(var);
+          non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
+        }
+        Err(err) => {
+          non_fatal_errors.push(ErrorRecord::error(err));
+        }
+      }
+    }
+
+    let mut matches = Vec::new();
+    for yaml_match in yaml_group.matches.as_ref().cloned().unwrap_or_default() {
+      match try_convert_into_match(yaml_match) {
+        Ok((m, warnings)) => {
+          matches.push(m);
+          non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
+        }
+        Err(err) => {
+          non_fatal_errors.push(ErrorRecord::error(err));
+        }
+      }
+    }
+
+    // Resolve imports
+    let (resolved_imports, import_errors) =
+      resolve_imports(path, &yaml_group.imports.unwrap_or_default())
+        .context("failed to resolve YAML match group imports")?;
+    non_fatal_errors.extend(import_errors);
+
+    let non_fatal_error_set = if !non_fatal_errors.is_empty() {
+      Some(NonFatalErrorSet::new(path, non_fatal_errors))
+    } else {
+      None
+    };
+
+    Ok((
+      MatchGroup {
+        imports: resolved_imports,
+        global_vars,
+        matches,
+      },
+      non_fatal_error_set,
+    ))
+  }
+}
+
+pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warning>)> {
+  let mut warnings = Vec::new();
+
+  if yaml_match.uppercase_style.is_some() && yaml_match.propagate_case.is_none() {
+    warnings.push(anyhow!(
+      "specifying the 'uppercase_style' option without 'propagate_case' has no effect"
+    ));
+  }
+
+  let triggers = if let Some(trigger) = yaml_match.trigger {
+    Some(vec![trigger])
+  } else {
+    yaml_match.triggers
+  };
+
+  let uppercase_style = match yaml_match
+    .uppercase_style
+    .map(|s| s.to_lowercase())
+    .as_deref()
+  {
+    Some("uppercase") => UpperCasingStyle::Uppercase,
+    Some("capitalize") => UpperCasingStyle::Capitalize,
+    Some("capitalize_words") => UpperCasingStyle::CapitalizeWords,
+    Some(style) => {
+      warnings.push(anyhow!(
+        "unrecognized uppercase_style: {:?}, falling back to the default",
+        style
+      ));
+      TriggerCause::default().uppercase_style
+    }
+    _ => TriggerCause::default().uppercase_style,
+  };
+
+  let cause = if let Some(triggers) = triggers {
+    MatchCause::Trigger(TriggerCause {
+      triggers,
+      left_word: yaml_match
+        .left_word
+        .or(yaml_match.word)
+        .unwrap_or(TriggerCause::default().left_word),
+      right_word: yaml_match
+        .right_word
+        .or(yaml_match.word)
+        .unwrap_or(TriggerCause::default().right_word),
+      propagate_case: yaml_match
+        .propagate_case
+        .unwrap_or(TriggerCause::default().propagate_case),
+      uppercase_style,
+    })
+  } else if let Some(regex) = yaml_match.regex {
+    // TODO: add test case
+    MatchCause::Regex(RegexCause { regex })
+  } else {
+    MatchCause::None
+  };
+
+  // TODO: test force_mode/force_clipboard
+  let force_mode = if let Some(true) = yaml_match.force_clipboard {
+    Some(TextInjectMode::Clipboard)
+  } else if let Some(mode) = yaml_match.force_mode {
+    match mode.to_lowercase().as_str() {
+      "clipboard" => Some(TextInjectMode::Clipboard),
+      "keys" => Some(TextInjectMode::Keys),
+      _ => None,
+    }
+  } else {
+    None
+  };
+
+  let effect =
+    if yaml_match.replace.is_some() || yaml_match.markdown.is_some() || yaml_match.html.is_some() {
+      // TODO: test markdown and html cases
+      let (replace, format) = if let Some(plain) = yaml_match.replace {
+        (plain, TextFormat::Plain)
+      } else if let Some(markdown) = yaml_match.markdown {
+        (markdown, TextFormat::Markdown)
+      } else if let Some(html) = yaml_match.html {
+        (html, TextFormat::Html)
+      } else {
+        unreachable!();
+      };
+
+      let mut vars: Vec<Variable> = Vec::new();
+      for yaml_var in yaml_match.vars.unwrap_or_default() {
+        let (var, var_warnings) = try_convert_into_variable(yaml_var.clone())
+          .with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
+        warnings.extend(var_warnings);
+        vars.push(var);
+      }
+
+      MatchEffect::Text(TextEffect {
+        replace,
+        vars,
+        format,
+        force_mode,
+      })
+    } else if let Some(form_layout) = yaml_match.form {
+      // TODO: test form case
+      // Replace all the form fields with actual variables
+      let resolved_layout = VAR_REGEX
+        .replace_all(&form_layout, |caps: &Captures| {
+          let var_name = caps.get(1).unwrap().as_str();
+          format!("{{{{form1.{}}}}}", var_name)
+        })
+        .to_string();
+
+      // Convert escaped brakets in forms
+      let resolved_layout = resolved_layout.replace("\\{", "{ ").replace("\\}", " }");
+
+      // Convert the form data to valid variables
+      let mut params = Params::new();
+      params.insert("layout".to_string(), Value::String(form_layout));
+
+      if let Some(fields) = yaml_match.form_fields {
+        params.insert("fields".to_string(), Value::Object(convert_params(fields)?));
+      }
+
+      let vars = vec![Variable {
+        id: next_id(),
+        name: "form1".to_owned(),
+        var_type: "form".to_owned(),
+        params,
+      }];
+
+      MatchEffect::Text(TextEffect {
+        replace: resolved_layout,
+        vars,
+        format: TextFormat::Plain,
+        force_mode,
+      })
+    } else if let Some(image_path) = yaml_match.image_path {
+      // TODO: test image case
+      MatchEffect::Image(ImageEffect { path: image_path })
+    } else {
+      MatchEffect::None
+    };
+
+  if let MatchEffect::None = effect {
+    bail!(
+      "match triggered by {:?} does not produce any effect. Did you forget the 'replace' field?",
+      cause.long_description()
+    );
+  }
+
+  Ok((
+    Match {
+      cause,
+      effect,
+      label: yaml_match.label,
+      id: next_id(),
+    },
+    warnings,
+  ))
+}
+
+pub fn try_convert_into_variable(yaml_var: YAMLVariable) -> Result<(Variable, Vec<Warning>)> {
+  Ok((
+    Variable {
+      name: yaml_var.name,
+      var_type: yaml_var.var_type,
+      params: convert_params(yaml_var.params)?,
+      id: next_id(),
+    },
+    Vec::new(),
+  ))
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::{
+    matches::{Match, Params, Value},
+    util::tests::use_test_directory,
+  };
+  use std::fs::create_dir_all;
+
+  fn create_match_with_warnings(yaml: &str) -> Result<(Match, Vec<Warning>)> {
+    let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
+    let (mut m, warnings) = try_convert_into_match(yaml_match)?;
+
+    // Reset the IDs to correctly compare them
+    m.id = 0;
+    if let MatchEffect::Text(e) = &mut m.effect {
+      e.vars.iter_mut().for_each(|v| v.id = 0);
+    }
+
+    Ok((m, warnings))
+  }
+
+  fn create_match(yaml: &str) -> Result<Match> {
+    let (m, warnings) = create_match_with_warnings(yaml)?;
+    if !warnings.is_empty() {
+      panic!("warnings were detected but not handled: {:?}", warnings);
+    }
+    Ok(m)
+  }
+
+  #[test]
+  fn basic_match_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn multiple_triggers_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        triggers: ["Hello", "john"]
+        replace: "world"
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string(), "john".to_string()],
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn word_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        word: true
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          left_word: true,
+          right_word: true,
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn left_word_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        left_word: true
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          left_word: true,
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn right_word_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        right_word: true
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          right_word: true,
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn propagate_case_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        propagate_case: true
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          propagate_case: true,
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn uppercase_style_maps_correctly() {
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        uppercase_style: "capitalize"
+        propagate_case: true
+        "#
+      )
+      .unwrap()
+      .cause
+      .into_trigger()
+      .unwrap()
+      .uppercase_style,
+      UpperCasingStyle::Capitalize,
+    );
+
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        uppercase_style: "capitalize_words"
+        propagate_case: true
+        "#
+      )
+      .unwrap()
+      .cause
+      .into_trigger()
+      .unwrap()
+      .uppercase_style,
+      UpperCasingStyle::CapitalizeWords,
+    );
+
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        uppercase_style: "uppercase"
+        propagate_case: true
+        "#
+      )
+      .unwrap()
+      .cause
+      .into_trigger()
+      .unwrap()
+      .uppercase_style,
+      UpperCasingStyle::Uppercase,
+    );
+
+    // Invalid without propagate_case
+    let (m, warnings) = create_match_with_warnings(
+      r#"
+        trigger: "Hello"
+        replace: "world"
+        uppercase_style: "capitalize"
+        "#,
+    )
+    .unwrap();
+    assert_eq!(
+      m.cause.into_trigger().unwrap().uppercase_style,
+      UpperCasingStyle::Capitalize,
+    );
+    assert_eq!(warnings.len(), 1);
+
+    // Invalid style
+    let (m, warnings) = create_match_with_warnings(
+      r#"
+        trigger: "Hello"
+        replace: "world"
+        uppercase_style: "invalid"
+        propagate_case: true
+        "#,
+    )
+    .unwrap();
+    assert_eq!(
+      m.cause.into_trigger().unwrap().uppercase_style,
+      UpperCasingStyle::Uppercase,
+    );
+    assert_eq!(warnings.len(), 1);
+  }
+
+  #[test]
+  fn vars_maps_correctly() {
+    let mut params = Params::new();
+    params.insert("param1".to_string(), Value::Bool(true));
+    let vars = vec![Variable {
+      name: "var1".to_string(),
+      var_type: "test".to_string(),
+      params,
+      ..Default::default()
+    }];
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        vars:
+          - name: var1
+            type: test
+            params:
+              param1: true
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          vars,
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn vars_no_params_maps_correctly() {
+    let vars = vec![Variable {
+      name: "var1".to_string(),
+      var_type: "test".to_string(),
+      params: Params::new(),
+      ..Default::default()
+    }];
+    assert_eq!(
+      create_match(
+        r#"
+        trigger: "Hello"
+        replace: "world"
+        vars:
+          - name: var1
+            type: test
+        "#
+      )
+      .unwrap(),
+      Match {
+        cause: MatchCause::Trigger(TriggerCause {
+          triggers: vec!["Hello".to_string()],
+          ..Default::default()
+        }),
+        effect: MatchEffect::Text(TextEffect {
+          replace: "world".to_string(),
+          vars,
+          ..Default::default()
+        }),
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn importer_is_supported() {
+    let importer = YAMLImporter::new();
+    assert!(importer.is_supported("yaml"));
+    assert!(importer.is_supported("yml"));
+    assert!(!importer.is_supported("invalid"));
+  }
+
+  #[test]
+  fn importer_works_correctly() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+        - "invalid/import.yml" # This should be discarded
+      
+      global_vars:
+        - name: "var1"
+          type: "test"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "").unwrap();
+
+      let importer = YAMLImporter::new();
+      let (mut group, non_fatal_error_set) = importer.load_group(&base_file).unwrap();
+      // The invalid import path should be reported as error
+      assert_eq!(non_fatal_error_set.unwrap().errors.len(), 1);
+
+      // Reset the ids to compare them correctly
+      group.matches.iter_mut().for_each(|mut m| m.id = 0);
+      group.global_vars.iter_mut().for_each(|mut v| v.id = 0);
+
+      let vars = vec![Variable {
+        name: "var1".to_string(),
+        var_type: "test".to_string(),
+        params: Params::new(),
+        ..Default::default()
+      }];
+
+      assert_eq!(
+        group,
+        MatchGroup {
+          imports: vec![sub_file.to_string_lossy().to_string(),],
+          global_vars: vars,
+          matches: vec![Match {
+            cause: MatchCause::Trigger(TriggerCause {
+              triggers: vec!["hello".to_string()],
+              ..Default::default()
+            }),
+            effect: MatchEffect::Text(TextEffect {
+              replace: "world".to_string(),
+              ..Default::default()
+            }),
+            ..Default::default()
+          }],
+        }
+      )
+    });
+  }
+
+  #[test]
+  fn importer_invalid_syntax() {
+    use_test_directory(|_, match_dir, _| {
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - invalid
+       - indentation
+      "#,
+      )
+      .unwrap();
+
+      let importer = YAMLImporter::new();
+      assert!(importer.load_group(&base_file).is_err());
+    })
+  }
+}
diff --git a/espanso-config/src/matches/group/loader/yaml/parse.rs b/espanso-config/src/matches/group/loader/yaml/parse.rs
new file mode 100644
index 00000000..39a18fa8
--- /dev/null
+++ b/espanso-config/src/matches/group/loader/yaml/parse.rs
@@ -0,0 +1,132 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::path::Path;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use serde_yaml::Mapping;
+
+use crate::util::is_yaml_empty;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct YAMLMatchGroup {
+  #[serde(default)]
+  pub imports: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub global_vars: Option<Vec<YAMLVariable>>,
+
+  #[serde(default)]
+  pub matches: Option<Vec<YAMLMatch>>,
+}
+
+impl YAMLMatchGroup {
+  pub fn parse_from_str(yaml: &str) -> Result<Self> {
+    // Because an empty string is not valid YAML but we want to support it anyway
+    if is_yaml_empty(yaml) {
+      return Ok(serde_yaml::from_str(
+        "arbitrary_field_that_will_not_block_the_parser: true",
+      )?);
+    }
+
+    Ok(serde_yaml::from_str(yaml)?)
+  }
+
+  // TODO: test
+  pub fn parse_from_file(path: &Path) -> Result<Self> {
+    let content = std::fs::read_to_string(path)?;
+    Self::parse_from_str(&content)
+  }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct YAMLMatch {
+  #[serde(default)]
+  pub label: Option<String>,
+
+  #[serde(default)]
+  pub trigger: Option<String>,
+
+  #[serde(default)]
+  pub triggers: Option<Vec<String>>,
+
+  #[serde(default)]
+  pub regex: Option<String>,
+
+  #[serde(default)]
+  pub replace: Option<String>,
+
+  #[serde(default)]
+  pub image_path: Option<String>,
+
+  #[serde(default)]
+  pub form: Option<String>,
+
+  #[serde(default)]
+  pub form_fields: Option<Mapping>,
+
+  #[serde(default)]
+  pub vars: Option<Vec<YAMLVariable>>,
+
+  #[serde(default)]
+  pub word: Option<bool>,
+
+  #[serde(default)]
+  pub left_word: Option<bool>,
+
+  #[serde(default)]
+  pub right_word: Option<bool>,
+
+  #[serde(default)]
+  pub propagate_case: Option<bool>,
+
+  #[serde(default)]
+  pub uppercase_style: Option<String>,
+
+  #[serde(default)]
+  pub force_clipboard: Option<bool>,
+
+  #[serde(default)]
+  pub force_mode: Option<String>,
+
+  #[serde(default)]
+  pub markdown: Option<String>,
+
+  #[serde(default)]
+  pub paragraph: Option<bool>,
+
+  #[serde(default)]
+  pub html: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+pub struct YAMLVariable {
+  pub name: String,
+
+  #[serde(rename = "type")]
+  pub var_type: String,
+
+  #[serde(default = "default_params")]
+  pub params: Mapping,
+}
+
+fn default_params() -> Mapping {
+  Mapping::new()
+}
diff --git a/espanso-config/src/matches/group/loader/yaml/util.rs b/espanso-config/src/matches/group/loader/yaml/util.rs
new file mode 100644
index 00000000..88e5c814
--- /dev/null
+++ b/espanso-config/src/matches/group/loader/yaml/util.rs
@@ -0,0 +1,176 @@
+/*
+ * 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::convert::TryInto;
+
+use anyhow::Result;
+use serde_yaml::Mapping;
+use thiserror::Error;
+
+use crate::matches::{Number, Params, Value};
+
+pub(crate) fn convert_params(m: Mapping) -> Result<Params> {
+  let mut params = Params::new();
+
+  for (key, value) in m {
+    let key = key.as_str().ok_or(ConversionError::InvalidKeyFormat)?;
+    let value = convert_value(value)?;
+    params.insert(key.to_owned(), value);
+  }
+
+  Ok(params)
+}
+
+fn convert_value(value: serde_yaml::Value) -> Result<Value> {
+  Ok(match value {
+    serde_yaml::Value::Null => Value::Null,
+    serde_yaml::Value::Bool(val) => Value::Bool(val),
+    serde_yaml::Value::Number(n) => {
+      if n.is_i64() {
+        Value::Number(Number::Integer(
+          n.as_i64().ok_or(ConversionError::InvalidNumberFormat)?,
+        ))
+      } else if n.is_u64() {
+        Value::Number(Number::Integer(
+          n.as_u64()
+            .ok_or(ConversionError::InvalidNumberFormat)?
+            .try_into()?,
+        ))
+      } else if n.is_f64() {
+        Value::Number(Number::Float(
+          n.as_f64()
+            .ok_or(ConversionError::InvalidNumberFormat)?
+            .into(),
+        ))
+      } else {
+        return Err(ConversionError::InvalidNumberFormat.into());
+      }
+    }
+    serde_yaml::Value::String(s) => Value::String(s),
+    serde_yaml::Value::Sequence(arr) => Value::Array(
+      arr
+        .into_iter()
+        .map(convert_value)
+        .collect::<Result<Vec<Value>>>()?,
+    ),
+    serde_yaml::Value::Mapping(m) => Value::Object(convert_params(m)?),
+  })
+}
+
+#[derive(Error, Debug)]
+pub enum ConversionError {
+  #[error("invalid key format")]
+  InvalidKeyFormat,
+
+  #[error("invalid number format")]
+  InvalidNumberFormat,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn convert_value_null() {
+    assert_eq!(convert_value(serde_yaml::Value::Null).unwrap(), Value::Null);
+  }
+
+  #[test]
+  fn convert_value_bool() {
+    assert_eq!(
+      convert_value(serde_yaml::Value::Bool(true)).unwrap(),
+      Value::Bool(true)
+    );
+    assert_eq!(
+      convert_value(serde_yaml::Value::Bool(false)).unwrap(),
+      Value::Bool(false)
+    );
+  }
+
+  #[test]
+  fn convert_value_number() {
+    assert_eq!(
+      convert_value(serde_yaml::Value::Number(0.into())).unwrap(),
+      Value::Number(Number::Integer(0))
+    );
+    assert_eq!(
+      convert_value(serde_yaml::Value::Number((-100).into())).unwrap(),
+      Value::Number(Number::Integer(-100))
+    );
+    assert_eq!(
+      convert_value(serde_yaml::Value::Number(1.5.into())).unwrap(),
+      Value::Number(Number::Float(1.5.into()))
+    );
+  }
+  #[test]
+  fn convert_value_string() {
+    assert_eq!(
+      convert_value(serde_yaml::Value::String("hello".to_string())).unwrap(),
+      Value::String("hello".to_string())
+    );
+  }
+  #[test]
+  fn convert_value_array() {
+    assert_eq!(
+      convert_value(serde_yaml::Value::Sequence(vec![
+        serde_yaml::Value::Bool(true),
+        serde_yaml::Value::Null,
+      ]))
+      .unwrap(),
+      Value::Array(vec![Value::Bool(true), Value::Null,])
+    );
+  }
+
+  #[test]
+  fn convert_value_params() {
+    let mut mapping = serde_yaml::Mapping::new();
+    mapping.insert(
+      serde_yaml::Value::String("test".to_string()),
+      serde_yaml::Value::Null,
+    );
+
+    let mut expected = Params::new();
+    expected.insert("test".to_string(), Value::Null);
+    assert_eq!(
+      convert_value(serde_yaml::Value::Mapping(mapping)).unwrap(),
+      Value::Object(expected)
+    );
+  }
+
+  #[test]
+  fn convert_params_works_correctly() {
+    let mut mapping = serde_yaml::Mapping::new();
+    mapping.insert(
+      serde_yaml::Value::String("test".to_string()),
+      serde_yaml::Value::Null,
+    );
+
+    let mut expected = Params::new();
+    expected.insert("test".to_string(), Value::Null);
+    assert_eq!(convert_params(mapping).unwrap(), expected);
+  }
+
+  #[test]
+  fn convert_params_invalid_key_type() {
+    let mut mapping = serde_yaml::Mapping::new();
+    mapping.insert(serde_yaml::Value::Null, serde_yaml::Value::Null);
+
+    assert!(convert_params(mapping).is_err());
+  }
+}
diff --git a/espanso-config/src/matches/group/mod.rs b/espanso-config/src/matches/group/mod.rs
new file mode 100644
index 00000000..3d61c239
--- /dev/null
+++ b/espanso-config/src/matches/group/mod.rs
@@ -0,0 +1,52 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::path::Path;
+
+use crate::error::NonFatalErrorSet;
+
+use super::{Match, Variable};
+
+pub(crate) mod loader;
+mod path;
+
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct MatchGroup {
+  pub imports: Vec<String>,
+  pub global_vars: Vec<Variable>,
+  pub matches: Vec<Match>,
+}
+
+impl Default for MatchGroup {
+  fn default() -> Self {
+    Self {
+      imports: Vec::new(),
+      global_vars: Vec::new(),
+      matches: Vec::new(),
+    }
+  }
+}
+
+impl MatchGroup {
+  // TODO: test
+  pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {
+    loader::load_match_group(group_path)
+  }
+}
diff --git a/espanso-config/src/matches/group/path.rs b/espanso-config/src/matches/group/path.rs
new file mode 100644
index 00000000..01fc94ea
--- /dev/null
+++ b/espanso-config/src/matches/group/path.rs
@@ -0,0 +1,164 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{anyhow, Context, Result};
+use std::path::{Path, PathBuf};
+use thiserror::Error;
+
+use crate::error::ErrorRecord;
+
+pub fn resolve_imports(
+  group_path: &Path,
+  imports: &[String],
+) -> Result<(Vec<String>, Vec<ErrorRecord>)> {
+  let mut paths = Vec::new();
+
+  // Get the containing directory
+  let current_dir = if group_path.is_file() {
+    if let Some(parent) = group_path.parent() {
+      parent
+    } else {
+      return Err(
+        ResolveImportError::Failed(format!(
+          "unable to resolve imports for match group starting from current path: {:?}",
+          group_path
+        ))
+        .into(),
+      );
+    }
+  } else {
+    group_path
+  };
+
+  let mut non_fatal_errors = Vec::new();
+
+  for import in imports.iter() {
+    let import_path = PathBuf::from(import);
+
+    // Absolute or relative import
+    let full_path = if import_path.is_relative() {
+      current_dir.join(import_path)
+    } else {
+      import_path
+    };
+
+    match dunce::canonicalize(&full_path)
+      .with_context(|| format!("unable to canonicalize import path: {:?}", full_path))
+    {
+      Ok(canonical_path) => {
+        if canonical_path.exists() && canonical_path.is_file() {
+          paths.push(canonical_path)
+        } else {
+          // Best effort imports
+          non_fatal_errors.push(ErrorRecord::error(anyhow!(
+            "unable to resolve import at path: {:?}",
+            canonical_path
+          )))
+        }
+      }
+      Err(error) => non_fatal_errors.push(ErrorRecord::error(error)),
+    }
+  }
+
+  let string_paths = paths
+    .into_iter()
+    .map(|path| path.to_string_lossy().to_string())
+    .collect();
+
+  Ok((string_paths, non_fatal_errors))
+}
+
+#[derive(Error, Debug)]
+pub enum ResolveImportError {
+  #[error("resolve import failed: `{0}`")]
+  Failed(String),
+}
+
+#[cfg(test)]
+pub mod tests {
+  use super::*;
+  use crate::util::tests::use_test_directory;
+  use std::fs::create_dir_all;
+
+  #[test]
+  fn resolve_imports_works_correctly() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+
+      let another_file = match_dir.join("another.yml");
+      std::fs::write(&another_file, "test").unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let absolute_file = sub_dir.join("absolute.yml");
+      std::fs::write(&absolute_file, "test").unwrap();
+
+      let imports = vec![
+        "another.yml".to_string(),
+        "sub/sub.yml".to_string(),
+        absolute_file.to_string_lossy().to_string(),
+        "sub/invalid.yml".to_string(), // Should be skipped
+      ];
+
+      let (resolved_imports, errors) = resolve_imports(&base_file, &imports).unwrap();
+
+      assert_eq!(
+        resolved_imports,
+        vec![
+          another_file.to_string_lossy().to_string(),
+          sub_file.to_string_lossy().to_string(),
+          absolute_file.to_string_lossy().to_string(),
+        ]
+      );
+
+      // The "sub/invalid.yml" should generate an error
+      assert_eq!(errors.len(), 1);
+    });
+  }
+
+  #[test]
+  fn resolve_imports_parent_relative_path() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(&base_file, "test").unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(&sub_file, "test").unwrap();
+
+      let imports = vec!["../base.yml".to_string()];
+
+      let (resolved_imports, errors) = resolve_imports(&sub_file, &imports).unwrap();
+
+      assert_eq!(
+        resolved_imports,
+        vec![base_file.to_string_lossy().to_string(),]
+      );
+
+      assert_eq!(errors.len(), 0);
+    });
+  }
+}
diff --git a/espanso-config/src/matches/mod.rs b/espanso-config/src/matches/mod.rs
new file mode 100644
index 00000000..7e3fb13f
--- /dev/null
+++ b/espanso-config/src/matches/mod.rs
@@ -0,0 +1,237 @@
+/*
+ * 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 enum_as_inner::EnumAsInner;
+use ordered_float::OrderedFloat;
+use std::collections::BTreeMap;
+
+use crate::counter::StructId;
+
+pub(crate) mod group;
+pub mod store;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Match {
+  pub id: StructId,
+
+  pub cause: MatchCause,
+  pub effect: MatchEffect,
+
+  // Metadata
+  pub label: Option<String>,
+}
+
+impl Default for Match {
+  fn default() -> Self {
+    Self {
+      cause: MatchCause::None,
+      effect: MatchEffect::None,
+      label: None,
+      id: 0,
+    }
+  }
+}
+
+impl Match {
+  // TODO: test
+  pub fn description(&self) -> &str {
+    if let Some(label) = &self.label {
+      label
+    } else if let MatchEffect::Text(text_effect) = &self.effect {
+      &text_effect.replace
+    } else if let MatchEffect::Image(_) = &self.effect {
+      "Image content"
+    } else {
+      "No description available for this match"
+    }
+  }
+
+  // TODO: test
+  pub fn cause_description(&self) -> Option<&str> {
+    self.cause.description()
+  }
+}
+
+// Causes
+
+#[derive(Debug, Clone, Eq, Hash, PartialEq, EnumAsInner)]
+pub enum MatchCause {
+  None,
+  Trigger(TriggerCause),
+  Regex(RegexCause),
+  // TODO: shortcut
+}
+
+impl MatchCause {
+  // TODO: test
+  pub fn description(&self) -> Option<&str> {
+    if let MatchCause::Trigger(trigger_cause) = &self {
+      trigger_cause.triggers.first().map(|s| s.as_str())
+    } else {
+      None
+    }
+    // TODO: insert rendering for hotkey/shortcut
+    // TODO: insert rendering for regex? I'm worried it might be too long
+  }
+
+  // TODO: test
+  pub fn long_description(&self) -> String {
+    if let MatchCause::Trigger(trigger_cause) = &self {
+      format!("triggers: {:?}", trigger_cause.triggers)
+    } else {
+      "No description available".to_owned()
+    }
+    // TODO: insert rendering for hotkey/shortcut
+    // TODO: insert rendering for regex? I'm worried it might be too long
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct TriggerCause {
+  pub triggers: Vec<String>,
+
+  pub left_word: bool,
+  pub right_word: bool,
+
+  pub propagate_case: bool,
+  pub uppercase_style: UpperCasingStyle,
+}
+
+impl Default for TriggerCause {
+  fn default() -> Self {
+    Self {
+      triggers: Vec::new(),
+      left_word: false,
+      right_word: false,
+      propagate_case: false,
+      uppercase_style: UpperCasingStyle::Uppercase,
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum UpperCasingStyle {
+  Uppercase,
+  Capitalize,
+  CapitalizeWords,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct RegexCause {
+  pub regex: String,
+}
+
+impl Default for RegexCause {
+  fn default() -> Self {
+    Self {
+      regex: String::new(),
+    }
+  }
+}
+
+// Effects
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
+pub enum MatchEffect {
+  None,
+  Text(TextEffect),
+  Image(ImageEffect),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct TextEffect {
+  pub replace: String,
+  pub vars: Vec<Variable>,
+  pub format: TextFormat,
+  pub force_mode: Option<TextInjectMode>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum TextFormat {
+  Plain,
+  Markdown,
+  Html,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum TextInjectMode {
+  Keys,
+  Clipboard,
+}
+
+impl Default for TextEffect {
+  fn default() -> Self {
+    Self {
+      replace: String::new(),
+      vars: Vec::new(),
+      format: TextFormat::Plain,
+      force_mode: None,
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ImageEffect {
+  pub path: String,
+}
+
+impl Default for ImageEffect {
+  fn default() -> Self {
+    Self {
+      path: String::new(),
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Variable {
+  pub id: StructId,
+  pub name: String,
+  pub var_type: String,
+  pub params: Params,
+}
+
+impl Default for Variable {
+  fn default() -> Self {
+    Self {
+      id: 0,
+      name: String::new(),
+      var_type: String::new(),
+      params: Params::new(),
+    }
+  }
+}
+
+pub type Params = BTreeMap<String, Value>;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
+pub enum Value {
+  Null,
+  Bool(bool),
+  Number(Number),
+  String(String),
+  Array(Vec<Value>),
+  Object(Params),
+}
+
+#[derive(Debug, Clone, Eq, Hash, PartialEq)]
+pub enum Number {
+  Integer(i64),
+  Float(OrderedFloat<f64>),
+}
diff --git a/espanso-config/src/matches/store/default.rs b/espanso-config/src/matches/store/default.rs
new file mode 100644
index 00000000..241074dc
--- /dev/null
+++ b/espanso-config/src/matches/store/default.rs
@@ -0,0 +1,738 @@
+/*
+ * 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 super::{MatchSet, MatchStore};
+use crate::{
+  counter::StructId,
+  error::NonFatalErrorSet,
+  matches::{group::MatchGroup, Match, Variable},
+};
+use anyhow::Context;
+use std::{
+  collections::{HashMap, HashSet},
+  path::PathBuf,
+};
+
+pub(crate) struct DefaultMatchStore {
+  pub groups: HashMap<String, MatchGroup>,
+}
+
+impl DefaultMatchStore {
+  pub fn load(paths: &[String]) -> (Self, Vec<NonFatalErrorSet>) {
+    let mut groups = HashMap::new();
+    let mut non_fatal_error_sets = Vec::new();
+
+    // Because match groups can imports other match groups,
+    // we have to load them recursively starting from the
+    // top-level ones.
+    load_match_groups_recursively(&mut groups, paths, &mut non_fatal_error_sets);
+
+    (Self { groups }, non_fatal_error_sets)
+  }
+}
+
+impl MatchStore for DefaultMatchStore {
+  fn query(&self, paths: &[String]) -> MatchSet {
+    let mut matches: Vec<&Match> = Vec::new();
+    let mut global_vars: Vec<&Variable> = Vec::new();
+    let mut visited_paths = HashSet::new();
+    let mut visited_matches = HashSet::new();
+    let mut visited_global_vars = HashSet::new();
+
+    query_matches_for_paths(
+      &self.groups,
+      &mut visited_paths,
+      &mut visited_matches,
+      &mut visited_global_vars,
+      &mut matches,
+      &mut global_vars,
+      paths,
+    );
+
+    MatchSet {
+      matches,
+      global_vars,
+    }
+  }
+
+  fn loaded_paths(&self) -> Vec<String> {
+    self.groups.keys().cloned().collect()
+  }
+}
+
+fn load_match_groups_recursively(
+  groups: &mut HashMap<String, MatchGroup>,
+  paths: &[String],
+  non_fatal_error_sets: &mut Vec<NonFatalErrorSet>,
+) {
+  for path in paths.iter() {
+    if !groups.contains_key(path) {
+      let group_path = PathBuf::from(path);
+      match MatchGroup::load(&group_path)
+        .with_context(|| format!("unable to load match group {:?}", group_path))
+      {
+        Ok((group, non_fatal_error_set)) => {
+          let imports = group.imports.clone();
+          groups.insert(path.clone(), group);
+
+          if let Some(non_fatal_error_set) = non_fatal_error_set {
+            non_fatal_error_sets.push(non_fatal_error_set);
+          }
+
+          load_match_groups_recursively(groups, &imports, non_fatal_error_sets);
+        }
+        Err(err) => {
+          non_fatal_error_sets.push(NonFatalErrorSet::single_error(&group_path, err));
+        }
+      }
+    }
+  }
+}
+
+fn query_matches_for_paths<'a>(
+  groups: &'a HashMap<String, MatchGroup>,
+  visited_paths: &mut HashSet<String>,
+  visited_matches: &mut HashSet<StructId>,
+  visited_global_vars: &mut HashSet<StructId>,
+  matches: &mut Vec<&'a Match>,
+  global_vars: &mut Vec<&'a Variable>,
+  paths: &[String],
+) {
+  for path in paths.iter() {
+    if !visited_paths.contains(path) {
+      visited_paths.insert(path.clone());
+
+      if let Some(group) = groups.get(path) {
+        query_matches_for_paths(
+          groups,
+          visited_paths,
+          visited_matches,
+          visited_global_vars,
+          matches,
+          global_vars,
+          &group.imports,
+        );
+
+        for m in group.matches.iter() {
+          if !visited_matches.contains(&m.id) {
+            matches.push(m);
+            visited_matches.insert(m.id);
+          }
+        }
+
+        for var in group.global_vars.iter() {
+          if !visited_global_vars.contains(&var.id) {
+            global_vars.push(var);
+            visited_global_vars.insert(var.id);
+          }
+        }
+      }
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::{
+    matches::{MatchCause, MatchEffect, TextEffect, TriggerCause},
+    util::tests::use_test_directory,
+  };
+  use std::fs::create_dir_all;
+
+  fn create_match(trigger: &str, replace: &str) -> Match {
+    Match {
+      cause: MatchCause::Trigger(TriggerCause {
+        triggers: vec![trigger.to_string()],
+        ..Default::default()
+      }),
+      effect: MatchEffect::Text(TextEffect {
+        replace: replace.to_string(),
+        ..Default::default()
+      }),
+      ..Default::default()
+    }
+  }
+
+  fn create_matches(matches: &[(&str, &str)]) -> Vec<Match> {
+    matches
+      .iter()
+      .map(|(trigger, replace)| create_match(trigger, replace))
+      .collect()
+  }
+
+  fn create_test_var(name: &str) -> Variable {
+    Variable {
+      name: name.to_string(),
+      var_type: "test".to_string(),
+      ..Default::default()
+    }
+  }
+
+  fn create_vars(vars: &[&str]) -> Vec<Variable> {
+    vars.iter().map(|var| create_test_var(var)).collect()
+  }
+
+  #[test]
+  fn match_store_loads_correctly() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) =
+        DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+      assert_eq!(match_store.groups.len(), 3);
+
+      let base_group = &match_store
+        .groups
+        .get(&base_file.to_string_lossy().to_string())
+        .unwrap()
+        .matches;
+      let base_group: Vec<Match> = base_group
+        .iter()
+        .map(|m| {
+          let mut copy = m.clone();
+          copy.id = 0;
+          copy
+        })
+        .collect();
+
+      assert_eq!(base_group, create_matches(&[("hello", "world")]));
+
+      let another_group = &match_store
+        .groups
+        .get(&another_file.to_string_lossy().to_string())
+        .unwrap()
+        .matches;
+      let another_group: Vec<Match> = another_group
+        .iter()
+        .map(|m| {
+          let mut copy = m.clone();
+          copy.id = 0;
+          copy
+        })
+        .collect();
+      assert_eq!(
+        another_group,
+        create_matches(&[("hello", "world2"), ("foo", "bar")])
+      );
+
+      let sub_group = &match_store
+        .groups
+        .get(&sub_file.to_string_lossy().to_string())
+        .unwrap()
+        .matches;
+      let sub_group: Vec<Match> = sub_group
+        .iter()
+        .map(|m| {
+          let mut copy = m.clone();
+          copy.id = 0;
+          copy
+        })
+        .collect();
+      assert_eq!(sub_group, create_matches(&[("hello", "world3")]));
+    });
+  }
+
+  #[test]
+  fn match_store_handles_circular_dependency() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      imports:
+        - "../_another.yml"
+
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) =
+        DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
+
+      assert_eq!(match_store.groups.len(), 3);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+    });
+  }
+
+  #[test]
+  fn match_store_query_single_path_with_imports() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      global_vars:
+        - name: var2
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) =
+        DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+
+      let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
+
+      assert_eq!(
+        match_set
+          .matches
+          .into_iter()
+          .cloned()
+          .map(|mut m| {
+            m.id = 0;
+            m
+          })
+          .collect::<Vec<Match>>(),
+        create_matches(&[
+          ("hello", "world3"),
+          ("hello", "world2"),
+          ("foo", "bar"),
+          ("hello", "world"),
+        ])
+      );
+
+      assert_eq!(
+        match_set
+          .global_vars
+          .into_iter()
+          .cloned()
+          .map(|mut v| {
+            v.id = 0;
+            v
+          })
+          .collect::<Vec<Variable>>(),
+        create_vars(&["var2", "var1"])
+      );
+    });
+  }
+
+  #[test]
+  fn match_store_query_handles_circular_depencencies() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      imports:
+        - "../_another.yml"  # Circular import
+
+      global_vars:
+        - name: var2
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) =
+        DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+
+      let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
+
+      assert_eq!(
+        match_set
+          .matches
+          .into_iter()
+          .cloned()
+          .map(|mut m| {
+            m.id = 0;
+            m
+          })
+          .collect::<Vec<Match>>(),
+        create_matches(&[
+          ("hello", "world3"),
+          ("hello", "world2"),
+          ("foo", "bar"),
+          ("hello", "world"),
+        ])
+      );
+
+      assert_eq!(
+        match_set
+          .global_vars
+          .into_iter()
+          .cloned()
+          .map(|mut v| {
+            v.id = 0;
+            v
+          })
+          .collect::<Vec<Variable>>(),
+        create_vars(&["var2", "var1"])
+      );
+    });
+  }
+
+  #[test]
+  fn match_store_query_multiple_paths() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      global_vars:
+        - name: var2
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) = DefaultMatchStore::load(&[
+        base_file.to_string_lossy().to_string(),
+        sub_file.to_string_lossy().to_string(),
+      ]);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+
+      let match_set = match_store.query(&[
+        base_file.to_string_lossy().to_string(),
+        sub_file.to_string_lossy().to_string(),
+      ]);
+
+      assert_eq!(
+        match_set
+          .matches
+          .into_iter()
+          .cloned()
+          .map(|mut m| {
+            m.id = 0;
+            m
+          })
+          .collect::<Vec<Match>>(),
+        create_matches(&[
+          ("hello", "world2"),
+          ("foo", "bar"),
+          ("hello", "world"),
+          ("hello", "world3"),
+        ])
+      );
+
+      assert_eq!(
+        match_set
+          .global_vars
+          .into_iter()
+          .cloned()
+          .map(|mut v| {
+            v.id = 0;
+            v
+          })
+          .collect::<Vec<Variable>>(),
+        create_vars(&["var1", "var2"])
+      );
+    });
+  }
+
+  #[test]
+  fn match_store_query_handle_duplicates_when_imports_and_paths_overlap() {
+    use_test_directory(|_, match_dir, _| {
+      let sub_dir = match_dir.join("sub");
+      create_dir_all(&sub_dir).unwrap();
+
+      let base_file = match_dir.join("base.yml");
+      std::fs::write(
+        &base_file,
+        r#"
+      imports:
+        - "_another.yml"
+      
+      global_vars:
+        - name: var1
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world"
+      "#,
+      )
+      .unwrap();
+
+      let another_file = match_dir.join("_another.yml");
+      std::fs::write(
+        &another_file,
+        r#"
+      imports:
+        - "sub/sub.yml"
+      
+      matches:
+        - trigger: "hello"
+          replace: "world2" 
+        - trigger: "foo"
+          replace: "bar"
+      "#,
+      )
+      .unwrap();
+
+      let sub_file = sub_dir.join("sub.yml");
+      std::fs::write(
+        &sub_file,
+        r#"
+      global_vars:
+        - name: var2
+          type: test
+
+      matches:
+        - trigger: "hello"
+          replace: "world3" 
+      "#,
+      )
+      .unwrap();
+
+      let (match_store, non_fatal_error_sets) =
+        DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
+      assert_eq!(non_fatal_error_sets.len(), 0);
+
+      let match_set = match_store.query(&[
+        base_file.to_string_lossy().to_string(),
+        sub_file.to_string_lossy().to_string(),
+      ]);
+
+      assert_eq!(
+        match_set
+          .matches
+          .into_iter()
+          .cloned()
+          .map(|mut m| {
+            m.id = 0;
+            m
+          })
+          .collect::<Vec<Match>>(),
+        create_matches(&[
+          ("hello", "world3"), // This appears only once, though it appears 2 times
+          ("hello", "world2"),
+          ("foo", "bar"),
+          ("hello", "world"),
+        ])
+      );
+
+      assert_eq!(
+        match_set
+          .global_vars
+          .into_iter()
+          .cloned()
+          .map(|mut v| {
+            v.id = 0;
+            v
+          })
+          .collect::<Vec<Variable>>(),
+        create_vars(&["var2", "var1"])
+      );
+    });
+  }
+
+  // TODO: add fatal and non-fatal error cases
+}
diff --git a/espanso-config/src/matches/store/mod.rs b/espanso-config/src/matches/store/mod.rs
new file mode 100644
index 00000000..2c252fa5
--- /dev/null
+++ b/espanso-config/src/matches/store/mod.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::error::NonFatalErrorSet;
+
+use super::{Match, Variable};
+
+mod default;
+
+pub trait MatchStore: Send {
+  fn query(&self, paths: &[String]) -> MatchSet;
+  fn loaded_paths(&self) -> Vec<String>;
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchSet<'a> {
+  pub matches: Vec<&'a Match>,
+  pub global_vars: Vec<&'a Variable>,
+}
+
+pub fn load(paths: &[String]) -> (impl MatchStore, Vec<NonFatalErrorSet>) {
+  // TODO: here we can replace the DefaultMatchStore with a caching wrapper
+  // that returns the same response for the given "paths" query
+  default::DefaultMatchStore::load(paths)
+}
diff --git a/espanso-config/src/util.rs b/espanso-config/src/util.rs
new file mode 100644
index 00000000..266d4a36
--- /dev/null
+++ b/espanso-config/src/util.rs
@@ -0,0 +1,74 @@
+/*
+ * 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/>.
+ */
+
+/// Check if the given string represents an empty YAML.
+/// In other words, it checks if the document is only composed
+/// of spaces and/or comments
+pub fn is_yaml_empty(yaml: &str) -> bool {
+  for line in yaml.lines() {
+    let trimmed_line = line.trim();
+    if !trimmed_line.starts_with('#') && !trimmed_line.is_empty() {
+      return false;
+    }
+  }
+
+  true
+}
+
+#[cfg(test)]
+pub mod tests {
+  use super::*;
+  use std::{fs::create_dir_all, path::Path};
+  use tempdir::TempDir;
+
+  pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
+    let dir = TempDir::new("tempconfig").unwrap();
+    let match_dir = dir.path().join("match");
+    create_dir_all(&match_dir).unwrap();
+
+    let config_dir = dir.path().join("config");
+    create_dir_all(&config_dir).unwrap();
+
+    callback(
+      &dunce::canonicalize(&dir.path()).unwrap(),
+      &dunce::canonicalize(match_dir).unwrap(),
+      &dunce::canonicalize(config_dir).unwrap(),
+    );
+  }
+
+  #[test]
+  fn is_yaml_empty_document_empty() {
+    assert!(is_yaml_empty(""));
+  }
+
+  #[test]
+  fn is_yaml_empty_document_with_comments() {
+    assert!(is_yaml_empty("\n#comment \n \n"));
+  }
+
+  #[test]
+  fn is_yaml_empty_document_with_comments_and_content() {
+    assert!(!is_yaml_empty("\n#comment \n field: true\n"));
+  }
+
+  #[test]
+  fn is_yaml_empty_document_with_content() {
+    assert!(!is_yaml_empty("\nfield: true\n"));
+  }
+}
diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml
new file mode 100644
index 00000000..ae760fd3
--- /dev/null
+++ b/espanso-detect/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "espanso-detect"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[features]
+# If the wayland feature is enabled, all X11 dependencies will be dropped
+# and only EVDEV-based methods will be supported.
+wayland = ["sctk"]
+
+[dependencies]
+log = "0.4.14"
+lazycell = "1.3.0"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+regex = "1.4.3"
+lazy_static = "1.4.0"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+
+[target.'cfg(target_os="linux")'.dependencies]
+libc = "0.2.85"
+scopeguard = "1.1.0"
+sctk = { package = "smithay-client-toolkit", version = "0.14.0", optional = true }
+
+[build-dependencies]
+cc = "1.0.66"
+
+[dev-dependencies]
+enum-as-inner = "0.3.3"
\ No newline at end of file
diff --git a/espanso-detect/build.rs b/espanso-detect/build.rs
new file mode 100644
index 00000000..75e398a9
--- /dev/null
+++ b/espanso-detect/build.rs
@@ -0,0 +1,83 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/win32/native.cpp");
+  println!("cargo:rerun-if-changed=src/win32/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/win32/native.h")
+    .file("src/win32/native.cpp")
+    .compile("espansodetect");
+
+  println!("cargo:rustc-link-lib=static=espansodetect");
+  println!("cargo:rustc-link-lib=dylib=user32");
+  #[cfg(target_env = "gnu")]
+  println!("cargo:rustc-link-lib=dylib=stdc++");
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/x11/native.cpp");
+  println!("cargo:rerun-if-changed=src/x11/native.h");
+  println!("cargo:rerun-if-changed=src/evdev/native.cpp");
+  println!("cargo:rerun-if-changed=src/evdev/native.h");
+
+  if cfg!(not(feature = "wayland")) {
+    cc::Build::new()
+      .cpp(true)
+      .include("src/x11")
+      .file("src/x11/native.cpp")
+      .compile("espansodetect");
+
+    println!("cargo:rustc-link-lib=static=espansodetect");
+    println!("cargo:rustc-link-lib=dylib=X11");
+    println!("cargo:rustc-link-lib=dylib=Xtst");
+  }
+
+  cc::Build::new()
+    .cpp(true)
+    .include("src/evdev")
+    .file("src/evdev/native.cpp")
+    .compile("espansodetectevdev");
+
+  println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
+  println!("cargo:rustc-link-lib=static=espansodetectevdev");
+  println!("cargo:rustc-link-lib=dylib=xkbcommon");
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/mac/native.mm");
+  println!("cargo:rerun-if-changed=src/mac/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/mac/native.h")
+    .file("src/mac/native.mm")
+    .compile("espansodetect");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansodetect");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+  println!("cargo:rustc-link-lib=framework=Carbon");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-detect/src/evdev/README.md b/espanso-detect/src/evdev/README.md
new file mode 100644
index 00000000..3037eda8
--- /dev/null
+++ b/espanso-detect/src/evdev/README.md
@@ -0,0 +1,3 @@
+This module is used to detect keyboard and mouse input using EVDEV layer, which is necessary on Wayland.
+The module started as a port of this xkbcommon example
+https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
\ No newline at end of file
diff --git a/espanso-detect/src/evdev/context.rs b/espanso-detect/src/evdev/context.rs
new file mode 100644
index 00000000..bcda581c
--- /dev/null
+++ b/espanso-detect/src/evdev/context.rs
@@ -0,0 +1,47 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use scopeguard::ScopeGuard;
+
+use super::ffi::{xkb_context, xkb_context_new, xkb_context_unref, XKB_CONTEXT_NO_FLAGS};
+use anyhow::Result;
+use thiserror::Error;
+
+pub struct Context {
+  context: *mut xkb_context,
+}
+
+impl Context {
+  pub fn new() -> Result<Context> {
+    let raw_context = unsafe { xkb_context_new(XKB_CONTEXT_NO_FLAGS) };
+    let context = scopeguard::guard(raw_context, |raw_context| unsafe {
+      xkb_context_unref(raw_context);
+    });
+
+    if raw_context.is_null() {
+      return Err(ContextError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      context: ScopeGuard::into_inner(context),
+    })
+  }
+
+  pub fn get_handle(&self) -> *mut xkb_context {
+    self.context
+  }
+}
+
+impl Drop for Context {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_context_unref(self.context);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ContextError {
+  #[error("could not create xkb context")]
+  FailedCreation(),
+}
diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs
new file mode 100644
index 00000000..2dd0b25f
--- /dev/null
+++ b/espanso-detect/src/evdev/device.rs
@@ -0,0 +1,326 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use anyhow::Result;
+use libc::{input_event, size_t, ssize_t, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY};
+use log::trace;
+use scopeguard::ScopeGuard;
+use std::collections::HashMap;
+use std::os::unix::io::AsRawFd;
+use std::{
+  ffi::{c_void, CStr},
+  fs::OpenOptions,
+};
+use std::{fs::File, os::unix::fs::OpenOptionsExt};
+use thiserror::Error;
+
+use super::sync::ModifiersState;
+use super::{
+  ffi::{
+    is_keyboard_or_mouse, xkb_key_direction, xkb_keycode_t, xkb_keymap_key_repeats, xkb_state,
+    xkb_state_get_keymap, xkb_state_key_get_one_sym, xkb_state_key_get_utf8, xkb_state_new,
+    xkb_state_unref, xkb_state_update_key, EV_KEY,
+  },
+  keymap::Keymap,
+};
+
+const EVDEV_OFFSET: i32 = 8;
+pub const KEY_STATE_RELEASE: i32 = 0;
+pub const KEY_STATE_PRESS: i32 = 1;
+pub const KEY_STATE_REPEAT: i32 = 2;
+
+#[derive(Debug)]
+pub enum RawInputEvent {
+  Keyboard(RawKeyboardEvent),
+  Mouse(RawMouseEvent),
+}
+
+#[derive(Debug)]
+pub struct RawKeyboardEvent {
+  pub sym: u32,
+  pub code: u32,
+  pub value: String,
+  pub state: i32,
+}
+
+#[derive(Debug)]
+pub struct RawMouseEvent {
+  pub code: u16,
+  pub is_down: bool,
+}
+
+pub struct Device {
+  path: String,
+  file: File,
+  state: *mut xkb_state,
+}
+
+impl Device {
+  pub fn from(path: &str, keymap: &Keymap) -> Result<Device> {
+    let file = OpenOptions::new()
+      .read(true)
+      .custom_flags(O_NONBLOCK | O_CLOEXEC | O_RDONLY)
+      .open(&path)?;
+
+    if unsafe { is_keyboard_or_mouse(file.as_raw_fd()) == 0 } {
+      return Err(DeviceError::InvalidDevice(path.to_string()).into());
+    }
+
+    let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
+    // Automatically close the state if the function does not return correctly
+    let state = scopeguard::guard(raw_state, |raw_state| unsafe {
+      xkb_state_unref(raw_state);
+    });
+
+    if raw_state.is_null() {
+      return Err(DeviceError::InvalidState(path.to_string()).into());
+    }
+
+    Ok(Self {
+      path: path.to_string(),
+      file,
+      // Release the state without freeing it
+      state: ScopeGuard::into_inner(state),
+    })
+  }
+
+  pub fn get_state(&self) -> *mut xkb_state {
+    self.state
+  }
+
+  pub fn get_raw_fd(&self) -> i32 {
+    self.file.as_raw_fd()
+  }
+
+  pub fn get_path(&self) -> String {
+    self.path.to_string()
+  }
+
+  pub fn read(&self) -> Result<Vec<RawInputEvent>> {
+    let errno_ptr = unsafe { libc::__errno_location() };
+    let mut len: ssize_t;
+    let mut evs: [input_event; 16] = unsafe { std::mem::zeroed() };
+    let mut events = Vec::new();
+
+    loop {
+      len = unsafe {
+        libc::read(
+          self.file.as_raw_fd(),
+          evs.as_mut_ptr() as *mut c_void,
+          std::mem::size_of_val(&evs),
+        )
+      };
+      if len <= 0 {
+        break;
+      }
+
+      let nevs: size_t = len as usize / std::mem::size_of::<input_event>();
+
+      #[allow(clippy::needless_range_loop)]
+      for i in 0..nevs {
+        let event = self.process_event(evs[i].type_, evs[i].code, evs[i].value);
+        if let Some(event) = event {
+          events.push(event);
+        }
+      }
+    }
+
+    if len < 0 && unsafe { *errno_ptr } != EWOULDBLOCK {
+      return Err(DeviceError::BlockingReadOperation().into());
+    }
+
+    Ok(events)
+  }
+
+  fn process_event(&self, _type: u16, code: u16, value: i32) -> Option<RawInputEvent> {
+    if _type != EV_KEY {
+      return None;
+    }
+
+    let is_down = value == KEY_STATE_PRESS;
+
+    // Check if the current event originated from a mouse
+    if (0x110..=0x117).contains(&code) {
+      // Mouse event
+      return Some(RawInputEvent::Mouse(RawMouseEvent { code, is_down }));
+    }
+
+    // Keyboard event
+
+    let keycode: xkb_keycode_t = EVDEV_OFFSET as u32 + code as u32;
+    let keymap = unsafe { xkb_state_get_keymap(self.get_state()) };
+
+    if value == KEY_STATE_REPEAT && unsafe { xkb_keymap_key_repeats(keymap, keycode) } == 0 {
+      return None;
+    }
+
+    let sym = unsafe { xkb_state_key_get_one_sym(self.get_state(), keycode) };
+    if sym == 0 {
+      return None;
+    }
+
+    // Extract the utf8 char
+    let mut buffer: [u8; 16] = [0; 16];
+    unsafe {
+      xkb_state_key_get_utf8(
+        self.get_state(),
+        keycode,
+        buffer.as_mut_ptr() as *mut i8,
+        std::mem::size_of_val(&buffer),
+      )
+    };
+    let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) };
+    let content = content_raw.to_string_lossy().to_string();
+
+    let event = RawKeyboardEvent {
+      state: value,
+      code: keycode,
+      sym,
+      value: content,
+    };
+
+    if value == KEY_STATE_RELEASE {
+      unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::UP) };
+    } else {
+      unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::DOWN) };
+    }
+
+    Some(RawInputEvent::Keyboard(event))
+  }
+
+  pub fn update_key(&mut self, code: u32, pressed: bool) {
+    let direction = if pressed {
+      super::ffi::xkb_key_direction::DOWN
+    } else {
+      super::ffi::xkb_key_direction::UP
+    };
+    unsafe {
+      xkb_state_update_key(self.get_state(), code, direction);
+    }
+  }
+
+  pub fn update_modifier_state(
+    &mut self,
+    modifiers_state: &ModifiersState,
+    modifiers_map: &HashMap<String, u32>,
+  ) {
+    if modifiers_state.alt {
+      self.update_key(
+        *modifiers_map
+          .get("alt")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+    }
+    if modifiers_state.ctrl {
+      self.update_key(
+        *modifiers_map
+          .get("ctrl")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+    }
+    if modifiers_state.meta {
+      self.update_key(
+        *modifiers_map
+          .get("meta")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+    }
+    if modifiers_state.num_lock {
+      self.update_key(
+        *modifiers_map
+          .get("num_lock")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+      self.update_key(
+        *modifiers_map
+          .get("num_lock")
+          .expect("unable to find modifiers key in map"),
+        false,
+      );
+    }
+    if modifiers_state.shift {
+      self.update_key(
+        *modifiers_map
+          .get("shift")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+    }
+    if modifiers_state.caps_lock {
+      self.update_key(
+        *modifiers_map
+          .get("caps_lock")
+          .expect("unable to find modifiers key in map"),
+        true,
+      );
+      self.update_key(
+        *modifiers_map
+          .get("caps_lock")
+          .expect("unable to find modifiers key in map"),
+        false,
+      );
+    }
+  }
+}
+
+impl Drop for Device {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_state_unref(self.state);
+    }
+  }
+}
+
+pub fn get_devices(keymap: &Keymap) -> Result<Vec<Device>> {
+  let mut keyboards = Vec::new();
+  let dirs = std::fs::read_dir("/dev/input/")?;
+  for entry in dirs {
+    match entry {
+      Ok(device) => {
+        // Skip non-eventX devices
+        if !device.file_name().to_string_lossy().starts_with("event") {
+          continue;
+        }
+
+        let path = device.path().to_string_lossy().to_string();
+        let keyboard = Device::from(&path, keymap);
+        match keyboard {
+          Ok(keyboard) => {
+            keyboards.push(keyboard);
+          }
+          Err(error) => {
+            trace!("error opening keyboard: {}", error);
+          }
+        }
+      }
+      Err(error) => {
+        trace!("could not read keyboard device: {}", error);
+      }
+    }
+  }
+
+  if keyboards.is_empty() {
+    return Err(DeviceError::NoDevicesFound().into());
+  }
+
+  Ok(keyboards)
+}
+
+#[derive(Error, Debug)]
+pub enum DeviceError {
+  #[error("could not create xkb state for `{0}`")]
+  InvalidState(String),
+
+  #[error("`{0}` is not a valid device")]
+  InvalidDevice(String),
+
+  #[error("no devices found")]
+  NoDevicesFound(),
+
+  #[error("read operation can't block device")]
+  BlockingReadOperation(),
+}
diff --git a/espanso-detect/src/evdev/ffi.rs b/espanso-detect/src/evdev/ffi.rs
new file mode 100644
index 00000000..b7b3e198
--- /dev/null
+++ b/espanso-detect/src/evdev/ffi.rs
@@ -0,0 +1,78 @@
+// Bindings taken from: https://github.com/rtbo/xkbcommon-rs/blob/master/src/xkb/ffi.rs
+
+use std::os::raw::c_int;
+
+use libc::c_char;
+
+#[allow(non_camel_case_types)]
+pub enum xkb_context {}
+#[allow(non_camel_case_types)]
+pub enum xkb_state {}
+#[allow(non_camel_case_types)]
+pub enum xkb_keymap {}
+#[allow(non_camel_case_types)]
+pub type xkb_keycode_t = u32;
+#[allow(non_camel_case_types)]
+pub type xkb_keysym_t = u32;
+
+#[repr(C)]
+pub struct xkb_rule_names {
+  pub rules: *const c_char,
+  pub model: *const c_char,
+  pub layout: *const c_char,
+  pub variant: *const c_char,
+  pub options: *const c_char,
+}
+
+#[repr(C)]
+#[allow(clippy::upper_case_acronyms)]
+pub enum xkb_key_direction {
+  UP,
+  DOWN,
+}
+
+#[allow(non_camel_case_types)]
+pub type xkb_keymap_compile_flags = u32;
+pub const XKB_KEYMAP_COMPILE_NO_FLAGS: u32 = 0;
+
+#[allow(non_camel_case_types)]
+pub type xkb_context_flags = u32;
+pub const XKB_CONTEXT_NO_FLAGS: u32 = 0;
+
+#[allow(non_camel_case_types)]
+pub type xkb_state_component = u32;
+
+pub const EV_KEY: u16 = 0x01;
+
+#[link(name = "xkbcommon")]
+extern "C" {
+  pub fn xkb_state_unref(state: *mut xkb_state);
+  pub fn xkb_state_new(keymap: *mut xkb_keymap) -> *mut xkb_state;
+  pub fn xkb_keymap_new_from_names(
+    context: *mut xkb_context,
+    names: *const xkb_rule_names,
+    flags: xkb_keymap_compile_flags,
+  ) -> *mut xkb_keymap;
+  pub fn xkb_keymap_unref(keymap: *mut xkb_keymap);
+  pub fn xkb_context_new(flags: xkb_context_flags) -> *mut xkb_context;
+  pub fn xkb_context_unref(context: *mut xkb_context);
+  pub fn xkb_state_get_keymap(state: *mut xkb_state) -> *mut xkb_keymap;
+  pub fn xkb_keymap_key_repeats(keymap: *mut xkb_keymap, key: xkb_keycode_t) -> c_int;
+  pub fn xkb_state_update_key(
+    state: *mut xkb_state,
+    key: xkb_keycode_t,
+    direction: xkb_key_direction,
+  ) -> xkb_state_component;
+  pub fn xkb_state_key_get_utf8(
+    state: *mut xkb_state,
+    key: xkb_keycode_t,
+    buffer: *mut c_char,
+    size: usize,
+  ) -> c_int;
+  pub fn xkb_state_key_get_one_sym(state: *mut xkb_state, key: xkb_keycode_t) -> xkb_keysym_t;
+}
+
+#[link(name = "espansodetectevdev", kind = "static")]
+extern "C" {
+  pub fn is_keyboard_or_mouse(fd: i32) -> i32;
+}
diff --git a/espanso-detect/src/evdev/hotkey.rs b/espanso-detect/src/evdev/hotkey.rs
new file mode 100644
index 00000000..bb768c3b
--- /dev/null
+++ b/espanso-detect/src/evdev/hotkey.rs
@@ -0,0 +1,149 @@
+/*
+ * 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 log::error;
+
+use crate::{
+  event::{KeyboardEvent, Status},
+  hotkey::HotKey,
+};
+use std::{collections::HashMap, time::Instant};
+
+use super::state::State;
+
+// Number of milliseconds that define how long the hotkey memory
+// should retain pressed keys
+const HOTKEY_WINDOW_TIMEOUT: u128 = 5000;
+
+pub type KeySym = u32;
+pub type KeyCode = u32;
+pub type HotkeyMemoryMap = Vec<(KeyCode, Instant)>;
+
+pub struct HotKeyFilter {
+  map: HashMap<KeySym, KeyCode>,
+  memory: HotkeyMemoryMap,
+  hotkey_raw_map: HashMap<i32, Vec<KeyCode>>,
+}
+
+impl HotKeyFilter {
+  pub fn new() -> Self {
+    Self {
+      map: HashMap::new(),
+      memory: HotkeyMemoryMap::new(),
+      hotkey_raw_map: HashMap::new(),
+    }
+  }
+
+  pub fn initialize(&mut self, state: &State, hotkeys: &[HotKey]) {
+    // First load the map
+    self.map = HashMap::new();
+    for code in 0..256 {
+      if let Some(sym) = state.get_sym(code) {
+        self.map.insert(sym, code);
+      }
+    }
+
+    // Then the actual hotkeys
+    self.hotkey_raw_map = hotkeys
+      .iter()
+      .filter_map(|hk| {
+        let codes = Self::convert_hotkey_to_codes(self, hk);
+        if codes.is_none() {
+          error!("unable to register hotkey {:?}", hk);
+        }
+        Some((hk.id, codes?))
+      })
+      .collect();
+  }
+
+  pub fn process_event(&mut self, event: &KeyboardEvent) -> Option<i32> {
+    let mut hotkey = None;
+    let mut key_code = None;
+
+    let mut to_be_removed = Vec::new();
+
+    if event.status == Status::Released {
+      // Remove from the memory all the key occurrences
+      to_be_removed.extend(self.memory.iter().enumerate().filter_map(|(i, (code, _))| {
+        if *code == event.code {
+          Some(i)
+        } else {
+          None
+        }
+      }));
+    } else {
+      key_code = Some(event.code)
+    }
+
+    // Remove the old entries
+    to_be_removed.extend(
+      self
+        .memory
+        .iter()
+        .enumerate()
+        .filter_map(|(i, (_, instant))| {
+          if instant.elapsed().as_millis() > HOTKEY_WINDOW_TIMEOUT {
+            Some(i)
+          } else {
+            None
+          }
+        }),
+    );
+
+    // Remove duplicates and revert
+    if !to_be_removed.is_empty() {
+      #[allow(clippy::stable_sort_primitive)]
+      to_be_removed.sort();
+      to_be_removed.dedup();
+      to_be_removed.reverse();
+      to_be_removed.into_iter().for_each(|index| {
+        self.memory.remove(index);
+      })
+    }
+
+    if let Some(code) = key_code {
+      self.memory.push((code, Instant::now()));
+
+      for (id, codes) in self.hotkey_raw_map.iter() {
+        if codes
+          .iter()
+          .all(|hk_code| self.memory.iter().any(|(m_code, _)| m_code == hk_code))
+        {
+          hotkey = Some(*id);
+          break;
+        }
+      }
+    }
+
+    hotkey
+  }
+
+  fn convert_hotkey_to_codes(&self, hk: &HotKey) -> Option<Vec<KeyCode>> {
+    let mut codes = Vec::new();
+    let key_code = self.map.get(&hk.key.to_code()?)?;
+    codes.push(*key_code);
+
+    for modifier in hk.modifiers.iter() {
+      let code = self.map.get(&modifier.to_code()?)?;
+      codes.push(*code);
+    }
+
+    Some(codes)
+  }
+}
diff --git a/espanso-detect/src/evdev/keymap.rs b/espanso-detect/src/evdev/keymap.rs
new file mode 100644
index 00000000..341bd632
--- /dev/null
+++ b/espanso-detect/src/evdev/keymap.rs
@@ -0,0 +1,112 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use std::ffi::CString;
+
+use scopeguard::ScopeGuard;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::KeyboardConfig;
+
+use super::{
+  context::Context,
+  ffi::{
+    xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref, xkb_rule_names,
+    XKB_KEYMAP_COMPILE_NO_FLAGS,
+  },
+};
+
+pub struct Keymap {
+  keymap: *mut xkb_keymap,
+}
+
+impl Keymap {
+  pub fn new(context: &Context, rmlvo: Option<KeyboardConfig>) -> Result<Keymap> {
+    let owned_rmlvo = Self::generate_owned_rmlvo(rmlvo);
+    let names = Self::generate_names(&owned_rmlvo);
+
+    let raw_keymap = unsafe {
+      xkb_keymap_new_from_names(context.get_handle(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS)
+    };
+    let keymap = scopeguard::guard(raw_keymap, |raw_keymap| unsafe {
+      xkb_keymap_unref(raw_keymap);
+    });
+
+    if raw_keymap.is_null() {
+      return Err(KeymapError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      keymap: ScopeGuard::into_inner(keymap),
+    })
+  }
+
+  pub fn get_handle(&self) -> *mut xkb_keymap {
+    self.keymap
+  }
+
+  fn generate_owned_rmlvo(rmlvo: Option<KeyboardConfig>) -> OwnedRawKeyboardConfig {
+    let rules = rmlvo
+      .as_ref()
+      .and_then(|config| config.rules.clone())
+      .unwrap_or_default();
+    let model = rmlvo
+      .as_ref()
+      .and_then(|config| config.model.clone())
+      .unwrap_or_default();
+    let layout = rmlvo
+      .as_ref()
+      .and_then(|config| config.layout.clone())
+      .unwrap_or_default();
+    let variant = rmlvo
+      .as_ref()
+      .and_then(|config| config.variant.clone())
+      .unwrap_or_default();
+    let options = rmlvo
+      .as_ref()
+      .and_then(|config| config.options.clone())
+      .unwrap_or_default();
+
+    OwnedRawKeyboardConfig {
+      rules: CString::new(rules).expect("unable to create CString for keymap"),
+      model: CString::new(model).expect("unable to create CString for keymap"),
+      layout: CString::new(layout).expect("unable to create CString for keymap"),
+      variant: CString::new(variant).expect("unable to create CString for keymap"),
+      options: CString::new(options).expect("unable to create CString for keymap"),
+    }
+  }
+
+  fn generate_names(owned_config: &OwnedRawKeyboardConfig) -> xkb_rule_names {
+    xkb_rule_names {
+      rules: owned_config.rules.as_ptr(),
+      model: owned_config.model.as_ptr(),
+      layout: owned_config.layout.as_ptr(),
+      variant: owned_config.variant.as_ptr(),
+      options: owned_config.options.as_ptr(),
+    }
+  }
+}
+
+impl Drop for Keymap {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_keymap_unref(self.keymap);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum KeymapError {
+  #[error("could not create xkb keymap")]
+  FailedCreation(),
+}
+
+struct OwnedRawKeyboardConfig {
+  rules: CString,
+  model: CString,
+  layout: CString,
+  variant: CString,
+  options: CString,
+}
diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs
new file mode 100644
index 00000000..d7d63377
--- /dev/null
+++ b/espanso-detect/src/evdev/mod.rs
@@ -0,0 +1,422 @@
+// This code is heavily inspired by the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+/*
+ * 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/>.
+ */
+
+mod context;
+mod device;
+mod ffi;
+mod hotkey;
+mod keymap;
+mod state;
+mod sync;
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+use anyhow::{Context as AnyhowContext, Result};
+use context::Context;
+use device::{get_devices, Device};
+use keymap::Keymap;
+use lazycell::LazyCell;
+use libc::{
+  __errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD,
+};
+use log::{debug, error, info, trace};
+use thiserror::Error;
+
+use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
+use crate::event::{Key::*, MouseButton, MouseEvent};
+use crate::{event::HotKeyEvent, event::Variant::*, hotkey::HotKey};
+use crate::{event::Status::*, KeyboardConfig, Source, SourceCallback, SourceCreationOptions};
+
+use self::{
+  device::{DeviceError, RawInputEvent, KEY_STATE_PRESS, KEY_STATE_RELEASE},
+  hotkey::HotKeyFilter,
+  state::State,
+};
+
+const BTN_LEFT: u16 = 0x110;
+const BTN_RIGHT: u16 = 0x111;
+const BTN_MIDDLE: u16 = 0x112;
+const BTN_SIDE: u16 = 0x113;
+const BTN_EXTRA: u16 = 0x114;
+
+// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
+// keycode set (where ESC is 9).
+const EVDEV_OFFSET: u32 = 8;
+
+// List of modifier keycodes, as defined in the "input-event-codes.h" header
+// TODO: create an option to override them if needed
+const KEY_CTRL: u32 = 29;
+const KEY_SHIFT: u32 = 42;
+const KEY_ALT: u32 = 56;
+const KEY_META: u32 = 125;
+const KEY_CAPSLOCK: u32 = 58;
+const KEY_NUMLOCK: u32 = 69;
+
+pub struct EVDEVSource {
+  devices: Vec<Device>,
+  hotkeys: Vec<HotKey>,
+
+  _keyboard_rmlvo: Option<KeyboardConfig>,
+  _context: LazyCell<Context>,
+  _keymap: LazyCell<Keymap>,
+  _hotkey_filter: RefCell<HotKeyFilter>,
+  _modifiers_map: HashMap<String, u32>,
+}
+
+#[allow(clippy::new_without_default)]
+impl EVDEVSource {
+  pub fn new(options: SourceCreationOptions) -> EVDEVSource {
+    let mut modifiers_map = HashMap::new();
+    modifiers_map.insert("ctrl".to_string(), KEY_CTRL + EVDEV_OFFSET);
+    modifiers_map.insert("shift".to_string(), KEY_SHIFT + EVDEV_OFFSET);
+    modifiers_map.insert("alt".to_string(), KEY_ALT + EVDEV_OFFSET);
+    modifiers_map.insert("meta".to_string(), KEY_META + EVDEV_OFFSET);
+    modifiers_map.insert("caps_lock".to_string(), KEY_CAPSLOCK + EVDEV_OFFSET);
+    modifiers_map.insert("num_lock".to_string(), KEY_NUMLOCK + EVDEV_OFFSET);
+
+    Self {
+      devices: Vec::new(),
+      hotkeys: options.hotkeys,
+      _context: LazyCell::new(),
+      _keymap: LazyCell::new(),
+      _keyboard_rmlvo: options.evdev_keyboard_rmlvo,
+      _hotkey_filter: RefCell::new(HotKeyFilter::new()),
+      _modifiers_map: modifiers_map,
+    }
+  }
+}
+
+impl Source for EVDEVSource {
+  fn initialize(&mut self) -> Result<()> {
+    let context = Context::new().expect("unable to obtain xkb context");
+    let keymap =
+      Keymap::new(&context, self._keyboard_rmlvo.clone()).expect("unable to create xkb keymap");
+
+    match get_devices(&keymap) {
+      Ok(devices) => self.devices = devices,
+      Err(error) => {
+        if let Some(device_error) = error.downcast_ref::<DeviceError>() {
+          if matches!(device_error, DeviceError::NoDevicesFound()) {
+            error!("Unable to open EVDEV devices, this usually has to do with permissions.");
+            error!(
+              "You can either add the current user to the 'input' group or run espanso as root"
+            );
+            return Err(EVDEVSourceError::PermissionDenied().into());
+          }
+        }
+        return Err(error);
+      }
+    }
+
+    let state = State::new(&keymap)?;
+
+    info!("Querying modifier status...");
+    if let Some(modifiers_state) =
+      sync::get_modifiers_state().context("EVDEV modifier context state synchronization")?
+    {
+      debug!("Updating device modifier state: {:?}", modifiers_state);
+
+      for device in &mut self.devices {
+        device.update_modifier_state(&modifiers_state, &self._modifiers_map);
+      }
+    }
+
+    // Initialize the hotkeys
+    self
+      ._hotkey_filter
+      .borrow_mut()
+      .initialize(&state, &self.hotkeys);
+
+    if self._context.fill(context).is_err() {
+      return Err(EVDEVSourceError::InitFailure().into());
+    }
+    if self._keymap.fill(keymap).is_err() {
+      return Err(EVDEVSourceError::InitFailure().into());
+    }
+
+    Ok(())
+  }
+
+  fn eventloop(&self, event_callback: SourceCallback) -> Result<()> {
+    if self.devices.is_empty() {
+      error!("can't start eventloop without evdev devices");
+      return Err(EVDEVSourceError::NoDevices().into());
+    }
+
+    let raw_epfd = unsafe { libc::epoll_create1(0) };
+    let epfd = scopeguard::guard(raw_epfd, |raw_epfd| unsafe {
+      close(raw_epfd);
+    });
+
+    if *epfd < 0 {
+      error!("could not create epoll instance");
+      return Err(EVDEVSourceError::Internal().into());
+    }
+
+    // Setup epoll for all input devices
+    let errno_ptr = unsafe { __errno_location() };
+    for (i, device) in self.devices.iter().enumerate() {
+      let mut ev: epoll_event = unsafe { std::mem::zeroed() };
+      ev.events = EPOLLIN as u32;
+      ev.u64 = i as u64;
+      if unsafe { epoll_ctl(*epfd, EPOLL_CTL_ADD, device.get_raw_fd(), &mut ev) } != 0 {
+        error!(
+          "Could not add {} to epoll, errno {}",
+          device.get_path(),
+          unsafe { *errno_ptr }
+        );
+        return Err(EVDEVSourceError::Internal().into());
+      }
+    }
+
+    let mut hotkey_filter = self._hotkey_filter.borrow_mut();
+
+    // Read events indefinitely
+    let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() };
+    loop {
+      let ret = unsafe { epoll_wait(*epfd, evs.as_mut_ptr(), 16, -1) };
+      if ret < 0 {
+        if unsafe { *errno_ptr } == EINTR {
+          continue;
+        } else {
+          error!("Could not poll for events, {}", unsafe { *errno_ptr });
+          return Err(EVDEVSourceError::Internal().into());
+        }
+      }
+
+      for ev in evs.iter() {
+        let device = &self.devices[ev.u64 as usize];
+        match device.read() {
+          Ok(events) if !events.is_empty() => {
+            // Convert raw events to the common format and invoke the callback
+            events.into_iter().for_each(|raw_event| {
+              let event: Option<InputEvent> = raw_event.into();
+              if let Some(event) = event {
+                // On Wayland we need to detect the global shortcuts manually
+                if let InputEvent::Keyboard(key_event) = &event {
+                  if let Some(hotkey) = (*hotkey_filter).process_event(key_event) {
+                    event_callback(InputEvent::HotKey(HotKeyEvent { hotkey_id: hotkey }))
+                  }
+                }
+
+                event_callback(event);
+              } else {
+                trace!("unable to convert raw event to input event");
+              }
+            });
+          }
+          Ok(_) => { /* SKIP EMPTY */ }
+          Err(err) => error!("Can't read from device {}: {}", device.get_path(), err),
+        }
+      }
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum EVDEVSourceError {
+  #[error("initialization failed")]
+  InitFailure(),
+
+  #[error("permission denied")]
+  PermissionDenied(),
+
+  #[error("no devices")]
+  NoDevices(),
+
+  #[error("internal error")]
+  Internal(),
+}
+
+impl From<RawInputEvent> for Option<InputEvent> {
+  fn from(raw: RawInputEvent) -> Option<InputEvent> {
+    match raw {
+      RawInputEvent::Keyboard(keyboard_event) => {
+        let (key, variant) = key_sym_to_key(keyboard_event.sym as i32);
+        let value = if keyboard_event.value.is_empty() {
+          None
+        } else {
+          Some(keyboard_event.value)
+        };
+
+        let status = if keyboard_event.state == KEY_STATE_PRESS {
+          Pressed
+        } else if keyboard_event.state == KEY_STATE_RELEASE {
+          Released
+        } else {
+          // Filter out the "repeated" events
+          return None;
+        };
+
+        return Some(InputEvent::Keyboard(KeyboardEvent {
+          key,
+          value,
+          status,
+          variant,
+          code: keyboard_event.code,
+        }));
+      }
+      RawInputEvent::Mouse(mouse_event) => {
+        let button = raw_to_mouse_button(mouse_event.code);
+
+        let status = if mouse_event.is_down {
+          Pressed
+        } else {
+          Released
+        };
+
+        if let Some(button) = button {
+          return Some(InputEvent::Mouse(MouseEvent { button, status }));
+        }
+      }
+    }
+
+    None
+  }
+}
+
+// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
+  match key_sym {
+    // Modifiers
+    0xFFE9 => (Alt, Some(Left)),
+    0xFFEA => (Alt, Some(Right)),
+    0xFFE5 => (CapsLock, None),
+    0xFFE3 => (Control, Some(Left)),
+    0xFFE4 => (Control, Some(Right)),
+    0xFFE7 | 0xFFEB => (Meta, Some(Left)),
+    0xFFE8 | 0xFFEC => (Meta, Some(Right)),
+    0xFF7F => (NumLock, None),
+    0xFFE1 => (Shift, Some(Left)),
+    0xFFE2 => (Shift, Some(Right)),
+
+    // Whitespace
+    0xFF0D => (Enter, None),
+    0xFF09 => (Tab, None),
+    0x20 => (Space, None),
+
+    // Navigation
+    0xFF54 => (ArrowDown, None),
+    0xFF51 => (ArrowLeft, None),
+    0xFF53 => (ArrowRight, None),
+    0xFF52 => (ArrowUp, None),
+    0xFF57 => (End, None),
+    0xFF50 => (Home, None),
+    0xFF56 => (PageDown, None),
+    0xFF55 => (PageUp, None),
+
+    // UI
+    0xFF1B => (Escape, None),
+
+    // Editing keys
+    0xFF08 => (Backspace, None),
+
+    // Function keys
+    0xFFBE => (F1, None),
+    0xFFBF => (F2, None),
+    0xFFC0 => (F3, None),
+    0xFFC1 => (F4, None),
+    0xFFC2 => (F5, None),
+    0xFFC3 => (F6, None),
+    0xFFC4 => (F7, None),
+    0xFFC5 => (F8, None),
+    0xFFC6 => (F9, None),
+    0xFFC7 => (F10, None),
+    0xFFC8 => (F11, None),
+    0xFFC9 => (F12, None),
+    0xFFCA => (F13, None),
+    0xFFCB => (F14, None),
+    0xFFCC => (F15, None),
+    0xFFCD => (F16, None),
+    0xFFCE => (F17, None),
+    0xFFCF => (F18, None),
+    0xFFD0 => (F19, None),
+    0xFFD1 => (F20, None),
+
+    // Other keys, includes the raw code provided by the operating system
+    _ => (Other(key_sym), None),
+  }
+}
+
+// These codes can be found in the "input-event-codes.h" header file
+fn raw_to_mouse_button(raw: u16) -> Option<MouseButton> {
+  match raw {
+    BTN_LEFT => Some(MouseButton::Left),
+    BTN_RIGHT => Some(MouseButton::Right),
+    BTN_MIDDLE => Some(MouseButton::Middle),
+    BTN_SIDE => Some(MouseButton::Button1),
+    BTN_EXTRA => Some(MouseButton::Button2),
+    _ => None,
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use device::RawMouseEvent;
+
+  use crate::event::{InputEvent, Key::Other, KeyboardEvent};
+
+  use super::{
+    device::{RawInputEvent, RawKeyboardEvent},
+    *,
+  };
+
+  #[test]
+  fn raw_to_input_event_keyboard_works_correctly() {
+    let raw = RawInputEvent::Keyboard(RawKeyboardEvent {
+      sym: 0x4B,
+      value: "k".to_owned(),
+      state: KEY_STATE_RELEASE,
+      code: 0,
+    });
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Keyboard(KeyboardEvent {
+        key: Other(0x4B),
+        status: Released,
+        value: Some("k".to_string()),
+        variant: None,
+        code: 0,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_event_mouse_works_correctly() {
+    let raw = RawInputEvent::Mouse(RawMouseEvent {
+      code: BTN_RIGHT,
+      is_down: false,
+    });
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Mouse(MouseEvent {
+        status: Released,
+        button: MouseButton::Right,
+      })
+    );
+  }
+}
diff --git a/espanso-detect/src/evdev/native.cpp b/espanso-detect/src/evdev/native.cpp
new file mode 100644
index 00000000..31e4629a
--- /dev/null
+++ b/espanso-detect/src/evdev/native.cpp
@@ -0,0 +1,90 @@
+// A good portion of the following code has been taken by the "interactive-evdev.c"
+// example of "libxkbcommon" by Ran Benita. The original license is included as follows:
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+/*
+ * Copyright © 2012 Ran Benita <ran234@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "native.h"
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <getopt.h>
+#include <limits.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <string>
+
+#include <sys/epoll.h>
+#include <linux/input.h>
+
+#include "xkbcommon/xkbcommon.h"
+
+#define NLONGS(n) (((n) + LONG_BIT - 1) / LONG_BIT)
+
+static bool
+evdev_bit_is_set(const unsigned long *array, int bit)
+{
+  return array[bit / LONG_BIT] & (1LL << (bit % LONG_BIT));
+}
+
+/* Some heuristics to see if the device is a keyboard. */
+int32_t is_keyboard_or_mouse(int fd)
+{
+  int i;
+  unsigned long evbits[NLONGS(EV_CNT)] = {0};
+  unsigned long keybits[NLONGS(KEY_CNT)] = {0};
+
+  errno = 0;
+  ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), evbits);
+  if (errno)
+    return false;
+
+  if (!evdev_bit_is_set(evbits, EV_KEY))
+    return false;
+
+  errno = 0;
+  ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybits)), keybits);
+  if (errno)
+    return false;
+
+  // Test for keyboard keys
+  for (i = KEY_RESERVED; i <= KEY_MIN_INTERESTING; i++)
+    if (evdev_bit_is_set(keybits, i))
+      return true;
+
+  // Test for mouse keys
+  for (i = BTN_MOUSE; i <= BTN_TASK; i++)
+    if (evdev_bit_is_set(keybits, i))
+      return true;
+
+  return false;
+}
\ No newline at end of file
diff --git a/espanso-detect/src/evdev/native.h b/espanso-detect/src/evdev/native.h
new file mode 100644
index 00000000..1949e203
--- /dev/null
+++ b/espanso-detect/src/evdev/native.h
@@ -0,0 +1,27 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_DETECT_EVDEV_H
+#define ESPANSO_DETECT_EVDEV_H
+
+#include <stdint.h>
+
+extern "C" int32_t is_keyboard_or_mouse(int fd);
+
+#endif //ESPANSO_DETECT_EVDEV_H
\ No newline at end of file
diff --git a/espanso-detect/src/evdev/state.rs b/espanso-detect/src/evdev/state.rs
new file mode 100644
index 00000000..b7edf0ef
--- /dev/null
+++ b/espanso-detect/src/evdev/state.rs
@@ -0,0 +1,56 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use scopeguard::ScopeGuard;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use super::{
+  ffi::{xkb_state, xkb_state_key_get_one_sym, xkb_state_new, xkb_state_unref},
+  keymap::Keymap,
+};
+
+pub struct State {
+  state: *mut xkb_state,
+}
+
+impl State {
+  pub fn new(keymap: &Keymap) -> Result<State> {
+    let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
+    let state = scopeguard::guard(raw_state, |raw_state| unsafe {
+      xkb_state_unref(raw_state);
+    });
+
+    if raw_state.is_null() {
+      return Err(StateError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      state: ScopeGuard::into_inner(state),
+    })
+  }
+
+  pub fn get_sym(&self, code: u32) -> Option<u32> {
+    let sym = unsafe { xkb_state_key_get_one_sym(self.state, code) };
+    if sym == 0 {
+      None
+    } else {
+      Some(sym)
+    }
+  }
+}
+
+impl Drop for State {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_state_unref(self.state);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum StateError {
+  #[error("could not create xkb state")]
+  FailedCreation(),
+}
diff --git a/espanso-detect/src/evdev/sync/mod.rs b/espanso-detect/src/evdev/sync/mod.rs
new file mode 100644
index 00000000..f285132c
--- /dev/null
+++ b/espanso-detect/src/evdev/sync/mod.rs
@@ -0,0 +1,39 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, Clone, Copy)]
+pub struct ModifiersState {
+  pub ctrl: bool,
+  pub alt: bool,
+  pub shift: bool,
+  pub caps_lock: bool,
+  pub meta: bool,
+  pub num_lock: bool,
+}
+
+#[cfg(feature = "wayland")]
+mod wayland;
+#[cfg(feature = "wayland")]
+pub use wayland::get_modifiers_state;
+
+#[cfg(not(feature = "wayland"))]
+pub fn get_modifiers_state() -> anyhow::Result<Option<ModifiersState>> {
+  // Fallback for non-wayland systems
+  Ok(None)
+}
diff --git a/espanso-detect/src/evdev/sync/wayland.rs b/espanso-detect/src/evdev/sync/wayland.rs
new file mode 100644
index 00000000..750b0530
--- /dev/null
+++ b/espanso-detect/src/evdev/sync/wayland.rs
@@ -0,0 +1,248 @@
+// This module was implemented starting from this wonderful example:
+// https://github.com/Smithay/client-toolkit/blob/master/examples/kbd_input.rs
+
+use std::cell::RefCell;
+use std::cmp::min;
+use std::rc::Rc;
+
+use anyhow::{Context, Result};
+use log::error;
+use sctk::reexports::calloop;
+use sctk::reexports::client::protocol::{wl_keyboard, wl_shm, wl_surface};
+use sctk::seat::keyboard::{map_keyboard_repeat, Event as KbEvent, RepeatKind};
+use sctk::shm::AutoMemPool;
+use sctk::window::{Event as WEvent, FallbackFrame};
+
+sctk::default_environment!(EspansoModifiersSync, desktop);
+
+pub fn get_modifiers_state() -> Result<Option<super::ModifiersState>> {
+  let (env, display, queue) = sctk::new_default_environment!(EspansoModifiersSync, desktop)
+    .context("Unable to connect to a Wayland compositor")?;
+
+  let result = Rc::new(RefCell::new(None));
+
+  /*
+   * Prepare a calloop event loop to handle key repetion
+   */
+  // Here `Option<WEvent>` is the type of a global value that will be shared by
+  // all callbacks invoked by the event loop.
+  let mut event_loop = calloop::EventLoop::<Option<WEvent>>::try_new().unwrap();
+
+  /*
+   * Create a buffer with window contents
+   */
+
+  let mut dimensions = (1u32, 1u32);
+
+  /*
+   * Init wayland objects
+   */
+
+  let surface = env.create_surface().detach();
+
+  let mut window = env
+    .create_window::<FallbackFrame, _>(surface, None, dimensions, move |evt, mut dispatch_data| {
+      let next_action = dispatch_data.get::<Option<WEvent>>().unwrap();
+      // Keep last event in priority order : Close > Configure > Refresh
+      let replace = matches!(
+        (&evt, &*next_action),
+        (_, &None)
+          | (_, &Some(WEvent::Refresh))
+          | (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
+          | (&WEvent::Close, _)
+      );
+      if replace {
+        *next_action = Some(evt);
+      }
+    })
+    .context("Failed to create a window !")?;
+
+  window.set_title("Espanso Sync Tool".to_string());
+
+  let mut pool = env
+    .create_auto_pool()
+    .context("Failed to create a memory pool !")?;
+
+  /*
+   * Keyboard initialization
+   */
+
+  let mut seats = Vec::<(
+    String,
+    Option<(wl_keyboard::WlKeyboard, calloop::RegistrationToken)>,
+  )>::new();
+
+  // first process already existing seats
+  for seat in env.get_all_seats() {
+    if let Some((has_kbd, name)) = sctk::seat::with_seat_data(&seat, |seat_data| {
+      (
+        seat_data.has_keyboard && !seat_data.defunct,
+        seat_data.name.clone(),
+      )
+    }) {
+      if has_kbd {
+        let result_clone = result.clone();
+        match map_keyboard_repeat(
+          event_loop.handle(),
+          &seat,
+          None,
+          RepeatKind::System,
+          move |event, _, _| keyboard_event_handler(event, &result_clone),
+        ) {
+          Ok((kbd, repeat_source)) => {
+            seats.push((name, Some((kbd, repeat_source))));
+          }
+          Err(e) => {
+            error!("Failed to map keyboard on seat {} : {:?}.", name, e);
+            seats.push((name, None));
+          }
+        }
+      } else {
+        seats.push((name, None));
+      }
+    }
+  }
+
+  // then setup a listener for changes
+  let loop_handle = event_loop.handle();
+  let result_clone = result.clone();
+  let _seat_listener = env.listen_for_seats(move |seat, seat_data, _| {
+    let result_clone = result_clone.clone();
+    // find the seat in the vec of seats, or insert it if it is unknown
+    let idx = seats.iter().position(|(name, _)| name == &seat_data.name);
+    let idx = idx.unwrap_or_else(|| {
+      seats.push((seat_data.name.clone(), None));
+      seats.len() - 1
+    });
+
+    let (_, ref mut opt_kbd) = &mut seats[idx];
+    // we should map a keyboard if the seat has the capability & is not defunct
+    if seat_data.has_keyboard && !seat_data.defunct {
+      if opt_kbd.is_none() {
+        // we should initalize a keyboard
+        match map_keyboard_repeat(
+          loop_handle.clone(),
+          &seat,
+          None,
+          RepeatKind::System,
+          move |event, _, _| keyboard_event_handler(event, &result_clone),
+        ) {
+          Ok((kbd, repeat_source)) => {
+            *opt_kbd = Some((kbd, repeat_source));
+          }
+          Err(e) => {
+            eprintln!(
+              "Failed to map keyboard on seat {} : {:?}.",
+              seat_data.name, e
+            )
+          }
+        }
+      }
+    } else if let Some((kbd, source)) = opt_kbd.take() {
+      // the keyboard has been removed, cleanup
+      kbd.release();
+      loop_handle.remove(source);
+    }
+  });
+
+  if !env.get_shell().unwrap().needs_configure() {
+    // initial draw to bootstrap on wl_shell
+    redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
+    window.refresh();
+  }
+
+  sctk::WaylandSource::new(queue)
+    .quick_insert(event_loop.handle())
+    .unwrap();
+
+  let mut next_action = None;
+
+  loop {
+    match next_action.take() {
+      Some(WEvent::Close) => break,
+      Some(WEvent::Refresh) => {
+        window.refresh();
+        window.surface().commit();
+      }
+      Some(WEvent::Configure {
+        new_size,
+        states: _,
+      }) => {
+        if let Some((w, h)) = new_size {
+          window.resize(w, h);
+          dimensions = (w, h)
+        }
+        window.refresh();
+        redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
+      }
+      None => {
+        let result_clone = result.clone();
+        let result_ref = result_clone.borrow();
+
+        if let Some(result) = &*result_ref {
+          return Ok(Some(*result));
+        }
+      }
+    }
+
+    // always flush the connection before going to sleep waiting for events
+    display.flush().unwrap();
+
+    event_loop
+      .dispatch(Some(std::time::Duration::from_millis(10)), &mut next_action)
+      .unwrap();
+  }
+
+  Ok(None)
+}
+
+fn keyboard_event_handler(
+  event: KbEvent,
+  result_clone: &Rc<RefCell<Option<super::ModifiersState>>>,
+) {
+  if let KbEvent::Modifiers { modifiers } = event {
+    let mut result_mut = (**result_clone).borrow_mut();
+    *result_mut = Some(super::ModifiersState {
+      ctrl: modifiers.ctrl,
+      alt: modifiers.alt,
+      shift: modifiers.shift,
+      caps_lock: modifiers.caps_lock,
+      meta: modifiers.logo,
+      num_lock: modifiers.num_lock,
+    })
+  }
+}
+
+#[allow(clippy::many_single_char_names)]
+fn redraw(
+  pool: &mut AutoMemPool,
+  surface: &wl_surface::WlSurface,
+  (buf_x, buf_y): (u32, u32),
+) -> Result<(), ::std::io::Error> {
+  let (canvas, new_buffer) = pool.buffer(
+    buf_x as i32,
+    buf_y as i32,
+    4 * buf_x as i32,
+    wl_shm::Format::Argb8888,
+  )?;
+  for (i, dst_pixel) in canvas.chunks_exact_mut(4).enumerate() {
+    let x = i as u32 % buf_x;
+    let y = i as u32 / buf_x;
+    let r: u32 = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
+    let g: u32 = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
+    let b: u32 = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y);
+    let pixel: [u8; 4] = ((0xFF << 24) + (r << 16) + (g << 8) + b).to_ne_bytes();
+    dst_pixel[0] = pixel[0];
+    dst_pixel[1] = pixel[1];
+    dst_pixel[2] = pixel[2];
+    dst_pixel[3] = pixel[3];
+  }
+  surface.attach(Some(&new_buffer), 0, 0);
+  if surface.as_ref().version() >= 4 {
+    surface.damage_buffer(0, 0, buf_x as i32, buf_y as i32);
+  } else {
+    surface.damage(0, 0, buf_x as i32, buf_y as i32);
+  }
+  surface.commit();
+  Ok(())
+}
diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs
new file mode 100644
index 00000000..6eae8ade
--- /dev/null
+++ b/espanso-detect/src/event.rs
@@ -0,0 +1,130 @@
+/*
+ * 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/>.
+ */
+#[cfg(test)]
+use enum_as_inner::EnumAsInner;
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(test, derive(EnumAsInner))]
+pub enum InputEvent {
+  Mouse(MouseEvent),
+  Keyboard(KeyboardEvent),
+  HotKey(HotKeyEvent),
+}
+
+#[derive(Debug, PartialEq)]
+pub enum MouseButton {
+  Left,
+  Right,
+  Middle,
+  Button1,
+  Button2,
+  Button3,
+  Button4,
+  Button5,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct MouseEvent {
+  pub button: MouseButton,
+  pub status: Status,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum Status {
+  Pressed,
+  Released,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum Variant {
+  Left,
+  Right,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct KeyboardEvent {
+  pub key: Key,
+  pub value: Option<String>,
+  pub status: Status,
+  pub variant: Option<Variant>,
+  pub code: u32,
+}
+
+// A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+#[derive(Debug, PartialEq)]
+pub enum Key {
+  // Modifiers
+  Alt,
+  CapsLock,
+  Control,
+  Meta,
+  NumLock,
+  Shift,
+
+  // Whitespace
+  Enter,
+  Tab,
+  Space,
+
+  // Navigation
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  ArrowUp,
+  End,
+  Home,
+  PageDown,
+  PageUp,
+
+  // UI
+  Escape,
+
+  // Editing keys
+  Backspace,
+
+  // Function keys
+  F1,
+  F2,
+  F3,
+  F4,
+  F5,
+  F6,
+  F7,
+  F8,
+  F9,
+  F10,
+  F11,
+  F12,
+  F13,
+  F14,
+  F15,
+  F16,
+  F17,
+  F18,
+  F19,
+  F20,
+
+  // Other keys, includes the raw code provided by the operating system
+  Other(i32),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct HotKeyEvent {
+  pub hotkey_id: i32,
+}
diff --git a/espanso-detect/src/hotkey/keys.rs b/espanso-detect/src/hotkey/keys.rs
new file mode 100644
index 00000000..fa1ccf75
--- /dev/null
+++ b/espanso-detect/src/hotkey/keys.rs
@@ -0,0 +1,628 @@
+/*
+ * 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::fmt::Display;
+
+use regex::Regex;
+
+lazy_static! {
+  static ref RAW_PARSER: Regex = Regex::new(r"^RAW\((\d+)\)$").unwrap();
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ShortcutKey {
+  Alt,
+  Control,
+  Meta,
+  Shift,
+
+  Enter,
+  Tab,
+  Space,
+  Insert,
+
+  // Navigation
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  ArrowUp,
+  End,
+  Home,
+  PageDown,
+  PageUp,
+
+  // Function ShortcutKeys
+  F1,
+  F2,
+  F3,
+  F4,
+  F5,
+  F6,
+  F7,
+  F8,
+  F9,
+  F10,
+  F11,
+  F12,
+  F13,
+  F14,
+  F15,
+  F16,
+  F17,
+  F18,
+  F19,
+  F20,
+
+  // Alphabet
+  A,
+  B,
+  C,
+  D,
+  E,
+  F,
+  G,
+  H,
+  I,
+  J,
+  K,
+  L,
+  M,
+  N,
+  O,
+  P,
+  Q,
+  R,
+  S,
+  T,
+  U,
+  V,
+  W,
+  X,
+  Y,
+  Z,
+
+  // Numbers
+  N0,
+  N1,
+  N2,
+  N3,
+  N4,
+  N5,
+  N6,
+  N7,
+  N8,
+  N9,
+
+  // Numpad
+  Numpad0,
+  Numpad1,
+  Numpad2,
+  Numpad3,
+  Numpad4,
+  Numpad5,
+  Numpad6,
+  Numpad7,
+  Numpad8,
+  Numpad9,
+
+  // Specify the raw platform-specific virtual key code.
+  Raw(u32),
+}
+
+impl Display for ShortcutKey {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match *self {
+      ShortcutKey::Alt => write!(f, "ALT"),
+      ShortcutKey::Control => write!(f, "CTRL"),
+      ShortcutKey::Meta => write!(f, "META"),
+      ShortcutKey::Shift => write!(f, "SHIFT"),
+      ShortcutKey::Enter => write!(f, "ENTER"),
+      ShortcutKey::Tab => write!(f, "TAB"),
+      ShortcutKey::Space => write!(f, "SPACE"),
+      ShortcutKey::Insert => write!(f, "INSERT"),
+      ShortcutKey::ArrowDown => write!(f, "DOWN"),
+      ShortcutKey::ArrowLeft => write!(f, "LEFT"),
+      ShortcutKey::ArrowRight => write!(f, "RIGHT"),
+      ShortcutKey::ArrowUp => write!(f, "UP"),
+      ShortcutKey::End => write!(f, "END"),
+      ShortcutKey::Home => write!(f, "HOME"),
+      ShortcutKey::PageDown => write!(f, "PAGEDOWN"),
+      ShortcutKey::PageUp => write!(f, "PAGEUP"),
+      ShortcutKey::F1 => write!(f, "F1"),
+      ShortcutKey::F2 => write!(f, "F2"),
+      ShortcutKey::F3 => write!(f, "F3"),
+      ShortcutKey::F4 => write!(f, "F4"),
+      ShortcutKey::F5 => write!(f, "F5"),
+      ShortcutKey::F6 => write!(f, "F6"),
+      ShortcutKey::F7 => write!(f, "F7"),
+      ShortcutKey::F8 => write!(f, "F8"),
+      ShortcutKey::F9 => write!(f, "F9"),
+      ShortcutKey::F10 => write!(f, "F10"),
+      ShortcutKey::F11 => write!(f, "F11"),
+      ShortcutKey::F12 => write!(f, "F12"),
+      ShortcutKey::F13 => write!(f, "F13"),
+      ShortcutKey::F14 => write!(f, "F14"),
+      ShortcutKey::F15 => write!(f, "F15"),
+      ShortcutKey::F16 => write!(f, "F16"),
+      ShortcutKey::F17 => write!(f, "F17"),
+      ShortcutKey::F18 => write!(f, "F18"),
+      ShortcutKey::F19 => write!(f, "F19"),
+      ShortcutKey::F20 => write!(f, "F20"),
+      ShortcutKey::A => write!(f, "A"),
+      ShortcutKey::B => write!(f, "B"),
+      ShortcutKey::C => write!(f, "C"),
+      ShortcutKey::D => write!(f, "D"),
+      ShortcutKey::E => write!(f, "E"),
+      ShortcutKey::F => write!(f, "F"),
+      ShortcutKey::G => write!(f, "G"),
+      ShortcutKey::H => write!(f, "H"),
+      ShortcutKey::I => write!(f, "I"),
+      ShortcutKey::J => write!(f, "J"),
+      ShortcutKey::K => write!(f, "K"),
+      ShortcutKey::L => write!(f, "L"),
+      ShortcutKey::M => write!(f, "M"),
+      ShortcutKey::N => write!(f, "N"),
+      ShortcutKey::O => write!(f, "O"),
+      ShortcutKey::P => write!(f, "P"),
+      ShortcutKey::Q => write!(f, "Q"),
+      ShortcutKey::R => write!(f, "R"),
+      ShortcutKey::S => write!(f, "S"),
+      ShortcutKey::T => write!(f, "T"),
+      ShortcutKey::U => write!(f, "U"),
+      ShortcutKey::V => write!(f, "V"),
+      ShortcutKey::W => write!(f, "W"),
+      ShortcutKey::X => write!(f, "X"),
+      ShortcutKey::Y => write!(f, "Y"),
+      ShortcutKey::Z => write!(f, "Z"),
+      ShortcutKey::N0 => write!(f, "0"),
+      ShortcutKey::N1 => write!(f, "1"),
+      ShortcutKey::N2 => write!(f, "2"),
+      ShortcutKey::N3 => write!(f, "3"),
+      ShortcutKey::N4 => write!(f, "4"),
+      ShortcutKey::N5 => write!(f, "5"),
+      ShortcutKey::N6 => write!(f, "6"),
+      ShortcutKey::N7 => write!(f, "7"),
+      ShortcutKey::N8 => write!(f, "8"),
+      ShortcutKey::N9 => write!(f, "9"),
+      ShortcutKey::Numpad0 => write!(f, "NUMPAD0"),
+      ShortcutKey::Numpad1 => write!(f, "NUMPAD1"),
+      ShortcutKey::Numpad2 => write!(f, "NUMPAD2"),
+      ShortcutKey::Numpad3 => write!(f, "NUMPAD3"),
+      ShortcutKey::Numpad4 => write!(f, "NUMPAD4"),
+      ShortcutKey::Numpad5 => write!(f, "NUMPAD5"),
+      ShortcutKey::Numpad6 => write!(f, "NUMPAD6"),
+      ShortcutKey::Numpad7 => write!(f, "NUMPAD7"),
+      ShortcutKey::Numpad8 => write!(f, "NUMPAD8"),
+      ShortcutKey::Numpad9 => write!(f, "NUMPAD9"),
+      ShortcutKey::Raw(code) => write!(f, "RAW({})", code),
+    }
+  }
+}
+
+impl ShortcutKey {
+  pub fn parse(key: &str) -> Option<ShortcutKey> {
+    let parsed = match key {
+      "ALT" | "OPTION" => Some(ShortcutKey::Alt),
+      "CTRL" => Some(ShortcutKey::Control),
+      "META" | "CMD" => Some(ShortcutKey::Meta),
+      "SHIFT" => Some(ShortcutKey::Shift),
+      "ENTER" => Some(ShortcutKey::Enter),
+      "TAB" => Some(ShortcutKey::Tab),
+      "SPACE" => Some(ShortcutKey::Space),
+      "INSERT" => Some(ShortcutKey::Insert),
+      "DOWN" => Some(ShortcutKey::ArrowDown),
+      "LEFT" => Some(ShortcutKey::ArrowLeft),
+      "RIGHT" => Some(ShortcutKey::ArrowRight),
+      "UP" => Some(ShortcutKey::ArrowUp),
+      "END" => Some(ShortcutKey::End),
+      "HOME" => Some(ShortcutKey::Home),
+      "PAGEDOWN" => Some(ShortcutKey::PageDown),
+      "PAGEUP" => Some(ShortcutKey::PageUp),
+      "F1" => Some(ShortcutKey::F1),
+      "F2" => Some(ShortcutKey::F2),
+      "F3" => Some(ShortcutKey::F3),
+      "F4" => Some(ShortcutKey::F4),
+      "F5" => Some(ShortcutKey::F5),
+      "F6" => Some(ShortcutKey::F6),
+      "F7" => Some(ShortcutKey::F7),
+      "F8" => Some(ShortcutKey::F8),
+      "F9" => Some(ShortcutKey::F9),
+      "F10" => Some(ShortcutKey::F10),
+      "F11" => Some(ShortcutKey::F11),
+      "F12" => Some(ShortcutKey::F12),
+      "F13" => Some(ShortcutKey::F13),
+      "F14" => Some(ShortcutKey::F14),
+      "F15" => Some(ShortcutKey::F15),
+      "F16" => Some(ShortcutKey::F16),
+      "F17" => Some(ShortcutKey::F17),
+      "F18" => Some(ShortcutKey::F18),
+      "F19" => Some(ShortcutKey::F19),
+      "F20" => Some(ShortcutKey::F20),
+      "A" => Some(ShortcutKey::A),
+      "B" => Some(ShortcutKey::B),
+      "C" => Some(ShortcutKey::C),
+      "D" => Some(ShortcutKey::D),
+      "E" => Some(ShortcutKey::E),
+      "F" => Some(ShortcutKey::F),
+      "G" => Some(ShortcutKey::G),
+      "H" => Some(ShortcutKey::H),
+      "I" => Some(ShortcutKey::I),
+      "J" => Some(ShortcutKey::J),
+      "K" => Some(ShortcutKey::K),
+      "L" => Some(ShortcutKey::L),
+      "M" => Some(ShortcutKey::M),
+      "N" => Some(ShortcutKey::N),
+      "O" => Some(ShortcutKey::O),
+      "P" => Some(ShortcutKey::P),
+      "Q" => Some(ShortcutKey::Q),
+      "R" => Some(ShortcutKey::R),
+      "S" => Some(ShortcutKey::S),
+      "T" => Some(ShortcutKey::T),
+      "U" => Some(ShortcutKey::U),
+      "V" => Some(ShortcutKey::V),
+      "W" => Some(ShortcutKey::W),
+      "X" => Some(ShortcutKey::X),
+      "Y" => Some(ShortcutKey::Y),
+      "Z" => Some(ShortcutKey::Z),
+      "0" => Some(ShortcutKey::N0),
+      "1" => Some(ShortcutKey::N1),
+      "2" => Some(ShortcutKey::N2),
+      "3" => Some(ShortcutKey::N3),
+      "4" => Some(ShortcutKey::N4),
+      "5" => Some(ShortcutKey::N5),
+      "6" => Some(ShortcutKey::N6),
+      "7" => Some(ShortcutKey::N7),
+      "8" => Some(ShortcutKey::N8),
+      "9" => Some(ShortcutKey::N9),
+      "NUMPAD0" => Some(ShortcutKey::Numpad0),
+      "NUMPAD1" => Some(ShortcutKey::Numpad1),
+      "NUMPAD2" => Some(ShortcutKey::Numpad2),
+      "NUMPAD3" => Some(ShortcutKey::Numpad3),
+      "NUMPAD4" => Some(ShortcutKey::Numpad4),
+      "NUMPAD5" => Some(ShortcutKey::Numpad5),
+      "NUMPAD6" => Some(ShortcutKey::Numpad6),
+      "NUMPAD7" => Some(ShortcutKey::Numpad7),
+      "NUMPAD8" => Some(ShortcutKey::Numpad8),
+      "NUMPAD9" => Some(ShortcutKey::Numpad9),
+      _ => None,
+    };
+
+    if parsed.is_none() {
+      // Attempt to parse raw ShortcutKeys
+      if RAW_PARSER.is_match(key) {
+        if let Some(caps) = RAW_PARSER.captures(key) {
+          let code_str = caps.get(1).map_or("", |m| m.as_str());
+          let code = code_str.parse::<u32>();
+          if let Ok(code) = code {
+            return Some(ShortcutKey::Raw(code));
+          }
+        }
+      }
+    }
+
+    parsed
+  }
+
+  // macOS keycodes
+
+  #[cfg(target_os = "macos")]
+  pub fn to_code(&self) -> Option<u32> {
+    match self {
+      ShortcutKey::Alt => Some(0x3A),
+      ShortcutKey::Control => Some(0x3B),
+      ShortcutKey::Meta => Some(0x37),
+      ShortcutKey::Shift => Some(0x38),
+      ShortcutKey::Enter => Some(0x24),
+      ShortcutKey::Tab => Some(0x30),
+      ShortcutKey::Space => Some(0x31),
+      ShortcutKey::ArrowDown => Some(0x7D),
+      ShortcutKey::ArrowLeft => Some(0x7B),
+      ShortcutKey::ArrowRight => Some(0x7C),
+      ShortcutKey::ArrowUp => Some(0x7E),
+      ShortcutKey::End => Some(0x77),
+      ShortcutKey::Home => Some(0x73),
+      ShortcutKey::PageDown => Some(0x79),
+      ShortcutKey::PageUp => Some(0x74),
+      ShortcutKey::Insert => None,
+      ShortcutKey::F1 => Some(0x7A),
+      ShortcutKey::F2 => Some(0x78),
+      ShortcutKey::F3 => Some(0x63),
+      ShortcutKey::F4 => Some(0x76),
+      ShortcutKey::F5 => Some(0x60),
+      ShortcutKey::F6 => Some(0x61),
+      ShortcutKey::F7 => Some(0x62),
+      ShortcutKey::F8 => Some(0x64),
+      ShortcutKey::F9 => Some(0x65),
+      ShortcutKey::F10 => Some(0x6D),
+      ShortcutKey::F11 => Some(0x67),
+      ShortcutKey::F12 => Some(0x6F),
+      ShortcutKey::F13 => Some(0x69),
+      ShortcutKey::F14 => Some(0x6B),
+      ShortcutKey::F15 => Some(0x71),
+      ShortcutKey::F16 => Some(0x6A),
+      ShortcutKey::F17 => Some(0x40),
+      ShortcutKey::F18 => Some(0x4F),
+      ShortcutKey::F19 => Some(0x50),
+      ShortcutKey::F20 => Some(0x5A),
+      ShortcutKey::A => Some(0x00),
+      ShortcutKey::B => Some(0x0B),
+      ShortcutKey::C => Some(0x08),
+      ShortcutKey::D => Some(0x02),
+      ShortcutKey::E => Some(0x0E),
+      ShortcutKey::F => Some(0x03),
+      ShortcutKey::G => Some(0x05),
+      ShortcutKey::H => Some(0x04),
+      ShortcutKey::I => Some(0x22),
+      ShortcutKey::J => Some(0x26),
+      ShortcutKey::K => Some(0x28),
+      ShortcutKey::L => Some(0x25),
+      ShortcutKey::M => Some(0x2E),
+      ShortcutKey::N => Some(0x2D),
+      ShortcutKey::O => Some(0x1F),
+      ShortcutKey::P => Some(0x23),
+      ShortcutKey::Q => Some(0x0C),
+      ShortcutKey::R => Some(0x0F),
+      ShortcutKey::S => Some(0x01),
+      ShortcutKey::T => Some(0x11),
+      ShortcutKey::U => Some(0x20),
+      ShortcutKey::V => Some(0x09),
+      ShortcutKey::W => Some(0x0D),
+      ShortcutKey::X => Some(0x07),
+      ShortcutKey::Y => Some(0x10),
+      ShortcutKey::Z => Some(0x06),
+      ShortcutKey::N0 => Some(0x1D),
+      ShortcutKey::N1 => Some(0x12),
+      ShortcutKey::N2 => Some(0x13),
+      ShortcutKey::N3 => Some(0x14),
+      ShortcutKey::N4 => Some(0x15),
+      ShortcutKey::N5 => Some(0x17),
+      ShortcutKey::N6 => Some(0x16),
+      ShortcutKey::N7 => Some(0x1A),
+      ShortcutKey::N8 => Some(0x1C),
+      ShortcutKey::N9 => Some(0x19),
+      ShortcutKey::Numpad0 => Some(0x52),
+      ShortcutKey::Numpad1 => Some(0x53),
+      ShortcutKey::Numpad2 => Some(0x54),
+      ShortcutKey::Numpad3 => Some(0x55),
+      ShortcutKey::Numpad4 => Some(0x56),
+      ShortcutKey::Numpad5 => Some(0x57),
+      ShortcutKey::Numpad6 => Some(0x58),
+      ShortcutKey::Numpad7 => Some(0x59),
+      ShortcutKey::Numpad8 => Some(0x5B),
+      ShortcutKey::Numpad9 => Some(0x5C),
+      ShortcutKey::Raw(code) => Some(*code),
+    }
+  }
+
+  // Windows key codes
+
+  #[cfg(target_os = "windows")]
+  pub fn to_code(&self) -> Option<u32> {
+    let vkey = match self {
+      ShortcutKey::Alt => 0x12,
+      ShortcutKey::Control => 0x11,
+      ShortcutKey::Meta => 0x5B,
+      ShortcutKey::Shift => 0xA0,
+      ShortcutKey::Enter => 0x0D,
+      ShortcutKey::Tab => 0x09,
+      ShortcutKey::Space => 0x20,
+      ShortcutKey::ArrowDown => 0x28,
+      ShortcutKey::ArrowLeft => 0x25,
+      ShortcutKey::ArrowRight => 0x27,
+      ShortcutKey::ArrowUp => 0x26,
+      ShortcutKey::End => 0x23,
+      ShortcutKey::Home => 0x24,
+      ShortcutKey::PageDown => 0x22,
+      ShortcutKey::PageUp => 0x21,
+      ShortcutKey::Insert => 0x2D,
+      ShortcutKey::F1 => 0x70,
+      ShortcutKey::F2 => 0x71,
+      ShortcutKey::F3 => 0x72,
+      ShortcutKey::F4 => 0x73,
+      ShortcutKey::F5 => 0x74,
+      ShortcutKey::F6 => 0x75,
+      ShortcutKey::F7 => 0x76,
+      ShortcutKey::F8 => 0x77,
+      ShortcutKey::F9 => 0x78,
+      ShortcutKey::F10 => 0x79,
+      ShortcutKey::F11 => 0x7A,
+      ShortcutKey::F12 => 0x7B,
+      ShortcutKey::F13 => 0x7C,
+      ShortcutKey::F14 => 0x7D,
+      ShortcutKey::F15 => 0x7E,
+      ShortcutKey::F16 => 0x7F,
+      ShortcutKey::F17 => 0x80,
+      ShortcutKey::F18 => 0x81,
+      ShortcutKey::F19 => 0x82,
+      ShortcutKey::F20 => 0x83,
+      ShortcutKey::A => 0x41,
+      ShortcutKey::B => 0x42,
+      ShortcutKey::C => 0x43,
+      ShortcutKey::D => 0x44,
+      ShortcutKey::E => 0x45,
+      ShortcutKey::F => 0x46,
+      ShortcutKey::G => 0x47,
+      ShortcutKey::H => 0x48,
+      ShortcutKey::I => 0x49,
+      ShortcutKey::J => 0x4A,
+      ShortcutKey::K => 0x4B,
+      ShortcutKey::L => 0x4C,
+      ShortcutKey::M => 0x4D,
+      ShortcutKey::N => 0x4E,
+      ShortcutKey::O => 0x4F,
+      ShortcutKey::P => 0x50,
+      ShortcutKey::Q => 0x51,
+      ShortcutKey::R => 0x52,
+      ShortcutKey::S => 0x53,
+      ShortcutKey::T => 0x54,
+      ShortcutKey::U => 0x55,
+      ShortcutKey::V => 0x56,
+      ShortcutKey::W => 0x57,
+      ShortcutKey::X => 0x58,
+      ShortcutKey::Y => 0x59,
+      ShortcutKey::Z => 0x5A,
+      ShortcutKey::N0 => 0x30,
+      ShortcutKey::N1 => 0x31,
+      ShortcutKey::N2 => 0x32,
+      ShortcutKey::N3 => 0x33,
+      ShortcutKey::N4 => 0x34,
+      ShortcutKey::N5 => 0x35,
+      ShortcutKey::N6 => 0x36,
+      ShortcutKey::N7 => 0x37,
+      ShortcutKey::N8 => 0x38,
+      ShortcutKey::N9 => 0x39,
+      ShortcutKey::Numpad0 => 0x60,
+      ShortcutKey::Numpad1 => 0x61,
+      ShortcutKey::Numpad2 => 0x62,
+      ShortcutKey::Numpad3 => 0x63,
+      ShortcutKey::Numpad4 => 0x64,
+      ShortcutKey::Numpad5 => 0x65,
+      ShortcutKey::Numpad6 => 0x66,
+      ShortcutKey::Numpad7 => 0x67,
+      ShortcutKey::Numpad8 => 0x68,
+      ShortcutKey::Numpad9 => 0x69,
+      ShortcutKey::Raw(code) => *code,
+    };
+    Some(vkey)
+  }
+
+  // Linux mappings
+  // NOTE: on linux, this method returns the KeySym and not the KeyCode
+  //       which should be obtained in other ways depending on the backend.
+  //       (X11 or Wayland)
+  #[cfg(target_os = "linux")]
+  pub fn to_code(&self) -> Option<u32> {
+    match self {
+      ShortcutKey::Alt => Some(0xFFE9),
+      ShortcutKey::Control => Some(0xFFE3),
+      ShortcutKey::Meta => Some(0xFFEB),
+      ShortcutKey::Shift => Some(0xFFE1),
+      ShortcutKey::Enter => Some(0xFF0D),
+      ShortcutKey::Tab => Some(0xFF09),
+      ShortcutKey::Space => Some(0x20),
+      ShortcutKey::ArrowDown => Some(0xFF54),
+      ShortcutKey::ArrowLeft => Some(0xFF51),
+      ShortcutKey::ArrowRight => Some(0xFF53),
+      ShortcutKey::ArrowUp => Some(0xFF52),
+      ShortcutKey::End => Some(0xFF57),
+      ShortcutKey::Home => Some(0xFF50),
+      ShortcutKey::PageDown => Some(0xFF56),
+      ShortcutKey::PageUp => Some(0xFF55),
+      ShortcutKey::Insert => Some(0xff63),
+      ShortcutKey::F1 => Some(0xFFBE),
+      ShortcutKey::F2 => Some(0xFFBF),
+      ShortcutKey::F3 => Some(0xFFC0),
+      ShortcutKey::F4 => Some(0xFFC1),
+      ShortcutKey::F5 => Some(0xFFC2),
+      ShortcutKey::F6 => Some(0xFFC3),
+      ShortcutKey::F7 => Some(0xFFC4),
+      ShortcutKey::F8 => Some(0xFFC5),
+      ShortcutKey::F9 => Some(0xFFC6),
+      ShortcutKey::F10 => Some(0xFFC7),
+      ShortcutKey::F11 => Some(0xFFC8),
+      ShortcutKey::F12 => Some(0xFFC9),
+      ShortcutKey::F13 => Some(0xFFCA),
+      ShortcutKey::F14 => Some(0xFFCB),
+      ShortcutKey::F15 => Some(0xFFCC),
+      ShortcutKey::F16 => Some(0xFFCD),
+      ShortcutKey::F17 => Some(0xFFCE),
+      ShortcutKey::F18 => Some(0xFFCF),
+      ShortcutKey::F19 => Some(0xFFD0),
+      ShortcutKey::F20 => Some(0xFFD1),
+      ShortcutKey::A => Some(0x0061),
+      ShortcutKey::B => Some(0x0062),
+      ShortcutKey::C => Some(0x0063),
+      ShortcutKey::D => Some(0x0064),
+      ShortcutKey::E => Some(0x0065),
+      ShortcutKey::F => Some(0x0066),
+      ShortcutKey::G => Some(0x0067),
+      ShortcutKey::H => Some(0x0068),
+      ShortcutKey::I => Some(0x0069),
+      ShortcutKey::J => Some(0x006a),
+      ShortcutKey::K => Some(0x006b),
+      ShortcutKey::L => Some(0x006c),
+      ShortcutKey::M => Some(0x006d),
+      ShortcutKey::N => Some(0x006e),
+      ShortcutKey::O => Some(0x006f),
+      ShortcutKey::P => Some(0x0070),
+      ShortcutKey::Q => Some(0x0071),
+      ShortcutKey::R => Some(0x0072),
+      ShortcutKey::S => Some(0x0073),
+      ShortcutKey::T => Some(0x0074),
+      ShortcutKey::U => Some(0x0075),
+      ShortcutKey::V => Some(0x0076),
+      ShortcutKey::W => Some(0x0077),
+      ShortcutKey::X => Some(0x0078),
+      ShortcutKey::Y => Some(0x0079),
+      ShortcutKey::Z => Some(0x007a),
+      ShortcutKey::N0 => Some(0x0030),
+      ShortcutKey::N1 => Some(0x0031),
+      ShortcutKey::N2 => Some(0x0032),
+      ShortcutKey::N3 => Some(0x0033),
+      ShortcutKey::N4 => Some(0x0034),
+      ShortcutKey::N5 => Some(0x0035),
+      ShortcutKey::N6 => Some(0x0036),
+      ShortcutKey::N7 => Some(0x0037),
+      ShortcutKey::N8 => Some(0x0038),
+      ShortcutKey::N9 => Some(0x0039),
+      ShortcutKey::Numpad0 => Some(0xffb0),
+      ShortcutKey::Numpad1 => Some(0xffb1),
+      ShortcutKey::Numpad2 => Some(0xffb2),
+      ShortcutKey::Numpad3 => Some(0xffb3),
+      ShortcutKey::Numpad4 => Some(0xffb4),
+      ShortcutKey::Numpad5 => Some(0xffb5),
+      ShortcutKey::Numpad6 => Some(0xffb6),
+      ShortcutKey::Numpad7 => Some(0xffb7),
+      ShortcutKey::Numpad8 => Some(0xffb8),
+      ShortcutKey::Numpad9 => Some(0xffb9),
+      ShortcutKey::Raw(code) => Some(*code as u32),
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn parse_works_correctly() {
+    assert!(matches!(
+      ShortcutKey::parse("ALT").unwrap(),
+      ShortcutKey::Alt
+    ));
+    assert!(matches!(
+      ShortcutKey::parse("META").unwrap(),
+      ShortcutKey::Meta
+    ));
+    assert!(matches!(
+      ShortcutKey::parse("CMD").unwrap(),
+      ShortcutKey::Meta
+    ));
+    assert!(matches!(
+      ShortcutKey::parse("RAW(1234)").unwrap(),
+      ShortcutKey::Raw(1234)
+    ));
+  }
+
+  #[test]
+  fn parse_invalid_keys() {
+    assert!(ShortcutKey::parse("INVALID").is_none());
+    assert!(ShortcutKey::parse("RAW(a)").is_none());
+  }
+}
diff --git a/espanso-detect/src/hotkey/mod.rs b/espanso-detect/src/hotkey/mod.rs
new file mode 100644
index 00000000..d08440bc
--- /dev/null
+++ b/espanso-detect/src/hotkey/mod.rs
@@ -0,0 +1,150 @@
+/*
+ * 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/>.
+ */
+
+pub mod keys;
+
+use std::fmt::Display;
+
+use anyhow::Result;
+use keys::ShortcutKey;
+use thiserror::Error;
+
+static MODIFIERS: &[ShortcutKey; 4] = &[
+  ShortcutKey::Control,
+  ShortcutKey::Alt,
+  ShortcutKey::Shift,
+  ShortcutKey::Meta,
+];
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct HotKey {
+  pub id: i32,
+  pub key: ShortcutKey,
+  pub modifiers: Vec<ShortcutKey>,
+}
+
+impl HotKey {
+  pub fn new(id: i32, shortcut: &str) -> Result<Self> {
+    let tokens: Vec<String> = shortcut
+      .split('+')
+      .map(|token| token.trim().to_uppercase())
+      .collect();
+
+    let mut modifiers = Vec::new();
+    let mut main_key = None;
+    for token in tokens {
+      let key = ShortcutKey::parse(&token);
+      match key {
+        Some(key) => {
+          if MODIFIERS.contains(&key) {
+            modifiers.push(key)
+          } else {
+            main_key = Some(key)
+          }
+        }
+        None => return Err(HotKeyError::InvalidKey(token).into()),
+      };
+    }
+
+    if modifiers.is_empty() || main_key.is_none() {
+      return Err(HotKeyError::InvalidShortcut(shortcut.to_string()).into());
+    }
+
+    Ok(Self {
+      id,
+      modifiers,
+      key: main_key.unwrap(),
+    })
+  }
+
+  #[allow(dead_code)]
+  pub(crate) fn has_ctrl(&self) -> bool {
+    self.modifiers.contains(&ShortcutKey::Control)
+  }
+
+  #[allow(dead_code)]
+  pub(crate) fn has_meta(&self) -> bool {
+    self.modifiers.contains(&ShortcutKey::Meta)
+  }
+
+  #[allow(dead_code)]
+  pub(crate) fn has_alt(&self) -> bool {
+    self.modifiers.contains(&ShortcutKey::Alt)
+  }
+
+  #[allow(dead_code)]
+  pub(crate) fn has_shift(&self) -> bool {
+    self.modifiers.contains(&ShortcutKey::Shift)
+  }
+}
+
+impl Display for HotKey {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    let str_modifiers: Vec<String> = self.modifiers.iter().map(|m| m.to_string()).collect();
+    let modifiers = str_modifiers.join("+");
+    write!(f, "{}+{}", &modifiers, &self.key)
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum HotKeyError {
+  #[error("invalid hotkey shortcut, `{0}` is not a valid key")]
+  InvalidKey(String),
+
+  #[error("invalid hotkey shortcut `{0}`")]
+  InvalidShortcut(String),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn parse_correctly() {
+    assert_eq!(
+      HotKey::new(1, "CTRL+V").unwrap(),
+      HotKey {
+        id: 1,
+        key: ShortcutKey::V,
+        modifiers: vec![ShortcutKey::Control],
+      }
+    );
+    assert_eq!(
+      HotKey::new(2, "SHIFT + Ctrl + v").unwrap(),
+      HotKey {
+        id: 2,
+        key: ShortcutKey::V,
+        modifiers: vec![ShortcutKey::Shift, ShortcutKey::Control],
+      }
+    );
+    assert!(HotKey::new(3, "invalid").is_err());
+  }
+
+  #[test]
+  fn modifiers_detected_correcty() {
+    assert!(HotKey::new(1, "CTRL+V").unwrap().has_ctrl());
+    assert!(HotKey::new(1, "ALT + V").unwrap().has_alt());
+    assert!(HotKey::new(1, "CMD + V").unwrap().has_meta());
+    assert!(HotKey::new(1, "SHIFT+ V").unwrap().has_shift());
+
+    assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_ctrl());
+    assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_alt());
+    assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_meta());
+  }
+}
diff --git a/espanso-detect/src/layout/gnome.rs b/espanso-detect/src/layout/gnome.rs
new file mode 100644
index 00000000..24252581
--- /dev/null
+++ b/espanso-detect/src/layout/gnome.rs
@@ -0,0 +1,56 @@
+/*
+ * 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::process::Command;
+
+use log::error;
+use regex::Regex;
+use std::path::PathBuf;
+
+lazy_static! {
+  static ref LAYOUT_EXTRACT_REGEX: Regex = Regex::new(r"^\[\('.*?', '(.*?)'\)").unwrap();
+}
+
+pub fn get_active_layout() -> Option<String> {
+  match Command::new("gsettings")
+    .arg("get")
+    .arg("org.gnome.desktop.input-sources")
+    .arg("mru-sources")
+    .output()
+  {
+    Ok(output) => {
+      let output_str = String::from_utf8_lossy(&output.stdout);
+      let captures = LAYOUT_EXTRACT_REGEX.captures(&output_str)?;
+      let layout = captures.get(1)?.as_str();
+      Some(layout.to_string())
+    }
+    Err(err) => {
+      error!(
+        "unable to retrieve current keyboard layout with 'gsettings': {}",
+        err
+      );
+      None
+    }
+  }
+}
+
+pub fn is_gnome() -> bool {
+  let target_session_file = PathBuf::from("/usr/bin/gnome-session");
+  target_session_file.exists()
+}
diff --git a/espanso-detect/src/layout/mod.rs b/espanso-detect/src/layout/mod.rs
new file mode 100644
index 00000000..02155296
--- /dev/null
+++ b/espanso-detect/src/layout/mod.rs
@@ -0,0 +1,49 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+mod x11;
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+mod gnome;
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub fn get_active_layout() -> Option<String> {
+  x11::get_active_layout()
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+pub fn get_active_layout() -> Option<String> {
+  if gnome::is_gnome() {
+    gnome::get_active_layout()
+  } else {
+    log::warn!("unable to determine the currently active layout, you might need to explicitly specify the layout in the config for espanso to work correctly.");
+    None
+  }
+}
+
+#[cfg(not(target_os = "linux"))]
+pub fn get_active_layout() -> Option<String> {
+  // Not available on Windows and macOS yet
+  None
+}
diff --git a/espanso-detect/src/layout/x11.rs b/espanso-detect/src/layout/x11.rs
new file mode 100644
index 00000000..c60d6495
--- /dev/null
+++ b/espanso-detect/src/layout/x11.rs
@@ -0,0 +1,40 @@
+/*
+ * 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::process::Command;
+
+use log::error;
+
+pub fn get_active_layout() -> Option<String> {
+  match Command::new("setxkbmap").arg("-query").output() {
+    Ok(output) => {
+      let output_str = String::from_utf8_lossy(&output.stdout);
+      let layout_line = output_str.lines().find(|line| line.contains("layout:"))?;
+      let layout_raw: Vec<&str> = layout_line.split("layout:").collect();
+      Some(layout_raw.get(1)?.trim().to_string())
+    }
+    Err(err) => {
+      error!(
+        "unable to retrieve current keyboard layout with 'setxkbmap': {}",
+        err
+      );
+      None
+    }
+  }
+}
diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs
new file mode 100644
index 00000000..393b40ec
--- /dev/null
+++ b/espanso-detect/src/lib.rs
@@ -0,0 +1,128 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use hotkey::HotKey;
+use log::info;
+
+pub mod event;
+pub mod hotkey;
+pub mod layout;
+
+#[cfg(target_os = "windows")]
+pub mod win32;
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub mod x11;
+
+#[cfg(target_os = "linux")]
+pub mod evdev;
+
+#[cfg(target_os = "macos")]
+pub mod mac;
+
+#[macro_use]
+extern crate lazy_static;
+
+pub type SourceCallback = Box<dyn Fn(event::InputEvent)>;
+
+pub trait Source {
+  fn initialize(&mut self) -> Result<()>;
+  fn eventloop(&self, event_callback: SourceCallback) -> Result<()>;
+}
+
+#[allow(dead_code)]
+pub struct SourceCreationOptions {
+  // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
+  pub use_evdev: bool,
+
+  // Can be used to overwrite the keymap configuration
+  // used by espanso to inject key presses.
+  pub evdev_keyboard_rmlvo: Option<KeyboardConfig>,
+
+  // List of global hotkeys the detection module has to register
+  // NOTE: Hotkeys don't work under the EVDEV backend yet (Wayland)
+  pub hotkeys: Vec<HotKey>,
+
+  // 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.
+  // Disabling this option might conflict with the undo feature.
+  pub win32_exclude_orphan_events: bool,
+}
+
+// This struct identifies the keyboard layout that
+// should be used by EVDEV when loading the keymap.
+// For more information: https://xkbcommon.org/doc/current/structxkb__rule__names.html
+#[derive(Debug, Clone)]
+pub struct KeyboardConfig {
+  pub rules: Option<String>,
+  pub model: Option<String>,
+  pub layout: Option<String>,
+  pub variant: Option<String>,
+  pub options: Option<String>,
+}
+
+impl Default for SourceCreationOptions {
+  fn default() -> Self {
+    Self {
+      use_evdev: false,
+      evdev_keyboard_rmlvo: None,
+      hotkeys: Vec::new(),
+      win32_exclude_orphan_events: true,
+    }
+  }
+}
+
+#[cfg(target_os = "windows")]
+pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
+  info!("using Win32Source");
+  Ok(Box::new(win32::Win32Source::new(
+    &options.hotkeys,
+    options.win32_exclude_orphan_events,
+  )))
+}
+
+#[cfg(target_os = "macos")]
+pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
+  info!("using CocoaSource");
+  Ok(Box::new(mac::CocoaSource::new(&options.hotkeys)))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
+  if options.use_evdev {
+    info!("using EVDEVSource");
+    Ok(Box::new(evdev::EVDEVSource::new(options)))
+  } else {
+    info!("using X11Source");
+    Ok(Box::new(x11::X11Source::new(&options.hotkeys)))
+  }
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
+  info!("using EVDEVSource");
+  Ok(Box::new(evdev::EVDEVSource::new(options)))
+}
+
+pub use layout::get_active_layout;
diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs
new file mode 100644
index 00000000..d9bc5b0f
--- /dev/null
+++ b/espanso-detect/src/mac/mod.rs
@@ -0,0 +1,426 @@
+/*
+ * 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::{
+  convert::TryInto,
+  ffi::CStr,
+  sync::{
+    mpsc::{channel, Receiver, Sender},
+    Arc, Mutex,
+  },
+};
+
+use lazycell::LazyCell;
+use log::{error, trace, warn};
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Variant};
+use crate::event::{Key::*, MouseButton, MouseEvent};
+use crate::{event::Status::*, Source, SourceCallback};
+use crate::{event::Variant::*, hotkey::HotKey};
+
+const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1;
+const INPUT_EVENT_TYPE_MOUSE: i32 = 2;
+const INPUT_EVENT_TYPE_HOTKEY: i32 = 3;
+
+const INPUT_STATUS_PRESSED: i32 = 1;
+const INPUT_STATUS_RELEASED: i32 = 2;
+
+const INPUT_MOUSE_LEFT_BUTTON: i32 = 1;
+const INPUT_MOUSE_RIGHT_BUTTON: i32 = 2;
+const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawInputEvent {
+  pub event_type: i32,
+
+  pub buffer: [u8; 24],
+  pub buffer_len: i32,
+
+  pub key_code: i32,
+  pub status: i32,
+}
+
+#[repr(C)]
+pub struct RawHotKey {
+  pub id: i32,
+  pub code: u16,
+  pub flags: u32,
+}
+
+#[repr(C)]
+pub struct RawInitializationOptions {
+  pub hotkeys: *const RawHotKey,
+  pub hotkeys_count: i32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansodetect", kind = "static")]
+extern "C" {
+  pub fn detect_initialize(
+    callback: extern "C" fn(event: RawInputEvent),
+    options: RawInitializationOptions,
+  );
+}
+
+lazy_static! {
+  static ref CURRENT_SENDER: Arc<Mutex<Option<Sender<InputEvent>>>> = Arc::new(Mutex::new(None));
+}
+
+extern "C" fn native_callback(raw_event: RawInputEvent) {
+  let lock = CURRENT_SENDER
+    .lock()
+    .expect("unable to acquire CocoaSource sender lock");
+  if let Some(sender) = lock.as_ref() {
+    let event: Option<InputEvent> = raw_event.into();
+    if let Some(event) = event {
+      if let Err(error) = sender.send(event) {
+        error!("Unable to send event to Cocoa Sender: {}", error);
+      }
+    } else {
+      trace!("Unable to convert raw event to input event");
+    }
+  } else {
+    warn!("Lost raw event, as Cocoa Sender is not available");
+  }
+}
+
+pub struct CocoaSource {
+  receiver: LazyCell<Receiver<InputEvent>>,
+  hotkeys: Vec<HotKey>,
+}
+
+#[allow(clippy::new_without_default)]
+impl CocoaSource {
+  pub fn new(hotkeys: &[HotKey]) -> CocoaSource {
+    Self {
+      receiver: LazyCell::new(),
+      hotkeys: hotkeys.to_vec(),
+    }
+  }
+}
+
+impl Source for CocoaSource {
+  fn initialize(&mut self) -> Result<()> {
+    let (sender, receiver) = channel();
+
+    // Set the global sender
+    {
+      let mut lock = CURRENT_SENDER
+        .lock()
+        .expect("unable to acquire CocoaSource sender lock during initialization");
+      *lock = Some(sender);
+    }
+
+    // Generate the options
+    let hotkeys: Vec<RawHotKey> = self
+      .hotkeys
+      .iter()
+      .filter_map(|hk| {
+        let raw = convert_hotkey_to_raw(hk);
+        if raw.is_none() {
+          error!("unable to register hotkey: {:?}", hk);
+        }
+        raw
+      })
+      .collect();
+    let options = RawInitializationOptions {
+      hotkeys: hotkeys.as_ptr(),
+      hotkeys_count: hotkeys.len() as i32,
+    };
+
+    unsafe { detect_initialize(native_callback, options) };
+
+    if self.receiver.fill(receiver).is_err() {
+      error!("Unable to set CocoaSource receiver");
+      return Err(CocoaSourceError::Unknown().into());
+    }
+
+    Ok(())
+  }
+
+  fn eventloop(&self, event_callback: SourceCallback) -> Result<()> {
+    if let Some(receiver) = self.receiver.borrow() {
+      loop {
+        let event = receiver.recv();
+        match event {
+          Ok(event) => {
+            event_callback(event);
+          }
+          Err(error) => {
+            error!("CocoaSource receiver reported error: {}", error);
+            break;
+          }
+        }
+      }
+    } else {
+      error!("Unable to start event loop if CocoaSource receiver is null");
+      return Err(CocoaSourceError::Unknown().into());
+    }
+
+    Ok(())
+  }
+}
+
+impl Drop for CocoaSource {
+  fn drop(&mut self) {
+    // Reset the global sender
+    {
+      let mut lock = CURRENT_SENDER
+        .lock()
+        .expect("unable to acquire CocoaSource sender lock during initialization");
+      *lock = None;
+    }
+  }
+}
+
+fn convert_hotkey_to_raw(hk: &HotKey) -> Option<RawHotKey> {
+  let key_code = hk.key.to_code()?;
+  let code: Result<u16, _> = key_code.try_into();
+  if let Ok(code) = code {
+    let mut flags = 0;
+    if hk.has_ctrl() {
+      flags |= 1 << 12;
+    }
+    if hk.has_alt() {
+      flags |= 1 << 11;
+    }
+    if hk.has_meta() {
+      flags |= 1 << 8;
+    }
+    if hk.has_shift() {
+      flags |= 1 << 9;
+    }
+
+    Some(RawHotKey {
+      id: hk.id,
+      code,
+      flags,
+    })
+  } else {
+    error!("unable to generate raw hotkey, the key_code is overflowing");
+    None
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum CocoaSourceError {
+  #[error("unknown error")]
+  Unknown(),
+}
+
+impl From<RawInputEvent> for Option<InputEvent> {
+  fn from(raw: RawInputEvent) -> Option<InputEvent> {
+    let status = match raw.status {
+      INPUT_STATUS_RELEASED => Released,
+      INPUT_STATUS_PRESSED => Pressed,
+      _ => Pressed,
+    };
+
+    match raw.event_type {
+      // Keyboard events
+      INPUT_EVENT_TYPE_KEYBOARD => {
+        let (key, variant) = key_code_to_key(raw.key_code);
+
+        let value = if raw.buffer_len > 0 {
+          let raw_string_result =
+            CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]);
+          match raw_string_result {
+            Ok(c_string) => {
+              let string_result = c_string.to_str();
+              match string_result {
+                Ok(value) => Some(value.to_string()),
+                Err(err) => {
+                  warn!("CocoaSource event utf8 conversion error: {}", err);
+                  None
+                }
+              }
+            }
+            Err(err) => {
+              trace!("Received malformed event buffer: {}", err);
+              None
+            }
+          }
+        } else {
+          None
+        };
+
+        return Some(InputEvent::Keyboard(KeyboardEvent {
+          key,
+          value,
+          status,
+          variant,
+          code: raw
+            .key_code
+            .try_into()
+            .expect("unable to convert keycode to u32"),
+        }));
+      }
+      // Mouse events
+      INPUT_EVENT_TYPE_MOUSE => {
+        let button = raw_to_mouse_button(raw.key_code);
+
+        if let Some(button) = button {
+          return Some(InputEvent::Mouse(MouseEvent { button, status }));
+        }
+      }
+      // HOTKEYS
+      INPUT_EVENT_TYPE_HOTKEY => {
+        let id = raw.key_code;
+        return Some(InputEvent::HotKey(HotKeyEvent { hotkey_id: id }));
+      }
+      _ => {}
+    }
+
+    None
+  }
+}
+
+// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+fn key_code_to_key(key_code: i32) -> (Key, Option<Variant>) {
+  match key_code {
+    // Modifiers
+    0x3A => (Alt, Some(Left)),
+    0x3D => (Alt, Some(Right)),
+    0x39 => (CapsLock, None), // TODO
+    0x3B => (Control, Some(Left)),
+    0x3E => (Control, Some(Right)),
+    0x37 => (Meta, Some(Left)),
+    0x36 => (Meta, Some(Right)),
+    0x38 => (Shift, Some(Left)),
+    0x3C => (Shift, Some(Right)),
+
+    // Whitespace
+    0x24 => (Enter, None),
+    0x30 => (Tab, None),
+    0x31 => (Space, None),
+
+    // Navigation
+    0x7D => (ArrowDown, None),
+    0x7B => (ArrowLeft, None),
+    0x7C => (ArrowRight, None),
+    0x7E => (ArrowUp, None),
+    0x77 => (End, None),
+    0x73 => (Home, None),
+    0x79 => (PageDown, None),
+    0x74 => (PageUp, None),
+
+    // UI
+    0x35 => (Escape, None),
+
+    // Editing keys
+    0x33 => (Backspace, None),
+
+    // Function keys
+    0x7A => (F1, None),
+    0x78 => (F2, None),
+    0x63 => (F3, None),
+    0x76 => (F4, None),
+    0x60 => (F5, None),
+    0x61 => (F6, None),
+    0x62 => (F7, None),
+    0x64 => (F8, None),
+    0x65 => (F9, None),
+    0x6D => (F10, None),
+    0x67 => (F11, None),
+    0x6F => (F12, None),
+    0x69 => (F13, None),
+    0x6B => (F14, None),
+    0x71 => (F15, None),
+    0x6A => (F16, None),
+    0x40 => (F17, None),
+    0x4F => (F18, None),
+    0x50 => (F19, None),
+    0x5A => (F20, None),
+
+    // Other keys, includes the raw code provided by the operating system
+    _ => (Other(key_code), None),
+  }
+}
+
+fn raw_to_mouse_button(raw: i32) -> Option<MouseButton> {
+  match raw {
+    INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left),
+    INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right),
+    INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle),
+    _ => None,
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use std::ffi::CString;
+
+  use super::*;
+
+  fn default_raw_input_event() -> RawInputEvent {
+    RawInputEvent {
+      event_type: INPUT_EVENT_TYPE_KEYBOARD,
+      buffer: [0; 24],
+      buffer_len: 0,
+      key_code: 0,
+      status: INPUT_STATUS_PRESSED,
+    }
+  }
+
+  #[test]
+  fn raw_to_input_event_keyboard_works_correctly() {
+    let c_string = CString::new("k".to_string()).unwrap();
+    let mut buffer: [u8; 24] = [0; 24];
+    buffer[..1].copy_from_slice(c_string.as_bytes());
+
+    let mut raw = default_raw_input_event();
+    raw.buffer = buffer;
+    raw.buffer_len = 1;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.key_code = 40;
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Keyboard(KeyboardEvent {
+        key: Other(40),
+        status: Released,
+        value: Some("k".to_string()),
+        variant: None,
+        code: 40,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_event_mouse_works_correctly() {
+    let mut raw = default_raw_input_event();
+    raw.event_type = INPUT_EVENT_TYPE_MOUSE;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.key_code = INPUT_MOUSE_RIGHT_BUTTON;
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Mouse(MouseEvent {
+        status: Released,
+        button: MouseButton::Right,
+      })
+    );
+  }
+}
diff --git a/espanso-detect/src/mac/native.h b/espanso-detect/src/mac/native.h
new file mode 100644
index 00000000..a86d9432
--- /dev/null
+++ b/espanso-detect/src/mac/native.h
@@ -0,0 +1,73 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_DETECT_H
+#define ESPANSO_DETECT_H
+
+#include <stdint.h>
+
+#define INPUT_EVENT_TYPE_KEYBOARD 1
+#define INPUT_EVENT_TYPE_MOUSE    2
+#define INPUT_EVENT_TYPE_HOTKEY   3
+
+#define INPUT_STATUS_PRESSED      1
+#define INPUT_STATUS_RELEASED     2
+
+#define INPUT_LEFT_VARIANT 1
+#define INPUT_RIGHT_VARIANT 2
+
+#define INPUT_MOUSE_LEFT_BUTTON   1
+#define INPUT_MOUSE_RIGHT_BUTTON  2
+#define INPUT_MOUSE_MIDDLE_BUTTON 3
+
+typedef struct {
+  // Keyboard, Mouse or Hotkey event
+  int32_t event_type;
+
+  // Contains the string corresponding to the key, if any
+  char buffer[24];
+  // Length of the extracted string. Equals 0 if no string is extracted
+  int32_t buffer_len;
+  
+  // Virtual key code of the pressed key in case of keyboard events
+  // Mouse button code if mouse_event.
+  // Hotkey ID in case of hotkeys
+  int32_t key_code;
+  
+  // Pressed or Released status
+  int32_t status;
+} InputEvent;
+
+typedef void (*EventCallback)(InputEvent data);
+
+typedef struct {
+  int32_t hk_id;
+  uint16_t key_code;
+  uint32_t flags;
+} HotKey;
+
+typedef struct {
+  HotKey *hotkeys;
+  int32_t hotkeys_count;
+} InitializeOptions;
+
+// Initialize the event global monitor
+extern "C" void * detect_initialize(EventCallback callback, InitializeOptions options);
+
+#endif //ESPANSO_DETECT_H
\ No newline at end of file
diff --git a/espanso-detect/src/mac/native.mm b/espanso-detect/src/mac/native.mm
new file mode 100644
index 00000000..6eb5e9a2
--- /dev/null
+++ b/espanso-detect/src/mac/native.mm
@@ -0,0 +1,128 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+#include <Carbon/Carbon.h>
+
+#include <string.h>
+
+const float ESPANSO_EVENT_MARKER = -27469;
+
+const unsigned long long FLAGS = NSEventMaskKeyDown | NSEventMaskKeyUp | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | 
+                                 NSEventMaskLeftMouseUp | NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | 
+                                 NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp;
+
+OSStatus hotkey_event_handler(EventHandlerCallRef _next, EventRef evt, void *userData);
+
+void * detect_initialize(EventCallback callback, InitializeOptions options) {
+  HotKey * hotkeys_clone = (HotKey*) malloc(sizeof(HotKey) * options.hotkeys_count);
+  memcpy(hotkeys_clone, options.hotkeys, sizeof(HotKey) * options.hotkeys_count);
+
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    // Setup hotkeys
+    if (options.hotkeys_count > 0) {
+      EventHotKeyRef hotkey_ref;
+      EventHotKeyID hotkey_id;
+      hotkey_id.signature='htk1';
+
+      EventTypeSpec eventType;
+      eventType.eventClass = kEventClassKeyboard;
+      eventType.eventKind = kEventHotKeyPressed;    
+      
+      InstallApplicationEventHandler(&hotkey_event_handler, 1, &eventType, (void*)callback, NULL);
+
+      for (int i = 0; i<options.hotkeys_count; i++) {
+        hotkey_id.id=hotkeys_clone[i].hk_id;
+        RegisterEventHotKey(hotkeys_clone[i].key_code, hotkeys_clone[i].flags, hotkey_id, GetApplicationEventTarget(), 0, &hotkey_ref);  
+      }
+    }
+    
+    free(hotkeys_clone);
+
+    // Setup key detection
+
+    [NSEvent addGlobalMonitorForEventsMatchingMask:FLAGS handler:^(NSEvent *event){
+        // Workaround used to determine if the event was generated by espanso itself
+        float eventLocation = event.locationInWindow.x;
+        if (fabs(eventLocation - ESPANSO_EVENT_MARKER) < 0.001) {
+          return;
+        }
+
+        InputEvent inputEvent = {};
+        if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp ) {
+          inputEvent.event_type = INPUT_EVENT_TYPE_KEYBOARD;
+          inputEvent.status = (event.type == NSEventTypeKeyDown) ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED;
+          inputEvent.key_code = event.keyCode;
+
+          const char *chars = [event.characters UTF8String];
+          strncpy(inputEvent.buffer, chars, 23);
+          inputEvent.buffer_len = event.characters.length;
+
+          callback(inputEvent);
+        }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown ||
+                  event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) {
+          inputEvent.event_type = INPUT_EVENT_TYPE_MOUSE;
+          inputEvent.status = (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown ||
+                               event.type == NSEventTypeOtherMouseDown) ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED;
+          if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeLeftMouseUp) {
+            inputEvent.key_code = INPUT_MOUSE_LEFT_BUTTON;
+          } else if (event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeRightMouseUp) {
+            inputEvent.key_code = INPUT_MOUSE_RIGHT_BUTTON;
+          } else if (event.type == NSEventTypeOtherMouseDown || event.type == NSEventTypeOtherMouseUp) {
+            inputEvent.key_code = INPUT_MOUSE_MIDDLE_BUTTON;
+          }
+
+          callback(inputEvent);
+        }else{
+          // Modifier keys (SHIFT, CTRL, ecc) are handled as a separate case on macOS
+          inputEvent.event_type = INPUT_EVENT_TYPE_KEYBOARD;
+          inputEvent.key_code = event.keyCode;
+
+          // To determine whether these keys are pressed or released, we have to analyze each case
+          if (event.keyCode == kVK_Shift || event.keyCode == kVK_RightShift) {
+            inputEvent.status = (([event modifierFlags] & NSEventModifierFlagShift) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
+          } else if (event.keyCode == kVK_Command || event.keyCode == kVK_RightCommand) {
+            inputEvent.status = (([event modifierFlags] & NSEventModifierFlagCommand) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
+          } else if (event.keyCode == kVK_Control || event.keyCode == kVK_RightControl) {
+            inputEvent.status = (([event modifierFlags] & NSEventModifierFlagControl) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
+          } else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) {
+            inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
+          }
+          callback(inputEvent);
+        }
+    }];
+  });
+}
+
+OSStatus hotkey_event_handler(EventHandlerCallRef _next, EventRef evt, void *userData)
+{
+    EventHotKeyID hotkey_id;
+    GetEventParameter(evt, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotkey_id), NULL, &hotkey_id);
+    
+    EventCallback callback = (EventCallback) userData;
+
+    InputEvent inputEvent = {};
+    inputEvent.event_type = INPUT_EVENT_TYPE_HOTKEY;
+    inputEvent.key_code = hotkey_id.id;
+    callback(inputEvent);
+
+    return noErr;
+}
\ No newline at end of file
diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs
new file mode 100644
index 00000000..d25e5284
--- /dev/null
+++ b/espanso-detect/src/win32/mod.rs
@@ -0,0 +1,482 @@
+/*
+ * 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::{convert::TryInto, ffi::c_void};
+
+use lazycell::LazyCell;
+use log::{debug, error, trace, warn};
+use widestring::U16CStr;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
+use crate::event::{Key::*, MouseButton, MouseEvent};
+use crate::{event::Status::*, Source, SourceCallback};
+use crate::{
+  event::{HotKeyEvent, Variant::*},
+  hotkey::HotKey,
+};
+
+const INPUT_LEFT_VARIANT: i32 = 1;
+const INPUT_RIGHT_VARIANT: i32 = 2;
+
+const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1;
+const INPUT_EVENT_TYPE_MOUSE: i32 = 2;
+const INPUT_EVENT_TYPE_HOTKEY: i32 = 3;
+
+const INPUT_STATUS_PRESSED: i32 = 1;
+const INPUT_STATUS_RELEASED: i32 = 2;
+
+const INPUT_MOUSE_LEFT_BUTTON: i32 = 1;
+const INPUT_MOUSE_RIGHT_BUTTON: i32 = 2;
+const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3;
+const INPUT_MOUSE_BUTTON_1: i32 = 4;
+const INPUT_MOUSE_BUTTON_2: i32 = 5;
+const INPUT_MOUSE_BUTTON_3: i32 = 6;
+const INPUT_MOUSE_BUTTON_4: i32 = 7;
+const INPUT_MOUSE_BUTTON_5: i32 = 8;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawInputEvent {
+  pub event_type: i32,
+
+  pub buffer: [u16; 24],
+  pub buffer_len: i32,
+
+  pub key_code: i32,
+  pub variant: i32,
+  pub status: i32,
+
+  // Only relevant for keyboard events, this is set to 1
+  // if a keyboard event has an explicit source, 0 otherwise.
+  // This is needed to filter out software generated events,
+  // including those from espanso.
+  pub has_known_source: i32,
+}
+
+#[repr(C)]
+pub struct RawHotKey {
+  pub id: i32,
+  pub code: u32,
+  pub flags: u32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansodetect", kind = "static")]
+extern "C" {
+  pub fn detect_initialize(_self: *const Win32Source, error_code: *mut i32) -> *mut c_void;
+  pub fn detect_register_hotkey(window: *const c_void, hotkey: RawHotKey) -> i32;
+
+  pub fn detect_eventloop(
+    window: *const c_void,
+    event_callback: extern "C" fn(_self: *mut Win32Source, event: RawInputEvent),
+  ) -> i32;
+
+  pub fn detect_destroy(window: *const c_void) -> i32;
+}
+
+pub struct Win32Source {
+  handle: *mut c_void,
+  callback: LazyCell<SourceCallback>,
+  hotkeys: Vec<HotKey>,
+
+  exclude_orphan_events: bool,
+}
+
+#[allow(clippy::new_without_default)]
+impl Win32Source {
+  pub fn new(hotkeys: &[HotKey], exclude_orphan_events: bool) -> Win32Source {
+    Self {
+      handle: std::ptr::null_mut(),
+      callback: LazyCell::new(),
+      hotkeys: hotkeys.to_vec(),
+      exclude_orphan_events,
+    }
+  }
+}
+
+impl Source for Win32Source {
+  fn initialize(&mut self) -> Result<()> {
+    let mut error_code = 0;
+    let handle = unsafe { detect_initialize(self as *const Win32Source, &mut error_code) };
+
+    if handle.is_null() {
+      let error = match error_code {
+        -1 => Win32SourceError::WindowFailed(),
+        -2 => Win32SourceError::RawInputFailed(),
+        _ => Win32SourceError::Unknown(),
+      };
+      return Err(error.into());
+    }
+
+    // Register the hotkeys
+    self.hotkeys.iter().for_each(|hk| {
+      let raw = convert_hotkey_to_raw(hk);
+      if let Some(raw_hk) = raw {
+        if unsafe { detect_register_hotkey(handle, raw_hk) } == 0 {
+          error!("unable to register hotkey: {}", hk);
+        } else {
+          debug!("registered hotkey: {}", hk);
+        }
+      } else {
+        error!("unable to generate raw hotkey mapping: {}", hk);
+      }
+    });
+
+    self.handle = handle;
+
+    Ok(())
+  }
+
+  fn eventloop(&self, event_callback: SourceCallback) -> Result<()> {
+    if self.handle.is_null() {
+      panic!("Attempt to start Win32Source eventloop without initialization");
+    }
+
+    if self.callback.fill(event_callback).is_err() {
+      error!("Unable to set Win32Source event callback");
+      return Err(Win32SourceError::Unknown().into());
+    }
+
+    extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) {
+      // Filter out keyboard events without an explicit HID device source.
+      // This is needed to filter out the software-generated events, including
+      // those from espanso.
+      if event.event_type == INPUT_EVENT_TYPE_KEYBOARD
+        && event.has_known_source == 0
+        && unsafe { (*_self).exclude_orphan_events }
+      {
+        trace!("skipping keyboard event with unknown HID source (probably software generated).");
+        return;
+      }
+
+      let event: Option<InputEvent> = event.into();
+      if let Some(callback) = unsafe { (*_self).callback.borrow() } {
+        if let Some(event) = event {
+          callback(event)
+        } else {
+          trace!("Unable to convert raw event to input event");
+        }
+      }
+    }
+
+    let error_code = unsafe { detect_eventloop(self.handle, callback) };
+
+    if error_code <= 0 {
+      error!("Win32Source eventloop returned a negative error code");
+      return Err(Win32SourceError::Unknown().into());
+    }
+
+    Ok(())
+  }
+}
+
+impl Drop for Win32Source {
+  fn drop(&mut self) {
+    if self.handle.is_null() {
+      error!("Win32Source destruction cannot be performed, handle is null");
+      return;
+    }
+
+    let result = unsafe { detect_destroy(self.handle) };
+
+    if result != 0 {
+      error!("Win32Source destruction returned non-zero code");
+    }
+  }
+}
+
+fn convert_hotkey_to_raw(hk: &HotKey) -> Option<RawHotKey> {
+  let key_code = hk.key.to_code()?;
+  let mut flags = 0x4000; // NOREPEAT flags
+  if hk.has_ctrl() {
+    flags |= 0x0002;
+  }
+  if hk.has_alt() {
+    flags |= 0x0001;
+  }
+  if hk.has_meta() {
+    flags |= 0x0008;
+  }
+  if hk.has_shift() {
+    flags |= 0x0004;
+  }
+
+  Some(RawHotKey {
+    id: hk.id,
+    code: key_code,
+    flags,
+  })
+}
+
+#[derive(Error, Debug)]
+pub enum Win32SourceError {
+  #[error("window registration failed")]
+  WindowFailed(),
+
+  #[error("raw input API failed")]
+  RawInputFailed(),
+
+  #[error("unknown error")]
+  Unknown(),
+}
+
+impl From<RawInputEvent> for Option<InputEvent> {
+  fn from(raw: RawInputEvent) -> Option<InputEvent> {
+    let status = match raw.status {
+      INPUT_STATUS_RELEASED => Released,
+      INPUT_STATUS_PRESSED => Pressed,
+      _ => Pressed,
+    };
+
+    match raw.event_type {
+      // Keyboard events
+      INPUT_EVENT_TYPE_KEYBOARD => {
+        let (key, variant_hint) = key_code_to_key(raw.key_code);
+
+        // If the raw event does not include an explicit variant, use the hint provided by the key code
+        let variant = match raw.variant {
+          INPUT_LEFT_VARIANT => Some(Left),
+          INPUT_RIGHT_VARIANT => Some(Right),
+          _ => variant_hint,
+        };
+
+        let value = if raw.buffer_len > 0 {
+          let raw_string_result = U16CStr::from_slice_with_nul(&raw.buffer);
+          match raw_string_result {
+            Ok(c_string) => {
+              let string_result = c_string.to_string();
+              match string_result {
+                Ok(value) => Some(value),
+                Err(err) => {
+                  warn!("Widechar conversion error: {}", err);
+                  None
+                }
+              }
+            }
+            Err(err) => {
+              warn!("Received malformed widechar: {}", err);
+              None
+            }
+          }
+        } else {
+          None
+        };
+
+        return Some(InputEvent::Keyboard(KeyboardEvent {
+          key,
+          value,
+          status,
+          variant,
+          code: raw
+            .key_code
+            .try_into()
+            .expect("unable to convert keycode to u32"),
+        }));
+      }
+      // Mouse events
+      INPUT_EVENT_TYPE_MOUSE => {
+        let button = raw_to_mouse_button(raw.key_code);
+
+        if let Some(button) = button {
+          return Some(InputEvent::Mouse(MouseEvent { button, status }));
+        }
+      }
+      // Hotkey events
+      INPUT_EVENT_TYPE_HOTKEY => {
+        return Some(InputEvent::HotKey(HotKeyEvent {
+          hotkey_id: raw.key_code,
+        }))
+      }
+      _ => {}
+    }
+
+    None
+  }
+}
+
+// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+fn key_code_to_key(key_code: i32) -> (Key, Option<Variant>) {
+  match key_code {
+    // Modifiers
+    0x12 => (Alt, None),
+    0xA4 => (Alt, Some(Left)),
+    0xA5 => (Alt, Some(Right)),
+    0x14 => (CapsLock, None),
+    0x11 => (Control, None),
+    0xA2 => (Control, Some(Left)),
+    0xA3 => (Control, Some(Right)),
+    0x5B => (Meta, Some(Left)),
+    0x5C => (Meta, Some(Right)),
+    0x90 => (NumLock, None),
+    0x10 => (Shift, None),
+    0xA0 => (Shift, Some(Left)),
+    0xA1 => (Shift, Some(Right)),
+
+    // Whitespace
+    0x0D => (Enter, None),
+    0x09 => (Tab, None),
+    0x20 => (Space, None),
+
+    // Navigation
+    0x28 => (ArrowDown, None),
+    0x25 => (ArrowLeft, None),
+    0x27 => (ArrowRight, None),
+    0x26 => (ArrowUp, None),
+    0x23 => (End, None),
+    0x24 => (Home, None),
+    0x22 => (PageDown, None),
+    0x21 => (PageUp, None),
+
+    // UI
+    0x1B => (Escape, None),
+
+    // Editing keys
+    0x08 => (Backspace, None),
+
+    // Function keys
+    0x70 => (F1, None),
+    0x71 => (F2, None),
+    0x72 => (F3, None),
+    0x73 => (F4, None),
+    0x74 => (F5, None),
+    0x75 => (F6, None),
+    0x76 => (F7, None),
+    0x77 => (F8, None),
+    0x78 => (F9, None),
+    0x79 => (F10, None),
+    0x7A => (F11, None),
+    0x7B => (F12, None),
+    0x7C => (F13, None),
+    0x7D => (F14, None),
+    0x7E => (F15, None),
+    0x7F => (F16, None),
+    0x80 => (F17, None),
+    0x81 => (F18, None),
+    0x82 => (F19, None),
+    0x83 => (F20, None),
+
+    // Other keys, includes the raw code provided by the operating system
+    _ => (Other(key_code), None),
+  }
+}
+
+fn raw_to_mouse_button(raw: i32) -> Option<MouseButton> {
+  match raw {
+    INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left),
+    INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right),
+    INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle),
+    INPUT_MOUSE_BUTTON_1 => Some(MouseButton::Button1),
+    INPUT_MOUSE_BUTTON_2 => Some(MouseButton::Button2),
+    INPUT_MOUSE_BUTTON_3 => Some(MouseButton::Button3),
+    INPUT_MOUSE_BUTTON_4 => Some(MouseButton::Button4),
+    INPUT_MOUSE_BUTTON_5 => Some(MouseButton::Button5),
+    _ => None,
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  fn default_raw_input_event() -> RawInputEvent {
+    RawInputEvent {
+      event_type: INPUT_EVENT_TYPE_KEYBOARD,
+      buffer: [0; 24],
+      buffer_len: 0,
+      key_code: 0,
+      variant: INPUT_LEFT_VARIANT,
+      status: INPUT_STATUS_PRESSED,
+      has_known_source: 1,
+    }
+  }
+
+  #[test]
+  fn raw_to_input_event_keyboard_works_correctly() {
+    let wide_string = widestring::WideString::from("k".to_string());
+    let mut buffer: [u16; 24] = [0; 24];
+    buffer[..1].copy_from_slice(wide_string.as_slice());
+
+    let mut raw = default_raw_input_event();
+    raw.buffer = buffer;
+    raw.buffer_len = 1;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.variant = 0;
+    raw.key_code = 0x4B;
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Keyboard(KeyboardEvent {
+        key: Other(0x4B),
+        status: Released,
+        value: Some("k".to_string()),
+        variant: None,
+        code: 0x4B,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_event_mouse_works_correctly() {
+    let mut raw = default_raw_input_event();
+    raw.event_type = INPUT_EVENT_TYPE_MOUSE;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.variant = 0;
+    raw.key_code = INPUT_MOUSE_RIGHT_BUTTON;
+
+    let result: Option<InputEvent> = raw.into();
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Mouse(MouseEvent {
+        status: Released,
+        button: MouseButton::Right,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_invalid_buffer() {
+    let buffer: [u16; 24] = [123; 24];
+
+    let mut raw = default_raw_input_event();
+    raw.buffer = buffer;
+    raw.buffer_len = 5;
+
+    let result: Option<InputEvent> = raw.into();
+    assert!(result.unwrap().into_keyboard().unwrap().value.is_none());
+  }
+
+  #[test]
+  fn raw_to_input_event_returns_none_when_missing_type() {
+    let result: Option<InputEvent> = RawInputEvent {
+      event_type: 0, // Missing type
+      buffer: [0; 24],
+      buffer_len: 0,
+      key_code: 123,
+      variant: INPUT_LEFT_VARIANT,
+      status: INPUT_STATUS_PRESSED,
+      has_known_source: 1,
+    }
+    .into();
+    assert!(result.is_none());
+  }
+}
diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp
new file mode 100644
index 00000000..673ccdb2
--- /dev/null
+++ b/espanso-detect/src/win32/native.cpp
@@ -0,0 +1,365 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <iostream>
+#include <stdio.h>
+#include <string>
+#include <vector>
+#include <memory>
+#include <array>
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include <windows.h>
+#include <winuser.h>
+#include <strsafe.h>
+#include <Windows.h>
+
+// How many milliseconds must pass between events before refreshing the keyboard layout
+const long DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL = 2000;
+const wchar_t *const DETECT_WINCLASS = L"EspansoDetect";
+const USHORT MOUSE_DOWN_FLAGS = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
+                        RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN |
+                        RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN;
+const USHORT MOUSE_UP_FLAGS = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP |
+                      RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP |
+                      RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP;
+
+typedef struct {
+  HKL current_keyboard_layout;
+  DWORD last_key_press_tick;
+
+  // Rust interop
+  void * rust_instance;
+  EventCallback event_callback;
+} DetectVariables;
+
+/*
+ * Message handler procedure for the window
+ */
+LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
+{
+  DetectVariables * variables = reinterpret_cast<DetectVariables*>(GetWindowLongPtrW(window, GWLP_USERDATA));
+
+  switch (msg)
+  {
+  case WM_DESTROY:
+    PostQuitMessage(0);
+
+    // Free the window variables
+    delete variables;
+    SetWindowLongPtrW(window, GWLP_USERDATA, NULL);
+
+    return 0L;
+  case WM_HOTKEY: // Hotkeys
+  {
+    InputEvent event = {};
+    event.event_type = INPUT_EVENT_TYPE_HOTKEY;
+    event.key_code = (int32_t) wp;
+    if (variables->rust_instance != NULL && variables->event_callback != NULL)
+    {
+      variables->event_callback(variables->rust_instance, event);
+    }
+    break;
+  }
+  case WM_INPUT: // Message relative to the RAW INPUT events
+  {
+    InputEvent event = {};
+
+    // Get the input size
+    UINT dwSize;
+    GetRawInputData(
+        (HRAWINPUT)lp,
+        RID_INPUT,
+        NULL,
+        &dwSize,
+        sizeof(RAWINPUTHEADER));
+
+    // Create a proper sized structure to hold the data
+    std::vector<BYTE> lpb(dwSize);
+
+    // Request the Raw input data
+    if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize,
+                        sizeof(RAWINPUTHEADER)) != dwSize)
+    {
+      return 0;
+    }
+
+    // Convert the input data
+    RAWINPUT *raw = reinterpret_cast<RAWINPUT *>(lpb.data());
+    
+    if (raw->header.dwType == RIM_TYPEKEYBOARD)  // Keyboard events
+    {
+      // We only want KEY UP AND KEY DOWN events
+      if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
+          raw->data.keyboard.Message != WM_SYSKEYDOWN)
+      {
+        return 0;
+      }
+
+      event.has_known_source = (raw->header.hDevice == 0) ? 0 : 1;
+
+      // The alt key sends a SYSKEYDOWN instead of KEYDOWN event
+      int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN ||
+                        raw->data.keyboard.Message == WM_SYSKEYDOWN;
+
+      DWORD currentTick = GetTickCount();
+
+      // If enough time has passed between the last keypress and now, refresh the keyboard layout
+      if ((currentTick - variables->last_key_press_tick) > DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL)
+      {
+
+        // Because keyboard layouts on windows are Window-specific, to get the current
+        // layout we need to get the foreground window and get its layout.
+
+        HWND hwnd = GetForegroundWindow();
+        if (hwnd)
+        {
+          DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
+          HKL newKeyboardLayout = GetKeyboardLayout(threadID);
+
+          // It's not always valid, so update the current value only if available.
+          if (newKeyboardLayout != 0)
+          {
+            variables->current_keyboard_layout = newKeyboardLayout;
+          }
+        }
+
+        variables->last_key_press_tick = currentTick;
+      }
+
+      // Get keyboard state ( necessary to decode the associated Unicode char )
+      std::vector<BYTE> lpKeyState(256);
+      if (GetKeyboardState(lpKeyState.data()))
+      {
+        // This flag is needed to avoid chaning the keyboard state for some layouts.
+        // Refer to issue: https://github.com/federico-terzi/espanso/issues/86
+        UINT flags = 1 << 2;
+
+        int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast<LPWSTR>(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout);
+
+        // Handle the corresponding string if present
+        if (result >= 1)
+        {
+          event.buffer_len = result;
+        }
+        else
+        {
+          // If the given key does not have a correspondent string, reset the buffer
+          memset(event.buffer, 0, sizeof(event.buffer));
+          event.buffer_len = 0;
+        }
+
+        event.event_type = INPUT_EVENT_TYPE_KEYBOARD;
+        event.key_code = raw->data.keyboard.VKey;
+        event.status = is_key_down ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED;
+
+        // Load the key variants when appropriate
+        if (raw->data.keyboard.VKey == VK_SHIFT)
+        {
+          // To discriminate between the left and right shift, we need to employ a workaround.
+          // See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput
+          if (raw->data.keyboard.MakeCode == 42)
+          { // Left shift
+            event.variant = INPUT_LEFT_VARIANT;
+          }
+          if (raw->data.keyboard.MakeCode == 54)
+          { // Right shift
+            event.variant = INPUT_RIGHT_VARIANT;
+          }
+        }
+        else
+        {
+          // Also the ALT and CTRL key are special cases
+          // Check out the previous Stackoverflow question for more information
+          if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU)
+          {
+            if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0)
+            {
+              event.variant = INPUT_RIGHT_VARIANT;
+            }
+            else
+            {
+              event.variant = INPUT_LEFT_VARIANT;
+            }
+          }
+        }
+      }
+    }
+    else if (raw->header.dwType == RIM_TYPEMOUSE)  // Mouse events
+    {
+      // Make sure the mouse event belongs to the supported ones
+      if ((raw->data.mouse.usButtonFlags & (MOUSE_DOWN_FLAGS | MOUSE_UP_FLAGS)) == 0) {
+        return 0;        
+      }
+
+      event.event_type = INPUT_EVENT_TYPE_MOUSE;
+
+      if ((raw->data.mouse.usButtonFlags & MOUSE_DOWN_FLAGS) != 0)
+      {
+        event.status = INPUT_STATUS_PRESSED;
+      } else if ((raw->data.mouse.usButtonFlags & MOUSE_UP_FLAGS) != 0) {
+        event.status = INPUT_STATUS_RELEASED;
+      }
+
+      // Convert the mouse flags into custom button mappings
+      if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_LEFT_BUTTON_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_LEFT_BUTTON;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_RIGHT_BUTTON;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_MIDDLE_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_MIDDLE_BUTTON;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_1_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_BUTTON_1;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_2_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_BUTTON_2;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_3_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_BUTTON_3;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_4_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_BUTTON_4;
+      } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_5_DOWN | RI_MOUSE_BUTTON_5_UP)) != 0) {
+        event.key_code = INPUT_MOUSE_BUTTON_5;
+      }
+    }
+
+    // If valid, send the event to the Rust layer
+    if (event.event_type != 0 &&  variables->rust_instance != NULL && variables->event_callback != NULL)
+    {
+      variables->event_callback(variables->rust_instance, event);
+    }
+
+    return 0;
+  }
+  default:
+    return DefWindowProc(window, msg, wp, lp);
+  }
+}
+
+void * detect_initialize(void *_self, int32_t *error_code)
+{
+  HWND window = NULL;
+
+  // Initialize the Worker window
+  // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
+  WNDCLASSEX wndclass = {
+      sizeof(WNDCLASSEX),       // cbSize: Size of this structure
+      0,                        // style: Class styles
+      detect_window_procedure,         // lpfnWndProc: Pointer to the window procedure
+      0,                        // cbClsExtra: Number of extra bytes to allocate following the window-class structure
+      0,                        // cbWndExtra: The number of extra bytes to allocate following the window instance.
+      GetModuleHandle(0),       // hInstance: A handle to the instance that contains the window procedure for the class.
+      NULL,                     // hIcon: A handle to the class icon.
+      LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor.
+      NULL,                     // hbrBackground: A handle to the class background brush.
+      NULL,                     // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
+      DETECT_WINCLASS,                 // lpszClassName: A pointer to a null-terminated string or is an atom.
+      NULL                      // hIconSm: A handle to a small icon that is associated with the window class.
+  };
+
+  if (RegisterClassEx(&wndclass))
+  {
+    DetectVariables * variables = new DetectVariables();
+    variables->rust_instance = _self;
+
+    // Initialize the default keyboard layout
+    variables->current_keyboard_layout = GetKeyboardLayout(0);
+
+    // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
+    window = CreateWindowEx(
+        0,                        // dwExStyle: The extended window style of the window being created.
+        DETECT_WINCLASS,                 // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
+        L"Espanso Worker Window", // lpWindowName: The window name.
+        WS_OVERLAPPEDWINDOW,      // dwStyle: The style of the window being created.
+        CW_USEDEFAULT,            // X: The initial horizontal position of the window.
+        CW_USEDEFAULT,            // Y: The initial vertical position of the window.
+        100,                      // nWidth: The width, in device units, of the window.
+        100,                      // nHeight: The height, in device units, of the window.
+        NULL,                     // hWndParent:  handle to the parent or owner window of the window being created.
+        NULL,                     // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
+        GetModuleHandle(0),       // hInstance: A handle to the instance of the module to be associated with the window.
+        NULL                      // lpParam: Pointer to a value to be passed to the window
+    );
+
+    SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
+
+    // Register raw inputs
+    RAWINPUTDEVICE Rid[2];
+
+    Rid[0].usUsagePage = 0x01;
+    Rid[0].usUsage = 0x06;
+    Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK;
+    Rid[0].hwndTarget = window;
+
+    Rid[1].usUsagePage = 0x01;
+    Rid[1].usUsage = 0x02;
+    Rid[1].dwFlags = RIDEV_INPUTSINK;
+    Rid[1].hwndTarget = window;
+
+    if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE)
+    { // Something went wrong, error.
+      *error_code = -2;
+      return nullptr;
+    }
+  }
+  else
+  {
+    // Something went wrong, error.
+    *error_code = -1;
+    return nullptr;
+  }
+
+  return window;
+}
+
+int32_t detect_register_hotkey(void * window, HotKey hotkey) {
+  return RegisterHotKey((HWND)window, hotkey.hk_id, hotkey.flags, hotkey.key_code);
+}
+
+int32_t detect_eventloop(void * window, EventCallback _callback)
+{
+  if (window)
+  {
+    DetectVariables * variables = reinterpret_cast<DetectVariables*>(GetWindowLongPtrW((HWND) window, GWLP_USERDATA));
+    variables->event_callback = _callback;
+
+    // Hide the window
+    ShowWindow((HWND) window, SW_HIDE);
+
+    // Enter the Event loop
+    MSG msg;
+    while (GetMessage(&msg, 0, 0, 0))
+      DispatchMessage(&msg);
+  }
+
+  return 1;
+}
+
+int32_t detect_destroy(void * window) {
+  if (window) {
+    return DestroyWindow((HWND) window);
+  }
+}
\ No newline at end of file
diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h
new file mode 100644
index 00000000..a93b9f79
--- /dev/null
+++ b/espanso-detect/src/win32/native.h
@@ -0,0 +1,92 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_DETECT_H
+#define ESPANSO_DETECT_H
+
+#include <stdint.h>
+
+#define INPUT_EVENT_TYPE_KEYBOARD 1
+#define INPUT_EVENT_TYPE_MOUSE    2
+#define INPUT_EVENT_TYPE_HOTKEY   3
+
+#define INPUT_STATUS_PRESSED      1
+#define INPUT_STATUS_RELEASED     2
+
+#define INPUT_LEFT_VARIANT 1
+#define INPUT_RIGHT_VARIANT 2
+
+#define INPUT_MOUSE_LEFT_BUTTON   1
+#define INPUT_MOUSE_RIGHT_BUTTON  2
+#define INPUT_MOUSE_MIDDLE_BUTTON 3
+#define INPUT_MOUSE_BUTTON_1      4
+#define INPUT_MOUSE_BUTTON_2      5
+#define INPUT_MOUSE_BUTTON_3      6
+#define INPUT_MOUSE_BUTTON_4      7
+#define INPUT_MOUSE_BUTTON_5      8
+
+typedef struct {
+  // Keyboard, Mouse or Hotkey event
+  int32_t event_type;
+
+  // Contains the string corresponding to the key, if any
+  uint16_t buffer[24];
+  // Length of the extracted string. Equals 0 if no string is extracted
+  int32_t buffer_len;
+  
+  // Virtual key code of the pressed key in case of keyboard events
+  // Mouse button code for mouse events.
+  // Hotkey id for hotkey events
+  int32_t key_code;
+  
+  // Left or Right variant
+  int32_t variant;
+
+  // Pressed or Released status
+  int32_t status;
+
+  // Only relevant for keyboard events, this is set to 1
+  // if a keyboard event has an explicit source, 0 otherwise.
+  // This is needed to filter out software generated events,
+  // including those from espanso.
+  int32_t has_known_source;
+} InputEvent;
+
+typedef struct {
+  int32_t hk_id;
+  uint32_t key_code;
+  uint32_t flags;
+} HotKey;
+
+typedef void (*EventCallback)(void * rust_istance, InputEvent data);
+
+
+// Initialize the Raw Input API and the Window.
+extern "C" void * detect_initialize(void * rust_istance, int32_t *error_code);
+
+// Register the given hotkey, return a non-zero code if successful
+extern "C" int32_t detect_register_hotkey(void * window, HotKey hotkey);
+
+// Run the event loop. Blocking call.
+extern "C" int32_t detect_eventloop(void * window, EventCallback callback);
+
+// Destroy the given window.
+extern "C" int32_t detect_destroy(void * window);
+
+#endif //ESPANSO_DETECT_H
\ No newline at end of file
diff --git a/espanso-detect/src/x11/mod.rs b/espanso-detect/src/x11/mod.rs
new file mode 100644
index 00000000..8665ee4f
--- /dev/null
+++ b/espanso-detect/src/x11/mod.rs
@@ -0,0 +1,517 @@
+/*
+ * 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,
+  convert::TryInto,
+  ffi::{c_void, CStr},
+};
+
+use lazycell::LazyCell;
+use log::{debug, error, trace, warn};
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::event::{HotKeyEvent, Key::*, MouseButton, MouseEvent};
+use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
+use crate::{event::Status::*, Source, SourceCallback};
+use crate::{event::Variant::*, hotkey::HotKey};
+
+const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1;
+const INPUT_EVENT_TYPE_MOUSE: i32 = 2;
+const INPUT_EVENT_TYPE_HOTKEY: i32 = 3;
+
+const INPUT_STATUS_PRESSED: i32 = 1;
+const INPUT_STATUS_RELEASED: i32 = 2;
+
+const INPUT_MOUSE_LEFT_BUTTON: i32 = 1;
+const INPUT_MOUSE_RIGHT_BUTTON: i32 = 3;
+const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 2;
+const INPUT_MOUSE_BUTTON_1: i32 = 9;
+const INPUT_MOUSE_BUTTON_2: i32 = 8;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawInputEvent {
+  pub event_type: i32,
+
+  pub buffer: [u8; 24],
+  pub buffer_len: i32,
+
+  pub key_sym: i32,
+  pub key_code: i32,
+  pub status: i32,
+  pub state: u32,
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+pub struct RawModifierIndexes {
+  pub ctrl: i32,
+  pub alt: i32,
+  pub shift: i32,
+  pub meta: i32,
+}
+
+#[repr(C)]
+pub struct RawHotKeyRequest {
+  pub key_sym: u32,
+  pub ctrl: i32,
+  pub alt: i32,
+  pub shift: i32,
+  pub meta: i32,
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct RawHotKeyResult {
+  pub success: i32,
+  pub key_code: i32,
+  pub state: u32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansodetect", kind = "static")]
+extern "C" {
+  pub fn detect_check_x11() -> i32;
+
+  pub fn detect_initialize(_self: *const X11Source, error_code: *mut i32) -> *mut c_void;
+
+  pub fn detect_get_modifier_indexes(context: *const c_void) -> RawModifierIndexes;
+
+  pub fn detect_register_hotkey(
+    context: *const c_void,
+    request: RawHotKeyRequest,
+    mod_indexes: RawModifierIndexes,
+  ) -> RawHotKeyResult;
+
+  pub fn detect_eventloop(
+    context: *const c_void,
+    event_callback: extern "C" fn(_self: *mut X11Source, event: RawInputEvent),
+  ) -> i32;
+
+  pub fn detect_destroy(context: *const c_void) -> i32;
+}
+
+pub struct X11Source {
+  handle: *mut c_void,
+  callback: LazyCell<SourceCallback>,
+  hotkeys: Vec<HotKey>,
+
+  raw_hotkey_mapping: HashMap<(i32, u32), i32>, // (key_code, state) -> hotkey ID
+  valid_modifiers_mask: u32,
+}
+
+#[allow(clippy::new_without_default)]
+impl X11Source {
+  pub fn new(hotkeys: &[HotKey]) -> X11Source {
+    Self {
+      handle: std::ptr::null_mut(),
+      callback: LazyCell::new(),
+      hotkeys: hotkeys.to_vec(),
+      raw_hotkey_mapping: HashMap::new(),
+      valid_modifiers_mask: 0,
+    }
+  }
+
+  pub fn is_compatible() -> bool {
+    unsafe { detect_check_x11() != 0 }
+  }
+}
+
+impl Source for X11Source {
+  fn initialize(&mut self) -> Result<()> {
+    let mut error_code = 0;
+    let handle = unsafe { detect_initialize(self as *const X11Source, &mut error_code) };
+
+    if handle.is_null() {
+      let error = match error_code {
+        -1 => X11SourceError::DisplayFailure(),
+        -2 => X11SourceError::XRecordMissing(),
+        -3 => X11SourceError::XKeyboardMissing(),
+        -4 => X11SourceError::FailedRegistration("cannot initialize record range".to_owned()),
+        -5 => X11SourceError::FailedRegistration("cannot initialize XRecord context".to_owned()),
+        -6 => X11SourceError::FailedRegistration("cannot enable XRecord context".to_owned()),
+        _ => X11SourceError::Unknown(),
+      };
+      return Err(error.into());
+    }
+
+    let mod_indexes = unsafe { detect_get_modifier_indexes(handle) };
+    self.valid_modifiers_mask |= 1 << mod_indexes.ctrl;
+    self.valid_modifiers_mask |= 1 << mod_indexes.alt;
+    self.valid_modifiers_mask |= 1 << mod_indexes.meta;
+    self.valid_modifiers_mask |= 1 << mod_indexes.shift;
+
+    // Register the hotkeys
+    let raw_hotkey_mapping = &mut self.raw_hotkey_mapping;
+    self.hotkeys.iter().for_each(|hk| {
+      let raw = convert_hotkey_to_raw(hk);
+      if let Some(raw_hk) = raw {
+        let result = unsafe { detect_register_hotkey(handle, raw_hk, mod_indexes) };
+        if result.success == 0 {
+          error!("unable to register hotkey: {}", hk);
+        } else {
+          raw_hotkey_mapping.insert((result.key_code, result.state), hk.id);
+          debug!("registered hotkey: {}", hk);
+        }
+      } else {
+        error!("unable to generate raw hotkey mapping: {}", hk);
+      }
+    });
+
+    self.handle = handle;
+
+    Ok(())
+  }
+
+  fn eventloop(&self, event_callback: SourceCallback) -> Result<()> {
+    if self.handle.is_null() {
+      error!("Attempt to start X11Source eventloop without initialization");
+      return Err(X11SourceError::Internal().into());
+    }
+
+    if self.callback.fill(event_callback).is_err() {
+      error!("Unable to set X11Source event callback");
+      return Err(X11SourceError::Internal().into());
+    }
+
+    extern "C" fn callback(_self: *mut X11Source, event: RawInputEvent) {
+      let source_self = unsafe { &*_self };
+      let event: Option<InputEvent> = convert_raw_input_event_to_input_event(
+        event,
+        &source_self.raw_hotkey_mapping,
+        source_self.valid_modifiers_mask,
+      );
+      if let Some(callback) = source_self.callback.borrow() {
+        if let Some(event) = event {
+          callback(event)
+        } else {
+          trace!("Unable to convert raw event to input event");
+        }
+      }
+    }
+
+    let error_code = unsafe { detect_eventloop(self.handle, callback) };
+
+    if error_code <= 0 {
+      error!("X11Source eventloop returned a negative error code");
+      return Err(X11SourceError::Internal().into());
+    }
+
+    Ok(())
+  }
+}
+
+impl Drop for X11Source {
+  fn drop(&mut self) {
+    if self.handle.is_null() {
+      error!("X11Source destruction cannot be performed, handle is null");
+      return;
+    }
+
+    let result = unsafe { detect_destroy(self.handle) };
+
+    if result != 0 {
+      error!("X11Source destruction returned non-zero code");
+    }
+  }
+}
+
+fn convert_hotkey_to_raw(hk: &HotKey) -> Option<RawHotKeyRequest> {
+  let key_sym = hk.key.to_code()?;
+  Some(RawHotKeyRequest {
+    key_sym,
+    ctrl: if hk.has_ctrl() { 1 } else { 0 },
+    alt: if hk.has_alt() { 1 } else { 0 },
+    shift: if hk.has_shift() { 1 } else { 0 },
+    meta: if hk.has_meta() { 1 } else { 0 },
+  })
+}
+
+#[derive(Error, Debug)]
+pub enum X11SourceError {
+  #[error("cannot open displays")]
+  DisplayFailure(),
+
+  #[error("X Record Extension is not installed")]
+  XRecordMissing(),
+
+  #[error("X Keyboard Extension is not installed")]
+  XKeyboardMissing(),
+
+  #[error("failed registration: ${0}")]
+  FailedRegistration(String),
+
+  #[error("unknown error")]
+  Unknown(),
+
+  #[error("internal error")]
+  Internal(),
+}
+
+fn convert_raw_input_event_to_input_event(
+  raw: RawInputEvent,
+  raw_hotkey_mapping: &HashMap<(i32, u32), i32>,
+  valid_modifiers_mask: u32,
+) -> Option<InputEvent> {
+  let status = match raw.status {
+    INPUT_STATUS_RELEASED => Released,
+    INPUT_STATUS_PRESSED => Pressed,
+    _ => Pressed,
+  };
+
+  match raw.event_type {
+    // Keyboard events
+    INPUT_EVENT_TYPE_KEYBOARD => {
+      let (key, variant) = key_sym_to_key(raw.key_sym);
+      let value = if raw.buffer_len > 0 {
+        let raw_string_result =
+          CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]);
+        match raw_string_result {
+          Ok(c_string) => {
+            let string_result = c_string.to_str();
+            match string_result {
+              Ok(value) => Some(value.to_string()),
+              Err(err) => {
+                warn!("char conversion error: {}", err);
+                None
+              }
+            }
+          }
+          Err(err) => {
+            warn!("Received malformed char: {}", err);
+            None
+          }
+        }
+      } else {
+        None
+      };
+
+      return Some(InputEvent::Keyboard(KeyboardEvent {
+        key,
+        value,
+        status,
+        variant,
+        code: raw
+          .key_code
+          .try_into()
+          .expect("invalid keycode conversion to u32"),
+      }));
+    }
+    // Mouse events
+    INPUT_EVENT_TYPE_MOUSE => {
+      let button = raw_to_mouse_button(raw.key_code);
+
+      if let Some(button) = button {
+        return Some(InputEvent::Mouse(MouseEvent { button, status }));
+      }
+    }
+    // Hotkey events
+    INPUT_EVENT_TYPE_HOTKEY => {
+      let state = raw.state & valid_modifiers_mask;
+      if let Some(id) = raw_hotkey_mapping.get(&(raw.key_code, state)) {
+        return Some(InputEvent::HotKey(HotKeyEvent { hotkey_id: *id }));
+      }
+    }
+    _ => {}
+  }
+
+  None
+}
+
+// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+// TODO: might need to add also the variants
+fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
+  match key_sym {
+    // Modifiers
+    0xFFE9 => (Alt, Some(Left)),
+    0xFFEA => (Alt, Some(Right)),
+    0xFFE5 => (CapsLock, None),
+    0xFFE3 => (Control, Some(Left)),
+    0xFFE4 => (Control, Some(Right)),
+    0xFFE7 | 0xFFEB => (Meta, Some(Left)),
+    0xFFE8 | 0xFFEC => (Meta, Some(Right)),
+    0xFF7F => (NumLock, None),
+    0xFFE1 => (Shift, Some(Left)),
+    0xFFE2 => (Shift, Some(Right)),
+
+    // Whitespace
+    0xFF0D => (Enter, None),
+    0xFF09 => (Tab, None),
+    0x20 => (Space, None),
+
+    // Navigation
+    0xFF54 => (ArrowDown, None),
+    0xFF51 => (ArrowLeft, None),
+    0xFF53 => (ArrowRight, None),
+    0xFF52 => (ArrowUp, None),
+    0xFF57 => (End, None),
+    0xFF50 => (Home, None),
+    0xFF56 => (PageDown, None),
+    0xFF55 => (PageUp, None),
+
+    // UI keys
+    0xFF1B => (Escape, None),
+
+    // Editing keys
+    0xFF08 => (Backspace, None),
+
+    // Function keys
+    0xFFBE => (F1, None),
+    0xFFBF => (F2, None),
+    0xFFC0 => (F3, None),
+    0xFFC1 => (F4, None),
+    0xFFC2 => (F5, None),
+    0xFFC3 => (F6, None),
+    0xFFC4 => (F7, None),
+    0xFFC5 => (F8, None),
+    0xFFC6 => (F9, None),
+    0xFFC7 => (F10, None),
+    0xFFC8 => (F11, None),
+    0xFFC9 => (F12, None),
+    0xFFCA => (F13, None),
+    0xFFCB => (F14, None),
+    0xFFCC => (F15, None),
+    0xFFCD => (F16, None),
+    0xFFCE => (F17, None),
+    0xFFCF => (F18, None),
+    0xFFD0 => (F19, None),
+    0xFFD1 => (F20, None),
+
+    // Other keys, includes the raw code provided by the operating system
+    _ => (Other(key_sym), None),
+  }
+}
+
+fn raw_to_mouse_button(raw: i32) -> Option<MouseButton> {
+  match raw {
+    INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left),
+    INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right),
+    INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle),
+    INPUT_MOUSE_BUTTON_1 => Some(MouseButton::Button1),
+    INPUT_MOUSE_BUTTON_2 => Some(MouseButton::Button2),
+    _ => None,
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use std::ffi::CString;
+
+  use super::*;
+
+  fn default_raw_input_event() -> RawInputEvent {
+    RawInputEvent {
+      event_type: INPUT_EVENT_TYPE_KEYBOARD,
+      buffer: [0; 24],
+      buffer_len: 0,
+      key_code: 0,
+      key_sym: 0,
+      status: INPUT_STATUS_PRESSED,
+      state: 0,
+    }
+  }
+
+  #[test]
+  fn raw_to_input_event_keyboard_works_correctly() {
+    let c_string = CString::new("k".to_string()).unwrap();
+    let mut buffer: [u8; 24] = [0; 24];
+    buffer[..1].copy_from_slice(c_string.as_bytes());
+
+    let mut raw = default_raw_input_event();
+    raw.buffer = buffer;
+    raw.buffer_len = 1;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.key_sym = 0x4B;
+    raw.key_code = 1;
+
+    let result: Option<InputEvent> =
+      convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Keyboard(KeyboardEvent {
+        key: Other(0x4B),
+        status: Released,
+        value: Some("k".to_string()),
+        variant: None,
+        code: 1,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_event_mouse_works_correctly() {
+    let mut raw = default_raw_input_event();
+    raw.event_type = INPUT_EVENT_TYPE_MOUSE;
+    raw.status = INPUT_STATUS_RELEASED;
+    raw.key_code = INPUT_MOUSE_RIGHT_BUTTON;
+
+    let result: Option<InputEvent> =
+      convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::Mouse(MouseEvent {
+        status: Released,
+        button: MouseButton::Right,
+      })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_event_hotkey_works_correctly() {
+    let mut raw = default_raw_input_event();
+    raw.event_type = INPUT_EVENT_TYPE_HOTKEY;
+    raw.state = 0b00000011;
+    raw.key_code = 10;
+
+    let mut raw_hotkey_mapping = HashMap::new();
+    raw_hotkey_mapping.insert((10, 1), 20);
+
+    let result: Option<InputEvent> =
+      convert_raw_input_event_to_input_event(raw, &raw_hotkey_mapping, 1);
+    assert_eq!(
+      result.unwrap(),
+      InputEvent::HotKey(HotKeyEvent { hotkey_id: 20 })
+    );
+  }
+
+  #[test]
+  fn raw_to_input_invalid_buffer() {
+    let buffer: [u8; 24] = [123; 24];
+
+    let mut raw = default_raw_input_event();
+    raw.buffer = buffer;
+    raw.buffer_len = 5;
+
+    let result: Option<InputEvent> =
+      convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
+    assert!(result.unwrap().into_keyboard().unwrap().value.is_none());
+  }
+
+  #[test]
+  fn raw_to_input_event_returns_none_when_missing_type() {
+    let mut raw = default_raw_input_event();
+    raw.event_type = 0;
+    let result: Option<InputEvent> =
+      convert_raw_input_event_to_input_event(raw, &HashMap::new(), 0);
+    assert!(result.is_none());
+  }
+}
diff --git a/espanso-detect/src/x11/native.cpp b/espanso-detect/src/x11/native.cpp
new file mode 100644
index 00000000..838ab8c1
--- /dev/null
+++ b/espanso-detect/src/x11/native.cpp
@@ -0,0 +1,445 @@
+/*
+ * 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/>.
+ */
+
+/*
+This code uses the X11 Record Extension to receive keyboard
+events. Documentation of this library can be found here:
+https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html
+We will refer to this extension as RE from now on.
+*/
+
+#include "native.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <array>
+#include <string.h>
+#include <memory>
+#include <iostream>
+
+#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>
+
+/*
+This struct is needed to receive events from the RE.
+The funny thing is: it's not defined there, it should though.
+The only place this is mentioned is the libxnee library,
+so check that out if you need a reference.
+*/
+typedef union
+{
+  unsigned char type;
+  xEvent event;
+  xResourceReq req;
+  xGenericReply reply;
+  xError error;
+  xConnSetupPrefix setup;
+} XRecordDatum;
+
+typedef struct
+{
+  // Connections to the X server, RE recommends 2 connections:
+  // one for recording control and one for reading the recorded data.
+  Display *data_disp;
+  Display *ctrl_disp;
+  XRecordRange *record_range;
+  XRecordContext x_context;
+
+  void *rust_instance;
+  EventCallback event_callback;
+} DetectContext;
+
+void detect_event_callback(XPointer, XRecordInterceptData *);
+int detect_error_callback(Display *display, XErrorEvent *error);
+
+int32_t detect_check_x11()
+{
+  Display *check_disp = XOpenDisplay(NULL);
+
+  if (!check_disp)
+  {
+    return 0;
+  }
+
+  XCloseDisplay(check_disp);
+  return 1;
+}
+
+void *detect_initialize(void *_rust_instance, int32_t *error_code)
+{
+  setlocale(LC_ALL, "");
+
+  std::unique_ptr<DetectContext> context;
+  context.reset(new DetectContext());
+  context->rust_instance = _rust_instance;
+
+  // Open the connections to the X server.
+  // RE recommends to open 2 connections to the X server:
+  // one for the recording control and one to read the protocol
+  // data.
+  context->ctrl_disp = XOpenDisplay(NULL);
+  context->data_disp = XOpenDisplay(NULL);
+
+  if (!context->ctrl_disp || !context->data_disp)
+  { // Display error
+    *error_code = -1;
+    return nullptr;
+  }
+
+  // We must set the ctrl_disp to sync mode, or, when we the enable
+  // context in data_disp, there will be a fatal X error.
+  XSynchronize(context->ctrl_disp, True);
+
+  int dummy;
+
+  // Make sure the X RE is installed in this system.
+  if (!XRecordQueryVersion(context->ctrl_disp, &dummy, &dummy))
+  {
+    *error_code = -2;
+    return nullptr;
+  }
+
+  // Make sure the X Keyboard Extension is installed
+  if (!XkbQueryExtension(context->ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy))
+  {
+    *error_code = -3;
+    return nullptr;
+  }
+
+  // Initialize the record range, that is the kind of events we want to track.
+  context->record_range = XRecordAllocRange();
+  if (!context->record_range)
+  {
+    *error_code = -4;
+    return nullptr;
+  }
+  context->record_range->device_events.first = KeyPress;
+  context->record_range->device_events.last = ButtonRelease;
+
+  // We want to get the keys from all clients
+  XRecordClientSpec client_spec;
+  client_spec = XRecordAllClients;
+
+  // Initialize the context
+  context->x_context = XRecordCreateContext(context->ctrl_disp, 0, &client_spec, 1, &context->record_range, 1);
+  if (!context->x_context)
+  {
+    *error_code = -5;
+    return nullptr;
+  }
+
+  if (!XRecordEnableContextAsync(context->data_disp, context->x_context, detect_event_callback, (XPointer)context.get()))
+  {
+    *error_code = -6;
+    return nullptr;
+  }
+
+  // Setup a custom error handler
+  XSetErrorHandler(&detect_error_callback);
+
+  // Note: We might never get a MappingNotify event if the
+  // modifier and keymap information was never cached in Xlib.
+  // The next line makes sure that this happens initially.
+  XKeysymToKeycode(context->ctrl_disp, XK_F1);
+
+  // Release the unique_ptr to avoid freeing the context right-away.
+  return context.release();
+}
+
+ModifierIndexes detect_get_modifier_indexes(void *_context) {
+  DetectContext *context = (DetectContext *)_context;
+  XModifierKeymap *map = XGetModifierMapping(context->ctrl_disp);
+
+  ModifierIndexes indexes = {};
+
+  for (int i = 0; i<8; i++) {
+    if (map->max_keypermod > 0) {
+      int code = map->modifiermap[i * map->max_keypermod];
+      KeySym sym = XkbKeycodeToKeysym(context->ctrl_disp, code, 0, 0);
+      if (sym == XK_Control_L || sym == XK_Control_R) {
+        indexes.ctrl = i;
+      } else if (sym == XK_Super_L || sym == XK_Super_R) {
+        indexes.meta = i;
+      } else if (sym == XK_Shift_L || sym == XK_Shift_R) {
+        indexes.shift = i;
+      } else if (sym == XK_Alt_L || sym == XK_Alt_R) {
+        indexes.alt = i;
+      }
+    }
+  }
+
+  XFreeModifiermap(map);
+
+  return indexes;
+}
+
+HotKeyResult detect_register_hotkey(void *_context, HotKeyRequest request, ModifierIndexes mod_indexes) {
+  DetectContext *context = (DetectContext *)_context;
+  KeyCode key_code = XKeysymToKeycode(context->ctrl_disp, request.key_sym);
+
+  HotKeyResult result = {};
+  if (key_code == 0) {
+    return result;
+  }
+
+  uint32_t valid_modifiers = 0;
+  valid_modifiers |= 1 << mod_indexes.alt;
+  valid_modifiers |= 1 << mod_indexes.ctrl;
+  valid_modifiers |= 1 << mod_indexes.shift;
+  valid_modifiers |= 1 << mod_indexes.meta;
+
+  uint32_t target_modifiers = 0;
+  if (request.ctrl) {
+    target_modifiers |= 1 << mod_indexes.ctrl;
+  }
+  if (request.alt) {
+    target_modifiers |= 1 << mod_indexes.alt;
+  }
+  if (request.shift) {
+    target_modifiers |= 1 << mod_indexes.shift;
+  }
+  if (request.meta) {
+    target_modifiers |= 1 << mod_indexes.meta;
+  }
+
+  result.state = target_modifiers;
+  result.key_code = key_code;
+  result.success = 1;
+
+  Window root = DefaultRootWindow(context->ctrl_disp);
+
+  // We need to register an hotkey for all combinations of "useless" modifiers,
+  // such as the NumLock, as the XGrabKey method wants an exact match.
+  for (uint state = 0; state<256; state++) {
+    // Check if the current state includes a "useless modifier" but none of the valid ones
+    if ((state == 0 || (state & ~valid_modifiers) != 0) && (state & valid_modifiers) == 0) {
+      uint final_modifiers = state | target_modifiers;
+      
+      int res = XGrabKey(context->ctrl_disp, key_code, final_modifiers, root, False, GrabModeAsync, GrabModeAsync);
+      if (res == BadAccess || res == BadValue) {
+        result.success = 0;
+      }
+    }
+  }
+
+  return result;
+}
+
+int32_t detect_eventloop(void *_context, EventCallback _callback)
+{
+  DetectContext *context = (DetectContext *)_context;
+  if (!context)
+  {
+    return -1;
+  }
+  context->event_callback = _callback;
+
+  bool running = true;
+
+  int ctrl_fd = XConnectionNumber(context->ctrl_disp);
+  int data_fd = XConnectionNumber(context->data_disp);
+
+  while (running)
+  {
+    fd_set fds;
+    FD_ZERO(&fds);
+    FD_SET(ctrl_fd, &fds);
+    FD_SET(data_fd, &fds);
+    timeval timeout;
+    timeout.tv_sec = 2;
+    timeout.tv_usec = 0;
+    int ret = select(max(ctrl_fd, data_fd) + 1,
+                        &fds, NULL, NULL, &timeout);
+    if (ret < 0) {
+      return -2;
+    }
+
+    if (FD_ISSET(data_fd, &fds))
+    {
+      XRecordProcessReplies(context->data_disp);
+
+      // On certain occasions (such as when a pointer remap occurs), some
+      // events might get stuck in the queue. If we don't handle them,
+      // this loop could get out of control, consuming 100% CPU.
+      while (XEventsQueued(context->data_disp, QueuedAlready) > 0) {
+        XEvent event;
+        XNextEvent(context->data_disp, &event);
+      }
+    }
+    if (FD_ISSET(ctrl_fd, &fds))
+    {
+      XEvent event;
+      XNextEvent(context->ctrl_disp, &event);
+      if (event.type == MappingNotify)
+      {
+        XMappingEvent *e = (XMappingEvent *)&event;
+        if (e->request == MappingKeyboard)
+        {
+          XRefreshKeyboardMapping(e);
+        }
+      } else if (event.type == KeyPress) {
+        InputEvent inputEvent = {};
+        inputEvent.event_type = INPUT_EVENT_TYPE_HOTKEY;
+        inputEvent.key_code = event.xkey.keycode;
+        inputEvent.state = event.xkey.state;
+        if (context->event_callback)
+        {
+          context->event_callback(context->rust_instance, inputEvent);
+        }
+      }
+    }
+  }
+
+  return 1;
+}
+
+int32_t detect_destroy(void *_context)
+{
+  DetectContext *context = (DetectContext *)_context;
+  if (!context)
+  {
+    return -1;
+  }
+
+  XRecordDisableContext(context->ctrl_disp, context->x_context);
+  XRecordFreeContext(context->ctrl_disp, context->x_context);
+  XFree(context->record_range);
+  XCloseDisplay(context->data_disp);
+  XCloseDisplay(context->ctrl_disp);
+  delete context;
+
+  return 1;
+}
+
+void detect_event_callback(XPointer p, XRecordInterceptData *hook)
+{
+  DetectContext *context = (DetectContext *)p;
+  if (!context)
+  {
+    return;
+  }
+
+  // Make sure the event comes from the X11 server
+  if (hook->category != XRecordFromServer)
+  {
+    XRecordFreeData(hook);
+    return;
+  }
+
+  // Cast the event payload to a XRecordDatum, needed later to access the fields
+  // This struct was hard to find and understand. Turn's out that all the
+  // required data are included in the "event" field of this structure.
+  // The funny thing is that it's not a XEvent as one might expect,
+  // but a xEvent, a very different beast defined in the Xproto.h header.
+  // I suggest you to look at that header if you want to understand where the
+  // upcoming field where taken from.
+  XRecordDatum *data = (XRecordDatum *)hook->data;
+
+  int event_type = data->type;
+  int key_code = data->event.u.u.detail;
+
+  // In order to convert the key_code into the corresponding string,
+  // we need to synthesize an artificial XKeyEvent, to feed later to the
+  // XLookupString function.
+  XKeyEvent raw_event;
+  raw_event.display = context->ctrl_disp;
+  raw_event.window = data->event.u.focus.window;
+  raw_event.root = XDefaultRootWindow(context->ctrl_disp);
+  raw_event.subwindow = None;
+  raw_event.time = data->event.u.keyButtonPointer.time;
+  raw_event.x = 1;
+  raw_event.y = 1;
+  raw_event.x_root = 1;
+  raw_event.y_root = 1;
+  raw_event.same_screen = True;
+  raw_event.keycode = key_code;
+  raw_event.state = data->event.u.keyButtonPointer.state;
+  raw_event.type = event_type;
+
+  InputEvent event = {};
+
+  // Extract the corresponding chars.
+  int res = XLookupString(&raw_event, event.buffer, sizeof(event.buffer) - 1, NULL, NULL);
+  if (res > 0)
+  {
+    event.buffer_len = res;
+  }
+  else
+  {
+    memset(event.buffer, 0, sizeof(event.buffer));
+    event.buffer_len = 0;
+  }
+  KeySym key_sym = XLookupKeysym(&raw_event, 0);
+
+  switch (event_type)
+  {
+  case KeyPress:
+  {
+    event.event_type = INPUT_EVENT_TYPE_KEYBOARD;
+    event.key_code = key_code;
+    event.key_sym = key_sym;
+    event.status = INPUT_STATUS_PRESSED;
+    break;
+  }
+  case KeyRelease:
+  {
+    event.event_type = INPUT_EVENT_TYPE_KEYBOARD;
+    event.key_code = key_code;
+    event.key_sym = key_sym;
+    event.status = INPUT_STATUS_RELEASED;
+    break;
+  }
+  case ButtonPress:
+  {
+    event.event_type = INPUT_EVENT_TYPE_MOUSE;
+    event.key_code = key_code;
+    event.status = INPUT_STATUS_PRESSED;
+    break;
+  }
+  case ButtonRelease:
+  {
+    event.event_type = INPUT_EVENT_TYPE_MOUSE;
+    event.key_code = key_code;
+    event.status = INPUT_STATUS_RELEASED;
+    break;
+  }
+  }
+
+  if (event.event_type != 0 && context->event_callback)
+  {
+    context->event_callback(context->rust_instance, event);
+  }
+
+  XRecordFreeData(hook);
+}
+
+int detect_error_callback(Display *, XErrorEvent *error)
+{
+  fprintf(stderr, "X11 Reported an error, code: %d, request_code: %d, minor_code: %d\n", error->error_code, error->request_code, error->minor_code);
+  return 0;
+}
\ No newline at end of file
diff --git a/espanso-detect/src/x11/native.h b/espanso-detect/src/x11/native.h
new file mode 100644
index 00000000..5cb35b40
--- /dev/null
+++ b/espanso-detect/src/x11/native.h
@@ -0,0 +1,97 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_DETECT_H
+#define ESPANSO_DETECT_H
+
+#include <stdint.h>
+
+#define INPUT_EVENT_TYPE_KEYBOARD 1
+#define INPUT_EVENT_TYPE_MOUSE 2
+#define INPUT_EVENT_TYPE_HOTKEY 3
+
+#define INPUT_STATUS_PRESSED 1
+#define INPUT_STATUS_RELEASED 2
+
+typedef struct
+{
+  // Keyboard, Mouse or Hotkey event
+  int32_t event_type;
+
+  // Contains the string corresponding to the key, if any
+  char buffer[24];
+  // Length of the extracted string. Equals 0 if no string is extracted
+  int32_t buffer_len;
+
+  // Code of the pressed key.
+  int32_t key_sym;
+
+  // Virtual key code of the pressed key in case of keyboard events
+  // Mouse button code for mouse events.
+  int32_t key_code;
+
+  // Pressed or Released status
+  int32_t status;
+
+  // Keycode state (modifiers) in a Hotkey event
+  uint32_t state;
+} InputEvent;
+
+typedef struct {
+  int32_t key_sym;
+  int32_t ctrl;
+  int32_t alt;
+  int32_t shift;
+  int32_t meta;
+} HotKeyRequest;
+
+typedef struct {
+  int32_t success;
+  int32_t key_code;
+  uint32_t state;
+} HotKeyResult;
+
+typedef struct {
+  int32_t ctrl;
+  int32_t alt;
+  int32_t shift;
+  int32_t meta;
+} ModifierIndexes;
+
+typedef void (*EventCallback)(void *rust_istance, InputEvent data);
+
+// Check if a X11 context is available, returning a non-zero code if true.
+extern "C" int32_t detect_check_x11();
+
+// Initialize the XRecord API and return the context pointer
+extern "C" void *detect_initialize(void *rust_istance, int32_t *error_code);
+
+// Get the modifiers indexes in the field mask
+extern "C" ModifierIndexes detect_get_modifier_indexes(void *context);
+
+// Register the given hotkey
+extern "C" HotKeyResult detect_register_hotkey(void *context, HotKeyRequest request, ModifierIndexes mod_indexes);
+
+// Run the event loop. Blocking call.
+extern "C" int32_t detect_eventloop(void *context, EventCallback callback);
+
+// Unregister from the XRecord API and destroy the context.
+extern "C" int32_t detect_destroy(void *context);
+
+#endif //ESPANSO_DETECT_H
\ No newline at end of file
diff --git a/espanso-engine/Cargo.toml b/espanso-engine/Cargo.toml
new file mode 100644
index 00000000..0ad0db86
--- /dev/null
+++ b/espanso-engine/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "espanso-engine"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+crossbeam = "0.8.0"
+markdown = "0.3.0"
+html2text = "0.2.1"
+
+
+[dev-dependencies]
+tempdir = "0.3.7"
\ No newline at end of file
diff --git a/espanso-engine/src/dispatch/default.rs b/espanso-engine/src/dispatch/default.rs
new file mode 100644
index 00000000..bb14a77a
--- /dev/null
+++ b/espanso-engine/src/dispatch/default.rs
@@ -0,0 +1,78 @@
+/*
+ * 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 super::{ContextMenuHandler, Event, IconHandler, ImageInjector, SecureInputManager};
+use super::{Dispatcher, Executor, HtmlInjector, KeyInjector, ModeProvider, TextInjector};
+
+pub struct DefaultDispatcher<'a> {
+  executors: Vec<Box<dyn Executor + 'a>>,
+}
+
+#[allow(clippy::too_many_arguments)]
+impl<'a> DefaultDispatcher<'a> {
+  pub fn new(
+    event_injector: &'a dyn TextInjector,
+    clipboard_injector: &'a dyn TextInjector,
+    mode_provider: &'a dyn ModeProvider,
+    key_injector: &'a dyn KeyInjector,
+    html_injector: &'a dyn HtmlInjector,
+    image_injector: &'a dyn ImageInjector,
+    context_menu_handler: &'a dyn ContextMenuHandler,
+    icon_handler: &'a dyn IconHandler,
+    secure_input_manager: &'a dyn SecureInputManager,
+  ) -> Self {
+    Self {
+      executors: vec![
+        Box::new(super::executor::text_inject::TextInjectExecutor::new(
+          event_injector,
+          clipboard_injector,
+          mode_provider,
+        )),
+        Box::new(super::executor::key_inject::KeyInjectExecutor::new(
+          key_injector,
+        )),
+        Box::new(super::executor::html_inject::HtmlInjectExecutor::new(
+          html_injector,
+        )),
+        Box::new(super::executor::image_inject::ImageInjectExecutor::new(
+          image_injector,
+        )),
+        Box::new(super::executor::context_menu::ContextMenuExecutor::new(
+          context_menu_handler,
+        )),
+        Box::new(super::executor::icon_update::IconUpdateExecutor::new(
+          icon_handler,
+        )),
+        Box::new(super::executor::secure_input::SecureInputExecutor::new(
+          secure_input_manager,
+        )),
+      ],
+    }
+  }
+}
+
+impl<'a> Dispatcher for DefaultDispatcher<'a> {
+  fn dispatch(&self, event: Event) {
+    for executor in self.executors.iter() {
+      if executor.execute(&event) {
+        break;
+      }
+    }
+  }
+}
diff --git a/espanso-engine/src/dispatch/executor/context_menu.rs b/espanso-engine/src/dispatch/executor/context_menu.rs
new file mode 100644
index 00000000..5c94ec1c
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/context_menu.rs
@@ -0,0 +1,53 @@
+/*
+ * 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 crate::event::{ui::MenuItem, EventType};
+use crate::{dispatch::Executor, event::Event};
+use anyhow::Result;
+use log::error;
+
+pub trait ContextMenuHandler {
+  fn show_context_menu(&self, items: &[MenuItem]) -> Result<()>;
+}
+
+pub struct ContextMenuExecutor<'a> {
+  handler: &'a dyn ContextMenuHandler,
+}
+
+impl<'a> ContextMenuExecutor<'a> {
+  pub fn new(handler: &'a dyn ContextMenuHandler) -> Self {
+    Self { handler }
+  }
+}
+
+impl<'a> Executor for ContextMenuExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::ShowContextMenu(context_menu_event) = &event.etype {
+      if let Err(error) = self.handler.show_context_menu(&context_menu_event.items) {
+        error!("context menu handler reported an error: {:?}", error);
+      }
+
+      return true;
+    }
+
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/dispatch/executor/html_inject.rs b/espanso-engine/src/dispatch/executor/html_inject.rs
new file mode 100644
index 00000000..d7207c0b
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/html_inject.rs
@@ -0,0 +1,64 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::error;
+
+use crate::{
+  dispatch::Executor,
+  event::{Event, EventType},
+};
+
+pub trait HtmlInjector {
+  fn inject_html(&self, html: &str, fallback: &str) -> Result<()>;
+}
+
+pub struct HtmlInjectExecutor<'a> {
+  injector: &'a dyn HtmlInjector,
+}
+
+impl<'a> HtmlInjectExecutor<'a> {
+  pub fn new(injector: &'a dyn HtmlInjector) -> Self {
+    Self { injector }
+  }
+}
+
+impl<'a> Executor for HtmlInjectExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::HtmlInject(inject_event) = &event.etype {
+      // Render the text fallback for those applications that don't support HTML clipboard
+      let decorator = html2text::render::text_renderer::TrivialDecorator::new();
+      let fallback_text =
+        html2text::from_read_with_decorator(inject_event.html.as_bytes(), 1000000, decorator);
+
+      if let Err(error) = self
+        .injector
+        .inject_html(&inject_event.html, &fallback_text)
+      {
+        error!("html injector reported an error: {:?}", error);
+      }
+
+      return true;
+    }
+
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/dispatch/executor/icon_update.rs b/espanso-engine/src/dispatch/executor/icon_update.rs
new file mode 100644
index 00000000..7a8d6f7a
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/icon_update.rs
@@ -0,0 +1,56 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::error;
+
+use crate::{
+  dispatch::Executor,
+  event::{ui::IconStatus, Event, EventType},
+};
+
+pub trait IconHandler {
+  fn update_icon(&self, status: &IconStatus) -> Result<()>;
+}
+
+pub struct IconUpdateExecutor<'a> {
+  handler: &'a dyn IconHandler,
+}
+
+impl<'a> IconUpdateExecutor<'a> {
+  pub fn new(handler: &'a dyn IconHandler) -> Self {
+    Self { handler }
+  }
+}
+
+impl<'a> Executor for IconUpdateExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::IconStatusChange(m_event) = &event.etype {
+      if let Err(error) = self.handler.update_icon(&m_event.status) {
+        error!("icon handler reported an error: {:?}", error);
+      }
+
+      return true;
+    }
+
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/dispatch/executor/image_inject.rs b/espanso-engine/src/dispatch/executor/image_inject.rs
new file mode 100644
index 00000000..df3daa52
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/image_inject.rs
@@ -0,0 +1,56 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::error;
+
+use crate::{
+  dispatch::Executor,
+  event::{Event, EventType},
+};
+
+pub trait ImageInjector {
+  fn inject_image(&self, path: &str) -> Result<()>;
+}
+
+pub struct ImageInjectExecutor<'a> {
+  injector: &'a dyn ImageInjector,
+}
+
+impl<'a> ImageInjectExecutor<'a> {
+  pub fn new(injector: &'a dyn ImageInjector) -> Self {
+    Self { injector }
+  }
+}
+
+impl<'a> Executor for ImageInjectExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::ImageInject(inject_event) = &event.etype {
+      if let Err(error) = self.injector.inject_image(&inject_event.image_path) {
+        error!("image injector reported an error: {:?}", error);
+      }
+
+      return true;
+    }
+
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/dispatch/executor/key_inject.rs b/espanso-engine/src/dispatch/executor/key_inject.rs
new file mode 100644
index 00000000..e2c8d741
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/key_inject.rs
@@ -0,0 +1,52 @@
+/*
+ * 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 crate::{
+  dispatch::Executor,
+  event::{input::Key, Event, EventType},
+};
+use anyhow::Result;
+use log::error;
+
+pub trait KeyInjector {
+  fn inject_sequence(&self, keys: &[Key]) -> Result<()>;
+}
+
+pub struct KeyInjectExecutor<'a> {
+  injector: &'a dyn KeyInjector,
+}
+
+impl<'a> KeyInjectExecutor<'a> {
+  pub fn new(injector: &'a dyn KeyInjector) -> Self {
+    Self { injector }
+  }
+}
+
+impl<'a> Executor for KeyInjectExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::KeySequenceInject(inject_event) = &event.etype {
+      if let Err(error) = self.injector.inject_sequence(&inject_event.keys) {
+        error!("key injector reported an error: {}", error);
+      }
+      return true;
+    }
+
+    false
+  }
+}
diff --git a/espanso-engine/src/dispatch/executor/mod.rs b/espanso-engine/src/dispatch/executor/mod.rs
new file mode 100644
index 00000000..31bb6d0b
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/mod.rs
@@ -0,0 +1,26 @@
+/*
+ * 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/>.
+ */
+
+pub mod context_menu;
+pub mod html_inject;
+pub mod icon_update;
+pub mod image_inject;
+pub mod key_inject;
+pub mod secure_input;
+pub mod text_inject;
diff --git a/espanso-engine/src/dispatch/executor/secure_input.rs b/espanso-engine/src/dispatch/executor/secure_input.rs
new file mode 100644
index 00000000..17481aba
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/secure_input.rs
@@ -0,0 +1,59 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::error;
+
+use crate::{
+  dispatch::Executor,
+  event::{Event, EventType},
+};
+
+pub trait SecureInputManager {
+  fn display_secure_input_troubleshoot(&self) -> Result<()>;
+  fn launch_secure_input_autofix(&self) -> Result<()>;
+}
+
+pub struct SecureInputExecutor<'a> {
+  manager: &'a dyn SecureInputManager,
+}
+
+impl<'a> SecureInputExecutor<'a> {
+  pub fn new(manager: &'a dyn SecureInputManager) -> Self {
+    Self { manager }
+  }
+}
+
+impl<'a> Executor for SecureInputExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::DisplaySecureInputTroubleshoot = &event.etype {
+      if let Err(error) = self.manager.display_secure_input_troubleshoot() {
+        error!("unable to display secure input troubleshoot: {}", error);
+      }
+      return true;
+    } else if let EventType::LaunchSecureInputAutoFix = &event.etype {
+      if let Err(error) = self.manager.launch_secure_input_autofix() {
+        error!("unable to launch secure input autofix: {}", error);
+      }
+      return true;
+    }
+
+    false
+  }
+}
diff --git a/espanso-engine/src/dispatch/executor/text_inject.rs b/espanso-engine/src/dispatch/executor/text_inject.rs
new file mode 100644
index 00000000..f2eb9fb7
--- /dev/null
+++ b/espanso-engine/src/dispatch/executor/text_inject.rs
@@ -0,0 +1,117 @@
+/*
+ * 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 crate::{
+  dispatch::Executor,
+  event::{effect::TextInjectMode, Event, EventType},
+};
+use anyhow::Result;
+use log::{error, trace};
+
+pub trait TextInjector {
+  fn name(&self) -> &'static str;
+  fn inject_text(&self, text: &str) -> Result<()>;
+}
+
+pub trait ModeProvider {
+  fn active_mode(&self) -> Mode;
+}
+
+pub enum Mode {
+  Event,
+  Clipboard,
+  Auto {
+    // Maximum size after which the clipboard backend
+    // is used over the event one to speed up the injection.
+    clipboard_threshold: usize,
+  },
+}
+
+pub struct TextInjectExecutor<'a> {
+  event_injector: &'a dyn TextInjector,
+  clipboard_injector: &'a dyn TextInjector,
+  mode_provider: &'a dyn ModeProvider,
+}
+
+impl<'a> TextInjectExecutor<'a> {
+  pub fn new(
+    event_injector: &'a dyn TextInjector,
+    clipboard_injector: &'a dyn TextInjector,
+    mode_provider: &'a dyn ModeProvider,
+  ) -> Self {
+    Self {
+      event_injector,
+      clipboard_injector,
+      mode_provider,
+    }
+  }
+}
+
+impl<'a> Executor for TextInjectExecutor<'a> {
+  fn execute(&self, event: &Event) -> bool {
+    if let EventType::TextInject(inject_event) = &event.etype {
+      let active_mode = self.mode_provider.active_mode();
+
+      let injector = if let Some(force_mode) = &inject_event.force_mode {
+        if let TextInjectMode::Keys = force_mode {
+          self.event_injector
+        } else {
+          self.clipboard_injector
+        }
+      } else if let Mode::Clipboard = active_mode {
+        self.clipboard_injector
+      } else if let Mode::Event = active_mode {
+        self.event_injector
+      } else if let Mode::Auto {
+        clipboard_threshold,
+      } = active_mode
+      {
+        if inject_event.text.chars().count() > clipboard_threshold {
+          self.clipboard_injector
+        } else if cfg!(target_os = "linux") {
+          if inject_event.text.chars().all(|c| c.is_ascii()) {
+            self.event_injector
+          } else {
+            self.clipboard_injector
+          }
+        } else {
+          self.event_injector
+        }
+      } else {
+        self.event_injector
+      };
+
+      trace!("using injector: {}", injector.name());
+
+      if let Err(error) = injector.inject_text(&inject_event.text) {
+        error!(
+          "text injector ({}) reported an error: {:?}",
+          injector.name(),
+          error
+        );
+      }
+
+      return true;
+    }
+
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/dispatch/mod.rs b/espanso-engine/src/dispatch/mod.rs
new file mode 100644
index 00000000..d2959fff
--- /dev/null
+++ b/espanso-engine/src/dispatch/mod.rs
@@ -0,0 +1,65 @@
+/*
+ * 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 crate::event::Event;
+
+mod default;
+mod executor;
+
+pub trait Executor {
+  fn execute(&self, event: &Event) -> bool;
+}
+
+pub trait Dispatcher {
+  fn dispatch(&self, event: Event);
+}
+
+// Re-export dependency injection entities
+pub use executor::context_menu::ContextMenuHandler;
+pub use executor::html_inject::HtmlInjector;
+pub use executor::icon_update::IconHandler;
+pub use executor::image_inject::ImageInjector;
+pub use executor::key_inject::KeyInjector;
+pub use executor::secure_input::SecureInputManager;
+pub use executor::text_inject::{Mode, ModeProvider, TextInjector};
+
+#[allow(clippy::too_many_arguments)]
+pub fn default<'a>(
+  event_injector: &'a dyn TextInjector,
+  clipboard_injector: &'a dyn TextInjector,
+  mode_provider: &'a dyn ModeProvider,
+  key_injector: &'a dyn KeyInjector,
+  html_injector: &'a dyn HtmlInjector,
+  image_injector: &'a dyn ImageInjector,
+  context_menu_handler: &'a dyn ContextMenuHandler,
+  icon_handler: &'a dyn IconHandler,
+  secure_input_manager: &'a dyn SecureInputManager,
+) -> impl Dispatcher + 'a {
+  default::DefaultDispatcher::new(
+    event_injector,
+    clipboard_injector,
+    mode_provider,
+    key_injector,
+    html_injector,
+    image_injector,
+    context_menu_handler,
+    icon_handler,
+    secure_input_manager,
+  )
+}
diff --git a/espanso-engine/src/event/effect.rs b/espanso-engine/src/event/effect.rs
new file mode 100644
index 00000000..505c0433
--- /dev/null
+++ b/espanso-engine/src/event/effect.rs
@@ -0,0 +1,63 @@
+/*
+ * 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 super::input::Key;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct TriggerCompensationEvent {
+  pub trigger: String,
+  pub left_separator: Option<String>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct CursorHintCompensationEvent {
+  pub cursor_hint_back_count: usize,
+}
+
+#[derive(Debug, Clone)]
+pub struct TextInjectRequest {
+  pub text: String,
+  pub force_mode: Option<TextInjectMode>,
+}
+
+#[derive(Debug, Clone)]
+pub struct MarkdownInjectRequest {
+  pub markdown: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct HtmlInjectRequest {
+  pub html: String,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum TextInjectMode {
+  Keys,
+  Clipboard,
+}
+
+#[derive(Debug, Clone)]
+pub struct KeySequenceInjectRequest {
+  pub keys: Vec<Key>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ImageInjectRequest {
+  pub image_path: String,
+}
diff --git a/espanso-engine/src/event/input.rs b/espanso-engine/src/event/input.rs
new file mode 100644
index 00000000..ebe830e9
--- /dev/null
+++ b/espanso-engine/src/event/input.rs
@@ -0,0 +1,123 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Status {
+  Pressed,
+  Released,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Variant {
+  Left,
+  Right,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct KeyboardEvent {
+  pub key: Key,
+  pub value: Option<String>,
+  pub status: Status,
+  pub variant: Option<Variant>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum MouseButton {
+  Left,
+  Right,
+  Middle,
+  Button1,
+  Button2,
+  Button3,
+  Button4,
+  Button5,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MouseEvent {
+  pub button: MouseButton,
+  pub status: Status,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Key {
+  // Modifiers
+  Alt,
+  CapsLock,
+  Control,
+  Meta,
+  NumLock,
+  Shift,
+
+  // Whitespace
+  Enter,
+  Tab,
+  Space,
+
+  // Navigation
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  ArrowUp,
+  End,
+  Home,
+  PageDown,
+  PageUp,
+
+  // UI
+  Escape,
+
+  // Editing keys
+  Backspace,
+
+  // Function keys
+  F1,
+  F2,
+  F3,
+  F4,
+  F5,
+  F6,
+  F7,
+  F8,
+  F9,
+  F10,
+  F11,
+  F12,
+  F13,
+  F14,
+  F15,
+  F16,
+  F17,
+  F18,
+  F19,
+  F20,
+
+  // Other keys, includes the raw code provided by the operating system
+  Other(i32),
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ContextMenuClickedEvent {
+  pub context_item_id: u32,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct HotKeyEvent {
+  pub hotkey_id: i32,
+}
diff --git a/espanso-engine/src/event/internal.rs b/espanso-engine/src/event/internal.rs
new file mode 100644
index 00000000..ca43c2e9
--- /dev/null
+++ b/espanso-engine/src/event/internal.rs
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchesDetectedEvent {
+  pub matches: Vec<DetectedMatch>,
+  pub is_search: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct DetectedMatch {
+  pub id: i32,
+  pub trigger: Option<String>,
+  pub left_separator: Option<String>,
+  pub right_separator: Option<String>,
+  pub args: HashMap<String, String>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchSelectedEvent {
+  pub chosen: DetectedMatch,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct CauseCompensatedMatchEvent {
+  pub m: DetectedMatch,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RenderingRequestedEvent {
+  pub match_id: i32,
+  pub trigger: Option<String>,
+  pub left_separator: Option<String>,
+  pub right_separator: Option<String>,
+  pub trigger_args: HashMap<String, String>,
+  pub format: TextFormat,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum TextFormat {
+  Plain,
+  Markdown,
+  Html,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ImageRequestedEvent {
+  pub match_id: i32,
+  pub image_path: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ImageResolvedEvent {
+  pub image_path: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RenderedEvent {
+  pub match_id: i32,
+  pub body: String,
+  pub format: TextFormat,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct DiscardPreviousEvent {
+  // All Events with a source_id smaller than this one will be discarded
+  pub minimum_source_id: u32,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct SecureInputEnabledEvent {
+  pub app_name: String,
+  pub app_path: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct UndoEvent {
+  pub match_id: i32,
+  pub trigger: String,
+  pub replace: String,
+}
diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs
new file mode 100644
index 00000000..f43eb96e
--- /dev/null
+++ b/espanso-engine/src/event/mod.rs
@@ -0,0 +1,108 @@
+/*
+ * 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/>.
+ */
+
+pub mod effect;
+pub mod input;
+pub mod internal;
+pub mod ui;
+
+pub type SourceId = u32;
+
+#[derive(Debug, Clone)]
+pub struct Event {
+  // The source id is a unique, monothonically increasing number
+  // that is given to each event by the source and is propagated
+  // to all consequential events.
+  // For example, if a keyboard event with source_id = 5 generates
+  // a detected match event, this event will have source_id = 5
+  pub source_id: SourceId,
+  pub etype: EventType,
+}
+
+impl Event {
+  pub fn caused_by(cause_id: SourceId, event_type: EventType) -> Event {
+    Event {
+      source_id: cause_id,
+      etype: event_type,
+    }
+  }
+}
+
+#[derive(Debug, Clone)]
+#[allow(clippy::upper_case_acronyms)]
+pub enum EventType {
+  NOOP,
+  ProcessingError(String),
+  ExitRequested(ExitMode),
+  Exit(ExitMode),
+  Heartbeat,
+
+  // Inputs
+  Keyboard(input::KeyboardEvent),
+  Mouse(input::MouseEvent),
+  HotKey(input::HotKeyEvent),
+  TrayIconClicked,
+  ContextMenuClicked(input::ContextMenuClickedEvent),
+
+  // Internal
+  MatchesDetected(internal::MatchesDetectedEvent),
+  MatchSelected(internal::MatchSelectedEvent),
+  CauseCompensatedMatch(internal::CauseCompensatedMatchEvent),
+
+  RenderingRequested(internal::RenderingRequestedEvent),
+  ImageRequested(internal::ImageRequestedEvent),
+  Rendered(internal::RenderedEvent),
+  ImageResolved(internal::ImageResolvedEvent),
+  MatchInjected,
+  DiscardPrevious(internal::DiscardPreviousEvent),
+  Undo(internal::UndoEvent),
+
+  Disabled,
+  Enabled,
+  DisableRequest,
+  EnableRequest,
+  SecureInputEnabled(internal::SecureInputEnabledEvent),
+  SecureInputDisabled,
+
+  // Effects
+  TriggerCompensation(effect::TriggerCompensationEvent),
+  CursorHintCompensation(effect::CursorHintCompensationEvent),
+
+  KeySequenceInject(effect::KeySequenceInjectRequest),
+  TextInject(effect::TextInjectRequest),
+  MarkdownInject(effect::MarkdownInjectRequest),
+  HtmlInject(effect::HtmlInjectRequest),
+  ImageInject(effect::ImageInjectRequest),
+
+  // UI
+  ShowContextMenu(ui::ShowContextMenuEvent),
+  IconStatusChange(ui::IconStatusChangeEvent),
+  DisplaySecureInputTroubleshoot,
+  ShowSearchBar,
+
+  // Other
+  LaunchSecureInputAutoFix,
+}
+
+#[derive(Debug, Clone)]
+pub enum ExitMode {
+  Exit,
+  ExitAllProcesses,
+  RestartWorker,
+}
diff --git a/espanso-engine/src/event/ui.rs b/espanso-engine/src/event/ui.rs
new file mode 100644
index 00000000..b172c45e
--- /dev/null
+++ b/espanso-engine/src/event/ui.rs
@@ -0,0 +1,54 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ShowContextMenuEvent {
+  pub items: Vec<MenuItem>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum MenuItem {
+  Simple(SimpleMenuItem),
+  Sub(SubMenuItem),
+  Separator,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct SimpleMenuItem {
+  pub id: u32,
+  pub label: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct SubMenuItem {
+  pub label: String,
+  pub items: Vec<MenuItem>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct IconStatusChangeEvent {
+  pub status: IconStatus,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum IconStatus {
+  Enabled,
+  Disabled,
+  SecureInputDisabled,
+}
diff --git a/espanso-engine/src/funnel/default.rs b/espanso-engine/src/funnel/default.rs
new file mode 100644
index 00000000..d409bc50
--- /dev/null
+++ b/espanso-engine/src/funnel/default.rs
@@ -0,0 +1,54 @@
+/*
+ * 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 crossbeam::channel::Select;
+
+use super::{Funnel, FunnelResult, Source};
+
+pub struct DefaultFunnel<'a> {
+  sources: &'a [&'a dyn Source<'a>],
+}
+
+impl<'a> DefaultFunnel<'a> {
+  pub fn new(sources: &'a [&'a dyn Source<'a>]) -> Self {
+    Self { sources }
+  }
+}
+
+impl<'a> Funnel for DefaultFunnel<'a> {
+  fn receive(&self) -> FunnelResult {
+    let mut select = Select::new();
+
+    // First register all the sources to the select operation
+    for source in self.sources.iter() {
+      source.register(&mut select);
+    }
+
+    // Wait for the first source (blocking operation)
+    let op = select.select();
+    let source = self
+      .sources
+      .get(op.index())
+      .expect("invalid source index returned by select operation");
+
+    // Receive (and convert) the event
+    let event = source.receive(op);
+    FunnelResult::Event(event)
+  }
+}
diff --git a/src/process.rs b/espanso-engine/src/funnel/mod.rs
similarity index 55%
rename from src/process.rs
rename to espanso-engine/src/funnel/mod.rs
index 660abb0b..d920310c 100644
--- a/src/process.rs
+++ b/espanso-engine/src/funnel/mod.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2020 Federico Terzi
+ * 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
@@ -17,20 +17,28 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use log::warn;
-use std::io;
-use std::process::{Child, Command, Stdio};
-
-#[cfg(target_os = "windows")]
-pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
-    use std::os::windows::process::CommandExt;
-    Command::new(cmd)
-        .creation_flags(0x08000008) // Detached Process without window
-        .args(args)
-        .spawn()
+use crossbeam::{channel::Select, channel::SelectedOperation};
+
+use self::default::DefaultFunnel;
+
+use crate::event::Event;
+
+mod default;
+
+pub trait Source<'a> {
+  fn register(&'a self, select: &mut Select<'a>) -> usize;
+  fn receive(&'a self, op: SelectedOperation) -> Event;
+}
+
+pub trait Funnel {
+  fn receive(&self) -> FunnelResult;
+}
+
+pub enum FunnelResult {
+  Event(Event),
+  EndOfStream,
 }
 
-#[cfg(not(target_os = "windows"))]
-pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
-    Command::new(cmd).args(args).spawn()
+pub fn default<'a>(sources: &'a [&'a dyn Source<'a>]) -> impl Funnel + 'a {
+  DefaultFunnel::new(sources)
 }
diff --git a/espanso-engine/src/lib.rs b/espanso-engine/src/lib.rs
new file mode 100644
index 00000000..eb4fbd48
--- /dev/null
+++ b/espanso-engine/src/lib.rs
@@ -0,0 +1,74 @@
+/*
+ * 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 log::debug;
+
+use self::{
+  dispatch::Dispatcher,
+  event::{Event, EventType, ExitMode},
+  funnel::{Funnel, FunnelResult},
+  process::Processor,
+};
+
+pub mod dispatch;
+pub mod event;
+pub mod funnel;
+pub mod process;
+
+pub struct Engine<'a> {
+  funnel: &'a dyn Funnel,
+  processor: &'a mut dyn Processor,
+  dispatcher: &'a dyn Dispatcher,
+}
+
+impl<'a> Engine<'a> {
+  pub fn new(
+    funnel: &'a dyn Funnel,
+    processor: &'a mut dyn Processor,
+    dispatcher: &'a dyn Dispatcher,
+  ) -> Self {
+    Self {
+      funnel,
+      processor,
+      dispatcher,
+    }
+  }
+
+  pub fn run(&mut self) -> ExitMode {
+    loop {
+      match self.funnel.receive() {
+        FunnelResult::Event(event) => {
+          let processed_events = self.processor.process(event);
+          for event in processed_events {
+            if let EventType::Exit(mode) = &event.etype {
+              debug!("exit event received with mode {:?}, exiting engine", mode);
+              return mode.clone();
+            }
+
+            self.dispatcher.dispatch(event);
+          }
+        }
+        FunnelResult::EndOfStream => {
+          debug!("end of stream received");
+          return ExitMode::Exit;
+        }
+      }
+    }
+  }
+}
diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs
new file mode 100644
index 00000000..c20fe6d8
--- /dev/null
+++ b/espanso-engine/src/process/default.rs
@@ -0,0 +1,161 @@
+/*
+ * 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 log::trace;
+
+use super::{
+  middleware::{
+    action::{ActionMiddleware, EventSequenceProvider},
+    cause::CauseCompensateMiddleware,
+    cursor_hint::CursorHintMiddleware,
+    delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
+    markdown::MarkdownMiddleware,
+    match_select::MatchSelectMiddleware,
+    matcher::MatcherMiddleware,
+    multiplex::MultiplexMiddleware,
+    past_discard::PastEventsDiscardMiddleware,
+    render::RenderMiddleware,
+  },
+  DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
+  MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider,
+  Processor, Renderer, UndoEnabledProvider,
+};
+use crate::{
+  event::{Event, EventType},
+  process::middleware::{
+    context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware,
+    hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware,
+    image_resolve::ImageResolverMiddleware, search::SearchMiddleware, suppress::SuppressMiddleware,
+    undo::UndoMiddleware,
+  },
+};
+use std::collections::VecDeque;
+
+pub struct DefaultProcessor<'a> {
+  event_queue: VecDeque<Event>,
+  middleware: Vec<Box<dyn Middleware + 'a>>,
+}
+
+#[allow(clippy::too_many_arguments)]
+impl<'a> DefaultProcessor<'a> {
+  pub fn new<MatcherState>(
+    matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
+    match_filter: &'a dyn MatchFilter,
+    match_selector: &'a dyn MatchSelector,
+    multiplexer: &'a dyn Multiplexer,
+    renderer: &'a dyn Renderer<'a>,
+    match_info_provider: &'a dyn MatchInfoProvider,
+    modifier_status_provider: &'a dyn ModifierStatusProvider,
+    event_sequence_provider: &'a dyn EventSequenceProvider,
+    path_provider: &'a dyn PathProvider,
+    disable_options: DisableOptions,
+    matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider,
+    match_provider: &'a dyn MatchProvider,
+    undo_enabled_provider: &'a dyn UndoEnabledProvider,
+    enabled_status_provider: &'a dyn EnabledStatusProvider,
+  ) -> DefaultProcessor<'a> {
+    Self {
+      event_queue: VecDeque::new(),
+      middleware: vec![
+        Box::new(PastEventsDiscardMiddleware::new()),
+        Box::new(DisableMiddleware::new(disable_options)),
+        Box::new(IconStatusMiddleware::new()),
+        Box::new(MatcherMiddleware::new(matchers, matcher_options_provider)),
+        Box::new(SuppressMiddleware::new(enabled_status_provider)),
+        Box::new(ContextMenuMiddleware::new()),
+        Box::new(HotKeyMiddleware::new()),
+        Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
+        Box::new(CauseCompensateMiddleware::new()),
+        Box::new(MultiplexMiddleware::new(multiplexer)),
+        Box::new(RenderMiddleware::new(renderer)),
+        Box::new(ImageResolverMiddleware::new(path_provider)),
+        Box::new(CursorHintMiddleware::new()),
+        Box::new(ExitMiddleware::new()),
+        Box::new(UndoMiddleware::new(undo_enabled_provider)),
+        Box::new(ActionMiddleware::new(
+          match_info_provider,
+          event_sequence_provider,
+        )),
+        Box::new(SearchMiddleware::new(match_provider)),
+        Box::new(MarkdownMiddleware::new()),
+        Box::new(DelayForModifierReleaseMiddleware::new(
+          modifier_status_provider,
+        )),
+      ],
+    }
+  }
+
+  fn process_one(&mut self) -> Option<Event> {
+    if let Some(event) = self.event_queue.pop_back() {
+      let mut current_event = event;
+
+      let mut current_queue = VecDeque::new();
+      let mut dispatch = |event: Event| {
+        trace!("dispatched event: {:?}", event);
+        current_queue.push_front(event);
+      };
+
+      trace!("--------------- new event -----------------");
+      for middleware in self.middleware.iter() {
+        trace!(
+          "middleware '{}' received event: {:?}",
+          middleware.name(),
+          current_event
+        );
+
+        current_event = middleware.next(current_event, &mut dispatch);
+
+        trace!(
+          "middleware '{}' produced event: {:?}",
+          middleware.name(),
+          current_event
+        );
+
+        if let EventType::NOOP = current_event.etype {
+          trace!("interrupting chain as the event is NOOP");
+          break;
+        }
+      }
+
+      while let Some(event) = current_queue.pop_back() {
+        self.event_queue.push_front(event);
+      }
+
+      Some(current_event)
+    } else {
+      None
+    }
+  }
+}
+
+impl<'a> Processor for DefaultProcessor<'a> {
+  fn process(&mut self, event: Event) -> Vec<Event> {
+    self.event_queue.push_front(event);
+
+    let mut processed_events = Vec::new();
+
+    while !self.event_queue.is_empty() {
+      if let Some(event) = self.process_one() {
+        processed_events.push(event);
+      }
+    }
+
+    processed_events
+  }
+}
diff --git a/espanso-engine/src/process/middleware/action.rs b/espanso-engine/src/process/middleware/action.rs
new file mode 100644
index 00000000..41d97ed2
--- /dev/null
+++ b/espanso-engine/src/process/middleware/action.rs
@@ -0,0 +1,154 @@
+/*
+ * 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 super::super::Middleware;
+use crate::event::{
+  effect::{
+    HtmlInjectRequest, ImageInjectRequest, KeySequenceInjectRequest, MarkdownInjectRequest,
+    TextInjectMode, TextInjectRequest,
+  },
+  input::Key,
+  internal::{DiscardPreviousEvent, TextFormat},
+  Event, EventType,
+};
+
+pub trait MatchInfoProvider {
+  fn get_force_mode(&self, match_id: i32) -> Option<TextInjectMode>;
+}
+
+pub trait EventSequenceProvider {
+  fn get_next_id(&self) -> u32;
+}
+
+pub struct ActionMiddleware<'a> {
+  match_info_provider: &'a dyn MatchInfoProvider,
+  event_sequence_provider: &'a dyn EventSequenceProvider,
+}
+
+impl<'a> ActionMiddleware<'a> {
+  pub fn new(
+    match_info_provider: &'a dyn MatchInfoProvider,
+    event_sequence_provider: &'a dyn EventSequenceProvider,
+  ) -> Self {
+    Self {
+      match_info_provider,
+      event_sequence_provider,
+    }
+  }
+}
+
+impl<'a> Middleware for ActionMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "action"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    match &event.etype {
+      EventType::Rendered(_) | EventType::ImageResolved(_) => {
+        dispatch(Event::caused_by(event.source_id, EventType::MatchInjected));
+        dispatch(Event::caused_by(
+          event.source_id,
+          EventType::DiscardPrevious(DiscardPreviousEvent {
+            minimum_source_id: self.event_sequence_provider.get_next_id(),
+          }),
+        ));
+
+        match &event.etype {
+          EventType::Rendered(m_event) => Event::caused_by(
+            event.source_id,
+            match m_event.format {
+              TextFormat::Plain => EventType::TextInject(TextInjectRequest {
+                text: m_event.body.clone(),
+                force_mode: self.match_info_provider.get_force_mode(m_event.match_id),
+              }),
+              TextFormat::Html => EventType::HtmlInject(HtmlInjectRequest {
+                html: m_event.body.clone(),
+              }),
+              TextFormat::Markdown => EventType::MarkdownInject(MarkdownInjectRequest {
+                markdown: m_event.body.clone(),
+              }),
+            },
+          ),
+          EventType::ImageResolved(m_event) => Event::caused_by(
+            event.source_id,
+            EventType::ImageInject(ImageInjectRequest {
+              image_path: m_event.image_path.clone(),
+            }),
+          ),
+          _ => unreachable!(),
+        }
+      }
+      EventType::CursorHintCompensation(m_event) => {
+        dispatch(Event::caused_by(
+          event.source_id,
+          EventType::DiscardPrevious(DiscardPreviousEvent {
+            minimum_source_id: self.event_sequence_provider.get_next_id(),
+          }),
+        ));
+
+        Event::caused_by(
+          event.source_id,
+          EventType::KeySequenceInject(KeySequenceInjectRequest {
+            keys: (0..m_event.cursor_hint_back_count)
+              .map(|_| Key::ArrowLeft)
+              .collect(),
+          }),
+        )
+      }
+      EventType::TriggerCompensation(m_event) => {
+        let mut backspace_count = m_event.trigger.chars().count();
+
+        // We want to preserve the left separator if present
+        if let Some(left_separator) = &m_event.left_separator {
+          backspace_count -= left_separator.chars().count();
+        }
+
+        Event::caused_by(
+          event.source_id,
+          EventType::KeySequenceInject(KeySequenceInjectRequest {
+            keys: (0..backspace_count).map(|_| Key::Backspace).collect(),
+          }),
+        )
+      }
+      EventType::Undo(m_event) => {
+        // We subtract one, because the backspace that triggered the undo feature
+        // already removed the last char
+        let backspace_count = m_event.replace.chars().count() - 1;
+
+        dispatch(Event::caused_by(
+          event.source_id,
+          EventType::TextInject(TextInjectRequest {
+            text: m_event.trigger.clone(),
+            force_mode: self.match_info_provider.get_force_mode(m_event.match_id),
+          }),
+        ));
+
+        Event::caused_by(
+          event.source_id,
+          EventType::KeySequenceInject(KeySequenceInjectRequest {
+            keys: (0..backspace_count).map(|_| Key::Backspace).collect(),
+          }),
+        )
+      }
+      _ => event,
+    }
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/cause.rs b/espanso-engine/src/process/middleware/cause.rs
new file mode 100644
index 00000000..b3ae7c98
--- /dev/null
+++ b/espanso-engine/src/process/middleware/cause.rs
@@ -0,0 +1,67 @@
+/*
+ * 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 super::super::Middleware;
+use crate::event::{
+  effect::TriggerCompensationEvent, internal::CauseCompensatedMatchEvent, Event, EventType,
+};
+
+pub struct CauseCompensateMiddleware {}
+
+impl CauseCompensateMiddleware {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Middleware for CauseCompensateMiddleware {
+  fn name(&self) -> &'static str {
+    "cause_compensate"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::MatchSelected(m_event) = &event.etype {
+      let compensated_event = Event::caused_by(
+        event.source_id,
+        EventType::CauseCompensatedMatch(CauseCompensatedMatchEvent {
+          m: m_event.chosen.clone(),
+        }),
+      );
+
+      if let Some(trigger) = &m_event.chosen.trigger {
+        dispatch(compensated_event);
+
+        // Before the event, place a trigger compensation
+        return Event::caused_by(
+          event.source_id,
+          EventType::TriggerCompensation(TriggerCompensationEvent {
+            trigger: trigger.clone(),
+            left_separator: m_event.chosen.left_separator.clone(),
+          }),
+        );
+      } else {
+        return compensated_event;
+      }
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/context_menu.rs b/espanso-engine/src/process/middleware/context_menu.rs
new file mode 100644
index 00000000..79de2e31
--- /dev/null
+++ b/espanso-engine/src/process/middleware/context_menu.rs
@@ -0,0 +1,185 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright  id: (), label: () id: (), label: () id: (), label: ()(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::cell::RefCell;
+
+use super::super::Middleware;
+use crate::event::{
+  ui::{MenuItem, ShowContextMenuEvent, SimpleMenuItem},
+  Event, EventType, ExitMode,
+};
+
+const CONTEXT_ITEM_EXIT: u32 = 0;
+const CONTEXT_ITEM_RELOAD: u32 = 1;
+const CONTEXT_ITEM_ENABLE: u32 = 2;
+const CONTEXT_ITEM_DISABLE: u32 = 3;
+const CONTEXT_ITEM_SECURE_INPUT_EXPLAIN: u32 = 4;
+const CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND: u32 = 5;
+const CONTEXT_ITEM_OPEN_SEARCH: u32 = 6;
+
+pub struct ContextMenuMiddleware {
+  is_enabled: RefCell<bool>,
+  is_secure_input_enabled: RefCell<bool>,
+}
+
+impl ContextMenuMiddleware {
+  pub fn new() -> Self {
+    Self {
+      is_enabled: RefCell::new(true),
+      is_secure_input_enabled: RefCell::new(false),
+    }
+  }
+}
+
+#[allow(clippy::needless_return)]
+impl Middleware for ContextMenuMiddleware {
+  fn name(&self) -> &'static str {
+    "context_menu"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    let mut is_enabled = self.is_enabled.borrow_mut();
+    let mut is_secure_input_enabled = self.is_secure_input_enabled.borrow_mut();
+
+    match &event.etype {
+      EventType::TrayIconClicked => {
+        // TODO: fetch top matches for the active config to be added
+
+        let mut items = vec![
+          MenuItem::Simple(if *is_enabled {
+            SimpleMenuItem {
+              id: CONTEXT_ITEM_DISABLE,
+              label: "Disable".to_string(),
+            }
+          } else {
+            SimpleMenuItem {
+              id: CONTEXT_ITEM_ENABLE,
+              label: "Enable".to_string(),
+            }
+          }),
+          MenuItem::Simple(SimpleMenuItem {
+            id: CONTEXT_ITEM_OPEN_SEARCH,
+            label: "Open search bar".to_string(),
+          }),
+          MenuItem::Separator,
+          MenuItem::Simple(SimpleMenuItem {
+            id: CONTEXT_ITEM_RELOAD,
+            label: "Reload config".to_string(),
+          }),
+          MenuItem::Separator,
+          MenuItem::Simple(SimpleMenuItem {
+            id: CONTEXT_ITEM_EXIT,
+            label: "Exit espanso".to_string(),
+          }),
+        ];
+
+        if *is_secure_input_enabled {
+          items.insert(
+            0,
+            MenuItem::Simple(SimpleMenuItem {
+              id: CONTEXT_ITEM_SECURE_INPUT_EXPLAIN,
+              label: "Why is espanso not working?".to_string(),
+            }),
+          );
+          items.insert(
+            1,
+            MenuItem::Simple(SimpleMenuItem {
+              id: CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND,
+              label: "Launch SecureInput auto-fix".to_string(),
+            }),
+          );
+          items.insert(2, MenuItem::Separator);
+        }
+
+        // TODO: my idea is to use a set of reserved u32 ids for built-in
+        // actions such as Exit, Open Editor etc
+        // then we need some u32 for the matches, so we need to create
+        // a mapping structure match_id <-> context-menu-id
+        return Event::caused_by(
+          event.source_id,
+          EventType::ShowContextMenu(ShowContextMenuEvent {
+            // TODO: add actual entries
+            items,
+          }),
+        );
+      }
+      EventType::ContextMenuClicked(context_click_event) => {
+        match context_click_event.context_item_id {
+          CONTEXT_ITEM_EXIT => Event::caused_by(
+            event.source_id,
+            EventType::ExitRequested(ExitMode::ExitAllProcesses),
+          ),
+          CONTEXT_ITEM_RELOAD => Event::caused_by(
+            event.source_id,
+            EventType::ExitRequested(ExitMode::RestartWorker),
+          ),
+          CONTEXT_ITEM_ENABLE => {
+            dispatch(Event::caused_by(event.source_id, EventType::EnableRequest));
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          CONTEXT_ITEM_DISABLE => {
+            dispatch(Event::caused_by(event.source_id, EventType::DisableRequest));
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          CONTEXT_ITEM_SECURE_INPUT_EXPLAIN => {
+            dispatch(Event::caused_by(
+              event.source_id,
+              EventType::DisplaySecureInputTroubleshoot,
+            ));
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND => {
+            dispatch(Event::caused_by(
+              event.source_id,
+              EventType::LaunchSecureInputAutoFix,
+            ));
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          CONTEXT_ITEM_OPEN_SEARCH => {
+            dispatch(Event::caused_by(event.source_id, EventType::ShowSearchBar));
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          _ => {
+            // TODO: handle dynamic items
+            todo!()
+          }
+        }
+      }
+      EventType::Disabled => {
+        *is_enabled = false;
+        event
+      }
+      EventType::Enabled => {
+        *is_enabled = true;
+        event
+      }
+      EventType::SecureInputEnabled(_) => {
+        *is_secure_input_enabled = true;
+        event
+      }
+      EventType::SecureInputDisabled => {
+        *is_secure_input_enabled = false;
+        event
+      }
+      _ => event,
+    }
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/cursor_hint.rs b/espanso-engine/src/process/middleware/cursor_hint.rs
new file mode 100644
index 00000000..cdb9bd0e
--- /dev/null
+++ b/espanso-engine/src/process/middleware/cursor_hint.rs
@@ -0,0 +1,82 @@
+/*
+ * 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 super::super::Middleware;
+use crate::event::{
+  effect::CursorHintCompensationEvent, internal::RenderedEvent, Event, EventType,
+};
+
+pub struct CursorHintMiddleware {}
+
+impl CursorHintMiddleware {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Middleware for CursorHintMiddleware {
+  fn name(&self) -> &'static str {
+    "cursor_hint"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::Rendered(m_event) = event.etype {
+      let (body, cursor_hint_back_count) = process_cursor_hint(m_event.body);
+
+      if let Some(cursor_hint_back_count) = cursor_hint_back_count {
+        dispatch(Event::caused_by(
+          event.source_id,
+          EventType::CursorHintCompensation(CursorHintCompensationEvent {
+            cursor_hint_back_count,
+          }),
+        ));
+      }
+
+      // Alter the rendered event to remove the cursor hint from the body
+      return Event::caused_by(
+        event.source_id,
+        EventType::Rendered(RenderedEvent { body, ..m_event }),
+      );
+    }
+
+    event
+  }
+}
+
+// TODO: test
+fn process_cursor_hint(body: String) -> (String, Option<usize>) {
+  if let Some(index) = body.find("$|$") {
+    // Convert the byte index to a char index
+    let char_str = &body[0..index];
+    let char_index = char_str.chars().count();
+    let total_size = body.chars().count();
+
+    // Remove the $|$ placeholder
+    let body = body.replace("$|$", "");
+
+    // Calculate the amount of rewind moves needed (LEFT ARROW).
+    // Subtract also 3, equal to the number of chars of the placeholder "$|$"
+    let moves = total_size - char_index - 3;
+    (body, Some(moves))
+  } else {
+    (body, None)
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/delay_modifiers.rs b/espanso-engine/src/process/middleware/delay_modifiers.rs
new file mode 100644
index 00000000..b4dfdf0c
--- /dev/null
+++ b/espanso-engine/src/process/middleware/delay_modifiers.rs
@@ -0,0 +1,87 @@
+/*
+ * 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::time::{Duration, Instant};
+
+use log::{trace, warn};
+
+use super::super::Middleware;
+use crate::event::{Event, EventType};
+
+/// Maximum time to wait for modifiers being released before
+/// giving up.
+const MODIFIER_DELAY_TIMEOUT: Duration = Duration::from_secs(3);
+
+pub trait ModifierStatusProvider {
+  fn is_any_conflicting_modifier_pressed(&self) -> bool;
+}
+
+/// This middleware is used to delay the injection of text until
+/// all modifiers have been released. This is needed as otherwise,
+/// injections might misbehave as pressed modifiers might alter
+/// the keys being injected.
+pub struct DelayForModifierReleaseMiddleware<'a> {
+  provider: &'a dyn ModifierStatusProvider,
+}
+
+impl<'a> DelayForModifierReleaseMiddleware<'a> {
+  pub fn new(provider: &'a dyn ModifierStatusProvider) -> Self {
+    Self { provider }
+  }
+}
+
+impl<'a> Middleware for DelayForModifierReleaseMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "delay_modifiers"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if is_injection_event(&event.etype) {
+      let start = Instant::now();
+      while self.provider.is_any_conflicting_modifier_pressed() {
+        if Instant::now().duration_since(start) > MODIFIER_DELAY_TIMEOUT {
+          warn!("injection delay has timed out, please release the modifier keys (SHIFT, CTRL, ALT, CMD) to trigger an expansion");
+          break;
+        }
+
+        // TODO: here we might show a popup window to tell the users to release those keys
+
+        trace!("delaying injection event as some modifiers are pressed");
+        std::thread::sleep(Duration::from_millis(100));
+      }
+    }
+
+    event
+  }
+}
+
+fn is_injection_event(event_type: &EventType) -> bool {
+  matches!(
+    event_type,
+    EventType::TriggerCompensation(_)
+      | EventType::CursorHintCompensation(_)
+      | EventType::KeySequenceInject(_)
+      | EventType::TextInject(_)
+      | EventType::ImageInject(_)
+      | EventType::HtmlInject(_)
+      | EventType::MarkdownInject(_)
+  )
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/disable.rs b/espanso-engine/src/process/middleware/disable.rs
new file mode 100644
index 00000000..77bd0e80
--- /dev/null
+++ b/espanso-engine/src/process/middleware/disable.rs
@@ -0,0 +1,138 @@
+/*
+ * 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::{
+  cell::RefCell,
+  time::{Duration, Instant},
+};
+
+use log::info;
+
+use super::super::Middleware;
+use crate::event::{
+  input::{Key, KeyboardEvent, Status, Variant},
+  Event, EventType,
+};
+
+pub struct DisableOptions {
+  pub toggle_key: Option<Key>,
+  pub toggle_key_variant: Option<Variant>,
+  pub toggle_key_maximum_window: Duration,
+  // TODO: toggle shortcut?
+}
+
+pub struct DisableMiddleware {
+  enabled: RefCell<bool>,
+  last_toggle_press: RefCell<Option<Instant>>,
+  options: DisableOptions,
+}
+
+impl DisableMiddleware {
+  pub fn new(options: DisableOptions) -> Self {
+    Self {
+      enabled: RefCell::new(true),
+      last_toggle_press: RefCell::new(None),
+      options,
+    }
+  }
+}
+
+impl Middleware for DisableMiddleware {
+  fn name(&self) -> &'static str {
+    "disable"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    let mut has_status_changed = false;
+    let mut enabled = self.enabled.borrow_mut();
+
+    match &event.etype {
+      EventType::Keyboard(m_event) => {
+        if is_toggle_key(m_event, &self.options) {
+          let mut last_toggle_press = self.last_toggle_press.borrow_mut();
+          if let Some(previous_press) = *last_toggle_press {
+            if previous_press.elapsed() < self.options.toggle_key_maximum_window {
+              *enabled = !*enabled;
+              *last_toggle_press = None;
+              has_status_changed = true;
+            } else {
+              *last_toggle_press = Some(Instant::now());
+            }
+          } else {
+            *last_toggle_press = Some(Instant::now());
+          }
+        }
+      }
+      EventType::EnableRequest => {
+        *enabled = true;
+        has_status_changed = true;
+      }
+      EventType::DisableRequest => {
+        *enabled = false;
+        has_status_changed = true;
+      }
+      _ => {}
+    }
+
+    if has_status_changed {
+      info!("toggled enabled state, is_enabled = {}", *enabled);
+      dispatch(Event::caused_by(
+        event.source_id,
+        if *enabled {
+          EventType::Enabled
+        } else {
+          EventType::Disabled
+        },
+      ))
+    }
+
+    // Block keyboard events when disabled
+    if let EventType::Keyboard(_) = &event.etype {
+      if !*enabled {
+        return Event::caused_by(event.source_id, EventType::NOOP);
+      }
+    }
+    // TODO: also ignore hotkey and mouse events
+
+    event
+  }
+}
+
+fn is_toggle_key(event: &KeyboardEvent, options: &DisableOptions) -> bool {
+  if event.status != Status::Released {
+    return false;
+  }
+
+  if options
+    .toggle_key
+    .as_ref()
+    .map(|key| key == &event.key)
+    .unwrap_or(false)
+  {
+    if let (Some(variant), Some(e_variant)) = (&options.toggle_key_variant, &event.variant) {
+      variant == e_variant
+    } else {
+      true
+    }
+  } else {
+    false
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/exit.rs b/espanso-engine/src/process/middleware/exit.rs
new file mode 100644
index 00000000..d4fa58f3
--- /dev/null
+++ b/espanso-engine/src/process/middleware/exit.rs
@@ -0,0 +1,51 @@
+/*
+ * 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 log::debug;
+
+use super::super::Middleware;
+use crate::event::{Event, EventType};
+
+pub struct ExitMiddleware {}
+
+impl ExitMiddleware {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Middleware for ExitMiddleware {
+  fn name(&self) -> &'static str {
+    "exit"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::ExitRequested(mode) = &event.etype {
+      debug!(
+        "received ExitRequested event with mode: {:?}, dispatching exit",
+        mode
+      );
+      return Event::caused_by(event.source_id, EventType::Exit(mode.clone()));
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/hotkey.rs b/espanso-engine/src/process/middleware/hotkey.rs
new file mode 100644
index 00000000..9f34fc2d
--- /dev/null
+++ b/espanso-engine/src/process/middleware/hotkey.rs
@@ -0,0 +1,57 @@
+/*
+ * This file is part of espanso id: (), trigger: (), trigger: (), left_separator: (), right_separator: (), args: () left_separator: (), right_separator: (), args: () id: (), trigger: (), left_separator: (), right_separator: (), args: ().
+ *
+ * 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 super::super::Middleware;
+use crate::event::{
+  internal::{DetectedMatch, MatchesDetectedEvent},
+  Event, EventType,
+};
+
+pub struct HotKeyMiddleware {}
+
+impl HotKeyMiddleware {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Middleware for HotKeyMiddleware {
+  fn name(&self) -> &'static str {
+    "hotkey"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::HotKey(m_event) = &event.etype {
+      return Event::caused_by(
+        event.source_id,
+        EventType::MatchesDetected(MatchesDetectedEvent {
+          matches: vec![DetectedMatch {
+            id: m_event.hotkey_id,
+            ..Default::default()
+          }],
+          is_search: false,
+        }),
+      );
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/icon_status.rs b/espanso-engine/src/process/middleware/icon_status.rs
new file mode 100644
index 00000000..b682a550
--- /dev/null
+++ b/espanso-engine/src/process/middleware/icon_status.rs
@@ -0,0 +1,81 @@
+/*
+ * 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::cell::RefCell;
+
+use super::super::Middleware;
+use crate::event::{
+  ui::{IconStatus, IconStatusChangeEvent},
+  Event, EventType,
+};
+
+pub struct IconStatusMiddleware {
+  enabled: RefCell<bool>,
+  secure_input_enabled: RefCell<bool>,
+}
+
+impl IconStatusMiddleware {
+  pub fn new() -> Self {
+    Self {
+      enabled: RefCell::new(true),
+      secure_input_enabled: RefCell::new(false),
+    }
+  }
+}
+
+impl Middleware for IconStatusMiddleware {
+  fn name(&self) -> &'static str {
+    "icon_status"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    let mut enabled = self.enabled.borrow_mut();
+    let mut secure_input_enabled = self.secure_input_enabled.borrow_mut();
+
+    let mut did_update = true;
+    match &event.etype {
+      EventType::Enabled => *enabled = true,
+      EventType::Disabled => *enabled = false,
+      EventType::SecureInputEnabled(_) => *secure_input_enabled = true,
+      EventType::SecureInputDisabled => *secure_input_enabled = false,
+      _ => did_update = false,
+    }
+
+    if did_update {
+      let status = if *enabled {
+        if *secure_input_enabled {
+          IconStatus::SecureInputDisabled
+        } else {
+          IconStatus::Enabled
+        }
+      } else {
+        IconStatus::Disabled
+      };
+
+      dispatch(Event::caused_by(
+        event.source_id,
+        EventType::IconStatusChange(IconStatusChangeEvent { status }),
+      ));
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/image_resolve.rs b/espanso-engine/src/process/middleware/image_resolve.rs
new file mode 100644
index 00000000..8a9569f2
--- /dev/null
+++ b/espanso-engine/src/process/middleware/image_resolve.rs
@@ -0,0 +1,81 @@
+/*
+ * 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::path::Path;
+
+use log::error;
+
+use super::super::Middleware;
+use crate::event::{internal::ImageResolvedEvent, Event, EventType};
+
+pub trait PathProvider {
+  fn get_config_path(&self) -> &Path;
+}
+
+pub struct ImageResolverMiddleware<'a> {
+  provider: &'a dyn PathProvider,
+}
+
+impl<'a> ImageResolverMiddleware<'a> {
+  pub fn new(provider: &'a dyn PathProvider) -> Self {
+    Self { provider }
+  }
+}
+
+impl<'a> Middleware for ImageResolverMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "image_resolve"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::ImageRequested(m_event) = &event.etype {
+      // On Windows, we have to replace the forward / with the backslash \ in the path
+      let path = if cfg!(target_os = "windows") {
+        m_event.image_path.replace("/", "\\")
+      } else {
+        m_event.image_path.to_owned()
+      };
+
+      let path = if path.contains("$CONFIG") {
+        let config_path = match self.provider.get_config_path().canonicalize() {
+          Ok(path) => path,
+          Err(err) => {
+            error!(
+              "unable to canonicalize the config path into the image resolver: {}",
+              err
+            );
+            self.provider.get_config_path().to_owned()
+          }
+        };
+        path.replace("$CONFIG", &config_path.to_string_lossy())
+      } else {
+        path
+      };
+
+      return Event::caused_by(
+        event.source_id,
+        EventType::ImageResolved(ImageResolvedEvent { image_path: path }),
+      );
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/markdown.rs b/espanso-engine/src/process/middleware/markdown.rs
new file mode 100644
index 00000000..abebc54c
--- /dev/null
+++ b/espanso-engine/src/process/middleware/markdown.rs
@@ -0,0 +1,63 @@
+/*
+ * 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 super::super::Middleware;
+use crate::event::{effect::HtmlInjectRequest, Event, EventType};
+
+// Convert markdown injection requests to HTML on the fly
+pub struct MarkdownMiddleware {}
+
+impl MarkdownMiddleware {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Middleware for MarkdownMiddleware {
+  fn name(&self) -> &'static str {
+    "markdown"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::MarkdownInject(m_event) = &event.etype {
+      // Render the markdown into HTML
+      let html = markdown::to_html(&m_event.markdown);
+      let mut html = html.trim();
+
+      // Remove the surrounding paragraph
+      if html.starts_with("<p>") {
+        html = html.trim_start_matches("<p>");
+      }
+      if html.ends_with("</p>") {
+        html = html.trim_end_matches("</p>");
+      }
+
+      return Event::caused_by(
+        event.source_id,
+        EventType::HtmlInject(HtmlInjectRequest {
+          html: html.to_owned(),
+        }),
+      );
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/match_select.rs b/espanso-engine/src/process/middleware/match_select.rs
new file mode 100644
index 00000000..5fe0461e
--- /dev/null
+++ b/espanso-engine/src/process/middleware/match_select.rs
@@ -0,0 +1,102 @@
+/*
+ * 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 log::{debug, error};
+
+use super::super::Middleware;
+use crate::event::{internal::MatchSelectedEvent, Event, EventType};
+
+pub trait MatchFilter {
+  fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
+}
+
+pub trait MatchSelector {
+  fn select(&self, matches_ids: &[i32], is_search: bool) -> Option<i32>;
+}
+
+pub struct MatchSelectMiddleware<'a> {
+  match_filter: &'a dyn MatchFilter,
+  match_selector: &'a dyn MatchSelector,
+}
+
+impl<'a> MatchSelectMiddleware<'a> {
+  pub fn new(match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector) -> Self {
+    Self {
+      match_filter,
+      match_selector,
+    }
+  }
+}
+
+impl<'a> Middleware for MatchSelectMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "match_select"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::MatchesDetected(m_event) = event.etype {
+      let matches_ids: Vec<i32> = m_event.matches.iter().map(|m| m.id).collect();
+
+      // Find the matches that are actually valid in the current context
+      let valid_ids = self.match_filter.filter_active(&matches_ids);
+
+      return match valid_ids.len() {
+        0 => Event::caused_by(event.source_id, EventType::NOOP), // No valid matches, consume the event
+        1 => {
+          // Only one match, no need to show a selection dialog
+          let m = m_event
+            .matches
+            .into_iter()
+            .find(|m| m.id == *valid_ids.first().unwrap());
+          if let Some(m) = m {
+            Event::caused_by(
+              event.source_id,
+              EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
+            )
+          } else {
+            error!("MatchSelectMiddleware could not find the correspondent match");
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+        }
+        _ => {
+          // Multiple matches, we need to ask the user which one to use
+          if let Some(selected_id) = self.match_selector.select(&valid_ids, m_event.is_search) {
+            let m = m_event.matches.into_iter().find(|m| m.id == selected_id);
+            if let Some(m) = m {
+              Event::caused_by(
+                event.source_id,
+                EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
+              )
+            } else {
+              error!("MatchSelectMiddleware could not find the correspondent match");
+              Event::caused_by(event.source_id, EventType::NOOP)
+            }
+          } else {
+            debug!("MatchSelectMiddleware did not receive any match selection");
+            Event::caused_by(event.source_id, EventType::NOOP)
+          }
+        }
+      };
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/matcher.rs b/espanso-engine/src/process/middleware/matcher.rs
new file mode 100644
index 00000000..7b3e6665
--- /dev/null
+++ b/espanso-engine/src/process/middleware/matcher.rs
@@ -0,0 +1,208 @@
+/*
+ * 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 log::trace;
+use std::{
+  cell::RefCell,
+  collections::{HashMap, VecDeque},
+};
+
+use super::super::Middleware;
+use crate::event::{
+  input::{Key, Status},
+  internal::{DetectedMatch, MatchesDetectedEvent},
+  Event, EventType,
+};
+
+pub trait Matcher<'a, State> {
+  fn process(
+    &'a self,
+    prev_state: Option<&State>,
+    event: &MatcherEvent,
+  ) -> (State, Vec<MatchResult>);
+}
+
+#[derive(Debug)]
+pub enum MatcherEvent {
+  Key { key: Key, chars: Option<String> },
+  VirtualSeparator,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchResult {
+  pub id: i32,
+  pub trigger: String,
+  pub left_separator: Option<String>,
+  pub right_separator: Option<String>,
+  pub args: HashMap<String, String>,
+}
+
+pub trait MatcherMiddlewareConfigProvider {
+  fn max_history_size(&self) -> usize;
+}
+
+pub struct MatcherMiddleware<'a, State> {
+  matchers: &'a [&'a dyn Matcher<'a, State>],
+
+  matcher_states: RefCell<VecDeque<Vec<State>>>,
+
+  max_history_size: usize,
+}
+
+impl<'a, State> MatcherMiddleware<'a, State> {
+  pub fn new(
+    matchers: &'a [&'a dyn Matcher<'a, State>],
+    options_provider: &'a dyn MatcherMiddlewareConfigProvider,
+  ) -> Self {
+    let max_history_size = options_provider.max_history_size();
+
+    Self {
+      matchers,
+      matcher_states: RefCell::new(VecDeque::new()),
+      max_history_size,
+    }
+  }
+}
+
+impl<'a, State> Middleware for MatcherMiddleware<'a, State> {
+  fn name(&self) -> &'static str {
+    "matcher"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if is_event_of_interest(&event.etype) {
+      let mut matcher_states = self.matcher_states.borrow_mut();
+      let prev_states = if !matcher_states.is_empty() {
+        matcher_states.get(matcher_states.len() - 1)
+      } else {
+        None
+      };
+
+      if let EventType::Keyboard(keyboard_event) = &event.etype {
+        // Backspace handling
+        if keyboard_event.key == Key::Backspace {
+          trace!("popping the last matcher state");
+          matcher_states.pop_back();
+          return event;
+        }
+      }
+
+      // Some keys (such as the arrow keys) and mouse clicks prevent espanso from building
+      // an accurate key buffer, so we need to invalidate it.
+      if is_invalidating_event(&event.etype) {
+        trace!("invalidating event detected, clearing matching state");
+        matcher_states.clear();
+        return event;
+      }
+
+      let mut all_results = Vec::new();
+
+      if let Some(matcher_event) = convert_to_matcher_event(&event.etype) {
+        let mut new_states = Vec::new();
+        for (i, matcher) in self.matchers.iter().enumerate() {
+          let prev_state = prev_states.and_then(|states| states.get(i));
+
+          let (state, results) = matcher.process(prev_state, &matcher_event);
+          all_results.extend(results);
+
+          new_states.push(state);
+        }
+
+        matcher_states.push_back(new_states);
+        if matcher_states.len() > self.max_history_size {
+          matcher_states.pop_front();
+        }
+
+        if !all_results.is_empty() {
+          return Event::caused_by(
+            event.source_id,
+            EventType::MatchesDetected(MatchesDetectedEvent {
+              matches: all_results
+                .into_iter()
+                .map(|result| DetectedMatch {
+                  id: result.id,
+                  trigger: Some(result.trigger),
+                  right_separator: result.right_separator,
+                  left_separator: result.left_separator,
+                  args: result.args,
+                })
+                .collect(),
+              is_search: false,
+            }),
+          );
+        }
+      }
+    }
+
+    event
+  }
+}
+
+fn is_event_of_interest(event_type: &EventType) -> bool {
+  match event_type {
+    EventType::Keyboard(keyboard_event) => {
+      if keyboard_event.status != Status::Pressed {
+        // Skip non-press events
+        false
+      } else {
+        // Skip modifier keys
+        !matches!(
+          keyboard_event.key,
+          Key::Alt | Key::Shift | Key::CapsLock | Key::Meta | Key::NumLock | Key::Control
+        )
+      }
+    }
+    EventType::Mouse(mouse_event) => mouse_event.status == Status::Pressed,
+    EventType::MatchInjected => true,
+    _ => false,
+  }
+}
+
+fn convert_to_matcher_event(event_type: &EventType) -> Option<MatcherEvent> {
+  match event_type {
+    EventType::Keyboard(keyboard_event) => Some(MatcherEvent::Key {
+      key: keyboard_event.key.clone(),
+      chars: keyboard_event.value.clone(),
+    }),
+    EventType::Mouse(_) => Some(MatcherEvent::VirtualSeparator),
+    EventType::MatchInjected => Some(MatcherEvent::VirtualSeparator),
+    _ => None,
+  }
+}
+
+fn is_invalidating_event(event_type: &EventType) -> bool {
+  match event_type {
+    EventType::Keyboard(keyboard_event) => matches!(
+      keyboard_event.key,
+      Key::ArrowDown
+        | Key::ArrowLeft
+        | Key::ArrowRight
+        | Key::ArrowUp
+        | Key::End
+        | Key::Home
+        | Key::PageDown
+        | Key::PageUp
+        | Key::Escape
+    ),
+    EventType::Mouse(_) => true,
+    _ => false,
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs
new file mode 100644
index 00000000..ec2bcc7c
--- /dev/null
+++ b/espanso-engine/src/process/middleware/mod.rs
@@ -0,0 +1,38 @@
+/*
+ * 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/>.
+ */
+
+pub mod action;
+pub mod cause;
+pub mod context_menu;
+pub mod cursor_hint;
+pub mod delay_modifiers;
+pub mod disable;
+pub mod exit;
+pub mod hotkey;
+pub mod icon_status;
+pub mod image_resolve;
+pub mod markdown;
+pub mod match_select;
+pub mod matcher;
+pub mod multiplex;
+pub mod past_discard;
+pub mod render;
+pub mod search;
+pub mod suppress;
+pub mod undo;
diff --git a/espanso-engine/src/process/middleware/multiplex.rs b/espanso-engine/src/process/middleware/multiplex.rs
new file mode 100644
index 00000000..dbd38e74
--- /dev/null
+++ b/espanso-engine/src/process/middleware/multiplex.rs
@@ -0,0 +1,59 @@
+/*
+ * 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 log::error;
+
+use super::super::Middleware;
+use crate::event::{internal::DetectedMatch, Event, EventType};
+
+pub trait Multiplexer {
+  fn convert(&self, m: DetectedMatch) -> Option<EventType>;
+}
+
+pub struct MultiplexMiddleware<'a> {
+  multiplexer: &'a dyn Multiplexer,
+}
+
+impl<'a> MultiplexMiddleware<'a> {
+  pub fn new(multiplexer: &'a dyn Multiplexer) -> Self {
+    Self { multiplexer }
+  }
+}
+
+impl<'a> Middleware for MultiplexMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "multiplex"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::CauseCompensatedMatch(m_event) = event.etype {
+      return match self.multiplexer.convert(m_event.m) {
+        Some(new_event) => Event::caused_by(event.source_id, new_event),
+        None => {
+          error!("match multiplexing failed");
+          Event::caused_by(event.source_id, EventType::NOOP)
+        }
+      };
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/past_discard.rs b/espanso-engine/src/process/middleware/past_discard.rs
new file mode 100644
index 00000000..5b2b9b58
--- /dev/null
+++ b/espanso-engine/src/process/middleware/past_discard.rs
@@ -0,0 +1,69 @@
+/*
+ * 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::cell::RefCell;
+
+use log::trace;
+
+use super::super::Middleware;
+use crate::event::{Event, EventType, SourceId};
+
+/// This middleware discards all events that have a source_id smaller than its
+/// configured threshold. This useful to discard past events that might have
+/// been stuck in the event queue for too long.
+pub struct PastEventsDiscardMiddleware {
+  source_id_threshold: RefCell<SourceId>,
+}
+
+impl PastEventsDiscardMiddleware {
+  pub fn new() -> Self {
+    Self {
+      source_id_threshold: RefCell::new(0),
+    }
+  }
+}
+
+impl Middleware for PastEventsDiscardMiddleware {
+  fn name(&self) -> &'static str {
+    "past_discard"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    let mut source_id_threshold = self.source_id_threshold.borrow_mut();
+
+    // Filter out previous events
+    if event.source_id < *source_id_threshold {
+      trace!("discarding previous event: {:?}", event);
+      return Event::caused_by(event.source_id, EventType::NOOP);
+    }
+
+    // Update the minimum threshold
+    if let EventType::DiscardPrevious(m_event) = &event.etype {
+      trace!(
+        "updating minimum source id threshold for events to: {}",
+        m_event.minimum_source_id
+      );
+      *source_id_threshold = m_event.minimum_source_id;
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/render.rs b/espanso-engine/src/process/middleware/render.rs
new file mode 100644
index 00000000..c227c0c6
--- /dev/null
+++ b/espanso-engine/src/process/middleware/render.rs
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+use log::error;
+
+use super::super::Middleware;
+use crate::event::{internal::RenderedEvent, Event, EventType};
+use anyhow::Result;
+use thiserror::Error;
+
+pub trait Renderer<'a> {
+  fn render(
+    &'a self,
+    match_id: i32,
+    trigger: Option<&str>,
+    trigger_args: HashMap<String, String>,
+  ) -> Result<String>;
+}
+
+#[derive(Error, Debug)]
+pub enum RendererError {
+  #[error("rendering error")]
+  RenderingError(#[from] anyhow::Error),
+
+  #[error("match not found")]
+  NotFound,
+
+  #[error("aborted")]
+  Aborted,
+}
+
+pub struct RenderMiddleware<'a> {
+  renderer: &'a dyn Renderer<'a>,
+}
+
+impl<'a> RenderMiddleware<'a> {
+  pub fn new(renderer: &'a dyn Renderer<'a>) -> Self {
+    Self { renderer }
+  }
+}
+
+impl<'a> Middleware for RenderMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "render"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::RenderingRequested(m_event) = event.etype {
+      match self.renderer.render(
+        m_event.match_id,
+        m_event.trigger.as_deref(),
+        m_event.trigger_args,
+      ) {
+        Ok(body) => {
+          let body = if let Some(right_separator) = m_event.right_separator {
+            format!("{}{}", body, right_separator)
+          } else {
+            body
+          };
+
+          return Event::caused_by(
+            event.source_id,
+            EventType::Rendered(RenderedEvent {
+              match_id: m_event.match_id,
+              body,
+              format: m_event.format,
+            }),
+          );
+        }
+        Err(err) => match err.downcast_ref::<RendererError>() {
+          Some(RendererError::Aborted) => {
+            return Event::caused_by(event.source_id, EventType::NOOP)
+          }
+          _ => {
+            error!("error during rendering: {:?}", err);
+            return Event::caused_by(event.source_id, EventType::ProcessingError("An error has occurred during rendering, please examine the logs or contact support.".to_string()));
+          }
+        },
+      }
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/search.rs b/espanso-engine/src/process/middleware/search.rs
new file mode 100644
index 00000000..7b2168b7
--- /dev/null
+++ b/espanso-engine/src/process/middleware/search.rs
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+use super::super::Middleware;
+use crate::event::{
+  internal::{DetectedMatch, MatchesDetectedEvent},
+  Event, EventType,
+};
+
+pub trait MatchProvider {
+  fn get_all_matches_ids(&self) -> Vec<i32>;
+}
+
+pub struct SearchMiddleware<'a> {
+  match_provider: &'a dyn MatchProvider,
+}
+
+impl<'a> SearchMiddleware<'a> {
+  pub fn new(match_provider: &'a dyn MatchProvider) -> Self {
+    Self { match_provider }
+  }
+}
+
+impl<'a> Middleware for SearchMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "search"
+  }
+
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::ShowSearchBar = event.etype {
+      let detected_matches = Event::caused_by(
+        event.source_id,
+        EventType::MatchesDetected(MatchesDetectedEvent {
+          matches: self
+            .match_provider
+            .get_all_matches_ids()
+            .into_iter()
+            .map(|id| DetectedMatch {
+              id,
+              trigger: None,
+              left_separator: None,
+              right_separator: None,
+              args: HashMap::new(),
+            })
+            .collect(),
+          is_search: true,
+        }),
+      );
+      dispatch(detected_matches);
+
+      return Event::caused_by(event.source_id, EventType::NOOP);
+    }
+
+    event
+  }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/suppress.rs b/espanso-engine/src/process/middleware/suppress.rs
new file mode 100644
index 00000000..ad459314
--- /dev/null
+++ b/espanso-engine/src/process/middleware/suppress.rs
@@ -0,0 +1,54 @@
+/*
+ * 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 log::trace;
+
+use super::super::Middleware;
+use crate::event::{Event, EventType};
+
+pub trait EnabledStatusProvider {
+  fn is_config_enabled(&self) -> bool;
+}
+
+pub struct SuppressMiddleware<'a> {
+  provider: &'a dyn EnabledStatusProvider,
+}
+
+impl<'a> SuppressMiddleware<'a> {
+  pub fn new(provider: &'a dyn EnabledStatusProvider) -> Self {
+    Self { provider }
+  }
+}
+
+impl<'a> Middleware for SuppressMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "suppress"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    if let EventType::MatchesDetected(_) = event.etype {
+      if !self.provider.is_config_enabled() {
+        trace!("suppressing match detected event as active config has enable=false");
+        return Event::caused_by(event.source_id, EventType::NOOP);
+      }
+    }
+
+    event
+  }
+}
diff --git a/espanso-engine/src/process/middleware/undo.rs b/espanso-engine/src/process/middleware/undo.rs
new file mode 100644
index 00000000..29421e28
--- /dev/null
+++ b/espanso-engine/src/process/middleware/undo.rs
@@ -0,0 +1,115 @@
+/*
+ * 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::cell::RefCell;
+
+use super::super::Middleware;
+use crate::event::{
+  input::{Key, Status},
+  internal::{TextFormat, UndoEvent},
+  Event, EventType,
+};
+
+pub trait UndoEnabledProvider {
+  fn is_undo_enabled(&self) -> bool;
+}
+
+pub struct UndoMiddleware<'a> {
+  undo_enabled_provider: &'a dyn UndoEnabledProvider,
+  record: RefCell<Option<InjectionRecord>>,
+}
+
+impl<'a> UndoMiddleware<'a> {
+  pub fn new(undo_enabled_provider: &'a dyn UndoEnabledProvider) -> Self {
+    Self {
+      undo_enabled_provider,
+      record: RefCell::new(None),
+    }
+  }
+}
+
+impl<'a> Middleware for UndoMiddleware<'a> {
+  fn name(&self) -> &'static str {
+    "undo"
+  }
+
+  fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+    let mut record = self.record.borrow_mut();
+
+    if let EventType::TriggerCompensation(m_event) = &event.etype {
+      *record = Some(InjectionRecord {
+        id: Some(event.source_id),
+        trigger: Some(m_event.trigger.clone()),
+        ..Default::default()
+      });
+    } else if let EventType::Rendered(m_event) = &event.etype {
+      if let TextFormat::Plain = m_event.format {
+        if let Some(record) = &mut *record {
+          if record.id == Some(event.source_id) {
+            record.injected_text = Some(m_event.body.clone());
+            record.match_id = Some(m_event.match_id);
+          }
+        }
+      }
+    } else if let EventType::Keyboard(m_event) = &event.etype {
+      if m_event.status == Status::Pressed {
+        if m_event.key == Key::Backspace {
+          if let Some(record) = (*record).take() {
+            if let (Some(trigger), Some(injected_text), Some(match_id)) =
+              (record.trigger, record.injected_text, record.match_id)
+            {
+              if self.undo_enabled_provider.is_undo_enabled() {
+                return Event::caused_by(
+                  event.source_id,
+                  EventType::Undo(UndoEvent {
+                    match_id,
+                    trigger,
+                    replace: injected_text,
+                  }),
+                );
+              }
+            }
+          }
+        }
+        *record = None;
+      }
+    } else if let EventType::Mouse(_) = &event.etype {
+      // Any mouse event invalidates the undo feature, as it could
+      // represent a change in application
+      *record = None;
+    } else if let EventType::CursorHintCompensation(_) = &event.etype {
+      // Cursor hints invalidate the undo feature, as it would be pretty
+      // complex to determine which delete operations should be performed.
+      // This might change in the future.
+      *record = None;
+    }
+
+    event
+  }
+}
+
+#[derive(Default)]
+struct InjectionRecord {
+  id: Option<u32>,
+  match_id: Option<i32>,
+  trigger: Option<String>,
+  injected_text: Option<String>,
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs
new file mode 100644
index 00000000..91bce0d0
--- /dev/null
+++ b/espanso-engine/src/process/mod.rs
@@ -0,0 +1,83 @@
+/*
+ * 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 super::Event;
+
+mod default;
+mod middleware;
+
+pub trait Middleware {
+  fn name(&self) -> &'static str;
+  fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event;
+}
+
+pub trait Processor {
+  fn process(&mut self, event: Event) -> Vec<Event>;
+}
+
+// Dependency inversion entities
+
+pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
+pub use middleware::delay_modifiers::ModifierStatusProvider;
+pub use middleware::disable::DisableOptions;
+pub use middleware::image_resolve::PathProvider;
+pub use middleware::match_select::{MatchFilter, MatchSelector};
+pub use middleware::matcher::{
+  MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider,
+};
+pub use middleware::multiplex::Multiplexer;
+pub use middleware::render::{Renderer, RendererError};
+pub use middleware::search::MatchProvider;
+pub use middleware::suppress::EnabledStatusProvider;
+pub use middleware::undo::UndoEnabledProvider;
+
+#[allow(clippy::too_many_arguments)]
+pub fn default<'a, MatcherState>(
+  matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
+  match_filter: &'a dyn MatchFilter,
+  match_selector: &'a dyn MatchSelector,
+  multiplexer: &'a dyn Multiplexer,
+  renderer: &'a dyn Renderer<'a>,
+  match_info_provider: &'a dyn MatchInfoProvider,
+  modifier_status_provider: &'a dyn ModifierStatusProvider,
+  event_sequence_provider: &'a dyn EventSequenceProvider,
+  path_provider: &'a dyn PathProvider,
+  disable_options: DisableOptions,
+  matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider,
+  match_provider: &'a dyn MatchProvider,
+  undo_enabled_provider: &'a dyn UndoEnabledProvider,
+  enabled_status_provider: &'a dyn EnabledStatusProvider,
+) -> impl Processor + 'a {
+  default::DefaultProcessor::new(
+    matchers,
+    match_filter,
+    match_selector,
+    multiplexer,
+    renderer,
+    match_info_provider,
+    modifier_status_provider,
+    event_sequence_provider,
+    path_provider,
+    disable_options,
+    matcher_options_provider,
+    match_provider,
+    undo_enabled_provider,
+    enabled_status_provider,
+  )
+}
diff --git a/espanso-info/Cargo.toml b/espanso-info/Cargo.toml
new file mode 100644
index 00000000..56aa9791
--- /dev/null
+++ b/espanso-info/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "espanso-info"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[features]
+# If the wayland feature is enabled, all X11 dependencies will be dropped
+wayland = []
+
+[dependencies]
+log = "0.4.14"
+lazycell = "1.3.0"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+lazy_static = "1.4.0"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+
+[build-dependencies]
+cc = "1.0.66"
\ No newline at end of file
diff --git a/espanso-info/build.rs b/espanso-info/build.rs
new file mode 100644
index 00000000..6d3f7a59
--- /dev/null
+++ b/espanso-info/build.rs
@@ -0,0 +1,72 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/win32/native.cpp");
+  println!("cargo:rerun-if-changed=src/win32/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/win32/native.h")
+    .file("src/win32/native.cpp")
+    .compile("espansoinfo");
+
+  println!("cargo:rustc-link-lib=static=espansoinfo");
+  println!("cargo:rustc-link-lib=dylib=user32");
+  #[cfg(target_env = "gnu")]
+  println!("cargo:rustc-link-lib=dylib=stdc++");
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+  if cfg!(not(feature = "wayland")) {
+    println!("cargo:rerun-if-changed=src/x11/native.h");
+    println!("cargo:rerun-if-changed=src/x11/native.c");
+    cc::Build::new()
+      .cpp(true)
+      .include("src/x11")
+      .file("src/x11/native.cpp")
+      .compile("espansoinfo");
+
+    println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
+    println!("cargo:rustc-link-lib=static=espansoinfo");
+    println!("cargo:rustc-link-lib=dylib=stdc++");
+    println!("cargo:rustc-link-lib=dylib=X11");
+  } else {
+    // Nothing to compile on wayland
+  }
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/cocoa/native.mm");
+  println!("cargo:rerun-if-changed=src/cocoa/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/cocoa/native.h")
+    .file("src/cocoa/native.mm")
+    .compile("espansoinfo");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansoinfo");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-info/src/cocoa/ffi.rs b/espanso-info/src/cocoa/ffi.rs
new file mode 100644
index 00000000..473de730
--- /dev/null
+++ b/espanso-info/src/cocoa/ffi.rs
@@ -0,0 +1,28 @@
+/*
+ * 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::os::raw::c_char;
+
+#[link(name = "espansoinfo", kind = "static")]
+extern "C" {
+  pub fn info_get_title(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn info_get_title_fallback(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn info_get_exec(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn info_get_class(buffer: *mut c_char, buffer_size: i32) -> i32;
+}
diff --git a/espanso-info/src/cocoa/mod.rs b/espanso-info/src/cocoa/mod.rs
new file mode 100644
index 00000000..16f40c9f
--- /dev/null
+++ b/espanso-info/src/cocoa/mod.rs
@@ -0,0 +1,107 @@
+/*
+ * 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::{ffi::CStr, os::raw::c_char};
+
+use crate::{AppInfo, AppInfoProvider};
+
+use self::ffi::{info_get_class, info_get_exec, info_get_title, info_get_title_fallback};
+
+mod ffi;
+
+pub struct CocoaAppInfoProvider {}
+
+impl CocoaAppInfoProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl AppInfoProvider for CocoaAppInfoProvider {
+  fn get_info(&self) -> AppInfo {
+    AppInfo {
+      title: self.get_title().or_else(|| self.get_title_fallback()),
+      class: self.get_class(),
+      exec: self.get_exec(),
+    }
+  }
+}
+
+impl CocoaAppInfoProvider {
+  fn get_exec(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_exec(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  fn get_class(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  fn get_title(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_title(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  // Fallback using Accessibility API instead of Carbon
+  fn get_title_fallback(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_title_fallback(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+}
diff --git a/espanso-info/src/cocoa/native.h b/espanso-info/src/cocoa/native.h
new file mode 100644
index 00000000..77b8edc3
--- /dev/null
+++ b/espanso-info/src/cocoa/native.h
@@ -0,0 +1,30 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INFO_H
+#define ESPANSO_INFO_H
+
+#include <stdint.h>
+
+extern "C" int32_t info_get_title(char * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_title_fallback(char * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_exec(char * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_class(char * buffer, int32_t buffer_size);
+
+#endif //ESPANSO_INFO_H
\ No newline at end of file
diff --git a/espanso-info/src/cocoa/native.mm b/espanso-info/src/cocoa/native.mm
new file mode 100644
index 00000000..86d9403f
--- /dev/null
+++ b/espanso-info/src/cocoa/native.mm
@@ -0,0 +1,123 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+
+int32_t info_get_title(char *buffer, int32_t buffer_size)
+{
+  @autoreleasepool {
+    CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
+    int32_t result = 0;
+
+    if (windows) {
+      for (NSDictionary *window in (NSArray *)windows) {
+        NSNumber *ownerPid = window[(id) kCGWindowOwnerPID];
+
+        NSRunningApplication *currentApp = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]];
+
+        if ([currentApp isActive]) {
+          NSString *name = window[(id) kCGWindowName];
+          if (name.length > 0) {
+            const char * title = [name UTF8String];
+            snprintf(buffer, buffer_size, "%s", title);
+            result = 1;
+          }
+          break;
+        }
+      }
+
+      CFRelease(windows);
+    }
+  }
+
+  return 0;
+}
+
+// Partially taken from: https://stackoverflow.com/questions/480866/get-the-title-of-the-current-active-window-document-in-mac-os-x/23451568#23451568
+int32_t info_get_title_fallback(char *buffer, int32_t buffer_size)
+{
+  @autoreleasepool {
+    // Get the process ID of the frontmost application.
+    NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
+    pid_t pid = [app processIdentifier];
+
+    AXUIElementRef appElem = AXUIElementCreateApplication(pid);
+    if (!appElem) {
+      return -1;
+    }
+
+    // Get the accessibility element corresponding to the frontmost window
+    // of the frontmost application.
+    AXUIElementRef window = NULL;
+    if (AXUIElementCopyAttributeValue(appElem, 
+          kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) {
+      CFRelease(appElem);
+      return -2;
+    }
+
+    // Finally, get the title of the frontmost window.
+    CFStringRef title = NULL;
+    AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute,
+                      (CFTypeRef*)&title);
+
+    // At this point, we don't need window and appElem anymore.
+    CFRelease(window);
+    CFRelease(appElem);
+
+    if (result != kAXErrorSuccess) {
+      // Failed to get the window title.
+      return -3;
+    }
+
+    if (CFStringGetCString(title, buffer, buffer_size, kCFStringEncodingUTF8)) {
+      CFRelease(title);
+      return 1;
+    } else {
+      return -4;
+    }
+  }
+}
+
+int32_t info_get_exec(char *buffer, int32_t buffer_size)
+{
+  @autoreleasepool {
+    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
+    NSString *bundlePath = [frontApp bundleURL].path;
+    const char * path = [bundlePath UTF8String];
+
+    snprintf(buffer, buffer_size, "%s", path);
+  }
+
+  return 1;
+}
+
+int32_t info_get_class(char *buffer, int32_t buffer_size)
+{
+  @autoreleasepool {
+    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
+    NSString *bundleId = frontApp.bundleIdentifier;
+    const char * bundle = [bundleId UTF8String];
+
+    snprintf(buffer, buffer_size, "%s", bundle);
+  }
+
+  return 1;
+}
\ No newline at end of file
diff --git a/espanso-info/src/lib.rs b/espanso-info/src/lib.rs
new file mode 100644
index 00000000..2e2767cf
--- /dev/null
+++ b/espanso-info/src/lib.rs
@@ -0,0 +1,72 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::info;
+
+#[cfg(target_os = "windows")]
+mod win32;
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+mod x11;
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+mod wayland;
+
+#[cfg(target_os = "macos")]
+mod cocoa;
+
+pub trait AppInfoProvider {
+  fn get_info(&self) -> AppInfo;
+}
+
+#[derive(Debug, Clone)]
+pub struct AppInfo {
+  pub title: Option<String>,
+  pub exec: Option<String>,
+  pub class: Option<String>,
+}
+
+#[cfg(target_os = "windows")]
+pub fn get_provider() -> Result<Box<dyn AppInfoProvider>> {
+  info!("using Win32AppInfoProvider");
+  Ok(Box::new(win32::WinAppInfoProvider::new()))
+}
+
+#[cfg(target_os = "macos")]
+pub fn get_provider() -> Result<Box<dyn AppInfoProvider>> {
+  info!("using CocoaAppInfoProvider");
+  Ok(Box::new(cocoa::CocoaAppInfoProvider::new()))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub fn get_provider() -> Result<Box<dyn AppInfoProvider>> {
+  info!("using X11AppInfoProvider");
+  Ok(Box::new(x11::X11AppInfoProvider::new()))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+pub fn get_provider() -> Result<Box<dyn AppInfoProvider>> {
+  info!("using WaylandAppInfoProvider");
+  Ok(Box::new(wayland::WaylandAppInfoProvider::new()))
+}
diff --git a/espanso-info/src/wayland/mod.rs b/espanso-info/src/wayland/mod.rs
new file mode 100644
index 00000000..e4cb3c64
--- /dev/null
+++ b/espanso-info/src/wayland/mod.rs
@@ -0,0 +1,39 @@
+/*
+ * 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 crate::{AppInfo, AppInfoProvider};
+
+pub(crate) struct WaylandAppInfoProvider {}
+
+impl WaylandAppInfoProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl AppInfoProvider for WaylandAppInfoProvider {
+  // TODO: can we read these info on Wayland?
+  fn get_info(&self) -> AppInfo {
+    AppInfo {
+      title: None,
+      exec: None,
+      class: None,
+    }
+  }
+}
diff --git a/espanso-info/src/win32/ffi.rs b/espanso-info/src/win32/ffi.rs
new file mode 100644
index 00000000..9d8b9644
--- /dev/null
+++ b/espanso-info/src/win32/ffi.rs
@@ -0,0 +1,24 @@
+/*
+ * 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/>.
+ */
+
+#[link(name = "espansoinfo", kind = "static")]
+extern "C" {
+  pub fn info_get_title(buffer: *mut u16, buffer_size: i32) -> i32;
+  pub fn info_get_exec(buffer: *mut u16, buffer_size: i32) -> i32;
+}
diff --git a/espanso-info/src/win32/mod.rs b/espanso-info/src/win32/mod.rs
new file mode 100644
index 00000000..cb40ece5
--- /dev/null
+++ b/espanso-info/src/win32/mod.rs
@@ -0,0 +1,76 @@
+/*
+ * 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 widestring::U16CStr;
+
+use crate::{AppInfo, AppInfoProvider};
+
+use self::ffi::{info_get_exec, info_get_title};
+
+mod ffi;
+
+pub struct WinAppInfoProvider {}
+
+impl WinAppInfoProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl AppInfoProvider for WinAppInfoProvider {
+  fn get_info(&self) -> AppInfo {
+    AppInfo {
+      title: self.get_title(),
+      class: None,
+      exec: self.get_exec(),
+    }
+  }
+}
+
+impl WinAppInfoProvider {
+  fn get_exec(&self) -> Option<String> {
+    let mut buffer: [u16; 2048] = [0; 2048];
+    if unsafe { info_get_exec(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } != 0 {
+      let string = unsafe { U16CStr::from_ptr_str(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string)
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  fn get_title(&self) -> Option<String> {
+    let mut buffer: [u16; 2048] = [0; 2048];
+    if unsafe { info_get_title(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { U16CStr::from_ptr_str(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string)
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+}
diff --git a/espanso-info/src/win32/native.cpp b/espanso-info/src/win32/native.cpp
new file mode 100644
index 00000000..ef00d638
--- /dev/null
+++ b/espanso-info/src/win32/native.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <iostream>
+#include <stdio.h>
+#include <string>
+#include <vector>
+#include <memory>
+#include <array>
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include <windows.h>
+#include <winuser.h>
+#include <strsafe.h>
+
+#include <Windows.h>
+
+int32_t info_get_title(wchar_t *buffer, int32_t buffer_size)
+{
+  HWND hwnd = GetForegroundWindow();
+  return GetWindowText(hwnd, buffer, buffer_size);
+}
+
+int32_t info_get_exec(wchar_t *buffer, int32_t buffer_size)
+{
+  HWND hwnd = GetForegroundWindow();
+
+  // Extract the window PID
+  DWORD windowPid;
+  GetWindowThreadProcessId(hwnd, &windowPid);
+
+  DWORD dsize = (DWORD)buffer_size;
+
+  // Extract the process executable file path
+  HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid);
+  int res = QueryFullProcessImageNameW(process, 0, buffer, &dsize);
+  CloseHandle(process);
+
+  return res;
+}
\ No newline at end of file
diff --git a/espanso-info/src/win32/native.h b/espanso-info/src/win32/native.h
new file mode 100644
index 00000000..6f6dd027
--- /dev/null
+++ b/espanso-info/src/win32/native.h
@@ -0,0 +1,28 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INFO_H
+#define ESPANSO_INFO_H
+
+#include <stdint.h>
+
+extern "C" int32_t info_get_title(wchar_t * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_exec(wchar_t * buffer, int32_t buffer_size);
+
+#endif //ESPANSO_INFO_H
\ No newline at end of file
diff --git a/espanso-info/src/x11/ffi.rs b/espanso-info/src/x11/ffi.rs
new file mode 100644
index 00000000..4a94f7b9
--- /dev/null
+++ b/espanso-info/src/x11/ffi.rs
@@ -0,0 +1,27 @@
+/*
+ * 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::os::raw::c_char;
+
+#[link(name = "espansoinfo", kind = "static")]
+extern "C" {
+  pub fn info_get_title(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn info_get_exec(buffer: *mut c_char, buffer_size: i32) -> i32;
+  pub fn info_get_class(buffer: *mut c_char, buffer_size: i32) -> i32;
+}
diff --git a/espanso-info/src/x11/mod.rs b/espanso-info/src/x11/mod.rs
new file mode 100644
index 00000000..54a64c7c
--- /dev/null
+++ b/espanso-info/src/x11/mod.rs
@@ -0,0 +1,91 @@
+/*
+ * 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::{ffi::CStr, os::raw::c_char};
+
+use crate::{AppInfo, AppInfoProvider};
+
+use self::ffi::{info_get_class, info_get_exec, info_get_title};
+
+mod ffi;
+
+pub struct X11AppInfoProvider {}
+
+impl X11AppInfoProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl AppInfoProvider for X11AppInfoProvider {
+  fn get_info(&self) -> AppInfo {
+    AppInfo {
+      title: self.get_title(),
+      class: self.get_class(),
+      exec: self.get_exec(),
+    }
+  }
+}
+
+impl X11AppInfoProvider {
+  fn get_exec(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_exec(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  fn get_class(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
+  fn get_title(&self) -> Option<String> {
+    let mut buffer: [c_char; 2048] = [0; 2048];
+    if unsafe { info_get_title(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) } > 0 {
+      let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
+      let string = string.to_string_lossy();
+      if !string.is_empty() {
+        Some(string.to_string())
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+}
diff --git a/espanso-info/src/x11/native.cpp b/espanso-info/src/x11/native.cpp
new file mode 100644
index 00000000..910d3451
--- /dev/null
+++ b/espanso-info/src/x11/native.cpp
@@ -0,0 +1,214 @@
+/*
+ * 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/>.
+ */
+
+#include "native.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/XKBlib.h>
+#include <X11/Xatom.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+// Function taken from the wmlib tool source code
+char *get_property(Display *disp, Window win,
+                   Atom xa_prop_type, char *prop_name, unsigned long *size)
+{
+  unsigned long ret_nitems, ret_bytes_after, tmp_size;
+  Atom xa_prop_name, xa_ret_type;
+  unsigned char *ret_prop;
+  int ret_format;
+  char *ret;
+  int size_in_byte;
+
+  xa_prop_name = XInternAtom(disp, prop_name, False);
+
+  if (XGetWindowProperty(disp, win, xa_prop_name, 0, 4096 / 4, False,
+                         xa_prop_type, &xa_ret_type, &ret_format, &ret_nitems,
+                         &ret_bytes_after, &ret_prop) != Success)
+    return NULL;
+
+  if (xa_ret_type != xa_prop_type)
+  {
+    XFree(ret_prop);
+    return NULL;
+  }
+
+  switch (ret_format)
+  {
+  case 8:
+    size_in_byte = sizeof(char);
+    break;
+  case 16:
+    size_in_byte = sizeof(short);
+    break;
+  case 32:
+    size_in_byte = sizeof(long);
+    break;
+  }
+
+  tmp_size = size_in_byte * ret_nitems;
+  ret = (char *)malloc(tmp_size + 1);
+  memcpy(ret, ret_prop, tmp_size);
+  ret[tmp_size] = '\0';
+
+  if (size)
+    *size = tmp_size;
+
+  XFree(ret_prop);
+  return ret;
+}
+
+// Function taken from Window Management Library for Ruby
+char *xwm_get_win_title(Display *disp, Window win)
+{
+  char *wname = (char *)get_property(disp, win, XA_STRING, (char*)"WM_NAME", NULL);
+  char *nwname = (char *)get_property(disp, win, XInternAtom(disp, (char*)"UTF8_STRING", False), (char*)"_NET_WM_NAME", NULL);
+
+  return nwname ? nwname : (wname ? wname : NULL);
+}
+
+// Function taken from the wmctrl tool source code
+Window get_active_window(Display *disp) {
+  char *prop;
+  unsigned long size;
+  Window ret = (Window)0;
+  
+  prop = get_property(disp, DefaultRootWindow(disp), XA_WINDOW, 
+                      "_NET_ACTIVE_WINDOW", &size);
+  if (prop) {
+    ret = *((Window*)prop);
+    XFree(prop);
+  }
+
+  return(ret);
+}
+
+int32_t info_get_title(char *buffer, int32_t buffer_size)
+{
+  Display *display = XOpenDisplay(0);
+
+  if (!display)
+  {
+    return -1;
+  }
+
+  Window focused = get_active_window(display);
+
+  int result = 1;
+  if (!focused)
+  {
+    fprintf(stderr, "get_active_window reported an error\n");
+    result = -2;
+  }
+  else
+  {
+    char *title = xwm_get_win_title(display, focused);
+
+    snprintf(buffer, buffer_size, "%s", title);
+
+    XFree(title);
+  }
+
+  XCloseDisplay(display);
+
+  return result;
+}
+
+int32_t info_get_exec(char *buffer, int32_t buffer_size)
+{
+  Display *display = XOpenDisplay(0);
+
+  if (!display)
+  {
+    return -1;
+  }
+
+  Window focused = get_active_window(display);
+
+  int result = 1;
+  if (!focused)
+  {
+    fprintf(stderr, "get_active_window reported an error\n");
+    result = -2;
+  }
+  else
+  {
+    // Get the window process PID
+    char *pid_raw = (char *)get_property(display, focused, XA_CARDINAL, (char*)"_NET_WM_PID", NULL);
+    if (pid_raw == NULL)
+    {
+      result = -3;
+    }
+    else
+    {
+      int pid = pid_raw[0] | pid_raw[1] << 8 | pid_raw[2] << 16 | pid_raw[3] << 24;
+
+      // Get the executable path from it
+      char proc_path[250];
+      snprintf(proc_path, 250, "/proc/%d/exe", pid);
+
+      readlink(proc_path, buffer, buffer_size);
+
+      XFree(pid_raw);
+    }
+  }
+
+  XCloseDisplay(display);
+
+  return result;
+}
+
+int32_t info_get_class(char *buffer, int32_t buffer_size)
+{
+  Display *display = XOpenDisplay(0);
+
+  if (!display)
+  {
+    return -1;
+  }
+
+  Window focused = get_active_window(display);
+
+  int result = 1;
+  if (!focused)
+  {
+    fprintf(stderr, "get_active_window reported an error\n");
+    result = -2;
+  }
+  else
+  {
+    XClassHint hint;
+
+    if (XGetClassHint(display, focused, &hint))
+    {
+      snprintf(buffer, buffer_size, "%s", hint.res_class);
+      XFree(hint.res_name);
+      XFree(hint.res_class);
+    }
+  }
+
+  XCloseDisplay(display);
+
+  return result;
+}
\ No newline at end of file
diff --git a/espanso-info/src/x11/native.h b/espanso-info/src/x11/native.h
new file mode 100644
index 00000000..027299ab
--- /dev/null
+++ b/espanso-info/src/x11/native.h
@@ -0,0 +1,29 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INFO_H
+#define ESPANSO_INFO_H
+
+#include <stdint.h>
+
+extern "C" int32_t info_get_title(char * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_exec(char * buffer, int32_t buffer_size);
+extern "C" int32_t info_get_class(char * buffer, int32_t buffer_size);
+
+#endif //ESPANSO_INFO_H
\ No newline at end of file
diff --git a/espanso-inject/Cargo.toml b/espanso-inject/Cargo.toml
new file mode 100644
index 00000000..5a0c9a0a
--- /dev/null
+++ b/espanso-inject/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "espanso-inject"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[features]
+# If the wayland feature is enabled, all X11 dependencies will be dropped
+# and only EVDEV-based methods will be supported.
+wayland = []
+
+[dependencies]
+log = "0.4.14"
+lazycell = "1.3.0"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+lazy_static = "1.4.0"
+regex = "1.4.3"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+
+[target.'cfg(target_os="linux")'.dependencies]
+libc = "0.2.85"
+scopeguard = "1.1.0"
+itertools = "0.10.0"
+
+[build-dependencies]
+cc = "1.0.66"
+
+[dev-dependencies]
+enum-as-inner = "0.3.3"
\ No newline at end of file
diff --git a/espanso-inject/build.rs b/espanso-inject/build.rs
new file mode 100644
index 00000000..16bd1a4a
--- /dev/null
+++ b/espanso-inject/build.rs
@@ -0,0 +1,71 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/win32/native.cpp");
+  println!("cargo:rerun-if-changed=src/win32/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/win32/native.h")
+    .file("src/win32/native.cpp")
+    .compile("espansoinject");
+
+  println!("cargo:rustc-link-lib=static=espansoinject");
+  println!("cargo:rustc-link-lib=dylib=user32");
+  #[cfg(target_env = "gnu")]
+  println!("cargo:rustc-link-lib=dylib=stdc++");
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/evdev/native.h");
+  println!("cargo:rerun-if-changed=src/evdev/native.c");
+  cc::Build::new()
+    .include("src/evdev")
+    .file("src/evdev/native.c")
+    .compile("espansoinjectev");
+
+  println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
+  println!("cargo:rustc-link-lib=static=espansoinjectev");
+  println!("cargo:rustc-link-lib=dylib=xkbcommon");
+
+  if cfg!(not(feature = "wayland")) {
+    println!("cargo:rustc-link-lib=dylib=X11");
+    println!("cargo:rustc-link-lib=dylib=Xtst");
+  }
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/mac/native.mm");
+  println!("cargo:rerun-if-changed=src/mac/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/mac/native.h")
+    .file("src/mac/native.mm")
+    .compile("espansoinject");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansoinject");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-inject/src/evdev/README.md b/espanso-inject/src/evdev/README.md
new file mode 100644
index 00000000..0c70cb0a
--- /dev/null
+++ b/espanso-inject/src/evdev/README.md
@@ -0,0 +1,48 @@
+To support EVDEV injection
+
+At startup, the EVDEVInjector has to populate a lookup table to find the string <-> keycode + modifiers pairs.
+
+* We know that the keycode goes from 1 to 256
+* We can then rely on the `xkb_state_key_get_utf8` to find the string correspondent to each keycode
+* Then we cycle between every modifier combination, updating the `xkb_state` with `xkb_state_update_key`
+
+Ref: https://xkbcommon.org/doc/current/structxkb__keymap.html
+
+```
+  1 #include <xkbcommon/xkbcommon.h>
+  2 #include <stdio.h>
+  3 
+  4 int main() {
+  5   struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+  6   struct xkb_keymap *keymap =xkb_keymap_new_from_names(ctx, NULL, 0);
+  7   struct xkb_state *state = xkb_state_new(keymap);
+  8   // a = 38
+  9 
+ 10   xkb_state_update_key(state, 42 + 8, XKB_KEY_DOWN);
+ 11 
+ 12   xkb_layout_index_t num = xkb_keymap_num_layouts_for_key(keymap, 42 + 8);
+ 13   char buff[10];
+ 14   xkb_state_key_get_utf8(state, 38, buff, 9);
+ 15 
+ 16   printf("hey %s %d\n", buff, num);
+ 17 }
+```
+
+The available modifiers can be found in the https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
+
+#define KEY_LEFTCTRL		29
+#define KEY_LEFTSHIFT		42
+#define KEY_RIGHTSHIFT		54
+#define KEY_LEFTALT		56
+#define KEY_LEFTMETA		125
+#define KEY_RIGHTMETA		126
+#define KEY_RIGHTCTRL		97
+#define KEY_RIGHTALT		100
+#define KEY_CAPSLOCK		58
+#define KEY_NUMLOCK		69
+
+All these codes have to be added the EVDEV_OFFSET = 8
+
+From the lookup table, we can generate the input event as shown here: https://github.com/ReimuNotMoe/ydotool/blob/7972e5e3390489c1395b06ca9dc7639763c7cc98/Tools/Type/Type.cpp
+
+Note that we also need to inject the correct modifiers to obtain the text
\ No newline at end of file
diff --git a/espanso-inject/src/evdev/context.rs b/espanso-inject/src/evdev/context.rs
new file mode 100644
index 00000000..bcda581c
--- /dev/null
+++ b/espanso-inject/src/evdev/context.rs
@@ -0,0 +1,47 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use scopeguard::ScopeGuard;
+
+use super::ffi::{xkb_context, xkb_context_new, xkb_context_unref, XKB_CONTEXT_NO_FLAGS};
+use anyhow::Result;
+use thiserror::Error;
+
+pub struct Context {
+  context: *mut xkb_context,
+}
+
+impl Context {
+  pub fn new() -> Result<Context> {
+    let raw_context = unsafe { xkb_context_new(XKB_CONTEXT_NO_FLAGS) };
+    let context = scopeguard::guard(raw_context, |raw_context| unsafe {
+      xkb_context_unref(raw_context);
+    });
+
+    if raw_context.is_null() {
+      return Err(ContextError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      context: ScopeGuard::into_inner(context),
+    })
+  }
+
+  pub fn get_handle(&self) -> *mut xkb_context {
+    self.context
+  }
+}
+
+impl Drop for Context {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_context_unref(self.context);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ContextError {
+  #[error("could not create xkb context")]
+  FailedCreation(),
+}
diff --git a/espanso-inject/src/evdev/ffi.rs b/espanso-inject/src/evdev/ffi.rs
new file mode 100644
index 00000000..18c96eeb
--- /dev/null
+++ b/espanso-inject/src/evdev/ffi.rs
@@ -0,0 +1,85 @@
+// Bindings taken from: https://github.com/rtbo/xkbcommon-rs/blob/master/src/xkb/ffi.rs
+
+use std::os::raw::c_int;
+
+use libc::{c_char, c_uint, c_ulong};
+
+#[allow(non_camel_case_types)]
+pub enum xkb_context {}
+#[allow(non_camel_case_types)]
+pub enum xkb_state {}
+#[allow(non_camel_case_types)]
+pub enum xkb_keymap {}
+#[allow(non_camel_case_types)]
+pub type xkb_keycode_t = u32;
+#[allow(non_camel_case_types)]
+pub type xkb_keysym_t = u32;
+
+#[repr(C)]
+pub struct xkb_rule_names {
+  pub rules: *const c_char,
+  pub model: *const c_char,
+  pub layout: *const c_char,
+  pub variant: *const c_char,
+  pub options: *const c_char,
+}
+
+#[repr(C)]
+#[allow(clippy::upper_case_acronyms)]
+pub enum xkb_key_direction {
+  UP,
+  DOWN,
+}
+
+#[allow(non_camel_case_types)]
+pub type xkb_keymap_compile_flags = u32;
+pub const XKB_KEYMAP_COMPILE_NO_FLAGS: u32 = 0;
+
+#[allow(non_camel_case_types)]
+pub type xkb_context_flags = u32;
+pub const XKB_CONTEXT_NO_FLAGS: u32 = 0;
+
+#[allow(non_camel_case_types)]
+pub type xkb_state_component = u32;
+
+pub const EV_KEY: u16 = 0x01;
+
+#[link(name = "xkbcommon")]
+extern "C" {
+  pub fn xkb_state_unref(state: *mut xkb_state);
+  pub fn xkb_state_new(keymap: *mut xkb_keymap) -> *mut xkb_state;
+  pub fn xkb_keymap_new_from_names(
+    context: *mut xkb_context,
+    names: *const xkb_rule_names,
+    flags: xkb_keymap_compile_flags,
+  ) -> *mut xkb_keymap;
+  pub fn xkb_keymap_unref(keymap: *mut xkb_keymap);
+  pub fn xkb_context_new(flags: xkb_context_flags) -> *mut xkb_context;
+  pub fn xkb_context_unref(context: *mut xkb_context);
+  pub fn xkb_state_update_key(
+    state: *mut xkb_state,
+    key: xkb_keycode_t,
+    direction: xkb_key_direction,
+  ) -> xkb_state_component;
+  pub fn xkb_state_key_get_utf8(
+    state: *mut xkb_state,
+    key: xkb_keycode_t,
+    buffer: *mut c_char,
+    size: usize,
+  ) -> c_int;
+  pub fn xkb_state_key_get_one_sym(state: *mut xkb_state, key: xkb_keycode_t) -> xkb_keysym_t;
+}
+
+// These are used to retrieve constants from the C side.
+// This is needed as those constants are defined with C macros,
+// and converting them manually is error-prone.
+#[link(name = "espansoinjectev", kind = "static")]
+extern "C" {
+  pub fn ui_dev_create() -> c_ulong;
+  pub fn ui_dev_destroy() -> c_ulong;
+  pub fn ui_set_evbit() -> c_ulong;
+  pub fn ui_set_keybit() -> c_ulong;
+
+  pub fn setup_uinput_device(fd: c_int) -> c_int;
+  pub fn uinput_emit(fd: c_int, code: c_uint, pressed: c_int);
+}
diff --git a/espanso-inject/src/evdev/keymap.rs b/espanso-inject/src/evdev/keymap.rs
new file mode 100644
index 00000000..341bd632
--- /dev/null
+++ b/espanso-inject/src/evdev/keymap.rs
@@ -0,0 +1,112 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use std::ffi::CString;
+
+use scopeguard::ScopeGuard;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::KeyboardConfig;
+
+use super::{
+  context::Context,
+  ffi::{
+    xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref, xkb_rule_names,
+    XKB_KEYMAP_COMPILE_NO_FLAGS,
+  },
+};
+
+pub struct Keymap {
+  keymap: *mut xkb_keymap,
+}
+
+impl Keymap {
+  pub fn new(context: &Context, rmlvo: Option<KeyboardConfig>) -> Result<Keymap> {
+    let owned_rmlvo = Self::generate_owned_rmlvo(rmlvo);
+    let names = Self::generate_names(&owned_rmlvo);
+
+    let raw_keymap = unsafe {
+      xkb_keymap_new_from_names(context.get_handle(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS)
+    };
+    let keymap = scopeguard::guard(raw_keymap, |raw_keymap| unsafe {
+      xkb_keymap_unref(raw_keymap);
+    });
+
+    if raw_keymap.is_null() {
+      return Err(KeymapError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      keymap: ScopeGuard::into_inner(keymap),
+    })
+  }
+
+  pub fn get_handle(&self) -> *mut xkb_keymap {
+    self.keymap
+  }
+
+  fn generate_owned_rmlvo(rmlvo: Option<KeyboardConfig>) -> OwnedRawKeyboardConfig {
+    let rules = rmlvo
+      .as_ref()
+      .and_then(|config| config.rules.clone())
+      .unwrap_or_default();
+    let model = rmlvo
+      .as_ref()
+      .and_then(|config| config.model.clone())
+      .unwrap_or_default();
+    let layout = rmlvo
+      .as_ref()
+      .and_then(|config| config.layout.clone())
+      .unwrap_or_default();
+    let variant = rmlvo
+      .as_ref()
+      .and_then(|config| config.variant.clone())
+      .unwrap_or_default();
+    let options = rmlvo
+      .as_ref()
+      .and_then(|config| config.options.clone())
+      .unwrap_or_default();
+
+    OwnedRawKeyboardConfig {
+      rules: CString::new(rules).expect("unable to create CString for keymap"),
+      model: CString::new(model).expect("unable to create CString for keymap"),
+      layout: CString::new(layout).expect("unable to create CString for keymap"),
+      variant: CString::new(variant).expect("unable to create CString for keymap"),
+      options: CString::new(options).expect("unable to create CString for keymap"),
+    }
+  }
+
+  fn generate_names(owned_config: &OwnedRawKeyboardConfig) -> xkb_rule_names {
+    xkb_rule_names {
+      rules: owned_config.rules.as_ptr(),
+      model: owned_config.model.as_ptr(),
+      layout: owned_config.layout.as_ptr(),
+      variant: owned_config.variant.as_ptr(),
+      options: owned_config.options.as_ptr(),
+    }
+  }
+}
+
+impl Drop for Keymap {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_keymap_unref(self.keymap);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum KeymapError {
+  #[error("could not create xkb keymap")]
+  FailedCreation(),
+}
+
+struct OwnedRawKeyboardConfig {
+  rules: CString,
+  model: CString,
+  layout: CString,
+  variant: CString,
+  options: CString,
+}
diff --git a/espanso-inject/src/evdev/mod.rs b/espanso-inject/src/evdev/mod.rs
new file mode 100644
index 00000000..5e0d3cb3
--- /dev/null
+++ b/espanso-inject/src/evdev/mod.rs
@@ -0,0 +1,385 @@
+/*
+ * 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/>.
+ */
+
+mod context;
+mod ffi;
+mod keymap;
+mod state;
+mod uinput;
+
+use std::{
+  collections::{HashMap, HashSet},
+  ffi::CString,
+  time::Instant,
+};
+
+use context::Context;
+use keymap::Keymap;
+use log::{error, warn};
+use uinput::UInputDevice;
+
+use crate::{
+  linux::raw_keys::convert_to_sym_array, InjectorCreationOptions, KeyboardStateProvider,
+};
+use anyhow::{bail, Result};
+use itertools::Itertools;
+use thiserror::Error;
+
+use crate::{keys, InjectionOptions, Injector};
+
+use self::state::State;
+
+// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
+// keycode set (where ESC is 9).
+const EVDEV_OFFSET: u32 = 8;
+
+// List of modifier keycodes, as defined in the "input-event-codes.h" header
+// These can be overridden by changing the "evdev_modifier" option during initialization
+const KEY_LEFTCTRL: u32 = 29;
+const KEY_LEFTSHIFT: u32 = 42;
+const KEY_RIGHTSHIFT: u32 = 54;
+const KEY_LEFTALT: u32 = 56;
+const KEY_LEFTMETA: u32 = 125;
+const KEY_RIGHTMETA: u32 = 126;
+const KEY_RIGHTCTRL: u32 = 97;
+const KEY_RIGHTALT: u32 = 100;
+const KEY_CAPSLOCK: u32 = 58;
+const KEY_NUMLOCK: u32 = 69;
+
+const DEFAULT_MODIFIERS: [u32; 10] = [
+  KEY_LEFTCTRL,
+  KEY_LEFTSHIFT,
+  KEY_RIGHTSHIFT,
+  KEY_LEFTALT,
+  KEY_LEFTMETA,
+  KEY_RIGHTMETA,
+  KEY_RIGHTCTRL,
+  KEY_RIGHTALT,
+  KEY_CAPSLOCK,
+  KEY_NUMLOCK,
+];
+
+const DEFAULT_MAX_MODIFIER_COMBINATION_LEN: i32 = 3;
+
+// TODO: make the timeout a configurable option
+const DEFAULT_WAIT_KEY_RELEASE_TIMEOUT_MS: u64 = 4000;
+
+pub type KeySym = u32;
+
+#[derive(Clone, Debug)]
+struct KeyRecord {
+  // Keycode
+  code: u32,
+  // List of modifiers that must be pressed
+  modifiers: Vec<u32>,
+}
+
+type CharMap = HashMap<String, KeyRecord>;
+type SymMap = HashMap<KeySym, KeyRecord>;
+
+pub struct EVDEVInjector {
+  device: UInputDevice,
+
+  // Lookup maps
+  char_map: CharMap,
+  sym_map: SymMap,
+
+  // Ownership
+  _context: Context,
+  _keymap: Keymap,
+
+  // Keyboard state provider
+  keyboard_state_provider: Option<Box<dyn KeyboardStateProvider>>,
+}
+
+#[allow(clippy::new_without_default)]
+impl EVDEVInjector {
+  pub fn new(options: InjectorCreationOptions) -> Result<Self> {
+    let modifiers = options
+      .evdev_modifiers
+      .unwrap_or_else(|| DEFAULT_MODIFIERS.to_vec());
+    let max_modifier_combination_len = options
+      .evdev_max_modifier_combination_len
+      .unwrap_or(DEFAULT_MAX_MODIFIER_COMBINATION_LEN);
+
+    // Necessary to properly handle non-ascii chars
+    let empty_string = CString::new("")?;
+    unsafe {
+      libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
+    }
+
+    let context = Context::new().expect("unable to obtain xkb context");
+    let keymap =
+      Keymap::new(&context, options.evdev_keyboard_rmlvo).expect("unable to create xkb keymap");
+
+    let (char_map, sym_map) =
+      Self::generate_maps(&modifiers, max_modifier_combination_len, &keymap)?;
+
+    // Create the uinput virtual device
+    let device = UInputDevice::new()?;
+
+    if options.keyboard_state_provider.is_none() {
+      warn!("EVDEVInjection has been initialized without a KeyboardStateProvider, which might result in partial injections.");
+    }
+
+    Ok(Self {
+      device,
+      char_map,
+      sym_map,
+      _context: context,
+      _keymap: keymap,
+      keyboard_state_provider: options.keyboard_state_provider,
+    })
+  }
+
+  fn generate_maps(
+    modifiers: &[u32],
+    max_modifier_sequence_len: i32,
+    keymap: &Keymap,
+  ) -> Result<(CharMap, SymMap)> {
+    let mut char_map = HashMap::new();
+    let mut sym_map = HashMap::new();
+
+    let modifier_combinations = Self::generate_combinations(modifiers, max_modifier_sequence_len);
+
+    // Cycle through all code/modifiers combinations to populate the reverse lookup tables
+    for key_code in 8..256u32 {
+      for modifier_combination in modifier_combinations.iter() {
+        let state = State::new(keymap)?;
+
+        // Apply the modifiers
+        for modifier in modifier_combination.iter() {
+          // We need to add the EVDEV offset for xkbcommon to recognize it correctly
+          state.update_key(*modifier + EVDEV_OFFSET, true);
+        }
+
+        let key_record = KeyRecord {
+          code: key_code - EVDEV_OFFSET,
+          modifiers: modifier_combination.clone(),
+        };
+
+        // Keysym was found
+        if let Some(sym) = state.get_sym(key_code) {
+          sym_map.entry(sym).or_insert_with(|| key_record.clone());
+        }
+
+        // Char was found
+        if let Some(string) = state.get_string(key_code) {
+          char_map.entry(string).or_insert(key_record);
+        }
+      }
+    }
+
+    Ok((char_map, sym_map))
+  }
+
+  fn generate_combinations(modifiers: &[u32], max_modifier_sequence_len: i32) -> Vec<Vec<u32>> {
+    let mut combinations = vec![vec![]]; // Initial empty combination
+
+    for sequence_len in 1..=max_modifier_sequence_len {
+      let current_combinations = modifiers
+        .iter()
+        .cloned()
+        .combinations(sequence_len as usize);
+      combinations.extend(current_combinations);
+    }
+
+    combinations
+  }
+
+  fn convert_to_record_array(&self, syms: &[u64]) -> Result<Vec<KeyRecord>> {
+    syms
+      .iter()
+      .map(|sym| {
+        self
+          .sym_map
+          .get(&(*sym as u32))
+          .cloned()
+          .ok_or_else(|| EVDEVInjectorError::SymMappingFailure(*sym as u32).into())
+      })
+      .collect()
+  }
+
+  fn send_key(&self, code: u32, pressed: bool, delay_us: u32) {
+    self.device.emit(code, pressed);
+    if delay_us != 0 {
+      unsafe {
+        libc::usleep(delay_us);
+      }
+    }
+  }
+
+  fn wait_until_key_is_released(&self, code: u32) -> Result<()> {
+    if let Some(key_provider) = &self.keyboard_state_provider {
+      let key_provider_code = code + EVDEV_OFFSET;
+
+      if !key_provider.is_key_pressed(key_provider_code) {
+        return Ok(());
+      }
+
+      // Key is pressed, wait until timeout
+      let now = Instant::now();
+      while now.elapsed() < std::time::Duration::from_millis(DEFAULT_WAIT_KEY_RELEASE_TIMEOUT_MS) {
+        if !key_provider.is_key_pressed(key_provider_code) {
+          return Ok(());
+        }
+
+        std::thread::sleep(std::time::Duration::from_millis(50));
+      }
+
+      bail!("timed-out while waiting for key release: {}", code);
+    } else {
+      // Keyboard provider not available,
+      Ok(())
+    }
+  }
+
+  fn wait_delay(&self, delay_ms: u32) {
+    if delay_ms > 0 {
+      std::thread::sleep(std::time::Duration::from_millis(delay_ms as u64));
+    }
+  }
+}
+
+impl Injector for EVDEVInjector {
+  fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> {
+    // 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(|| EVDEVInjectorError::CharMappingFailure(char).into())
+      })
+      .collect();
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+    let modifier_delay_ms = options.evdev_modifier_delay;
+
+    // We need to keep track of the modifiers currently pressed to
+    // press or release them accordingly
+    let mut current_modifiers: HashSet<u32> = HashSet::new();
+
+    for record in records? {
+      let record_modifiers = record.modifiers.iter().cloned().collect::<HashSet<_>>();
+
+      // Release all the modifiers that are not needed anymore
+      for expired_modifier in current_modifiers.difference(&record_modifiers) {
+        self.wait_delay(modifier_delay_ms);
+
+        self.send_key(*expired_modifier, false, delay_us);
+
+        self.wait_delay(modifier_delay_ms);
+      }
+
+      // Press all the new modifiers that are now needed
+      for new_modifier in record_modifiers.difference(&current_modifiers) {
+        self.wait_delay(modifier_delay_ms);
+
+        self.wait_until_key_is_released(record.code)?;
+        self.send_key(*new_modifier, true, delay_us);
+
+        self.wait_delay(modifier_delay_ms);
+      }
+
+      // Send the char
+      self.wait_until_key_is_released(record.code)?;
+      self.send_key(record.code, true, delay_us);
+      self.send_key(record.code, false, delay_us);
+
+      current_modifiers = record_modifiers;
+    }
+
+    // Release all the remaining modifiers
+    for expired_modifier in current_modifiers {
+      self.send_key(expired_modifier, false, delay_us);
+    }
+
+    Ok(())
+  }
+
+  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    // 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)?;
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+
+    for record in records {
+      // Press the modifiers
+      for modifier in record.modifiers.iter() {
+        self.send_key(*modifier, true, delay_us);
+      }
+
+      // Send the key
+      self.send_key(record.code, true, delay_us);
+      self.send_key(record.code, false, delay_us);
+
+      // Release the modifiers
+      for modifier in record.modifiers.iter() {
+        self.send_key(*modifier, false, delay_us);
+      }
+    }
+
+    Ok(())
+  }
+
+  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    // 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)?;
+
+    let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
+
+    // First press the keys
+    for record in records.iter() {
+      // Press the modifiers
+      for modifier in record.modifiers.iter() {
+        self.send_key(*modifier, true, delay_us);
+      }
+
+      // Send the key
+      self.send_key(record.code, true, delay_us);
+    }
+
+    // Then release them
+    for record in records.iter().rev() {
+      self.send_key(record.code, false, delay_us);
+
+      // Release the modifiers
+      for modifier in record.modifiers.iter() {
+        self.send_key(*modifier, false, delay_us);
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum EVDEVInjectorError {
+  #[error("missing vkey mapping for char `{0}`")]
+  CharMappingFailure(String),
+
+  #[error("missing record mapping for sym `{0}`")]
+  SymMappingFailure(u32),
+}
diff --git a/espanso-inject/src/evdev/native.c b/espanso-inject/src/evdev/native.c
new file mode 100644
index 00000000..7b42dade
--- /dev/null
+++ b/espanso-inject/src/evdev/native.c
@@ -0,0 +1,74 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <linux/uinput.h>
+#include <unistd.h>
+#include <memory.h>
+
+unsigned long ui_dev_destroy()
+{
+  return UI_DEV_DESTROY;
+}
+
+unsigned long ui_dev_create()
+{
+  return UI_DEV_CREATE;
+}
+
+unsigned long ui_set_evbit()
+{
+  return UI_SET_EVBIT;
+}
+
+unsigned long ui_set_keybit()
+{
+  return UI_SET_KEYBIT;
+}
+
+int setup_uinput_device(int fd)
+{
+  struct uinput_setup usetup;
+
+  memset(&usetup, 0, sizeof(usetup));
+  usetup.id.bustype = BUS_USB;
+  usetup.id.vendor = 0x1234;  // sample vendor
+  usetup.id.product = 0x5678; // sample product
+  strcpy(usetup.name, "Espanso virtual device");
+
+  return ioctl(fd, UI_DEV_SETUP, &usetup);
+}
+
+void emit(int fd, int type, int code, int val)
+{
+  struct input_event ie;
+  ie.type = type;
+  ie.code = code;
+  ie.value = val;
+  // timestamp values below are ignored
+  ie.time.tv_sec = 0;
+  ie.time.tv_usec = 0;
+
+  write(fd, &ie, sizeof(ie));
+}
+
+void uinput_emit(int fd, unsigned int code, int pressed) {
+  emit(fd, EV_KEY, code, pressed);
+  emit(fd, EV_SYN, SYN_REPORT, 0);
+}
\ No newline at end of file
diff --git a/espanso-inject/src/evdev/native.h b/espanso-inject/src/evdev/native.h
new file mode 100644
index 00000000..5a56ca8c
--- /dev/null
+++ b/espanso-inject/src/evdev/native.h
@@ -0,0 +1,31 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INJECT_EV_CONSTANTS_H
+#define ESPANSO_INJECT_EV_CONSTANTS_H
+
+unsigned long ui_dev_destroy();
+unsigned long ui_dev_create();
+unsigned long ui_set_evbit();
+unsigned long ui_set_keybit();
+
+int setup_uinput_device(int fd);
+void uinput_emit(int fd, unsigned int code, int pressed);
+
+#endif //ESPANSO_INJECT_EV_CONSTANTS_H
\ No newline at end of file
diff --git a/espanso-inject/src/evdev/state.rs b/espanso-inject/src/evdev/state.rs
new file mode 100644
index 00000000..5a962b14
--- /dev/null
+++ b/espanso-inject/src/evdev/state.rs
@@ -0,0 +1,95 @@
+// This code is a port of the libxkbcommon "interactive-evdev.c" example
+// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
+
+use std::ffi::CStr;
+
+use scopeguard::ScopeGuard;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use super::{
+  ffi::{
+    xkb_state, xkb_state_key_get_one_sym, xkb_state_key_get_utf8, xkb_state_new, xkb_state_unref,
+    xkb_state_update_key,
+  },
+  keymap::Keymap,
+};
+
+pub struct State {
+  state: *mut xkb_state,
+}
+
+impl State {
+  pub fn new(keymap: &Keymap) -> Result<State> {
+    let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
+    let state = scopeguard::guard(raw_state, |raw_state| unsafe {
+      xkb_state_unref(raw_state);
+    });
+
+    if raw_state.is_null() {
+      return Err(StateError::FailedCreation().into());
+    }
+
+    Ok(Self {
+      state: ScopeGuard::into_inner(state),
+    })
+  }
+
+  pub fn update_key(&self, code: u32, pressed: bool) {
+    let direction = if pressed {
+      super::ffi::xkb_key_direction::DOWN
+    } else {
+      super::ffi::xkb_key_direction::UP
+    };
+    unsafe {
+      xkb_state_update_key(self.state, code, direction);
+    }
+  }
+
+  pub fn get_string(&self, code: u32) -> Option<String> {
+    let mut buffer: [u8; 16] = [0; 16];
+    let len = unsafe {
+      xkb_state_key_get_utf8(
+        self.state,
+        code,
+        buffer.as_mut_ptr() as *mut i8,
+        std::mem::size_of_val(&buffer),
+      )
+    };
+    if len > 0 {
+      let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) };
+      let string = content_raw.to_string_lossy().to_string();
+      if string.is_empty() {
+        None
+      } else {
+        Some(string)
+      }
+    } else {
+      None
+    }
+  }
+
+  pub fn get_sym(&self, code: u32) -> Option<u32> {
+    let sym = unsafe { xkb_state_key_get_one_sym(self.state, code) };
+    if sym == 0 {
+      None
+    } else {
+      Some(sym)
+    }
+  }
+}
+
+impl Drop for State {
+  fn drop(&mut self) {
+    unsafe {
+      xkb_state_unref(self.state);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum StateError {
+  #[error("could not create xkb state")]
+  FailedCreation(),
+}
diff --git a/espanso-inject/src/evdev/uinput.rs b/espanso-inject/src/evdev/uinput.rs
new file mode 100644
index 00000000..ebacf48c
--- /dev/null
+++ b/espanso-inject/src/evdev/uinput.rs
@@ -0,0 +1,108 @@
+/*
+ * 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::ffi::CString;
+
+use libc::{c_uint, close, ioctl, open, O_NONBLOCK, O_WRONLY};
+use scopeguard::ScopeGuard;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use super::ffi::{
+  setup_uinput_device, ui_dev_create, ui_dev_destroy, ui_set_evbit, ui_set_keybit, uinput_emit,
+  EV_KEY,
+};
+
+pub struct UInputDevice {
+  fd: i32,
+}
+
+impl UInputDevice {
+  pub fn new() -> Result<UInputDevice> {
+    let uinput_path = CString::new("/dev/uinput").expect("unable to generate /dev/uinput path");
+    let raw_fd = unsafe { open(uinput_path.as_ptr(), O_WRONLY | O_NONBLOCK) };
+    if raw_fd < 0 {
+      return Err(UInputDeviceError::Open().into());
+    }
+    let fd = scopeguard::guard(raw_fd, |raw_fd| unsafe {
+      close(raw_fd);
+    });
+
+    // Enable keyboard events
+    if unsafe { ioctl(*fd, ui_set_evbit(), EV_KEY as c_uint) } != 0 {
+      return Err(UInputDeviceError::KeyEVBit().into());
+    }
+
+    // Register all keycodes
+    for key_code in 0..256 {
+      if unsafe { ioctl(*fd, ui_set_keybit(), key_code) } != 0 {
+        return Err(UInputDeviceError::KeyBit().into());
+      }
+    }
+
+    // Register the virtual device
+    if unsafe { setup_uinput_device(*fd) } != 0 {
+      return Err(UInputDeviceError::DeviceSetup().into());
+    }
+
+    // Create the device
+    if unsafe { ioctl(*fd, ui_dev_create()) } != 0 {
+      return Err(UInputDeviceError::DeviceCreate().into());
+    }
+
+    Ok(Self {
+      fd: ScopeGuard::into_inner(fd),
+    })
+  }
+
+  pub fn emit(&self, key_code: u32, pressed: bool) {
+    let pressed = if pressed { 1 } else { 0 };
+    unsafe {
+      uinput_emit(self.fd, key_code, pressed);
+    }
+  }
+}
+
+impl Drop for UInputDevice {
+  fn drop(&mut self) {
+    unsafe {
+      ioctl(self.fd, ui_dev_destroy());
+      close(self.fd);
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum UInputDeviceError {
+  #[error("could not open uinput device")]
+  Open(),
+
+  #[error("could not set keyboard evbit")]
+  KeyEVBit(),
+
+  #[error("could not set keyboard keybit")]
+  KeyBit(),
+
+  #[error("could not register virtual device")]
+  DeviceSetup(),
+
+  #[error("could not create uinput device")]
+  DeviceCreate(),
+}
diff --git a/espanso-inject/src/keys.rs b/espanso-inject/src/keys.rs
new file mode 100644
index 00000000..e1d46b21
--- /dev/null
+++ b/espanso-inject/src/keys.rs
@@ -0,0 +1,359 @@
+/*
+ * 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::fmt::Display;
+
+use regex::Regex;
+
+lazy_static! {
+  static ref RAW_PARSER: Regex = Regex::new(r"^RAW\((\d+)\)$").unwrap();
+}
+
+#[derive(Debug, Clone)]
+pub enum Key {
+  // Modifiers
+  Alt,
+  CapsLock,
+  Control,
+  Meta,
+  NumLock,
+  Shift,
+
+  // Whitespace
+  Enter,
+  Tab,
+  Space,
+
+  // Navigation
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  ArrowUp,
+  End,
+  Home,
+  PageDown,
+  PageUp,
+
+  // UI
+  Escape,
+
+  // Editing keys
+  Backspace,
+  Insert,
+  Delete,
+
+  // Function keys
+  F1,
+  F2,
+  F3,
+  F4,
+  F5,
+  F6,
+  F7,
+  F8,
+  F9,
+  F10,
+  F11,
+  F12,
+  F13,
+  F14,
+  F15,
+  F16,
+  F17,
+  F18,
+  F19,
+  F20,
+
+  // Alphabet
+  A,
+  B,
+  C,
+  D,
+  E,
+  F,
+  G,
+  H,
+  I,
+  J,
+  K,
+  L,
+  M,
+  N,
+  O,
+  P,
+  Q,
+  R,
+  S,
+  T,
+  U,
+  V,
+  W,
+  X,
+  Y,
+  Z,
+
+  // Numbers
+  N0,
+  N1,
+  N2,
+  N3,
+  N4,
+  N5,
+  N6,
+  N7,
+  N8,
+  N9,
+
+  // Numpad
+  Numpad0,
+  Numpad1,
+  Numpad2,
+  Numpad3,
+  Numpad4,
+  Numpad5,
+  Numpad6,
+  Numpad7,
+  Numpad8,
+  Numpad9,
+
+  // Specify the raw platform-specific virtual key code.
+  Raw(i32),
+}
+
+impl Display for Key {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match *self {
+      Key::Alt => write!(f, "ALT"),
+      Key::CapsLock => write!(f, "CAPSLOCK"),
+      Key::Control => write!(f, "CTRL"),
+      Key::Meta => write!(f, "META"),
+      Key::NumLock => write!(f, "NUMLOCK"),
+      Key::Shift => write!(f, "SHIFT"),
+      Key::Enter => write!(f, "ENTER"),
+      Key::Tab => write!(f, "TAB"),
+      Key::Space => write!(f, "SPACE"),
+      Key::ArrowDown => write!(f, "DOWN"),
+      Key::ArrowLeft => write!(f, "LEFT"),
+      Key::ArrowRight => write!(f, "RIGHT"),
+      Key::ArrowUp => write!(f, "UP"),
+      Key::End => write!(f, "END"),
+      Key::Home => write!(f, "HOME"),
+      Key::PageDown => write!(f, "PAGEDOWN"),
+      Key::PageUp => write!(f, "PAGEUP"),
+      Key::Escape => write!(f, "ESC"),
+      Key::Backspace => write!(f, "BACKSPACE"),
+      Key::Insert => write!(f, "INSERT"),
+      Key::Delete => write!(f, "DELETE"),
+      Key::F1 => write!(f, "F1"),
+      Key::F2 => write!(f, "F2"),
+      Key::F3 => write!(f, "F3"),
+      Key::F4 => write!(f, "F4"),
+      Key::F5 => write!(f, "F5"),
+      Key::F6 => write!(f, "F6"),
+      Key::F7 => write!(f, "F7"),
+      Key::F8 => write!(f, "F8"),
+      Key::F9 => write!(f, "F9"),
+      Key::F10 => write!(f, "F10"),
+      Key::F11 => write!(f, "F11"),
+      Key::F12 => write!(f, "F12"),
+      Key::F13 => write!(f, "F13"),
+      Key::F14 => write!(f, "F14"),
+      Key::F15 => write!(f, "F15"),
+      Key::F16 => write!(f, "F16"),
+      Key::F17 => write!(f, "F17"),
+      Key::F18 => write!(f, "F18"),
+      Key::F19 => write!(f, "F19"),
+      Key::F20 => write!(f, "F20"),
+      Key::A => write!(f, "A"),
+      Key::B => write!(f, "B"),
+      Key::C => write!(f, "C"),
+      Key::D => write!(f, "D"),
+      Key::E => write!(f, "E"),
+      Key::F => write!(f, "F"),
+      Key::G => write!(f, "G"),
+      Key::H => write!(f, "H"),
+      Key::I => write!(f, "I"),
+      Key::J => write!(f, "J"),
+      Key::K => write!(f, "K"),
+      Key::L => write!(f, "L"),
+      Key::M => write!(f, "M"),
+      Key::N => write!(f, "N"),
+      Key::O => write!(f, "O"),
+      Key::P => write!(f, "P"),
+      Key::Q => write!(f, "Q"),
+      Key::R => write!(f, "R"),
+      Key::S => write!(f, "S"),
+      Key::T => write!(f, "T"),
+      Key::U => write!(f, "U"),
+      Key::V => write!(f, "V"),
+      Key::W => write!(f, "W"),
+      Key::X => write!(f, "X"),
+      Key::Y => write!(f, "Y"),
+      Key::Z => write!(f, "Z"),
+      Key::N0 => write!(f, "0"),
+      Key::N1 => write!(f, "1"),
+      Key::N2 => write!(f, "2"),
+      Key::N3 => write!(f, "3"),
+      Key::N4 => write!(f, "4"),
+      Key::N5 => write!(f, "5"),
+      Key::N6 => write!(f, "6"),
+      Key::N7 => write!(f, "7"),
+      Key::N8 => write!(f, "8"),
+      Key::N9 => write!(f, "9"),
+      Key::Numpad0 => write!(f, "NUMPAD0"),
+      Key::Numpad1 => write!(f, "NUMPAD1"),
+      Key::Numpad2 => write!(f, "NUMPAD2"),
+      Key::Numpad3 => write!(f, "NUMPAD3"),
+      Key::Numpad4 => write!(f, "NUMPAD4"),
+      Key::Numpad5 => write!(f, "NUMPAD5"),
+      Key::Numpad6 => write!(f, "NUMPAD6"),
+      Key::Numpad7 => write!(f, "NUMPAD7"),
+      Key::Numpad8 => write!(f, "NUMPAD8"),
+      Key::Numpad9 => write!(f, "NUMPAD9"),
+      Key::Raw(code) => write!(f, "RAW({})", code),
+    }
+  }
+}
+
+impl Key {
+  pub fn parse(key: &str) -> Option<Key> {
+    let parsed = match key {
+      "ALT" | "OPTION" => Some(Key::Alt),
+      "CAPSLOCK" => Some(Key::CapsLock),
+      "CTRL" => Some(Key::Control),
+      "META" | "CMD" => Some(Key::Meta),
+      "NUMLOCK" => Some(Key::NumLock),
+      "SHIFT" => Some(Key::Shift),
+      "ENTER" => Some(Key::Enter),
+      "TAB" => Some(Key::Tab),
+      "SPACE" => Some(Key::Space),
+      "DOWN" => Some(Key::ArrowDown),
+      "LEFT" => Some(Key::ArrowLeft),
+      "RIGHT" => Some(Key::ArrowRight),
+      "UP" => Some(Key::ArrowUp),
+      "END" => Some(Key::End),
+      "HOME" => Some(Key::Home),
+      "PAGEDOWN" => Some(Key::PageDown),
+      "PAGEUP" => Some(Key::PageUp),
+      "ESC" => Some(Key::Escape),
+      "BACKSPACE" => Some(Key::Backspace),
+      "INSERT" => Some(Key::Insert),
+      "DELETE" => Some(Key::Delete),
+      "F1" => Some(Key::F1),
+      "F2" => Some(Key::F2),
+      "F3" => Some(Key::F3),
+      "F4" => Some(Key::F4),
+      "F5" => Some(Key::F5),
+      "F6" => Some(Key::F6),
+      "F7" => Some(Key::F7),
+      "F8" => Some(Key::F8),
+      "F9" => Some(Key::F9),
+      "F10" => Some(Key::F10),
+      "F11" => Some(Key::F11),
+      "F12" => Some(Key::F12),
+      "F13" => Some(Key::F13),
+      "F14" => Some(Key::F14),
+      "F15" => Some(Key::F15),
+      "F16" => Some(Key::F16),
+      "F17" => Some(Key::F17),
+      "F18" => Some(Key::F18),
+      "F19" => Some(Key::F19),
+      "F20" => Some(Key::F20),
+      "A" => Some(Key::A),
+      "B" => Some(Key::B),
+      "C" => Some(Key::C),
+      "D" => Some(Key::D),
+      "E" => Some(Key::E),
+      "F" => Some(Key::F),
+      "G" => Some(Key::G),
+      "H" => Some(Key::H),
+      "I" => Some(Key::I),
+      "J" => Some(Key::J),
+      "K" => Some(Key::K),
+      "L" => Some(Key::L),
+      "M" => Some(Key::M),
+      "N" => Some(Key::N),
+      "O" => Some(Key::O),
+      "P" => Some(Key::P),
+      "Q" => Some(Key::Q),
+      "R" => Some(Key::R),
+      "S" => Some(Key::S),
+      "T" => Some(Key::T),
+      "U" => Some(Key::U),
+      "V" => Some(Key::V),
+      "W" => Some(Key::W),
+      "X" => Some(Key::X),
+      "Y" => Some(Key::Y),
+      "Z" => Some(Key::Z),
+      "0" => Some(Key::N0),
+      "1" => Some(Key::N1),
+      "2" => Some(Key::N2),
+      "3" => Some(Key::N3),
+      "4" => Some(Key::N4),
+      "5" => Some(Key::N5),
+      "6" => Some(Key::N6),
+      "7" => Some(Key::N7),
+      "8" => Some(Key::N8),
+      "9" => Some(Key::N9),
+      "NUMPAD0" => Some(Key::Numpad0),
+      "NUMPAD1" => Some(Key::Numpad1),
+      "NUMPAD2" => Some(Key::Numpad2),
+      "NUMPAD3" => Some(Key::Numpad3),
+      "NUMPAD4" => Some(Key::Numpad4),
+      "NUMPAD5" => Some(Key::Numpad5),
+      "NUMPAD6" => Some(Key::Numpad6),
+      "NUMPAD7" => Some(Key::Numpad7),
+      "NUMPAD8" => Some(Key::Numpad8),
+      "NUMPAD9" => Some(Key::Numpad9),
+      _ => None,
+    };
+
+    if parsed.is_none() {
+      // Attempt to parse raw keys
+      if RAW_PARSER.is_match(key) {
+        if let Some(caps) = RAW_PARSER.captures(key) {
+          let code_str = caps.get(1).map_or("", |m| m.as_str());
+          let code = code_str.parse::<i32>();
+          if let Ok(code) = code {
+            return Some(Key::Raw(code));
+          }
+        }
+      }
+    }
+
+    parsed
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn parse_works_correctly() {
+    assert!(matches!(Key::parse("ALT").unwrap(), Key::Alt));
+    assert!(matches!(Key::parse("RAW(1234)").unwrap(), Key::Raw(1234)));
+  }
+
+  #[test]
+  fn parse_invalid_keys() {
+    assert!(Key::parse("INVALID").is_none());
+    assert!(Key::parse("RAW(a)").is_none());
+  }
+}
diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs
new file mode 100644
index 00000000..64162163
--- /dev/null
+++ b/espanso-inject/src/lib.rs
@@ -0,0 +1,172 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::info;
+
+pub mod keys;
+
+#[cfg(target_os = "windows")]
+mod win32;
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+mod x11;
+
+#[cfg(target_os = "linux")]
+mod evdev;
+
+#[cfg(target_os = "linux")]
+mod linux;
+
+#[cfg(target_os = "macos")]
+mod mac;
+
+#[macro_use]
+extern crate lazy_static;
+
+pub trait Injector {
+  fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()>;
+  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()>;
+  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()>;
+}
+
+#[allow(dead_code)]
+#[derive(Clone, Copy)]
+pub struct InjectionOptions {
+  // Delay between injected events
+  pub delay: i32,
+
+  // Use original libxdo methods instead of patched version
+  // using XSendEvent rather than XTestFakeKeyEvent
+  // NOTE: Only relevant on X11 linux systems.
+  pub disable_fast_inject: bool,
+
+  // Used to set a modifier-specific delay.
+  // NOTE: Only relevant on Wayland systems.
+  pub evdev_modifier_delay: u32,
+}
+
+impl Default for InjectionOptions {
+  fn default() -> Self {
+    #[allow(clippy::if_same_then_else)]
+    let default_delay = if cfg!(target_os = "windows") {
+      0
+    } else if cfg!(target_os = "macos") {
+      1
+    } else if cfg!(target_os = "linux") {
+      if cfg!(feature = "wayland") {
+        1
+      } else {
+        0
+      }
+    } else {
+      panic!("unsupported OS");
+    };
+
+    Self {
+      delay: default_delay,
+      disable_fast_inject: false,
+      evdev_modifier_delay: 10,
+    }
+  }
+}
+
+#[allow(dead_code)]
+pub struct InjectorCreationOptions {
+  // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
+  pub use_evdev: bool,
+
+  // Overwrite the list of modifiers to be scanned when
+  // populating the evdev injector lookup maps
+  pub evdev_modifiers: Option<Vec<u32>>,
+
+  // Overwrite the maximum number of modifiers used tested in
+  // a single combination to populate the lookup maps
+  pub evdev_max_modifier_combination_len: Option<i32>,
+
+  // Can be used to overwrite the keymap configuration
+  // used by espanso to inject key presses.
+  pub evdev_keyboard_rmlvo: Option<KeyboardConfig>,
+
+  // An optional provider that can be used by the injector
+  // to determine which keys are pressed at the time of injection.
+  // This is needed on Wayland to "wait" for key releases when
+  // the injected string contains a key that it's currently pressed.
+  // Otherwise, a key that is already pressed cannot be injected.
+  pub keyboard_state_provider: Option<Box<dyn KeyboardStateProvider>>,
+}
+
+// This struct identifies the keyboard layout that
+// should be used by EVDEV when loading the keymap.
+// For more information: https://xkbcommon.org/doc/current/structxkb__rule__names.html
+pub struct KeyboardConfig {
+  pub rules: Option<String>,
+  pub model: Option<String>,
+  pub layout: Option<String>,
+  pub variant: Option<String>,
+  pub options: Option<String>,
+}
+
+pub trait KeyboardStateProvider {
+  fn is_key_pressed(&self, code: u32) -> bool;
+}
+
+impl Default for InjectorCreationOptions {
+  fn default() -> Self {
+    Self {
+      use_evdev: false,
+      evdev_modifiers: None,
+      evdev_max_modifier_combination_len: None,
+      evdev_keyboard_rmlvo: None,
+      keyboard_state_provider: None,
+    }
+  }
+}
+
+#[cfg(target_os = "windows")]
+pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
+  info!("using Win32Injector");
+  Ok(Box::new(win32::Win32Injector::new()))
+}
+
+#[cfg(target_os = "macos")]
+pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
+  info!("using MacInjector");
+  Ok(Box::new(mac::MacInjector::new()))
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(not(feature = "wayland"))]
+pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
+  if options.use_evdev {
+    info!("using EVDEVInjector");
+    Ok(Box::new(evdev::EVDEVInjector::new(options)?))
+  } else {
+    info!("using X11Injector");
+    Ok(Box::new(x11::X11Injector::new()?))
+  }
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(feature = "wayland")]
+pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
+  info!("using EVDEVInjector");
+  Ok(Box::new(evdev::EVDEVInjector::new(options)?))
+}
diff --git a/espanso-inject/src/linux/mod.rs b/espanso-inject/src/linux/mod.rs
new file mode 100644
index 00000000..d7dbdf0a
--- /dev/null
+++ b/espanso-inject/src/linux/mod.rs
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+pub mod raw_keys;
diff --git a/espanso-inject/src/linux/raw_keys.rs b/espanso-inject/src/linux/raw_keys.rs
new file mode 100644
index 00000000..e586527c
--- /dev/null
+++ b/espanso-inject/src/linux/raw_keys.rs
@@ -0,0 +1,146 @@
+/*
+ * 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 crate::keys::Key;
+use anyhow::Result;
+use thiserror::Error;
+
+pub fn convert_key_to_sym(key: &Key) -> Option<u32> {
+  match key {
+    Key::Alt => Some(0xFFE9),
+    Key::CapsLock => Some(0xFFE5),
+    Key::Control => Some(0xFFE3),
+    Key::Meta => Some(0xFFEB),
+    Key::NumLock => Some(0xFF7F),
+    Key::Shift => Some(0xFFE1),
+
+    // Whitespace
+    Key::Enter => Some(0xFF0D),
+    Key::Tab => Some(0xFF09),
+    Key::Space => Some(0x20),
+
+    // Navigation
+    Key::ArrowDown => Some(0xFF54),
+    Key::ArrowLeft => Some(0xFF51),
+    Key::ArrowRight => Some(0xFF53),
+    Key::ArrowUp => Some(0xFF52),
+    Key::End => Some(0xFF57),
+    Key::Home => Some(0xFF50),
+    Key::PageDown => Some(0xFF56),
+    Key::PageUp => Some(0xFF55),
+
+    // UI keys
+    Key::Escape => Some(0xFF1B),
+
+    // Editing keys
+    Key::Backspace => Some(0xFF08),
+    Key::Insert => Some(0xff63),
+    Key::Delete => Some(0xffff),
+
+    // Function keys
+    Key::F1 => Some(0xFFBE),
+    Key::F2 => Some(0xFFBF),
+    Key::F3 => Some(0xFFC0),
+    Key::F4 => Some(0xFFC1),
+    Key::F5 => Some(0xFFC2),
+    Key::F6 => Some(0xFFC3),
+    Key::F7 => Some(0xFFC4),
+    Key::F8 => Some(0xFFC5),
+    Key::F9 => Some(0xFFC6),
+    Key::F10 => Some(0xFFC7),
+    Key::F11 => Some(0xFFC8),
+    Key::F12 => Some(0xFFC9),
+    Key::F13 => Some(0xFFCA),
+    Key::F14 => Some(0xFFCB),
+    Key::F15 => Some(0xFFCC),
+    Key::F16 => Some(0xFFCD),
+    Key::F17 => Some(0xFFCE),
+    Key::F18 => Some(0xFFCF),
+    Key::F19 => Some(0xFFD0),
+    Key::F20 => Some(0xFFD1),
+
+    Key::A => Some(0x0061),
+    Key::B => Some(0x0062),
+    Key::C => Some(0x0063),
+    Key::D => Some(0x0064),
+    Key::E => Some(0x0065),
+    Key::F => Some(0x0066),
+    Key::G => Some(0x0067),
+    Key::H => Some(0x0068),
+    Key::I => Some(0x0069),
+    Key::J => Some(0x006a),
+    Key::K => Some(0x006b),
+    Key::L => Some(0x006c),
+    Key::M => Some(0x006d),
+    Key::N => Some(0x006e),
+    Key::O => Some(0x006f),
+    Key::P => Some(0x0070),
+    Key::Q => Some(0x0071),
+    Key::R => Some(0x0072),
+    Key::S => Some(0x0073),
+    Key::T => Some(0x0074),
+    Key::U => Some(0x0075),
+    Key::V => Some(0x0076),
+    Key::W => Some(0x0077),
+    Key::X => Some(0x0078),
+    Key::Y => Some(0x0079),
+    Key::Z => Some(0x007a),
+
+    Key::N0 => Some(0x0030),
+    Key::N1 => Some(0x0031),
+    Key::N2 => Some(0x0032),
+    Key::N3 => Some(0x0033),
+    Key::N4 => Some(0x0034),
+    Key::N5 => Some(0x0035),
+    Key::N6 => Some(0x0036),
+    Key::N7 => Some(0x0037),
+    Key::N8 => Some(0x0038),
+    Key::N9 => Some(0x0039),
+    Key::Numpad0 => Some(0xffb0),
+    Key::Numpad1 => Some(0xffb1),
+    Key::Numpad2 => Some(0xffb2),
+    Key::Numpad3 => Some(0xffb3),
+    Key::Numpad4 => Some(0xffb4),
+    Key::Numpad5 => Some(0xffb5),
+    Key::Numpad6 => Some(0xffb6),
+    Key::Numpad7 => Some(0xffb7),
+    Key::Numpad8 => Some(0xffb8),
+    Key::Numpad9 => Some(0xffb9),
+    Key::Raw(code) => Some(*code as u32),
+  }
+}
+
+pub fn convert_to_sym_array(keys: &[Key]) -> Result<Vec<u64>> {
+  let mut virtual_keys: Vec<u64> = Vec::new();
+  for key in keys.iter() {
+    let vk = convert_key_to_sym(key);
+    if let Some(vk) = vk {
+      virtual_keys.push(vk as u64)
+    } else {
+      return Err(LinuxRawKeyError::MappingFailure(key.clone()).into());
+    }
+  }
+  Ok(virtual_keys)
+}
+
+#[derive(Error, Debug)]
+pub enum LinuxRawKeyError {
+  #[error("missing mapping for key `{0}`")]
+  MappingFailure(Key),
+}
diff --git a/espanso-inject/src/mac/mod.rs b/espanso-inject/src/mac/mod.rs
new file mode 100644
index 00000000..2bb12d70
--- /dev/null
+++ b/espanso-inject/src/mac/mod.rs
@@ -0,0 +1,117 @@
+/*
+ * 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/>.
+ */
+
+mod raw_keys;
+
+use std::{ffi::CString, os::raw::c_char};
+
+use log::error;
+use raw_keys::convert_key_to_vkey;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::{keys, InjectionOptions, Injector};
+
+#[allow(improper_ctypes)]
+#[link(name = "espansoinject", kind = "static")]
+extern "C" {
+  pub fn inject_string(string: *const c_char);
+  pub fn inject_separate_vkeys(vkey_array: *const i32, vkey_count: i32, delay: i32);
+  pub fn inject_vkeys_combination(vkey_array: *const i32, vkey_count: i32, delay: i32);
+}
+
+pub struct MacInjector {}
+
+#[allow(clippy::new_without_default)]
+impl MacInjector {
+  pub fn new() -> Self {
+    Self {}
+  }
+
+  pub fn convert_to_vk_array(keys: &[keys::Key]) -> Result<Vec<i32>> {
+    let mut virtual_keys: Vec<i32> = Vec::new();
+    for key in keys.iter() {
+      let vk = convert_key_to_vkey(key);
+      if let Some(vk) = vk {
+        virtual_keys.push(vk)
+      } else {
+        return Err(MacInjectorError::MappingFailure(key.clone()).into());
+      }
+    }
+    Ok(virtual_keys)
+  }
+}
+
+impl Injector for MacInjector {
+  fn send_string(&self, string: &str, _: InjectionOptions) -> Result<()> {
+    let c_string = CString::new(string)?;
+    unsafe {
+      inject_string(c_string.as_ptr());
+    }
+    Ok(())
+  }
+
+  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let virtual_keys = Self::convert_to_vk_array(keys)?;
+
+    unsafe {
+      inject_separate_vkeys(
+        virtual_keys.as_ptr(),
+        virtual_keys.len() as i32,
+        options.delay,
+      );
+    }
+
+    Ok(())
+  }
+
+  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let virtual_keys = Self::convert_to_vk_array(keys)?;
+
+    unsafe {
+      inject_vkeys_combination(
+        virtual_keys.as_ptr(),
+        virtual_keys.len() as i32,
+        options.delay,
+      );
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum MacInjectorError {
+  #[error("missing vkey mapping for key `{0}`")]
+  MappingFailure(keys::Key),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn convert_raw_to_virtual_key_array() {
+    assert_eq!(
+      MacInjector::convert_to_vk_array(&[keys::Key::Alt, keys::Key::V]).unwrap(),
+      vec![0x3A, 0x09]
+    );
+  }
+}
diff --git a/espanso-inject/src/mac/native.h b/espanso-inject/src/mac/native.h
new file mode 100644
index 00000000..6d52913a
--- /dev/null
+++ b/espanso-inject/src/mac/native.h
@@ -0,0 +1,35 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INJECT_H
+#define ESPANSO_INJECT_H
+
+#include <stdint.h>
+
+// Inject a complete string using the KEYEVENTF_UNICODE flag
+extern "C" void inject_string(char * string);
+
+// Send a sequence of vkey presses and releases
+extern "C" void inject_separate_vkeys(int32_t *vkey_array, int32_t vkey_count, int32_t delay);
+
+// Send a combination of vkeys, first pressing all the vkeys and then releasing
+// This is needed for keyboard shortcuts, for example.
+extern "C" void inject_vkeys_combination(int32_t *vkey_array, int32_t vkey_count, int32_t delay);
+
+#endif //ESPANSO_INJECT_H
\ No newline at end of file
diff --git a/espanso-inject/src/mac/native.mm b/espanso-inject/src/mac/native.mm
new file mode 100644
index 00000000..7a8e27f1
--- /dev/null
+++ b/espanso-inject/src/mac/native.mm
@@ -0,0 +1,153 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <string.h>
+#import <Foundation/Foundation.h>
+#include <vector>
+
+// Events dispatched by espanso are "marked" with a custom location
+// so that we can later skip them in the detect module.
+CGPoint ESPANSO_POINT_MARKER = CGPointMake(-27469, 0);
+
+void inject_string(char *string)
+{
+  char * stringCopy = strdup(string);
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    // Convert the c string to a UniChar array as required by the CGEventKeyboardSetUnicodeString method
+    NSString *nsString = [NSString stringWithUTF8String:stringCopy];
+    CFStringRef cfString = (__bridge CFStringRef) nsString;
+    std::vector <UniChar> buffer(nsString.length);
+    CFStringGetCharacters(cfString, CFRangeMake(0, nsString.length), buffer.data());
+
+    free(stringCopy);
+
+    // Send the event
+
+    // Check if the shift key is down, and if so, release it
+    // To see why: https://github.com/federico-terzi/espanso/issues/279
+    if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, 0x38)) {
+      CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x38, false);
+      CGEventSetLocation(e2, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, e2);
+      CFRelease(e2);
+
+      usleep(2000);
+    }
+
+    // Because of a bug ( or undocumented limit ) of the CGEventKeyboardSetUnicodeString method
+    // the string gets truncated after 20 characters, so we need to send multiple events.
+
+    int i = 0;
+    while (i < buffer.size()) {
+      int chunk_size = 20;
+      if ((i+chunk_size) >  buffer.size()) {
+        chunk_size = buffer.size() - i;
+      }
+
+      UniChar * offset_buffer = buffer.data() + i;
+      CGEventRef e = CGEventCreateKeyboardEvent(NULL, 0x31, true);
+      CGEventSetLocation(e, ESPANSO_POINT_MARKER);
+      CGEventKeyboardSetUnicodeString(e, chunk_size, offset_buffer);
+      CGEventPost(kCGHIDEventTap, e);
+      CFRelease(e);
+
+      usleep(2000);
+
+      // Some applications require an explicit release of the space key
+      // For more information: https://github.com/federico-terzi/espanso/issues/159
+      CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false);
+      CGEventSetLocation(e2, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, e2);
+      CFRelease(e2);
+
+      usleep(2000);
+
+      i += chunk_size;
+    }
+  });
+}
+
+void inject_separate_vkeys(int32_t *_vkey_array, int32_t vkey_count, int32_t delay)
+{
+  long udelay = delay * 1000;
+  
+  // Create an heap allocated copy of the array, so that it doesn't get freed within the block
+  int32_t *vkey_array = (int32_t*)malloc(sizeof(int32_t)*vkey_count);
+  memcpy(vkey_array, _vkey_array, sizeof(int32_t)*vkey_count);
+
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    for (int i = 0; i<vkey_count; i++) {
+      CGEventRef keydown;
+      keydown = CGEventCreateKeyboardEvent(NULL, vkey_array[i], true);
+      CGEventSetLocation(keydown, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, keydown);
+      CFRelease(keydown);
+
+      usleep(udelay);
+
+      CGEventRef keyup;
+      keyup = CGEventCreateKeyboardEvent(NULL, vkey_array[i], false);
+      CGEventSetLocation(keyup, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, keyup);
+      CFRelease(keyup);
+
+      usleep(udelay);
+    }
+
+    free(vkey_array);
+  });
+}
+
+void inject_vkeys_combination(int32_t *_vkey_array, int32_t vkey_count, int32_t delay)
+{
+  long udelay = delay * 1000;
+  
+  // Create an heap allocated copy of the array, so that it doesn't get freed within the block
+  int32_t *vkey_array = (int32_t*)malloc(sizeof(int32_t)*vkey_count);
+  memcpy(vkey_array, _vkey_array, sizeof(int32_t)*vkey_count);
+
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    // First send the presses
+    for (int i = 0; i < vkey_count; i++)
+    {
+      CGEventRef keydown;
+      keydown = CGEventCreateKeyboardEvent(NULL, vkey_array[i], true);
+      CGEventSetLocation(keydown, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, keydown);
+      CFRelease(keydown);
+
+      usleep(udelay);
+    }
+
+    // Then the releases
+    for (int i = (vkey_count - 1); i >= 0; i--)
+    {
+      CGEventRef keyup;
+      keyup = CGEventCreateKeyboardEvent(NULL, vkey_array[i], false);
+      CGEventSetLocation(keyup, ESPANSO_POINT_MARKER);
+      CGEventPost(kCGHIDEventTap, keyup);
+      CFRelease(keyup);
+
+      usleep(udelay);
+    }
+    
+    free(vkey_array);
+  });
+}
diff --git a/espanso-inject/src/mac/raw_keys.rs b/espanso-inject/src/mac/raw_keys.rs
new file mode 100644
index 00000000..9e1fd6a7
--- /dev/null
+++ b/espanso-inject/src/mac/raw_keys.rs
@@ -0,0 +1,113 @@
+/*
+ * 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 crate::keys::Key;
+
+pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
+  match key {
+    Key::Alt => Some(0x3A),
+    Key::CapsLock => Some(0x39),
+    Key::Control => Some(0x3B),
+    Key::Meta => Some(0x37),
+    Key::NumLock => None,
+    Key::Shift => Some(0x38),
+    Key::Enter => Some(0x24),
+    Key::Tab => Some(0x30),
+    Key::Space => Some(0x31),
+    Key::ArrowDown => Some(0x7D),
+    Key::ArrowLeft => Some(0x7B),
+    Key::ArrowRight => Some(0x7C),
+    Key::ArrowUp => Some(0x7E),
+    Key::End => Some(0x77),
+    Key::Home => Some(0x73),
+    Key::PageDown => Some(0x79),
+    Key::PageUp => Some(0x74),
+    Key::Escape => Some(0x35),
+    Key::Backspace => Some(0x33),
+    Key::Insert => None,
+    Key::Delete => Some(0x75),
+    Key::F1 => Some(0x7A),
+    Key::F2 => Some(0x78),
+    Key::F3 => Some(0x63),
+    Key::F4 => Some(0x76),
+    Key::F5 => Some(0x60),
+    Key::F6 => Some(0x61),
+    Key::F7 => Some(0x62),
+    Key::F8 => Some(0x64),
+    Key::F9 => Some(0x65),
+    Key::F10 => Some(0x6D),
+    Key::F11 => Some(0x67),
+    Key::F12 => Some(0x6F),
+    Key::F13 => Some(0x69),
+    Key::F14 => Some(0x6B),
+    Key::F15 => Some(0x71),
+    Key::F16 => Some(0x6A),
+    Key::F17 => Some(0x40),
+    Key::F18 => Some(0x4F),
+    Key::F19 => Some(0x50),
+    Key::F20 => Some(0x5A),
+    Key::A => Some(0x00),
+    Key::B => Some(0x0B),
+    Key::C => Some(0x08),
+    Key::D => Some(0x02),
+    Key::E => Some(0x0E),
+    Key::F => Some(0x03),
+    Key::G => Some(0x05),
+    Key::H => Some(0x04),
+    Key::I => Some(0x22),
+    Key::J => Some(0x26),
+    Key::K => Some(0x28),
+    Key::L => Some(0x25),
+    Key::M => Some(0x2E),
+    Key::N => Some(0x2D),
+    Key::O => Some(0x1F),
+    Key::P => Some(0x23),
+    Key::Q => Some(0x0C),
+    Key::R => Some(0x0F),
+    Key::S => Some(0x01),
+    Key::T => Some(0x11),
+    Key::U => Some(0x20),
+    Key::V => Some(0x09),
+    Key::W => Some(0x0D),
+    Key::X => Some(0x07),
+    Key::Y => Some(0x10),
+    Key::Z => Some(0x06),
+    Key::N0 => Some(0x1D),
+    Key::N1 => Some(0x12),
+    Key::N2 => Some(0x13),
+    Key::N3 => Some(0x14),
+    Key::N4 => Some(0x15),
+    Key::N5 => Some(0x17),
+    Key::N6 => Some(0x16),
+    Key::N7 => Some(0x1A),
+    Key::N8 => Some(0x1C),
+    Key::N9 => Some(0x19),
+    Key::Numpad0 => Some(0x52),
+    Key::Numpad1 => Some(0x53),
+    Key::Numpad2 => Some(0x54),
+    Key::Numpad3 => Some(0x55),
+    Key::Numpad4 => Some(0x56),
+    Key::Numpad5 => Some(0x57),
+    Key::Numpad6 => Some(0x58),
+    Key::Numpad7 => Some(0x59),
+    Key::Numpad8 => Some(0x5B),
+    Key::Numpad9 => Some(0x5C),
+    Key::Raw(code) => Some(*code),
+  }
+}
diff --git a/espanso-inject/src/win32/mod.rs b/espanso-inject/src/win32/mod.rs
new file mode 100644
index 00000000..45c7d033
--- /dev/null
+++ b/espanso-inject/src/win32/mod.rs
@@ -0,0 +1,129 @@
+/*
+ * 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/>.
+ */
+
+mod raw_keys;
+
+use log::error;
+use raw_keys::convert_key_to_vkey;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::{keys, InjectionOptions, Injector};
+
+#[allow(improper_ctypes)]
+#[link(name = "espansoinject", kind = "static")]
+extern "C" {
+  pub fn inject_string(string: *const u16);
+  pub fn inject_separate_vkeys(vkey_array: *const i32, vkey_count: i32);
+  pub fn inject_vkeys_combination(vkey_array: *const i32, vkey_count: i32);
+  pub fn inject_separate_vkeys_with_delay(vkey_array: *const i32, vkey_count: i32, delay: i32);
+  pub fn inject_vkeys_combination_with_delay(vkey_array: *const i32, vkey_count: i32, delay: i32);
+}
+
+pub struct Win32Injector {}
+
+#[allow(clippy::new_without_default)]
+impl Win32Injector {
+  pub fn new() -> Self {
+    Self {}
+  }
+
+  pub fn convert_to_vk_array(keys: &[keys::Key]) -> Result<Vec<i32>> {
+    let mut virtual_keys: Vec<i32> = Vec::new();
+    for key in keys.iter() {
+      let vk = convert_key_to_vkey(key);
+      if let Some(vk) = vk {
+        virtual_keys.push(vk)
+      } else {
+        return Err(Win32InjectorError::MappingFailure(key.clone()).into());
+      }
+    }
+    Ok(virtual_keys)
+  }
+}
+
+impl Injector for Win32Injector {
+  fn send_string(&self, string: &str, _: InjectionOptions) -> Result<()> {
+    let wide_string = widestring::WideCString::from_str(string)?;
+    unsafe {
+      inject_string(wide_string.as_ptr());
+    }
+    Ok(())
+  }
+
+  fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let virtual_keys = Self::convert_to_vk_array(keys)?;
+
+    if options.delay == 0 {
+      unsafe {
+        inject_separate_vkeys(virtual_keys.as_ptr(), virtual_keys.len() as i32);
+      }
+    } else {
+      unsafe {
+        inject_separate_vkeys_with_delay(
+          virtual_keys.as_ptr(),
+          virtual_keys.len() as i32,
+          options.delay,
+        );
+      }
+    }
+
+    Ok(())
+  }
+
+  fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
+    let virtual_keys = Self::convert_to_vk_array(keys)?;
+
+    if options.delay == 0 {
+      unsafe {
+        inject_vkeys_combination(virtual_keys.as_ptr(), virtual_keys.len() as i32);
+      }
+    } else {
+      unsafe {
+        inject_vkeys_combination_with_delay(
+          virtual_keys.as_ptr(),
+          virtual_keys.len() as i32,
+          options.delay,
+        );
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum Win32InjectorError {
+  #[error("missing vkey mapping for key `{0}`")]
+  MappingFailure(keys::Key),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn convert_raw_to_virtual_key_array() {
+    assert_eq!(
+      Win32Injector::convert_to_vk_array(&[keys::Key::Alt, keys::Key::V]).unwrap(),
+      vec![0x12, 0x56]
+    );
+  }
+}
diff --git a/espanso-inject/src/win32/native.cpp b/espanso-inject/src/win32/native.cpp
new file mode 100644
index 00000000..f7c366b3
--- /dev/null
+++ b/espanso-inject/src/win32/native.cpp
@@ -0,0 +1,172 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <iostream>
+#include <stdio.h>
+#include <string>
+#include <vector>
+#include <memory>
+#include <array>
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include <windows.h>
+#include <winuser.h>
+#include <strsafe.h>
+#include <Windows.h>
+
+void inject_string(wchar_t *string)
+{
+  std::wstring msg(string);
+
+  std::vector<INPUT> vec;
+  for (auto ch : msg)
+  {
+    INPUT input = {0};
+    input.type = INPUT_KEYBOARD;
+    input.ki.dwFlags = KEYEVENTF_UNICODE;
+    input.ki.wScan = ch;
+    vec.push_back(input);
+
+    input.ki.dwFlags |= KEYEVENTF_KEYUP;
+    vec.push_back(input);
+  }
+
+  SendInput(vec.size(), vec.data(), sizeof(INPUT));
+}
+
+void inject_separate_vkeys(int32_t *vkey_array, int32_t vkey_count)
+{
+  std::vector<INPUT> vec;
+
+  for (int i = 0; i < vkey_count; i++)
+  {
+    INPUT input = {0};
+
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = 0; // 0 for key press
+    vec.push_back(input);
+
+    input.ki.dwFlags = KEYEVENTF_KEYUP;
+    vec.push_back(input);
+  }
+
+  SendInput(vec.size(), vec.data(), sizeof(INPUT));
+}
+
+void inject_vkeys_combination(int32_t *vkey_array, int32_t vkey_count)
+{
+  std::vector<INPUT> vec;
+
+  // First send the presses
+  for (int i = 0; i < vkey_count; i++)
+  {
+    INPUT input = {0};
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = 0;
+    vec.push_back(input);
+  }
+
+  // Then the releases
+  for (int i = (vkey_count - 1); i >= 0; i--)
+  {
+    INPUT input = {0};
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = KEYEVENTF_KEYUP;
+    vec.push_back(input);
+  }
+
+  SendInput(vec.size(), vec.data(), sizeof(INPUT));
+}
+
+void inject_separate_vkeys_with_delay(int32_t *vkey_array, int32_t vkey_count, int32_t delay)
+{
+  for (int i = 0; i < vkey_count; i++)
+  {
+    INPUT input = {0};
+
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = 0; // 0 for key press
+    SendInput(1, &input, sizeof(INPUT));
+
+    Sleep(delay);
+
+    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
+    SendInput(1, &input, sizeof(INPUT));
+
+    Sleep(delay);
+  }
+}
+
+void inject_vkeys_combination_with_delay(int32_t *vkey_array, int32_t vkey_count, int32_t delay)
+{
+  // First send the presses
+  for (int i = 0; i < vkey_count; i++)
+  {
+    INPUT input = {0};
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = 0;
+    
+    SendInput(1, &input, sizeof(INPUT));
+    Sleep(delay);
+  }
+
+  // Then the releases
+  for (int i = (vkey_count - 1); i >= 0; i--)
+  {
+    INPUT input = {0};
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = vkey_array[i];
+    input.ki.dwFlags = KEYEVENTF_KEYUP;
+
+    SendInput(1, &input, sizeof(INPUT));
+    Sleep(delay);
+  }
+}
\ No newline at end of file
diff --git a/espanso-inject/src/win32/native.h b/espanso-inject/src/win32/native.h
new file mode 100644
index 00000000..7e66fb3d
--- /dev/null
+++ b/espanso-inject/src/win32/native.h
@@ -0,0 +1,40 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_INJECT_H
+#define ESPANSO_INJECT_H
+
+#include <stdint.h>
+
+// Inject a complete string using the KEYEVENTF_UNICODE flag
+extern "C" void inject_string(wchar_t * string);
+
+// Send a sequence of vkey presses and releases
+extern "C" void inject_separate_vkeys(int32_t *vkey_array, int32_t vkey_count);
+
+// Send a combination of vkeys, first pressing all the vkeys and then releasing
+// This is needed for keyboard shortcuts, for example.
+extern "C" void inject_vkeys_combination(int32_t *vkey_array, int32_t vkey_count);
+
+// These two variants introduce a delay between each event, which is sometimes needed when
+// dealing with slow applications
+extern "C" void inject_separate_vkeys_with_delay(int32_t *vkey_array, int32_t vkey_count, int32_t delay);
+extern "C" void inject_vkeys_combination_with_delay(int32_t *vkey_array, int32_t vkey_count, int32_t delay);
+
+#endif //ESPANSO_INJECT_H
\ No newline at end of file
diff --git a/espanso-inject/src/win32/raw_keys.rs b/espanso-inject/src/win32/raw_keys.rs
new file mode 100644
index 00000000..c1374c40
--- /dev/null
+++ b/espanso-inject/src/win32/raw_keys.rs
@@ -0,0 +1,114 @@
+/*
+ * 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 crate::keys::Key;
+
+pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
+  let vkey = match key {
+    Key::Alt => 0x12,
+    Key::CapsLock => 0x14,
+    Key::Control => 0x11,
+    Key::Meta => 0x5B,
+    Key::NumLock => 0x90,
+    Key::Shift => 0xA0,
+    Key::Enter => 0x0D,
+    Key::Tab => 0x09,
+    Key::Space => 0x20,
+    Key::ArrowDown => 0x28,
+    Key::ArrowLeft => 0x25,
+    Key::ArrowRight => 0x27,
+    Key::ArrowUp => 0x26,
+    Key::End => 0x23,
+    Key::Home => 0x24,
+    Key::PageDown => 0x22,
+    Key::PageUp => 0x21,
+    Key::Escape => 0x1B,
+    Key::Backspace => 0x08,
+    Key::Insert => 0x2D,
+    Key::Delete => 0x2E,
+    Key::F1 => 0x70,
+    Key::F2 => 0x71,
+    Key::F3 => 0x72,
+    Key::F4 => 0x73,
+    Key::F5 => 0x74,
+    Key::F6 => 0x75,
+    Key::F7 => 0x76,
+    Key::F8 => 0x77,
+    Key::F9 => 0x78,
+    Key::F10 => 0x79,
+    Key::F11 => 0x7A,
+    Key::F12 => 0x7B,
+    Key::F13 => 0x7C,
+    Key::F14 => 0x7D,
+    Key::F15 => 0x7E,
+    Key::F16 => 0x7F,
+    Key::F17 => 0x80,
+    Key::F18 => 0x81,
+    Key::F19 => 0x82,
+    Key::F20 => 0x83,
+    Key::A => 0x41,
+    Key::B => 0x42,
+    Key::C => 0x43,
+    Key::D => 0x44,
+    Key::E => 0x45,
+    Key::F => 0x46,
+    Key::G => 0x47,
+    Key::H => 0x48,
+    Key::I => 0x49,
+    Key::J => 0x4A,
+    Key::K => 0x4B,
+    Key::L => 0x4C,
+    Key::M => 0x4D,
+    Key::N => 0x4E,
+    Key::O => 0x4F,
+    Key::P => 0x50,
+    Key::Q => 0x51,
+    Key::R => 0x52,
+    Key::S => 0x53,
+    Key::T => 0x54,
+    Key::U => 0x55,
+    Key::V => 0x56,
+    Key::W => 0x57,
+    Key::X => 0x58,
+    Key::Y => 0x59,
+    Key::Z => 0x5A,
+    Key::N0 => 0x30,
+    Key::N1 => 0x31,
+    Key::N2 => 0x32,
+    Key::N3 => 0x33,
+    Key::N4 => 0x34,
+    Key::N5 => 0x35,
+    Key::N6 => 0x36,
+    Key::N7 => 0x37,
+    Key::N8 => 0x38,
+    Key::N9 => 0x39,
+    Key::Numpad0 => 0x60,
+    Key::Numpad1 => 0x61,
+    Key::Numpad2 => 0x62,
+    Key::Numpad3 => 0x63,
+    Key::Numpad4 => 0x64,
+    Key::Numpad5 => 0x65,
+    Key::Numpad6 => 0x66,
+    Key::Numpad7 => 0x67,
+    Key::Numpad8 => 0x68,
+    Key::Numpad9 => 0x69,
+    Key::Raw(code) => *code,
+  };
+  Some(vkey)
+}
diff --git a/espanso-inject/src/x11/README.md b/espanso-inject/src/x11/README.md
new file mode 100644
index 00000000..ac258d22
--- /dev/null
+++ b/espanso-inject/src/x11/README.md
@@ -0,0 +1,53 @@
+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/ffi.rs b/espanso-inject/src/x11/ffi.rs
new file mode 100644
index 00000000..d90d3330
--- /dev/null
+++ b/espanso-inject/src/x11/ffi.rs
@@ -0,0 +1,85 @@
+// Some of these structures/methods are taken from the X11-rs project
+// https://github.com/erlepereira/x11-rs
+
+use std::{
+  ffi::c_void,
+  os::raw::{c_char, c_long, c_uint, c_ulong},
+};
+
+use libc::c_int;
+
+pub enum Display {}
+pub type Window = u64;
+pub type Bool = i32;
+pub type Time = u64;
+pub type KeySym = u64;
+pub type KeyCode = u8;
+
+#[allow(non_upper_case_globals)]
+pub const KeyPress: c_int = 2;
+#[allow(non_upper_case_globals)]
+pub const KeyRelease: c_int = 3;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[repr(C)]
+pub struct XKeyEvent {
+  pub type_: c_int,
+  pub serial: c_ulong,
+  pub send_event: Bool,
+  pub display: *mut Display,
+  pub window: Window,
+  pub root: Window,
+  pub subwindow: Window,
+  pub time: Time,
+  pub x: c_int,
+  pub y: c_int,
+  pub x_root: c_int,
+  pub y_root: c_int,
+  pub state: c_uint,
+  pub keycode: c_uint,
+  pub same_screen: Bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[repr(C)]
+pub struct XModifierKeymap {
+  pub max_keypermod: c_int,
+  pub modifiermap: *mut KeyCode,
+}
+
+#[link(name = "X11")]
+extern "C" {
+  pub fn XOpenDisplay(name: *const c_char) -> *mut Display;
+  pub fn XCloseDisplay(display: *mut Display);
+  pub fn XLookupString(
+    event: *const XKeyEvent,
+    buffer_return: *mut c_char,
+    bytes_buffer: c_int,
+    keysym_return: *mut KeySym,
+    status_in_out: *const c_void,
+  ) -> c_int;
+  pub fn XDefaultRootWindow(display: *mut Display) -> Window;
+  pub fn XGetInputFocus(
+    display: *mut Display,
+    window_out: *mut Window,
+    revert_to: *mut c_int,
+  ) -> c_int;
+  pub fn XFlush(display: *mut Display) -> c_int;
+  pub fn XSendEvent(
+    display: *mut Display,
+    window: Window,
+    propagate: c_int,
+    event_mask: c_long,
+    event_send: *mut XKeyEvent,
+  ) -> c_int;
+  pub fn XGetModifierMapping(display: *mut Display) -> *mut XModifierKeymap;
+  pub fn XFreeModifiermap(map: *mut XModifierKeymap) -> c_int;
+  pub fn XTestFakeKeyEvent(
+    display: *mut Display,
+    key_code: c_uint,
+    is_press: c_int,
+    time: c_ulong,
+  ) -> c_int;
+  pub fn XSync(display: *mut Display, discard: c_int) -> c_int;
+  pub fn XQueryKeymap(display: *mut Display, keys_return: *mut u8);
+}
diff --git a/espanso-inject/src/x11/mod.rs b/espanso-inject/src/x11/mod.rs
new file mode 100644
index 00000000..7878fe51
--- /dev/null
+++ b/espanso-inject/src/x11/mod.rs
@@ -0,0 +1,433 @@
+/*
+ * 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/>.
+ */
+
+mod ffi;
+
+use std::{
+  collections::HashMap,
+  ffi::{CStr, CString},
+  os::raw::c_char,
+  slice,
+};
+
+use ffi::{
+  Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
+  XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString,
+  XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent,
+};
+use log::error;
+
+use crate::linux::raw_keys::convert_to_sym_array;
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::{keys, InjectionOptions, Injector};
+
+// 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 KeyRecord {
+  // Keycode
+  code: u32,
+  // Modifier state which combined with the code produces the char
+  // This is a bit mask:
+  state: u32,
+}
+
+type CharMap = HashMap<String, KeyRecord>;
+type SymMap = HashMap<KeySym, KeyRecord>;
+
+pub struct X11Injector {
+  display: *mut Display,
+
+  char_map: CharMap,
+  sym_map: SymMap,
+}
+
+#[allow(clippy::new_without_default)]
+impl X11Injector {
+  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) -> (CharMap, SymMap) {
+    let mut char_map = HashMap::new();
+    let mut sym_map = HashMap::new();
+
+    let root_window = unsafe { XDefaultRootWindow(display) };
+
+    // 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 event = XKeyEvent {
+          display,
+          keycode: code_with_offset,
+          state: modifier_state,
+
+          // These might not even need to be filled
+          window: root_window,
+          root: root_window,
+          same_screen: 1,
+          time: 0,
+          type_: KeyRelease,
+          x_root: 1,
+          y_root: 1,
+          x: 1,
+          y: 1,
+          subwindow: 0,
+          serial: 0,
+          send_event: 0,
+        };
+
+        let mut sym: KeySym = 0;
+        let mut buffer: [c_char; 10] = [0; 10];
+        let result = unsafe {
+          XLookupString(
+            &event,
+            buffer.as_mut_ptr(),
+            (buffer.len() - 1) as i32,
+            &mut sym,
+            std::ptr::null(),
+          )
+        };
+
+        let key_record = KeyRecord {
+          code: code_with_offset,
+          state: modifier_state,
+        };
+
+        // 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);
+        }
+      }
+    }
+
+    (char_map, sym_map)
+  }
+
+  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.code as u8)) {
+          current_state |= 1 << mod_index;
+        }
+      }
+
+      current_record.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: &KeyRecord, 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: &KeyRecord, 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 X11Injector {
+  fn drop(&mut self) {
+    unsafe {
+      XCloseDisplay(self.display);
+    }
+  }
+}
+
+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 {
+        self.xtest_send_key(&record, true, delay_us);
+        self.xtest_send_key(&record, false, delay_us);
+      } else {
+        self.send_key(focused_window, &record, true, delay_us);
+        self.send_key(focused_window, &record, 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, true, delay_us);
+        self.xtest_send_key(&record, false, delay_us);
+      } else {
+        self.send_key(focused_window, &record, true, delay_us);
+        self.send_key(focused_window, &record, 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, true, delay_us);
+      } else {
+        self.send_key(focused_window, record, true, delay_us);
+      }
+    }
+
+    // Then release them
+    for record in records.iter().rev() {
+      if options.disable_fast_inject {
+        self.xtest_send_key(record, false, delay_us);
+      } else {
+        self.send_key(focused_window, record, 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-ipc/Cargo.toml b/espanso-ipc/Cargo.toml
new file mode 100644
index 00000000..9103412e
--- /dev/null
+++ b/espanso-ipc/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "espanso-ipc"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+serde = { version = "1.0.123", features = ["derive"] }
+serde_json = "1.0.62"
+crossbeam = "0.8.0"
+
+[target.'cfg(windows)'.dependencies]
+named_pipe = "0.4.1"
\ No newline at end of file
diff --git a/espanso-ipc/src/lib.rs b/espanso-ipc/src/lib.rs
new file mode 100644
index 00000000..1ba7b118
--- /dev/null
+++ b/espanso-ipc/src/lib.rs
@@ -0,0 +1,269 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use serde::{de::DeserializeOwned, Serialize};
+use std::path::Path;
+use thiserror::Error;
+
+#[cfg(target_os = "windows")]
+pub mod windows;
+
+#[cfg(not(target_os = "windows"))]
+pub mod unix;
+
+mod util;
+
+pub type EventHandler<Event> = Box<dyn Fn(Event) -> EventHandlerResponse<Event>>;
+
+pub enum EventHandlerResponse<Event> {
+  NoResponse,
+  Response(Event),
+  Error(anyhow::Error),
+  Exit,
+}
+
+pub trait IPCServer<Event> {
+  fn run(self, handler: EventHandler<Event>) -> Result<()>;
+}
+
+pub trait IPCClient<Event> {
+  fn send_sync(&mut self, event: Event) -> Result<Event>;
+  fn send_async(&mut self, event: Event) -> Result<()>;
+}
+
+#[cfg(not(target_os = "windows"))]
+pub fn server<Event: Send + Sync + DeserializeOwned + Serialize>(
+  id: &str,
+  parent_dir: &Path,
+) -> Result<impl IPCServer<Event>> {
+  let server = unix::UnixIPCServer::new(id, parent_dir)?;
+  Ok(server)
+}
+
+#[cfg(not(target_os = "windows"))]
+pub fn client<Event: Serialize + DeserializeOwned>(
+  id: &str,
+  parent_dir: &Path,
+) -> Result<impl IPCClient<Event>> {
+  let client = unix::UnixIPCClient::new(id, parent_dir)?;
+  Ok(client)
+}
+
+#[cfg(target_os = "windows")]
+pub fn server<Event: Send + Sync + DeserializeOwned + Serialize>(
+  id: &str,
+  _: &Path,
+) -> Result<impl IPCServer<Event>> {
+  let server = windows::WinIPCServer::new(id)?;
+  Ok(server)
+}
+
+#[cfg(target_os = "windows")]
+pub fn client<Event: Serialize + DeserializeOwned>(
+  id: &str,
+  _: &Path,
+) -> Result<impl IPCClient<Event>> {
+  let client = windows::WinIPCClient::new(id)?;
+  Ok(client)
+}
+
+#[derive(Error, Debug)]
+pub enum IPCServerError {
+  #[error("stream ended")]
+  StreamEnded,
+
+  #[error("handler reported error `{0}`")]
+  HandlerError(#[from] anyhow::Error),
+}
+
+#[derive(Error, Debug)]
+pub enum IPCClientError {
+  #[error("empty response")]
+  EmptyResponse,
+
+  #[error("malformed response received `{0}`")]
+  MalformedResponse(#[from] anyhow::Error),
+
+  #[error("message response timed out")]
+  Timeout,
+}
+
+#[cfg(test)]
+mod tests {
+  use std::sync::mpsc::channel;
+
+  use super::*;
+  use serde::{Deserialize, Serialize};
+
+  #[derive(Serialize, Deserialize)]
+  enum Event {
+    Async,
+    Sync(String),
+    SyncResult(String),
+    ExitRequest,
+  }
+
+  #[test]
+  fn ipc_async_message() {
+    let server = server::<Event>("testespansoipcasync", &std::env::temp_dir()).unwrap();
+
+    let client_handle = std::thread::spawn(move || {
+      let mut client = client::<Event>("testespansoipcasync", &std::env::temp_dir()).unwrap();
+
+      client.send_async(Event::Async).unwrap();
+      client.send_async(Event::ExitRequest).unwrap();
+    });
+
+    server
+      .run(Box::new(move |event| match event {
+        Event::ExitRequest => EventHandlerResponse::Exit,
+        evt => {
+          assert!(matches!(evt, Event::Async));
+          EventHandlerResponse::NoResponse
+        }
+      }))
+      .unwrap();
+
+    client_handle.join().unwrap();
+  }
+
+  #[test]
+  fn ipc_sync_message() {
+    let server = server::<Event>("testespansoipcsync", &std::env::temp_dir()).unwrap();
+
+    let client_handle = std::thread::spawn(move || {
+      let mut client = client::<Event>("testespansoipcsync", &std::env::temp_dir()).unwrap();
+
+      let response = client.send_sync(Event::Sync("test".to_owned())).unwrap();
+      client.send_async(Event::ExitRequest).unwrap();
+
+      assert!(matches!(response, Event::SyncResult(s) if s == "test"));
+    });
+
+    server
+      .run(Box::new(move |event| match event {
+        Event::ExitRequest => EventHandlerResponse::Exit,
+        Event::Sync(s) => EventHandlerResponse::Response(Event::SyncResult(s)),
+        _ => EventHandlerResponse::NoResponse,
+      }))
+      .unwrap();
+
+    client_handle.join().unwrap();
+  }
+
+  #[test]
+  fn ipc_multiple_sync_with_delay_message() {
+    let server = server::<Event>("testespansoipcmultiplesync", &std::env::temp_dir()).unwrap();
+
+    let client_handle = std::thread::spawn(move || {
+      let mut client =
+        client::<Event>("testespansoipcmultiplesync", &std::env::temp_dir()).unwrap();
+
+      let response = client.send_sync(Event::Sync("test".to_owned())).unwrap();
+
+      std::thread::sleep(std::time::Duration::from_millis(500));
+
+      let response2 = client.send_sync(Event::Sync("test2".to_owned())).unwrap();
+      client.send_async(Event::ExitRequest).unwrap();
+
+      assert!(matches!(response, Event::SyncResult(s) if s == "test"));
+      assert!(matches!(response2, Event::SyncResult(s) if s == "test2"));
+    });
+
+    server
+      .run(Box::new(move |event| match event {
+        Event::ExitRequest => EventHandlerResponse::Exit,
+        Event::Sync(s) => EventHandlerResponse::Response(Event::SyncResult(s)),
+        _ => EventHandlerResponse::NoResponse,
+      }))
+      .unwrap();
+
+    client_handle.join().unwrap();
+  }
+
+  #[test]
+  fn ipc_multiple_clients() {
+    let server = server::<Event>("testespansoipcmultiple", &std::env::temp_dir()).unwrap();
+
+    let (tx, rx) = channel();
+
+    let client_handle = std::thread::spawn(move || {
+      let mut client = client::<Event>("testespansoipcmultiple", &std::env::temp_dir()).unwrap();
+
+      let response = client.send_sync(Event::Sync("client1".to_owned())).unwrap();
+
+      tx.send(()).unwrap();
+
+      assert!(matches!(response, Event::SyncResult(s) if s == "client1"));
+    });
+
+    let client_handle2 = std::thread::spawn(move || {
+      let mut client = client::<Event>("testespansoipcmultiple", &std::env::temp_dir()).unwrap();
+
+      let response = client.send_sync(Event::Sync("client2".to_owned())).unwrap();
+
+      // Wait for the other client before terminating
+      rx.recv().unwrap();
+
+      client.send_async(Event::ExitRequest).unwrap();
+
+      assert!(matches!(response, Event::SyncResult(s) if s == "client2"));
+    });
+
+    server
+      .run(Box::new(move |event| match event {
+        Event::ExitRequest => EventHandlerResponse::Exit,
+        Event::Sync(s) => EventHandlerResponse::Response(Event::SyncResult(s)),
+        _ => EventHandlerResponse::NoResponse,
+      }))
+      .unwrap();
+
+    client_handle.join().unwrap();
+    client_handle2.join().unwrap();
+  }
+
+  #[test]
+  fn ipc_sync_big_payload_message() {
+    let server = server::<Event>("testespansoipcsyncbig", &std::env::temp_dir()).unwrap();
+
+    let client_handle = std::thread::spawn(move || {
+      let mut client = client::<Event>("testespansoipcsyncbig", &std::env::temp_dir()).unwrap();
+
+      let mut payload = String::new();
+      for _ in 0..10000 {
+        payload.push_str("log string repeated");
+      }
+      let response = client.send_sync(Event::Sync(payload.clone())).unwrap();
+      client.send_async(Event::ExitRequest).unwrap();
+
+      assert!(matches!(response, Event::SyncResult(s) if s == payload));
+    });
+
+    server
+      .run(Box::new(move |event| match event {
+        Event::ExitRequest => EventHandlerResponse::Exit,
+        Event::Sync(s) => EventHandlerResponse::Response(Event::SyncResult(s)),
+        _ => EventHandlerResponse::NoResponse,
+      }))
+      .unwrap();
+
+    client_handle.join().unwrap();
+  }
+}
diff --git a/espanso-ipc/src/unix.rs b/espanso-ipc/src/unix.rs
new file mode 100644
index 00000000..4ba4336e
--- /dev/null
+++ b/espanso-ipc/src/unix.rs
@@ -0,0 +1,146 @@
+/*
+ * 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 crate::{util::read_line, EventHandlerResponse, IPCClientError};
+use anyhow::Result;
+use log::{error, info};
+use serde::{de::DeserializeOwned, Serialize};
+use std::{
+  io::Write,
+  os::unix::net::{UnixListener, UnixStream},
+  path::Path,
+};
+
+use crate::{EventHandler, IPCClient, IPCServer};
+
+pub struct UnixIPCServer {
+  listener: UnixListener,
+}
+
+impl UnixIPCServer {
+  pub fn new(id: &str, parent_dir: &Path) -> Result<Self> {
+    let socket_path = parent_dir.join(format!("{}.sock", id));
+
+    // Remove previous Unix socket
+    if socket_path.exists() {
+      std::fs::remove_file(&socket_path)?;
+    }
+
+    let listener = UnixListener::bind(&socket_path)?;
+
+    info!(
+      "binded to IPC unix socket: {}",
+      socket_path.to_string_lossy()
+    );
+
+    Ok(Self { listener })
+  }
+}
+
+impl<Event: Send + Sync + DeserializeOwned + Serialize> IPCServer<Event> for UnixIPCServer {
+  fn run(self, handler: EventHandler<Event>) -> Result<()> {
+    loop {
+      let (mut stream, _) = self.listener.accept()?;
+
+      // Read multiple commands from the client
+      loop {
+        match read_line(&mut stream) {
+          Ok(Some(line)) => {
+            let event: Result<Event, serde_json::Error> = serde_json::from_str(&line);
+            match event {
+              Ok(event) => match handler(event) {
+                EventHandlerResponse::Response(response) => {
+                  let mut json_event = serde_json::to_string(&response)?;
+                  json_event.push('\n');
+                  stream.write_all(json_event.as_bytes())?;
+                  stream.flush()?;
+                }
+                EventHandlerResponse::NoResponse => {
+                  // Async event, no need to reply
+                }
+                EventHandlerResponse::Error(err) => {
+                  error!("ipc handler reported an error: {}", err);
+                }
+                EventHandlerResponse::Exit => {
+                  return Ok(());
+                }
+              },
+              Err(error) => {
+                error!("received malformed event from ipc stream: {}", error);
+                break;
+              }
+            }
+          }
+          Ok(None) => {
+            // EOF reached
+            break;
+          }
+          Err(error) => {
+            error!("error reading ipc stream: {}", error);
+            break;
+          }
+        }
+      }
+    }
+  }
+}
+
+pub struct UnixIPCClient {
+  stream: UnixStream,
+}
+
+impl UnixIPCClient {
+  pub fn new(id: &str, parent_dir: &Path) -> Result<Self> {
+    let socket_path = parent_dir.join(format!("{}.sock", id));
+    let stream = UnixStream::connect(&socket_path)?;
+
+    Ok(Self { stream })
+  }
+}
+
+impl<Event: Serialize + DeserializeOwned> IPCClient<Event> for UnixIPCClient {
+  fn send_sync(&mut self, event: Event) -> Result<Event> {
+    {
+      let mut json_event = serde_json::to_string(&event)?;
+      json_event.push('\n');
+      self.stream.write_all(json_event.as_bytes())?;
+      self.stream.flush()?;
+    }
+
+    // Read the response
+    if let Some(line) = read_line(&mut self.stream)? {
+      let event: Result<Event, serde_json::Error> = serde_json::from_str(&line);
+      match event {
+        Ok(response) => Ok(response),
+        Err(err) => Err(IPCClientError::MalformedResponse(err.into()).into()),
+      }
+    } else {
+      Err(IPCClientError::EmptyResponse.into())
+    }
+  }
+
+  fn send_async(&mut self, event: Event) -> Result<()> {
+    let mut json_event = serde_json::to_string(&event)?;
+    json_event.push('\n');
+    self.stream.write_all(json_event.as_bytes())?;
+    self.stream.flush()?;
+
+    Ok(())
+  }
+}
diff --git a/espanso-ipc/src/util.rs b/espanso-ipc/src/util.rs
new file mode 100644
index 00000000..0c5753ca
--- /dev/null
+++ b/espanso-ipc/src/util.rs
@@ -0,0 +1,47 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+
+// Unbuffered version, necessary to concurrently write
+// to the buffer if necessary (when receiving sync messages)
+pub fn read_line<R: std::io::Read>(stream: R) -> Result<Option<String>> {
+  let mut buffer = Vec::new();
+
+  let mut is_eof = true;
+
+  for byte_res in stream.bytes() {
+    let byte = byte_res?;
+
+    if byte == 10 {
+      // Newline
+      break;
+    } else {
+      buffer.push(byte);
+    }
+
+    is_eof = false;
+  }
+
+  if is_eof {
+    Ok(None)
+  } else {
+    Ok(Some(String::from_utf8(buffer)?))
+  }
+}
diff --git a/espanso-ipc/src/windows.rs b/espanso-ipc/src/windows.rs
new file mode 100644
index 00000000..99e8eaf8
--- /dev/null
+++ b/espanso-ipc/src/windows.rs
@@ -0,0 +1,144 @@
+/*
+ * 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 crate::util::read_line;
+use anyhow::Result;
+use log::{error, info};
+use named_pipe::{ConnectingServer, PipeClient, PipeOptions};
+use serde::{de::DeserializeOwned, Serialize};
+use std::io::Write;
+
+use crate::{EventHandler, EventHandlerResponse, IPCClient, IPCClientError, IPCServer};
+
+const DEFAULT_CLIENT_TIMEOUT: u32 = 2000;
+
+pub struct WinIPCServer {
+  server: Option<ConnectingServer>,
+}
+
+impl WinIPCServer {
+  pub fn new(id: &str) -> Result<Self> {
+    let pipe_name = format!("\\\\.\\pipe\\{}", id);
+
+    let options = PipeOptions::new(&pipe_name);
+    let server = Some(options.single()?);
+
+    info!("binded to named pipe: {}", pipe_name);
+
+    Ok(Self { server })
+  }
+}
+
+impl<Event: Send + Sync + DeserializeOwned + Serialize> IPCServer<Event> for WinIPCServer {
+  fn run(mut self, handler: EventHandler<Event>) -> anyhow::Result<()> {
+    let server = self
+      .server
+      .take()
+      .expect("unable to extract IPC server handle");
+    let mut stream = server.wait()?;
+
+    loop {
+      // Read multiple commands from the client
+      loop {
+        match read_line(&mut stream) {
+          Ok(Some(line)) => {
+            let event: Result<Event, serde_json::Error> = serde_json::from_str(&line);
+            match event {
+              Ok(event) => match handler(event) {
+                EventHandlerResponse::Response(response) => {
+                  let mut json_event = serde_json::to_string(&response)?;
+                  json_event.push('\n');
+                  stream.write_all(json_event.as_bytes())?;
+                  stream.flush()?;
+                }
+                EventHandlerResponse::NoResponse => {
+                  // Async event, no need to reply
+                }
+                EventHandlerResponse::Error(err) => {
+                  error!("ipc handler reported an error: {}", err);
+                }
+                EventHandlerResponse::Exit => {
+                  return Ok(());
+                }
+              },
+              Err(error) => {
+                error!("received malformed event from ipc stream: {}", error);
+                break;
+              }
+            }
+          }
+          Ok(None) => {
+            // EOF reached
+            break;
+          }
+          Err(error) => {
+            error!("error reading ipc stream: {}", error);
+            break;
+          }
+        }
+      }
+
+      stream = stream.disconnect()?.wait()?;
+    }
+  }
+}
+
+pub struct WinIPCClient {
+  stream: PipeClient,
+}
+
+impl WinIPCClient {
+  pub fn new(id: &str) -> Result<Self> {
+    let pipe_name = format!("\\\\.\\pipe\\{}", id);
+
+    let stream = PipeClient::connect_ms(&pipe_name, DEFAULT_CLIENT_TIMEOUT)?;
+    Ok(Self { stream })
+  }
+}
+
+impl<Event: Serialize + DeserializeOwned> IPCClient<Event> for WinIPCClient {
+  fn send_sync(&mut self, event: Event) -> Result<Event> {
+    {
+      let mut json_event = serde_json::to_string(&event)?;
+      json_event.push('\n');
+      self.stream.write_all(json_event.as_bytes())?;
+      self.stream.flush()?;
+    }
+
+    // Read the response
+    if let Some(line) = read_line(&mut self.stream)? {
+      let event: Result<Event, serde_json::Error> = serde_json::from_str(&line);
+      match event {
+        Ok(response) => Ok(response),
+        Err(err) => Err(IPCClientError::MalformedResponse(err.into()).into()),
+      }
+    } else {
+      Err(IPCClientError::EmptyResponse.into())
+    }
+  }
+
+  fn send_async(&mut self, event: Event) -> Result<()> {
+    let mut json_event = serde_json::to_string(&event)?;
+    json_event.push('\n');
+    self.stream.write_all(json_event.as_bytes())?;
+    self.stream.flush()?;
+
+    Ok(())
+  }
+}
diff --git a/espanso-kvs/Cargo.toml b/espanso-kvs/Cargo.toml
new file mode 100644
index 00000000..c22a9089
--- /dev/null
+++ b/espanso-kvs/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "espanso-kvs"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+serde = { version = "1.0.123", features = ["derive"] }
+serde_json = "1.0.62"
+
+
+[dev-dependencies]
+tempdir = "0.3.7"
\ No newline at end of file
diff --git a/espanso-kvs/src/lib.rs b/espanso-kvs/src/lib.rs
new file mode 100644
index 00000000..885c124d
--- /dev/null
+++ b/espanso-kvs/src/lib.rs
@@ -0,0 +1,103 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use serde::{de::DeserializeOwned, Serialize};
+
+mod persistent;
+
+#[allow(clippy::upper_case_acronyms)]
+pub trait KVS: Send + Sync + Clone {
+  fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>>;
+  fn set<T: Serialize>(&self, key: &str, value: T) -> Result<()>;
+  fn delete(&self, key: &str) -> Result<()>;
+}
+
+pub fn get_persistent(base_dir: &Path) -> Result<impl KVS> {
+  persistent::PersistentJsonKVS::new(base_dir)
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  use tempdir::TempDir;
+
+  pub fn use_test_directory(callback: impl FnOnce(&Path)) {
+    let dir = TempDir::new("kvstempconfig").unwrap();
+
+    callback(dir.path());
+  }
+
+  #[test]
+  fn test_base_types() {
+    use_test_directory(|base_dir| {
+      let kvs = get_persistent(base_dir).unwrap();
+
+      assert!(kvs.get::<String>("my_key").unwrap().is_none());
+      assert!(kvs.get::<bool>("another_key").unwrap().is_none());
+
+      kvs.set("my_key", "test".to_string()).unwrap();
+      kvs.set("another_key", false).unwrap();
+
+      assert_eq!(kvs.get::<String>("my_key").unwrap().unwrap(), "test");
+      assert!(!kvs.get::<bool>("another_key").unwrap().unwrap());
+
+      kvs.delete("my_key").unwrap();
+
+      assert!(kvs.get::<String>("my_key").unwrap().is_none());
+      assert!(!kvs.get::<bool>("another_key").unwrap().unwrap());
+    });
+  }
+
+  #[test]
+  fn test_type_mismatch() {
+    use_test_directory(|base_dir| {
+      let kvs = get_persistent(base_dir).unwrap();
+
+      assert!(kvs.get::<String>("my_key").unwrap().is_none());
+
+      kvs.set("my_key", "test".to_string()).unwrap();
+
+      assert!(kvs.get::<bool>("my_key").is_err());
+      assert!(kvs.get::<String>("my_key").is_ok());
+    });
+  }
+
+  #[test]
+  fn test_delete_non_existing_key() {
+    use_test_directory(|base_dir| {
+      let kvs = get_persistent(base_dir).unwrap();
+
+      kvs.delete("my_key").unwrap();
+    });
+  }
+
+  #[test]
+  fn test_invalid_key_name() {
+    use_test_directory(|base_dir| {
+      let kvs = get_persistent(base_dir).unwrap();
+
+      assert!(kvs.get::<String>("invalid key name").is_err());
+      assert!(kvs.get::<String>("").is_err());
+    });
+  }
+}
diff --git a/espanso-kvs/src/persistent.rs b/espanso-kvs/src/persistent.rs
new file mode 100644
index 00000000..034ea3e3
--- /dev/null
+++ b/espanso-kvs/src/persistent.rs
@@ -0,0 +1,153 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use serde_json::Value;
+use std::{
+  collections::HashMap,
+  path::{Path, PathBuf},
+  sync::{Arc, Mutex},
+};
+use thiserror::Error;
+
+use super::KVS;
+
+const DEFAULT_KVS_DIR_NAME: &str = "kvs";
+
+#[derive(Clone)]
+pub struct PersistentJsonKVS {
+  kvs_dir: PathBuf,
+  store: Arc<Mutex<HashMap<String, Value>>>,
+}
+
+impl PersistentJsonKVS {
+  pub fn new(base_dir: &Path) -> Result<Self> {
+    let kvs_dir = base_dir.join(DEFAULT_KVS_DIR_NAME);
+    if !kvs_dir.is_dir() {
+      std::fs::create_dir_all(&kvs_dir)?;
+    }
+
+    Ok(Self {
+      kvs_dir,
+      store: Arc::new(Mutex::new(HashMap::new())),
+    })
+  }
+}
+
+impl KVS for PersistentJsonKVS {
+  fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
+    if !is_valid_key_name(key) {
+      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
+    }
+
+    let mut lock = self.store.lock().expect("unable to obtain KVS read lock");
+
+    if let Some(cached_value) = lock.get(key) {
+      let converted_value = serde_json::from_value(cached_value.clone())?;
+      return Ok(Some(converted_value));
+    }
+
+    // Not found in the cache, read from the file
+    let target_file = self.kvs_dir.join(key);
+    if target_file.is_file() {
+      let content = std::fs::read_to_string(&target_file)?;
+      let deserialized_value: Value = serde_json::from_str(&content)?;
+      let converted_value = serde_json::from_value(deserialized_value.clone())?;
+
+      lock.insert(key.to_string(), deserialized_value);
+
+      return Ok(Some(converted_value));
+    }
+
+    Ok(None)
+  }
+
+  fn set<T: serde::Serialize>(&self, key: &str, value: T) -> Result<()> {
+    if !is_valid_key_name(key) {
+      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
+    }
+
+    let mut lock = self.store.lock().expect("unable to obtain KVS write lock");
+
+    let serialized_value = serde_json::to_value(value)?;
+    let serialized_string = serde_json::to_string(&serialized_value)?;
+
+    lock.insert(key.to_string(), serialized_value);
+
+    let target_file = self.kvs_dir.join(key);
+    std::fs::write(target_file, serialized_string)?;
+
+    Ok(())
+  }
+
+  fn delete(&self, key: &str) -> Result<()> {
+    if !is_valid_key_name(key) {
+      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
+    }
+
+    let mut lock = self.store.lock().expect("unable to obtain KVS delete lock");
+
+    lock.remove(key);
+
+    let target_file = self.kvs_dir.join(key);
+    if target_file.is_file() {
+      std::fs::remove_file(target_file)?;
+    }
+
+    Ok(())
+  }
+}
+
+fn is_valid_key_name(key: &str) -> bool {
+  if key.is_empty() || key.len() > 200 {
+    return false;
+  }
+
+  if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
+    return false;
+  }
+
+  true
+}
+
+#[derive(Error, Debug)]
+pub enum PersistentJsonKVSError {
+  #[error("The provided key `{0}` is is invalid. Keys must only be composed of ascii letters, numbers and underscores.")]
+  InvalidKey(String),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_valid_key_names() {
+    assert!(is_valid_key_name("key"));
+    assert!(is_valid_key_name("key_name"));
+    assert!(is_valid_key_name("Another_long_key_name_2"));
+  }
+
+  #[test]
+  fn test_invalid_key_names() {
+    assert!(!is_valid_key_name(""));
+    assert!(!is_valid_key_name("with space"));
+    assert!(!is_valid_key_name("with/special"));
+    assert!(!is_valid_key_name("with\\special"));
+  }
+}
diff --git a/espanso-mac-utils/Cargo.toml b/espanso-mac-utils/Cargo.toml
new file mode 100644
index 00000000..6172e9cf
--- /dev/null
+++ b/espanso-mac-utils/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "espanso-mac-utils"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[dependencies]
+log = "0.4.14"
+lazycell = "1.3.0"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+lazy_static = "1.4.0"
+regex = "1.4.3"
+
+[build-dependencies]
+cc = "1.0.66"
\ No newline at end of file
diff --git a/espanso-mac-utils/build.rs b/espanso-mac-utils/build.rs
new file mode 100644
index 00000000..d6f921a4
--- /dev/null
+++ b/espanso-mac-utils/build.rs
@@ -0,0 +1,42 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(not(target_os = "macos"))]
+fn cc_config() {
+  // Do nothing on Linux and Windows
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/native.mm");
+  println!("cargo:rerun-if-changed=src/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/native.h")
+    .file("src/native.mm")
+    .compile("espansomacutils");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansomacutils");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+  println!("cargo:rustc-link-lib=framework=Carbon");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-mac-utils/src/ffi.rs b/espanso-mac-utils/src/ffi.rs
new file mode 100644
index 00000000..190a6734
--- /dev/null
+++ b/espanso-mac-utils/src/ffi.rs
@@ -0,0 +1,32 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "macos")]
+use std::os::raw::c_char;
+
+#[cfg(target_os = "macos")]
+#[link(name = "espansomacutils", kind = "static")]
+extern "C" {
+  pub fn mac_utils_get_secure_input_process(pid: *mut i64) -> i32;
+  pub fn mac_utils_get_path_from_pid(pid: i64, buffer: *mut c_char, size: i32) -> i32;
+  pub fn mac_utils_check_accessibility() -> i32;
+  pub fn mac_utils_prompt_accessibility() -> i32;
+  pub fn mac_utils_transition_to_foreground_app();
+  pub fn mac_utils_transition_to_background_app();
+}
diff --git a/espanso-mac-utils/src/lib.rs b/espanso-mac-utils/src/lib.rs
new file mode 100644
index 00000000..de6c3b33
--- /dev/null
+++ b/espanso-mac-utils/src/lib.rs
@@ -0,0 +1,138 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "macos")]
+use std::{ffi::CStr, os::raw::c_char};
+
+#[macro_use]
+#[cfg(target_os = "macos")]
+extern crate lazy_static;
+
+mod ffi;
+
+/// Check whether an application is currently holding the Secure Input.
+/// Return None if no application has claimed SecureInput, its PID otherwise.
+#[cfg(target_os = "macos")]
+pub fn get_secure_input_pid() -> Option<i64> {
+  unsafe {
+    let mut pid: i64 = -1;
+    let res = ffi::mac_utils_get_secure_input_process(&mut pid as *mut i64);
+
+    if res > 0 {
+      Some(pid)
+    } else {
+      None
+    }
+  }
+}
+
+/// Check whether an application is currently holding the Secure Input.
+/// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise.
+#[cfg(target_os = "macos")]
+pub fn get_secure_input_application() -> Option<(String, String)> {
+  unsafe {
+    let pid = get_secure_input_pid();
+
+    if let Some(pid) = pid {
+      // Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant.
+      // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently.
+      let mut buffer: [c_char; 4096] = [0; 4096];
+      let res = ffi::mac_utils_get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32);
+
+      if res > 0 {
+        let c_string = CStr::from_ptr(buffer.as_ptr());
+        let string = c_string.to_str();
+        if let Ok(path) = string {
+          if !path.trim().is_empty() {
+            let process = path.trim().to_string();
+            let app_name = if let Some(name) = get_app_name_from_path(&process) {
+              name
+            } else {
+              process.to_owned()
+            };
+
+            return Some((app_name, process));
+          }
+        }
+      }
+    }
+
+    None
+  }
+}
+
+#[cfg(target_os = "macos")]
+fn get_app_name_from_path(path: &str) -> Option<String> {
+  use regex::Regex;
+
+  lazy_static! {
+    static ref APP_REGEX: Regex = Regex::new("/([^/]+).(app|bundle)/").unwrap();
+  };
+
+  let caps = APP_REGEX.captures(path);
+  caps.map(|caps| caps.get(1).map_or("", |m| m.as_str()).to_owned())
+}
+
+#[cfg(target_os = "macos")]
+pub fn check_accessibility() -> bool {
+  unsafe { ffi::mac_utils_check_accessibility() > 0 }
+}
+
+#[cfg(target_os = "macos")]
+pub fn prompt_accessibility() -> bool {
+  unsafe { ffi::mac_utils_prompt_accessibility() > 0 }
+}
+
+#[cfg(target_os = "macos")]
+pub fn convert_to_foreground_app() {
+  unsafe {
+    ffi::mac_utils_transition_to_foreground_app();
+  }
+}
+
+#[cfg(target_os = "macos")]
+pub fn convert_to_background_app() {
+  unsafe {
+    ffi::mac_utils_transition_to_background_app();
+  }
+}
+
+#[cfg(test)]
+#[cfg(target_os = "macos")]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_get_app_name_from_path() {
+    let app_name = get_app_name_from_path("/Applications/iTerm.app/Contents/MacOS/iTerm2");
+    assert_eq!(app_name.unwrap(), "iTerm")
+  }
+
+  #[test]
+  fn test_get_app_name_from_path_no_app_name() {
+    let app_name = get_app_name_from_path("/another/directory");
+    assert!(app_name.is_none())
+  }
+
+  #[test]
+  fn test_get_app_name_from_path_security_bundle() {
+    let app_name = get_app_name_from_path("/System/Library/Frameworks/Security.framework/Versions/A/MachServices/SecurityAgent.bundle/Contents/MacOS/SecurityAgent");
+    assert_eq!(app_name.unwrap(), "SecurityAgent")
+  }
+}
diff --git a/espanso-mac-utils/src/native.h b/espanso-mac-utils/src/native.h
new file mode 100644
index 00000000..984b540a
--- /dev/null
+++ b/espanso-mac-utils/src/native.h
@@ -0,0 +1,43 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_MAC_UTILS_H
+#define ESPANSO_MAC_UTILS_H
+
+#include <stdint.h>
+
+// If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
+extern "C" int32_t mac_utils_get_secure_input_process(int64_t *pid);
+
+// Find the executable path corresponding to the given PID, return 0 if no process was found.
+extern "C" int32_t mac_utils_get_path_from_pid(int64_t pid, char *buff, int buff_size);
+
+// Return 1 if the accessibility permissions have been granted, 0 otherwise
+extern "C" int32_t mac_utils_check_accessibility();
+
+// Return 1 if the accessibility permissions have been granted, 0 otherwise
+extern "C" int32_t mac_utils_prompt_accessibility();
+
+// When called, convert the current process to a foreground app (showing the dock icon).
+extern "C" void mac_utils_transition_to_foreground_app();
+
+// When called, convert the current process to a background app (hide the dock icon).
+extern "C" void mac_utils_transition_to_background_app();
+
+#endif //ESPANSO_MAC_UTILS_H
\ No newline at end of file
diff --git a/espanso-mac-utils/src/native.mm b/espanso-mac-utils/src/native.mm
new file mode 100644
index 00000000..75ff286a
--- /dev/null
+++ b/espanso-mac-utils/src/native.mm
@@ -0,0 +1,91 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <libproc.h>
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+#import <Carbon/Carbon.h>
+
+// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys
+int32_t mac_utils_get_secure_input_process(int64_t *pid) {
+  NSArray *consoleUsersArray;
+  io_service_t rootService;
+  int32_t result = 0;
+
+  if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0)
+  {
+    if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil)
+    {
+      if ([consoleUsersArray isKindOfClass:[NSArray class]])  // Be careful - ensure this really is an array
+      {
+        for (NSDictionary *consoleUserDict in consoleUsersArray) {
+          NSNumber *secureInputPID;
+
+          if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil)
+          {
+            if ([secureInputPID isKindOfClass:[NSNumber class]])
+            {
+              *pid = ((UInt64) [secureInputPID intValue]);
+              result = 1;
+              break;
+            }
+          }
+        }
+      }
+
+      CFRelease((CFTypeRef)consoleUsersArray);
+    }
+
+    IOObjectRelease((io_object_t) rootService);
+  }
+
+  return result;
+}
+
+int32_t mac_utils_get_path_from_pid(int64_t pid, char *buff, int buff_size) {
+  int res = proc_pidpath((pid_t) pid, buff, buff_size);
+  if ( res <= 0 ) {
+    return 0;
+  } else {
+    return 1;
+  }
+}
+
+int32_t mac_utils_check_accessibility() {
+  NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @NO};
+  return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
+}
+
+int32_t mac_utils_prompt_accessibility() {
+  NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES};
+  return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
+}
+
+void mac_utils_transition_to_foreground_app() {
+  ProcessSerialNumber psn = { 0, kCurrentProcess };
+  TransformProcessType(&psn, kProcessTransformToForegroundApplication);
+
+  [[NSApplication sharedApplication] activateIgnoringOtherApps : YES];
+}
+
+void mac_utils_transition_to_background_app() {
+  ProcessSerialNumber psn = { 0, kCurrentProcess };
+  TransformProcessType(&psn, kProcessTransformToUIElementApplication);
+}
\ No newline at end of file
diff --git a/espanso-match/Cargo.toml b/espanso-match/Cargo.toml
new file mode 100644
index 00000000..5cbf6b6b
--- /dev/null
+++ b/espanso-match/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "espanso-match"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+regex = "1.4.3"
+unicase = "2.6.0"
\ No newline at end of file
diff --git a/espanso-match/src/event.rs b/espanso-match/src/event.rs
new file mode 100644
index 00000000..10a3cbbb
--- /dev/null
+++ b/espanso-match/src/event.rs
@@ -0,0 +1,81 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Event {
+  Key { key: Key, chars: Option<String> },
+  VirtualSeparator,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Key {
+  // Modifiers
+  Alt,
+  CapsLock,
+  Control,
+  Meta,
+  NumLock,
+  Shift,
+
+  // Whitespace
+  Enter,
+  Tab,
+  Space,
+
+  // Navigation
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  ArrowUp,
+  End,
+  Home,
+  PageDown,
+  PageUp,
+
+  // UI
+  Escape,
+
+  // Editing keys
+  Backspace,
+
+  // Function keys
+  F1,
+  F2,
+  F3,
+  F4,
+  F5,
+  F6,
+  F7,
+  F8,
+  F9,
+  F10,
+  F11,
+  F12,
+  F13,
+  F14,
+  F15,
+  F16,
+  F17,
+  F18,
+  F19,
+  F20,
+
+  // Others
+  Other,
+}
diff --git a/espanso-match/src/lib.rs b/espanso-match/src/lib.rs
new file mode 100644
index 00000000..f79c0a51
--- /dev/null
+++ b/espanso-match/src/lib.rs
@@ -0,0 +1,55 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::collections::HashMap;
+
+use event::Event;
+
+pub mod event;
+pub mod regex;
+pub mod rolling;
+mod util;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchResult<Id> {
+  pub id: Id,
+  pub trigger: String,
+  pub left_separator: Option<String>,
+  pub right_separator: Option<String>,
+  pub vars: HashMap<String, String>,
+}
+
+impl<Id: Default> Default for MatchResult<Id> {
+  fn default() -> Self {
+    Self {
+      id: Id::default(),
+      trigger: "".to_string(),
+      left_separator: None,
+      right_separator: None,
+      vars: HashMap::new(),
+    }
+  }
+}
+
+pub trait Matcher<'a, State, Id>
+where
+  Id: Clone,
+{
+  fn process(&'a self, prev_state: Option<&State>, event: Event) -> (State, Vec<MatchResult<Id>>);
+}
diff --git a/espanso-match/src/regex/mod.rs b/espanso-match/src/regex/mod.rs
new file mode 100644
index 00000000..560f513d
--- /dev/null
+++ b/espanso-match/src/regex/mod.rs
@@ -0,0 +1,276 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-202 case_insensitive: (), preserve_case_markers: (), left_word: (), right_word: ()1 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;
+
+use log::error;
+use regex::{Regex, RegexSet};
+
+use crate::Matcher;
+use crate::{event::Event, MatchResult};
+
+#[derive(Debug)]
+pub struct RegexMatch<Id> {
+  pub id: Id,
+  pub regex: String,
+}
+
+impl<Id> RegexMatch<Id> {
+  pub fn new(id: Id, regex: &str) -> Self {
+    Self {
+      id,
+      regex: regex.to_string(),
+    }
+  }
+}
+
+#[derive(Clone)]
+pub struct RegexMatcherState {
+  buffer: String,
+}
+
+impl Default for RegexMatcherState {
+  fn default() -> Self {
+    Self {
+      buffer: String::new(),
+    }
+  }
+}
+
+pub struct RegexMatcherOptions {
+  pub max_buffer_size: usize,
+}
+
+impl Default for RegexMatcherOptions {
+  fn default() -> Self {
+    Self {
+      max_buffer_size: 30,
+    }
+  }
+}
+
+pub struct RegexMatcher<Id> {
+  ids: Vec<Id>,
+  // The RegexSet is used to efficiently determine which regexes match
+  regex_set: RegexSet,
+
+  // The single regexes are then used to find the captures
+  regexes: Vec<Regex>,
+
+  max_buffer_size: usize,
+}
+
+impl<'a, Id> Matcher<'a, RegexMatcherState, Id> for RegexMatcher<Id>
+where
+  Id: Clone,
+{
+  fn process(
+    &'a self,
+    prev_state: Option<&RegexMatcherState>,
+    event: Event,
+  ) -> (RegexMatcherState, Vec<MatchResult<Id>>) {
+    let mut buffer = if let Some(prev_state) = prev_state {
+      prev_state.buffer.clone()
+    } else {
+      "".to_string()
+    };
+
+    if let Event::Key {
+      key: _,
+      chars: Some(chars),
+    } = event
+    {
+      buffer.push_str(&chars);
+    }
+
+    // Keep the buffer length in check
+    if buffer.len() > self.max_buffer_size {
+      buffer.remove(0);
+    }
+
+    // Find matches
+    if self.regex_set.is_match(&buffer) {
+      let mut matches = Vec::new();
+
+      for index in self.regex_set.matches(&buffer) {
+        if let (Some(id), Some(regex)) = (self.ids.get(index), self.regexes.get(index)) {
+          if let Some(captures) = regex.captures(&buffer) {
+            let full_match = captures.get(0).map_or("", |m| m.as_str());
+            if !full_match.is_empty() {
+              // Now extract the captured names as variables
+              let variables: HashMap<String, String> = regex
+                .capture_names()
+                .flatten()
+                .filter_map(|n| Some((n.to_string(), captures.name(n)?.as_str().to_string())))
+                .collect();
+
+              let result = MatchResult {
+                id: (*id).clone(),
+                trigger: full_match.to_string(),
+                left_separator: None,
+                right_separator: None,
+                vars: variables,
+              };
+
+              matches.push(result);
+            }
+          }
+        } else {
+          error!(
+            "received inconsistent index from regex set with index: {}",
+            index
+          );
+        }
+      }
+
+      if !matches.is_empty() {
+        return (RegexMatcherState::default(), matches);
+      }
+    }
+
+    let current_state = RegexMatcherState { buffer };
+    (current_state, Vec::new())
+  }
+}
+
+impl<Id: Clone> RegexMatcher<Id> {
+  pub fn new(matches: &[RegexMatch<Id>], opt: RegexMatcherOptions) -> Self {
+    let mut ids = Vec::new();
+    let mut regexes = Vec::new();
+    let mut good_regexes = Vec::new();
+
+    for m in matches {
+      match Regex::new(&m.regex) {
+        Ok(regex) => {
+          ids.push(m.id.clone());
+          good_regexes.push(&m.regex);
+          regexes.push(regex);
+        }
+        Err(err) => {
+          error!("unable to compile regex: '{}', error: {:?}", m.regex, err);
+        }
+      }
+    }
+
+    let regex_set = RegexSet::new(&good_regexes).expect("unable to build regex set");
+
+    Self {
+      ids,
+      regex_set,
+      regexes,
+      max_buffer_size: opt.max_buffer_size,
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::util::tests::get_matches_after_str;
+
+  fn match_result<Id: Default>(id: Id, trigger: &str, vars: &[(&str, &str)]) -> MatchResult<Id> {
+    let vars: HashMap<String, String> = vars
+      .iter()
+      .map(|(key, value)| (key.to_string(), value.to_string()))
+      .collect();
+
+    MatchResult {
+      id,
+      trigger: trigger.to_string(),
+      left_separator: None,
+      right_separator: None,
+      vars,
+    }
+  }
+
+  #[test]
+  fn matcher_simple_matches() {
+    let matcher = RegexMatcher::new(
+      &[
+        RegexMatch::new(1, "hello"),
+        RegexMatch::new(2, "num\\d{1,3}s"),
+      ],
+      RegexMatcherOptions::default(),
+    );
+    assert_eq!(get_matches_after_str("hi", &matcher), vec![]);
+    assert_eq!(
+      get_matches_after_str("hello", &matcher),
+      vec![match_result(1, "hello", &[])]
+    );
+    assert_eq!(
+      get_matches_after_str("say hello", &matcher),
+      vec![match_result(1, "hello", &[])]
+    );
+    assert_eq!(
+      get_matches_after_str("num1s", &matcher),
+      vec![match_result(2, "num1s", &[])]
+    );
+    assert_eq!(
+      get_matches_after_str("num134s", &matcher),
+      vec![match_result(2, "num134s", &[])]
+    );
+    assert_eq!(get_matches_after_str("nums", &matcher), vec![]);
+  }
+
+  #[test]
+  fn matcher_with_variables() {
+    let matcher = RegexMatcher::new(
+      &[
+        RegexMatch::new(1, "hello\\((?P<name>.*?)\\)"),
+        RegexMatch::new(2, "multi\\((?P<name1>.*?),(?P<name2>.*?)\\)"),
+      ],
+      RegexMatcherOptions::default(),
+    );
+    assert_eq!(get_matches_after_str("hi", &matcher), vec![]);
+    assert_eq!(
+      get_matches_after_str("say hello(mary)", &matcher),
+      vec![match_result(1, "hello(mary)", &[("name", "mary")])]
+    );
+    assert_eq!(get_matches_after_str("hello(mary", &matcher), vec![]);
+    assert_eq!(
+      get_matches_after_str("multi(mary,jane)", &matcher),
+      vec![match_result(
+        2,
+        "multi(mary,jane)",
+        &[("name1", "mary"), ("name2", "jane")]
+      )]
+    );
+  }
+
+  #[test]
+  fn matcher_max_buffer_size() {
+    let matcher = RegexMatcher::new(
+      &[
+        RegexMatch::new(1, "hello\\((?P<name>.*?)\\)"),
+        RegexMatch::new(2, "multi\\((?P<name1>.*?),(?P<name2>.*?)\\)"),
+      ],
+      RegexMatcherOptions {
+        max_buffer_size: 15,
+      },
+    );
+    assert_eq!(
+      get_matches_after_str("say hello(mary)", &matcher),
+      vec![match_result(1, "hello(mary)", &[("name", "mary")])]
+    );
+    assert_eq!(
+      get_matches_after_str("hello(very long name over buffer)", &matcher),
+      vec![]
+    );
+  }
+}
diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs
new file mode 100644
index 00000000..136bed31
--- /dev/null
+++ b/espanso-match/src/rolling/matcher.rs
@@ -0,0 +1,364 @@
+/*
+ * 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;
+
+use super::{
+  tree::{MatcherTreeNode, MatcherTreeRef},
+  util::extract_string_from_events,
+  RollingMatch,
+};
+use crate::Matcher;
+use crate::{
+  event::{Event, Key},
+  MatchResult,
+};
+use unicase::UniCase;
+
+pub(crate) type IsWordSeparator = bool;
+
+#[derive(Clone)]
+pub struct RollingMatcherState<'a, Id> {
+  paths: Vec<RollingMatcherStatePath<'a, Id>>,
+}
+
+impl<'a, Id> Default for RollingMatcherState<'a, Id> {
+  fn default() -> Self {
+    Self { paths: Vec::new() }
+  }
+}
+
+#[derive(Clone)]
+struct RollingMatcherStatePath<'a, Id> {
+  node: &'a MatcherTreeNode<Id>,
+  events: Vec<(Event, IsWordSeparator)>,
+}
+
+pub struct RollingMatcherOptions {
+  pub char_word_separators: Vec<String>,
+  pub key_word_separators: Vec<Key>,
+}
+
+impl Default for RollingMatcherOptions {
+  fn default() -> Self {
+    Self {
+      char_word_separators: Vec::new(),
+      key_word_separators: Vec::new(),
+    }
+  }
+}
+
+pub struct RollingMatcher<Id> {
+  char_word_separators: Vec<String>,
+  key_word_separators: Vec<Key>,
+
+  root: MatcherTreeNode<Id>,
+}
+
+impl<'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher<Id>
+where
+  Id: Clone,
+{
+  fn process(
+    &'a self,
+    prev_state: Option<&RollingMatcherState<'a, Id>>,
+    event: Event,
+  ) -> (RollingMatcherState<'a, Id>, Vec<MatchResult<Id>>) {
+    let mut next_refs = Vec::new();
+
+    // First compute the old refs
+    if let Some(prev_state) = prev_state {
+      for node_path in prev_state.paths.iter() {
+        next_refs.extend(
+          self
+            .find_refs(node_path.node, &event, true)
+            .into_iter()
+            .map(|(node_ref, is_word_separator)| {
+              let mut new_events = node_path.events.clone();
+              new_events.push((event.clone(), is_word_separator));
+              (node_ref, new_events)
+            }),
+        );
+      }
+    }
+
+    // Calculate new ones
+    let root_refs = self.find_refs(&self.root, &event, prev_state.is_some());
+    next_refs.extend(
+      root_refs
+        .into_iter()
+        .map(|(node_ref, is_word_separator)| (node_ref, vec![(event.clone(), is_word_separator)])),
+    );
+
+    let mut next_paths = Vec::new();
+
+    for (node_ref, events) in next_refs {
+      match node_ref {
+        MatcherTreeRef::Matches(matches) => {
+          let (trigger, left_separator, right_separator) = extract_string_from_events(&events);
+          let results = matches
+            .iter()
+            .map(|id| MatchResult {
+              id: id.clone(),
+              trigger: trigger.clone(),
+              left_separator: left_separator.clone(),
+              right_separator: right_separator.clone(),
+              vars: HashMap::new(),
+            })
+            .collect();
+
+          // Reset the state and return the matches
+          return (RollingMatcherState::default(), results);
+        }
+        MatcherTreeRef::Node(node) => {
+          next_paths.push(RollingMatcherStatePath {
+            node: node.as_ref(),
+            events,
+          });
+        }
+      }
+    }
+
+    let current_state = RollingMatcherState { paths: next_paths };
+
+    (current_state, Vec::new())
+  }
+}
+
+impl<Id: Clone> RollingMatcher<Id> {
+  pub fn new(matches: &[RollingMatch<Id>], opt: RollingMatcherOptions) -> Self {
+    let root = MatcherTreeNode::from_matches(matches);
+    Self {
+      root,
+      char_word_separators: opt.char_word_separators,
+      key_word_separators: opt.key_word_separators,
+    }
+  }
+
+  fn find_refs<'a>(
+    &'a self,
+    node: &'a MatcherTreeNode<Id>,
+    event: &Event,
+    has_previous_state: bool,
+  ) -> Vec<(&'a MatcherTreeRef<Id>, IsWordSeparator)> {
+    let mut refs = Vec::new();
+
+    if let Event::Key { key, chars } = event {
+      // Key matching
+      if let Some((_, node_ref)) = node.keys.iter().find(|(_key, _)| _key == key) {
+        refs.push((node_ref, false));
+      }
+
+      if let Some(char) = chars {
+        // Char matching
+        if let Some((_, node_ref)) = node.chars.iter().find(|(_char, _)| _char == char) {
+          refs.push((node_ref, false));
+        }
+
+        // Char case-insensitive
+        let insensitive_char = UniCase::new(char);
+        if let Some((_, node_ref)) = node
+          .chars_insensitive
+          .iter()
+          .find(|(_char, _)| *_char == insensitive_char)
+        {
+          refs.push((node_ref, false));
+        }
+      }
+    }
+
+    if self.is_word_separator(event) {
+      if let Some(node_ref) = node.word_separators.as_ref() {
+        refs.push((node_ref, true))
+      }
+    }
+
+    // If there is no previous state, we handle it as a word separator, exploring a step forward
+    // in the state.
+    if !has_previous_state {
+      if let Some(MatcherTreeRef::Node(node)) = node.word_separators.as_ref() {
+        refs.extend(self.find_refs(&*node, event, true));
+      }
+    }
+
+    refs
+  }
+
+  fn is_word_separator(&self, event: &Event) -> bool {
+    match event {
+      Event::Key { key, chars } => {
+        if self.key_word_separators.contains(key) {
+          true
+        } else if let Some(char) = chars {
+          self.char_word_separators.contains(char)
+        } else {
+          false
+        }
+      }
+      Event::VirtualSeparator => true,
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::rolling::StringMatchOptions;
+  use crate::util::tests::get_matches_after_str;
+
+  fn match_result<Id: Default>(id: Id, trigger: &str) -> MatchResult<Id> {
+    MatchResult {
+      id,
+      trigger: trigger.to_string(),
+      ..Default::default()
+    }
+  }
+
+  fn match_result_with_sep<Id: Default>(
+    id: Id,
+    trigger: &str,
+    left: Option<&str>,
+    right: Option<&str>,
+  ) -> MatchResult<Id> {
+    MatchResult {
+      id,
+      trigger: trigger.to_string(),
+      left_separator: left.map(str::to_owned),
+      right_separator: right.map(str::to_owned),
+      ..Default::default()
+    }
+  }
+
+  #[test]
+  fn matcher_process_simple_strings() {
+    let matcher = RollingMatcher::new(
+      &[
+        RollingMatch::from_string(1, "hi", &StringMatchOptions::default()),
+        RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
+        RollingMatch::from_string(3, "my", &StringMatchOptions::default()),
+        RollingMatch::from_string(4, "myself", &StringMatchOptions::default()),
+        RollingMatch::from_string(5, "hi", &StringMatchOptions::default()),
+      ],
+      RollingMatcherOptions {
+        ..Default::default()
+      },
+    );
+
+    assert_eq!(
+      get_matches_after_str("hi", &matcher),
+      vec![match_result(1, "hi"), match_result(5, "hi")]
+    );
+    assert_eq!(
+      get_matches_after_str("my", &matcher),
+      vec![match_result(3, "my")]
+    );
+    assert_eq!(
+      get_matches_after_str("mmy", &matcher),
+      vec![match_result(3, "my")]
+    );
+    assert_eq!(get_matches_after_str("invalid", &matcher), vec![]);
+  }
+
+  #[test]
+  fn matcher_process_word_matches() {
+    let matcher = RollingMatcher::new(
+      &[
+        RollingMatch::from_string(
+          1,
+          "hi",
+          &StringMatchOptions {
+            left_word: true,
+            right_word: true,
+            ..Default::default()
+          },
+        ),
+        RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
+      ],
+      RollingMatcherOptions {
+        char_word_separators: vec![".".to_string(), ",".to_string()],
+        ..Default::default()
+      },
+    );
+
+    assert_eq!(get_matches_after_str("hi", &matcher), vec![]);
+    // Word matches are also triggered when there is no left separator but it's a new state
+    assert_eq!(
+      get_matches_after_str("hi,", &matcher),
+      vec![match_result_with_sep(1, "hi,", None, Some(","))]
+    );
+    assert_eq!(
+      get_matches_after_str(".hi,", &matcher),
+      vec![match_result_with_sep(1, ".hi,", Some("."), Some(","))]
+    );
+  }
+
+  #[test]
+  fn matcher_process_case_insensitive() {
+    let matcher = RollingMatcher::new(
+      &[
+        RollingMatch::from_string(
+          1,
+          "hi",
+          &StringMatchOptions {
+            case_insensitive: true,
+            ..Default::default()
+          },
+        ),
+        RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
+        RollingMatch::from_string(
+          3,
+          "arty",
+          &StringMatchOptions {
+            case_insensitive: true,
+            ..Default::default()
+          },
+        ),
+      ],
+      RollingMatcherOptions {
+        char_word_separators: vec![".".to_string(), ",".to_string()],
+        ..Default::default()
+      },
+    );
+
+    assert_eq!(
+      get_matches_after_str("hi", &matcher),
+      vec![match_result(1, "hi")]
+    );
+    assert_eq!(
+      get_matches_after_str("Hi", &matcher),
+      vec![match_result(1, "Hi")]
+    );
+    assert_eq!(
+      get_matches_after_str("HI", &matcher),
+      vec![match_result(1, "HI")]
+    );
+    assert_eq!(
+      get_matches_after_str("arty", &matcher),
+      vec![match_result(3, "arty")]
+    );
+    assert_eq!(
+      get_matches_after_str("arTY", &matcher),
+      vec![match_result(3, "arTY")]
+    );
+    assert_eq!(
+      get_matches_after_str("ARTY", &matcher),
+      vec![match_result(3, "ARTY")]
+    );
+  }
+}
diff --git a/espanso-match/src/rolling/mod.rs b/espanso-match/src/rolling/mod.rs
new file mode 100644
index 00000000..57d60b7e
--- /dev/null
+++ b/espanso-match/src/rolling/mod.rs
@@ -0,0 +1,181 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-202 case_insensitive: (), preserve_case_markers: (), left_word: (), right_word: ()1 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 crate::event::Key;
+
+pub mod matcher;
+mod tree;
+mod util;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum RollingItem {
+  WordSeparator,
+  Key(Key),
+  Char(String),
+  CharInsensitive(String),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct RollingMatch<Id> {
+  pub id: Id,
+  pub items: Vec<RollingItem>,
+}
+
+impl<Id> RollingMatch<Id> {
+  pub fn new(id: Id, items: Vec<RollingItem>) -> Self {
+    Self { id, items }
+  }
+
+  pub fn from_string(id: Id, string: &str, opt: &StringMatchOptions) -> Self {
+    let mut items = Vec::new();
+
+    if opt.left_word {
+      items.push(RollingItem::WordSeparator);
+    }
+
+    for c in string.chars() {
+      if opt.case_insensitive {
+        items.push(RollingItem::CharInsensitive(c.to_string()))
+      } else {
+        items.push(RollingItem::Char(c.to_string()))
+      }
+    }
+
+    if opt.right_word {
+      items.push(RollingItem::WordSeparator);
+    }
+
+    Self { id, items }
+  }
+
+  pub fn from_items(id: Id, items: &[RollingItem]) -> Self {
+    Self {
+      id,
+      items: items.to_vec(),
+    }
+  }
+}
+
+pub struct StringMatchOptions {
+  pub case_insensitive: bool,
+  pub left_word: bool,
+  pub right_word: bool,
+}
+
+impl Default for StringMatchOptions {
+  fn default() -> Self {
+    Self {
+      case_insensitive: false,
+      left_word: false,
+      right_word: false,
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_match_from_string_base_case() {
+    assert_eq!(
+      RollingMatch::from_string(1, "test", &StringMatchOptions::default()),
+      RollingMatch {
+        id: 1,
+        items: vec![
+          RollingItem::Char("t".to_string()),
+          RollingItem::Char("e".to_string()),
+          RollingItem::Char("s".to_string()),
+          RollingItem::Char("t".to_string()),
+        ]
+      }
+    )
+  }
+
+  #[test]
+  fn test_match_from_string_left_word() {
+    assert_eq!(
+      RollingMatch::from_string(
+        1,
+        "test",
+        &StringMatchOptions {
+          left_word: true,
+          ..Default::default()
+        }
+      ),
+      RollingMatch {
+        id: 1,
+        items: vec![
+          RollingItem::WordSeparator,
+          RollingItem::Char("t".to_string()),
+          RollingItem::Char("e".to_string()),
+          RollingItem::Char("s".to_string()),
+          RollingItem::Char("t".to_string()),
+        ]
+      }
+    )
+  }
+
+  #[test]
+  fn test_match_from_string_right_word() {
+    assert_eq!(
+      RollingMatch::from_string(
+        1,
+        "test",
+        &StringMatchOptions {
+          right_word: true,
+          ..Default::default()
+        }
+      ),
+      RollingMatch {
+        id: 1,
+        items: vec![
+          RollingItem::Char("t".to_string()),
+          RollingItem::Char("e".to_string()),
+          RollingItem::Char("s".to_string()),
+          RollingItem::Char("t".to_string()),
+          RollingItem::WordSeparator,
+        ]
+      }
+    )
+  }
+
+  #[test]
+  fn test_match_from_string_case_insensitive() {
+    assert_eq!(
+      RollingMatch::from_string(
+        1,
+        "test",
+        &StringMatchOptions {
+          case_insensitive: true,
+          ..Default::default()
+        }
+      ),
+      RollingMatch {
+        id: 1,
+        items: vec![
+          RollingItem::CharInsensitive("t".to_string()),
+          RollingItem::CharInsensitive("e".to_string()),
+          RollingItem::CharInsensitive("s".to_string()),
+          RollingItem::CharInsensitive("t".to_string()),
+        ]
+      }
+    )
+  }
+}
diff --git a/espanso-match/src/rolling/tree.rs b/espanso-match/src/rolling/tree.rs
new file mode 100644
index 00000000..9b9c1389
--- /dev/null
+++ b/espanso-match/src/rolling/tree.rs
@@ -0,0 +1,356 @@
+/*
+ * 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 unicase::UniCase;
+
+use crate::event::Key;
+
+use super::{RollingItem, RollingMatch};
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum MatcherTreeRef<Id> {
+  Matches(Vec<Id>),
+  Node(Box<MatcherTreeNode<Id>>),
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct MatcherTreeNode<Id> {
+  pub word_separators: Option<MatcherTreeRef<Id>>,
+  pub keys: Vec<(Key, MatcherTreeRef<Id>)>,
+  pub chars: Vec<(String, MatcherTreeRef<Id>)>,
+  pub chars_insensitive: Vec<(UniCase<String>, MatcherTreeRef<Id>)>,
+}
+
+impl<Id> Default for MatcherTreeNode<Id> {
+  fn default() -> Self {
+    Self {
+      word_separators: None,
+      keys: Vec::new(),
+      chars: Vec::new(),
+      chars_insensitive: Vec::new(),
+    }
+  }
+}
+
+impl<Id> MatcherTreeNode<Id>
+where
+  Id: Clone,
+{
+  pub fn from_matches(matches: &[RollingMatch<Id>]) -> MatcherTreeNode<Id> {
+    let mut root = MatcherTreeNode::default();
+    for m in matches {
+      insert_items_recursively(m.id.clone(), &mut root, &m.items);
+    }
+    root
+  }
+}
+
+fn insert_items_recursively<Id>(id: Id, node: &mut MatcherTreeNode<Id>, items: &[RollingItem]) {
+  if items.is_empty() {
+    return;
+  }
+
+  let item = items.get(0).unwrap();
+  if items.len() == 1 {
+    match item {
+      RollingItem::WordSeparator => {
+        let mut new_matches = Vec::new();
+        if let Some(MatcherTreeRef::Matches(matches)) = node.word_separators.take() {
+          new_matches.extend(matches);
+        }
+        new_matches.push(id);
+        node.word_separators = Some(MatcherTreeRef::Matches(new_matches))
+      }
+      RollingItem::Key(key) => {
+        if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) {
+          if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
+            matches.push(id)
+          } else {
+            entry.1 = MatcherTreeRef::Matches(vec![id])
+          };
+        } else {
+          node
+            .keys
+            .push((key.clone(), MatcherTreeRef::Matches(vec![id])));
+        }
+      }
+      RollingItem::Char(c) => {
+        if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) {
+          if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
+            matches.push(id)
+          } else {
+            entry.1 = MatcherTreeRef::Matches(vec![id])
+          };
+        } else {
+          node
+            .chars
+            .push((c.clone(), MatcherTreeRef::Matches(vec![id])));
+        }
+      }
+      RollingItem::CharInsensitive(c) => {
+        let uni_char = UniCase::new(c.clone());
+        if let Some(entry) = node
+          .chars_insensitive
+          .iter_mut()
+          .find(|(_c, _)| _c == &uni_char)
+        {
+          if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
+            matches.push(id)
+          } else {
+            entry.1 = MatcherTreeRef::Matches(vec![id])
+          };
+        } else {
+          node
+            .chars_insensitive
+            .push((uni_char, MatcherTreeRef::Matches(vec![id])));
+        }
+      }
+    }
+  } else {
+    match item {
+      RollingItem::WordSeparator => match node.word_separators.as_mut() {
+        Some(MatcherTreeRef::Node(next_node)) => {
+          insert_items_recursively(id, next_node.as_mut(), &items[1..])
+        }
+        None => {
+          let mut next_node = Box::new(MatcherTreeNode::default());
+          insert_items_recursively(id, next_node.as_mut(), &items[1..]);
+          node.word_separators = Some(MatcherTreeRef::Node(next_node));
+        }
+        _ => {}
+      },
+      RollingItem::Key(key) => {
+        if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) {
+          if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
+            insert_items_recursively(id, next_node, &items[1..])
+          }
+        } else {
+          let mut next_node = Box::new(MatcherTreeNode::default());
+          insert_items_recursively(id, next_node.as_mut(), &items[1..]);
+          node
+            .keys
+            .push((key.clone(), MatcherTreeRef::Node(next_node)));
+        }
+      }
+      RollingItem::Char(c) => {
+        if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) {
+          if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
+            insert_items_recursively(id, next_node, &items[1..])
+          }
+        } else {
+          let mut next_node = Box::new(MatcherTreeNode::default());
+          insert_items_recursively(id, next_node.as_mut(), &items[1..]);
+          node
+            .chars
+            .push((c.clone(), MatcherTreeRef::Node(next_node)));
+        }
+      }
+      RollingItem::CharInsensitive(c) => {
+        let uni_char = UniCase::new(c.clone());
+        if let Some(entry) = node
+          .chars_insensitive
+          .iter_mut()
+          .find(|(_c, _)| _c == &uni_char)
+        {
+          if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
+            insert_items_recursively(id, next_node, &items[1..])
+          }
+        } else {
+          let mut next_node = Box::new(MatcherTreeNode::default());
+          insert_items_recursively(id, next_node.as_mut(), &items[1..]);
+          node
+            .chars_insensitive
+            .push((uni_char, MatcherTreeRef::Node(next_node)));
+        }
+      }
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::rolling::StringMatchOptions;
+
+  #[test]
+  fn generate_tree_from_items_simple_strings() {
+    let root = MatcherTreeNode::from_matches(&[
+      RollingMatch::from_string(1, "hi", &StringMatchOptions::default()),
+      RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
+      RollingMatch::from_string(3, "my", &StringMatchOptions::default()),
+      RollingMatch::from_string(4, "myself", &StringMatchOptions::default()),
+    ]);
+
+    assert_eq!(
+      root,
+      MatcherTreeNode {
+        chars: vec![
+          (
+            "h".to_string(),
+            MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+              chars: vec![
+                ("i".to_string(), MatcherTreeRef::Matches(vec![1])),
+                (
+                  "e".to_string(),
+                  MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+                    chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2]))],
+                    ..Default::default()
+                  }))
+                ),
+              ],
+              ..Default::default()
+            }))
+          ),
+          (
+            "m".to_string(),
+            MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+              chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![3])),],
+              ..Default::default()
+            }))
+          )
+        ],
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn generate_tree_from_items_keys() {
+    let root = MatcherTreeNode::from_matches(&[
+      RollingMatch::from_items(
+        1,
+        &[
+          RollingItem::Key(Key::ArrowUp),
+          RollingItem::Key(Key::ArrowLeft),
+        ],
+      ),
+      RollingMatch::from_items(
+        2,
+        &[
+          RollingItem::Key(Key::ArrowUp),
+          RollingItem::Key(Key::ArrowRight),
+        ],
+      ),
+    ]);
+
+    assert_eq!(
+      root,
+      MatcherTreeNode {
+        keys: vec![(
+          Key::ArrowUp,
+          MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+            keys: vec![
+              (Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])),
+              (Key::ArrowRight, MatcherTreeRef::Matches(vec![2])),
+            ],
+            ..Default::default()
+          }))
+        ),],
+        ..Default::default()
+      }
+    )
+  }
+
+  #[test]
+  fn generate_tree_from_items_mixed() {
+    let root = MatcherTreeNode::from_matches(&[
+      RollingMatch::from_items(
+        1,
+        &[
+          RollingItem::Key(Key::ArrowUp),
+          RollingItem::Key(Key::ArrowLeft),
+        ],
+      ),
+      RollingMatch::from_string(
+        2,
+        "my",
+        &StringMatchOptions {
+          left_word: true,
+          ..Default::default()
+        },
+      ),
+      RollingMatch::from_string(
+        3,
+        "hi",
+        &StringMatchOptions {
+          left_word: true,
+          right_word: true,
+          ..Default::default()
+        },
+      ),
+      RollingMatch::from_string(
+        4,
+        "no",
+        &StringMatchOptions {
+          case_insensitive: true,
+          ..Default::default()
+        },
+      ),
+    ]);
+
+    assert_eq!(
+      root,
+      MatcherTreeNode {
+        keys: vec![(
+          Key::ArrowUp,
+          MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+            keys: vec![(Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])),],
+            ..Default::default()
+          }))
+        ),],
+        word_separators: Some(MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+          chars: vec![
+            (
+              "m".to_string(),
+              MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+                chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2])),],
+                ..Default::default()
+              }))
+            ),
+            (
+              "h".to_string(),
+              MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+                chars: vec![(
+                  "i".to_string(),
+                  MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+                    word_separators: Some(MatcherTreeRef::Matches(vec![3])),
+                    ..Default::default()
+                  }))
+                ),],
+                ..Default::default()
+              }))
+            ),
+          ],
+          ..Default::default()
+        }))),
+        chars_insensitive: vec![(
+          UniCase::new("n".to_string()),
+          MatcherTreeRef::Node(Box::new(MatcherTreeNode {
+            chars_insensitive: vec![(
+              UniCase::new("o".to_string()),
+              MatcherTreeRef::Matches(vec![4])
+            ),],
+            ..Default::default()
+          }))
+        ),],
+        ..Default::default()
+      }
+    )
+  }
+}
diff --git a/espanso-match/src/rolling/util.rs b/espanso-match/src/rolling/util.rs
new file mode 100644
index 00000000..d137c353
--- /dev/null
+++ b/espanso-match/src/rolling/util.rs
@@ -0,0 +1,223 @@
+/*
+ * 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 crate::Event;
+
+use super::matcher::IsWordSeparator;
+
+pub(crate) fn extract_string_from_events(
+  events: &[(Event, IsWordSeparator)],
+) -> (String, Option<String>, Option<String>) {
+  let mut string = String::new();
+
+  let mut left_separator = None;
+  let mut right_separator = None;
+
+  for (i, (event, is_word_separator)) in events.iter().enumerate() {
+    if let Event::Key {
+      key: _,
+      chars: Some(chars),
+    } = event
+    {
+      string.push_str(chars);
+
+      if *is_word_separator {
+        if i == 0 {
+          left_separator = Some(chars.clone());
+        } else if i == (events.len() - 1) {
+          right_separator = Some(chars.clone());
+        }
+      }
+    }
+  }
+
+  (string, left_separator, right_separator)
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::event::Key;
+
+  #[test]
+  fn extract_string_from_events_all_chars() {
+    assert_eq!(
+      extract_string_from_events(&[
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("h".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("e".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("l".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("l".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("o".to_string())
+          },
+          false
+        ),
+      ]),
+      ("hello".to_string(), None, None)
+    );
+  }
+
+  #[test]
+  fn extract_string_from_events_word_separators() {
+    assert_eq!(
+      extract_string_from_events(&[
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some(".".to_string())
+          },
+          true
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("h".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("i".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some(",".to_string())
+          },
+          true
+        ),
+      ]),
+      (
+        ".hi,".to_string(),
+        Some(".".to_string()),
+        Some(",".to_string())
+      ),
+    );
+  }
+
+  #[test]
+  fn extract_string_from_events_no_chars() {
+    assert_eq!(
+      extract_string_from_events(&[
+        (
+          Event::Key {
+            key: Key::ArrowUp,
+            chars: None
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::ArrowUp,
+            chars: None
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::ArrowUp,
+            chars: None
+          },
+          false
+        ),
+      ]),
+      ("".to_string(), None, None)
+    );
+  }
+
+  #[test]
+  fn extract_string_from_events_mixed() {
+    assert_eq!(
+      extract_string_from_events(&[
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("h".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("e".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("l".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("l".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::Other,
+            chars: Some("o".to_string())
+          },
+          false
+        ),
+        (
+          Event::Key {
+            key: Key::ArrowUp,
+            chars: None
+          },
+          false
+        ),
+      ]),
+      ("hello".to_string(), None, None)
+    );
+  }
+}
diff --git a/espanso-match/src/util.rs b/espanso-match/src/util.rs
new file mode 100644
index 00000000..be9e62df
--- /dev/null
+++ b/espanso-match/src/util.rs
@@ -0,0 +1,49 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(test)]
+pub(crate) mod tests {
+  use crate::{
+    event::{Event, Key},
+    MatchResult, Matcher,
+  };
+
+  pub(crate) fn get_matches_after_str<'a, Id: Clone, S, M: Matcher<'a, S, Id>>(
+    string: &str,
+    matcher: &'a M,
+  ) -> Vec<MatchResult<Id>> {
+    let mut prev_state = None;
+    let mut matches = Vec::new();
+
+    for c in string.chars() {
+      let (state, _matches) = matcher.process(
+        prev_state.as_ref(),
+        Event::Key {
+          key: Key::Other,
+          chars: Some(c.to_string()),
+        },
+      );
+
+      prev_state = Some(state);
+      matches = _matches;
+    }
+
+    matches
+  }
+}
diff --git a/espanso-migrate/Cargo.toml b/espanso-migrate/Cargo.toml
new file mode 100644
index 00000000..16aaab82
--- /dev/null
+++ b/espanso-migrate/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "espanso-migrate"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+glob = "0.3.0"
+regex = "1.4.3"
+lazy_static = "1.4.0"
+dunce = "1.0.1"
+walkdir = "2.3.1"
+yaml-rust = { git = "https://github.com/federico-terzi/yaml-rust" }
+path-slash = "0.1.4"
+tempdir = "0.3.7"
+fs_extra = "1.2.0"
+
+[dev-dependencies]
+tempfile = "3.2.0"
+include_dir = { version = "0.6.0", features = ["search"] }
+test-case = "1.1.0"
+pretty_assertions = "0.7.2"
\ No newline at end of file
diff --git a/espanso-migrate/src/convert.rs b/espanso-migrate/src/convert.rs
new file mode 100644
index 00000000..9fd86131
--- /dev/null
+++ b/espanso-migrate/src/convert.rs
@@ -0,0 +1,326 @@
+/*
+ * 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::{cmp::Ordering, collections::HashMap, path::PathBuf};
+use yaml_rust::{yaml::Hash, Yaml};
+
+pub struct ConvertedFile {
+  pub origin: String,
+  pub content: Hash,
+}
+
+pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, ConvertedFile> {
+  let mut output_files = HashMap::new();
+
+  let sorted_input_files = sort_input_files(&input_files);
+
+  for input_path in sorted_input_files {
+    let yaml = input_files
+      .get(&input_path)
+      .expect("received unexpected file in input function");
+
+    let yaml_matches = yaml_get_vec(yaml, "matches");
+    let yaml_global_vars = yaml_get_vec(yaml, "global_vars");
+
+    let yaml_parent = yaml_get_string(yaml, "parent");
+
+    if let Some(parent) = yaml_parent {
+      if parent != "default" {
+        eprintln!(
+          "WARNING: nested 'parent' instructions are not currently supported by the migration tool"
+        );
+      }
+    }
+
+    let should_generate_match = yaml_matches.is_some() || yaml_global_vars.is_some();
+    let match_file_path_if_unlisted = if should_generate_match {
+      let should_underscore = !input_path.starts_with("default") && yaml_parent != Some("default");
+      let match_output_path = calculate_output_match_path(&input_path, should_underscore);
+      if match_output_path.is_none() {
+        eprintln!(
+          "unable to determine output path for {}, skipping...",
+          input_path
+        );
+        continue;
+      }
+      let match_output_path = match_output_path.unwrap();
+
+      let output_yaml = output_files
+        .entry(match_output_path.clone())
+        .or_insert(ConvertedFile {
+          origin: input_path.to_string(),
+          content: Hash::new(),
+        });
+
+      if let Some(global_vars) = yaml_global_vars {
+        let output_global_vars = output_yaml
+          .content
+          .entry(Yaml::String("global_vars".to_string()))
+          .or_insert(Yaml::Array(Vec::new()));
+        if let Yaml::Array(out_global_vars) = output_global_vars {
+          out_global_vars.extend(global_vars.clone());
+        } else {
+          eprintln!("unable to transform global_vars for file: {}", input_path);
+        }
+      }
+
+      if let Some(matches) = yaml_matches {
+        let output_matches = output_yaml
+          .content
+          .entry(Yaml::String("matches".to_string()))
+          .or_insert(Yaml::Array(Vec::new()));
+        if let Yaml::Array(out_matches) = output_matches {
+          out_matches.extend(matches.clone());
+        } else {
+          eprintln!("unable to transform matches for file: {}", input_path);
+        }
+      }
+
+      if should_underscore {
+        Some(match_output_path)
+      } else {
+        None
+      }
+    } else {
+      None
+    };
+
+    let yaml_filter_class = yaml_get_string(yaml, "filter_class");
+    let yaml_filter_title = yaml_get_string(yaml, "filter_title");
+    let yaml_filter_exec = yaml_get_string(yaml, "filter_exec");
+
+    let should_generate_config = input_path.starts_with("default")
+      || yaml_filter_class.is_some()
+      || yaml_filter_exec.is_some()
+      || yaml_filter_title.is_some();
+
+    if should_generate_config {
+      let config_output_path = calculate_output_config_path(&input_path);
+
+      let mut output_yaml = Hash::new();
+
+      copy_field_if_present(yaml, "filter_title", &mut output_yaml, "filter_title");
+      copy_field_if_present(yaml, "filter_class", &mut output_yaml, "filter_class");
+      copy_field_if_present(yaml, "filter_exec", &mut output_yaml, "filter_exec");
+      copy_field_if_present(yaml, "enable_active", &mut output_yaml, "enable");
+      copy_field_if_present(yaml, "backend", &mut output_yaml, "backend");
+      map_field_if_present(
+        yaml,
+        "paste_shortcut",
+        &mut output_yaml,
+        "paste_shortcut",
+        |val| match val {
+          Yaml::String(shortcut) if shortcut == "CtrlV" => Some(Yaml::String("CTRL+V".to_string())),
+          Yaml::String(shortcut) if shortcut == "CtrlShiftV" => {
+            Some(Yaml::String("CTRL+SHIFT+V".to_string()))
+          }
+          Yaml::String(shortcut) if shortcut == "ShiftInsert" => {
+            Some(Yaml::String("SHIFT+INSERT".to_string()))
+          }
+          Yaml::String(shortcut) if shortcut == "CtrlAltV" => {
+            Some(Yaml::String("CTRL+ALT+V".to_string()))
+          }
+          Yaml::String(shortcut) if shortcut == "MetaV" => Some(Yaml::String("META+V".to_string())),
+          Yaml::String(_) => None,
+          _ => None,
+        },
+      );
+      //copy_field_if_present(yaml, "secure_input_watcher_enabled", &mut output_yaml, "secure_input_watcher_enabled");
+      //copy_field_if_present(yaml, "secure_input_watcher_interval", &mut output_yaml, "secure_input_watcher_interval");
+      //copy_field_if_present(yaml, "config_caching_interval", &mut output_yaml, "config_caching_interval");
+      //copy_field_if_present(yaml, "use_system_agent", &mut output_yaml, "use_system_agent");
+
+      copy_field_if_present(
+        yaml,
+        "secure_input_notification",
+        &mut output_yaml,
+        "secure_input_notification",
+      );
+      copy_field_if_present(yaml, "toggle_interval", &mut output_yaml, "toggle_interval");
+      copy_field_if_present(yaml, "toggle_key", &mut output_yaml, "toggle_key");
+      copy_field_if_present(
+        yaml,
+        "preserve_clipboard",
+        &mut output_yaml,
+        "preserve_clipboard",
+      );
+      copy_field_if_present(yaml, "backspace_limit", &mut output_yaml, "backspace_limit");
+      map_field_if_present(
+        yaml,
+        "fast_inject",
+        &mut output_yaml,
+        "disable_x11_fast_inject",
+        |val| match val {
+          Yaml::Boolean(false) => Some(Yaml::Boolean(true)),
+          Yaml::Boolean(true) => Some(Yaml::Boolean(false)),
+          _ => None,
+        },
+      );
+
+      copy_field_if_present(yaml, "auto_restart", &mut output_yaml, "auto_restart");
+      copy_field_if_present(yaml, "undo_backspace", &mut output_yaml, "undo_backspace");
+      copy_field_if_present(yaml, "show_icon", &mut output_yaml, "show_icon");
+      copy_field_if_present(
+        yaml,
+        "show_notifications",
+        &mut output_yaml,
+        "show_notifications",
+      );
+      copy_field_if_present(yaml, "inject_delay", &mut output_yaml, "inject_delay");
+      copy_field_if_present(
+        yaml,
+        "restore_clipboard_delay",
+        &mut output_yaml,
+        "restore_clipboard_delay",
+      );
+      copy_field_if_present(yaml, "backspace_delay", &mut output_yaml, "key_delay");
+      copy_field_if_present(yaml, "word_separators", &mut output_yaml, "word_separators");
+
+      if yaml
+        .get(&Yaml::String("enable_passive".to_string()))
+        .is_some()
+      {
+        eprintln!("WARNING: passive-mode directives were detected, but passive-mode is not supported anymore.");
+        eprintln!("Please follow this issue to discover the alternatives: https://github.com/federico-terzi/espanso/issues/540");
+      }
+
+      // Link any unlisted match file (the ones starting with the _ underscore, which are excluded by the
+      // default.yml config) explicitly, if present.
+      if let Some(match_file_path) = match_file_path_if_unlisted {
+        let yaml_exclude_default_entries =
+          yaml_get_bool(yaml, "exclude_default_entries").unwrap_or(false);
+        let key_name = if yaml_exclude_default_entries {
+          "includes"
+        } else {
+          "extra_includes"
+        };
+
+        let includes = vec![Yaml::String(format!("../{}", match_file_path))];
+
+        output_yaml.insert(Yaml::String(key_name.to_string()), Yaml::Array(includes));
+      }
+
+      output_files.insert(
+        config_output_path,
+        ConvertedFile {
+          origin: input_path,
+          content: output_yaml,
+        },
+      );
+    }
+  }
+
+  output_files
+}
+
+fn sort_input_files(input_files: &HashMap<String, Hash>) -> Vec<String> {
+  let mut files: Vec<String> = input_files.iter().map(|(key, _)| key.clone()).collect();
+  files.sort_by(|f1, f2| {
+    let f1_slashes = f1.matches('/').count();
+    let f2_slashes = f2.matches('/').count();
+    #[allow(clippy::comparison_chain)]
+    if f1_slashes > f2_slashes {
+      Ordering::Greater
+    } else if f1_slashes < f2_slashes {
+      Ordering::Less
+    } else {
+      f1.cmp(f2)
+    }
+  });
+  files
+}
+
+// TODO: test
+fn calculate_output_match_path(path: &str, is_underscored: bool) -> Option<String> {
+  let path_buf = PathBuf::from(path);
+  let file_name = path_buf.file_name()?.to_string_lossy().to_string();
+
+  let path = if is_underscored {
+    path.replace(&file_name, &format!("_{}", file_name))
+  } else {
+    path.to_string()
+  };
+
+  Some(if path.starts_with("user/") {
+    format!("match/{}", path.trim_start_matches("user/"))
+  } else if path.starts_with("packages/") {
+    format!("match/packages/{}", path.trim_start_matches("packages/"))
+  } else if path == "default.yml" {
+    "match/base.yml".to_string()
+  } else {
+    format!("match/{}", path)
+  })
+}
+
+// TODO: test
+fn calculate_output_config_path(path: &str) -> String {
+  if path.starts_with("user/") {
+    format!("config/{}", path.trim_start_matches("user/"))
+  } else if path.starts_with("packages/") {
+    format!("config/packages/{}", path.trim_start_matches("packages/"))
+  } else {
+    format!("config/{}", path)
+  }
+}
+
+fn yaml_get_vec<'a>(yaml: &'a Hash, name: &str) -> Option<&'a Vec<Yaml>> {
+  yaml
+    .get(&Yaml::String(name.to_string()))
+    .and_then(|v| v.as_vec())
+}
+
+fn yaml_get_string<'a>(yaml: &'a Hash, name: &str) -> Option<&'a str> {
+  yaml
+    .get(&Yaml::String(name.to_string()))
+    .and_then(|v| v.as_str())
+}
+
+fn yaml_get_bool(yaml: &Hash, name: &str) -> Option<bool> {
+  yaml
+    .get(&Yaml::String(name.to_string()))
+    .and_then(|v| v.as_bool())
+}
+
+fn copy_field_if_present(
+  input_yaml: &Hash,
+  input_field_name: &str,
+  output_yaml: &mut Hash,
+  output_field_name: &str,
+) {
+  if let Some(value) = input_yaml.get(&Yaml::String(input_field_name.to_string())) {
+    output_yaml.insert(Yaml::String(output_field_name.to_string()), value.clone());
+  }
+}
+
+fn map_field_if_present(
+  input_yaml: &Hash,
+  input_field_name: &str,
+  output_yaml: &mut Hash,
+  output_field_name: &str,
+  transform: impl FnOnce(&Yaml) -> Option<Yaml>,
+) {
+  if let Some(value) = input_yaml.get(&Yaml::String(input_field_name.to_string())) {
+    let transformed = transform(value);
+    if let Some(transformed) = transformed {
+      output_yaml.insert(Yaml::String(output_field_name.to_string()), transformed);
+    } else {
+      eprintln!("could not convert value for field: {}", input_field_name);
+    }
+  }
+}
diff --git a/espanso-migrate/src/lib.rs b/espanso-migrate/src/lib.rs
new file mode 100644
index 00000000..1208053d
--- /dev/null
+++ b/espanso-migrate/src/lib.rs
@@ -0,0 +1,232 @@
+/*
+ * 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/>.
+ */
+
+#[allow(unused_imports)]
+#[macro_use]
+extern crate lazy_static;
+
+#[allow(unused_imports)]
+#[macro_use]
+#[cfg(test)]
+extern crate include_dir;
+
+#[allow(unused_imports)]
+#[macro_use]
+#[cfg(test)]
+extern crate test_case;
+
+use std::path::Path;
+
+use anyhow::Result;
+use fs_extra::dir::CopyOptions;
+use tempdir::TempDir;
+use thiserror::Error;
+
+mod convert;
+mod load;
+mod render;
+
+// TODO: implement
+// Use yaml-rust with "preserve-order" = true
+// Strategy:
+// 1. Backup the current config directory in a zip archive (also with the packages)
+// 2. Create a temporary directory alonside the legacy one called "espanso-new"
+// 3. Convert all the files and write the output into "espanso-new"
+// 4. Rename the legacy dir to "espanso-old"
+// 5. Rename new dir to "espanso"
+// 6. If the legacy directory was a symlink, try to recreate it (ask the user first)
+
+// TODO: before attempting the migration strategy, check if the current
+// espanso config directory is a symlink and, if so, attempt to remap
+// the symlink with the new dir (after asking the user)
+// This is necessary because in order to be safe, the migration strategy
+// creates the new config on a new temporary directory and then "swaps"
+// the old with the new one
+
+// TODO: test also with non-lowercase file names
+
+pub fn migrate(config_dir: &Path, packages_dir: &Path, output_dir: &Path) -> Result<()> {
+  if !config_dir.is_dir() {
+    return Err(MigrationError::InvalidConfigDir.into());
+  }
+
+  let working_dir = TempDir::new("espanso-migration")?;
+
+  fs_extra::dir::copy(
+    config_dir,
+    working_dir.path(),
+    &CopyOptions {
+      content_only: true,
+      ..Default::default()
+    },
+  )?;
+
+  // If packages are located within the config_dir, no need to copy them in
+  // the working directory
+  if packages_dir.parent() != Some(config_dir) {
+    fs_extra::dir::copy(packages_dir, working_dir.path(), &CopyOptions::new())?;
+  }
+
+  // Create the output directory
+  if output_dir.exists() {
+    return Err(MigrationError::OutputDirAlreadyPresent.into());
+  }
+
+  std::fs::create_dir_all(output_dir)?;
+
+  // Convert the configurations
+  let legacy_files = load::load(working_dir.path())?;
+  let converted_files = convert::convert(legacy_files);
+  let rendered_files = render::render(converted_files)?;
+
+  for (file, content) in rendered_files {
+    let target = output_dir.join(file);
+
+    if let Some(parent) = target.parent() {
+      if !parent.is_dir() {
+        std::fs::create_dir_all(parent)?;
+      }
+    }
+
+    std::fs::write(target, content)?;
+  }
+
+  // Copy all non-YAML directories
+  for entry in std::fs::read_dir(working_dir.path())? {
+    let entry = entry?;
+    let path = entry.path();
+    if path.is_dir() {
+      let dir_name = path.file_name();
+      if let Some(name) = dir_name.map(|s| s.to_string_lossy().to_string().to_lowercase()) {
+        if name != "user" && name != "packages" {
+          fs_extra::dir::copy(path, output_dir, &CopyOptions::new())?;
+        }
+      }
+    }
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum MigrationError {
+  #[error("invalid config directory")]
+  InvalidConfigDir,
+
+  #[error("output directory already present")]
+  OutputDirAlreadyPresent,
+}
+
+#[cfg(test)]
+mod tests {
+  use std::{collections::HashMap, fs::create_dir_all, path::Path};
+
+  use super::*;
+  use include_dir::{include_dir, Dir};
+  use tempdir::TempDir;
+  use test_case::test_case;
+
+  use pretty_assertions::assert_eq as assert_peq;
+  use yaml_rust::{yaml::Hash, Yaml};
+
+  fn run_with_temp_dir(test_data: &Dir, action: impl FnOnce(&Path, &Path)) {
+    let tmp_dir = TempDir::new("espanso-migrate").unwrap();
+    let tmp_path = tmp_dir.path();
+    let legacy_path = tmp_dir.path().join("legacy");
+    let expected_path = tmp_dir.path().join("expected");
+
+    for entry in test_data.find("**/*").unwrap() {
+      let entry_path = entry.path();
+
+      let entry_path_str = entry_path.to_string_lossy().to_string();
+      if entry_path_str.is_empty() {
+        continue;
+      }
+
+      let target = tmp_path.join(entry_path);
+
+      if entry_path.extension().is_none() {
+        create_dir_all(target).unwrap();
+      } else {
+        std::fs::write(target, test_data.get_file(entry_path).unwrap().contents()).unwrap();
+      }
+    }
+
+    action(&legacy_path, &expected_path);
+  }
+
+  fn to_sorted_list<T>(hash: HashMap<String, T>) -> Vec<(String, T)> {
+    let mut tuples: Vec<(String, T)> = hash.into_iter().map(|(k, v)| (k, v)).collect();
+    tuples.sort_by_key(|(k, _)| k.clone());
+    tuples
+  }
+
+  fn to_sorted_hash(hash: &Hash) -> Vec<(String, &Yaml)> {
+    let mut tuples: Vec<(String, &Yaml)> = hash
+      .into_iter()
+      .map(|(k, v)| (k.as_str().unwrap().to_string(), v))
+      .collect();
+    tuples.sort_by_key(|(k, _)| k.clone());
+    tuples
+  }
+
+  fn list_files_in_dir(dir: &Path) -> Vec<String> {
+    let prefix = dir.to_string_lossy().to_string();
+    fs_extra::dir::get_dir_content(&dir)
+      .unwrap()
+      .files
+      .into_iter()
+      .map(|file| file.trim_start_matches(&prefix).to_string())
+      .collect()
+  }
+
+  static SIMPLE_CASE: Dir = include_dir!("test/simple");
+  static BASE_CASE: Dir = include_dir!("test/base");
+  static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params");
+  static OTHER_DIRS_CASE: Dir = include_dir!("test/other_dirs");
+
+  #[allow(clippy::unused_unit)]
+  #[test_case(&SIMPLE_CASE; "simple case")]
+  #[test_case(&BASE_CASE; "base case")]
+  #[test_case(&ALL_PARAMS_CASE; "all config parameters case")]
+  #[test_case(&OTHER_DIRS_CASE; "other directories case")]
+  fn test_migration(test_data: &Dir) {
+    run_with_temp_dir(test_data, |legacy, expected| {
+      let tmp_out_dir = TempDir::new("espanso-migrate-out").unwrap();
+      let output_dir = tmp_out_dir.path().join("out");
+
+      migrate(legacy, &legacy.join("packages"), &output_dir).unwrap();
+
+      let converted_files = load::load(&output_dir).unwrap();
+
+      // Verify configuration content
+      let expected_files = load::load(expected).unwrap();
+      assert_eq!(converted_files.len(), expected_files.len());
+      for (file, converted) in to_sorted_list(converted_files) {
+        assert_peq!(
+          to_sorted_hash(&converted),
+          to_sorted_hash(expected_files.get(&file).unwrap())
+        );
+      }
+
+      // Verify file structure
+      assert_peq!(list_files_in_dir(expected), list_files_in_dir(&output_dir));
+    });
+  }
+}
diff --git a/espanso-migrate/src/load.rs b/espanso-migrate/src/load.rs
new file mode 100644
index 00000000..d288a4fc
--- /dev/null
+++ b/espanso-migrate/src/load.rs
@@ -0,0 +1,127 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use path_slash::PathExt;
+use std::{collections::HashMap, path::Path};
+use thiserror::Error;
+use walkdir::WalkDir;
+use yaml_rust::{yaml::Hash, Yaml, YamlLoader};
+
+pub fn load(config_dir: &Path) -> Result<HashMap<String, Hash>> {
+  if !config_dir.is_dir() {
+    return Err(LoadError::NotDirectory.into());
+  }
+
+  let mut input_files = HashMap::new();
+
+  for entry in WalkDir::new(config_dir) {
+    match entry {
+      Ok(entry) => {
+        // Skip directories
+        if entry.path().is_dir() {
+          continue;
+        }
+
+        // Skip non-yaml files
+        let extension = entry
+          .path()
+          .extension()
+          .map(|s| s.to_string_lossy().to_ascii_lowercase())
+          .unwrap_or_default();
+
+        if extension != "yaml" && extension != "yml" {
+          continue;
+        }
+
+        match entry.path().strip_prefix(config_dir) {
+          Ok(relative_path) => {
+            let corrected_path = relative_path.to_slash_lossy();
+
+            if corrected_path.is_empty() {
+              continue;
+            }
+
+            match std::fs::read_to_string(entry.path()) {
+              Ok(content) => {
+                // Empty files are not valid YAML, but we want to handle them anyway
+                if content.trim().is_empty() {
+                  input_files.insert(corrected_path, Hash::new());
+                } else {
+                  match YamlLoader::load_from_str(&content) {
+                    Ok(mut yaml) => {
+                      if !yaml.is_empty() {
+                        let yaml = yaml.remove(0);
+                        if let Yaml::Hash(hash) = yaml {
+                          input_files.insert(corrected_path, hash);
+                        } else {
+                          eprintln!(
+                            "yaml file does not have a valid format: {}",
+                            entry.path().display()
+                          );
+                        }
+                      } else {
+                        eprintln!(
+                          "error, found empty document while reading entry: {}",
+                          entry.path().display()
+                        );
+                      }
+                    }
+                    Err(err) => {
+                      eprintln!(
+                        "experienced error while parsing file: {}, error: {}",
+                        entry.path().display(),
+                        err
+                      );
+                    }
+                  }
+                }
+              }
+              Err(err) => {
+                eprintln!(
+                  "error while reading entry: {}, error: {}",
+                  entry.path().display(),
+                  err
+                );
+              }
+            }
+          }
+          Err(err) => {
+            eprintln!(
+              "error while analyzing entry: {}, error: {}",
+              entry.path().display(),
+              err
+            );
+          }
+        }
+      }
+      Err(err) => {
+        eprintln!("experienced error while reading entry: {}", err)
+      }
+    }
+  }
+
+  Ok(input_files)
+}
+
+#[derive(Error, Debug)]
+pub enum LoadError {
+  #[error("the provided legacy_config_dir is not a directory")]
+  NotDirectory,
+}
diff --git a/espanso-migrate/src/render.rs b/espanso-migrate/src/render.rs
new file mode 100644
index 00000000..8d161fef
--- /dev/null
+++ b/espanso-migrate/src/render.rs
@@ -0,0 +1,54 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::collections::HashMap;
+use yaml_rust::{Yaml, YamlEmitter};
+
+use crate::convert::ConvertedFile;
+
+pub fn render(converted_files: HashMap<String, ConvertedFile>) -> Result<Vec<(String, String)>> {
+  let mut output = Vec::new();
+  for (file, content) in converted_files {
+    let rendered = render_file(content)?;
+    output.push((file, rendered));
+  }
+  Ok(output)
+}
+
+fn render_file(file: ConvertedFile) -> Result<String> {
+  let mut dump_str = String::new();
+  let mut emitter = YamlEmitter::new(&mut dump_str);
+  emitter.multiline_strings(true);
+  emitter.dump(&Yaml::Hash(file.content))?;
+
+  let lines: Vec<&str> = dump_str.lines().collect();
+  let header = format!("# Original file: {}", file.origin);
+  let mut output_lines: Vec<&str> = vec![
+    "# Automatically generated by espanso migration tool",
+    &header,
+    "",
+  ];
+  if !lines.is_empty() {
+    output_lines.extend(&lines[1..]);
+  }
+
+  let output = output_lines.join("\n");
+  Ok(output)
+}
diff --git a/espanso-modulo/Cargo.toml b/espanso-modulo/Cargo.toml
new file mode 100644
index 00000000..a6f3ea5d
--- /dev/null
+++ b/espanso-modulo/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "espanso-modulo"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[dependencies]
+log = "0.4.14"
+serde_json = "1.0.61"
+serde = { version = "1.0.123", features = ["derive"] }
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+lazy_static = "1.4.0"
+regex = "1.4.3"
+
+[build-dependencies]
+cc = "1.0.66"
+regex = "1.4.3"
+zip = "0.5.12"
+winres = "0.1.11"
+glob = "0.3.0"
\ No newline at end of file
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
new file mode 100644
index 00000000..86bc21d7
--- /dev/null
+++ b/espanso-modulo/build.rs
@@ -0,0 +1,461 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 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::path::PathBuf;
+
+#[cfg(not(target_os = "windows"))]
+use std::path::Path;
+
+#[cfg(not(target_os = "linux"))]
+const WX_WIDGETS_ARCHIVE_NAME: &str = "wxWidgets-3.1.5.zip";
+
+#[cfg(target_os = "windows")]
+fn build_native() {
+  use std::process::Command;
+
+  let project_dir =
+    PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
+  let wx_archive = project_dir.join("vendor").join(WX_WIDGETS_ARCHIVE_NAME);
+  if !wx_archive.is_file() {
+    panic!("could not find wxWidgets archive!");
+  }
+
+  let out_dir = 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() {
+    // Extract the wxWidgets archive
+    let wx_archive =
+      std::fs::File::open(&wx_archive).expect("unable to open wxWidgets source archive");
+    let mut archive = zip::ZipArchive::new(wx_archive).expect("unable to read wxWidgets archive");
+    archive
+      .extract(&out_wx_dir)
+      .expect("unable to extract wxWidgets source dir");
+
+    // Compile wxWidgets
+    let tool = cc::windows_registry::find_tool("msvc", "msbuild")
+      .expect("unable to locate MSVC compiler, did you install Visual Studio?");
+    let mut vcvars_path = None;
+    let mut current_root = tool.path();
+    while let Some(parent) = current_root.parent() {
+      let target = parent
+        .join("VC")
+        .join("Auxiliary")
+        .join("Build")
+        .join("vcvars64.bat");
+      if target.exists() {
+        vcvars_path = Some(target);
+        break;
+      }
+      current_root = parent;
+    }
+
+    let vcvars_path = vcvars_path.expect("unable to find vcvars64.bat file");
+    let mut handle = Command::new("cmd")
+      .current_dir(
+        out_wx_dir
+          .join("build")
+          .join("msw")
+          .to_string_lossy()
+          .to_string(),
+      )
+      .args(&[
+        "/k",
+        &vcvars_path.to_string_lossy().to_string(),
+        "&",
+        "nmake",
+        "/f",
+        "makefile.vc",
+        "BUILD=release",
+        "TARGET_CPU=X64",
+        "&",
+        "exit",
+      ])
+      .spawn()
+      .expect("failed to execute nmake");
+    if !handle
+      .wait()
+      .expect("unable to wait for nmake command")
+      .success()
+    {
+      panic!("nmake returned non-zero exit code!");
+    }
+  }
+
+  // Make sure wxWidgets is compiled
+  if !out_wx_dir
+    .join("build")
+    .join("msw")
+    .join("vc_mswu_x64")
+    .is_dir()
+  {
+    panic!("wxWidgets is not compiled correctly, missing 'build/msw/vc_mswu_x64' directory")
+  }
+
+  let wx_include_dir = out_wx_dir.join("include");
+  let wx_include_msvc_dir = wx_include_dir.join("msvc");
+  let wx_lib_dir = out_wx_dir.join("lib").join("vc_x64_lib");
+
+  cc::Build::new()
+    .cpp(true)
+    .file("src/sys/form/form.cpp")
+    .file("src/sys/search/search.cpp")
+    .file("src/sys/common/common.cpp")
+    .file("src/sys/wizard/wizard.cpp")
+    .file("src/sys/wizard/wizard_gui.cpp")
+    .file("src/sys/welcome/welcome.cpp")
+    .file("src/sys/welcome/welcome_gui.cpp")
+    .file("src/sys/troubleshooting/troubleshooting.cpp")
+    .file("src/sys/troubleshooting/troubleshooting_gui.cpp")
+    .flag("/EHsc")
+    .include(wx_include_dir)
+    .include(wx_include_msvc_dir)
+    .compile("espansomodulosys");
+
+  // Add resources (manifest)
+  let mut resources = winres::WindowsResource::new();
+  resources.set_manifest(include_str!("res/win.manifest"));
+  resources
+    .compile()
+    .expect("unable to compile resource file");
+
+  println!(
+    "cargo:rustc-link-search=native={}",
+    wx_lib_dir.to_string_lossy()
+  );
+}
+
+#[cfg(target_os = "macos")]
+fn build_native() {
+  use std::process::Command;
+
+  let project_dir =
+    PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
+  let wx_archive = project_dir.join("vendor").join(WX_WIDGETS_ARCHIVE_NAME);
+  if !wx_archive.is_file() {
+    panic!("could not find wxWidgets archive!");
+  }
+
+  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
+  let out_wx_dir = out_dir.join("wx");
+
+  let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
+    .expect("unable to read target arch")
+    .as_str()
+  {
+    "x86_64" => "x86_64",
+    "aarch64" => "arm64",
+    arch => panic!("unsupported arch {}", arch),
+  };
+
+  let should_use_ci_m1_workaround =
+    std::env::var("CI").unwrap_or_default() == "true" && target_arch == "arm64";
+
+  if !out_wx_dir.is_dir() {
+    // Extract the wxWidgets archive
+    let wx_archive =
+      std::fs::File::open(&wx_archive).expect("unable to open wxWidgets source archive");
+    let mut archive = zip::ZipArchive::new(wx_archive).expect("unable to read wxWidgets archive");
+    archive
+      .extract(&out_wx_dir)
+      .expect("unable to extract wxWidgets source dir");
+
+    // Compile wxWidgets
+    let build_dir = out_wx_dir.join("build-cocoa");
+    std::fs::create_dir_all(&build_dir).expect("unable to create build-cocoa directory");
+
+    let mut handle = if should_use_ci_m1_workaround {
+      // Because of a configuration problem on the GitHub CI pipeline,
+      // we need to use a series of workarounds to build for M1 machines.
+      // See: https://github.com/actions/virtual-environments/issues/3288#issuecomment-830207746
+      Command::new(out_wx_dir.join("configure"))
+        .current_dir(build_dir.to_string_lossy().to_string())
+        .args(&[
+          "--disable-shared",
+          "--without-libtiff",
+          "--without-liblzma",
+          "--with-libjpeg=builtin",
+          "--with-libpng=builtin",
+          "--enable-universal-binary=arm64,x86_64",
+        ])
+        .spawn()
+        .expect("failed to execute configure")
+    } else {
+      Command::new(out_wx_dir.join("configure"))
+        .current_dir(build_dir.to_string_lossy().to_string())
+        .args(&[
+          "--disable-shared",
+          "--without-libtiff",
+          "--without-liblzma",
+          "--with-libjpeg=builtin",
+          "--with-libpng=builtin",
+          &format!("--enable-macosx_arch={}", target_arch),
+        ])
+        .spawn()
+        .expect("failed to execute configure")
+    };
+
+    if !handle
+      .wait()
+      .expect("unable to wait for configure command")
+      .success()
+    {
+      panic!("configure returned non-zero exit code!");
+    }
+
+    let mut handle = Command::new("make")
+      .current_dir(build_dir.to_string_lossy().to_string())
+      .args(&["-j8"])
+      .spawn()
+      .expect("failed to execute make");
+    if !handle
+      .wait()
+      .expect("unable to wait for make command")
+      .success()
+    {
+      panic!("make returned non-zero exit code!");
+    }
+  }
+
+  // Make sure wxWidgets is compiled
+  if !out_wx_dir.join("build-cocoa").is_dir() {
+    panic!("wxWidgets is not compiled correctly, missing 'build-cocoa/' directory")
+  }
+
+  // If using the M1 CI workaround, convert all the universal libraries to arm64 ones
+  // This is needed until https://github.com/rust-lang/rust/issues/55235 is fixed
+  if should_use_ci_m1_workaround {
+    convert_fat_libraries_to_arm(&out_wx_dir.join("build-cocoa").join("lib"));
+    convert_fat_libraries_to_arm(&out_wx_dir.join("build-cocoa"));
+  }
+
+  let config_path = out_wx_dir.join("build-cocoa").join("wx-config");
+  let cpp_flags = get_cpp_flags(&config_path);
+
+  let mut build = cc::Build::new();
+  build
+    .cpp(true)
+    .file("src/sys/form/form.cpp")
+    .file("src/sys/common/common.cpp")
+    .file("src/sys/search/search.cpp")
+    .file("src/sys/wizard/wizard.cpp")
+    .file("src/sys/wizard/wizard_gui.cpp")
+    .file("src/sys/welcome/welcome.cpp")
+    .file("src/sys/welcome/welcome_gui.cpp")
+    .file("src/sys/troubleshooting/troubleshooting.cpp")
+    .file("src/sys/troubleshooting/troubleshooting_gui.cpp")
+    .file("src/sys/common/mac.mm");
+  build.flag("-std=c++17");
+
+  for flag in cpp_flags {
+    build.flag(&flag);
+  }
+
+  build.compile("espansomodulosys");
+
+  // Render linker flags
+
+  generate_linker_flags(&config_path);
+
+  // On (older) OSX we need to link against the clang runtime,
+  // which is hidden in some non-default path.
+  //
+  // More details at https://github.com/alexcrichton/curl-rust/issues/279.
+  if let Some(path) = macos_link_search_path() {
+    println!("cargo:rustc-link-lib=clang_rt.osx");
+    println!("cargo:rustc-link-search={}", path);
+  }
+}
+
+#[cfg(target_os = "macos")]
+fn convert_fat_libraries_to_arm(lib_dir: &Path) {
+  for entry in
+    glob::glob(&format!("{}/*", lib_dir.to_string_lossy())).expect("failed to glob directory")
+  {
+    let path = entry.expect("unable to unwrap glob entry");
+
+    // Make sure it's a fat library
+    let lipo_output = std::process::Command::new("lipo")
+      .args(&["-detailed_info", &path.to_string_lossy().to_string()])
+      .output()
+      .expect("unable to check if library is fat");
+    let lipo_output = String::from_utf8_lossy(&lipo_output.stdout);
+    let lipo_output = lipo_output.trim();
+    if !lipo_output.contains("Fat header") {
+      println!(
+        "skipping {} as it's not a fat library",
+        path.to_string_lossy()
+      );
+      continue;
+    }
+
+    println!("converting {} to arm", path.to_string_lossy(),);
+
+    let result = std::process::Command::new("lipo")
+      .args(&[
+        "-thin",
+        "arm64",
+        &path.to_string_lossy().to_string(),
+        "-output",
+        &path.to_string_lossy().to_string(),
+      ])
+      .output()
+      .expect("unable to extract arm64 slice from library");
+
+    if !result.status.success() {
+      panic!("unable to convert fat library to arm64 version");
+    }
+  }
+}
+
+#[cfg(not(target_os = "windows"))]
+fn get_cpp_flags(wx_config_path: &Path) -> Vec<String> {
+  let config_output = std::process::Command::new(&wx_config_path)
+    .arg("--cxxflags")
+    .output()
+    .expect("unable to execute wx-config");
+  let config_libs =
+    String::from_utf8(config_output.stdout).expect("unable to parse wx-config output");
+  let cpp_flags: Vec<String> = config_libs
+    .split(' ')
+    .filter_map(|s| {
+      if !s.trim().is_empty() {
+        Some(s.trim().to_owned())
+      } else {
+        None
+      }
+    })
+    .collect();
+  cpp_flags
+}
+
+#[cfg(not(target_os = "windows"))]
+fn generate_linker_flags(wx_config_path: &Path) {
+  use regex::Regex;
+  let config_output = std::process::Command::new(&wx_config_path)
+    .arg("--libs")
+    .output()
+    .expect("unable to execute wx-config libs");
+  let config_libs =
+    String::from_utf8(config_output.stdout).expect("unable to parse wx-config libs output");
+  let linker_flags: Vec<String> = config_libs
+    .split(' ')
+    .filter_map(|s| {
+      if !s.trim().is_empty() {
+        Some(s.trim().to_owned())
+      } else {
+        None
+      }
+    })
+    .collect();
+
+  let static_lib_extract = Regex::new(r"lib/lib(.*)\.a").unwrap();
+
+  // Translate the flags generated by `wx-config` to commands
+  // that cargo can understand.
+  for (i, flag) in linker_flags.iter().enumerate() {
+    if flag.starts_with("-L") {
+      let path = flag.trim_start_matches("-L");
+      println!("cargo:rustc-link-search=native={}", path);
+    } else if flag.starts_with("-framework") {
+      println!("cargo:rustc-link-lib=framework={}", linker_flags[i + 1]);
+    } else if flag.starts_with('/') {
+      let captures = static_lib_extract
+        .captures(flag)
+        .expect("unable to capture flag regex");
+      let libname = captures.get(1).expect("unable to find static libname");
+      println!("cargo:rustc-link-lib=static={}", libname.as_str());
+    } else if flag.starts_with("-l") {
+      let libname = flag.trim_start_matches("-l");
+      println!("cargo:rustc-link-lib=dylib={}", libname);
+    }
+  }
+}
+
+// Taken from curl-rust: https://github.com/alexcrichton/curl-rust/pull/283/files
+#[cfg(target_os = "macos")]
+fn macos_link_search_path() -> Option<String> {
+  let output = std::process::Command::new("clang")
+    .arg("--print-search-dirs")
+    .output()
+    .ok()?;
+  if !output.status.success() {
+    println!("failed to run 'clang --print-search-dirs', continuing without a link search path");
+    return None;
+  }
+
+  let stdout = String::from_utf8_lossy(&output.stdout);
+  for line in stdout.lines() {
+    if line.contains("libraries: =") {
+      let path = line.split('=').nth(1)?;
+      return Some(format!("{}/lib/darwin", path));
+    }
+  }
+
+  println!("failed to determine link search path, continuing without it");
+  None
+}
+
+// TODO: add documentation for linux
+// Install wxWidgets:
+// sudo apt install libwxgtk3.0-0v5 libwxgtk3.0-dev
+//
+// cargo run
+#[cfg(target_os = "linux")]
+fn build_native() {
+  // Make sure wxWidgets is installed
+  if std::process::Command::new("wx-config")
+    .arg("--version")
+    .output()
+    .is_err()
+  {
+    panic!("wxWidgets is not installed, as `wx-config` cannot be exectued")
+  }
+
+  let config_path = PathBuf::from("wx-config");
+  let cpp_flags = get_cpp_flags(&config_path);
+
+  let mut build = cc::Build::new();
+  build
+    .cpp(true)
+    .file("src/sys/form/form.cpp")
+    .file("src/sys/search/search.cpp")
+    .file("src/sys/common/common.cpp")
+    .file("src/sys/wizard/wizard.cpp")
+    .file("src/sys/wizard/wizard_gui.cpp")
+    .file("src/sys/welcome/welcome.cpp")
+    .file("src/sys/welcome/welcome_gui.cpp")
+    .file("src/sys/troubleshooting/troubleshooting.cpp")
+    .file("src/sys/troubleshooting/troubleshooting_gui.cpp");
+  build.flag("-std=c++17");
+
+  for flag in cpp_flags {
+    build.flag(&flag);
+  }
+
+  build.compile("espansomodulosys");
+
+  // Render linker flags
+
+  generate_linker_flags(&config_path);
+}
+
+fn main() {
+  build_native();
+}
diff --git a/espanso-modulo/res/win.manifest b/espanso-modulo/res/win.manifest
new file mode 100644
index 00000000..88d7e2a0
--- /dev/null
+++ b/espanso-modulo/res/win.manifest
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+    <assemblyIdentity version="0.1.0.0" processorArchitecture="*" name="com.federicoterzi.modulo" type="win32"/>
+    <description>TODO</description>
+    <dependency>
+        <dependentAssembly>
+            <assemblyIdentity name="Microsoft.Windows.Common-Controls" version="6.0.0.0" type="win32" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+        </dependentAssembly>
+    </dependency>
+</assembly>
\ No newline at end of file
diff --git a/espanso-modulo/src/form/config.rs b/espanso-modulo/src/form/config.rs
new file mode 100644
index 00000000..236137ae
--- /dev/null
+++ b/espanso-modulo/src/form/config.rs
@@ -0,0 +1,202 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use serde::{Deserialize, Deserializer, Serialize};
+use std::collections::HashMap;
+
+fn default_title() -> String {
+  "espanso".to_owned()
+}
+
+fn default_icon() -> Option<String> {
+  None
+}
+
+fn default_fields() -> HashMap<String, FieldConfig> {
+  HashMap::new()
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct FormConfig {
+  #[serde(default = "default_title")]
+  pub title: String,
+
+  #[serde(default = "default_icon")]
+  pub icon: Option<String>,
+
+  pub layout: String,
+
+  #[serde(default = "default_fields")]
+  pub fields: HashMap<String, FieldConfig>,
+}
+
+#[derive(Debug, Serialize, Clone)]
+pub struct FieldConfig {
+  pub field_type: FieldTypeConfig,
+}
+
+impl Default for FieldConfig {
+  fn default() -> Self {
+    Self {
+      field_type: FieldTypeConfig::Text(TextFieldConfig {
+        ..Default::default()
+      }),
+    }
+  }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub enum FieldTypeConfig {
+  Text(TextFieldConfig),
+  Choice(ChoiceFieldConfig),
+  List(ListFieldConfig),
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct TextFieldConfig {
+  pub default: String,
+  pub multiline: bool,
+}
+
+impl Default for TextFieldConfig {
+  fn default() -> Self {
+    Self {
+      default: "".to_owned(),
+      multiline: false,
+    }
+  }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ChoiceFieldConfig {
+  pub values: Vec<String>,
+  pub default: String,
+}
+
+impl Default for ChoiceFieldConfig {
+  fn default() -> Self {
+    Self {
+      values: Vec::new(),
+      default: "".to_owned(),
+    }
+  }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ListFieldConfig {
+  pub values: Vec<String>,
+  pub default: String,
+}
+
+impl Default for ListFieldConfig {
+  fn default() -> Self {
+    Self {
+      values: Vec::new(),
+      default: "".to_owned(),
+    }
+  }
+}
+
+impl<'de> serde::Deserialize<'de> for FieldConfig {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    let auto_field = AutoFieldConfig::deserialize(deserializer)?;
+    Ok(FieldConfig::from(&auto_field))
+  }
+}
+
+impl<'a> From<&'a AutoFieldConfig> for FieldConfig {
+  fn from(other: &'a AutoFieldConfig) -> Self {
+    let field_type = match other.field_type.as_ref() {
+      "text" => {
+        let mut config: TextFieldConfig = Default::default();
+
+        if let Some(default) = &other.default {
+          config.default = default.clone();
+        }
+
+        config.multiline = other.multiline;
+
+        FieldTypeConfig::Text(config)
+      }
+      "choice" => {
+        let mut config = ChoiceFieldConfig {
+          values: other.values.clone(),
+          ..Default::default()
+        };
+
+        if let Some(default) = &other.default {
+          config.default = default.clone();
+        }
+
+        FieldTypeConfig::Choice(config)
+      }
+      "list" => {
+        let mut config = ListFieldConfig {
+          values: other.values.clone(),
+          ..Default::default()
+        };
+
+        if let Some(default) = &other.default {
+          config.default = default.clone();
+        }
+
+        FieldTypeConfig::List(config)
+      }
+      _ => {
+        panic!("invalid field type: {}", other.field_type);
+      }
+    };
+
+    Self { field_type }
+  }
+}
+
+fn default_type() -> String {
+  "text".to_owned()
+}
+
+fn default_default() -> Option<String> {
+  None
+}
+
+fn default_multiline() -> bool {
+  false
+}
+
+fn default_values() -> Vec<String> {
+  Vec::new()
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct AutoFieldConfig {
+  #[serde(rename = "type", default = "default_type")]
+  pub field_type: String,
+
+  #[serde(default = "default_default")]
+  pub default: Option<String>,
+
+  #[serde(default = "default_multiline")]
+  pub multiline: bool,
+
+  #[serde(default = "default_values")]
+  pub values: Vec<String>,
+}
diff --git a/espanso-modulo/src/form/generator.rs b/espanso-modulo/src/form/generator.rs
new file mode 100644
index 00000000..fb13e5eb
--- /dev/null
+++ b/espanso-modulo/src/form/generator.rs
@@ -0,0 +1,99 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use super::config::{FieldConfig, FieldTypeConfig, FormConfig};
+use super::parser::layout::Token;
+use crate::sys::form::types::*;
+use std::collections::HashMap;
+
+pub fn generate(config: FormConfig) -> Form {
+  let structure = super::parser::layout::parse_layout(&config.layout);
+  build_form(config, structure)
+}
+
+fn create_field(token: &Token, field_map: &HashMap<String, FieldConfig>) -> Field {
+  match token {
+    Token::Text(text) => Field {
+      field_type: FieldType::Label(LabelMetadata { text: text.clone() }),
+      ..Default::default()
+    },
+    Token::Field(name) => {
+      let config = if let Some(config) = field_map.get(name) {
+        config.clone()
+      } else {
+        Default::default()
+      };
+
+      let field_type = match &config.field_type {
+        FieldTypeConfig::Text(config) => FieldType::Text(TextMetadata {
+          default_text: config.default.clone(),
+          multiline: config.multiline,
+        }),
+        FieldTypeConfig::Choice(config) => FieldType::Choice(ChoiceMetadata {
+          values: config.values.clone(),
+          choice_type: ChoiceType::Dropdown,
+          default_value: config.default.clone(),
+        }),
+        FieldTypeConfig::List(config) => FieldType::Choice(ChoiceMetadata {
+          values: config.values.clone(),
+          choice_type: ChoiceType::List,
+          default_value: config.default.clone(),
+        }),
+      };
+
+      Field {
+        id: Some(name.clone()),
+        field_type,
+      }
+    }
+  }
+}
+
+fn build_form(form: FormConfig, structure: Vec<Vec<Token>>) -> Form {
+  let field_map = form.fields;
+  let mut fields = Vec::new();
+
+  for row in structure.iter() {
+    let current_field = if row.len() == 1 {
+      // Single field
+      create_field(&row[0], &field_map)
+    } else {
+      // Row field
+      let inner_fields = row
+        .iter()
+        .map(|token| create_field(token, &field_map))
+        .collect();
+
+      Field {
+        field_type: FieldType::Row(RowMetadata {
+          fields: inner_fields,
+        }),
+        ..Default::default()
+      }
+    };
+
+    fields.push(current_field)
+  }
+
+  Form {
+    title: form.title,
+    icon: form.icon,
+    fields,
+  }
+}
diff --git a/espanso-modulo/src/form/mod.rs b/espanso-modulo/src/form/mod.rs
new file mode 100644
index 00000000..0556ba34
--- /dev/null
+++ b/espanso-modulo/src/form/mod.rs
@@ -0,0 +1,24 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub mod config;
+pub mod generator;
+pub mod parser;
+
+pub use crate::sys::form::show;
diff --git a/espanso-modulo/src/form/parser/layout.rs b/espanso-modulo/src/form/parser/layout.rs
new file mode 100644
index 00000000..0f77587b
--- /dev/null
+++ b/espanso-modulo/src/form/parser/layout.rs
@@ -0,0 +1,108 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use super::split::*;
+use regex::Regex;
+
+lazy_static! {
+  static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Token {
+  Text(String),  // Text
+  Field(String), // id: String
+}
+
+pub fn parse_layout(layout: &str) -> Vec<Vec<Token>> {
+  let mut rows = Vec::new();
+
+  for line in layout.lines() {
+    let line = line.trim();
+
+    // Skip empty lines
+    if line.is_empty() {
+      continue;
+    }
+
+    let mut row: Vec<Token> = Vec::new();
+
+    let splitter = SplitCaptures::new(&FIELD_REGEX, line);
+
+    // Get the individual tokens
+    for state in splitter {
+      match state {
+        SplitState::Unmatched(text) => {
+          if !text.is_empty() {
+            row.push(Token::Text(text.to_owned()))
+          }
+        }
+        SplitState::Captured(caps) => {
+          if let Some(name) = caps.get(1) {
+            let name = name.as_str().to_owned();
+            row.push(Token::Field(name));
+          }
+        }
+      }
+    }
+
+    rows.push(row);
+  }
+
+  rows
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_parse_layout() {
+    let layout = "Hey {{name}},\nHow are you?\n  \nCheers";
+    let result = parse_layout(layout);
+    assert_eq!(
+      result,
+      vec![
+        vec![
+          Token::Text("Hey ".to_owned()),
+          Token::Field("name".to_owned()),
+          Token::Text(",".to_owned())
+        ],
+        vec![Token::Text("How are you?".to_owned())],
+        vec![Token::Text("Cheers".to_owned())],
+      ]
+    );
+  }
+
+  #[test]
+  fn test_parse_layout_2() {
+    let layout = "Hey {{name}} {{surname}},";
+    let result = parse_layout(layout);
+    assert_eq!(
+      result,
+      vec![vec![
+        Token::Text("Hey ".to_owned()),
+        Token::Field("name".to_owned()),
+        Token::Text(" ".to_owned()),
+        Token::Field("surname".to_owned()),
+        Token::Text(",".to_owned())
+      ],]
+    );
+  }
+}
diff --git a/espanso-modulo/src/form/parser/mod.rs b/espanso-modulo/src/form/parser/mod.rs
new file mode 100644
index 00000000..75c5493d
--- /dev/null
+++ b/espanso-modulo/src/form/parser/mod.rs
@@ -0,0 +1,21 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub mod layout;
+mod split;
diff --git a/espanso-modulo/src/form/parser/split.rs b/espanso-modulo/src/form/parser/split.rs
new file mode 100644
index 00000000..b510d6d0
--- /dev/null
+++ b/espanso-modulo/src/form/parser/split.rs
@@ -0,0 +1,54 @@
+// Taken from: https://github.com/rust-lang/regex/issues/330#issuecomment-274058261
+use regex::{CaptureMatches, Captures, Regex};
+
+pub struct SplitCaptures<'r, 't> {
+  finder: CaptureMatches<'r, 't>,
+  text: &'t str,
+  last: usize,
+  caps: Option<Captures<'t>>,
+}
+
+impl<'r, 't> SplitCaptures<'r, 't> {
+  pub fn new(re: &'r Regex, text: &'t str) -> SplitCaptures<'r, 't> {
+    SplitCaptures {
+      finder: re.captures_iter(text),
+      text,
+      last: 0,
+      caps: None,
+    }
+  }
+}
+
+#[derive(Debug)]
+pub enum SplitState<'t> {
+  Unmatched(&'t str),
+  Captured(Captures<'t>),
+}
+
+impl<'r, 't> Iterator for SplitCaptures<'r, 't> {
+  type Item = SplitState<'t>;
+
+  fn next(&mut self) -> Option<SplitState<'t>> {
+    if let Some(caps) = self.caps.take() {
+      return Some(SplitState::Captured(caps));
+    }
+    match self.finder.next() {
+      None => {
+        if self.last >= self.text.len() {
+          None
+        } else {
+          let s = &self.text[self.last..];
+          self.last = self.text.len();
+          Some(SplitState::Unmatched(s))
+        }
+      }
+      Some(caps) => {
+        let m = caps.get(0).unwrap();
+        let unmatched = &self.text[self.last..m.start()];
+        self.last = m.end();
+        self.caps = Some(caps);
+        Some(SplitState::Unmatched(unmatched))
+      }
+    }
+  }
+}
diff --git a/src/bridge/mod.rs b/espanso-modulo/src/lib.rs
similarity index 77%
rename from src/bridge/mod.rs
rename to espanso-modulo/src/lib.rs
index 3446e17e..afd53965 100644
--- a/src/bridge/mod.rs
+++ b/espanso-modulo/src/lib.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -17,11 +17,12 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-#[cfg(target_os = "windows")]
-pub(crate) mod windows;
+#[macro_use]
+extern crate lazy_static;
 
-#[cfg(target_os = "linux")]
-pub(crate) mod linux;
-
-#[cfg(target_os = "macos")]
-pub(crate) mod macos;
+pub mod form;
+pub mod search;
+mod sys;
+pub mod troubleshooting;
+pub mod welcome;
+pub mod wizard;
diff --git a/espanso-modulo/src/search/algorithm.rs b/espanso-modulo/src/search/algorithm.rs
new file mode 100644
index 00000000..80c829aa
--- /dev/null
+++ b/espanso-modulo/src/search/algorithm.rs
@@ -0,0 +1,126 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::collections::HashSet;
+
+use crate::sys::search::types::SearchItem;
+
+pub fn get_algorithm(
+  name: &str,
+  use_command_filter: bool,
+) -> Box<dyn Fn(&str, &[SearchItem]) -> Vec<usize>> {
+  let search_algorithm: Box<dyn Fn(&str, &[SearchItem]) -> Vec<usize>> = match name {
+    "exact" => Box::new(exact_match),
+    "iexact" => Box::new(case_insensitive_exact_match),
+    "ikey" => Box::new(case_insensitive_keyword),
+    _ => panic!("unknown search algorithm: {}", name),
+  };
+
+  if use_command_filter {
+    command_filter(search_algorithm)
+  } else {
+    search_algorithm
+  }
+}
+
+fn exact_match(query: &str, items: &[SearchItem]) -> Vec<usize> {
+  items
+    .iter()
+    .enumerate()
+    .filter(|(_, item)| {
+      item.label.contains(query) || item.trigger.as_deref().map_or(false, |t| t.contains(query))
+    })
+    .map(|(i, _)| i)
+    .collect()
+}
+
+fn case_insensitive_exact_match(query: &str, items: &[SearchItem]) -> Vec<usize> {
+  let lowercase_query = query.to_lowercase();
+  items
+    .iter()
+    .enumerate()
+    .filter(|(_, item)| {
+      item.label.to_lowercase().contains(&lowercase_query)
+        || item
+          .trigger
+          .as_deref()
+          .map_or(false, |t| t.to_lowercase().contains(query))
+    })
+    .map(|(i, _)| i)
+    .collect()
+}
+
+fn case_insensitive_keyword(query: &str, items: &[SearchItem]) -> Vec<usize> {
+  let lowercase_query = query.to_lowercase();
+  let keywords: Vec<&str> = lowercase_query.split_whitespace().collect();
+  items
+    .iter()
+    .enumerate()
+    .filter(|(_, item)| {
+      for keyword in keywords.iter() {
+        if !item.label.to_lowercase().contains(keyword)
+          && !item
+            .trigger
+            .as_deref()
+            .map_or(false, |t| t.to_lowercase().contains(keyword))
+        {
+          return false;
+        }
+      }
+
+      true
+    })
+    .map(|(i, _)| i)
+    .collect()
+}
+
+fn command_filter(
+  search_algorithm: Box<dyn Fn(&str, &[SearchItem]) -> Vec<usize>>,
+) -> Box<dyn Fn(&str, &[SearchItem]) -> Vec<usize>> {
+  Box::new(move |query, items| {
+    let (valid_ids, trimmed_query) = if query.starts_with('>') {
+      (
+        items
+          .iter()
+          .enumerate()
+          .filter(|(_, item)| item.is_builtin)
+          .map(|(i, _)| i)
+          .collect::<HashSet<usize>>(),
+        query.trim_start_matches('>'),
+      )
+    } else {
+      (
+        items
+          .iter()
+          .enumerate()
+          .filter(|(_, item)| !item.is_builtin)
+          .map(|(i, _)| i)
+          .collect::<HashSet<usize>>(),
+        query,
+      )
+    };
+
+    let results = search_algorithm(trimmed_query, items);
+
+    results
+      .into_iter()
+      .filter(|id| valid_ids.contains(id))
+      .collect()
+  })
+}
diff --git a/espanso-modulo/src/search/config.rs b/espanso-modulo/src/search/config.rs
new file mode 100644
index 00000000..69fc5421
--- /dev/null
+++ b/espanso-modulo/src/search/config.rs
@@ -0,0 +1,64 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use serde::{Deserialize, Serialize};
+
+fn default_title() -> String {
+  "espanso".to_owned()
+}
+
+fn default_icon() -> Option<String> {
+  None
+}
+
+fn default_items() -> Vec<SearchItem> {
+  Vec::new()
+}
+
+fn default_algorithm() -> String {
+  "ikey".to_owned()
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct SearchConfig {
+  #[serde(default = "default_title")]
+  pub title: String,
+
+  #[serde(default = "default_icon")]
+  pub icon: Option<String>,
+
+  #[serde(default = "default_items")]
+  pub items: Vec<SearchItem>,
+
+  #[serde(default = "default_algorithm")]
+  pub algorithm: String,
+
+  #[serde(default)]
+  pub hint: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct SearchItem {
+  pub id: String,
+  pub label: String,
+  pub trigger: Option<String>,
+
+  #[serde(default)]
+  pub is_builtin: bool,
+}
diff --git a/espanso-modulo/src/search/generator.rs b/espanso-modulo/src/search/generator.rs
new file mode 100644
index 00000000..98a9c887
--- /dev/null
+++ b/espanso-modulo/src/search/generator.rs
@@ -0,0 +1,43 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::search::config::SearchConfig;
+use crate::sys::search::types;
+
+pub fn generate(config: SearchConfig) -> types::Search {
+  let mut items: Vec<types::SearchItem> = config
+    .items
+    .into_iter()
+    .map(|item| types::SearchItem {
+      id: item.id,
+      label: item.label,
+      trigger: item.trigger,
+      is_builtin: item.is_builtin,
+    })
+    .collect();
+
+  items.sort_by(|a, b| a.label.as_str().cmp(b.label.as_str()));
+
+  types::Search {
+    title: config.title,
+    items,
+    icon: config.icon,
+    hint: config.hint,
+  }
+}
diff --git a/espanso-modulo/src/search/mod.rs b/espanso-modulo/src/search/mod.rs
new file mode 100644
index 00000000..c076798c
--- /dev/null
+++ b/espanso-modulo/src/search/mod.rs
@@ -0,0 +1,24 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub mod algorithm;
+pub mod config;
+pub mod generator;
+
+pub use crate::sys::search::show;
diff --git a/espanso-modulo/src/sys/common/common.cpp b/espanso-modulo/src/sys/common/common.cpp
new file mode 100644
index 00000000..53a4e163
--- /dev/null
+++ b/espanso-modulo/src/sys/common/common.cpp
@@ -0,0 +1,83 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "common.h"
+
+#ifdef __WXMSW__
+#include <windows.h>
+#endif
+#ifdef __WXOSX__
+#include "mac.h"
+#endif
+
+void setFrameIcon(const char * iconPath, wxFrame * frame) {
+    if (iconPath) {
+        wxString iconPath(iconPath);
+        wxBitmapType imgType = wxICON_DEFAULT_TYPE;
+
+        #ifdef __WXMSW__
+            imgType = wxBITMAP_TYPE_ICO;
+        #endif
+
+        wxIcon icon;
+        icon.LoadFile(iconPath, imgType);
+        if (icon.IsOk()) {
+            frame->SetIcon(icon);
+        }
+    }
+}
+
+void Activate(wxFrame * frame) {
+    #ifdef __WXMSW__
+
+    HWND handle = frame->GetHandle();
+    if (handle == GetForegroundWindow()) {
+        return;
+    }
+
+    if (IsIconic(handle)) {
+        ShowWindow(handle, 9);
+    }
+
+    INPUT ip;
+    ip.type = INPUT_KEYBOARD;
+    ip.ki.wScan = 0;
+    ip.ki.time = 0;
+    ip.ki.dwExtraInfo = 0;
+    ip.ki.wVk = VK_MENU;
+    ip.ki.dwFlags = 0;
+
+    SendInput(1, &ip, sizeof(INPUT));
+    ip.ki.dwFlags = KEYEVENTF_KEYUP;
+
+    SendInput(1, &ip, sizeof(INPUT));
+
+    SetForegroundWindow(handle);
+
+    #endif
+    #ifdef __WXOSX__
+    ActivateApp();
+    #endif
+}
+
+void SetupWindowStyle(wxFrame * frame) {
+    #ifdef __WXOSX__
+        SetWindowStyles((NSWindow*) frame->MacGetTopLevelWindowRef());
+    #endif
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/common/common.h b/espanso-modulo/src/sys/common/common.h
new file mode 100644
index 00000000..99b1d7ab
--- /dev/null
+++ b/espanso-modulo/src/sys/common/common.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef MODULO_COMMON
+#define MODULO_COMMON
+
+#define _UNICODE
+
+#include <wx/wxprec.h>
+#ifndef WX_PRECOMP
+    #include <wx/wx.h>
+#endif
+
+void setFrameIcon(const char * iconPath, wxFrame * frame);
+
+void Activate(wxFrame * frame);
+
+void SetupWindowStyle(wxFrame * frame);
+
+#endif
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/common/mac.h b/espanso-modulo/src/sys/common/mac.h
new file mode 100644
index 00000000..4bdd9812
--- /dev/null
+++ b/espanso-modulo/src/sys/common/mac.h
@@ -0,0 +1,21 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+void ActivateApp();
+void SetWindowStyles(NSWindow * window);
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/common/mac.mm b/espanso-modulo/src/sys/common/mac.mm
new file mode 100644
index 00000000..d08348bd
--- /dev/null
+++ b/espanso-modulo/src/sys/common/mac.mm
@@ -0,0 +1,30 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#import <AppKit/AppKit.h>
+
+void ActivateApp() {
+    [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
+}
+
+void SetWindowStyles(NSWindow * window) {
+    window.titleVisibility = NSWindowTitleHidden;
+	window.styleMask &= ~NSWindowStyleMaskTitled;
+    window.movableByWindowBackground = true;
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp
new file mode 100644
index 00000000..120c053d
--- /dev/null
+++ b/espanso-modulo/src/sys/form/form.cpp
@@ -0,0 +1,289 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#define _UNICODE
+
+#include "../common/common.h"
+#include "../interop/interop.h"
+
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+// https://docs.wxwidgets.org/stable/classwx_frame.html
+const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxCLOSE_BOX | wxCAPTION;
+
+const int PADDING = 5;
+const int MULTILINE_MIN_HEIGHT = 100;
+const int MULTILINE_MIN_WIDTH = 100;
+
+FormMetadata *formMetadata = nullptr;
+std::vector<ValuePair> values;
+
+// Field Wrappers
+
+class FieldWrapper {
+public:
+    virtual wxString getValue() = 0;
+};
+
+class TextFieldWrapper {
+    wxTextCtrl * control;
+public:
+    explicit TextFieldWrapper(wxTextCtrl * control): control(control) {}
+
+    virtual wxString getValue() {
+        return control->GetValue();
+    }
+};
+
+class ChoiceFieldWrapper {
+    wxChoice * control;
+public:
+    explicit ChoiceFieldWrapper(wxChoice * control): control(control) {}
+
+    virtual wxString getValue() {
+        return control->GetStringSelection();
+    }
+};
+
+class ListFieldWrapper {
+    wxListBox * control;
+public:
+    explicit ListFieldWrapper(wxListBox * control): control(control) {}
+
+    virtual wxString getValue() {
+        return control->GetStringSelection();
+    }
+};
+
+// App Code
+
+class FormApp: public wxApp
+{
+public:
+    virtual bool OnInit();
+};
+class FormFrame: public wxFrame
+{
+public:
+    FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
+
+    wxPanel *panel;
+    std::vector<void *> fields;
+    std::unordered_map<const char *, std::unique_ptr<FieldWrapper>> idMap;
+    wxButton *submit;
+private:
+    void AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta);
+    void Submit();
+    void OnSubmitBtn(wxCommandEvent& event);
+    void OnEscape(wxKeyEvent& event);
+};
+enum
+{
+    ID_Submit = 20000
+};
+
+bool FormApp::OnInit()
+{
+    FormFrame *frame = new FormFrame(formMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340) );
+    setFrameIcon(formMetadata->iconPath, frame);
+    frame->Show( true );
+    
+    Activate(frame);
+
+    return true;
+}
+FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
+        : wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
+{
+    panel = new wxPanel(this, wxID_ANY);
+    wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
+    panel->SetSizer(vbox);
+
+    for (int field = 0; field < formMetadata->fieldSize; field++) {
+        FieldMetadata meta = formMetadata->fields[field];
+        AddComponent(panel, vbox, meta);
+    }
+
+    submit = new wxButton(panel, ID_Submit, "Submit");
+    vbox->Add(submit, 1, wxEXPAND | wxALL, PADDING);
+
+    Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
+    Bind(wxEVT_CHAR_HOOK, &FormFrame::OnEscape, this, wxID_ANY);
+    // TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
+
+    this->SetClientSize(panel->GetBestSize());
+    this->CentreOnScreen();
+}
+
+void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta) {
+    void * control = nullptr;
+
+    switch (meta.fieldType) {
+        case FieldType::LABEL:
+        {
+            const LabelMetadata *labelMeta = static_cast<const LabelMetadata*>(meta.specific);
+            auto label = new wxStaticText(parent, wxID_ANY, wxString::FromUTF8(labelMeta->text), wxDefaultPosition, wxDefaultSize);
+            control = label;
+            fields.push_back(label);
+            break;
+        }
+        case FieldType::TEXT:
+        {
+            const TextMetadata *textMeta = static_cast<const TextMetadata*>(meta.specific);
+            long style = 0;
+            if (textMeta->multiline) {
+                style |= wxTE_MULTILINE;
+            }
+
+            auto textControl = new wxTextCtrl(parent, NewControlId(), wxString::FromUTF8(textMeta->defaultText), wxDefaultPosition, wxDefaultSize, style);
+            
+            if (textMeta->multiline) {
+                textControl->SetMinSize(wxSize(MULTILINE_MIN_WIDTH, MULTILINE_MIN_HEIGHT));
+            }
+
+            // Create the field wrapper
+            std::unique_ptr<FieldWrapper> field((FieldWrapper*) new TextFieldWrapper(textControl));
+            idMap[meta.id] = std::move(field);
+            control = textControl;
+            fields.push_back(textControl);
+            break;
+        }
+        case FieldType::CHOICE:
+        {
+            const ChoiceMetadata *choiceMeta = static_cast<const ChoiceMetadata*>(meta.specific);
+
+            int selectedItem = -1;
+            wxArrayString choices;
+            for (int i = 0; i<choiceMeta->valueSize; i++) {
+                choices.Add(wxString::FromUTF8(choiceMeta->values[i]));
+
+                if (strcmp(choiceMeta->values[i], choiceMeta->defaultValue) == 0) {
+                    selectedItem = i;
+                }
+            }
+
+            void * choice = nullptr;
+            if (choiceMeta->choiceType == ChoiceType::DROPDOWN) {
+                choice = (void*) new wxChoice(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
+
+                if (selectedItem >= 0) {
+                    ((wxChoice*)choice)->SetSelection(selectedItem);
+                }
+
+                // Create the field wrapper
+                std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ChoiceFieldWrapper((wxChoice*) choice));
+                idMap[meta.id] = std::move(field);
+            }else {
+                choice = (void*) new wxListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
+                
+                if (selectedItem >= 0) {
+                    ((wxListBox*)choice)->SetSelection(selectedItem);
+                }
+                
+                // Create the field wrapper
+                std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
+                idMap[meta.id] = std::move(field);
+            }
+            
+            
+
+            control = choice;
+            fields.push_back(choice);
+            break;
+        }
+        case FieldType::ROW:
+        {
+            const RowMetadata *rowMeta = static_cast<const RowMetadata*>(meta.specific);
+
+            auto innerPanel = new wxPanel(panel, wxID_ANY);
+            wxBoxSizer *hbox = new wxBoxSizer(wxHORIZONTAL);
+            innerPanel->SetSizer(hbox);
+            sizer->Add(innerPanel, 0, wxEXPAND | wxALL, 0);
+            fields.push_back(innerPanel);
+
+            for (int field = 0; field < rowMeta->fieldSize; field++) {
+                FieldMetadata innerMeta = rowMeta->fields[field];
+                AddComponent(innerPanel, hbox, innerMeta);
+            }
+
+            break;
+        }
+        default:
+            // TODO: handle unknown field type
+            break;
+    }
+
+    if (control) {
+        sizer->Add((wxWindow*) control, 0, wxEXPAND | wxALL, PADDING);
+    }
+}
+
+void FormFrame::Submit() {
+    for (auto& field: idMap) {
+        FieldWrapper * fieldWrapper = (FieldWrapper*) field.second.get();
+        wxString value {fieldWrapper->getValue()};
+        wxCharBuffer buffer {value.ToUTF8()};
+        char * id = strdup(field.first);
+        char * c_value = strdup(buffer.data());
+        ValuePair valuePair = {
+            id,
+            c_value,
+        };
+        values.push_back(valuePair);
+    }
+
+    Close(true);
+}
+
+void FormFrame::OnSubmitBtn(wxCommandEvent &event) {
+    Submit();
+}
+
+void FormFrame::OnEscape(wxKeyEvent& event) {
+    if (event.GetKeyCode() == WXK_ESCAPE) {
+        Close(true);
+    }else if(event.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_RAW_CONTROL)) {
+        Submit();
+    }else{
+        event.Skip();
+    }
+}
+
+extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
+    // Setup high DPI support on Windows
+    #ifdef __WXMSW__
+        SetProcessDPIAware();
+    #endif
+    
+    formMetadata = _metadata;
+
+    wxApp::SetInstance(new FormApp());
+    int argc = 0;
+    wxEntry(argc, (char **)nullptr);
+
+    callback(values.data(), values.size(), data);
+
+    // Free up values
+    for (auto pair: values) {
+        free((void*) pair.id);
+        free((void*) pair.value);
+    }
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/form/mod.rs b/espanso-modulo/src/sys/form/mod.rs
new file mode 100644
index 00000000..42f3cb97
--- /dev/null
+++ b/espanso-modulo/src/sys/form/mod.rs
@@ -0,0 +1,383 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::collections::HashMap;
+use std::ffi::CStr;
+use std::os::raw::c_int;
+
+// Form schema
+
+pub mod types {
+  #[derive(Debug)]
+  pub struct Form {
+    pub title: String,
+    pub icon: Option<String>,
+    pub fields: Vec<Field>,
+  }
+
+  #[derive(Debug)]
+  pub struct Field {
+    pub id: Option<String>,
+    pub field_type: FieldType,
+  }
+
+  impl Default for Field {
+    fn default() -> Self {
+      Self {
+        id: None,
+        field_type: FieldType::Unknown,
+      }
+    }
+  }
+
+  #[derive(Debug)]
+  pub enum FieldType {
+    Unknown,
+    Row(RowMetadata),
+    Label(LabelMetadata),
+    Text(TextMetadata),
+    Choice(ChoiceMetadata),
+  }
+
+  #[derive(Debug)]
+  pub struct RowMetadata {
+    pub fields: Vec<Field>,
+  }
+
+  #[derive(Debug)]
+  pub struct LabelMetadata {
+    pub text: String,
+  }
+
+  #[derive(Debug)]
+  pub struct TextMetadata {
+    pub default_text: String,
+    pub multiline: bool,
+  }
+
+  #[derive(Debug)]
+  pub enum ChoiceType {
+    Dropdown,
+    List,
+  }
+
+  #[derive(Debug)]
+  pub struct ChoiceMetadata {
+    pub values: Vec<String>,
+    pub choice_type: ChoiceType,
+    pub default_value: String,
+  }
+}
+
+// Form interop
+
+#[allow(dead_code)]
+mod interop {
+  use super::super::interop::*;
+  use super::types;
+  use std::ffi::{c_void, CString};
+  use std::os::raw::{c_char, c_int};
+  use std::ptr::null;
+
+  pub(crate) struct OwnedForm {
+    title: CString,
+    icon_path: CString,
+    fields: Vec<OwnedField>,
+
+    _metadata: Vec<FieldMetadata>,
+    _interop: Box<FormMetadata>,
+  }
+
+  impl Interoperable for OwnedForm {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const FormMetadata as *const c_void
+    }
+  }
+
+  impl From<types::Form> for OwnedForm {
+    fn from(form: types::Form) -> Self {
+      let title = CString::new(form.title).expect("unable to convert form title to CString");
+      let fields: Vec<OwnedField> = form.fields.into_iter().map(|field| field.into()).collect();
+
+      let _metadata: Vec<FieldMetadata> = fields.iter().map(|field| field.metadata()).collect();
+
+      let icon_path = if let Some(icon_path) = form.icon.as_ref() {
+        icon_path.clone()
+      } else {
+        "".to_owned()
+      };
+
+      let icon_path = CString::new(icon_path).expect("unable to convert form icon to CString");
+
+      let icon_path_ptr = if form.icon.is_some() {
+        icon_path.as_ptr()
+      } else {
+        std::ptr::null()
+      };
+
+      let _interop = Box::new(FormMetadata {
+        windowTitle: title.as_ptr(),
+        iconPath: icon_path_ptr,
+        fields: _metadata.as_ptr(),
+        fieldSize: fields.len() as c_int,
+      });
+
+      Self {
+        title,
+        icon_path,
+        fields,
+        _metadata,
+        _interop,
+      }
+    }
+  }
+
+  struct OwnedField {
+    id: Option<CString>,
+    field_type: FieldType,
+    specific: Box<dyn Interoperable>,
+  }
+
+  impl From<types::Field> for OwnedField {
+    fn from(field: types::Field) -> Self {
+      let id = field
+        .id
+        .map(|id| CString::new(id).expect("unable to create cstring for field id"));
+
+      let field_type = match field.field_type {
+        types::FieldType::Row(_) => FieldType_ROW,
+        types::FieldType::Label(_) => FieldType_LABEL,
+        types::FieldType::Text(_) => FieldType_TEXT,
+        types::FieldType::Choice(_) => FieldType_CHOICE,
+        types::FieldType::Unknown => panic!("unknown field type"),
+      };
+
+      // TODO: clean up this match
+      let specific: Box<dyn Interoperable> = match field.field_type {
+        types::FieldType::Row(metadata) => {
+          let owned_metadata: OwnedRowMetadata = metadata.into();
+          Box::new(owned_metadata)
+        }
+        types::FieldType::Label(metadata) => {
+          let owned_metadata: OwnedLabelMetadata = metadata.into();
+          Box::new(owned_metadata)
+        }
+        types::FieldType::Text(metadata) => {
+          let owned_metadata: OwnedTextMetadata = metadata.into();
+          Box::new(owned_metadata)
+        }
+        types::FieldType::Choice(metadata) => {
+          let owned_metadata: OwnedChoiceMetadata = metadata.into();
+          Box::new(owned_metadata)
+        }
+        types::FieldType::Unknown => panic!("unknown field type"),
+      };
+
+      Self {
+        id,
+        field_type,
+        specific,
+      }
+    }
+  }
+
+  impl OwnedField {
+    pub fn metadata(&self) -> FieldMetadata {
+      let id_ptr = if let Some(id) = self.id.as_ref() {
+        id.as_ptr()
+      } else {
+        null()
+      };
+
+      FieldMetadata {
+        id: id_ptr,
+        fieldType: self.field_type,
+        specific: self.specific.as_ptr(),
+      }
+    }
+  }
+
+  struct OwnedLabelMetadata {
+    text: CString,
+    _interop: Box<LabelMetadata>,
+  }
+
+  impl Interoperable for OwnedLabelMetadata {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const LabelMetadata as *const c_void
+    }
+  }
+
+  impl From<types::LabelMetadata> for OwnedLabelMetadata {
+    fn from(label_metadata: types::LabelMetadata) -> Self {
+      let text =
+        CString::new(label_metadata.text).expect("unable to convert label text to CString");
+      let _interop = Box::new(LabelMetadata {
+        text: text.as_ptr(),
+      });
+      Self { text, _interop }
+    }
+  }
+
+  struct OwnedTextMetadata {
+    default_text: CString,
+    _interop: Box<TextMetadata>,
+  }
+
+  impl Interoperable for OwnedTextMetadata {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const TextMetadata as *const c_void
+    }
+  }
+
+  impl From<types::TextMetadata> for OwnedTextMetadata {
+    fn from(text_metadata: types::TextMetadata) -> Self {
+      let default_text = CString::new(text_metadata.default_text)
+        .expect("unable to convert default text to CString");
+      let _interop = Box::new(TextMetadata {
+        defaultText: default_text.as_ptr(),
+        multiline: if text_metadata.multiline { 1 } else { 0 },
+      });
+      Self {
+        default_text,
+        _interop,
+      }
+    }
+  }
+
+  struct OwnedChoiceMetadata {
+    values: Vec<CString>,
+    values_ptr_array: Vec<*const c_char>,
+    default_value: CString,
+    _interop: Box<ChoiceMetadata>,
+  }
+
+  impl Interoperable for OwnedChoiceMetadata {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const ChoiceMetadata as *const c_void
+    }
+  }
+
+  impl From<types::ChoiceMetadata> for OwnedChoiceMetadata {
+    fn from(metadata: types::ChoiceMetadata) -> Self {
+      let values: Vec<CString> = metadata
+        .values
+        .into_iter()
+        .map(|value| CString::new(value).expect("unable to convert choice value to string"))
+        .collect();
+
+      let values_ptr_array: Vec<*const c_char> =
+        values.iter().map(|value| value.as_ptr()).collect();
+
+      let choice_type = match metadata.choice_type {
+        types::ChoiceType::Dropdown => ChoiceType_DROPDOWN,
+        types::ChoiceType::List => ChoiceType_LIST,
+      };
+
+      let default_value =
+        CString::new(metadata.default_value).expect("unable to convert default value to CString");
+
+      let _interop = Box::new(ChoiceMetadata {
+        values: values_ptr_array.as_ptr(),
+        valueSize: values.len() as c_int,
+        choiceType: choice_type,
+        defaultValue: default_value.as_ptr(),
+      });
+      Self {
+        values,
+        values_ptr_array,
+        default_value,
+        _interop,
+      }
+    }
+  }
+
+  struct OwnedRowMetadata {
+    fields: Vec<OwnedField>,
+
+    _metadata: Vec<FieldMetadata>,
+    _interop: Box<RowMetadata>,
+  }
+
+  impl Interoperable for OwnedRowMetadata {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const RowMetadata as *const c_void
+    }
+  }
+
+  impl From<types::RowMetadata> for OwnedRowMetadata {
+    fn from(row_metadata: types::RowMetadata) -> Self {
+      let fields: Vec<OwnedField> = row_metadata
+        .fields
+        .into_iter()
+        .map(|field| field.into())
+        .collect();
+
+      let _metadata: Vec<FieldMetadata> = fields.iter().map(|field| field.metadata()).collect();
+
+      let _interop = Box::new(RowMetadata {
+        fields: _metadata.as_ptr(),
+        fieldSize: _metadata.len() as c_int,
+      });
+
+      Self {
+        fields,
+        _metadata,
+        _interop,
+      }
+    }
+  }
+}
+
+pub fn show(form: types::Form) -> HashMap<String, String> {
+  use super::interop::*;
+  use std::os::raw::c_void;
+
+  let owned_form: interop::OwnedForm = form.into();
+  let metadata: *const FormMetadata = owned_form.as_ptr() as *const FormMetadata;
+
+  let mut value_map: HashMap<String, String> = HashMap::new();
+
+  extern "C" fn callback(values: *const ValuePair, size: c_int, map: *mut c_void) {
+    let values: &[ValuePair] = unsafe { std::slice::from_raw_parts(values, size as usize) };
+    let map = map as *mut HashMap<String, String>;
+    let map = unsafe { &mut (*map) };
+    for pair in values.iter() {
+      unsafe {
+        let id = CStr::from_ptr(pair.id);
+        let value = CStr::from_ptr(pair.value);
+
+        let id = id.to_string_lossy().to_string();
+        let value = value.to_string_lossy().to_string();
+        map.insert(id, value);
+      }
+    }
+  }
+
+  unsafe {
+    // TODO: Nested rows should fail, add check
+    interop_show_form(
+      metadata,
+      callback,
+      &mut value_map as *mut HashMap<String, String> as *mut c_void,
+    );
+  }
+
+  value_map
+}
diff --git a/espanso-modulo/src/sys/interop/interop.h b/espanso-modulo/src/sys/interop/interop.h
new file mode 100644
index 00000000..2d97c1eb
--- /dev/null
+++ b/espanso-modulo/src/sys/interop/interop.h
@@ -0,0 +1,170 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// FORM
+
+typedef enum FieldType {
+  ROW,
+  LABEL,
+  TEXT,
+  CHOICE,
+  CHECKBOX,
+} FieldType;
+
+typedef struct LabelMetadata {
+  const char *text;
+} LabelMetadata;
+
+typedef struct TextMetadata {
+  const char *defaultText;
+  const int multiline;
+} TextMetadata;
+
+typedef enum ChoiceType {
+  DROPDOWN,
+  LIST,
+} ChoiceType;
+
+typedef struct ChoiceMetadata {
+  const char * const * values;
+  const int valueSize;
+  const char *defaultValue;
+  const ChoiceType choiceType;
+} ChoiceMetadata;
+
+typedef struct FieldMetadata {
+  const char * id;
+  FieldType fieldType;
+  const void * specific;
+} FieldMetadata;
+
+typedef struct RowMetadata {
+  const FieldMetadata *fields;
+  const int fieldSize;
+} RowMetadata;
+
+typedef struct FormMetadata {
+  const char *windowTitle;
+  const char *iconPath;
+  const FieldMetadata *fields;
+  const int fieldSize;
+} FormMetadata;
+
+typedef struct ValuePair {
+  const char *id;
+  const char *value;
+} ValuePair;
+
+// SEARCH
+
+typedef struct SearchItem {
+  const char *id;
+  const char *label;
+  const char *trigger;
+} SearchItem;
+
+typedef struct SearchResults {
+  const SearchItem * items;
+  const int itemSize;
+} SearchResults;
+
+typedef struct SearchMetadata {
+  const char *windowTitle;
+  const char *iconPath;
+  const char *hintText;
+} SearchMetadata;
+
+// WIZARD
+
+const int MIGRATE_RESULT_SUCCESS = 0;
+const int MIGRATE_RESULT_CLEAN_FAILURE = 1;
+const int MIGRATE_RESULT_DIRTY_FAILURE = 2;
+const int MIGRATE_RESULT_UNKNOWN_FAILURE = 3;
+
+const int DETECTED_OS_UNKNOWN = 0;
+const int DETECTED_OS_X11 = 1;
+const int DETECTED_OS_WAYLAND = 2;
+
+typedef struct WizardMetadata {
+  const char *version;
+
+  const int is_welcome_page_enabled; 
+  const int is_move_bundle_page_enabled; 
+  const int is_legacy_version_page_enabled; 
+  const int is_wrong_edition_page_enabled; 
+  const int is_migrate_page_enabled; 
+  const int is_auto_start_page_enabled; 
+  const int is_add_path_page_enabled; 
+  const int is_accessibility_page_enabled; 
+
+  const char *window_icon_path;
+  const char *welcome_image_path;
+  const char *accessibility_image_1_path;
+  const char *accessibility_image_2_path;
+  const int detected_os;
+
+  // METHODS
+  int (*is_legacy_version_running)();
+  int (*backup_and_migrate)();
+  int (*auto_start)(int);
+  int (*add_to_path)();
+  int (*enable_accessibility)();
+  int (*is_accessibility_enabled)();
+  void (*on_completed)();
+} WizardMetadata;
+
+// WELCOME
+
+typedef struct WelcomeMetadata {
+  const char *window_icon_path;
+  const char *tray_image_path;
+
+  const int already_running;
+
+  // METHODS
+  int (*dont_show_again_changed)(int);
+} WelcomeMetadata;
+
+// TROUBLESHOOTING
+
+const int ERROR_METADATA_LEVEL_ERROR = 1;
+const int ERROR_METADATA_LEVEL_WARNING = 2;
+typedef struct ErrorMetadata {
+  const int level;
+  const char *message;
+} ErrorMetadata;
+
+typedef struct ErrorSetMetadata {
+  const char *file_path;
+  const ErrorMetadata * errors;
+  const int errors_count;
+} ErrorSetMetadata;
+
+typedef struct TroubleshootingMetadata {
+  const char *window_icon_path;
+
+  const int is_fatal_error;
+
+  const ErrorSetMetadata * error_sets;
+  const int error_sets_count;
+
+  // METHODS
+  int (*dont_show_again_changed)(int);
+  int (*open_file)(const char * file_name);
+} TroubleshootingMetadata;
diff --git a/espanso-modulo/src/sys/interop/mod.rs b/espanso-modulo/src/sys/interop/mod.rs
new file mode 100644
index 00000000..debd2b9e
--- /dev/null
+++ b/espanso-modulo/src/sys/interop/mod.rs
@@ -0,0 +1,223 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::ffi::c_void;
+use std::os::raw::{c_char, c_int};
+
+pub(crate) trait Interoperable {
+  fn as_ptr(&self) -> *const c_void;
+}
+
+pub const FieldType_ROW: FieldType = 0;
+pub const FieldType_LABEL: FieldType = 1;
+pub const FieldType_TEXT: FieldType = 2;
+pub const FieldType_CHOICE: FieldType = 3;
+pub const FieldType_CHECKBOX: FieldType = 4;
+pub type FieldType = i32;
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct LabelMetadata {
+  pub text: *const ::std::os::raw::c_char,
+}
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct TextMetadata {
+  pub defaultText: *const ::std::os::raw::c_char,
+  pub multiline: ::std::os::raw::c_int,
+}
+
+pub const ChoiceType_DROPDOWN: ChoiceType = 0;
+pub const ChoiceType_LIST: ChoiceType = 1;
+pub type ChoiceType = i32;
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct ChoiceMetadata {
+  pub values: *const *const ::std::os::raw::c_char,
+  pub valueSize: ::std::os::raw::c_int,
+  pub defaultValue: *const ::std::os::raw::c_char,
+  pub choiceType: ChoiceType,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct FieldMetadata {
+  pub id: *const ::std::os::raw::c_char,
+  pub fieldType: FieldType,
+  pub specific: *const ::std::os::raw::c_void,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct RowMetadata {
+  pub fields: *const FieldMetadata,
+  pub fieldSize: ::std::os::raw::c_int,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct FormMetadata {
+  pub windowTitle: *const ::std::os::raw::c_char,
+  pub iconPath: *const ::std::os::raw::c_char,
+  pub fields: *const FieldMetadata,
+  pub fieldSize: ::std::os::raw::c_int,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct ValuePair {
+  pub id: *const ::std::os::raw::c_char,
+  pub value: *const ::std::os::raw::c_char,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct SearchItem {
+  pub id: *const ::std::os::raw::c_char,
+  pub label: *const ::std::os::raw::c_char,
+  pub trigger: *const ::std::os::raw::c_char,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct SearchResults {
+  pub items: *const SearchItem,
+  pub itemSize: ::std::os::raw::c_int,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct SearchMetadata {
+  pub windowTitle: *const ::std::os::raw::c_char,
+  pub iconPath: *const ::std::os::raw::c_char,
+  pub hintText: *const ::std::os::raw::c_char,
+}
+
+pub const WIZARD_MIGRATE_RESULT_SUCCESS: i32 = 0;
+pub const WIZARD_MIGRATE_RESULT_CLEAN_FAILURE: i32 = 1;
+pub const WIZARD_MIGRATE_RESULT_DIRTY_FAILURE: i32 = 2;
+pub const WIZARD_MIGRATE_RESULT_UNKNOWN_FAILURE: i32 = 3;
+
+pub const WIZARD_DETECTED_OS_UNKNOWN: i32 = 0;
+pub const WIZARD_DETECTED_OS_X11: i32 = 1;
+pub const WIZARD_DETECTED_OS_WAYLAND: i32 = 2;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct WizardMetadata {
+  pub version: *const c_char,
+
+  pub is_welcome_page_enabled: c_int,
+  pub is_move_bundle_page_enabled: c_int,
+  pub is_legacy_version_page_enabled: c_int,
+  pub is_wrong_edition_page_enabled: c_int,
+  pub is_migrate_page_enabled: c_int,
+  pub is_auto_start_page_enabled: c_int,
+  pub is_add_path_page_enabled: c_int,
+  pub is_accessibility_page_enabled: c_int,
+
+  pub window_icon_path: *const c_char,
+  pub welcome_image_path: *const c_char,
+  pub accessibility_image_1_path: *const c_char,
+  pub accessibility_image_2_path: *const c_char,
+  pub detected_os: c_int,
+
+  pub is_legacy_version_running: extern "C" fn() -> c_int,
+  pub backup_and_migrate: extern "C" fn() -> c_int,
+  pub auto_start: extern "C" fn(auto_start: c_int) -> c_int,
+  pub add_to_path: extern "C" fn() -> c_int,
+  pub enable_accessibility: extern "C" fn() -> c_int,
+  pub is_accessibility_enabled: extern "C" fn() -> c_int,
+  pub on_completed: extern "C" fn(),
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct WelcomeMetadata {
+  pub window_icon_path: *const c_char,
+  pub tray_image_path: *const c_char,
+
+  pub already_running: c_int,
+
+  pub dont_show_again_changed: extern "C" fn(c_int),
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct TroubleshootingMetadata {
+  pub window_icon_path: *const c_char,
+  pub is_fatal_error: c_int,
+
+  pub error_sets: *const ErrorSetMetadata,
+  pub error_sets_count: c_int,
+
+  pub dont_show_again_changed: extern "C" fn(c_int),
+  pub open_file: extern "C" fn(*const c_char),
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct ErrorSetMetadata {
+  pub file_path: *const c_char,
+
+  pub errors: *const ErrorMetadata,
+  pub errors_count: c_int,
+}
+
+pub const ERROR_METADATA_LEVEL_ERROR: c_int = 1;
+pub const ERROR_METADATA_LEVEL_WARNING: c_int = 2;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct ErrorMetadata {
+  pub level: c_int,
+  pub message: *const c_char,
+}
+
+// Native bindings
+
+#[allow(improper_ctypes)]
+#[link(name = "espansomodulosys", kind = "static")]
+extern "C" {
+  // FORM
+  pub(crate) fn interop_show_form(
+    metadata: *const FormMetadata,
+    callback: extern "C" fn(values: *const ValuePair, size: c_int, map: *mut c_void),
+    map: *mut c_void,
+  );
+
+  // SEARCH
+  pub(crate) fn interop_show_search(
+    metadata: *const SearchMetadata,
+    search_callback: extern "C" fn(query: *const c_char, app: *const c_void, data: *const c_void),
+    items: *const c_void,
+    result_callback: extern "C" fn(id: *const c_char, result: *mut c_void),
+    result: *mut c_void,
+  );
+
+  pub(crate) fn update_items(app: *const c_void, items: *const SearchItem, itemCount: c_int);
+
+  // WIZARD
+  pub(crate) fn interop_show_wizard(metadata: *const WizardMetadata) -> c_int;
+
+  // WELCOME
+  pub(crate) fn interop_show_welcome(metadata: *const WelcomeMetadata);
+
+  // TROUBLESHOOTING
+  pub(crate) fn interop_show_troubleshooting(metadata: *const TroubleshootingMetadata);
+}
diff --git a/espanso-modulo/src/sys/mod.rs b/espanso-modulo/src/sys/mod.rs
new file mode 100644
index 00000000..d69b485a
--- /dev/null
+++ b/espanso-modulo/src/sys/mod.rs
@@ -0,0 +1,31 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub mod form;
+pub mod search;
+pub mod troubleshooting;
+pub mod welcome;
+pub mod wizard;
+
+#[allow(non_upper_case_globals)]
+#[allow(dead_code)]
+#[allow(non_snake_case)]
+pub mod interop;
+
+mod util;
diff --git a/espanso-modulo/src/sys/search/mod.rs b/espanso-modulo/src/sys/search/mod.rs
new file mode 100644
index 00000000..de448369
--- /dev/null
+++ b/espanso-modulo/src/sys/search/mod.rs
@@ -0,0 +1,210 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::ffi::CStr;
+use std::os::raw::{c_char, c_int, c_void};
+
+pub mod types {
+  #[derive(Debug)]
+  pub struct SearchItem {
+    pub id: String,
+    pub label: String,
+    pub trigger: Option<String>,
+    pub is_builtin: bool,
+  }
+
+  #[derive(Debug)]
+  pub struct Search {
+    pub title: String,
+    pub icon: Option<String>,
+    pub hint: Option<String>,
+    pub items: Vec<SearchItem>,
+  }
+}
+
+#[allow(dead_code)]
+mod interop {
+  use super::super::interop::*;
+  use super::types;
+  use std::ffi::{c_void, CString};
+
+  pub(crate) struct OwnedSearch {
+    title: CString,
+    icon_path: CString,
+    hint: CString,
+    items: Vec<OwnedSearchItem>,
+    pub(crate) interop_items: Vec<SearchItem>,
+    _interop: Box<SearchMetadata>,
+  }
+
+  impl Interoperable for OwnedSearch {
+    fn as_ptr(&self) -> *const c_void {
+      &(*self._interop) as *const SearchMetadata as *const c_void
+    }
+  }
+
+  impl From<&types::Search> for OwnedSearch {
+    fn from(search: &types::Search) -> Self {
+      let title =
+        CString::new(search.title.clone()).expect("unable to convert search title to CString");
+
+      let items: Vec<OwnedSearchItem> = search.items.iter().map(|item| item.into()).collect();
+
+      let interop_items: Vec<SearchItem> = items.iter().map(|item| item.to_search_item()).collect();
+
+      let icon_path = if let Some(icon_path) = search.icon.as_ref() {
+        icon_path.clone()
+      } else {
+        "".to_owned()
+      };
+
+      let icon_path = CString::new(icon_path).expect("unable to convert search icon to CString");
+
+      let icon_path_ptr = if search.icon.is_some() {
+        icon_path.as_ptr()
+      } else {
+        std::ptr::null()
+      };
+
+      let hint = if let Some(hint) = search.hint.as_ref() {
+        hint.clone()
+      } else {
+        "".to_owned()
+      };
+
+      let hint = CString::new(hint).expect("unable to convert search icon to CString");
+
+      let hint_ptr = if search.hint.is_some() {
+        hint.as_ptr()
+      } else {
+        std::ptr::null()
+      };
+
+      let _interop = Box::new(SearchMetadata {
+        iconPath: icon_path_ptr,
+        windowTitle: title.as_ptr(),
+        hintText: hint_ptr,
+      });
+
+      Self {
+        title,
+        icon_path,
+        hint,
+        items,
+        interop_items,
+        _interop,
+      }
+    }
+  }
+
+  pub(crate) struct OwnedSearchItem {
+    id: CString,
+    label: CString,
+    trigger: CString,
+  }
+
+  impl OwnedSearchItem {
+    fn to_search_item(&self) -> SearchItem {
+      SearchItem {
+        id: self.id.as_ptr(),
+        label: self.label.as_ptr(),
+        trigger: self.trigger.as_ptr(),
+      }
+    }
+  }
+
+  impl From<&types::SearchItem> for OwnedSearchItem {
+    fn from(item: &types::SearchItem) -> Self {
+      let id = CString::new(item.id.clone()).expect("unable to convert item id to CString");
+      let label =
+        CString::new(item.label.clone()).expect("unable to convert item label to CString");
+
+      let trigger = if let Some(trigger) = item.trigger.as_deref() {
+        CString::new(trigger.to_string()).expect("unable to convert item trigger to CString")
+      } else {
+        CString::new("".to_string()).expect("unable to convert item trigger to CString")
+      };
+
+      Self { id, label, trigger }
+    }
+  }
+}
+
+struct SearchData {
+  owned_search: interop::OwnedSearch,
+  items: Vec<types::SearchItem>,
+  algorithm: Box<dyn Fn(&str, &[types::SearchItem]) -> Vec<usize>>,
+}
+
+pub fn show(
+  search: types::Search,
+  algorithm: Box<dyn Fn(&str, &[types::SearchItem]) -> Vec<usize>>,
+) -> Option<String> {
+  use super::interop::*;
+
+  let owned_search: interop::OwnedSearch = (&search).into();
+  let metadata: *const SearchMetadata = owned_search.as_ptr() as *const SearchMetadata;
+
+  let search_data = SearchData {
+    owned_search,
+    items: search.items,
+    algorithm,
+  };
+
+  extern "C" fn search_callback(query: *const c_char, app: *const c_void, data: *const c_void) {
+    let query = unsafe { CStr::from_ptr(query) };
+    let query = query.to_string_lossy().to_string();
+
+    let search_data = data as *const SearchData;
+    let search_data = unsafe { &*search_data };
+
+    let indexes = (*search_data.algorithm)(&query, &search_data.items);
+    let items: Vec<SearchItem> = indexes
+      .into_iter()
+      .map(|index| search_data.owned_search.interop_items[index])
+      .collect();
+
+    unsafe {
+      update_items(app, items.as_ptr(), items.len() as c_int);
+    }
+  }
+
+  let mut result: Option<String> = None;
+
+  extern "C" fn result_callback(id: *const c_char, result: *mut c_void) {
+    let id = unsafe { CStr::from_ptr(id) };
+    let id = id.to_string_lossy().to_string();
+    let result: *mut Option<String> = result as *mut Option<String>;
+    unsafe {
+      *result = Some(id);
+    }
+  }
+
+  unsafe {
+    interop_show_search(
+      metadata,
+      search_callback,
+      &search_data as *const SearchData as *const c_void,
+      result_callback,
+      &mut result as *mut Option<String> as *mut c_void,
+    );
+  }
+
+  result
+}
diff --git a/espanso-modulo/src/sys/search/search.cpp b/espanso-modulo/src/sys/search/search.cpp
new file mode 100644
index 00000000..b514e951
--- /dev/null
+++ b/espanso-modulo/src/sys/search/search.cpp
@@ -0,0 +1,488 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// Mouse dragging mechanism greatly inspired by: https://developpaper.com/wxwidgets-implementing-the-drag-effect-of-titleless-bar-window/
+
+#define _UNICODE
+
+#include "../common/common.h"
+#include "../interop/interop.h"
+
+#include "wx/htmllbox.h"
+
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+// Platform-specific styles
+#ifdef __WXMSW__
+const int SEARCH_BAR_FONT_SIZE = 16;
+const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW;
+#endif
+#ifdef __WXOSX__
+const int SEARCH_BAR_FONT_SIZE = 20;
+const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxRESIZE_BORDER;
+#endif
+#ifdef __LINUX__
+const int SEARCH_BAR_FONT_SIZE = 20;
+const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxBORDER_NONE;
+#endif
+
+const int HELP_TEXT_FONT_SIZE = 10;
+
+const wxColour SELECTION_LIGHT_BG = wxColour(164, 210, 253);
+const wxColour SELECTION_DARK_BG = wxColour(49, 88, 126);
+
+// https://docs.wxwidgets.org/stable/classwx_frame.html
+const int MIN_WIDTH = 500;
+const int MIN_HEIGHT = 80;
+
+typedef void (*QueryCallback)(const char *query, void *app, void *data);
+typedef void (*ResultCallback)(const char *id, void *data);
+
+SearchMetadata *searchMetadata = nullptr;
+QueryCallback queryCallback = nullptr;
+ResultCallback resultCallback = nullptr;
+void *data = nullptr;
+void *resultData = nullptr;
+wxArrayString wxItems;
+wxArrayString wxTriggers;
+wxArrayString wxIds;
+
+// App Code
+
+class SearchApp : public wxApp
+{
+public:
+    virtual bool OnInit();
+};
+
+class ResultListBox : public wxHtmlListBox
+{
+public:
+    ResultListBox() {}
+    ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size);
+
+protected:
+    // override this method to return data to be shown in the listbox (this is
+    // mandatory)
+    virtual wxString OnGetItem(size_t n) const;
+
+    // change the appearance by overriding these functions (this is optional)
+    virtual void OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const;
+
+    bool isDark;
+
+public:
+    wxDECLARE_NO_COPY_CLASS(ResultListBox);
+    wxDECLARE_DYNAMIC_CLASS(ResultListBox);
+};
+
+wxIMPLEMENT_DYNAMIC_CLASS(ResultListBox, wxHtmlListBox);
+
+ResultListBox::ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size)
+    : wxHtmlListBox(parent, id, pos, size, 0)
+{
+    this->isDark = isDark;
+    SetMargins(5, 5);
+    Refresh();
+}
+
+void ResultListBox::OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const
+{
+    if (IsSelected(n))
+    {
+        if (isDark)
+        {
+            dc.SetBrush(wxBrush(SELECTION_DARK_BG));
+        }
+        else
+        {
+            dc.SetBrush(wxBrush(SELECTION_LIGHT_BG));
+        }
+    }
+    else
+    {
+        dc.SetBrush(*wxTRANSPARENT_BRUSH);
+    }
+    dc.SetPen(*wxTRANSPARENT_PEN);
+    dc.DrawRectangle(0, 0, rect.GetRight(), rect.GetBottom());
+}
+
+wxString ResultListBox::OnGetItem(size_t n) const
+{
+    wxString textColor = isDark ? "white" : "";
+    wxString shortcut = (n < 8) ? wxString::Format(wxT("Alt+%i"), (int)n + 1) : " ";
+    return wxString::Format(wxT("<font color='%s'><table width='100%%'><tr><td>%s</td><td align='right'><b>%s</b> <font color='#636e72'> %s</font></td></tr></table></font>"), textColor, wxItems[n], wxTriggers[n], shortcut);
+}
+
+class SearchFrame : public wxFrame
+{
+public:
+    SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size);
+
+    wxPanel *panel;
+    wxTextCtrl *searchBar;
+    wxStaticBitmap *iconPanel;
+    wxStaticText *helpText;
+    ResultListBox *resultBox;
+    void SetItems(SearchItem *items, int itemSize);
+
+private:
+    void OnCharEvent(wxKeyEvent &event);
+    void OnQueryChange(wxCommandEvent &event);
+    void OnItemClickEvent(wxCommandEvent &event);
+    void OnActivate(wxActivateEvent &event);
+
+    // Mouse events
+    void OnMouseCaptureLost(wxMouseCaptureLostEvent &event);
+    void OnMouseLeave(wxMouseEvent &event);
+    void OnMouseMove(wxMouseEvent &event);
+    void OnMouseLUp(wxMouseEvent &event);
+    void OnMouseLDown(wxMouseEvent &event);
+    wxPoint mLastPt;
+
+    // Selection
+    void SelectNext();
+    void SelectPrevious();
+    void Submit();
+};
+
+bool SearchApp::OnInit()
+{
+    SearchFrame *frame = new SearchFrame(searchMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340));
+    frame->Show(true);
+    SetupWindowStyle(frame);
+    Activate(frame);
+    return true;
+}
+SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size)
+    : wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
+{
+    wxInitAllImageHandlers();
+
+#if wxCHECK_VERSION(3, 1, 3)
+    bool isDark = wxSystemSettings::GetAppearance().IsDark();
+#else
+    // Workaround needed for previous versions of wxWidgets
+    const wxColour bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+    const wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
+    unsigned int bgSum = (bg.Red() + bg.Blue() + bg.Green());
+    unsigned int fgSum = (fg.Red() + fg.Blue() + fg.Green());
+    bool isDark = fgSum > bgSum;
+#endif
+
+    panel = new wxPanel(this, wxID_ANY);
+    wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
+    panel->SetSizer(vbox);
+
+    wxBoxSizer *topBox = new wxBoxSizer(wxHORIZONTAL);
+
+    int iconId = NewControlId();
+    iconPanel = nullptr;
+    if (searchMetadata->iconPath)
+    {
+        wxString iconPath = wxString(searchMetadata->iconPath);
+        if (wxFileExists(iconPath))
+        {
+            wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG);
+            if (bitmap.IsOk())
+            {
+                wxImage image = bitmap.ConvertToImage();
+                image.Rescale(32, 32, wxIMAGE_QUALITY_HIGH);
+                wxBitmap resizedBitmap = wxBitmap(image, -1);
+                iconPanel = new wxStaticBitmap(panel, iconId, resizedBitmap, wxDefaultPosition, wxSize(32, 32));
+                topBox->Add(iconPanel, 0, wxEXPAND | wxLEFT | wxUP | wxDOWN, 10);
+            }
+        }
+    }
+
+    int textId = NewControlId();
+    searchBar = new wxTextCtrl(panel, textId, "", wxDefaultPosition, wxDefaultSize);
+    wxFont font = searchBar->GetFont();
+    font.SetPointSize(SEARCH_BAR_FONT_SIZE);
+    searchBar->SetFont(font);
+    topBox->Add(searchBar, 1, wxEXPAND | wxALL, 10);
+
+    vbox->Add(topBox, 1, wxEXPAND);
+
+    if (searchMetadata->hintText) {
+        helpText = new wxStaticText(panel, wxID_ANY, wxString::FromUTF8(searchMetadata->hintText));
+        vbox->Add(helpText, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
+        wxFont helpFont = helpText->GetFont();
+        helpFont.SetPointSize(HELP_TEXT_FONT_SIZE);
+        helpText->SetFont(helpFont);
+    }
+
+    wxArrayString choices;
+    int resultId = NewControlId();
+    resultBox = new ResultListBox(panel, isDark, resultId, wxDefaultPosition, wxSize(MIN_WIDTH, MIN_HEIGHT));
+    vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0);
+
+    Bind(wxEVT_CHAR_HOOK, &SearchFrame::OnCharEvent, this, wxID_ANY);
+    Bind(wxEVT_TEXT, &SearchFrame::OnQueryChange, this, textId);
+    Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId);
+    Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY);
+
+    // Events to handle the mouse drag
+    if (iconPanel)
+    {
+        iconPanel->Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this);
+        iconPanel->Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this);
+        Bind(wxEVT_MOTION, &SearchFrame::OnMouseMove, this);
+        Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this);
+        Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this);
+        Bind(wxEVT_MOUSE_CAPTURE_LOST, &SearchFrame::OnMouseCaptureLost, this);
+        Bind(wxEVT_LEAVE_WINDOW, &SearchFrame::OnMouseLeave, this);
+    }
+
+    this->SetClientSize(panel->GetBestSize());
+    this->CentreOnScreen();
+
+    // Trigger the first data update
+    queryCallback("", (void *)this, data);
+}
+
+void SearchFrame::OnCharEvent(wxKeyEvent &event)
+{
+    if (event.GetKeyCode() == WXK_ESCAPE)
+    {
+        Close(true);
+    }
+    else if (event.GetKeyCode() == WXK_TAB)
+    {
+        if (wxGetKeyState(WXK_SHIFT))
+        {
+            SelectPrevious();
+        }
+        else
+        {
+            SelectNext();
+        }
+    }
+    else if (event.GetKeyCode() >= 49 && event.GetKeyCode() <= 56)
+    { // Alt + num shortcut
+        int index = event.GetKeyCode() - 49;
+        if (wxGetKeyState(WXK_ALT))
+        {
+            if (resultBox->GetItemCount() > index)
+            {
+                resultBox->SetSelection(index);
+                Submit();
+            }
+        } else {
+            event.Skip();
+        }
+    }
+    else if (event.GetKeyCode() == WXK_DOWN)
+    {
+        SelectNext();
+    }
+    else if (event.GetKeyCode() == WXK_UP)
+    {
+        SelectPrevious();
+    }
+    else if (event.GetKeyCode() == WXK_RETURN)
+    {
+        Submit();
+    }
+    else
+    {
+        event.Skip();
+    }
+}
+
+void SearchFrame::OnQueryChange(wxCommandEvent &event)
+{
+    if (helpText != nullptr) {
+        helpText->Destroy();
+        panel->Layout();
+        helpText = nullptr;
+    }
+    
+    wxString queryString = searchBar->GetValue();
+    const char *query = queryString.ToUTF8();
+    queryCallback(query, (void *)this, data);
+}
+
+void SearchFrame::OnItemClickEvent(wxCommandEvent &event)
+{
+    resultBox->SetSelection(event.GetInt());
+    Submit();
+}
+
+void SearchFrame::OnActivate(wxActivateEvent &event)
+{
+    if (!event.GetActive())
+    {
+        Close(true);
+    }
+    event.Skip();
+}
+
+void SearchFrame::OnMouseMove(wxMouseEvent &event)
+{
+    if (event.LeftIsDown() && event.Dragging())
+    {
+        wxPoint pt = event.GetPosition();
+        wxPoint wndLeftTopPt = GetPosition();
+        int distanceX = pt.x - mLastPt.x;
+        int distanceY = pt.y - mLastPt.y;
+
+        wxPoint desPt;
+        desPt.x = distanceX + wndLeftTopPt.x - 24;
+        desPt.y = distanceY + wndLeftTopPt.y - 24;
+        this->Move(desPt);
+    }
+
+    if (event.LeftDown())
+    {
+        this->CaptureMouse();
+        mLastPt = event.GetPosition();
+    }
+}
+
+void SearchFrame::OnMouseLeave(wxMouseEvent &event)
+{
+    if (event.LeftIsDown() && event.Dragging())
+    {
+        wxPoint pt = event.GetPosition();
+        wxPoint wndLeftTopPt = GetPosition();
+        int distanceX = pt.x - mLastPt.x;
+        int distanceY = pt.y - mLastPt.y;
+
+        wxPoint desPt;
+        desPt.x = distanceX + wndLeftTopPt.x - 24;
+        desPt.y = distanceY + wndLeftTopPt.y - 24;
+        this->Move(desPt);
+    }
+}
+
+void SearchFrame::OnMouseLDown(wxMouseEvent &event)
+{
+    if (!HasCapture())
+        this->CaptureMouse();
+}
+
+void SearchFrame::OnMouseLUp(wxMouseEvent &event)
+{
+    if (HasCapture())
+        ReleaseMouse();
+}
+
+void SearchFrame::OnMouseCaptureLost(wxMouseCaptureLostEvent &event)
+{
+    if (HasCapture())
+        ReleaseMouse();
+}
+
+void SearchFrame::SetItems(SearchItem *items, int itemSize)
+{
+    wxItems.Clear();
+    wxIds.Clear();
+    wxTriggers.Clear();
+
+    for (int i = 0; i < itemSize; i++)
+    {
+        wxString item = wxString::FromUTF8(items[i].label);
+        wxItems.Add(item);
+
+        wxString id = wxString::FromUTF8(items[i].id);
+        wxIds.Add(id);
+
+        wxString trigger = wxString::FromUTF8(items[i].trigger);
+        wxTriggers.Add(trigger);
+    }
+
+    resultBox->SetItemCount(itemSize);
+
+    if (itemSize > 0)
+    {
+        resultBox->SetSelection(0);
+    }
+    resultBox->RefreshAll();
+    resultBox->Refresh();
+}
+
+void SearchFrame::SelectNext()
+{
+    if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
+    {
+        int newSelected = 0;
+        if (resultBox->GetSelection() < (resultBox->GetItemCount() - 1))
+        {
+            newSelected = resultBox->GetSelection() + 1;
+        }
+
+        resultBox->SetSelection(newSelected);
+    }
+}
+
+void SearchFrame::SelectPrevious()
+{
+    if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
+    {
+        int newSelected = resultBox->GetItemCount() - 1;
+        if (resultBox->GetSelection() > 0)
+        {
+            newSelected = resultBox->GetSelection() - 1;
+        }
+
+        resultBox->SetSelection(newSelected);
+    }
+}
+
+void SearchFrame::Submit()
+{
+    if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
+    {
+        long index = resultBox->GetSelection();
+        wxString id = wxIds[index];
+        if (resultCallback)
+        {
+            resultCallback(id.ToUTF8(), resultData);
+        }
+
+        Close(true);
+    }
+}
+
+extern "C" void interop_show_search(SearchMetadata *_metadata, QueryCallback _queryCallback, void *_data, ResultCallback _resultCallback, void *_resultData)
+{
+// Setup high DPI support on Windows
+#ifdef __WXMSW__
+    SetProcessDPIAware();
+#endif
+
+    searchMetadata = _metadata;
+    queryCallback = _queryCallback;
+    resultCallback = _resultCallback;
+    data = _data;
+    resultData = _resultData;
+
+    wxApp::SetInstance(new SearchApp());
+    int argc = 0;
+    wxEntry(argc, (char **)nullptr);
+}
+
+extern "C" void update_items(void *app, SearchItem *items, int itemSize)
+{
+    SearchFrame *frame = (SearchFrame *)app;
+    frame->SetItems(items, itemSize);
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/troubleshooting/mod.rs b/espanso-modulo/src/sys/troubleshooting/mod.rs
new file mode 100644
index 00000000..c303f5d0
--- /dev/null
+++ b/espanso-modulo/src/sys/troubleshooting/mod.rs
@@ -0,0 +1,171 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::ffi::CStr;
+use std::os::raw::{c_char, c_int};
+use std::path::PathBuf;
+use std::sync::Mutex;
+
+use crate::sys::interop::{ErrorSetMetadata, TroubleshootingMetadata};
+use crate::sys::troubleshooting::interop::OwnedErrorSet;
+use crate::sys::util::convert_to_cstring_or_null;
+use crate::troubleshooting::{TroubleshootingHandlers, TroubleshootingOptions};
+use anyhow::Result;
+
+lazy_static! {
+  static ref HANDLERS: Mutex<Option<TroubleshootingHandlers>> = Mutex::new(None);
+}
+
+#[allow(dead_code)]
+mod interop {
+  use crate::troubleshooting::{ErrorRecord, ErrorSet};
+
+  use super::interop::{ErrorMetadata, ErrorSetMetadata};
+
+  use super::super::interop::*;
+  use std::{ffi::CString, os::raw::c_int};
+
+  pub(crate) struct OwnedErrorSet {
+    file_path: Option<CString>,
+    errors: Vec<OwnedErrorMetadata>,
+    pub(crate) _interop_errors: Vec<ErrorMetadata>,
+  }
+
+  impl OwnedErrorSet {
+    pub fn to_error_set_metadata(&self) -> ErrorSetMetadata {
+      let file_path_ptr = if let Some(file_path) = self.file_path.as_ref() {
+        file_path.as_ptr()
+      } else {
+        std::ptr::null()
+      };
+
+      ErrorSetMetadata {
+        file_path: file_path_ptr,
+        errors: self._interop_errors.as_ptr(),
+        errors_count: self._interop_errors.len() as c_int,
+      }
+    }
+  }
+
+  impl From<&ErrorSet> for OwnedErrorSet {
+    fn from(error_set: &ErrorSet) -> Self {
+      let file_path = error_set.file.as_ref().map(|file_path| {
+        CString::new(file_path.to_string_lossy().to_string())
+          .expect("unable to convert file_path to CString")
+      });
+
+      let errors: Vec<OwnedErrorMetadata> =
+        error_set.errors.iter().map(|item| item.into()).collect();
+
+      let _interop_errors: Vec<ErrorMetadata> =
+        errors.iter().map(|item| item.to_error_metadata()).collect();
+
+      Self {
+        file_path,
+        errors,
+        _interop_errors,
+      }
+    }
+  }
+
+  pub(crate) struct OwnedErrorMetadata {
+    level: c_int,
+    message: CString,
+  }
+
+  impl OwnedErrorMetadata {
+    fn to_error_metadata(&self) -> ErrorMetadata {
+      ErrorMetadata {
+        level: self.level,
+        message: self.message.as_ptr(),
+      }
+    }
+  }
+
+  impl From<&ErrorRecord> for OwnedErrorMetadata {
+    fn from(item: &ErrorRecord) -> Self {
+      let message =
+        CString::new(item.message.clone()).expect("unable to convert item message to CString");
+
+      Self {
+        level: match item.level {
+          crate::troubleshooting::ErrorLevel::Error => ERROR_METADATA_LEVEL_ERROR,
+          crate::troubleshooting::ErrorLevel::Warning => ERROR_METADATA_LEVEL_WARNING,
+        },
+        message,
+      }
+    }
+  }
+}
+
+pub fn show(options: TroubleshootingOptions) -> Result<()> {
+  let (_c_window_icon_path, c_window_icon_path_ptr) =
+    convert_to_cstring_or_null(options.window_icon_path);
+
+  let owned_error_sets: Vec<OwnedErrorSet> =
+    options.error_sets.iter().map(|set| set.into()).collect();
+  let error_sets: Vec<ErrorSetMetadata> = owned_error_sets
+    .iter()
+    .map(|set| set.to_error_set_metadata())
+    .collect();
+
+  extern "C" fn dont_show_again_changed(dont_show: c_int) {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in dont_show_again_changed method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.dont_show_again_changed.as_ref() {
+      let value = dont_show == 1;
+      (*handler_ref)(value);
+    }
+  }
+
+  extern "C" fn open_file(file_path: *const c_char) {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in open_file method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.open_file.as_ref() {
+      let c_string = unsafe { CStr::from_ptr(file_path) };
+      let string = c_string.to_string_lossy();
+      let path = PathBuf::from(string.to_string());
+      (*handler_ref)(&path);
+    }
+  }
+
+  {
+    let mut lock = HANDLERS.lock().expect("unable to acquire handlers lock");
+    *lock = Some(options.handlers)
+  }
+
+  let troubleshooting_metadata = TroubleshootingMetadata {
+    window_icon_path: c_window_icon_path_ptr,
+    is_fatal_error: if options.is_fatal_error { 1 } else { 0 },
+    error_sets: error_sets.as_ptr(),
+    error_sets_count: error_sets.len() as c_int,
+    dont_show_again_changed,
+    open_file,
+  };
+
+  unsafe {
+    super::interop::interop_show_troubleshooting(&troubleshooting_metadata);
+  }
+
+  Ok(())
+}
diff --git a/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp b/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp
new file mode 100644
index 00000000..f34bd754
--- /dev/null
+++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting.cpp
@@ -0,0 +1,196 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#define _UNICODE
+
+#include "../common/common.h"
+#include "../interop/interop.h"
+#include "./troubleshooting_gui.h"
+
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+TroubleshootingMetadata *troubleshooting_metadata = nullptr;
+
+// App Code
+
+class TroubleshootingApp : public wxApp
+{
+public:
+  virtual bool OnInit();
+};
+
+// Custom controller to display an ErrorSet
+
+class ErrorSetPanel : public wxPanel
+{
+private:
+protected:
+  wxStaticText *filename_label;
+  wxButton *open_file_btn;
+  wxTextCtrl *error_text_ctrl;
+  const ErrorSetMetadata * error_set_metadata;
+
+public:
+  ErrorSetPanel(wxWindow *parent, const ErrorSetMetadata * error_set_metadata) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL)
+  {
+    this->error_set_metadata = error_set_metadata;
+
+    wxBoxSizer *main_file_sizer;
+    main_file_sizer = new wxBoxSizer(wxVERTICAL);
+    main_file_sizer->SetMinSize(0, 150);
+
+    wxBoxSizer *header_sizer;
+    header_sizer = new wxBoxSizer(wxHORIZONTAL);
+
+    wxString path = wxString::FromUTF8(error_set_metadata->file_path);
+    wxString filename = wxString::Format(wxT("\u2022 %s (%i errors)"), path, error_set_metadata->errors_count);
+    filename_label = new wxStaticText(this, wxID_ANY, filename, wxDefaultPosition, wxDefaultSize, 0);
+    filename_label->Wrap(-1);
+    filename_label->SetFont(wxFont(wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString));
+
+    header_sizer->Add(filename_label, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
+
+    header_sizer->Add(0, 0, 1, wxEXPAND, 5);
+
+    open_file_btn = new wxButton(this, wxID_ANY, wxT("Open file"), wxDefaultPosition, wxDefaultSize, 0);
+    header_sizer->Add(open_file_btn, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
+
+    main_file_sizer->Add(header_sizer, 0, wxEXPAND, 5);
+
+    wxString errors_text = wxEmptyString;
+    for (int i = 0; i<error_set_metadata->errors_count; i++) {
+      wxString level = wxT("ERROR");
+      if (error_set_metadata->errors[i].level == ERROR_METADATA_LEVEL_WARNING) {
+        level = wxT("WARNING");
+      }
+      wxString error_text = wxString::Format(wxT("[%s] %s\n"), level, error_set_metadata->errors[i].message);
+      errors_text.Append(error_text);
+    }
+
+    error_text_ctrl = new wxTextCtrl(this, wxID_ANY, errors_text, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
+
+    main_file_sizer->Add(error_text_ctrl, 1, wxALL | wxEXPAND, 5);
+
+    this->SetSizer(main_file_sizer);
+    this->Layout();
+    main_file_sizer->Fit(this);
+
+    if (!this->error_set_metadata->file_path) {
+      filename_label->Hide();
+      open_file_btn->Hide();
+    }
+
+    open_file_btn->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(ErrorSetPanel::on_open_file), NULL, this);
+  }
+
+  void on_open_file(wxCommandEvent &event)
+  {
+    if (troubleshooting_metadata->open_file && this->error_set_metadata->file_path) {
+      troubleshooting_metadata->open_file(this->error_set_metadata->file_path);
+    }
+  }
+
+  ~ErrorSetPanel()
+  {
+    open_file_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(ErrorSetPanel::on_open_file), NULL, this);
+  }
+};
+
+// Frame
+
+class DerivedTroubleshootingFrame : public TroubleshootingFrame
+{
+protected:
+  void on_dont_show_change(wxCommandEvent &event);
+  void on_ignore(wxCommandEvent &event);
+
+public:
+  DerivedTroubleshootingFrame(wxWindow *parent);
+};
+
+DerivedTroubleshootingFrame::DerivedTroubleshootingFrame(wxWindow *parent)
+    : TroubleshootingFrame(parent)
+{
+  if (troubleshooting_metadata->error_sets_count == 0) {
+    dont_show_checkbox->Hide();
+    ignore_button->Hide();
+    info_label->SetLabel(wxT("Looks like your configuration is correct!"));
+    title_label->SetLabel(wxT("No errors detected"));
+  } else if (troubleshooting_metadata->is_fatal_error) {
+    dont_show_checkbox->Hide();
+    ignore_button->Hide();
+    info_label->SetLabel(wxT("Espanso couldn't load some files due to configuration errors and won't be able to start until you fix them."));
+    title_label->SetLabel(wxT("Errors detected, action needed"));
+  }
+
+  // If there is just one error, expand the error panel to fit the container
+  int error_set_proportion = troubleshooting_metadata->error_sets_count == 1 ? 1 : 0;
+
+  for (int i = 0; i<troubleshooting_metadata->error_sets_count; i++) {
+    const ErrorSetMetadata * metadata = &troubleshooting_metadata->error_sets[i];
+    ErrorSetPanel *panel = new ErrorSetPanel(scrollview, metadata);
+    this->scrollview_sizer->Add(panel, error_set_proportion, wxEXPAND | wxALL, 5);
+  }
+}
+
+void DerivedTroubleshootingFrame::on_dont_show_change(wxCommandEvent &event)
+{
+  if (troubleshooting_metadata->dont_show_again_changed)
+  {
+    int value = this->dont_show_checkbox->IsChecked() ? 1 : 0;
+    troubleshooting_metadata->dont_show_again_changed(value);
+  }
+}
+
+void DerivedTroubleshootingFrame::on_ignore(wxCommandEvent &event)
+{
+  Close(true);
+}
+
+bool TroubleshootingApp::OnInit()
+{
+  DerivedTroubleshootingFrame *frame = new DerivedTroubleshootingFrame(NULL);
+
+  if (troubleshooting_metadata->window_icon_path)
+  {
+    setFrameIcon(troubleshooting_metadata->window_icon_path, frame);
+  }
+
+  frame->Show(true);
+
+  Activate(frame);
+
+  return true;
+}
+
+extern "C" void interop_show_troubleshooting(TroubleshootingMetadata *_metadata)
+{
+// Setup high DPI support on Windows
+#ifdef __WXMSW__
+  SetProcessDPIAware();
+#endif
+
+  troubleshooting_metadata = _metadata;
+
+  wxApp::SetInstance(new TroubleshootingApp());
+  int argc = 0;
+  wxEntry(argc, (char **)nullptr);
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/troubleshooting/troubleshooting.fbp b/espanso-modulo/src/sys/troubleshooting/troubleshooting.fbp
new file mode 100644
index 00000000..ab412c22
--- /dev/null
+++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting.fbp
@@ -0,0 +1,479 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<wxFormBuilder_Project>
+    <FileVersion major="1" minor="15" />
+    <object class="Project" expanded="1">
+        <property name="class_decoration">; </property>
+        <property name="code_generation">C++</property>
+        <property name="disconnect_events">1</property>
+        <property name="disconnect_mode">source_name</property>
+        <property name="disconnect_php_events">0</property>
+        <property name="disconnect_python_events">0</property>
+        <property name="embedded_files_path">res</property>
+        <property name="encoding">UTF-8</property>
+        <property name="event_generation">connect</property>
+        <property name="file">troubleshooting_gui</property>
+        <property name="first_id">1000</property>
+        <property name="help_provider">none</property>
+        <property name="indent_with_spaces"></property>
+        <property name="internationalize">0</property>
+        <property name="name">Troubleshooting</property>
+        <property name="namespace"></property>
+        <property name="path">.</property>
+        <property name="precompiled_header">#define _UNICODE</property>
+        <property name="relative_path">1</property>
+        <property name="skip_lua_events">1</property>
+        <property name="skip_php_events">1</property>
+        <property name="skip_python_events">1</property>
+        <property name="ui_table">UI</property>
+        <property name="use_enum">0</property>
+        <property name="use_microsoft_bom">0</property>
+        <object class="Frame" expanded="1">
+            <property name="aui_managed">0</property>
+            <property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
+            <property name="bg">wxSYS_COLOUR_WINDOW</property>
+            <property name="center">wxBOTH</property>
+            <property name="context_help"></property>
+            <property name="context_menu">1</property>
+            <property name="enabled">1</property>
+            <property name="event_handler">impl_virtual</property>
+            <property name="extra_style"></property>
+            <property name="fg"></property>
+            <property name="font"></property>
+            <property name="hidden">0</property>
+            <property name="id">wxID_ANY</property>
+            <property name="maximum_size"></property>
+            <property name="minimum_size"></property>
+            <property name="name">TroubleshootingFrame</property>
+            <property name="pos"></property>
+            <property name="size">841,544</property>
+            <property name="style">wxDEFAULT_FRAME_STYLE</property>
+            <property name="subclass">; ; forward_declare</property>
+            <property name="title">Troubleshooting</property>
+            <property name="tooltip"></property>
+            <property name="window_extra_style"></property>
+            <property name="window_name"></property>
+            <property name="window_style">wxTAB_TRAVERSAL</property>
+            <property name="xrc_skip_sizer">1</property>
+            <object class="wxBoxSizer" expanded="1">
+                <property name="minimum_size"></property>
+                <property name="name">bSizer1</property>
+                <property name="orient">wxVERTICAL</property>
+                <property name="permission">none</property>
+                <object class="sizeritem" expanded="0">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND</property>
+                    <property name="proportion">0</property>
+                    <object class="spacer" expanded="0">
+                        <property name="height">10</property>
+                        <property name="permission">protected</property>
+                        <property name="width">0</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="0">
+                    <property name="border">10</property>
+                    <property name="flag">wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="0">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font">,90,92,20,70,0</property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">Errors detected</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">title_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="0">
+                    <property name="border">10</property>
+                    <property name="flag">wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="0">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">Espanso couldn&apos;t load some files due to configuration errors. Some snippets or settings might not be available until you fix them.</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size">-1,-1</property>
+                        <property name="moveable">1</property>
+                        <property name="name">info_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size">-1,-1</property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND | wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticLine" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">m_staticline1</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style">wxLI_HORIZONTAL</property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND | wxALL</property>
+                    <property name="proportion">5</property>
+                    <object class="wxScrolledWindow" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">scrollview</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="scroll_rate_x">5</property>
+                        <property name="scroll_rate_y">5</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style">wxHSCROLL|wxVSCROLL</property>
+                        <object class="wxBoxSizer" expanded="1">
+                            <property name="minimum_size"></property>
+                            <property name="name">scrollview_sizer</property>
+                            <property name="orient">wxVERTICAL</property>
+                            <property name="permission">protected</property>
+                        </object>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxEXPAND</property>
+                    <property name="proportion">0</property>
+                    <object class="wxBoxSizer" expanded="1">
+                        <property name="minimum_size"></property>
+                        <property name="name">bSizer2</property>
+                        <property name="orient">wxHORIZONTAL</property>
+                        <property name="permission">none</property>
+                        <object class="sizeritem" expanded="0">
+                            <property name="border">10</property>
+                            <property name="flag">wxALIGN_CENTER_VERTICAL|wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxCheckBox" expanded="0">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="checked">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label">Don&apos;t show again for non-critical errors</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">dont_show_checkbox</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="style"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="validator_data_type"></property>
+                                <property name="validator_style">wxFILTER_NONE</property>
+                                <property name="validator_type">wxDefaultValidator</property>
+                                <property name="validator_variable"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                                <event name="OnCheckBox">on_dont_show_change</event>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="0">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND</property>
+                            <property name="proportion">1</property>
+                            <object class="spacer" expanded="0">
+                                <property name="height">0</property>
+                                <property name="permission">protected</property>
+                                <property name="width">0</property>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="0">
+                            <property name="border">10</property>
+                            <property name="flag">wxALIGN_CENTER_VERTICAL|wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxButton" expanded="0">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="bitmap"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="current"></property>
+                                <property name="default">0</property>
+                                <property name="default_pane">0</property>
+                                <property name="disabled"></property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="focus"></property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label">Ignore errors</property>
+                                <property name="margins"></property>
+                                <property name="markup">0</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">ignore_button</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="position"></property>
+                                <property name="pressed"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="style"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="validator_data_type"></property>
+                                <property name="validator_style">wxFILTER_NONE</property>
+                                <property name="validator_type">wxDefaultValidator</property>
+                                <property name="validator_variable"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                                <event name="OnButtonClick">on_ignore</event>
+                            </object>
+                        </object>
+                    </object>
+                </object>
+            </object>
+        </object>
+    </object>
+</wxFormBuilder_Project>
diff --git a/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.cpp b/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.cpp
new file mode 100644
index 00000000..0ce9fc36
--- /dev/null
+++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.cpp
@@ -0,0 +1,80 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#define _UNICODE
+
+#include "troubleshooting_gui.h"
+
+///////////////////////////////////////////////////////////////////////////
+
+TroubleshootingFrame::TroubleshootingFrame( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style )
+{
+	this->SetSizeHints( wxDefaultSize, wxDefaultSize );
+	this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer1;
+	bSizer1 = new wxBoxSizer( wxVERTICAL );
+
+
+	bSizer1->Add( 0, 10, 0, wxEXPAND, 5 );
+
+	title_label = new wxStaticText( this, wxID_ANY, wxT("Errors detected"), wxDefaultPosition, wxDefaultSize, 0 );
+	title_label->Wrap( -1 );
+	title_label->SetFont( wxFont( 20, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer1->Add( title_label, 0, wxALL, 10 );
+
+	info_label = new wxStaticText( this, wxID_ANY, wxT("Espanso couldn't load some files due to configuration errors. Some snippets or settings might not be available until you fix them."), wxDefaultPosition, wxSize( -1,-1 ), 0 );
+	info_label->Wrap( -1 );
+	bSizer1->Add( info_label, 0, wxALL, 10 );
+
+	m_staticline1 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
+	bSizer1->Add( m_staticline1, 0, wxEXPAND | wxALL, 5 );
+
+	scrollview = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL );
+	scrollview->SetScrollRate( 5, 5 );
+	scrollview_sizer = new wxBoxSizer( wxVERTICAL );
+
+
+	scrollview->SetSizer( scrollview_sizer );
+	scrollview->Layout();
+	scrollview_sizer->Fit( scrollview );
+	bSizer1->Add( scrollview, 5, wxEXPAND | wxALL, 5 );
+
+	wxBoxSizer* bSizer2;
+	bSizer2 = new wxBoxSizer( wxHORIZONTAL );
+
+	dont_show_checkbox = new wxCheckBox( this, wxID_ANY, wxT("Don't show again for non-critical errors"), wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer2->Add( dont_show_checkbox, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 );
+
+
+	bSizer2->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	ignore_button = new wxButton( this, wxID_ANY, wxT("Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer2->Add( ignore_button, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 );
+
+
+	bSizer1->Add( bSizer2, 0, wxEXPAND, 10 );
+
+
+	this->SetSizer( bSizer1 );
+	this->Layout();
+
+	this->Centre( wxBOTH );
+
+	// Connect Events
+	dont_show_checkbox->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( TroubleshootingFrame::on_dont_show_change ), NULL, this );
+	ignore_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( TroubleshootingFrame::on_ignore ), NULL, this );
+}
+
+TroubleshootingFrame::~TroubleshootingFrame()
+{
+	// Disconnect Events
+	dont_show_checkbox->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( TroubleshootingFrame::on_dont_show_change ), NULL, this );
+	ignore_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( TroubleshootingFrame::on_ignore ), NULL, this );
+
+}
diff --git a/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.h b/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.h
new file mode 100644
index 00000000..3c637634
--- /dev/null
+++ b/espanso-modulo/src/sys/troubleshooting/troubleshooting_gui.h
@@ -0,0 +1,59 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <wx/artprov.h>
+#include <wx/xrc/xmlres.h>
+#include <wx/string.h>
+#include <wx/stattext.h>
+#include <wx/gdicmn.h>
+#include <wx/font.h>
+#include <wx/colour.h>
+#include <wx/settings.h>
+#include <wx/statline.h>
+#include <wx/sizer.h>
+#include <wx/scrolwin.h>
+#include <wx/checkbox.h>
+#include <wx/bitmap.h>
+#include <wx/image.h>
+#include <wx/icon.h>
+#include <wx/button.h>
+#include <wx/frame.h>
+
+///////////////////////////////////////////////////////////////////////////
+
+
+///////////////////////////////////////////////////////////////////////////////
+/// Class TroubleshootingFrame
+///////////////////////////////////////////////////////////////////////////////
+class TroubleshootingFrame : public wxFrame
+{
+	private:
+
+	protected:
+		wxStaticText* title_label;
+		wxStaticText* info_label;
+		wxStaticLine* m_staticline1;
+		wxScrolledWindow* scrollview;
+		wxBoxSizer* scrollview_sizer;
+		wxCheckBox* dont_show_checkbox;
+		wxButton* ignore_button;
+
+		// Virtual event handlers, overide them in your derived class
+		virtual void on_dont_show_change( wxCommandEvent& event ) { event.Skip(); }
+		virtual void on_ignore( wxCommandEvent& event ) { event.Skip(); }
+
+
+	public:
+
+		TroubleshootingFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Troubleshooting"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 841,544 ), long style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL );
+
+		~TroubleshootingFrame();
+
+};
+
diff --git a/espanso-modulo/src/sys/util.rs b/espanso-modulo/src/sys/util.rs
new file mode 100644
index 00000000..5c3b5cb2
--- /dev/null
+++ b/espanso-modulo/src/sys/util.rs
@@ -0,0 +1,31 @@
+/*
+ * 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::ffi::CString;
+use std::os::raw::c_char;
+
+pub fn convert_to_cstring_or_null(str: Option<String>) -> (Option<CString>, *const c_char) {
+  let c_string =
+    str.map(|str| CString::new(str).expect("unable to convert Option<String> to CString"));
+  let c_ptr = c_string
+    .as_ref()
+    .map_or(std::ptr::null(), |path| path.as_ptr());
+
+  (c_string, c_ptr)
+}
diff --git a/espanso-modulo/src/sys/welcome/mod.rs b/espanso-modulo/src/sys/welcome/mod.rs
new file mode 100644
index 00000000..95570ba6
--- /dev/null
+++ b/espanso-modulo/src/sys/welcome/mod.rs
@@ -0,0 +1,65 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::os::raw::c_int;
+use std::sync::Mutex;
+
+use crate::sys::util::convert_to_cstring_or_null;
+use crate::{
+  sys::interop::WelcomeMetadata,
+  welcome::{WelcomeHandlers, WelcomeOptions},
+};
+
+lazy_static! {
+  static ref HANDLERS: Mutex<Option<WelcomeHandlers>> = Mutex::new(None);
+}
+
+pub fn show(options: WelcomeOptions) {
+  let (_c_window_icon_path, c_window_icon_path_ptr) =
+    convert_to_cstring_or_null(options.window_icon_path);
+  let (_c_tray_image_path, c_tray_image_path_ptr) =
+    convert_to_cstring_or_null(options.tray_image_path);
+
+  extern "C" fn dont_show_again_changed(dont_show: c_int) {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in dont_show_again_changed method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.dont_show_again_changed.as_ref() {
+      let value = dont_show == 1;
+      (*handler_ref)(value);
+    }
+  }
+
+  {
+    let mut lock = HANDLERS.lock().expect("unable to acquire handlers lock");
+    *lock = Some(options.handlers)
+  }
+
+  let welcome_metadata = WelcomeMetadata {
+    window_icon_path: c_window_icon_path_ptr,
+    tray_image_path: c_tray_image_path_ptr,
+    already_running: if options.is_already_running { 1 } else { 0 },
+    dont_show_again_changed,
+  };
+
+  unsafe {
+    super::interop::interop_show_welcome(&welcome_metadata);
+  }
+}
diff --git a/espanso-modulo/src/sys/welcome/welcome.cpp b/espanso-modulo/src/sys/welcome/welcome.cpp
new file mode 100644
index 00000000..d6c02011
--- /dev/null
+++ b/espanso-modulo/src/sys/welcome/welcome.cpp
@@ -0,0 +1,116 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#define _UNICODE
+
+#include "../common/common.h"
+#include "../interop/interop.h"
+#include "./welcome_gui.h"
+
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+WelcomeMetadata *welcome_metadata = nullptr;
+
+// App Code
+
+class WelcomeApp : public wxApp
+{
+public:
+  virtual bool OnInit();
+};
+
+class DerivedWelcomeFrame : public WelcomeFrame
+{
+protected:
+  void on_dont_show_change( wxCommandEvent& event );
+  void on_complete( wxCommandEvent& event );
+
+public:
+  DerivedWelcomeFrame(wxWindow *parent);
+};
+
+DerivedWelcomeFrame::DerivedWelcomeFrame(wxWindow *parent)
+    : WelcomeFrame(parent)
+{
+  // Welcome images
+
+  if (welcome_metadata->tray_image_path)
+  {
+    wxBitmap trayBitmap = wxBitmap(welcome_metadata->tray_image_path, wxBITMAP_TYPE_PNG);
+    this->tray_bitmap->SetBitmap(trayBitmap);
+    #ifdef __WXOSX__
+      this->tray_info_label->SetLabel("You should see the espanso icon on the status bar:");
+    #endif
+  }
+  else 
+  {
+    this->tray_info_label->Hide();
+  }
+
+  this->dont_show_checkbox->Hide();
+
+  if (welcome_metadata->already_running) {  
+    this->title_label->SetLabel("Espanso is already running!");
+  }
+}
+
+void DerivedWelcomeFrame::on_dont_show_change( wxCommandEvent& event ) {
+  if (welcome_metadata->dont_show_again_changed) {
+    int value = this->dont_show_checkbox->IsChecked() ? 1 : 0;
+    welcome_metadata->dont_show_again_changed(value);
+  }
+}
+
+void DerivedWelcomeFrame::on_complete( wxCommandEvent& event ) {
+  Close(true);
+}
+
+
+bool WelcomeApp::OnInit()
+{
+  wxInitAllImageHandlers();
+  DerivedWelcomeFrame *frame = new DerivedWelcomeFrame(NULL);
+
+  if (welcome_metadata->window_icon_path)
+  {
+    setFrameIcon(welcome_metadata->window_icon_path, frame);
+  }
+
+  frame->Show(true);
+
+  Activate(frame);
+
+  return true;
+}
+
+extern "C" void interop_show_welcome(WelcomeMetadata *_metadata)
+{
+// Setup high DPI support on Windows
+#ifdef __WXMSW__
+  SetProcessDPIAware();
+#endif
+
+  welcome_metadata = _metadata;
+
+  wxApp::SetInstance(new WelcomeApp());
+  int argc = 0;
+  wxEntry(argc, (char **)nullptr);
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/welcome/welcome.fbp b/espanso-modulo/src/sys/welcome/welcome.fbp
new file mode 100644
index 00000000..d9e83737
--- /dev/null
+++ b/espanso-modulo/src/sys/welcome/welcome.fbp
@@ -0,0 +1,683 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<wxFormBuilder_Project>
+    <FileVersion major="1" minor="15" />
+    <object class="Project" expanded="1">
+        <property name="class_decoration">; </property>
+        <property name="code_generation">C++</property>
+        <property name="disconnect_events">1</property>
+        <property name="disconnect_mode">source_name</property>
+        <property name="disconnect_php_events">0</property>
+        <property name="disconnect_python_events">0</property>
+        <property name="embedded_files_path">res</property>
+        <property name="encoding">UTF-8</property>
+        <property name="event_generation">connect</property>
+        <property name="file">welcome_gui</property>
+        <property name="first_id">1000</property>
+        <property name="help_provider">none</property>
+        <property name="indent_with_spaces"></property>
+        <property name="internationalize">0</property>
+        <property name="name">Welcome</property>
+        <property name="namespace"></property>
+        <property name="path">.</property>
+        <property name="precompiled_header">#define _UNICODE</property>
+        <property name="relative_path">1</property>
+        <property name="skip_lua_events">1</property>
+        <property name="skip_php_events">1</property>
+        <property name="skip_python_events">1</property>
+        <property name="ui_table">UI</property>
+        <property name="use_enum">0</property>
+        <property name="use_microsoft_bom">0</property>
+        <object class="Frame" expanded="1">
+            <property name="aui_managed">0</property>
+            <property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
+            <property name="bg">wxSYS_COLOUR_WINDOW</property>
+            <property name="center">wxBOTH</property>
+            <property name="context_help"></property>
+            <property name="context_menu">1</property>
+            <property name="enabled">1</property>
+            <property name="event_handler">impl_virtual</property>
+            <property name="extra_style"></property>
+            <property name="fg"></property>
+            <property name="font"></property>
+            <property name="hidden">0</property>
+            <property name="id">wxID_ANY</property>
+            <property name="maximum_size"></property>
+            <property name="minimum_size"></property>
+            <property name="name">WelcomeFrame</property>
+            <property name="pos"></property>
+            <property name="size">521,544</property>
+            <property name="style">wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU</property>
+            <property name="subclass">; ; forward_declare</property>
+            <property name="title">Espanso is running!</property>
+            <property name="tooltip"></property>
+            <property name="window_extra_style"></property>
+            <property name="window_name"></property>
+            <property name="window_style">wxTAB_TRAVERSAL</property>
+            <property name="xrc_skip_sizer">1</property>
+            <object class="wxBoxSizer" expanded="1">
+                <property name="minimum_size"></property>
+                <property name="name">bSizer1</property>
+                <property name="orient">wxVERTICAL</property>
+                <property name="permission">none</property>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND</property>
+                    <property name="proportion">0</property>
+                    <object class="spacer" expanded="1">
+                        <property name="height">10</property>
+                        <property name="permission">protected</property>
+                        <property name="width">0</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font">,90,92,20,70,0</property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">Espanso is running!</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">title_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">You should now see its icon on the tray bar:</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">tray_info_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticBitmap" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="bitmap"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">tray_bitmap</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag"></property>
+                    <property name="proportion">0</property>
+                    <object class="spacer" expanded="1">
+                        <property name="height">10</property>
+                        <property name="permission">protected</property>
+                        <property name="width">0</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">Try typing &quot;:espanso&quot; below (without quotes)</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">test_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALL|wxEXPAND</property>
+                    <property name="proportion">0</property>
+                    <object class="wxTextCtrl" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font">,90,90,16,70,0</property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="maxlength"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">test_text_ctrl</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="validator_data_type"></property>
+                        <property name="validator_style">wxFILTER_NONE</property>
+                        <property name="validator_type">wxDefaultValidator</property>
+                        <property name="validator_variable"></property>
+                        <property name="value"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxStaticText" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">Do you want to know more? Visit the documentation:</property>
+                        <property name="markup">0</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">doc_label</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <property name="wrap">-1</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxALIGN_CENTER|wxALL</property>
+                    <property name="proportion">0</property>
+                    <object class="wxHyperlinkCtrl" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="hover_color"></property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="label">https://espanso.org/docs/get-started/</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">m_hyperlink1</property>
+                        <property name="normal_color"></property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="style">wxHL_DEFAULT_STYLE</property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="url">https://espanso.org/docs/get-started/</property>
+                        <property name="visited_color"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND</property>
+                    <property name="proportion">1</property>
+                    <object class="spacer" expanded="1">
+                        <property name="height">0</property>
+                        <property name="permission">protected</property>
+                        <property name="width">0</property>
+                    </object>
+                </object>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxEXPAND</property>
+                    <property name="proportion">0</property>
+                    <object class="wxBoxSizer" expanded="1">
+                        <property name="minimum_size"></property>
+                        <property name="name">bSizer2</property>
+                        <property name="orient">wxHORIZONTAL</property>
+                        <property name="permission">none</property>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">10</property>
+                            <property name="flag">wxALIGN_CENTER_VERTICAL|wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxCheckBox" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="checked">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label">Don&apos;t show this again</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">dont_show_checkbox</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="style"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="validator_data_type"></property>
+                                <property name="validator_style">wxFILTER_NONE</property>
+                                <property name="validator_type">wxDefaultValidator</property>
+                                <property name="validator_variable"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                                <event name="OnCheckBox">on_dont_show_change</event>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND</property>
+                            <property name="proportion">1</property>
+                            <object class="spacer" expanded="1">
+                                <property name="height">0</property>
+                                <property name="permission">protected</property>
+                                <property name="width">0</property>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">10</property>
+                            <property name="flag">wxALIGN_CENTER_VERTICAL|wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxButton" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="bitmap"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="current"></property>
+                                <property name="default">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="disabled"></property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="focus"></property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label">Got it!</property>
+                                <property name="margins"></property>
+                                <property name="markup">0</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">got_it_btn</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="position"></property>
+                                <property name="pressed"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="style"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="validator_data_type"></property>
+                                <property name="validator_style">wxFILTER_NONE</property>
+                                <property name="validator_type">wxDefaultValidator</property>
+                                <property name="validator_variable"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                                <event name="OnButtonClick">on_complete</event>
+                            </object>
+                        </object>
+                    </object>
+                </object>
+            </object>
+        </object>
+    </object>
+</wxFormBuilder_Project>
diff --git a/espanso-modulo/src/sys/welcome/welcome_gui.cpp b/espanso-modulo/src/sys/welcome/welcome_gui.cpp
new file mode 100644
index 00000000..b04dcb7f
--- /dev/null
+++ b/espanso-modulo/src/sys/welcome/welcome_gui.cpp
@@ -0,0 +1,94 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#define _UNICODE
+
+#include "welcome_gui.h"
+
+///////////////////////////////////////////////////////////////////////////
+
+WelcomeFrame::WelcomeFrame( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style )
+{
+	this->SetSizeHints( wxDefaultSize, wxDefaultSize );
+	this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer1;
+	bSizer1 = new wxBoxSizer( wxVERTICAL );
+
+
+	bSizer1->Add( 0, 10, 0, wxEXPAND, 5 );
+
+	title_label = new wxStaticText( this, wxID_ANY, wxT("Espanso is running!"), wxDefaultPosition, wxDefaultSize, 0 );
+	title_label->Wrap( -1 );
+	title_label->SetFont( wxFont( 20, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer1->Add( title_label, 0, wxALIGN_CENTER|wxALL, 10 );
+
+	tray_info_label = new wxStaticText( this, wxID_ANY, wxT("You should now see its icon on the tray bar:"), wxDefaultPosition, wxDefaultSize, 0 );
+	tray_info_label->Wrap( -1 );
+	bSizer1->Add( tray_info_label, 0, wxALIGN_CENTER|wxALL, 10 );
+
+	tray_bitmap = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer1->Add( tray_bitmap, 0, wxALIGN_CENTER|wxALL, 5 );
+
+
+	bSizer1->Add( 0, 10, 0, 0, 10 );
+
+	test_label = new wxStaticText( this, wxID_ANY, wxT("Try typing \":espanso\" below (without quotes)"), wxDefaultPosition, wxDefaultSize, 0 );
+	test_label->Wrap( -1 );
+	bSizer1->Add( test_label, 0, wxALIGN_CENTER|wxALL, 10 );
+
+	test_text_ctrl = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 );
+	test_text_ctrl->SetFont( wxFont( 16, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) );
+
+	bSizer1->Add( test_text_ctrl, 0, wxALL|wxEXPAND, 10 );
+
+	doc_label = new wxStaticText( this, wxID_ANY, wxT("Do you want to know more? Visit the documentation:"), wxDefaultPosition, wxDefaultSize, 0 );
+	doc_label->Wrap( -1 );
+	bSizer1->Add( doc_label, 0, wxALIGN_CENTER|wxALL, 10 );
+
+	m_hyperlink1 = new wxHyperlinkCtrl( this, wxID_ANY, wxT("https://espanso.org/docs/get-started/"), wxT("https://espanso.org/docs/get-started/"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
+	bSizer1->Add( m_hyperlink1, 0, wxALIGN_CENTER|wxALL, 10 );
+
+
+	bSizer1->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	wxBoxSizer* bSizer2;
+	bSizer2 = new wxBoxSizer( wxHORIZONTAL );
+
+	dont_show_checkbox = new wxCheckBox( this, wxID_ANY, wxT("Don't show this again"), wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer2->Add( dont_show_checkbox, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 );
+
+
+	bSizer2->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	got_it_btn = new wxButton( this, wxID_ANY, wxT("Got it!"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	got_it_btn->SetDefault();
+	bSizer2->Add( got_it_btn, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 );
+
+
+	bSizer1->Add( bSizer2, 0, wxEXPAND, 10 );
+
+
+	this->SetSizer( bSizer1 );
+	this->Layout();
+
+	this->Centre( wxBOTH );
+
+	// Connect Events
+	dont_show_checkbox->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( WelcomeFrame::on_dont_show_change ), NULL, this );
+	got_it_btn->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WelcomeFrame::on_complete ), NULL, this );
+}
+
+WelcomeFrame::~WelcomeFrame()
+{
+	// Disconnect Events
+	dont_show_checkbox->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( WelcomeFrame::on_dont_show_change ), NULL, this );
+	got_it_btn->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WelcomeFrame::on_complete ), NULL, this );
+
+}
diff --git a/espanso-modulo/src/sys/welcome/welcome_gui.h b/espanso-modulo/src/sys/welcome/welcome_gui.h
new file mode 100644
index 00000000..44e21fef
--- /dev/null
+++ b/espanso-modulo/src/sys/welcome/welcome_gui.h
@@ -0,0 +1,62 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <wx/artprov.h>
+#include <wx/xrc/xmlres.h>
+#include <wx/string.h>
+#include <wx/stattext.h>
+#include <wx/gdicmn.h>
+#include <wx/font.h>
+#include <wx/colour.h>
+#include <wx/settings.h>
+#include <wx/bitmap.h>
+#include <wx/image.h>
+#include <wx/icon.h>
+#include <wx/statbmp.h>
+#include <wx/textctrl.h>
+#include <wx/hyperlink.h>
+#include <wx/checkbox.h>
+#include <wx/button.h>
+#include <wx/sizer.h>
+#include <wx/frame.h>
+
+///////////////////////////////////////////////////////////////////////////
+
+
+///////////////////////////////////////////////////////////////////////////////
+/// Class WelcomeFrame
+///////////////////////////////////////////////////////////////////////////////
+class WelcomeFrame : public wxFrame
+{
+	private:
+
+	protected:
+		wxStaticText* title_label;
+		wxStaticText* tray_info_label;
+		wxStaticBitmap* tray_bitmap;
+		wxStaticText* test_label;
+		wxTextCtrl* test_text_ctrl;
+		wxStaticText* doc_label;
+		wxHyperlinkCtrl* m_hyperlink1;
+		wxCheckBox* dont_show_checkbox;
+		wxButton* got_it_btn;
+
+		// Virtual event handlers, overide them in your derived class
+		virtual void on_dont_show_change( wxCommandEvent& event ) { event.Skip(); }
+		virtual void on_complete( wxCommandEvent& event ) { event.Skip(); }
+
+
+	public:
+
+		WelcomeFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso is running!"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 521,544 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL );
+
+		~WelcomeFrame();
+
+};
+
diff --git a/espanso-modulo/src/sys/wizard/mod.rs b/espanso-modulo/src/sys/wizard/mod.rs
new file mode 100644
index 00000000..d1289927
--- /dev/null
+++ b/espanso-modulo/src/sys/wizard/mod.rs
@@ -0,0 +1,226 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::os::raw::c_int;
+use std::{ffi::CString, sync::Mutex};
+
+use crate::sys::interop::{
+  WIZARD_DETECTED_OS_UNKNOWN, WIZARD_DETECTED_OS_WAYLAND, WIZARD_DETECTED_OS_X11,
+};
+use crate::sys::util::convert_to_cstring_or_null;
+use crate::{
+  sys::interop::{
+    WizardMetadata, WIZARD_MIGRATE_RESULT_CLEAN_FAILURE, WIZARD_MIGRATE_RESULT_DIRTY_FAILURE,
+    WIZARD_MIGRATE_RESULT_SUCCESS, WIZARD_MIGRATE_RESULT_UNKNOWN_FAILURE,
+  },
+  wizard::{WizardHandlers, WizardOptions},
+};
+
+lazy_static! {
+  static ref HANDLERS: Mutex<Option<WizardHandlers>> = Mutex::new(None);
+}
+
+pub fn show(options: WizardOptions) -> bool {
+  let c_version = CString::new(options.version).expect("unable to convert version to CString");
+
+  let (_c_window_icon_path, c_window_icon_path_ptr) =
+    convert_to_cstring_or_null(options.window_icon_path);
+  let (_c_welcome_image, c_welcome_image_path_ptr) =
+    convert_to_cstring_or_null(options.welcome_image_path);
+  let (_c_accessibility_image_1_path, c_accessibility_image_1_path_ptr) =
+    convert_to_cstring_or_null(options.accessibility_image_1_path);
+  let (_c_accessibility_image_2_path, c_accessibility_image_2_path_ptr) =
+    convert_to_cstring_or_null(options.accessibility_image_2_path);
+
+  extern "C" fn is_legacy_version_running() -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in is_legacy_version_running method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.is_legacy_version_running.as_ref() {
+      if (*handler_ref)() {
+        1
+      } else {
+        0
+      }
+    } else {
+      -1
+    }
+  }
+
+  extern "C" fn backup_and_migrate() -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in backup_and_migrate method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.backup_and_migrate.as_ref() {
+      match (*handler_ref)() {
+        crate::wizard::MigrationResult::Success => WIZARD_MIGRATE_RESULT_SUCCESS,
+        crate::wizard::MigrationResult::CleanFailure => WIZARD_MIGRATE_RESULT_CLEAN_FAILURE,
+        crate::wizard::MigrationResult::DirtyFailure => WIZARD_MIGRATE_RESULT_DIRTY_FAILURE,
+        crate::wizard::MigrationResult::UnknownFailure => WIZARD_MIGRATE_RESULT_UNKNOWN_FAILURE,
+      }
+    } else {
+      WIZARD_MIGRATE_RESULT_UNKNOWN_FAILURE
+    }
+  }
+
+  extern "C" fn auto_start(auto_start: c_int) -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in auto_start method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.auto_start.as_ref() {
+      if (*handler_ref)(auto_start != 0) {
+        1
+      } else {
+        0
+      }
+    } else {
+      -1
+    }
+  }
+
+  extern "C" fn add_to_path() -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in add_to_path method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.add_to_path.as_ref() {
+      if (*handler_ref)() {
+        1
+      } else {
+        0
+      }
+    } else {
+      -1
+    }
+  }
+
+  extern "C" fn enable_accessibility() -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in enable_accessibility method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.enable_accessibility.as_ref() {
+      (*handler_ref)();
+      1
+    } else {
+      -1
+    }
+  }
+
+  extern "C" fn is_accessibility_enabled() -> c_int {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in is_accessibility_enabled method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.is_accessibility_enabled.as_ref() {
+      if (*handler_ref)() {
+        1
+      } else {
+        0
+      }
+    } else {
+      -1
+    }
+  }
+
+  extern "C" fn on_completed() {
+    let lock = HANDLERS
+      .lock()
+      .expect("unable to acquire lock in on_completed method");
+    let handlers_ref = (*lock).as_ref().expect("unable to unwrap handlers");
+    if let Some(handler_ref) = handlers_ref.on_completed.as_ref() {
+      (*handler_ref)()
+    }
+  }
+
+  {
+    let mut lock = HANDLERS.lock().expect("unable to acquire handlers lock");
+    *lock = Some(options.handlers)
+  }
+
+  let wizard_metadata = WizardMetadata {
+    version: c_version.as_ptr(),
+
+    is_welcome_page_enabled: if options.is_welcome_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_move_bundle_page_enabled: if options.is_move_bundle_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_legacy_version_page_enabled: if options.is_legacy_version_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_wrong_edition_page_enabled: if options.is_wrong_edition_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_migrate_page_enabled: if options.is_migrate_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_auto_start_page_enabled: if options.is_auto_start_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_add_path_page_enabled: if options.is_add_path_page_enabled {
+      1
+    } else {
+      0
+    },
+    is_accessibility_page_enabled: if options.is_accessibility_page_enabled {
+      1
+    } else {
+      0
+    },
+
+    window_icon_path: c_window_icon_path_ptr,
+    welcome_image_path: c_welcome_image_path_ptr,
+    accessibility_image_1_path: c_accessibility_image_1_path_ptr,
+    accessibility_image_2_path: c_accessibility_image_2_path_ptr,
+    detected_os: match options.detected_os {
+      crate::wizard::DetectedOS::Unknown => WIZARD_DETECTED_OS_UNKNOWN,
+      crate::wizard::DetectedOS::X11 => WIZARD_DETECTED_OS_X11,
+      crate::wizard::DetectedOS::Wayland => WIZARD_DETECTED_OS_WAYLAND,
+    },
+
+    is_legacy_version_running,
+    backup_and_migrate,
+    auto_start,
+    add_to_path,
+    enable_accessibility,
+    is_accessibility_enabled,
+    on_completed,
+  };
+
+  let successful = unsafe { super::interop::interop_show_wizard(&wizard_metadata) };
+
+  successful == 1
+}
diff --git a/espanso-modulo/src/sys/wizard/wizard.cpp b/espanso-modulo/src/sys/wizard/wizard.cpp
new file mode 100644
index 00000000..5b9de108
--- /dev/null
+++ b/espanso-modulo/src/sys/wizard/wizard.cpp
@@ -0,0 +1,407 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#define _UNICODE
+
+#include "../common/common.h"
+#include "../interop/interop.h"
+#include "./wizard_gui.h"
+
+#include <vector>
+#include <memory>
+#include <unordered_map>
+
+const int WELCOME_PAGE_INDEX = 0;
+const int MOVE_BUNDLE_PAGE_INDEX = WELCOME_PAGE_INDEX + 1;
+const int LEGACY_VERSION_PAGE_INDEX = MOVE_BUNDLE_PAGE_INDEX + 1;
+const int WRONG_EDITION_PAGE_INDEX = LEGACY_VERSION_PAGE_INDEX + 1;
+const int MIGRATE_PAGE_INDEX = WRONG_EDITION_PAGE_INDEX + 1;
+const int AUTO_START_PAGE_INDEX = MIGRATE_PAGE_INDEX + 1;
+const int ADD_PATH_PAGE_INDEX = AUTO_START_PAGE_INDEX + 1;
+const int ACCESSIBILITY_PAGE_INDEX = ADD_PATH_PAGE_INDEX + 1;
+const int MAX_PAGE_INDEX = ACCESSIBILITY_PAGE_INDEX + 1; // Update if a new page is added at the end
+
+WizardMetadata *wizard_metadata = nullptr;
+int completed_successfully = 0;
+
+// App Code
+
+class WizardApp : public wxApp
+{
+public:
+  virtual bool OnInit();
+};
+
+int find_next_page(int current_index)
+{
+  int next_index = current_index + 1;
+  if (next_index >= MAX_PAGE_INDEX)
+  {
+    return -1;
+  }
+
+  switch (next_index)
+  {
+  case WELCOME_PAGE_INDEX:
+    if (wizard_metadata->is_welcome_page_enabled)
+    {
+      return WELCOME_PAGE_INDEX;
+    }
+  case MOVE_BUNDLE_PAGE_INDEX:
+    if (wizard_metadata->is_move_bundle_page_enabled)
+    {
+      return MOVE_BUNDLE_PAGE_INDEX;
+    }
+  case LEGACY_VERSION_PAGE_INDEX:
+    if (wizard_metadata->is_legacy_version_page_enabled)
+    {
+      return LEGACY_VERSION_PAGE_INDEX;
+    }
+  case WRONG_EDITION_PAGE_INDEX:
+    if (wizard_metadata->is_wrong_edition_page_enabled)
+    {
+      return WRONG_EDITION_PAGE_INDEX;
+    }
+  case MIGRATE_PAGE_INDEX:
+    if (wizard_metadata->is_migrate_page_enabled)
+    {
+      return MIGRATE_PAGE_INDEX;
+    }
+  case AUTO_START_PAGE_INDEX:
+    if (wizard_metadata->is_auto_start_page_enabled)
+    {
+      return AUTO_START_PAGE_INDEX;
+    }
+  case ADD_PATH_PAGE_INDEX:
+    if (wizard_metadata->is_add_path_page_enabled)
+    {
+      return ADD_PATH_PAGE_INDEX;
+    }
+  case ACCESSIBILITY_PAGE_INDEX:
+    if (wizard_metadata->is_accessibility_page_enabled)
+    {
+      return ACCESSIBILITY_PAGE_INDEX;
+    }
+  }
+
+  return find_next_page(next_index);
+}
+
+class DerivedFrame : public WizardFrame
+{
+protected:
+  void check_timer_tick(wxTimerEvent &event);
+  void on_page_changed(wxBookCtrlEvent &event);
+  void welcome_start_clicked(wxCommandEvent &event);
+  void migrate_button_clicked(wxCommandEvent &event);
+  void migrate_compatibility_mode_clicked(wxCommandEvent &event);
+	void auto_start_continue_clicked( wxCommandEvent& event );
+	void add_path_continue_clicked( wxCommandEvent& event );
+	void accessibility_enable_clicked( wxCommandEvent& event );
+	void quit_espanso_clicked( wxCommandEvent& event );
+
+  void navigate_to_next_page_or_close();
+  void change_default_button(int target_page);
+
+public:
+  DerivedFrame(wxWindow *parent);
+};
+
+DerivedFrame::DerivedFrame(wxWindow *parent)
+    : WizardFrame(parent)
+{
+  // Welcome images
+
+  if (wizard_metadata->welcome_image_path)
+  {
+    wxBitmap welcomeBitmap = wxBitmap(wizard_metadata->welcome_image_path, wxBITMAP_TYPE_PNG);
+    this->welcome_image->SetBitmap(welcomeBitmap);
+  }
+
+  this->welcome_version_text->SetLabel(wxString::Format("( version %s )", wizard_metadata->version));
+
+  // Accessiblity images
+
+  if (wizard_metadata->accessibility_image_1_path)
+  {
+    wxBitmap accessiblityImage1 = wxBitmap(wizard_metadata->accessibility_image_1_path, wxBITMAP_TYPE_PNG);
+    this->accessibility_image1->SetBitmap(accessiblityImage1);
+  }
+  if (wizard_metadata->accessibility_image_2_path)
+  {
+    wxBitmap accessiblityImage2 = wxBitmap(wizard_metadata->accessibility_image_2_path, wxBITMAP_TYPE_PNG);
+    this->accessibility_image2->SetBitmap(accessiblityImage2);
+  }
+
+  // Wrong edition
+  if (wizard_metadata->is_wrong_edition_page_enabled) {
+    if (wizard_metadata->detected_os == DETECTED_OS_X11) {
+      this->wrong_edition_description_x11->Hide();
+    }
+    if (wizard_metadata->detected_os == DETECTED_OS_WAYLAND) {
+      this->wrong_edition_description_wayland->Hide();
+    }
+  }
+
+  // Load the first page
+  int page = find_next_page(-1);
+  if (page >= 0)
+  {
+    this->m_simplebook->SetSelection(page);
+    this->change_default_button(page);
+  }
+  else
+  {
+    Close(true);
+  }
+}
+
+void DerivedFrame::navigate_to_next_page_or_close()
+{
+  int current_page = this->m_simplebook->GetSelection();
+  int page = find_next_page(current_page);
+  if (page >= 0)
+  {
+    this->m_simplebook->SetSelection(page);
+  }
+  else
+  {
+    if (wizard_metadata->on_completed) {
+      wizard_metadata->on_completed();
+      completed_successfully = 1;
+    }
+
+    Close(true);
+  }
+}
+
+void DerivedFrame::welcome_start_clicked(wxCommandEvent &event)
+{
+  this->navigate_to_next_page_or_close();
+}
+
+void DerivedFrame::migrate_compatibility_mode_clicked(wxCommandEvent &event)
+{
+  this->navigate_to_next_page_or_close();
+}
+
+void DerivedFrame::migrate_button_clicked(wxCommandEvent &event)
+{
+  if (wizard_metadata->backup_and_migrate)
+  {
+    int result = wizard_metadata->backup_and_migrate();
+    if (result == MIGRATE_RESULT_SUCCESS)
+    {
+      this->navigate_to_next_page_or_close();
+    }
+    else if (result == MIGRATE_RESULT_CLEAN_FAILURE)
+    {
+      wxMessageBox(wxT("An error occurred during the migration, but your old files were not modified.\n\nPlease run 'espanso log' in a terminal for more information."), wxT("Migration error"), wxICON_ERROR);
+    }
+    else if (result == MIGRATE_RESULT_DIRTY_FAILURE)
+    {
+      wxMessageBox(wxT("An error occurred during the migration and espanso couldn't complete the process. Some configuration files might be missing, but you'll find the backup in the Documents folder.\n\nPlease run 'espanso log' in a terminal for more information."), wxT("Migration error"), wxICON_ERROR);
+    }
+    else if (result == MIGRATE_RESULT_UNKNOWN_FAILURE)
+    {
+      wxMessageBox(wxT("An error occurred during the migration.\n\nPlease run 'espanso log' in a terminal for more information."), wxT("Migration error"), wxICON_ERROR);
+    }
+  }
+}
+
+void DerivedFrame::auto_start_continue_clicked( wxCommandEvent& event ) {
+  if (!auto_start_checkbox->IsChecked()) {
+    if (wizard_metadata->auto_start)
+    {
+      wizard_metadata->auto_start(0);
+    }
+    this->navigate_to_next_page_or_close();
+    return;
+  } 
+
+  if (wizard_metadata->auto_start)
+  {
+    while (true) {
+      int result = wizard_metadata->auto_start(1);
+      if (result == 1)
+      {
+        this->navigate_to_next_page_or_close();
+        return;
+      }
+      else 
+      {
+        wxMessageDialog* dialog = new wxMessageDialog(this,
+          "An error occurred while registering Espanso as a service, please check the logs for more information.\nDo you want to retry? You can always configure this option later",
+          "Operation failed",
+          wxCENTER | wxOK_DEFAULT | wxOK | wxCANCEL |
+          wxICON_EXCLAMATION);
+
+        dialog->SetOKLabel("Retry");
+
+        int prompt_result = dialog->ShowModal(); 
+        if (prompt_result == wxID_CANCEL) {
+          this->navigate_to_next_page_or_close();
+          break;
+        }
+      }
+    }
+  }
+}
+
+void DerivedFrame::add_path_continue_clicked( wxCommandEvent& event ) {
+  if (!add_path_checkbox->IsChecked()) {
+    this->navigate_to_next_page_or_close();
+    return;
+  } 
+
+  if (wizard_metadata->add_to_path)
+  {
+    while (true) {
+      int result = wizard_metadata->add_to_path();
+      if (result == 1)
+      {
+        this->navigate_to_next_page_or_close();
+        return;
+      }
+      else 
+      {
+        wxMessageDialog* dialog = new wxMessageDialog(this,
+          "An error occurred while registering the 'espanso' command to the PATH, please check the logs for more information.\nDo you want to retry? You can always add espanso to the PATH later",
+          "Operation failed",
+          wxCENTER | wxOK_DEFAULT | wxOK | wxCANCEL |
+          wxICON_EXCLAMATION);
+
+        dialog->SetOKLabel("Retry");
+
+        int prompt_result = dialog->ShowModal(); 
+        if (prompt_result == wxID_CANCEL) {
+          this->navigate_to_next_page_or_close();
+          break;
+        }
+      }
+    }
+  }
+}
+
+
+void DerivedFrame::accessibility_enable_clicked( wxCommandEvent& event )
+{
+  if (wizard_metadata->enable_accessibility)
+  {
+    wizard_metadata->enable_accessibility();
+  }
+}
+
+void DerivedFrame::quit_espanso_clicked( wxCommandEvent& event )
+{
+  Close(true);
+}
+
+void DerivedFrame::check_timer_tick(wxTimerEvent &event)
+{
+  if (this->m_simplebook->GetSelection() == LEGACY_VERSION_PAGE_INDEX)
+  {
+    if (wizard_metadata->is_legacy_version_running)
+    {
+      if (wizard_metadata->is_legacy_version_running() == 0)
+      {
+        this->navigate_to_next_page_or_close();
+      }
+    }
+  } else if (this->m_simplebook->GetSelection() == ACCESSIBILITY_PAGE_INDEX) {
+    if (wizard_metadata->is_accessibility_enabled)
+    {
+      if (wizard_metadata->is_accessibility_enabled() == 1)
+      {
+        this->navigate_to_next_page_or_close();
+      }
+    }
+  }
+}
+
+void DerivedFrame::on_page_changed(wxBookCtrlEvent &event)
+{
+  int current_page = this->m_simplebook->GetSelection();
+  this->change_default_button(current_page);
+}
+
+void DerivedFrame::change_default_button(int target_page)
+{
+  switch (target_page)
+  {
+  case WELCOME_PAGE_INDEX:
+  {
+    this->welcome_start_button->SetDefault();
+    break;
+  }
+  case MOVE_BUNDLE_PAGE_INDEX:
+  {
+    this->move_bundle_quit_button->SetDefault();
+    break;
+  }
+  case MIGRATE_PAGE_INDEX:
+  {
+    this->migrate_backup_and_migrate_button->SetDefault();
+    break;
+  }
+  case ADD_PATH_PAGE_INDEX:
+  {
+    this->add_path_continue_button->SetDefault();
+    break;
+  }
+  case ACCESSIBILITY_PAGE_INDEX:
+  {
+    this->accessibility_enable_button->SetDefault();
+    break;
+  }
+  }
+}
+
+bool WizardApp::OnInit()
+{
+  wxInitAllImageHandlers();
+  DerivedFrame *frame = new DerivedFrame(NULL);
+
+  if (wizard_metadata->window_icon_path)
+  {
+    setFrameIcon(wizard_metadata->window_icon_path, frame);
+  }
+
+  frame->Show(true);
+
+  Activate(frame);
+
+  return true;
+}
+
+extern "C" int interop_show_wizard(WizardMetadata *_metadata)
+{
+// Setup high DPI support on Windows
+#ifdef __WXMSW__
+  SetProcessDPIAware();
+#endif
+
+  wizard_metadata = _metadata;
+
+  wxApp::SetInstance(new WizardApp());
+  int argc = 0;
+  wxEntry(argc, (char **)nullptr);
+
+  return completed_successfully;
+}
\ No newline at end of file
diff --git a/espanso-modulo/src/sys/wizard/wizard.fbp b/espanso-modulo/src/sys/wizard/wizard.fbp
new file mode 100644
index 00000000..913ec19c
--- /dev/null
+++ b/espanso-modulo/src/sys/wizard/wizard.fbp
@@ -0,0 +1,3292 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<wxFormBuilder_Project>
+    <FileVersion major="1" minor="15" />
+    <object class="Project" expanded="1">
+        <property name="class_decoration">; </property>
+        <property name="code_generation">C++</property>
+        <property name="disconnect_events">1</property>
+        <property name="disconnect_mode">source_name</property>
+        <property name="disconnect_php_events">0</property>
+        <property name="disconnect_python_events">0</property>
+        <property name="embedded_files_path">res</property>
+        <property name="encoding">UTF-8</property>
+        <property name="event_generation">connect</property>
+        <property name="file">wizard_gui</property>
+        <property name="first_id">1000</property>
+        <property name="help_provider">none</property>
+        <property name="indent_with_spaces"></property>
+        <property name="internationalize">0</property>
+        <property name="name">Wizard</property>
+        <property name="namespace"></property>
+        <property name="path">.</property>
+        <property name="precompiled_header">#define _UNICODE</property>
+        <property name="relative_path">1</property>
+        <property name="skip_lua_events">1</property>
+        <property name="skip_php_events">1</property>
+        <property name="skip_python_events">1</property>
+        <property name="ui_table">UI</property>
+        <property name="use_enum">0</property>
+        <property name="use_microsoft_bom">0</property>
+        <object class="Frame" expanded="1">
+            <property name="aui_managed">0</property>
+            <property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
+            <property name="bg">wxSYS_COLOUR_WINDOW</property>
+            <property name="center">wxBOTH</property>
+            <property name="context_help"></property>
+            <property name="context_menu">1</property>
+            <property name="enabled">1</property>
+            <property name="event_handler">impl_virtual</property>
+            <property name="extra_style"></property>
+            <property name="fg"></property>
+            <property name="font"></property>
+            <property name="hidden">0</property>
+            <property name="id">wxID_ANY</property>
+            <property name="maximum_size"></property>
+            <property name="minimum_size"></property>
+            <property name="name">WizardFrame</property>
+            <property name="pos"></property>
+            <property name="size">550,577</property>
+            <property name="style">wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU</property>
+            <property name="subclass">; ; forward_declare</property>
+            <property name="title">Espanso</property>
+            <property name="tooltip"></property>
+            <property name="window_extra_style"></property>
+            <property name="window_name"></property>
+            <property name="window_style">wxTAB_TRAVERSAL</property>
+            <property name="xrc_skip_sizer">1</property>
+            <object class="wxTimer" expanded="1">
+                <property name="enabled">1</property>
+                <property name="id">wxID_ANY</property>
+                <property name="name">check_timer</property>
+                <property name="oneshot">0</property>
+                <property name="period">500</property>
+                <property name="permission">protected</property>
+                <event name="OnTimer">check_timer_tick</event>
+            </object>
+            <object class="wxBoxSizer" expanded="1">
+                <property name="minimum_size"></property>
+                <property name="name">bSizer1</property>
+                <property name="orient">wxVERTICAL</property>
+                <property name="permission">none</property>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">5</property>
+                    <property name="flag">wxEXPAND | wxALL</property>
+                    <property name="proportion">1</property>
+                    <object class="wxSimplebook" expanded="1">
+                        <property name="BottomDockable">1</property>
+                        <property name="LeftDockable">1</property>
+                        <property name="RightDockable">1</property>
+                        <property name="TopDockable">1</property>
+                        <property name="aui_layer"></property>
+                        <property name="aui_name"></property>
+                        <property name="aui_position"></property>
+                        <property name="aui_row"></property>
+                        <property name="best_size"></property>
+                        <property name="bg"></property>
+                        <property name="caption"></property>
+                        <property name="caption_visible">1</property>
+                        <property name="center_pane">0</property>
+                        <property name="close_button">1</property>
+                        <property name="context_help"></property>
+                        <property name="context_menu">1</property>
+                        <property name="default_pane">0</property>
+                        <property name="dock">Dock</property>
+                        <property name="dock_fixed">0</property>
+                        <property name="docking">Left</property>
+                        <property name="enabled">1</property>
+                        <property name="fg"></property>
+                        <property name="floatable">1</property>
+                        <property name="font"></property>
+                        <property name="gripper">0</property>
+                        <property name="hidden">0</property>
+                        <property name="id">wxID_ANY</property>
+                        <property name="max_size"></property>
+                        <property name="maximize_button">0</property>
+                        <property name="maximum_size"></property>
+                        <property name="min_size"></property>
+                        <property name="minimize_button">0</property>
+                        <property name="minimum_size"></property>
+                        <property name="moveable">1</property>
+                        <property name="name">m_simplebook</property>
+                        <property name="pane_border">1</property>
+                        <property name="pane_position"></property>
+                        <property name="pane_size"></property>
+                        <property name="permission">protected</property>
+                        <property name="pin_button">1</property>
+                        <property name="pos"></property>
+                        <property name="resize">Resizable</property>
+                        <property name="show">1</property>
+                        <property name="size"></property>
+                        <property name="subclass">; ; forward_declare</property>
+                        <property name="toolbar_pane">0</property>
+                        <property name="tooltip"></property>
+                        <property name="window_extra_style"></property>
+                        <property name="window_name"></property>
+                        <property name="window_style"></property>
+                        <event name="OnSimplebookPageChanged">on_page_changed</event>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">welcome_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer2</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">0</property>
+                                        <property name="flag">wxALIGN_CENTER|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticBitmap" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size">256,256</property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">welcome_image</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size">256,256</property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Welcome to Espanso!</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">welcome_title_text</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">(version 1.2.3)</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">welcome_version_text</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">This wizard will help you to quickly get started with espanso. &#x0A;&#x0A;Click &quot;Start&quot; when you are ready</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">welcome_description_text</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Start</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">welcome_start_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">welcome_start_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">move_bundle_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer22</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Move to /Applications folder</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">move_bundle_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Espanso is being run from outside the Applications directory, which prevents it from working correctly.&#x0A;&#x0A;Please move the Espanso.app bundle inside your Applications folder and start it again.&#x0A;</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">move_bundle_description</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Start</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">move_bundle_quit_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">move_bundle_quit_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">legacy_version_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer21</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Legacy version detected</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">legacy_version_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">A legacy espanso process has been detected and prevents the new version from working correctly.&#x0A;&#x0A;Please terminate and uninstall the old espanso version to proceed.&#x0A;&#x0A;If you already uninstalled the previous version, you might need to restart your computer for changes to be detected.&#x0A;&#x0A;For more information, see: </property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">legacy_version_description</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxHyperlinkCtrl" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="hover_color"></property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">https://espanso.org/legacy/uninstall</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">legacy_version_docs_link</property>
+                                            <property name="normal_color"></property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style">wxHL_DEFAULT_STYLE</property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="url">https://espanso.org/legacy/uninstall</property>
+                                            <property name="visited_color"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">0</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Continue</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">legacy_version_continue_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">wrong_edition_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer213</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Incompatibility detected</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">wrong_edition_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxEXPAND|wxLEFT|wxRIGHT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">This version of espanso was compiled to support X11-based systems, but it seems you are on a Wayland-based desktop environment.&#x0A;&#x0A;Unfortunately, the two versions are incompatible. To use espanso, either switch to an X11-based environment or download the Wayland version from the website.&#x0A;&#x0A;For more information:</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">wrong_edition_description_x11</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxEXPAND|wxLEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">This version of espanso was compiled to support Wayland-based systems, but it seems you are on a X11-based desktop environment.&#x0A;&#x0A;Unfortunately, the two versions are incompatible. To use espanso, either switch to a Wayland-based environment or download the X11 version from the website.&#x0A;&#x0A;For more information:</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">wrong_edition_description_wayland</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxHyperlinkCtrl" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="hover_color"></property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">https://espanso.org/install</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">wrong_edition_link</property>
+                                            <property name="normal_color"></property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style">wxHL_DEFAULT_STYLE</property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="url">https://espanso.org/install</property>
+                                            <property name="visited_color"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Quit Espanso</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">wrong_edition_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">quit_espanso_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">migrate_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer211</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Migrate configuration</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">migrate_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT|wxTOP</property>
+                                        <property name="proportion">1</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">The new version uses a slightly different configuration format that powers some exciting new features.&#x0A;&#x0A;To ease the transition, espanso offers two possible choices: &#x0A;&#x0A;  - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). &#x0A;  - Use compatibility mode without changing the configs. &#x0A;&#x0A;Keep in mind that: &#x0A;&#x0A;  - Compatibility mode does not support all new espanso features &#x0A;  - You can always migrate the configs later &#x0A;&#x0A;For more information, see: </property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">migrate_description</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxHyperlinkCtrl" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="hover_color"></property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">https://espanso.org/migration</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">migrate_link</property>
+                                            <property name="normal_color"></property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style">wxHL_DEFAULT_STYLE</property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="url">https://espanso.org/migration</property>
+                                            <property name="visited_color"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">10</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="wxBoxSizer" expanded="1">
+                                            <property name="minimum_size">-1,-1</property>
+                                            <property name="name">bSizer8</property>
+                                            <property name="orient">wxHORIZONTAL</property>
+                                            <property name="permission">none</property>
+                                            <object class="sizeritem" expanded="1">
+                                                <property name="border">10</property>
+                                                <property name="flag">wxALL</property>
+                                                <property name="proportion">0</property>
+                                                <object class="wxButton" expanded="1">
+                                                    <property name="BottomDockable">1</property>
+                                                    <property name="LeftDockable">1</property>
+                                                    <property name="RightDockable">1</property>
+                                                    <property name="TopDockable">1</property>
+                                                    <property name="aui_layer"></property>
+                                                    <property name="aui_name"></property>
+                                                    <property name="aui_position"></property>
+                                                    <property name="aui_row"></property>
+                                                    <property name="best_size"></property>
+                                                    <property name="bg"></property>
+                                                    <property name="bitmap"></property>
+                                                    <property name="caption"></property>
+                                                    <property name="caption_visible">1</property>
+                                                    <property name="center_pane">0</property>
+                                                    <property name="close_button">1</property>
+                                                    <property name="context_help"></property>
+                                                    <property name="context_menu">1</property>
+                                                    <property name="current"></property>
+                                                    <property name="default">0</property>
+                                                    <property name="default_pane">0</property>
+                                                    <property name="disabled"></property>
+                                                    <property name="dock">Dock</property>
+                                                    <property name="dock_fixed">0</property>
+                                                    <property name="docking">Left</property>
+                                                    <property name="enabled">1</property>
+                                                    <property name="fg"></property>
+                                                    <property name="floatable">1</property>
+                                                    <property name="focus"></property>
+                                                    <property name="font"></property>
+                                                    <property name="gripper">0</property>
+                                                    <property name="hidden">0</property>
+                                                    <property name="id">wxID_ANY</property>
+                                                    <property name="label">Use compatibility mode</property>
+                                                    <property name="margins"></property>
+                                                    <property name="markup">0</property>
+                                                    <property name="max_size"></property>
+                                                    <property name="maximize_button">0</property>
+                                                    <property name="maximum_size"></property>
+                                                    <property name="min_size"></property>
+                                                    <property name="minimize_button">0</property>
+                                                    <property name="minimum_size"></property>
+                                                    <property name="moveable">1</property>
+                                                    <property name="name">migrate_compatibility_mode_button</property>
+                                                    <property name="pane_border">1</property>
+                                                    <property name="pane_position"></property>
+                                                    <property name="pane_size"></property>
+                                                    <property name="permission">protected</property>
+                                                    <property name="pin_button">1</property>
+                                                    <property name="pos"></property>
+                                                    <property name="position"></property>
+                                                    <property name="pressed"></property>
+                                                    <property name="resize">Resizable</property>
+                                                    <property name="show">1</property>
+                                                    <property name="size"></property>
+                                                    <property name="style"></property>
+                                                    <property name="subclass">; ; forward_declare</property>
+                                                    <property name="toolbar_pane">0</property>
+                                                    <property name="tooltip"></property>
+                                                    <property name="validator_data_type"></property>
+                                                    <property name="validator_style">wxFILTER_NONE</property>
+                                                    <property name="validator_type">wxDefaultValidator</property>
+                                                    <property name="validator_variable"></property>
+                                                    <property name="window_extra_style"></property>
+                                                    <property name="window_name"></property>
+                                                    <property name="window_style"></property>
+                                                    <event name="OnButtonClick">migrate_compatibility_mode_clicked</event>
+                                                </object>
+                                            </object>
+                                            <object class="sizeritem" expanded="1">
+                                                <property name="border">5</property>
+                                                <property name="flag">wxEXPAND</property>
+                                                <property name="proportion">1</property>
+                                                <object class="spacer" expanded="1">
+                                                    <property name="height">0</property>
+                                                    <property name="permission">protected</property>
+                                                    <property name="width">0</property>
+                                                </object>
+                                            </object>
+                                            <object class="sizeritem" expanded="1">
+                                                <property name="border">10</property>
+                                                <property name="flag">wxALL</property>
+                                                <property name="proportion">0</property>
+                                                <object class="wxButton" expanded="1">
+                                                    <property name="BottomDockable">1</property>
+                                                    <property name="LeftDockable">1</property>
+                                                    <property name="RightDockable">1</property>
+                                                    <property name="TopDockable">1</property>
+                                                    <property name="aui_layer"></property>
+                                                    <property name="aui_name"></property>
+                                                    <property name="aui_position"></property>
+                                                    <property name="aui_row"></property>
+                                                    <property name="best_size"></property>
+                                                    <property name="bg"></property>
+                                                    <property name="bitmap"></property>
+                                                    <property name="caption"></property>
+                                                    <property name="caption_visible">1</property>
+                                                    <property name="center_pane">0</property>
+                                                    <property name="close_button">1</property>
+                                                    <property name="context_help"></property>
+                                                    <property name="context_menu">1</property>
+                                                    <property name="current"></property>
+                                                    <property name="default">1</property>
+                                                    <property name="default_pane">0</property>
+                                                    <property name="disabled"></property>
+                                                    <property name="dock">Dock</property>
+                                                    <property name="dock_fixed">0</property>
+                                                    <property name="docking">Left</property>
+                                                    <property name="enabled">1</property>
+                                                    <property name="fg"></property>
+                                                    <property name="floatable">1</property>
+                                                    <property name="focus"></property>
+                                                    <property name="font"></property>
+                                                    <property name="gripper">0</property>
+                                                    <property name="hidden">0</property>
+                                                    <property name="id">wxID_ANY</property>
+                                                    <property name="label">Backup &amp;&amp; Migrate</property>
+                                                    <property name="margins"></property>
+                                                    <property name="markup">0</property>
+                                                    <property name="max_size"></property>
+                                                    <property name="maximize_button">0</property>
+                                                    <property name="maximum_size"></property>
+                                                    <property name="min_size"></property>
+                                                    <property name="minimize_button">0</property>
+                                                    <property name="minimum_size"></property>
+                                                    <property name="moveable">1</property>
+                                                    <property name="name">migrate_backup_and_migrate_button</property>
+                                                    <property name="pane_border">1</property>
+                                                    <property name="pane_position"></property>
+                                                    <property name="pane_size"></property>
+                                                    <property name="permission">protected</property>
+                                                    <property name="pin_button">1</property>
+                                                    <property name="pos"></property>
+                                                    <property name="position"></property>
+                                                    <property name="pressed"></property>
+                                                    <property name="resize">Resizable</property>
+                                                    <property name="show">1</property>
+                                                    <property name="size"></property>
+                                                    <property name="style"></property>
+                                                    <property name="subclass">; ; forward_declare</property>
+                                                    <property name="toolbar_pane">0</property>
+                                                    <property name="tooltip"></property>
+                                                    <property name="validator_data_type"></property>
+                                                    <property name="validator_style">wxFILTER_NONE</property>
+                                                    <property name="validator_type">wxDefaultValidator</property>
+                                                    <property name="validator_variable"></property>
+                                                    <property name="window_extra_style"></property>
+                                                    <property name="window_name"></property>
+                                                    <property name="window_style"></property>
+                                                    <event name="OnButtonClick">migrate_button_clicked</event>
+                                                </object>
+                                            </object>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">auto_start_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer2122</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Launch on System startup</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">auto_start_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Espanso can be launched automatically when you start your PC. &#x0A;&#x0A;Do you want to proceed?</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">auto_start_description</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxCheckBox" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="checked">1</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Yes, launch Espanso on system startup (recommended)</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">auto_start_checkbox</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Note: you can always disable this option later.</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">auto_start_note</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Continue</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">auto_start_continue</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">auto_start_continue_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">add_path_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer212</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Add to PATH</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">add_path_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxLEFT|wxRIGHT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems.&#x0A;&#x0A;To be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed?&#x0A;</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">add_path_description</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxCheckBox" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="checked">1</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Yes, add espanso to PATH</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">add_path_checkbox</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Note: if you don&apos;t know what the PATH env variable is, you should probably keep this checked.</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">add_path_note</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">500</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag">wxEXPAND</property>
+                                        <property name="proportion">1</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">0</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Continue</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">add_path_continue_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">add_path_continue_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                        <object class="simplebookpage" expanded="1">
+                            <property name="label">a page</property>
+                            <property name="select">0</property>
+                            <object class="wxPanel" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg">wxSYS_COLOUR_WINDOW</property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">accessibility_panel</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style">wxTAB_TRAVERSAL</property>
+                                <object class="wxBoxSizer" expanded="1">
+                                    <property name="minimum_size"></property>
+                                    <property name="name">bSizer2121</property>
+                                    <property name="orient">wxVERTICAL</property>
+                                    <property name="permission">none</property>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">20</property>
+                                        <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxStaticText" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font">,90,92,18,70,0</property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Enable Accessibility</property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">accessibility_title</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <property name="wrap">-1</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">5</property>
+                                        <property name="flag"></property>
+                                        <property name="proportion">0</property>
+                                        <object class="spacer" expanded="1">
+                                            <property name="height">20</property>
+                                            <property name="permission">protected</property>
+                                            <property name="width">0</property>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">0</property>
+                                        <property name="flag">wxEXPAND | wxALL</property>
+                                        <property name="proportion">1</property>
+                                        <object class="wxScrolledWindow" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">m_scrolledWindow1</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="scroll_rate_x">5</property>
+                                            <property name="scroll_rate_y">5</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style">wxVSCROLL</property>
+                                            <object class="wxBoxSizer" expanded="1">
+                                                <property name="minimum_size"></property>
+                                                <property name="name">bSizer81</property>
+                                                <property name="orient">wxVERTICAL</property>
+                                                <property name="permission">none</property>
+                                                <object class="sizeritem" expanded="1">
+                                                    <property name="border">10</property>
+                                                    <property name="flag">wxLEFT|wxRIGHT|wxTOP</property>
+                                                    <property name="proportion">0</property>
+                                                    <object class="wxStaticText" expanded="1">
+                                                        <property name="BottomDockable">1</property>
+                                                        <property name="LeftDockable">1</property>
+                                                        <property name="RightDockable">1</property>
+                                                        <property name="TopDockable">1</property>
+                                                        <property name="aui_layer"></property>
+                                                        <property name="aui_name"></property>
+                                                        <property name="aui_position"></property>
+                                                        <property name="aui_row"></property>
+                                                        <property name="best_size"></property>
+                                                        <property name="bg"></property>
+                                                        <property name="caption"></property>
+                                                        <property name="caption_visible">1</property>
+                                                        <property name="center_pane">0</property>
+                                                        <property name="close_button">1</property>
+                                                        <property name="context_help"></property>
+                                                        <property name="context_menu">1</property>
+                                                        <property name="default_pane">0</property>
+                                                        <property name="dock">Dock</property>
+                                                        <property name="dock_fixed">0</property>
+                                                        <property name="docking">Left</property>
+                                                        <property name="enabled">1</property>
+                                                        <property name="fg"></property>
+                                                        <property name="floatable">1</property>
+                                                        <property name="font"></property>
+                                                        <property name="gripper">0</property>
+                                                        <property name="hidden">0</property>
+                                                        <property name="id">wxID_ANY</property>
+                                                        <property name="label">Espanso needs Accessibility permissions to detect and insert snippets into applications. &#x0A;&#x0A;To enable it, follow these steps:&#x0A;&#x0A;1. Click on &quot;Enable&quot; (at the bottom right)&#x0A;2. In the dialog that appears, click on &quot;Open System Preferences&quot;&#x0A;</property>
+                                                        <property name="markup">0</property>
+                                                        <property name="max_size"></property>
+                                                        <property name="maximize_button">0</property>
+                                                        <property name="maximum_size"></property>
+                                                        <property name="min_size"></property>
+                                                        <property name="minimize_button">0</property>
+                                                        <property name="minimum_size"></property>
+                                                        <property name="moveable">1</property>
+                                                        <property name="name">accessibility_description</property>
+                                                        <property name="pane_border">1</property>
+                                                        <property name="pane_position"></property>
+                                                        <property name="pane_size"></property>
+                                                        <property name="permission">protected</property>
+                                                        <property name="pin_button">1</property>
+                                                        <property name="pos"></property>
+                                                        <property name="resize">Resizable</property>
+                                                        <property name="show">1</property>
+                                                        <property name="size"></property>
+                                                        <property name="style"></property>
+                                                        <property name="subclass">; ; forward_declare</property>
+                                                        <property name="toolbar_pane">0</property>
+                                                        <property name="tooltip"></property>
+                                                        <property name="window_extra_style"></property>
+                                                        <property name="window_name"></property>
+                                                        <property name="window_style"></property>
+                                                        <property name="wrap">500</property>
+                                                    </object>
+                                                </object>
+                                                <object class="sizeritem" expanded="1">
+                                                    <property name="border">5</property>
+                                                    <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALL</property>
+                                                    <property name="proportion">0</property>
+                                                    <object class="wxStaticBitmap" expanded="1">
+                                                        <property name="BottomDockable">1</property>
+                                                        <property name="LeftDockable">1</property>
+                                                        <property name="RightDockable">1</property>
+                                                        <property name="TopDockable">1</property>
+                                                        <property name="aui_layer"></property>
+                                                        <property name="aui_name"></property>
+                                                        <property name="aui_position"></property>
+                                                        <property name="aui_row"></property>
+                                                        <property name="best_size"></property>
+                                                        <property name="bg"></property>
+                                                        <property name="bitmap">Load From File; </property>
+                                                        <property name="caption"></property>
+                                                        <property name="caption_visible">1</property>
+                                                        <property name="center_pane">0</property>
+                                                        <property name="close_button">1</property>
+                                                        <property name="context_help"></property>
+                                                        <property name="context_menu">1</property>
+                                                        <property name="default_pane">0</property>
+                                                        <property name="dock">Dock</property>
+                                                        <property name="dock_fixed">0</property>
+                                                        <property name="docking">Left</property>
+                                                        <property name="enabled">1</property>
+                                                        <property name="fg"></property>
+                                                        <property name="floatable">1</property>
+                                                        <property name="font"></property>
+                                                        <property name="gripper">0</property>
+                                                        <property name="hidden">0</property>
+                                                        <property name="id">wxID_ANY</property>
+                                                        <property name="max_size"></property>
+                                                        <property name="maximize_button">0</property>
+                                                        <property name="maximum_size"></property>
+                                                        <property name="min_size"></property>
+                                                        <property name="minimize_button">0</property>
+                                                        <property name="minimum_size"></property>
+                                                        <property name="moveable">1</property>
+                                                        <property name="name">accessibility_image1</property>
+                                                        <property name="pane_border">1</property>
+                                                        <property name="pane_position"></property>
+                                                        <property name="pane_size"></property>
+                                                        <property name="permission">protected</property>
+                                                        <property name="pin_button">1</property>
+                                                        <property name="pos"></property>
+                                                        <property name="resize">Resizable</property>
+                                                        <property name="show">1</property>
+                                                        <property name="size"></property>
+                                                        <property name="subclass">; ; forward_declare</property>
+                                                        <property name="toolbar_pane">0</property>
+                                                        <property name="tooltip"></property>
+                                                        <property name="window_extra_style"></property>
+                                                        <property name="window_name"></property>
+                                                        <property name="window_style"></property>
+                                                    </object>
+                                                </object>
+                                                <object class="sizeritem" expanded="1">
+                                                    <property name="border">10</property>
+                                                    <property name="flag">wxALL</property>
+                                                    <property name="proportion">0</property>
+                                                    <object class="wxStaticText" expanded="1">
+                                                        <property name="BottomDockable">1</property>
+                                                        <property name="LeftDockable">1</property>
+                                                        <property name="RightDockable">1</property>
+                                                        <property name="TopDockable">1</property>
+                                                        <property name="aui_layer"></property>
+                                                        <property name="aui_name"></property>
+                                                        <property name="aui_position"></property>
+                                                        <property name="aui_row"></property>
+                                                        <property name="best_size"></property>
+                                                        <property name="bg"></property>
+                                                        <property name="caption"></property>
+                                                        <property name="caption_visible">1</property>
+                                                        <property name="center_pane">0</property>
+                                                        <property name="close_button">1</property>
+                                                        <property name="context_help"></property>
+                                                        <property name="context_menu">1</property>
+                                                        <property name="default_pane">0</property>
+                                                        <property name="dock">Dock</property>
+                                                        <property name="dock_fixed">0</property>
+                                                        <property name="docking">Left</property>
+                                                        <property name="enabled">1</property>
+                                                        <property name="fg"></property>
+                                                        <property name="floatable">1</property>
+                                                        <property name="font"></property>
+                                                        <property name="gripper">0</property>
+                                                        <property name="hidden">0</property>
+                                                        <property name="id">wxID_ANY</property>
+                                                        <property name="label">3. Then, under the &quot;Privacy&quot; panel click on the Lock icon (1) to enable edits and then check &quot;Espanso&quot; (2), as shown in the picture:</property>
+                                                        <property name="markup">0</property>
+                                                        <property name="max_size"></property>
+                                                        <property name="maximize_button">0</property>
+                                                        <property name="maximum_size"></property>
+                                                        <property name="min_size"></property>
+                                                        <property name="minimize_button">0</property>
+                                                        <property name="minimum_size"></property>
+                                                        <property name="moveable">1</property>
+                                                        <property name="name">accessibility_description2</property>
+                                                        <property name="pane_border">1</property>
+                                                        <property name="pane_position"></property>
+                                                        <property name="pane_size"></property>
+                                                        <property name="permission">protected</property>
+                                                        <property name="pin_button">1</property>
+                                                        <property name="pos"></property>
+                                                        <property name="resize">Resizable</property>
+                                                        <property name="show">1</property>
+                                                        <property name="size"></property>
+                                                        <property name="style"></property>
+                                                        <property name="subclass">; ; forward_declare</property>
+                                                        <property name="toolbar_pane">0</property>
+                                                        <property name="tooltip"></property>
+                                                        <property name="window_extra_style"></property>
+                                                        <property name="window_name"></property>
+                                                        <property name="window_style"></property>
+                                                        <property name="wrap">500</property>
+                                                    </object>
+                                                </object>
+                                                <object class="sizeritem" expanded="1">
+                                                    <property name="border">5</property>
+                                                    <property name="flag">wxALIGN_CENTER_HORIZONTAL|wxALL</property>
+                                                    <property name="proportion">0</property>
+                                                    <object class="wxStaticBitmap" expanded="1">
+                                                        <property name="BottomDockable">1</property>
+                                                        <property name="LeftDockable">1</property>
+                                                        <property name="RightDockable">1</property>
+                                                        <property name="TopDockable">1</property>
+                                                        <property name="aui_layer"></property>
+                                                        <property name="aui_name"></property>
+                                                        <property name="aui_position"></property>
+                                                        <property name="aui_row"></property>
+                                                        <property name="best_size"></property>
+                                                        <property name="bg"></property>
+                                                        <property name="bitmap">Load From File; </property>
+                                                        <property name="caption"></property>
+                                                        <property name="caption_visible">1</property>
+                                                        <property name="center_pane">0</property>
+                                                        <property name="close_button">1</property>
+                                                        <property name="context_help"></property>
+                                                        <property name="context_menu">1</property>
+                                                        <property name="default_pane">0</property>
+                                                        <property name="dock">Dock</property>
+                                                        <property name="dock_fixed">0</property>
+                                                        <property name="docking">Left</property>
+                                                        <property name="enabled">1</property>
+                                                        <property name="fg"></property>
+                                                        <property name="floatable">1</property>
+                                                        <property name="font"></property>
+                                                        <property name="gripper">0</property>
+                                                        <property name="hidden">0</property>
+                                                        <property name="id">wxID_ANY</property>
+                                                        <property name="max_size"></property>
+                                                        <property name="maximize_button">0</property>
+                                                        <property name="maximum_size"></property>
+                                                        <property name="min_size"></property>
+                                                        <property name="minimize_button">0</property>
+                                                        <property name="minimum_size"></property>
+                                                        <property name="moveable">1</property>
+                                                        <property name="name">accessibility_image2</property>
+                                                        <property name="pane_border">1</property>
+                                                        <property name="pane_position"></property>
+                                                        <property name="pane_size"></property>
+                                                        <property name="permission">protected</property>
+                                                        <property name="pin_button">1</property>
+                                                        <property name="pos"></property>
+                                                        <property name="resize">Resizable</property>
+                                                        <property name="show">1</property>
+                                                        <property name="size"></property>
+                                                        <property name="subclass">; ; forward_declare</property>
+                                                        <property name="toolbar_pane">0</property>
+                                                        <property name="tooltip"></property>
+                                                        <property name="window_extra_style"></property>
+                                                        <property name="window_name"></property>
+                                                        <property name="window_style"></property>
+                                                    </object>
+                                                </object>
+                                            </object>
+                                        </object>
+                                    </object>
+                                    <object class="sizeritem" expanded="1">
+                                        <property name="border">10</property>
+                                        <property name="flag">wxALIGN_RIGHT|wxALL</property>
+                                        <property name="proportion">0</property>
+                                        <object class="wxButton" expanded="1">
+                                            <property name="BottomDockable">1</property>
+                                            <property name="LeftDockable">1</property>
+                                            <property name="RightDockable">1</property>
+                                            <property name="TopDockable">1</property>
+                                            <property name="aui_layer"></property>
+                                            <property name="aui_name"></property>
+                                            <property name="aui_position"></property>
+                                            <property name="aui_row"></property>
+                                            <property name="best_size"></property>
+                                            <property name="bg"></property>
+                                            <property name="bitmap"></property>
+                                            <property name="caption"></property>
+                                            <property name="caption_visible">1</property>
+                                            <property name="center_pane">0</property>
+                                            <property name="close_button">1</property>
+                                            <property name="context_help"></property>
+                                            <property name="context_menu">1</property>
+                                            <property name="current"></property>
+                                            <property name="default">1</property>
+                                            <property name="default_pane">0</property>
+                                            <property name="disabled"></property>
+                                            <property name="dock">Dock</property>
+                                            <property name="dock_fixed">0</property>
+                                            <property name="docking">Left</property>
+                                            <property name="enabled">1</property>
+                                            <property name="fg"></property>
+                                            <property name="floatable">1</property>
+                                            <property name="focus"></property>
+                                            <property name="font"></property>
+                                            <property name="gripper">0</property>
+                                            <property name="hidden">0</property>
+                                            <property name="id">wxID_ANY</property>
+                                            <property name="label">Enable</property>
+                                            <property name="margins"></property>
+                                            <property name="markup">0</property>
+                                            <property name="max_size"></property>
+                                            <property name="maximize_button">0</property>
+                                            <property name="maximum_size"></property>
+                                            <property name="min_size"></property>
+                                            <property name="minimize_button">0</property>
+                                            <property name="minimum_size"></property>
+                                            <property name="moveable">1</property>
+                                            <property name="name">accessibility_enable_button</property>
+                                            <property name="pane_border">1</property>
+                                            <property name="pane_position"></property>
+                                            <property name="pane_size"></property>
+                                            <property name="permission">protected</property>
+                                            <property name="pin_button">1</property>
+                                            <property name="pos"></property>
+                                            <property name="position"></property>
+                                            <property name="pressed"></property>
+                                            <property name="resize">Resizable</property>
+                                            <property name="show">1</property>
+                                            <property name="size"></property>
+                                            <property name="style"></property>
+                                            <property name="subclass">; ; forward_declare</property>
+                                            <property name="toolbar_pane">0</property>
+                                            <property name="tooltip"></property>
+                                            <property name="validator_data_type"></property>
+                                            <property name="validator_style">wxFILTER_NONE</property>
+                                            <property name="validator_type">wxDefaultValidator</property>
+                                            <property name="validator_variable"></property>
+                                            <property name="window_extra_style"></property>
+                                            <property name="window_name"></property>
+                                            <property name="window_style"></property>
+                                            <event name="OnButtonClick">accessibility_enable_clicked</event>
+                                        </object>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                    </object>
+                </object>
+            </object>
+        </object>
+    </object>
+</wxFormBuilder_Project>
diff --git a/espanso-modulo/src/sys/wizard/wizard_gui.cpp b/espanso-modulo/src/sys/wizard/wizard_gui.cpp
new file mode 100644
index 00000000..bb537553
--- /dev/null
+++ b/espanso-modulo/src/sys/wizard/wizard_gui.cpp
@@ -0,0 +1,388 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#define _UNICODE
+
+#include "wizard_gui.h"
+
+///////////////////////////////////////////////////////////////////////////
+
+WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style )
+{
+	this->SetSizeHints( wxDefaultSize, wxDefaultSize );
+	this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	check_timer.SetOwner( this, wxID_ANY );
+	check_timer.Start( 500 );
+
+	wxBoxSizer* bSizer1;
+	bSizer1 = new wxBoxSizer( wxVERTICAL );
+
+	m_simplebook = new wxSimplebook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
+	welcome_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	welcome_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer2;
+	bSizer2 = new wxBoxSizer( wxVERTICAL );
+
+	welcome_image = new wxStaticBitmap( welcome_panel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( 256,256 ), 0 );
+	welcome_image->SetMinSize( wxSize( 256,256 ) );
+
+	bSizer2->Add( welcome_image, 0, wxALIGN_CENTER|wxALL, 0 );
+
+	welcome_title_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("Welcome to Espanso!"), wxDefaultPosition, wxDefaultSize, 0 );
+	welcome_title_text->Wrap( -1 );
+	welcome_title_text->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer2->Add( welcome_title_text, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 20 );
+
+	welcome_version_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("(version 1.2.3)"), wxDefaultPosition, wxDefaultSize, 0 );
+	welcome_version_text->Wrap( -1 );
+	bSizer2->Add( welcome_version_text, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 );
+
+
+	bSizer2->Add( 0, 20, 0, 0, 5 );
+
+	welcome_description_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("This wizard will help you to quickly get started with espanso. \n\nClick \"Start\" when you are ready"), wxDefaultPosition, wxDefaultSize, 0 );
+	welcome_description_text->Wrap( -1 );
+	bSizer2->Add( welcome_description_text, 0, wxALL, 10 );
+
+
+	bSizer2->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	welcome_start_button = new wxButton( welcome_panel, wxID_ANY, wxT("Start"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	welcome_start_button->SetDefault();
+	bSizer2->Add( welcome_start_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	welcome_panel->SetSizer( bSizer2 );
+	welcome_panel->Layout();
+	bSizer2->Fit( welcome_panel );
+	m_simplebook->AddPage( welcome_panel, wxT("a page"), false );
+	move_bundle_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	move_bundle_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer22;
+	bSizer22 = new wxBoxSizer( wxVERTICAL );
+
+	move_bundle_title = new wxStaticText( move_bundle_panel, wxID_ANY, wxT("Move to /Applications folder"), wxDefaultPosition, wxDefaultSize, 0 );
+	move_bundle_title->Wrap( -1 );
+	move_bundle_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer22->Add( move_bundle_title, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 20 );
+
+
+	bSizer22->Add( 0, 20, 0, 0, 5 );
+
+	move_bundle_description = new wxStaticText( move_bundle_panel, wxID_ANY, wxT("Espanso is being run from outside the Applications directory, which prevents it from working correctly.\n\nPlease move the Espanso.app bundle inside your Applications folder and start it again.\n"), wxDefaultPosition, wxDefaultSize, 0 );
+	move_bundle_description->Wrap( -1 );
+	bSizer22->Add( move_bundle_description, 0, wxALL, 10 );
+
+
+	bSizer22->Add( 0, 20, 1, wxEXPAND, 5 );
+
+	move_bundle_quit_button = new wxButton( move_bundle_panel, wxID_ANY, wxT("Start"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	move_bundle_quit_button->SetDefault();
+	bSizer22->Add( move_bundle_quit_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	move_bundle_panel->SetSizer( bSizer22 );
+	move_bundle_panel->Layout();
+	bSizer22->Fit( move_bundle_panel );
+	m_simplebook->AddPage( move_bundle_panel, wxT("a page"), false );
+	legacy_version_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	legacy_version_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer21;
+	bSizer21 = new wxBoxSizer( wxVERTICAL );
+
+	legacy_version_title = new wxStaticText( legacy_version_panel, wxID_ANY, wxT("Legacy version detected"), wxDefaultPosition, wxDefaultSize, 0 );
+	legacy_version_title->Wrap( -1 );
+	legacy_version_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer21->Add( legacy_version_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer21->Add( 0, 20, 0, 0, 5 );
+
+	legacy_version_description = new wxStaticText( legacy_version_panel, wxID_ANY, wxT("A legacy espanso process has been detected and prevents the new version from working correctly.\n\nPlease terminate and uninstall the old espanso version to proceed.\n\nIf you already uninstalled the previous version, you might need to restart your computer for changes to be detected.\n\nFor more information, see: "), wxDefaultPosition, wxDefaultSize, 0 );
+	legacy_version_description->Wrap( 500 );
+	bSizer21->Add( legacy_version_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	legacy_version_docs_link = new wxHyperlinkCtrl( legacy_version_panel, wxID_ANY, wxT("https://espanso.org/legacy/uninstall"), wxT("https://espanso.org/legacy/uninstall"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
+	bSizer21->Add( legacy_version_docs_link, 0, wxLEFT|wxRIGHT, 10 );
+
+
+	bSizer21->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	legacy_version_continue_button = new wxButton( legacy_version_panel, wxID_ANY, wxT("Continue"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	legacy_version_continue_button->SetDefault();
+	legacy_version_continue_button->Enable( false );
+
+	bSizer21->Add( legacy_version_continue_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	legacy_version_panel->SetSizer( bSizer21 );
+	legacy_version_panel->Layout();
+	bSizer21->Fit( legacy_version_panel );
+	m_simplebook->AddPage( legacy_version_panel, wxT("a page"), false );
+	wrong_edition_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	wrong_edition_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer213;
+	bSizer213 = new wxBoxSizer( wxVERTICAL );
+
+	wrong_edition_title = new wxStaticText( wrong_edition_panel, wxID_ANY, wxT("Incompatibility detected"), wxDefaultPosition, wxDefaultSize, 0 );
+	wrong_edition_title->Wrap( -1 );
+	wrong_edition_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer213->Add( wrong_edition_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer213->Add( 0, 20, 0, 0, 5 );
+
+	wrong_edition_description_x11 = new wxStaticText( wrong_edition_panel, wxID_ANY, wxT("This version of espanso was compiled to support X11-based systems, but it seems you are on a Wayland-based desktop environment.\n\nUnfortunately, the two versions are incompatible. To use espanso, either switch to an X11-based environment or download the Wayland version from the website.\n\nFor more information:"), wxDefaultPosition, wxDefaultSize, 0 );
+	wrong_edition_description_x11->Wrap( 500 );
+	bSizer213->Add( wrong_edition_description_x11, 0, wxEXPAND|wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	wrong_edition_description_wayland = new wxStaticText( wrong_edition_panel, wxID_ANY, wxT("This version of espanso was compiled to support Wayland-based systems, but it seems you are on a X11-based desktop environment.\n\nUnfortunately, the two versions are incompatible. To use espanso, either switch to a Wayland-based environment or download the X11 version from the website.\n\nFor more information:"), wxDefaultPosition, wxDefaultSize, 0 );
+	wrong_edition_description_wayland->Wrap( 500 );
+	bSizer213->Add( wrong_edition_description_wayland, 0, wxEXPAND|wxLEFT|wxTOP, 10 );
+
+	wrong_edition_link = new wxHyperlinkCtrl( wrong_edition_panel, wxID_ANY, wxT("https://espanso.org/install"), wxT("https://espanso.org/install"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
+	bSizer213->Add( wrong_edition_link, 0, wxLEFT|wxRIGHT, 10 );
+
+
+	bSizer213->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	wrong_edition_button = new wxButton( wrong_edition_panel, wxID_ANY, wxT("Quit Espanso"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	wrong_edition_button->SetDefault();
+	bSizer213->Add( wrong_edition_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	wrong_edition_panel->SetSizer( bSizer213 );
+	wrong_edition_panel->Layout();
+	bSizer213->Fit( wrong_edition_panel );
+	m_simplebook->AddPage( wrong_edition_panel, wxT("a page"), false );
+	migrate_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	migrate_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer211;
+	bSizer211 = new wxBoxSizer( wxVERTICAL );
+
+	migrate_title = new wxStaticText( migrate_panel, wxID_ANY, wxT("Migrate configuration"), wxDefaultPosition, wxDefaultSize, 0 );
+	migrate_title->Wrap( -1 );
+	migrate_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer211->Add( migrate_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer211->Add( 0, 20, 0, 0, 5 );
+
+	migrate_description = new wxStaticText( migrate_panel, wxID_ANY, wxT("The new version uses a slightly different configuration format that powers some exciting new features.\n\nTo ease the transition, espanso offers two possible choices: \n\n  - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). \n  - Use compatibility mode without changing the configs. \n\nKeep in mind that: \n\n  - Compatibility mode does not support all new espanso features \n  - You can always migrate the configs later \n\nFor more information, see: "), wxDefaultPosition, wxDefaultSize, 0 );
+	migrate_description->Wrap( 500 );
+	bSizer211->Add( migrate_description, 1, wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	migrate_link = new wxHyperlinkCtrl( migrate_panel, wxID_ANY, wxT("https://espanso.org/migration"), wxT("https://espanso.org/migration"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
+	bSizer211->Add( migrate_link, 0, wxLEFT|wxRIGHT, 10 );
+
+
+	bSizer211->Add( 0, 0, 10, wxEXPAND, 5 );
+
+	wxBoxSizer* bSizer8;
+	bSizer8 = new wxBoxSizer( wxHORIZONTAL );
+
+	migrate_compatibility_mode_button = new wxButton( migrate_panel, wxID_ANY, wxT("Use compatibility mode"), wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer8->Add( migrate_compatibility_mode_button, 0, wxALL, 10 );
+
+
+	bSizer8->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	migrate_backup_and_migrate_button = new wxButton( migrate_panel, wxID_ANY, wxT("Backup && Migrate"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	migrate_backup_and_migrate_button->SetDefault();
+	bSizer8->Add( migrate_backup_and_migrate_button, 0, wxALL, 10 );
+
+
+	bSizer211->Add( bSizer8, 1, wxEXPAND, 5 );
+
+
+	migrate_panel->SetSizer( bSizer211 );
+	migrate_panel->Layout();
+	bSizer211->Fit( migrate_panel );
+	m_simplebook->AddPage( migrate_panel, wxT("a page"), false );
+	auto_start_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	auto_start_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer2122;
+	bSizer2122 = new wxBoxSizer( wxVERTICAL );
+
+	auto_start_title = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Launch on System startup"), wxDefaultPosition, wxDefaultSize, 0 );
+	auto_start_title->Wrap( -1 );
+	auto_start_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer2122->Add( auto_start_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer2122->Add( 0, 20, 0, 0, 5 );
+
+	auto_start_description = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Espanso can be launched automatically when you start your PC. \n\nDo you want to proceed?"), wxDefaultPosition, wxDefaultSize, 0 );
+	auto_start_description->Wrap( 500 );
+	bSizer2122->Add( auto_start_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	auto_start_checkbox = new wxCheckBox( auto_start_panel, wxID_ANY, wxT("Yes, launch Espanso on system startup (recommended)"), wxDefaultPosition, wxDefaultSize, 0 );
+	auto_start_checkbox->SetValue(true);
+	bSizer2122->Add( auto_start_checkbox, 0, wxALL, 20 );
+
+	auto_start_note = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Note: you can always disable this option later."), wxDefaultPosition, wxDefaultSize, 0 );
+	auto_start_note->Wrap( 500 );
+	bSizer2122->Add( auto_start_note, 0, wxALL, 10 );
+
+
+	bSizer2122->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	auto_start_continue = new wxButton( auto_start_panel, wxID_ANY, wxT("Continue"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	auto_start_continue->SetDefault();
+	bSizer2122->Add( auto_start_continue, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	auto_start_panel->SetSizer( bSizer2122 );
+	auto_start_panel->Layout();
+	bSizer2122->Fit( auto_start_panel );
+	m_simplebook->AddPage( auto_start_panel, wxT("a page"), false );
+	add_path_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	add_path_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer212;
+	bSizer212 = new wxBoxSizer( wxVERTICAL );
+
+	add_path_title = new wxStaticText( add_path_panel, wxID_ANY, wxT("Add to PATH"), wxDefaultPosition, wxDefaultSize, 0 );
+	add_path_title->Wrap( -1 );
+	add_path_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer212->Add( add_path_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer212->Add( 0, 20, 0, 0, 5 );
+
+	add_path_description = new wxStaticText( add_path_panel, wxID_ANY, wxT("Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems.\n\nTo be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed?\n"), wxDefaultPosition, wxDefaultSize, 0 );
+	add_path_description->Wrap( 500 );
+	bSizer212->Add( add_path_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	add_path_checkbox = new wxCheckBox( add_path_panel, wxID_ANY, wxT("Yes, add espanso to PATH"), wxDefaultPosition, wxDefaultSize, 0 );
+	add_path_checkbox->SetValue(true);
+	bSizer212->Add( add_path_checkbox, 0, wxALL, 20 );
+
+	add_path_note = new wxStaticText( add_path_panel, wxID_ANY, wxT("Note: if you don't know what the PATH env variable is, you should probably keep this checked."), wxDefaultPosition, wxDefaultSize, 0 );
+	add_path_note->Wrap( 500 );
+	bSizer212->Add( add_path_note, 0, wxALL, 10 );
+
+
+	bSizer212->Add( 0, 0, 1, wxEXPAND, 5 );
+
+	add_path_continue_button = new wxButton( add_path_panel, wxID_ANY, wxT("Continue"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	add_path_continue_button->SetDefault();
+	bSizer212->Add( add_path_continue_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	add_path_panel->SetSizer( bSizer212 );
+	add_path_panel->Layout();
+	bSizer212->Fit( add_path_panel );
+	m_simplebook->AddPage( add_path_panel, wxT("a page"), false );
+	accessibility_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+	accessibility_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+	wxBoxSizer* bSizer2121;
+	bSizer2121 = new wxBoxSizer( wxVERTICAL );
+
+	accessibility_title = new wxStaticText( accessibility_panel, wxID_ANY, wxT("Enable Accessibility"), wxDefaultPosition, wxDefaultSize, 0 );
+	accessibility_title->Wrap( -1 );
+	accessibility_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+	bSizer2121->Add( accessibility_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 );
+
+
+	bSizer2121->Add( 0, 20, 0, 0, 5 );
+
+	m_scrolledWindow1 = new wxScrolledWindow( accessibility_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL );
+	m_scrolledWindow1->SetScrollRate( 5, 5 );
+	wxBoxSizer* bSizer81;
+	bSizer81 = new wxBoxSizer( wxVERTICAL );
+
+	accessibility_description = new wxStaticText( m_scrolledWindow1, wxID_ANY, wxT("Espanso needs Accessibility permissions to detect and insert snippets into applications. \n\nTo enable it, follow these steps:\n\n1. Click on \"Enable\" (at the bottom right)\n2. In the dialog that appears, click on \"Open System Preferences\"\n"), wxDefaultPosition, wxDefaultSize, 0 );
+	accessibility_description->Wrap( 500 );
+	bSizer81->Add( accessibility_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 );
+
+	accessibility_image1 = new wxStaticBitmap( m_scrolledWindow1, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer81->Add( accessibility_image1, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 );
+
+	accessibility_description2 = new wxStaticText( m_scrolledWindow1, wxID_ANY, wxT("3. Then, under the \"Privacy\" panel click on the Lock icon (1) to enable edits and then check \"Espanso\" (2), as shown in the picture:"), wxDefaultPosition, wxDefaultSize, 0 );
+	accessibility_description2->Wrap( 500 );
+	bSizer81->Add( accessibility_description2, 0, wxALL, 10 );
+
+	accessibility_image2 = new wxStaticBitmap( m_scrolledWindow1, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
+	bSizer81->Add( accessibility_image2, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 );
+
+
+	m_scrolledWindow1->SetSizer( bSizer81 );
+	m_scrolledWindow1->Layout();
+	bSizer81->Fit( m_scrolledWindow1 );
+	bSizer2121->Add( m_scrolledWindow1, 1, wxEXPAND | wxALL, 0 );
+
+	accessibility_enable_button = new wxButton( accessibility_panel, wxID_ANY, wxT("Enable"), wxDefaultPosition, wxDefaultSize, 0 );
+
+	accessibility_enable_button->SetDefault();
+	bSizer2121->Add( accessibility_enable_button, 0, wxALIGN_RIGHT|wxALL, 10 );
+
+
+	accessibility_panel->SetSizer( bSizer2121 );
+	accessibility_panel->Layout();
+	bSizer2121->Fit( accessibility_panel );
+	m_simplebook->AddPage( accessibility_panel, wxT("a page"), false );
+
+	bSizer1->Add( m_simplebook, 1, wxEXPAND | wxALL, 5 );
+
+
+	this->SetSizer( bSizer1 );
+	this->Layout();
+
+	this->Centre( wxBOTH );
+
+	// Connect Events
+	this->Connect( wxID_ANY, wxEVT_TIMER, wxTimerEventHandler( WizardFrame::check_timer_tick ) );
+	m_simplebook->Connect( wxEVT_COMMAND_BOOKCTRL_PAGE_CHANGED, wxBookCtrlEventHandler( WizardFrame::on_page_changed ), NULL, this );
+	welcome_start_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::welcome_start_clicked ), NULL, this );
+	move_bundle_quit_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::move_bundle_quit_clicked ), NULL, this );
+	wrong_edition_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::quit_espanso_clicked ), NULL, this );
+	migrate_compatibility_mode_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::migrate_compatibility_mode_clicked ), NULL, this );
+	migrate_backup_and_migrate_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::migrate_button_clicked ), NULL, this );
+	auto_start_continue->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::auto_start_continue_clicked ), NULL, this );
+	add_path_continue_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::add_path_continue_clicked ), NULL, this );
+	accessibility_enable_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::accessibility_enable_clicked ), NULL, this );
+}
+
+WizardFrame::~WizardFrame()
+{
+	// Disconnect Events
+	this->Disconnect( wxID_ANY, wxEVT_TIMER, wxTimerEventHandler( WizardFrame::check_timer_tick ) );
+	m_simplebook->Disconnect( wxEVT_COMMAND_BOOKCTRL_PAGE_CHANGED, wxBookCtrlEventHandler( WizardFrame::on_page_changed ), NULL, this );
+	welcome_start_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::welcome_start_clicked ), NULL, this );
+	move_bundle_quit_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::move_bundle_quit_clicked ), NULL, this );
+	wrong_edition_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::quit_espanso_clicked ), NULL, this );
+	migrate_compatibility_mode_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::migrate_compatibility_mode_clicked ), NULL, this );
+	migrate_backup_and_migrate_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::migrate_button_clicked ), NULL, this );
+	auto_start_continue->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::auto_start_continue_clicked ), NULL, this );
+	add_path_continue_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::add_path_continue_clicked ), NULL, this );
+	accessibility_enable_button->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WizardFrame::accessibility_enable_clicked ), NULL, this );
+
+}
diff --git a/espanso-modulo/src/sys/wizard/wizard_gui.h b/espanso-modulo/src/sys/wizard/wizard_gui.h
new file mode 100644
index 00000000..1a9e24a3
--- /dev/null
+++ b/espanso-modulo/src/sys/wizard/wizard_gui.h
@@ -0,0 +1,113 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <wx/artprov.h>
+#include <wx/xrc/xmlres.h>
+#include <wx/timer.h>
+#include <wx/bitmap.h>
+#include <wx/image.h>
+#include <wx/icon.h>
+#include <wx/statbmp.h>
+#include <wx/gdicmn.h>
+#include <wx/font.h>
+#include <wx/colour.h>
+#include <wx/settings.h>
+#include <wx/string.h>
+#include <wx/stattext.h>
+#include <wx/button.h>
+#include <wx/sizer.h>
+#include <wx/panel.h>
+#include <wx/hyperlink.h>
+#include <wx/checkbox.h>
+#include <wx/scrolwin.h>
+#include <wx/simplebook.h>
+#include <wx/frame.h>
+
+///////////////////////////////////////////////////////////////////////////
+
+
+///////////////////////////////////////////////////////////////////////////////
+/// Class WizardFrame
+///////////////////////////////////////////////////////////////////////////////
+class WizardFrame : public wxFrame
+{
+	private:
+
+	protected:
+		wxTimer check_timer;
+		wxSimplebook* m_simplebook;
+		wxPanel* welcome_panel;
+		wxStaticBitmap* welcome_image;
+		wxStaticText* welcome_title_text;
+		wxStaticText* welcome_version_text;
+		wxStaticText* welcome_description_text;
+		wxButton* welcome_start_button;
+		wxPanel* move_bundle_panel;
+		wxStaticText* move_bundle_title;
+		wxStaticText* move_bundle_description;
+		wxButton* move_bundle_quit_button;
+		wxPanel* legacy_version_panel;
+		wxStaticText* legacy_version_title;
+		wxStaticText* legacy_version_description;
+		wxHyperlinkCtrl* legacy_version_docs_link;
+		wxButton* legacy_version_continue_button;
+		wxPanel* wrong_edition_panel;
+		wxStaticText* wrong_edition_title;
+		wxStaticText* wrong_edition_description_x11;
+		wxStaticText* wrong_edition_description_wayland;
+		wxHyperlinkCtrl* wrong_edition_link;
+		wxButton* wrong_edition_button;
+		wxPanel* migrate_panel;
+		wxStaticText* migrate_title;
+		wxStaticText* migrate_description;
+		wxHyperlinkCtrl* migrate_link;
+		wxButton* migrate_compatibility_mode_button;
+		wxButton* migrate_backup_and_migrate_button;
+		wxPanel* auto_start_panel;
+		wxStaticText* auto_start_title;
+		wxStaticText* auto_start_description;
+		wxCheckBox* auto_start_checkbox;
+		wxStaticText* auto_start_note;
+		wxButton* auto_start_continue;
+		wxPanel* add_path_panel;
+		wxStaticText* add_path_title;
+		wxStaticText* add_path_description;
+		wxCheckBox* add_path_checkbox;
+		wxStaticText* add_path_note;
+		wxButton* add_path_continue_button;
+		wxPanel* accessibility_panel;
+		wxStaticText* accessibility_title;
+		wxScrolledWindow* m_scrolledWindow1;
+		wxStaticText* accessibility_description;
+		wxStaticBitmap* accessibility_image1;
+		wxStaticText* accessibility_description2;
+		wxStaticBitmap* accessibility_image2;
+		wxButton* accessibility_enable_button;
+
+		// Virtual event handlers, overide them in your derived class
+		virtual void check_timer_tick( wxTimerEvent& event ) { event.Skip(); }
+		virtual void on_page_changed( wxBookCtrlEvent& event ) { event.Skip(); }
+		virtual void welcome_start_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void move_bundle_quit_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void quit_espanso_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void migrate_compatibility_mode_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void migrate_button_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void auto_start_continue_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void add_path_continue_clicked( wxCommandEvent& event ) { event.Skip(); }
+		virtual void accessibility_enable_clicked( wxCommandEvent& event ) { event.Skip(); }
+
+
+	public:
+
+		WizardFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 550,577 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL );
+
+		~WizardFrame();
+
+};
+
diff --git a/espanso-modulo/src/troubleshooting.rs b/espanso-modulo/src/troubleshooting.rs
new file mode 100644
index 00000000..b19dc7de
--- /dev/null
+++ b/espanso-modulo/src/troubleshooting.rs
@@ -0,0 +1,50 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::path::{Path, PathBuf};
+
+pub use crate::sys::troubleshooting::show;
+
+pub struct TroubleshootingOptions {
+  pub window_icon_path: Option<String>,
+  pub error_sets: Vec<ErrorSet>,
+  pub is_fatal_error: bool,
+
+  pub handlers: TroubleshootingHandlers,
+}
+
+pub struct ErrorSet {
+  pub file: Option<PathBuf>,
+  pub errors: Vec<ErrorRecord>,
+}
+
+pub struct ErrorRecord {
+  pub level: ErrorLevel,
+  pub message: String,
+}
+
+pub enum ErrorLevel {
+  Error,
+  Warning,
+}
+
+pub struct TroubleshootingHandlers {
+  pub dont_show_again_changed: Option<Box<dyn Fn(bool) + Send>>,
+  pub open_file: Option<Box<dyn Fn(&Path) + Send>>,
+}
diff --git a/espanso-modulo/src/welcome/mod.rs b/espanso-modulo/src/welcome/mod.rs
new file mode 100644
index 00000000..4d7c2fee
--- /dev/null
+++ b/espanso-modulo/src/welcome/mod.rs
@@ -0,0 +1,32 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub use crate::sys::welcome::show;
+
+pub struct WelcomeOptions {
+  pub window_icon_path: Option<String>,
+  pub tray_image_path: Option<String>,
+  pub is_already_running: bool,
+
+  pub handlers: WelcomeHandlers,
+}
+
+pub struct WelcomeHandlers {
+  pub dont_show_again_changed: Option<Box<dyn Fn(bool) + Send>>,
+}
diff --git a/espanso-modulo/src/wizard/mod.rs b/espanso-modulo/src/wizard/mod.rs
new file mode 100644
index 00000000..afe90db9
--- /dev/null
+++ b/espanso-modulo/src/wizard/mod.rs
@@ -0,0 +1,66 @@
+/*
+ * This file is part of modulo.
+ *
+ * Copyright (C) 2020-2021 Federico Terzi
+ *
+ * modulo 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.
+ *
+ * modulo 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 modulo.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub use crate::sys::wizard::show;
+
+pub struct WizardOptions {
+  pub version: String,
+
+  pub is_welcome_page_enabled: bool,
+  pub is_move_bundle_page_enabled: bool,
+  pub is_legacy_version_page_enabled: bool,
+  pub is_wrong_edition_page_enabled: bool,
+  pub is_migrate_page_enabled: bool,
+  pub is_auto_start_page_enabled: bool,
+  pub is_add_path_page_enabled: bool,
+  pub is_accessibility_page_enabled: bool,
+
+  pub window_icon_path: Option<String>,
+  pub welcome_image_path: Option<String>,
+  pub accessibility_image_1_path: Option<String>,
+  pub accessibility_image_2_path: Option<String>,
+  pub detected_os: DetectedOS,
+
+  pub handlers: WizardHandlers,
+}
+
+pub struct WizardHandlers {
+  pub is_legacy_version_running: Option<Box<dyn Fn() -> bool + Send>>,
+  pub backup_and_migrate: Option<Box<dyn Fn() -> MigrationResult + Send>>,
+  pub auto_start: Option<Box<dyn Fn(bool) -> bool + Send>>,
+  pub add_to_path: Option<Box<dyn Fn() -> bool + Send>>,
+  pub enable_accessibility: Option<Box<dyn Fn() + Send>>,
+  pub is_accessibility_enabled: Option<Box<dyn Fn() -> bool + Send>>,
+  pub on_completed: Option<Box<dyn Fn() + Send>>,
+}
+
+#[derive(Debug)]
+pub enum MigrationResult {
+  Success,
+  CleanFailure,
+  DirtyFailure,
+  UnknownFailure,
+}
+
+#[derive(Debug)]
+pub enum DetectedOS {
+  Unknown,
+  X11,
+  Wayland,
+}
diff --git a/espanso-modulo/vendor/wxWidgets-3.1.5.zip b/espanso-modulo/vendor/wxWidgets-3.1.5.zip
new file mode 100644
index 00000000..ef81d886
Binary files /dev/null and b/espanso-modulo/vendor/wxWidgets-3.1.5.zip differ
diff --git a/espanso-package/Cargo.toml b/espanso-package/Cargo.toml
new file mode 100644
index 00000000..0ceeeb6c
--- /dev/null
+++ b/espanso-package/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "espanso-package"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+serde = { version = "1.0.123", features = ["derive"] }
+serde_json = "1.0.62"
+serde_yaml = "0.8.17"
+tempdir = "0.3.7"
+glob = "0.3.0"
+natord = "1.0.9"
+reqwest = { version = "0.11.4", features = ["blocking"] }
+lazy_static = "1.4.0"
+regex = "1.4.3"
+zip = "0.5.13"
+scopeguard = "1.1.0"
+fs_extra = "1.2.0"
+sha2 = "0.9.6"
+hex = "0.4.3"
\ No newline at end of file
diff --git a/espanso-package/src/archive/default.rs b/espanso-package/src/archive/default.rs
new file mode 100644
index 00000000..56ccfdf4
--- /dev/null
+++ b/espanso-package/src/archive/default.rs
@@ -0,0 +1,346 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{bail, Context, Result};
+use std::path::{Path, PathBuf};
+
+use crate::{
+  manifest::Manifest, ArchivedPackage, Archiver, Package, PackageSpecifier, SaveOptions,
+};
+
+use super::{LegacyPackage, PackageSource, StoredPackage, PACKAGE_SOURCE_FILE};
+
+pub struct DefaultArchiver {
+  package_dir: PathBuf,
+}
+
+impl DefaultArchiver {
+  pub fn new(package_dir: &Path) -> Self {
+    Self {
+      package_dir: package_dir.to_owned(),
+    }
+  }
+}
+
+impl Archiver for DefaultArchiver {
+  fn save(
+    &self,
+    package: &dyn Package,
+    specifier: &PackageSpecifier,
+    save_options: &SaveOptions,
+  ) -> Result<ArchivedPackage> {
+    let target_dir = self.package_dir.join(package.name());
+
+    if target_dir.is_dir() && !save_options.overwrite_existing {
+      bail!("package {} is already installed", package.name());
+    }
+
+    // Backup the previous directory if present
+    let backup_dir = self.package_dir.join(&format!("{}.old", package.name()));
+    let _backup_guard = if target_dir.is_dir() {
+      std::fs::rename(&target_dir, &backup_dir)
+        .context("unable to backup old package directory")?;
+
+      // If the function returns due to an error, restore the previous directory
+      Some(scopeguard::guard(
+        (backup_dir.clone(), target_dir.clone()),
+        |(backup_dir, target_dir)| {
+          if backup_dir.is_dir() {
+            if target_dir.is_dir() {
+              std::fs::remove_dir_all(&target_dir)
+                .expect("unable to remove dirty package directory");
+            }
+
+            std::fs::rename(backup_dir, target_dir).expect("unable to restore backup directory");
+          }
+        },
+      ))
+    } else {
+      None
+    };
+
+    std::fs::create_dir_all(&target_dir).context("unable to create target directory")?;
+
+    super::util::copy_dir_without_dot_files(package.location(), &target_dir)
+      .context("unable to copy package files")?;
+
+    super::util::create_package_source_file(specifier, &target_dir)
+      .context("unable to create _pkgsource.yml file")?;
+
+    // Remove backup
+    if backup_dir.is_dir() {
+      std::fs::remove_dir_all(backup_dir).context("unable to remove backup directory")?;
+    }
+
+    let archived_package =
+      super::read::read_archived_package(&target_dir).context("unable to load archived package")?;
+
+    Ok(archived_package)
+  }
+
+  fn get(&self, name: &str) -> Result<StoredPackage> {
+    let target_dir = self.package_dir.join(name);
+
+    if !target_dir.is_dir() {
+      bail!("package '{}' not found", name);
+    }
+
+    let manifest_path = target_dir.join("_manifest.yml");
+    if !manifest_path.is_file() {
+      return Ok(StoredPackage::Legacy(LegacyPackage {
+        name: name.to_string(),
+      }));
+    }
+
+    let manifest = Manifest::parse(&manifest_path).context("unable to parse package manifest")?;
+
+    let source_path = target_dir.join(PACKAGE_SOURCE_FILE);
+    let source =
+      PackageSource::parse(&source_path).context("unable to parse package source file")?;
+
+    Ok(StoredPackage::Modern(ArchivedPackage { manifest, source }))
+  }
+
+  fn list(&self) -> Result<Vec<StoredPackage>> {
+    let mut output = Vec::new();
+
+    for path in std::fs::read_dir(&self.package_dir)? {
+      let path = path?.path();
+      if !path.is_dir() {
+        continue;
+      }
+
+      if let Some(package_name) = path.file_name() {
+        let package_name = package_name.to_string_lossy().to_string();
+
+        output.push(self.get(&package_name)?);
+      }
+    }
+
+    Ok(output)
+  }
+
+  fn delete(&self, name: &str) -> Result<()> {
+    let target_dir = self.package_dir.join(name);
+
+    if !target_dir.is_dir() {
+      bail!("package {} not found", name);
+    }
+
+    std::fs::remove_dir_all(&target_dir).context("unable to remove package directory")?;
+
+    Ok(())
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use std::fs::{create_dir_all, write};
+  use tempdir::TempDir;
+
+  use crate::{manifest::Manifest, package::DefaultPackage, tests::run_with_temp_dir};
+
+  use super::*;
+
+  fn create_fake_package(dest_dir: &Path) -> Box<dyn Package> {
+    let package_dir = dest_dir.join("package1");
+    create_dir_all(&package_dir).unwrap();
+
+    write(
+      package_dir.join("_manifest.yml"),
+      r#"
+name: "package1"
+title: "Dummy package"
+description: A dummy package for testing
+version: 0.1.0
+author: Federico Terzi
+    "#,
+    )
+    .unwrap();
+
+    write(
+      package_dir.join("package.yml"),
+      r#"
+matches:
+  - trigger: ":hello"
+    replace: "github"name: "package1"
+    "#,
+    )
+    .unwrap();
+
+    write(
+      package_dir.join("README.md"),
+      r#"
+    A very dummy package
+    "#,
+    )
+    .unwrap();
+
+    let package = DefaultPackage::new(
+      Manifest::parse(&package_dir.join("_manifest.yml")).unwrap(),
+      TempDir::new("fake-package").unwrap(),
+      package_dir,
+    );
+
+    Box::new(package)
+  }
+
+  fn run_with_two_temp_dirs(action: impl FnOnce(&Path, &Path)) {
+    run_with_temp_dir(|base| {
+      let dir1 = base.join("dir1");
+      let dir2 = base.join("dir2");
+      create_dir_all(&dir1).unwrap();
+      create_dir_all(&dir2).unwrap();
+      action(&dir1, &dir2);
+    });
+  }
+
+  #[test]
+  fn test_package_saved_correctly() {
+    run_with_two_temp_dirs(|package_dir, dest_dir| {
+      let package = create_fake_package(package_dir);
+
+      let archiver = DefaultArchiver::new(dest_dir);
+      let result = archiver.save(
+        &*package,
+        &PackageSpecifier {
+          name: "package1".to_string(),
+          git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+          git_branch: Some("main".to_string()),
+          ..Default::default()
+        },
+        &SaveOptions::default(),
+      );
+
+      assert!(result.is_ok());
+
+      let package_out_dir = dest_dir.join("package1");
+      assert!(package_out_dir.is_dir());
+      assert!(package_out_dir.join("_manifest.yml").is_file());
+      assert!(package_out_dir.join("README.md").is_file());
+      assert!(package_out_dir.join("package.yml").is_file());
+      assert!(package_out_dir.join("_pkgsource.yml").is_file());
+    });
+  }
+
+  #[test]
+  fn test_package_already_present() {
+    run_with_two_temp_dirs(|package_dir, dest_dir| {
+      let package = create_fake_package(package_dir);
+
+      create_dir_all(dest_dir.join("package1")).unwrap();
+
+      let archiver = DefaultArchiver::new(dest_dir);
+      let result = archiver.save(
+        &*package,
+        &PackageSpecifier {
+          name: "package1".to_string(),
+          git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+          git_branch: Some("main".to_string()),
+          ..Default::default()
+        },
+        &SaveOptions::default(),
+      );
+
+      assert!(result.is_err());
+
+      let result = archiver.save(
+        &*package,
+        &PackageSpecifier {
+          name: "package1".to_string(),
+          git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+          git_branch: Some("main".to_string()),
+          ..Default::default()
+        },
+        &SaveOptions {
+          overwrite_existing: true,
+        },
+      );
+
+      assert!(result.is_ok());
+    });
+  }
+
+  #[test]
+  fn test_delete_package() {
+    run_with_two_temp_dirs(|package_dir, dest_dir| {
+      let package = create_fake_package(package_dir);
+
+      let archiver = DefaultArchiver::new(dest_dir);
+      let result = archiver.save(
+        &*package,
+        &PackageSpecifier {
+          name: "package1".to_string(),
+          git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+          git_branch: Some("main".to_string()),
+          ..Default::default()
+        },
+        &SaveOptions::default(),
+      );
+
+      assert!(result.is_ok());
+
+      let package_out_dir = dest_dir.join("package1");
+      assert!(package_out_dir.is_dir());
+
+      archiver.delete("package1").unwrap();
+
+      assert!(!package_out_dir.is_dir());
+    });
+  }
+
+  #[test]
+  fn test_list_packages() {
+    run_with_two_temp_dirs(|package_dir, dest_dir| {
+      let package = create_fake_package(package_dir);
+
+      let archiver = DefaultArchiver::new(dest_dir);
+      let result = archiver.save(
+        &*package,
+        &PackageSpecifier {
+          name: "package1".to_string(),
+          git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+          git_branch: Some("main".to_string()),
+          ..Default::default()
+        },
+        &SaveOptions::default(),
+      );
+
+      assert!(result.is_ok());
+
+      let package_out_dir = dest_dir.join("package1");
+      assert!(package_out_dir.is_dir());
+
+      let legacy_package = dest_dir.join("z_legacypackage1");
+      create_dir_all(&legacy_package).unwrap();
+
+      let package_list = archiver.list().unwrap();
+
+      assert!(package_list.iter().any(|package| *package
+        == StoredPackage::Modern(ArchivedPackage {
+          manifest: Manifest::parse(&package_out_dir.join("_manifest.yml")).unwrap(),
+          source: PackageSource::parse(&package_out_dir.join(PACKAGE_SOURCE_FILE)).unwrap(),
+        })));
+      assert!(package_list.iter().any(|package| *package
+        == StoredPackage::Legacy(LegacyPackage {
+          name: "z_legacypackage1".to_string()
+        })));
+    });
+  }
+}
diff --git a/espanso-package/src/archive/mod.rs b/espanso-package/src/archive/mod.rs
new file mode 100644
index 00000000..62ae7df5
--- /dev/null
+++ b/espanso-package/src/archive/mod.rs
@@ -0,0 +1,135 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+
+use crate::{manifest::Manifest, Package, PackageSpecifier};
+
+pub mod default;
+mod read;
+mod util;
+
+pub const PACKAGE_SOURCE_FILE: &str = "_pkgsource.yml";
+
+#[derive(Debug, PartialEq)]
+pub struct ArchivedPackage {
+  // Metadata
+  pub manifest: Manifest,
+
+  // Package source information (needed to update)
+  pub source: PackageSource,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct LegacyPackage {
+  pub name: String,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum StoredPackage {
+  Legacy(LegacyPackage),
+  Modern(ArchivedPackage),
+}
+
+pub trait Archiver {
+  fn get(&self, name: &str) -> Result<StoredPackage>;
+  fn save(
+    &self,
+    package: &dyn Package,
+    specifier: &PackageSpecifier,
+    save_options: &SaveOptions,
+  ) -> Result<ArchivedPackage>;
+  fn list(&self) -> Result<Vec<StoredPackage>>;
+  fn delete(&self, name: &str) -> Result<()>;
+}
+
+#[derive(Debug, Default)]
+pub struct SaveOptions {
+  pub overwrite_existing: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum PackageSource {
+  Hub,
+  Git {
+    repo_url: String,
+    repo_branch: Option<String>,
+    use_native_git: bool,
+  },
+}
+
+impl PackageSource {
+  pub fn parse(source_path: &Path) -> Result<Self> {
+    let source_str = std::fs::read_to_string(source_path)?;
+    Ok(serde_yaml::from_str(&source_str)?)
+  }
+}
+
+impl std::fmt::Display for PackageSource {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match self {
+      PackageSource::Hub => write!(f, "espanso-hub"),
+      PackageSource::Git {
+        repo_url,
+        repo_branch: _,
+        use_native_git: _,
+      } => write!(f, "git: {}", repo_url),
+    }
+  }
+}
+
+impl From<&PackageSpecifier> for PackageSource {
+  fn from(package: &PackageSpecifier) -> Self {
+    if let Some(git_repo_url) = package.git_repo_url.as_deref() {
+      Self::Git {
+        repo_url: git_repo_url.to_string(),
+        repo_branch: package.git_branch.clone(),
+        use_native_git: package.use_native_git,
+      }
+    } else {
+      Self::Hub
+    }
+  }
+}
+
+impl From<&ArchivedPackage> for PackageSpecifier {
+  fn from(package: &ArchivedPackage) -> Self {
+    match &package.source {
+      PackageSource::Hub => PackageSpecifier {
+        name: package.manifest.name.to_string(),
+        ..Default::default()
+      },
+      PackageSource::Git {
+        repo_url,
+        repo_branch,
+        use_native_git,
+      } => PackageSpecifier {
+        name: package.manifest.name.to_string(),
+        git_repo_url: Some(repo_url.to_string()),
+        git_branch: repo_branch.clone(),
+        use_native_git: *use_native_git,
+        ..Default::default()
+      },
+    }
+  }
+}
diff --git a/espanso-package/src/archive/read.rs b/espanso-package/src/archive/read.rs
new file mode 100644
index 00000000..dfd2b979
--- /dev/null
+++ b/espanso-package/src/archive/read.rs
@@ -0,0 +1,48 @@
+/*
+ * 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::path::Path;
+
+use anyhow::{bail, Context, Result};
+
+use crate::{manifest::Manifest, ArchivedPackage};
+
+use super::{PackageSource, PACKAGE_SOURCE_FILE};
+
+pub fn read_archived_package(containing_dir: &Path) -> Result<ArchivedPackage> {
+  let manifest_path = containing_dir.join("_manifest.yml");
+  if !manifest_path.is_file() {
+    bail!("missing _manifest.yml file");
+  }
+
+  let source_path = containing_dir.join(PACKAGE_SOURCE_FILE);
+  let source = if source_path.is_file() {
+    let yaml = std::fs::read_to_string(&source_path)?;
+    let source: PackageSource =
+      serde_yaml::from_str(&yaml).context("unable to parse package source file.")?;
+    source
+  } else {
+    // Fallback to hub installation
+    PackageSource::Hub
+  };
+
+  let manifest = Manifest::parse(&manifest_path).context("unable to parse manifest file")?;
+
+  Ok(ArchivedPackage { manifest, source })
+}
diff --git a/espanso-package/src/archive/util.rs b/espanso-package/src/archive/util.rs
new file mode 100644
index 00000000..25394f39
--- /dev/null
+++ b/espanso-package/src/archive/util.rs
@@ -0,0 +1,60 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use fs_extra::dir::CopyOptions;
+
+use crate::PackageSpecifier;
+
+use super::{PackageSource, PACKAGE_SOURCE_FILE};
+
+// TODO: test
+pub fn copy_dir_without_dot_files(source_dir: &Path, inside_dir: &Path) -> Result<()> {
+  fs_extra::dir::copy(
+    source_dir,
+    inside_dir,
+    &CopyOptions {
+      copy_inside: true,
+      content_only: true,
+      ..Default::default()
+    },
+  )?;
+
+  // Remove dot files and dirs (such as .git)
+  let mut to_be_removed = Vec::new();
+  for path in std::fs::read_dir(inside_dir)? {
+    let path = path?.path();
+    if path.starts_with(".") {
+      to_be_removed.push(path);
+    }
+  }
+
+  fs_extra::remove_items(&to_be_removed)?;
+
+  Ok(())
+}
+
+pub fn create_package_source_file(specifier: &PackageSpecifier, target_dir: &Path) -> Result<()> {
+  let source: PackageSource = specifier.into();
+  let yaml = serde_yaml::to_string(&source)?;
+  std::fs::write(target_dir.join(PACKAGE_SOURCE_FILE), yaml)?;
+  Ok(())
+}
diff --git a/espanso-package/src/lib.rs b/espanso-package/src/lib.rs
new file mode 100644
index 00000000..c5d793c0
--- /dev/null
+++ b/espanso-package/src/lib.rs
@@ -0,0 +1,112 @@
+/*
+ * 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::path::Path;
+
+use anyhow::{bail, Result};
+
+mod archive;
+#[macro_use]
+mod logging;
+mod manifest;
+mod package;
+mod provider;
+mod resolver;
+mod util;
+
+pub use archive::{ArchivedPackage, Archiver, SaveOptions, StoredPackage};
+pub use package::Package;
+pub use provider::{PackageProvider, PackageSpecifier, ProviderOptions};
+
+pub fn get_provider(
+  package: &PackageSpecifier,
+  runtime_dir: &Path,
+  options: &ProviderOptions,
+) -> Result<Box<dyn PackageProvider>> {
+  if let Some(git_repo_url) = package.git_repo_url.as_deref() {
+    if !package.use_native_git {
+      let matches_known_hosts =
+        if let Some(github_parts) = util::github::extract_github_url_parts(git_repo_url) {
+          if let Some(repo_scheme) =
+            util::github::resolve_repo_scheme(github_parts, package.git_branch.as_deref())?
+          {
+            return Ok(Box::new(provider::github::GitHubPackageProvider::new(
+              repo_scheme.author,
+              repo_scheme.name,
+              repo_scheme.branch,
+            )));
+          }
+
+          true
+        } else if let Some(gitlab_parts) = util::gitlab::extract_gitlab_url_parts(git_repo_url) {
+          if let Some(repo_scheme) =
+            util::gitlab::resolve_repo_scheme(gitlab_parts, package.git_branch.as_deref())?
+          {
+            return Ok(Box::new(provider::gitlab::GitLabPackageProvider::new(
+              repo_scheme.author,
+              repo_scheme.name,
+              repo_scheme.branch,
+            )));
+          }
+
+          true
+        } else {
+          false
+        };
+
+      // Git repository seems to be in one of the known hosts, but the direct methods
+      // couldn't retrieve its content. This might happen with private repos (as they are not
+      // available to non-authenticated requests), so we check if a "git ls-remote" command
+      // is able to access it.
+      if matches_known_hosts && !util::git::is_private_repo(git_repo_url) {
+        bail!("could not access repository: {}, make sure it exists and that you have the necessary access rights.", git_repo_url);
+      }
+    }
+
+    // Git repository is neither on Github or Gitlab
+    // OR it's a private repository, which means we can't use the direct method
+    // (because it's not authenticated)
+    Ok(Box::new(provider::git::GitPackageProvider::new()))
+  } else {
+    // Download from the official espanso hub
+    Ok(Box::new(provider::hub::EspansoHubPackageProvider::new(
+      runtime_dir,
+      options.force_index_update,
+    )))
+  }
+}
+
+pub fn get_archiver(package_dir: &Path) -> Result<Box<dyn Archiver>> {
+  Ok(Box::new(archive::default::DefaultArchiver::new(
+    package_dir,
+  )))
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+  use super::*;
+  use tempdir::TempDir;
+
+  pub(crate) fn run_with_temp_dir(action: impl FnOnce(&Path)) {
+    let tmp_dir = TempDir::new("espanso-package").unwrap();
+    let tmp_path = tmp_dir.path();
+
+    action(tmp_path);
+  }
+}
diff --git a/espanso-package/src/logging.rs b/espanso-package/src/logging.rs
new file mode 100644
index 00000000..3e31c13f
--- /dev/null
+++ b/espanso-package/src/logging.rs
@@ -0,0 +1,23 @@
+#[macro_export]
+macro_rules! info_println {
+  ($($tts:tt)*) => {
+    println!($($tts)*);
+    log::info!($($tts)*);
+  }
+}
+
+#[macro_export]
+macro_rules! warn_eprintln {
+  ($($tts:tt)*) => {
+    eprintln!($($tts)*);
+    log::warn!($($tts)*);
+  }
+}
+
+#[macro_export]
+macro_rules! error_eprintln {
+  ($($tts:tt)*) => {
+    eprintln!($($tts)*);
+    log::error!($($tts)*);
+  }
+}
diff --git a/espanso-package/src/manifest.rs b/espanso-package/src/manifest.rs
new file mode 100644
index 00000000..172a3a1b
--- /dev/null
+++ b/espanso-package/src/manifest.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::path::Path;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct Manifest {
+  pub name: String,
+  pub title: String,
+  pub description: String,
+  pub version: String,
+  pub author: String,
+}
+
+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)?)
+  }
+}
+
+// TODO: test
diff --git a/espanso-package/src/package/default.rs b/espanso-package/src/package/default.rs
new file mode 100644
index 00000000..b5abe9ba
--- /dev/null
+++ b/espanso-package/src/package/default.rs
@@ -0,0 +1,70 @@
+/*
+ * 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::path::PathBuf;
+
+use tempdir::TempDir;
+
+use crate::{manifest::Manifest, Package};
+
+#[allow(dead_code)]
+pub struct DefaultPackage {
+  manifest: Manifest,
+
+  temp_dir: TempDir,
+
+  // Sub-directory inside the temp_dir
+  location: PathBuf,
+}
+
+impl DefaultPackage {
+  pub fn new(manifest: Manifest, temp_dir: TempDir, location: PathBuf) -> Self {
+    Self {
+      manifest,
+      temp_dir,
+      location,
+    }
+  }
+}
+
+impl Package for DefaultPackage {
+  fn name(&self) -> &str {
+    self.manifest.name.as_str()
+  }
+
+  fn title(&self) -> &str {
+    self.manifest.title.as_str()
+  }
+
+  fn description(&self) -> &str {
+    self.manifest.description.as_str()
+  }
+
+  fn version(&self) -> &str {
+    self.manifest.version.as_str()
+  }
+
+  fn author(&self) -> &str {
+    self.manifest.author.as_str()
+  }
+
+  fn location(&self) -> &std::path::Path {
+    self.location.as_path()
+  }
+}
diff --git a/espanso-package/src/package/mod.rs b/espanso-package/src/package/mod.rs
new file mode 100644
index 00000000..a8c559c2
--- /dev/null
+++ b/espanso-package/src/package/mod.rs
@@ -0,0 +1,51 @@
+/*
+ * 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::path::Path;
+
+pub mod default;
+
+pub trait Package {
+  // Manifest
+  fn name(&self) -> &str;
+  fn title(&self) -> &str;
+  fn description(&self) -> &str;
+  fn version(&self) -> &str;
+  fn author(&self) -> &str;
+
+  // Directory containing the package files
+  fn location(&self) -> &Path;
+}
+
+impl std::fmt::Debug for dyn Package {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    writeln!(
+      f,
+      "name: {}\nversion: {}\ntitle: {}\ndescription: {}\nauthor: {}\nlocation: {:?}",
+      self.name(),
+      self.version(),
+      self.title(),
+      self.description(),
+      self.author(),
+      self.location()
+    )
+  }
+}
+
+pub use default::DefaultPackage;
diff --git a/espanso-package/src/provider/git.rs b/espanso-package/src/provider/git.rs
new file mode 100644
index 00000000..d47896d8
--- /dev/null
+++ b/espanso-package/src/provider/git.rs
@@ -0,0 +1,100 @@
+/*
+ * 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 super::PackageProvider;
+use crate::{package::DefaultPackage, resolver::resolve_package, Package, PackageSpecifier};
+use anyhow::{anyhow, bail, Context, Result};
+use std::{path::Path, process::Command};
+
+pub struct GitPackageProvider {}
+
+impl GitPackageProvider {
+  pub fn new() -> Self {
+    Self {}
+  }
+
+  fn is_git_installed() -> bool {
+    if let Ok(output) = Command::new("git").arg("--version").output() {
+      if output.status.success() {
+        return true;
+      }
+    }
+
+    false
+  }
+
+  fn clone_repo(dest_dir: &Path, repo_url: &str, repo_branch: Option<&str>) -> Result<()> {
+    let mut args = vec!["clone"];
+
+    if let Some(branch) = repo_branch {
+      args.push("-b");
+      args.push(branch);
+    }
+
+    args.push(repo_url);
+
+    let dest_dir_str = dest_dir.to_string_lossy().to_string();
+    args.push(&dest_dir_str);
+
+    let output = Command::new("git")
+      .args(&args)
+      .output()
+      .context("git command reported error")?;
+
+    if !output.status.success() {
+      let stderr = String::from_utf8_lossy(&output.stderr);
+      bail!("git command exited with non-zero status: {}", stderr);
+    } else {
+      Ok(())
+    }
+  }
+}
+
+impl PackageProvider for GitPackageProvider {
+  fn name(&self) -> String {
+    "git".to_string()
+  }
+
+  fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
+    if !Self::is_git_installed() {
+      bail!("unable to invoke 'git' command, please make sure it is installed and visible in PATH");
+    }
+
+    let repo_url = package
+      .git_repo_url
+      .as_deref()
+      .ok_or_else(|| anyhow!("missing git repository url"))?;
+    let repo_branch = package.git_branch.as_deref();
+
+    let temp_dir = tempdir::TempDir::new("espanso-package-download")?;
+
+    Self::clone_repo(temp_dir.path(), repo_url, repo_branch)?;
+
+    let resolved_package =
+      resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?;
+
+    let package = DefaultPackage::new(
+      resolved_package.manifest,
+      temp_dir,
+      resolved_package.base_dir,
+    );
+
+    Ok(Box::new(package))
+  }
+}
diff --git a/espanso-package/src/provider/github.rs b/espanso-package/src/provider/github.rs
new file mode 100644
index 00000000..d9e4f99f
--- /dev/null
+++ b/espanso-package/src/provider/github.rs
@@ -0,0 +1,92 @@
+/*
+ * 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 super::PackageProvider;
+use crate::{package::DefaultPackage, resolver::resolve_package, Package, PackageSpecifier};
+use anyhow::Result;
+
+pub struct GitHubPackageProvider {
+  repo_author: String,
+  repo_name: String,
+  repo_branch: String,
+}
+
+impl GitHubPackageProvider {
+  pub fn new(repo_author: String, repo_name: String, repo_branch: String) -> Self {
+    Self {
+      repo_author,
+      repo_name,
+      repo_branch,
+    }
+  }
+}
+
+impl PackageProvider for GitHubPackageProvider {
+  fn name(&self) -> String {
+    "github".to_string()
+  }
+
+  fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
+    let download_url = format!(
+      "https://github.com/{}/{}/archive/refs/heads/{}.zip",
+      &self.repo_author, &self.repo_name, &self.repo_branch
+    );
+
+    let temp_dir = tempdir::TempDir::new("espanso-package-download")?;
+
+    crate::util::download::download_and_extract_zip(&download_url, temp_dir.path())?;
+
+    let resolved_package =
+      resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?;
+
+    let package = DefaultPackage::new(
+      resolved_package.manifest,
+      temp_dir,
+      resolved_package.base_dir,
+    );
+
+    Ok(Box::new(package))
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  #[ignore]
+  fn test_download_github_package() {
+    let provider = GitHubPackageProvider::new(
+      "espanso".to_string(),
+      "dummy-package".to_string(),
+      "main".to_string(),
+    );
+    provider
+      .download(&PackageSpecifier {
+        name: "dummy-package".to_string(),
+        version: None,
+        git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
+        git_branch: None,
+        ..Default::default()
+      })
+      .unwrap();
+
+    std::thread::sleep(std::time::Duration::from_secs(300));
+  }
+}
diff --git a/espanso-package/src/provider/gitlab.rs b/espanso-package/src/provider/gitlab.rs
new file mode 100644
index 00000000..d1627f31
--- /dev/null
+++ b/espanso-package/src/provider/gitlab.rs
@@ -0,0 +1,66 @@
+/*
+ * 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 super::PackageProvider;
+use crate::{package::DefaultPackage, resolver::resolve_package, Package, PackageSpecifier};
+use anyhow::Result;
+
+pub struct GitLabPackageProvider {
+  repo_author: String,
+  repo_name: String,
+  repo_branch: String,
+}
+
+impl GitLabPackageProvider {
+  pub fn new(repo_author: String, repo_name: String, repo_branch: String) -> Self {
+    Self {
+      repo_author,
+      repo_name,
+      repo_branch,
+    }
+  }
+}
+
+impl PackageProvider for GitLabPackageProvider {
+  fn name(&self) -> String {
+    "gitlab".to_string()
+  }
+
+  fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
+    let download_url = format!(
+      "https://gitlab.com/{}/{}/-/archive/{}/{}-{}.zip",
+      &self.repo_author, &self.repo_name, &self.repo_branch, &self.repo_name, &self.repo_branch
+    );
+
+    let temp_dir = tempdir::TempDir::new("espanso-package-download")?;
+
+    crate::util::download::download_and_extract_zip(&download_url, temp_dir.path())?;
+
+    let resolved_package =
+      resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?;
+
+    let package = DefaultPackage::new(
+      resolved_package.manifest,
+      temp_dir,
+      resolved_package.base_dir,
+    );
+
+    Ok(Box::new(package))
+  }
+}
diff --git a/espanso-package/src/provider/hub.rs b/espanso-package/src/provider/hub.rs
new file mode 100644
index 00000000..17e9feed
--- /dev/null
+++ b/espanso-package/src/provider/hub.rs
@@ -0,0 +1,195 @@
+/*
+ * 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::{
+  path::{Path, PathBuf},
+  time::UNIX_EPOCH,
+};
+
+use super::PackageProvider;
+use crate::{
+  package::DefaultPackage, resolver::resolve_package, util::download::read_string_from_url,
+  Package, PackageSpecifier,
+};
+use anyhow::{anyhow, Context, Result};
+use serde::{Deserialize, Serialize};
+
+pub const ESPANSO_HUB_PACKAGE_INDEX_URL: &str =
+  "https://github.com/espanso/hub/releases/latest/download/package_index.json";
+
+const PACKAGE_INDEX_CACHE_FILE: &str = "package_index_cache.json";
+const PACKAGE_INDEX_CACHE_INVALIDATION_SECONDS: u64 = 60 * 60;
+
+pub struct EspansoHubPackageProvider {
+  runtime_dir: PathBuf,
+  force_index_update: bool,
+}
+
+impl EspansoHubPackageProvider {
+  pub fn new(runtime_dir: &Path, force_index_update: bool) -> Self {
+    Self {
+      runtime_dir: runtime_dir.to_path_buf(),
+      force_index_update,
+    }
+  }
+}
+
+impl PackageProvider for EspansoHubPackageProvider {
+  fn name(&self) -> String {
+    "espanso-hub".to_string()
+  }
+
+  fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
+    let index = self
+      .get_index(self.force_index_update)
+      .context("unable to get package index from espanso hub")?;
+
+    let package_info = index
+      .get_package(&package.name, package.version.as_deref())
+      .ok_or_else(|| {
+        anyhow!(
+          "unable to find package '{}@{}' in the espanso hub",
+          package.name,
+          package.version.as_deref().unwrap_or("latest")
+        )
+      })?;
+
+    let archive_sha256 = read_string_from_url(&package_info.archive_sha256_url)
+      .context("unable to read archive sha256 signature")?;
+
+    let temp_dir = tempdir::TempDir::new("espanso-package-download")?;
+
+    crate::util::download::download_and_extract_zip_verify_sha256(
+      &package_info.archive_url,
+      temp_dir.path(),
+      Some(&archive_sha256),
+    )?;
+
+    let resolved_package =
+      resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?;
+
+    let package = DefaultPackage::new(
+      resolved_package.manifest,
+      temp_dir,
+      resolved_package.base_dir,
+    );
+
+    Ok(Box::new(package))
+  }
+}
+
+impl EspansoHubPackageProvider {
+  fn get_index(&self, force_update: bool) -> Result<PackageIndex> {
+    let old_index = self.get_index_from_cache()?;
+
+    if let Some(old_index) = old_index {
+      if !force_update {
+        let current_time = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?;
+        let current_unix = current_time.as_secs();
+        if old_index.cached_at >= (current_unix - PACKAGE_INDEX_CACHE_INVALIDATION_SECONDS) {
+          info_println!("using cached package index");
+          return Ok(old_index.index);
+        }
+      }
+    }
+
+    let new_index = self.download_index()?;
+    self.save_index_to_cache(new_index.clone())?;
+    Ok(new_index)
+  }
+
+  fn download_index(&self) -> Result<PackageIndex> {
+    info_println!("fetching package index...");
+    let json_body = read_string_from_url(ESPANSO_HUB_PACKAGE_INDEX_URL)?;
+
+    let index: PackageIndex = serde_json::from_str(&json_body)?;
+
+    Ok(index)
+  }
+
+  fn get_index_from_cache(&self) -> Result<Option<CachedPackageIndex>> {
+    let target_file = self.runtime_dir.join(PACKAGE_INDEX_CACHE_FILE);
+    if !target_file.is_file() {
+      return Ok(None);
+    }
+
+    let content =
+      std::fs::read_to_string(&target_file).context("unable to read package index cache")?;
+    let index: CachedPackageIndex = serde_json::from_str(&content)?;
+    Ok(Some(index))
+  }
+
+  fn save_index_to_cache(&self, index: PackageIndex) -> Result<()> {
+    let target_file = self.runtime_dir.join(PACKAGE_INDEX_CACHE_FILE);
+    let current_time = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?;
+    let current_unix = current_time.as_secs();
+    let cached_index = CachedPackageIndex {
+      cached_at: current_unix,
+      index,
+    };
+    let serialized = serde_json::to_string(&cached_index)?;
+    std::fs::write(&target_file, serialized)?;
+    Ok(())
+  }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct CachedPackageIndex {
+  cached_at: u64,
+  index: PackageIndex,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct PackageIndex {
+  last_update: u64,
+  packages: Vec<PackageInfo>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct PackageInfo {
+  name: String,
+  title: String,
+  author: String,
+  description: String,
+  version: String,
+
+  archive_url: String,
+  archive_sha256_url: String,
+}
+
+impl PackageIndex {
+  fn get_package(&self, name: &str, version: Option<&str>) -> Option<PackageInfo> {
+    let mut matching_packages: Vec<PackageInfo> = self
+      .packages
+      .iter()
+      .filter(|package| package.name == name)
+      .cloned()
+      .collect();
+
+    matching_packages.sort_by(|a, b| natord::compare(&a.version, &b.version));
+
+    if let Some(explicit_version) = version {
+      matching_packages
+        .into_iter()
+        .find(|package| package.version == explicit_version)
+    } else {
+      matching_packages.into_iter().last()
+    }
+  }
+}
diff --git a/espanso-package/src/provider/mod.rs b/espanso-package/src/provider/mod.rs
new file mode 100644
index 00000000..a4690057
--- /dev/null
+++ b/espanso-package/src/provider/mod.rs
@@ -0,0 +1,51 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+
+use crate::Package;
+
+pub(crate) mod git;
+pub(crate) mod github;
+pub(crate) mod gitlab;
+pub(crate) mod hub;
+
+#[derive(Debug, Default)]
+pub struct PackageSpecifier {
+  pub name: String,
+  pub version: Option<String>,
+
+  // Source information
+  pub git_repo_url: Option<String>,
+  pub git_branch: Option<String>,
+
+  // Resolution options
+  pub use_native_git: bool,
+}
+
+pub trait PackageProvider {
+  fn name(&self) -> String;
+  fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>>;
+  // TODO: fn check update available? (probably should be only available in the hub)
+}
+
+#[derive(Debug, Default)]
+pub struct ProviderOptions {
+  pub force_index_update: bool,
+}
diff --git a/espanso-package/src/resolver.rs b/espanso-package/src/resolver.rs
new file mode 100644
index 00000000..ceab97b1
--- /dev/null
+++ b/espanso-package/src/resolver.rs
@@ -0,0 +1,354 @@
+/*
+* This file is part of espanso.
+*
+* Copyright (C) 2019-2021 Federico Terzi
+title: (), description: (), version: (), author: ()  *
+* 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::path::{Path, PathBuf};
+
+use anyhow::{anyhow, bail, Context, Result};
+
+use crate::manifest::Manifest;
+
+#[derive(Debug, PartialEq)]
+pub struct ResolvedPackage {
+  pub manifest: Manifest,
+  pub base_dir: PathBuf,
+}
+
+pub fn resolve_package(
+  base_dir: &Path,
+  name: &str,
+  version: Option<&str>,
+) -> Result<ResolvedPackage> {
+  let packages = resolve_all_packages(base_dir)?;
+
+  let mut matching_packages: Vec<ResolvedPackage> = packages
+    .into_iter()
+    .filter(|package| package.manifest.name == name)
+    .collect();
+
+  if matching_packages.is_empty() {
+    bail!("no package found with name: {}", name);
+  }
+
+  matching_packages.sort_by(|a, b| natord::compare(&a.manifest.version, &b.manifest.version));
+
+  let matching_package = if let Some(explicit_version) = version {
+    matching_packages
+      .into_iter()
+      .find(|package| package.manifest.version == explicit_version)
+  } else {
+    matching_packages.into_iter().last()
+  };
+
+  if let Some(matching_package) = matching_package {
+    Ok(matching_package)
+  } else {
+    bail!(
+      "unable to find version: {} for package: {}",
+      version.unwrap_or_default(),
+      name
+    );
+  }
+}
+
+pub fn resolve_all_packages(base_dir: &Path) -> Result<Vec<ResolvedPackage>> {
+  let manifest_files = find_all_manifests(base_dir)?;
+
+  if manifest_files.is_empty() {
+    bail!("no manifests found in base_dir");
+  }
+
+  let mut manifests = Vec::new();
+
+  for manifest_file in manifest_files {
+    let base_dir = manifest_file
+      .parent()
+      .ok_or_else(|| anyhow!("unable to determine base_dir from manifest path"))?
+      .to_owned();
+    let manifest = Manifest::parse(&manifest_file).context("manifest YAML parsing error")?;
+    manifests.push(ResolvedPackage { manifest, base_dir });
+  }
+
+  Ok(manifests)
+}
+
+fn find_all_manifests(base_dir: &Path) -> Result<Vec<PathBuf>> {
+  let pattern = format!("{}/{}", base_dir.to_string_lossy(), "**/_manifest.yml");
+
+  let mut manifests = Vec::new();
+
+  for entry in glob::glob(&pattern)? {
+    let path = entry?;
+    manifests.push(path);
+  }
+
+  Ok(manifests)
+}
+
+#[cfg(test)]
+mod tests {
+  use std::fs::create_dir_all;
+
+  use crate::tests::run_with_temp_dir;
+
+  use super::*;
+
+  #[test]
+  fn test_read_manifest_base_dir() {
+    run_with_temp_dir(|base_dir| {
+      std::fs::write(
+        base_dir.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      version: 0.1.0
+      description: An awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let packages = resolve_all_packages(base_dir).unwrap();
+
+      assert_eq!(
+        packages,
+        vec![ResolvedPackage {
+          manifest: Manifest {
+            name: "package1".to_owned(),
+            title: "Package 1".to_owned(),
+            version: "0.1.0".to_owned(),
+            author: "Federico".to_owned(),
+            description: "An awesome package".to_owned(),
+          },
+          base_dir: base_dir.to_path_buf(),
+        },]
+      )
+    });
+  }
+
+  #[test]
+  fn test_read_manifests_nested_dirs() {
+    run_with_temp_dir(|base_dir| {
+      let sub_dir1 = base_dir.join("package1");
+      let version_dir1 = sub_dir1.join("0.1.0");
+      create_dir_all(&version_dir1).unwrap();
+
+      std::fs::write(
+        version_dir1.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      version: 0.1.0
+      description: An awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let sub_dir2 = base_dir.join("package1");
+      let version_dir2 = sub_dir2.join("0.1.1");
+      create_dir_all(&version_dir2).unwrap();
+
+      std::fs::write(
+        version_dir2.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      version: 0.1.1
+      description: An awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let sub_dir3 = base_dir.join("package2");
+      create_dir_all(&sub_dir3).unwrap();
+
+      std::fs::write(
+        sub_dir3.join("_manifest.yml"),
+        r#"
+      name: package2
+      title: Package 2
+      author: Federico
+      version: 2.0.0
+      description: Another awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let packages = resolve_all_packages(base_dir).unwrap();
+
+      assert_eq!(
+        packages,
+        vec![
+          ResolvedPackage {
+            manifest: Manifest {
+              name: "package1".to_owned(),
+              title: "Package 1".to_owned(),
+              version: "0.1.0".to_owned(),
+              author: "Federico".to_owned(),
+              description: "An awesome package".to_owned(),
+            },
+            base_dir: version_dir1,
+          },
+          ResolvedPackage {
+            manifest: Manifest {
+              name: "package1".to_owned(),
+              title: "Package 1".to_owned(),
+              version: "0.1.1".to_owned(),
+              author: "Federico".to_owned(),
+              description: "An awesome package".to_owned(),
+            },
+            base_dir: version_dir2,
+          },
+          ResolvedPackage {
+            manifest: Manifest {
+              name: "package2".to_owned(),
+              title: "Package 2".to_owned(),
+              version: "2.0.0".to_owned(),
+              author: "Federico".to_owned(),
+              description: "Another awesome package".to_owned(),
+            },
+            base_dir: sub_dir3,
+          },
+        ]
+      )
+    });
+  }
+
+  #[test]
+  fn test_resolve_package() {
+    run_with_temp_dir(|base_dir| {
+      let sub_dir1 = base_dir.join("package1");
+      let version_dir1 = sub_dir1.join("0.1.0");
+      create_dir_all(&version_dir1).unwrap();
+
+      std::fs::write(
+        version_dir1.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      version: 0.1.0
+      description: An awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let sub_dir2 = base_dir.join("package1");
+      let version_dir2 = sub_dir2.join("0.1.1");
+      create_dir_all(&version_dir2).unwrap();
+
+      std::fs::write(
+        version_dir2.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      version: 0.1.1
+      description: An awesome package 
+      "#,
+      )
+      .unwrap();
+
+      let sub_dir3 = base_dir.join("package2");
+      create_dir_all(&sub_dir3).unwrap();
+
+      std::fs::write(
+        sub_dir3.join("_manifest.yml"),
+        r#"
+      name: package2
+      title: Package 2
+      author: Federico
+      version: 2.0.0
+      description: Another awesome package 
+      "#,
+      )
+      .unwrap();
+
+      assert_eq!(
+        resolve_package(base_dir, "package1", None).unwrap(),
+        ResolvedPackage {
+          manifest: Manifest {
+            name: "package1".to_owned(),
+            title: "Package 1".to_owned(),
+            version: "0.1.1".to_owned(),
+            author: "Federico".to_owned(),
+            description: "An awesome package".to_owned(),
+          },
+          base_dir: version_dir2,
+        },
+      );
+
+      assert_eq!(
+        resolve_package(base_dir, "package1", Some("0.1.0")).unwrap(),
+        ResolvedPackage {
+          manifest: Manifest {
+            name: "package1".to_owned(),
+            title: "Package 1".to_owned(),
+            version: "0.1.0".to_owned(),
+            author: "Federico".to_owned(),
+            description: "An awesome package".to_owned(),
+          },
+          base_dir: version_dir1,
+        },
+      );
+
+      assert_eq!(
+        resolve_package(base_dir, "package2", None).unwrap(),
+        ResolvedPackage {
+          manifest: Manifest {
+            name: "package2".to_owned(),
+            title: "Package 2".to_owned(),
+            version: "2.0.0".to_owned(),
+            author: "Federico".to_owned(),
+            description: "Another awesome package".to_owned(),
+          },
+          base_dir: sub_dir3,
+        },
+      );
+
+      assert!(resolve_package(base_dir, "invalid", None).is_err());
+      assert!(resolve_package(base_dir, "package1", Some("9.9.9")).is_err());
+    });
+  }
+
+  #[test]
+  fn test_no_manifest_error() {
+    run_with_temp_dir(|base_dir| {
+      assert!(resolve_all_packages(base_dir).is_err());
+    });
+  }
+
+  #[test]
+  fn test_malformed_manifest() {
+    run_with_temp_dir(|base_dir| {
+      std::fs::write(
+        base_dir.join("_manifest.yml"),
+        r#"
+      name: package1
+      title: Package 1
+      author: Federico
+      "#,
+      )
+      .unwrap();
+
+      assert!(resolve_all_packages(base_dir).is_err());
+    });
+  }
+}
diff --git a/espanso-package/src/util/download.rs b/espanso-package/src/util/download.rs
new file mode 100644
index 00000000..45258a20
--- /dev/null
+++ b/espanso-package/src/util/download.rs
@@ -0,0 +1,99 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{bail, Context, Result};
+use sha2::{Digest, Sha256};
+use std::io::{copy, Cursor};
+use std::path::Path;
+
+pub fn download_and_extract_zip(url: &str, dest_dir: &Path) -> Result<()> {
+  download_and_extract_zip_verify_sha256(url, dest_dir, None)
+}
+
+pub fn download_and_extract_zip_verify_sha256(
+  url: &str,
+  dest_dir: &Path,
+  sha256: Option<&str>,
+) -> Result<()> {
+  let data = download(url).context("error downloading archive")?;
+  if let Some(sha256) = sha256 {
+    info_println!("validating sha256 signature...");
+    if !verify_sha256(&data, sha256) {
+      bail!("signature mismatch");
+    }
+  }
+  extract_zip(data, dest_dir).context("error extracting archive")
+}
+
+pub fn read_string_from_url(url: &str) -> Result<String> {
+  let client = reqwest::blocking::Client::builder();
+  let client = client.build()?;
+
+  let response = client.get(url).send()?;
+
+  Ok(response.text()?)
+}
+
+fn download(url: &str) -> Result<Vec<u8>> {
+  let client = reqwest::blocking::Client::builder();
+  let client = client.build()?;
+
+  let mut response = client.get(url).send()?;
+
+  let mut buffer = Vec::new();
+  copy(&mut response, &mut buffer)?;
+  Ok(buffer)
+}
+
+fn verify_sha256(data: &[u8], sha256: &str) -> bool {
+  let mut hasher = Sha256::new();
+  hasher.update(data);
+  let result = hasher.finalize();
+  let hash = hex::encode(result);
+  hash == sha256
+}
+
+// Adapted from zip-rs extract.rs example
+fn extract_zip(data: Vec<u8>, dest_dir: &Path) -> Result<()> {
+  let reader = Cursor::new(data);
+
+  let mut archive = zip::ZipArchive::new(reader)?;
+
+  for i in 0..archive.len() {
+    let mut file = archive.by_index(i)?;
+    let outpath = match file.enclosed_name() {
+      Some(path) => dest_dir.join(path),
+      None => continue,
+    };
+
+    if (&*file.name()).ends_with('/') {
+      std::fs::create_dir_all(&outpath)?;
+    } else {
+      if let Some(p) = outpath.parent() {
+        if !p.exists() {
+          std::fs::create_dir_all(&p)?;
+        }
+      }
+      let mut outfile = std::fs::File::create(&outpath)?;
+      copy(&mut file, &mut outfile)?;
+    }
+  }
+
+  Ok(())
+}
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/main.m b/espanso-package/src/util/git.rs
similarity index 67%
rename from other/EspansoNotifyHelper/EspansoNotifyHelper/main.m
rename to espanso-package/src/util/git.rs
index 68731bb9..b757e481 100644
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/main.m
+++ b/espanso-package/src/util/git.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -17,14 +17,14 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-#import <Cocoa/Cocoa.h>
-#import "AppDelegate.h"
+use std::process::Command;
 
-int main(int argc, const char * argv[]) {
-    AppDelegate *delegate = [[AppDelegate alloc] init];
-    NSApplication * application = [NSApplication sharedApplication];
-    [application setDelegate:delegate];
-    [NSApp run];
-    
-    return 0;
+pub fn is_private_repo(url: &str) -> bool {
+  if let Ok(output) = Command::new("git").arg("ls-remote").arg(url).output() {
+    if output.status.success() {
+      return true;
+    }
+  }
+
+  false
 }
diff --git a/espanso-package/src/util/github.rs b/espanso-package/src/util/github.rs
new file mode 100644
index 00000000..69cbf8f2
--- /dev/null
+++ b/espanso-package/src/util/github.rs
@@ -0,0 +1,137 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use lazy_static::lazy_static;
+use regex::Regex;
+use reqwest::StatusCode;
+
+lazy_static! {
+  static ref GITHUB_REGEX: Regex =
+    Regex::new(r"(https://github.com/|git@github.com:)(?P<author>.*?)/(?P<name>.*?)(\.|$)")
+      .unwrap();
+}
+
+#[derive(Debug, PartialEq)]
+pub struct GitHubParts {
+  author: String,
+  name: String,
+}
+
+pub fn extract_github_url_parts(url: &str) -> Option<GitHubParts> {
+  let captures = GITHUB_REGEX.captures(url)?;
+  let author = captures.name("author")?;
+  let name = captures.name("name")?;
+
+  Some(GitHubParts {
+    author: author.as_str().to_string(),
+    name: name.as_str().to_string(),
+  })
+}
+
+pub struct ResolvedRepoScheme {
+  pub author: String,
+  pub name: String,
+  pub branch: String,
+}
+
+pub fn resolve_repo_scheme(
+  parts: GitHubParts,
+  force_branch: Option<&str>,
+) -> Result<Option<ResolvedRepoScheme>> {
+  if let Some(force_branch) = force_branch {
+    if check_repo_with_branch(&parts, force_branch)? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: force_branch.to_string(),
+      }));
+    }
+  } else {
+    if check_repo_with_branch(&parts, "main")? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: "main".to_string(),
+      }));
+    }
+
+    if check_repo_with_branch(&parts, "master")? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: "master".to_string(),
+      }));
+    }
+  }
+
+  Ok(None)
+}
+
+pub fn check_repo_with_branch(parts: &GitHubParts, branch: &str) -> Result<bool> {
+  let client = reqwest::blocking::Client::new();
+
+  let url = generate_github_download_url(parts, branch);
+  let response = client.head(url).send()?;
+
+  Ok(response.status() == StatusCode::FOUND || response.status() == StatusCode::OK)
+}
+
+fn generate_github_download_url(parts: &GitHubParts, branch: &str) -> String {
+  format!(
+    "https://github.com/{}/{}/archive/refs/heads/{}.zip",
+    parts.author, parts.name, branch
+  )
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_extract_github_url_parts() {
+    assert_eq!(
+      extract_github_url_parts("https://github.com/federico-terzi/espanso").unwrap(),
+      GitHubParts {
+        author: "federico-terzi".to_string(),
+        name: "espanso".to_string(),
+      }
+    );
+
+    assert_eq!(
+      extract_github_url_parts("https://github.com/federico-terzi/espanso.git").unwrap(),
+      GitHubParts {
+        author: "federico-terzi".to_string(),
+        name: "espanso".to_string(),
+      }
+    );
+
+    assert_eq!(
+      extract_github_url_parts("git@github.com:federico-terzi/espanso.git").unwrap(),
+      GitHubParts {
+        author: "federico-terzi".to_string(),
+        name: "espanso".to_string(),
+      }
+    );
+
+    assert!(
+      extract_github_url_parts("https://gitlab.com/federicoterzi/espanso-test-package/").is_none()
+    );
+  }
+}
diff --git a/espanso-package/src/util/gitlab.rs b/espanso-package/src/util/gitlab.rs
new file mode 100644
index 00000000..dbe15b70
--- /dev/null
+++ b/espanso-package/src/util/gitlab.rs
@@ -0,0 +1,138 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use lazy_static::lazy_static;
+use regex::Regex;
+use reqwest::StatusCode;
+
+lazy_static! {
+  static ref GITLAB_REGEX: Regex =
+    Regex::new(r"(https://gitlab.com/|git@gitlab.com:)(?P<author>.*?)/(?P<name>.*?)(/|\.|$)")
+      .unwrap();
+}
+
+#[derive(Debug, PartialEq)]
+pub struct GitLabParts {
+  author: String,
+  name: String,
+}
+
+pub fn extract_gitlab_url_parts(url: &str) -> Option<GitLabParts> {
+  let captures = GITLAB_REGEX.captures(url)?;
+  let author = captures.name("author")?;
+  let name = captures.name("name")?;
+
+  Some(GitLabParts {
+    author: author.as_str().to_string(),
+    name: name.as_str().to_string(),
+  })
+}
+
+pub struct ResolvedRepoScheme {
+  pub author: String,
+  pub name: String,
+  pub branch: String,
+}
+
+pub fn resolve_repo_scheme(
+  parts: GitLabParts,
+  force_branch: Option<&str>,
+) -> Result<Option<ResolvedRepoScheme>> {
+  if let Some(force_branch) = force_branch {
+    if check_repo_with_branch(&parts, force_branch)? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: force_branch.to_string(),
+      }));
+    }
+  } else {
+    if check_repo_with_branch(&parts, "main")? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: "main".to_string(),
+      }));
+    }
+
+    if check_repo_with_branch(&parts, "master")? {
+      return Ok(Some(ResolvedRepoScheme {
+        author: parts.author,
+        name: parts.name,
+        branch: "master".to_string(),
+      }));
+    }
+  }
+
+  Ok(None)
+}
+
+pub fn check_repo_with_branch(parts: &GitLabParts, branch: &str) -> Result<bool> {
+  let client = reqwest::blocking::Client::new();
+
+  let url = generate_gitlab_download_url(parts, branch);
+  let response = client.head(url).send()?;
+
+  Ok(response.status() == StatusCode::OK)
+}
+
+fn generate_gitlab_download_url(parts: &GitLabParts, branch: &str) -> String {
+  format!(
+    "https://gitlab.com/{}/{}/-/archive/{}/{}-{}.zip",
+    parts.author, parts.name, branch, parts.name, branch
+  )
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_extract_gitlab_url_parts() {
+    assert_eq!(
+      extract_gitlab_url_parts("https://gitlab.com/federicoterzi/espanso-test-package/").unwrap(),
+      GitLabParts {
+        author: "federicoterzi".to_string(),
+        name: "espanso-test-package".to_string(),
+      }
+    );
+
+    assert_eq!(
+      extract_gitlab_url_parts("git@gitlab.com:federicoterzi/espanso-test-package.git").unwrap(),
+      GitLabParts {
+        author: "federicoterzi".to_string(),
+        name: "espanso-test-package".to_string(),
+      }
+    );
+
+    assert_eq!(
+      extract_gitlab_url_parts("https://gitlab.com/federicoterzi/espanso-test-package.git")
+        .unwrap(),
+      GitLabParts {
+        author: "federicoterzi".to_string(),
+        name: "espanso-test-package".to_string(),
+      }
+    );
+
+    assert!(
+      extract_gitlab_url_parts("https://github.com/federicoterzi/espanso-test-package/").is_none()
+    );
+  }
+}
diff --git a/espanso-package/src/util/mod.rs b/espanso-package/src/util/mod.rs
new file mode 100644
index 00000000..157861e7
--- /dev/null
+++ b/espanso-package/src/util/mod.rs
@@ -0,0 +1,23 @@
+/*
+ * 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/>.
+ */
+
+pub(crate) mod download;
+pub(crate) mod git;
+pub(crate) mod github;
+pub(crate) mod gitlab;
diff --git a/espanso-path/Cargo.toml b/espanso-path/Cargo.toml
new file mode 100644
index 00000000..bff0b501
--- /dev/null
+++ b/espanso-path/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "espanso-path"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+dirs = "3.0.1"
\ No newline at end of file
diff --git a/espanso-path/src/lib.rs b/espanso-path/src/lib.rs
new file mode 100644
index 00000000..be9893b4
--- /dev/null
+++ b/espanso-path/src/lib.rs
@@ -0,0 +1,321 @@
+/*
+ * 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 log::{debug, info};
+use std::{
+  fs::create_dir_all,
+  path::{Path, PathBuf},
+};
+
+#[derive(Debug, Clone)]
+pub struct Paths {
+  pub config: PathBuf,
+  pub runtime: PathBuf,
+  pub packages: PathBuf,
+
+  pub is_portable_mode: bool,
+}
+
+pub fn resolve_paths(
+  force_config_dir: Option<&Path>,
+  force_package_dir: Option<&Path>,
+  force_runtime_dir: Option<&Path>,
+) -> Paths {
+  let config_dir = if let Some(config_dir) = force_config_dir {
+    config_dir.to_path_buf()
+  } else if let Some(config_dir) = get_config_dir() {
+    config_dir
+  } else {
+    // Create the config directory if not already present
+    let config_dir = get_default_config_path();
+    info!("creating config directory in {:?}", config_dir);
+    create_dir_all(&config_dir).expect("unable to create config directory");
+    config_dir
+  };
+
+  let runtime_dir = if let Some(runtime_dir) = force_runtime_dir {
+    runtime_dir.to_path_buf()
+  } else if let Some(runtime_dir) = get_runtime_dir() {
+    runtime_dir
+  } else {
+    // Create the runtime directory if not already present
+    let runtime_dir = if !is_portable_mode() {
+      get_default_runtime_path()
+    } else {
+      get_portable_runtime_path().expect("unable to obtain runtime directory path")
+    };
+    info!("creating runtime directory in {:?}", runtime_dir);
+    create_dir_all(&runtime_dir).expect("unable to create runtime directory");
+    runtime_dir
+  };
+
+  let packages_dir = if let Some(package_dir) = force_package_dir {
+    package_dir.to_path_buf()
+  } else if let Some(package_dir) = get_packages_dir(&config_dir, &runtime_dir) {
+    package_dir
+  } else {
+    // Create the packages directory if not already present
+    let packages_dir = get_default_packages_path(&config_dir);
+    info!("creating packages directory in {:?}", packages_dir);
+    create_dir_all(&packages_dir).expect("unable to create packages directory");
+    packages_dir
+  };
+
+  let is_portable_mode =
+    is_portable_mode() && force_config_dir.is_none() && force_runtime_dir.is_none();
+
+  Paths {
+    config: config_dir,
+    runtime: runtime_dir,
+    packages: packages_dir,
+    is_portable_mode,
+  }
+}
+
+fn get_config_dir() -> Option<PathBuf> {
+  if let Some(portable_dir) = get_portable_config_dir() {
+    // Portable mode
+    debug!("detected portable config directory in {:?}", portable_dir);
+    Some(portable_dir)
+  } else if let Some(config_dir) = get_home_espanso_dir() {
+    // $HOME/.espanso
+    debug!("detected config directory in $HOME/.espanso");
+    Some(config_dir)
+  } else if let Some(config_dir) = get_home_config_espanso_dir() {
+    // $HOME/.config/espanso
+    debug!("detected config directory in $HOME/.config/espanso");
+    Some(config_dir)
+  } else if let Some(legacy_mac_dir) = get_legacy_mac_dir() {
+    // Legacy macOS location in ~/Library/Preferences/espanso
+    debug!(
+      "detected legacy config directory location at {:?}",
+      legacy_mac_dir
+    );
+    Some(legacy_mac_dir)
+  } else if let Some(config_dir) = get_default_config_dir() {
+    debug!("detected default config directory at {:?}", config_dir);
+    Some(config_dir)
+  } else {
+    None
+  }
+}
+
+fn get_portable_config_dir() -> Option<PathBuf> {
+  let espanso_exe_path = std::env::current_exe().expect("unable to obtain executable path");
+  let exe_dir = espanso_exe_path.parent();
+  if let Some(parent) = exe_dir {
+    let config_dir = parent.join(".espanso");
+    if config_dir.is_dir() {
+      return Some(config_dir);
+    }
+  }
+  None
+}
+
+fn get_home_espanso_dir() -> Option<PathBuf> {
+  if let Some(home_dir) = dirs::home_dir() {
+    let config_espanso_dir = home_dir.join(".espanso");
+    if config_espanso_dir.is_dir() {
+      return Some(config_espanso_dir);
+    }
+  }
+  None
+}
+
+fn get_home_config_espanso_dir() -> Option<PathBuf> {
+  if let Some(home_dir) = dirs::home_dir() {
+    let home_espanso_dir = home_dir.join(".config").join("espanso");
+    if home_espanso_dir.is_dir() {
+      return Some(home_espanso_dir);
+    }
+  }
+  None
+}
+
+fn get_default_config_dir() -> Option<PathBuf> {
+  let config_path = get_default_config_path();
+  if config_path.is_dir() {
+    return Some(config_path);
+  }
+  None
+}
+
+fn get_default_config_path() -> PathBuf {
+  let config_dir = dirs::config_dir().expect("unable to obtain dirs::config_dir()");
+  config_dir.join("espanso")
+}
+
+// Due to the original behavior of the dirs crate, espanso placed the config
+// directory in the Preferences folder on macOS, but this is not an optimal
+// approach.
+// For more context, see: https://github.com/federico-terzi/espanso/issues/611
+fn get_legacy_mac_dir() -> Option<PathBuf> {
+  if cfg!(target_os = "macos") {
+    if let Some(preferences_dir) = dirs::preference_dir() {
+      let espanso_dir = preferences_dir.join("espanso");
+      if espanso_dir.is_dir() {
+        return Some(espanso_dir);
+      }
+    }
+  }
+  None
+}
+
+fn get_runtime_dir() -> Option<PathBuf> {
+  if let Some(runtime_dir) = get_portable_runtime_dir() {
+    debug!("detected portable runtime dir: {:?}", runtime_dir);
+    Some(runtime_dir)
+  } else if let Some(legacy_dir) = get_legacy_runtime_dir() {
+    debug!("detected legacy runtime dir: {:?}", legacy_dir);
+    Some(legacy_dir)
+  } else if let Some(default_dir) = get_default_runtime_dir() {
+    debug!("detected default runtime dir: {:?}", default_dir);
+    Some(default_dir)
+  } else {
+    None
+  }
+}
+
+fn get_portable_runtime_dir() -> Option<PathBuf> {
+  if let Some(runtime_dir) = get_portable_runtime_path() {
+    if runtime_dir.is_dir() {
+      return Some(runtime_dir);
+    }
+  }
+  None
+}
+
+fn get_portable_runtime_path() -> Option<PathBuf> {
+  let espanso_exe_path = std::env::current_exe().expect("unable to obtain executable path");
+  let exe_dir = espanso_exe_path.parent();
+  if let Some(parent) = exe_dir {
+    let config_dir = parent.join(".espanso-runtime");
+    return Some(config_dir);
+  }
+  None
+}
+
+fn get_legacy_runtime_dir() -> Option<PathBuf> {
+  let data_dir = dirs::data_local_dir().expect("unable to obtain dirs::data_local_dir()");
+  let espanso_dir = data_dir.join("espanso");
+  if is_legacy_runtime_dir(&espanso_dir) {
+    Some(espanso_dir)
+  } else {
+    None
+  }
+}
+
+fn get_default_runtime_dir() -> Option<PathBuf> {
+  let default_dir = get_default_runtime_path();
+  if default_dir.is_dir() {
+    Some(default_dir)
+  } else {
+    None
+  }
+}
+
+fn get_default_runtime_path() -> PathBuf {
+  let runtime_dir = dirs::cache_dir().expect("unable to obtain dirs::cache_dir()");
+  runtime_dir.join("espanso")
+}
+
+fn get_packages_dir(config_dir: &Path, legacy_data_dir: &Path) -> Option<PathBuf> {
+  if let Some(packages_dir) = get_default_packages_dir(config_dir) {
+    debug!("detected default packages dir: {:?}", packages_dir);
+    Some(packages_dir)
+  } else if let Some(packages_dir) = get_legacy_embedded_packages_dir(config_dir) {
+    debug!("detected legacy packages dir: {:?}", packages_dir);
+    Some(packages_dir)
+  } else if let Some(packages_dir) = get_legacy_packages_dir(legacy_data_dir) {
+    debug!("detected legacy packages dir: {:?}", packages_dir);
+    Some(packages_dir)
+  } else {
+    None
+  }
+}
+
+fn get_legacy_packages_dir(legacy_data_dir: &Path) -> Option<PathBuf> {
+  let legacy_dir = legacy_data_dir.join("packages");
+  if legacy_dir.is_dir() {
+    Some(legacy_dir)
+  } else {
+    None
+  }
+}
+
+fn get_legacy_embedded_packages_dir(config_dir: &Path) -> Option<PathBuf> {
+  let legacy_dir = config_dir.join("packages");
+  if legacy_dir.is_dir() {
+    Some(legacy_dir)
+  } else {
+    None
+  }
+}
+
+fn get_default_packages_dir(config_dir: &Path) -> Option<PathBuf> {
+  let packages_dir = get_default_packages_path(config_dir);
+  if packages_dir.is_dir() {
+    Some(packages_dir)
+  } else {
+    None
+  }
+}
+
+fn get_default_packages_path(config_dir: &Path) -> PathBuf {
+  config_dir.join("match").join("packages")
+}
+
+fn is_portable_mode() -> bool {
+  let espanso_exe_path = std::env::current_exe().expect("unable to obtain executable path");
+  let exe_dir = espanso_exe_path.parent();
+  if let Some(parent) = exe_dir {
+    let config_dir = parent.join(".espanso");
+    if config_dir.is_dir() {
+      return true;
+    }
+  }
+  false
+}
+
+const LEGACY_RUNTIME_DIR_CANDIDATES_FILE: &[&str] = &[
+  "espanso.log",
+  "espanso.lock",
+  "espanso-worker.lock",
+  "espanso-daemon.lock",
+];
+
+// Run an heuristic to determine if the given directory
+// is a legacy runtime dir or not.
+// Unfortunately, due to the way the legacy path works
+// we really have to analyse the content to determine this
+// information
+fn is_legacy_runtime_dir(path: &Path) -> bool {
+  if !path.is_dir() {
+    return false;
+  }
+
+  for candidate in LEGACY_RUNTIME_DIR_CANDIDATES_FILE {
+    let candidate_path = path.join(candidate);
+    if candidate_path.is_file() {
+      return true;
+    }
+  }
+
+  false
+}
diff --git a/espanso-render/Cargo.toml b/espanso-render/Cargo.toml
new file mode 100644
index 00000000..3940d0d0
--- /dev/null
+++ b/espanso-render/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "espanso-render"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+regex = "1.4.3"
+lazy_static = "1.4.0"
+chrono = "0.4.19"
+enum-as-inner = "0.3.3"
+rand = "0.8.3"
\ No newline at end of file
diff --git a/espanso-render/src/extension/clipboard.rs b/espanso-render/src/extension/clipboard.rs
new file mode 100644
index 00000000..cf1e800f
--- /dev/null
+++ b/espanso-render/src/extension/clipboard.rs
@@ -0,0 +1,100 @@
+/*
+ * 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 crate::{Extension, ExtensionOutput, ExtensionResult, Params};
+use thiserror::Error;
+
+pub trait ClipboardProvider {
+  fn get_text(&self) -> Option<String>;
+}
+
+pub struct ClipboardExtension<'a> {
+  provider: &'a dyn ClipboardProvider,
+}
+
+#[allow(clippy::new_without_default)]
+impl<'a> ClipboardExtension<'a> {
+  pub fn new(provider: &'a dyn ClipboardProvider) -> Self {
+    Self { provider }
+  }
+}
+
+impl<'a> Extension for ClipboardExtension<'a> {
+  fn name(&self) -> &str {
+    "clipboard"
+  }
+
+  fn calculate(&self, _: &crate::Context, _: &crate::Scope, _: &Params) -> crate::ExtensionResult {
+    if let Some(clipboard) = self.provider.get_text() {
+      ExtensionResult::Success(ExtensionOutput::Single(clipboard))
+    } else {
+      ExtensionResult::Error(ClipboardExtensionError::MissingClipboard.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ClipboardExtensionError {
+  #[error("clipboard provider returned error")]
+  MissingClipboard,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  struct MockClipboardProvider {
+    return_none: bool,
+  }
+
+  impl super::ClipboardProvider for MockClipboardProvider {
+    fn get_text(&self) -> Option<String> {
+      if self.return_none {
+        None
+      } else {
+        Some("test".to_string())
+      }
+    }
+  }
+
+  #[test]
+  fn clipboard_works_correctly() {
+    let provider = MockClipboardProvider { return_none: false };
+    let extension = ClipboardExtension::new(&provider);
+
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &Params::new())
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("test".to_string())
+    );
+  }
+
+  #[test]
+  fn none_clipboard_produces_error() {
+    let provider = MockClipboardProvider { return_none: true };
+    let extension = ClipboardExtension::new(&provider);
+
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &Params::new()),
+      ExtensionResult::Error(_)
+    ));
+  }
+}
diff --git a/espanso-render/src/extension/date.rs b/espanso-render/src/extension/date.rs
new file mode 100644
index 00000000..5cb08121
--- /dev/null
+++ b/espanso-render/src/extension/date.rs
@@ -0,0 +1,118 @@
+/*
+ * 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 chrono::{DateTime, Duration, Local};
+
+use crate::{Extension, ExtensionOutput, ExtensionResult, Number, Params, Value};
+
+pub struct DateExtension {
+  fixed_date: Option<DateTime<Local>>,
+}
+
+#[allow(clippy::new_without_default)]
+impl DateExtension {
+  pub fn new() -> Self {
+    Self { fixed_date: None }
+  }
+}
+
+impl Extension for DateExtension {
+  fn name(&self) -> &str {
+    "date"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    _: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    let mut now = self.get_date();
+
+    // Compute the given offset
+    let offset = params.get("offset");
+    if let Some(Value::Number(Number::Integer(offset))) = offset {
+      let offset = Duration::seconds(*offset);
+      now = now + offset;
+    }
+
+    let format = params.get("format");
+
+    let date = if let Some(Value::String(format)) = format {
+      now.format(format).to_string()
+    } else {
+      now.to_rfc2822()
+    };
+
+    ExtensionResult::Success(ExtensionOutput::Single(date))
+  }
+}
+
+impl DateExtension {
+  fn get_date(&self) -> DateTime<Local> {
+    if let Some(fixed_date) = self.fixed_date {
+      fixed_date
+    } else {
+      Local::now()
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use chrono::offset::TimeZone;
+
+  #[test]
+  fn date_formatted_correctly() {
+    let mut extension = DateExtension::new();
+    extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
+
+    let param = vec![("format".to_string(), Value::String("%H:%M:%S".to_string()))]
+      .into_iter()
+      .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("09:10:11".to_string())
+    );
+  }
+
+  #[test]
+  fn offset_works_correctly() {
+    let mut extension = DateExtension::new();
+    extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
+
+    let param = vec![
+      ("format".to_string(), Value::String("%H:%M:%S".to_string())),
+      ("offset".to_string(), Value::Number(Number::Integer(3600))),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("10:10:11".to_string())
+    );
+  }
+}
diff --git a/espanso-render/src/extension/echo.rs b/espanso-render/src/extension/echo.rs
new file mode 100644
index 00000000..95361c9f
--- /dev/null
+++ b/espanso-render/src/extension/echo.rs
@@ -0,0 +1,106 @@
+/*
+ * 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 crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
+use thiserror::Error;
+
+pub struct EchoExtension {
+  alias: String,
+}
+
+#[allow(clippy::new_without_default)]
+impl EchoExtension {
+  pub fn new() -> Self {
+    Self {
+      alias: "echo".to_string(),
+    }
+  }
+
+  pub fn new_with_alias(alias: &str) -> Self {
+    Self {
+      alias: alias.to_string(),
+    }
+  }
+}
+
+impl Extension for EchoExtension {
+  fn name(&self) -> &str {
+    self.alias.as_str()
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    _: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    if let Some(Value::String(echo)) = params.get("echo") {
+      ExtensionResult::Success(ExtensionOutput::Single(echo.clone()))
+    } else {
+      ExtensionResult::Error(EchoExtensionError::MissingEchoParameter.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum EchoExtensionError {
+  #[error("missing 'echo' parameter")]
+  MissingEchoParameter,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn echo_works_correctly() {
+    let extension = EchoExtension::new();
+
+    let param = vec![("echo".to_string(), Value::String("test".to_string()))]
+      .into_iter()
+      .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("test".to_string())
+    );
+  }
+
+  #[test]
+  fn missing_echo_parameter() {
+    let extension = EchoExtension::new();
+
+    let param = Params::new();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+
+  #[test]
+  fn alias() {
+    let extension_with_alias = EchoExtension::new_with_alias("dummy");
+    let extension = EchoExtension::new();
+
+    assert_eq!(extension.name(), "echo");
+    assert_eq!(extension_with_alias.name(), "dummy");
+  }
+}
diff --git a/espanso-render/src/extension/form.rs b/espanso-render/src/extension/form.rs
new file mode 100644
index 00000000..c75c75d6
--- /dev/null
+++ b/espanso-render/src/extension/form.rs
@@ -0,0 +1,113 @@
+/*
+ * 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 crate::renderer::VAR_REGEX;
+use log::error;
+use std::collections::HashMap;
+use thiserror::Error;
+
+use crate::{
+  renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
+};
+
+lazy_static! {
+  static ref EMPTY_PARAMS: Params = Params::new();
+}
+
+pub trait FormProvider {
+  fn show(&self, layout: &str, fields: &Params, options: &Params) -> FormProviderResult;
+}
+
+pub enum FormProviderResult {
+  Success(HashMap<String, String>),
+  Aborted,
+  Error(anyhow::Error),
+}
+
+pub struct FormExtension<'a> {
+  provider: &'a dyn FormProvider,
+}
+
+#[allow(clippy::new_without_default)]
+impl<'a> FormExtension<'a> {
+  pub fn new(provider: &'a dyn FormProvider) -> Self {
+    Self { provider }
+  }
+}
+
+impl<'a> Extension for FormExtension<'a> {
+  fn name(&self) -> &str {
+    "form"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    scope: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    let layout = if let Some(Value::String(layout)) = params.get("layout") {
+      layout
+    } else {
+      return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
+    };
+
+    let mut fields = if let Some(Value::Object(fields)) = params.get("fields") {
+      fields.clone()
+    } else {
+      Params::new()
+    };
+
+    // Inject scope variables into fields (if needed)
+    inject_scope(&mut fields, scope);
+
+    match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
+      FormProviderResult::Success(values) => {
+        ExtensionResult::Success(ExtensionOutput::Multiple(values))
+      }
+      FormProviderResult::Aborted => ExtensionResult::Aborted,
+      FormProviderResult::Error(error) => ExtensionResult::Error(error),
+    }
+  }
+}
+
+// TODO: test
+fn inject_scope(fields: &mut HashMap<String, Value>, scope: &HashMap<&str, ExtensionOutput>) {
+  for value in fields.values_mut() {
+    if let Value::Object(field_options) = value {
+      if let Some(Value::String(default_value)) = field_options.get_mut("default") {
+        if VAR_REGEX.is_match(default_value) {
+          match render_variables(default_value, scope) {
+            Ok(rendered) => *default_value = rendered,
+            Err(err) => error!(
+              "error while injecting variable in form default value: {}",
+              err
+            ),
+          }
+        }
+      }
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum FormExtensionError {
+  #[error("missing layout parameter")]
+  MissingLayout,
+}
diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs
new file mode 100644
index 00000000..35bae77b
--- /dev/null
+++ b/espanso-render/src/extension/mod.rs
@@ -0,0 +1,27 @@
+/*
+ * 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/>.
+ */
+
+pub mod clipboard;
+pub mod date;
+pub mod echo;
+pub mod form;
+pub mod random;
+pub mod script;
+pub mod shell;
+mod util;
diff --git a/espanso-render/src/extension/random.rs b/espanso-render/src/extension/random.rs
new file mode 100644
index 00000000..bc954ae7
--- /dev/null
+++ b/espanso-render/src/extension/random.rs
@@ -0,0 +1,108 @@
+/*
+ * 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 crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
+use rand::seq::SliceRandom;
+use thiserror::Error;
+
+pub struct RandomExtension {}
+
+#[allow(clippy::new_without_default)]
+impl RandomExtension {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl Extension for RandomExtension {
+  fn name(&self) -> &str {
+    "random"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    _: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    if let Some(Value::Array(choices)) = params.get("choices") {
+      let choices: Vec<String> = choices
+        .iter()
+        .filter_map(|arg| arg.as_string())
+        .cloned()
+        .collect();
+
+      if let Some(choice) = choices.choose(&mut rand::thread_rng()) {
+        ExtensionResult::Success(ExtensionOutput::Single(choice.clone()))
+      } else {
+        ExtensionResult::Error(RandomExtensionError::SelectionError.into())
+      }
+    } else {
+      ExtensionResult::Error(RandomExtensionError::MissingChoicesParameter.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum RandomExtensionError {
+  #[error("missing 'choices' parameter")]
+  MissingChoicesParameter,
+
+  #[error("could not select a choice randomly")]
+  SelectionError,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn random_works_correctly() {
+    let extension = RandomExtension::new();
+
+    let param = vec![(
+      "choices".to_string(),
+      Value::Array(vec![
+        Value::String("first".to_string()),
+        Value::String("second".to_string()),
+        Value::String("third".to_string()),
+      ]),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert!(matches!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single(result) if ["first", "second", "third"].contains(&result.as_str())
+    ));
+  }
+
+  #[test]
+  fn missing_echo_parameter() {
+    let extension = RandomExtension::new();
+
+    let param = Params::new();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+}
diff --git a/espanso-render/src/extension/script.rs b/espanso-render/src/extension/script.rs
new file mode 100644
index 00000000..a59c38b7
--- /dev/null
+++ b/espanso-render/src/extension/script.rs
@@ -0,0 +1,337 @@
+/*
+ * 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::{
+  path::{Path, PathBuf},
+  process::Command,
+};
+
+use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
+use log::{info, warn};
+use thiserror::Error;
+
+pub struct ScriptExtension {
+  home_path: PathBuf,
+  config_path: PathBuf,
+  packages_path: PathBuf,
+}
+
+#[allow(clippy::new_without_default)]
+impl ScriptExtension {
+  pub fn new(config_path: &Path, home_path: &Path, packages_path: &Path) -> Self {
+    Self {
+      config_path: config_path.to_owned(),
+      home_path: home_path.to_owned(),
+      packages_path: packages_path.to_owned(),
+    }
+  }
+}
+
+impl Extension for ScriptExtension {
+  fn name(&self) -> &str {
+    "script"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    scope: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    if let Some(Value::Array(args)) = params.get("args") {
+      let mut args: Vec<String> = args
+        .iter()
+        .filter_map(|arg| arg.as_string())
+        .cloned()
+        .collect();
+
+      // Replace %HOME% with current user home directory to
+      // create cross-platform paths. See issue #265
+      // Also replace %CONFIG% and %PACKAGES% path. See issue #380
+      args.iter_mut().for_each(|arg| {
+        if arg.contains("%HOME%") {
+          *arg = arg.replace("%HOME%", &self.home_path.to_string_lossy().to_string());
+        }
+        if arg.contains("%CONFIG%") {
+          *arg = arg.replace("%CONFIG%", &self.config_path.to_string_lossy().to_string());
+        }
+        if arg.contains("%PACKAGES%") {
+          *arg = arg.replace(
+            "%PACKAGES%",
+            &self.packages_path.to_string_lossy().to_string(),
+          );
+        }
+
+        // On Windows, correct paths separators
+        if cfg!(target_os = "windows") {
+          let path = PathBuf::from(&arg);
+          if path.exists() {
+            *arg = path.to_string_lossy().to_string()
+          }
+        }
+      });
+
+      let mut command = Command::new(&args[0]);
+      command.env("CONFIG", self.config_path.to_string_lossy().to_string());
+      for (key, value) in super::util::convert_to_env_variables(scope) {
+        command.env(key, value);
+      }
+
+      // Set the OS-specific flags
+      super::util::set_command_flags(&mut command);
+
+      let output = if args.len() > 1 {
+        command.args(&args[1..]).output()
+      } else {
+        command.output()
+      };
+
+      match output {
+        Ok(output) => {
+          let output_str = String::from_utf8_lossy(&output.stdout);
+          let error_str = String::from_utf8_lossy(&output.stderr);
+
+          let debug = params
+            .get("debug")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(false);
+
+          if debug {
+            info!("debug information for script> {:?}", args);
+            info!("exit status: '{}'", output.status);
+            info!("stdout: '{}'", output_str);
+            info!("stderr: '{}'", error_str);
+            info!("this debug information was shown because the 'debug' option is true.");
+          }
+
+          let ignore_error = params
+            .get("ignore_error")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(false);
+
+          if !output.status.success() || !error_str.trim().is_empty() {
+            warn!(
+              "script command exited with code: {} and error: {}",
+              output.status, error_str
+            );
+
+            if !ignore_error {
+              return ExtensionResult::Error(
+                ScriptExtensionError::ExecutionError(error_str.to_string()).into(),
+              );
+            }
+          }
+
+          let trim = params
+            .get("trim")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(true);
+
+          let output = if trim {
+            output_str.trim().to_owned()
+          } else {
+            output_str.to_string()
+          };
+
+          ExtensionResult::Success(ExtensionOutput::Single(output))
+        }
+        Err(error) => ExtensionResult::Error(
+          ScriptExtensionError::ExecutionFailed(args[0].to_string(), error.into()).into(),
+        ),
+      }
+    } else {
+      ExtensionResult::Error(ScriptExtensionError::MissingArgsParameter.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ScriptExtensionError {
+  #[error("missing 'args' parameter")]
+  MissingArgsParameter,
+
+  #[error("could not execute script: '`{0}`', error: '`{1}`'")]
+  ExecutionFailed(String, anyhow::Error),
+
+  #[error("script reported error: '`{0}`'")]
+  ExecutionError(String),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  #[cfg(not(target_os = "windows"))]
+  use crate::Scope;
+
+  fn get_extension() -> ScriptExtension {
+    ScriptExtension::new(&PathBuf::new(), &PathBuf::new(), &PathBuf::new())
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn basic() {
+    let extension = get_extension();
+
+    let param = vec![(
+      "args".to_string(),
+      Value::Array(vec![
+        Value::String("echo".to_string()),
+        Value::String("hello world".to_string()),
+      ]),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("hello world".to_string())
+    );
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn basic_no_trim() {
+    let extension = get_extension();
+
+    let param = vec![
+      (
+        "args".to_string(),
+        Value::Array(vec![
+          Value::String("echo".to_string()),
+          Value::String("hello world".to_string()),
+        ]),
+      ),
+      ("trim".to_string(), Value::Bool(false)),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    if cfg!(target_os = "windows") {
+      assert_eq!(
+        extension
+          .calculate(&Default::default(), &Default::default(), &param)
+          .into_success()
+          .unwrap(),
+        ExtensionOutput::Single("hello world\r\n".to_string())
+      );
+    } else {
+      assert_eq!(
+        extension
+          .calculate(&Default::default(), &Default::default(), &param)
+          .into_success()
+          .unwrap(),
+        ExtensionOutput::Single("hello world\n".to_string())
+      );
+    }
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn var_injection() {
+    let extension = get_extension();
+
+    let param = vec![(
+      "args".to_string(),
+      Value::Array(vec![
+        Value::String("sh".to_string()),
+        Value::String("-c".to_string()),
+        Value::String("echo $ESPANSO_VAR1".to_string()),
+      ]),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    let mut scope = Scope::new();
+    scope.insert("var1", ExtensionOutput::Single("hello world".to_string()));
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &scope, &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("hello world".to_string())
+    );
+  }
+
+  #[test]
+  fn invalid_command() {
+    let extension = get_extension();
+
+    let param = vec![(
+      "args".to_string(),
+      Value::Array(vec![Value::String("nonexistentcommand".to_string())]),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn exit_error() {
+    let extension = get_extension();
+
+    let param = vec![(
+      "args".to_string(),
+      Value::Array(vec![
+        Value::String("sh".to_string()),
+        Value::String("-c".to_string()),
+        Value::String("exit 1".to_string()),
+      ]),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn ignore_error() {
+    let extension = get_extension();
+
+    let param = vec![
+      (
+        "args".to_string(),
+        Value::Array(vec![
+          Value::String("sh".to_string()),
+          Value::String("-c".to_string()),
+          Value::String("exit 1".to_string()),
+        ]),
+      ),
+      ("ignore_error".to_string(), Value::Bool(true)),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("".to_string())
+    );
+  }
+}
diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs
new file mode 100644
index 00000000..a385a5b0
--- /dev/null
+++ b/espanso-render/src/extension/shell.rs
@@ -0,0 +1,411 @@
+/*
+ * 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,
+  path::{Path, PathBuf},
+  process::{Command, Output},
+};
+
+use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
+use log::{info, warn};
+use thiserror::Error;
+
+#[allow(clippy::upper_case_acronyms)]
+pub enum Shell {
+  Cmd,
+  Powershell,
+  WSL,
+  WSL2,
+  Bash,
+  Sh,
+}
+
+impl Shell {
+  fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
+    let mut is_wsl = false;
+
+    let mut command = match self {
+      Shell::Cmd => {
+        let mut command = Command::new("cmd");
+        command.args(&["/C", cmd]);
+        command
+      }
+      Shell::Powershell => {
+        let mut command = Command::new("powershell");
+        command.args(&["-Command", cmd]);
+        command
+      }
+      Shell::WSL => {
+        is_wsl = true;
+        let mut command = Command::new("bash");
+        command.args(&["-c", cmd]);
+        command
+      }
+      Shell::WSL2 => {
+        is_wsl = true;
+        let mut command = Command::new("wsl");
+        command.args(&["bash", "-c", cmd]);
+        command
+      }
+      Shell::Bash => {
+        let mut command = Command::new("bash");
+        command.args(&["-c", cmd]);
+        command
+      }
+      Shell::Sh => {
+        let mut command = Command::new("sh");
+        command.args(&["-c", cmd]);
+        command
+      }
+    };
+
+    // Set the OS-specific flags
+    super::util::set_command_flags(&mut command);
+
+    // Inject all the previous variables
+    for (key, value) in vars.iter() {
+      command.env(key, value);
+    }
+
+    // In WSL environment, we have to specify which ENV variables
+    // should be passed to linux.
+    // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
+    if is_wsl {
+      let mut tokens: Vec<&str> = vec!["CONFIG/p"];
+
+      // Add all the previous variables
+      for (key, _) in vars.iter() {
+        tokens.push(key);
+      }
+
+      let wsl_env = tokens.join(":");
+      command.env("WSLENV", wsl_env);
+    }
+
+    command.output()
+  }
+
+  fn from_string(shell: &str) -> Option<Shell> {
+    match shell {
+      "cmd" => Some(Shell::Cmd),
+      "powershell" => Some(Shell::Powershell),
+      "wsl" => Some(Shell::WSL),
+      "wsl2" => Some(Shell::WSL2),
+      "bash" => Some(Shell::Bash),
+      "sh" => Some(Shell::Sh),
+      _ => None,
+    }
+  }
+}
+
+impl Default for Shell {
+  fn default() -> Shell {
+    if cfg!(target_os = "windows") {
+      Shell::Powershell
+    } else if cfg!(target_os = "macos") {
+      Shell::Sh
+    } else if cfg!(target_os = "linux") {
+      Shell::Bash
+    } else {
+      panic!("invalid target os for shell")
+    }
+  }
+}
+
+pub struct ShellExtension {
+  config_path: PathBuf,
+}
+
+#[allow(clippy::new_without_default)]
+impl ShellExtension {
+  pub fn new(config_path: &Path) -> Self {
+    Self {
+      config_path: config_path.to_owned(),
+    }
+  }
+}
+
+impl Extension for ShellExtension {
+  fn name(&self) -> &str {
+    "shell"
+  }
+
+  fn calculate(
+    &self,
+    _: &crate::Context,
+    scope: &crate::Scope,
+    params: &Params,
+  ) -> crate::ExtensionResult {
+    if let Some(Value::String(cmd)) = params.get("cmd") {
+      let shell = if let Some(Value::String(shell_param)) = params.get("shell") {
+        if let Some(shell) = Shell::from_string(shell_param) {
+          shell
+        } else {
+          return ExtensionResult::Error(
+            ShellExtensionError::InvalidShell(shell_param.to_string()).into(),
+          );
+        }
+      } else {
+        Shell::default()
+      };
+
+      let mut env_variables = super::util::convert_to_env_variables(scope);
+      env_variables.insert(
+        "CONFIG".to_string(),
+        self.config_path.to_string_lossy().to_string(),
+      );
+
+      match shell.execute_cmd(cmd, &env_variables) {
+        Ok(output) => {
+          let output_str = String::from_utf8_lossy(&output.stdout);
+          let error_str = String::from_utf8_lossy(&output.stderr);
+
+          let debug = params
+            .get("debug")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(false);
+
+          if debug {
+            info!("debug information for command> {}", cmd);
+            info!("exit status: '{}'", output.status);
+            info!("stdout: '{}'", output_str);
+            info!("stderr: '{}'", error_str);
+            info!("this debug information was shown because the 'debug' option is true.");
+          }
+
+          let ignore_error = params
+            .get("ignore_error")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(false);
+
+          if !output.status.success() || !error_str.trim().is_empty() {
+            warn!(
+              "shell command exited with code: {} and error: {}",
+              output.status, error_str
+            );
+
+            if !ignore_error {
+              return ExtensionResult::Error(
+                ShellExtensionError::ExecutionError(error_str.to_string()).into(),
+              );
+            }
+          }
+
+          let trim = params
+            .get("trim")
+            .and_then(|v| v.as_bool())
+            .copied()
+            .unwrap_or(true);
+
+          let output = if trim {
+            output_str.trim().to_owned()
+          } else {
+            output_str.to_string()
+          };
+
+          ExtensionResult::Success(ExtensionOutput::Single(output))
+        }
+        Err(error) => ExtensionResult::Error(
+          ShellExtensionError::ExecutionFailed(cmd.to_string(), error.into()).into(),
+        ),
+      }
+    } else {
+      ExtensionResult::Error(ShellExtensionError::MissingCmdParameter.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ShellExtensionError {
+  #[error("missing 'cmd' parameter")]
+  MissingCmdParameter,
+
+  #[error("invalid shell: `{0}` is not a valid one")]
+  InvalidShell(String),
+
+  #[error("could not execute command: '`{0}`', error: '`{1}`'")]
+  ExecutionFailed(String, anyhow::Error),
+
+  #[error("command reported error: '`{0}`'")]
+  ExecutionError(String),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::Scope;
+
+  #[test]
+  fn shell_not_trimmed() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![
+      (
+        "cmd".to_string(),
+        Value::String("echo \"hello world\"".to_string()),
+      ),
+      ("trim".to_string(), Value::Bool(false)),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    if cfg!(target_os = "windows") {
+      assert_eq!(
+        extension
+          .calculate(&Default::default(), &Default::default(), &param)
+          .into_success()
+          .unwrap(),
+        ExtensionOutput::Single("hello world\r\n".to_string())
+      );
+    } else {
+      assert_eq!(
+        extension
+          .calculate(&Default::default(), &Default::default(), &param)
+          .into_success()
+          .unwrap(),
+        ExtensionOutput::Single("hello world\n".to_string())
+      );
+    }
+  }
+
+  #[test]
+  fn shell_trimmed() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![(
+      "cmd".to_string(),
+      Value::String("echo \"hello world\"".to_string()),
+    )]
+    .into_iter()
+    .collect::<Params>();
+
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("hello world".to_string())
+    );
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn pipes() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![(
+      "cmd".to_string(),
+      Value::String("echo \"hello world\" | cat".to_string()),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("hello world".to_string())
+    );
+  }
+
+  #[test]
+  fn var_injection() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = if cfg!(not(target_os = "windows")) {
+      vec![(
+        "cmd".to_string(),
+        Value::String("echo $ESPANSO_VAR1".to_string()),
+      )]
+      .into_iter()
+      .collect::<Params>()
+    } else {
+      vec![
+        (
+          "cmd".to_string(),
+          Value::String("echo %ESPANSO_VAR1%".to_string()),
+        ),
+        ("shell".to_string(), Value::String("cmd".to_string())),
+      ]
+      .into_iter()
+      .collect::<Params>()
+    };
+    let mut scope = Scope::new();
+    scope.insert("var1", ExtensionOutput::Single("hello world".to_string()));
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &scope, &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("hello world".to_string())
+    );
+  }
+
+  #[test]
+  fn invalid_command() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![(
+      "cmd".to_string(),
+      Value::String("nonexistentcommand".to_string()),
+    )]
+    .into_iter()
+    .collect::<Params>();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn exit_error() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![("cmd".to_string(), Value::String("exit 1".to_string()))]
+      .into_iter()
+      .collect::<Params>();
+    assert!(matches!(
+      extension.calculate(&Default::default(), &Default::default(), &param),
+      ExtensionResult::Error(_)
+    ));
+  }
+
+  #[test]
+  #[cfg(not(target_os = "windows"))]
+  fn ignore_error() {
+    let extension = ShellExtension::new(&PathBuf::new());
+
+    let param = vec![
+      ("cmd".to_string(), Value::String("exit 1".to_string())),
+      ("ignore_error".to_string(), Value::Bool(true)),
+    ]
+    .into_iter()
+    .collect::<Params>();
+    assert_eq!(
+      extension
+        .calculate(&Default::default(), &Default::default(), &param)
+        .into_success()
+        .unwrap(),
+      ExtensionOutput::Single("".to_string())
+    );
+  }
+}
diff --git a/espanso-render/src/extension/util.rs b/espanso-render/src/extension/util.rs
new file mode 100644
index 00000000..89eb24b5
--- /dev/null
+++ b/espanso-render/src/extension/util.rs
@@ -0,0 +1,75 @@
+/*
+ * 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 crate::{ExtensionOutput, Scope};
+use std::{collections::HashMap, process::Command};
+
+pub fn convert_to_env_variables(scope: &Scope) -> HashMap<String, String> {
+  let mut output = HashMap::new();
+
+  for (key, result) in scope.iter() {
+    match result {
+      ExtensionOutput::Single(value) => {
+        let name = format!("ESPANSO_{}", key.to_uppercase());
+        output.insert(name, value.clone());
+      }
+      ExtensionOutput::Multiple(values) => {
+        for (sub_key, sub_value) in values.iter() {
+          let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase());
+          output.insert(name, sub_value.clone());
+        }
+      }
+    }
+  }
+
+  output
+}
+
+#[cfg(target_os = "windows")]
+pub fn set_command_flags(command: &mut Command) {
+  use std::os::windows::process::CommandExt;
+  // Avoid showing the shell window
+  // See: https://github.com/federico-terzi/espanso/issues/249
+  command.creation_flags(0x08000000);
+}
+
+#[cfg(not(target_os = "windows"))]
+pub fn set_command_flags(_: &mut Command) {
+  // NOOP on Linux and macOS
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_convert_to_env_variables() {
+    let mut vars: Scope = HashMap::new();
+    let mut subvars = HashMap::new();
+    subvars.insert("name".to_owned(), "John".to_owned());
+    subvars.insert("lastname".to_owned(), "Snow".to_owned());
+    vars.insert("form1", ExtensionOutput::Multiple(subvars));
+    vars.insert("var1", ExtensionOutput::Single("test".to_owned()));
+
+    let output = convert_to_env_variables(&vars);
+    assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John");
+    assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow");
+    assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test");
+  }
+}
diff --git a/espanso-render/src/lib.rs b/espanso-render/src/lib.rs
new file mode 100644
index 00000000..b9932da6
--- /dev/null
+++ b/espanso-render/src/lib.rs
@@ -0,0 +1,150 @@
+/*
+ * 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/>.
+ */
+
+#[macro_use]
+extern crate lazy_static;
+
+use enum_as_inner::EnumAsInner;
+use std::collections::HashMap;
+
+pub mod extension;
+mod renderer;
+
+pub trait Renderer {
+  fn render(&self, template: &Template, context: &Context, options: &RenderOptions)
+    -> RenderResult;
+}
+
+pub fn create(extensions: Vec<&dyn Extension>) -> impl Renderer + '_ {
+  renderer::DefaultRenderer::new(extensions)
+}
+
+#[derive(Debug)]
+pub enum RenderResult {
+  Success(String),
+  Aborted,
+  Error(anyhow::Error),
+}
+
+pub struct Context<'a> {
+  pub global_vars: Vec<&'a Variable>,
+  pub templates: Vec<&'a Template>,
+}
+
+impl<'a> Default for Context<'a> {
+  fn default() -> Self {
+    Self {
+      global_vars: Vec::new(),
+      templates: Vec::new(),
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RenderOptions {
+  pub casing_style: CasingStyle,
+}
+
+impl Default for RenderOptions {
+  fn default() -> Self {
+    Self {
+      casing_style: CasingStyle::None,
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum CasingStyle {
+  None,
+  Capitalize,
+  CapitalizeWords,
+  Uppercase,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Template {
+  pub ids: Vec<String>,
+  pub body: String,
+  pub vars: Vec<Variable>,
+}
+
+impl Default for Template {
+  fn default() -> Self {
+    Self {
+      ids: Vec::new(),
+      body: "".to_string(),
+      vars: Vec::new(),
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Variable {
+  pub name: String,
+  pub var_type: String,
+  pub params: Params,
+}
+
+impl Default for Variable {
+  fn default() -> Self {
+    Self {
+      name: "".to_string(),
+      var_type: "".to_string(),
+      params: Params::new(),
+    }
+  }
+}
+
+pub type Params = HashMap<String, Value>;
+
+#[derive(Debug, Clone, PartialEq, EnumAsInner)]
+pub enum Value {
+  Null,
+  Bool(bool),
+  Number(Number),
+  String(String),
+  Array(Vec<Value>),
+  Object(HashMap<String, Value>),
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Number {
+  Integer(i64),
+  Float(f64),
+}
+
+pub trait Extension {
+  fn name(&self) -> &str;
+  fn calculate(&self, context: &Context, scope: &Scope, params: &Params) -> ExtensionResult;
+}
+
+pub type Scope<'a> = HashMap<&'a str, ExtensionOutput>;
+
+#[derive(Debug, PartialEq)]
+pub enum ExtensionOutput {
+  Single(String),
+  Multiple(HashMap<String, String>),
+}
+
+#[derive(Debug, EnumAsInner)]
+pub enum ExtensionResult {
+  Success(ExtensionOutput),
+  Aborted,
+  Error(anyhow::Error),
+}
diff --git a/espanso-render/src/renderer/mod.rs b/espanso-render/src/renderer/mod.rs
new file mode 100644
index 00000000..917d9211
--- /dev/null
+++ b/espanso-render/src/renderer/mod.rs
@@ -0,0 +1,554 @@
+/*
+ * This file is part of esp name: (), var_type: (), params: ()anso.
+ *
+ * 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};
+
+use crate::{
+  CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
+  Renderer, Scope, Template, Value, Variable,
+};
+use anyhow::Result;
+use log::{error, warn};
+use regex::{Captures, Regex};
+use thiserror::Error;
+use util::get_body_variable_names;
+
+mod util;
+
+lazy_static! {
+  pub(crate) static ref VAR_REGEX: Regex =
+    Regex::new(r"\{\{\s*((?P<name>\w+)(\.(?P<subname>(\w+)))?)\s*\}\}").unwrap();
+  static ref WORD_REGEX: Regex = Regex::new(r"(\w+)").unwrap();
+}
+
+pub(crate) struct DefaultRenderer<'a> {
+  extensions: HashMap<String, &'a dyn Extension>,
+}
+
+impl<'a> DefaultRenderer<'a> {
+  pub fn new(extensions: Vec<&'a dyn Extension>) -> Self {
+    let extensions = extensions
+      .into_iter()
+      .map(|ext| (ext.name().to_string(), ext))
+      .collect();
+    Self { extensions }
+  }
+}
+
+impl<'a> Renderer for DefaultRenderer<'a> {
+  fn render(
+    &self,
+    template: &Template,
+    context: &Context,
+    options: &RenderOptions,
+  ) -> RenderResult {
+    let body = if VAR_REGEX.is_match(&template.body) {
+      // In order to define a variable evaluation order, we first need to find
+      // the global variables that are being used but for which an explicit order
+      // is not defined.
+      let body_variable_names = get_body_variable_names(&template.body);
+      let local_variable_names: HashSet<&str> =
+        template.vars.iter().map(|var| var.name.as_str()).collect();
+      let missing_global_variable_names: HashSet<&str> = body_variable_names
+        .difference(&local_variable_names)
+        .copied()
+        .collect();
+      let missing_global_variables: Vec<&Variable> = context
+        .global_vars
+        .iter()
+        .copied()
+        .filter(|global_var| missing_global_variable_names.contains(&*global_var.name))
+        .collect();
+
+      // Convert "global" variable type aliases when needed
+      let local_variables: Vec<&Variable> =
+        if template.vars.iter().any(|var| var.var_type == "global") {
+          let global_vars: HashMap<&str, &Variable> = context
+            .global_vars
+            .iter()
+            .map(|var| (&*var.name, *var))
+            .collect();
+          template
+            .vars
+            .iter()
+            .filter_map(|var| {
+              if var.var_type == "global" {
+                global_vars.get(&*var.name).copied()
+              } else {
+                Some(var)
+              }
+            })
+            .collect()
+        } else {
+          template.vars.iter().collect()
+        };
+
+      // The implicit global variables will be evaluated first, followed by the local vars
+      let mut variables: Vec<&Variable> = Vec::new();
+      variables.extend(missing_global_variables);
+      variables.extend(local_variables.iter());
+
+      // Compute the variable outputs
+      let mut scope = Scope::new();
+      for variable in variables {
+        if variable.var_type == "match" {
+          // Recursive call
+          // Call render recursively
+          if let Some(sub_template) = get_matching_template(variable, context.templates.as_slice())
+          {
+            match self.render(sub_template, context, options) {
+              RenderResult::Success(output) => {
+                scope.insert(&variable.name, ExtensionOutput::Single(output));
+              }
+              result => return result,
+            }
+          } else {
+            error!("unable to find sub-match: {}", variable.name);
+            return RenderResult::Error(RendererError::MissingSubMatch.into());
+          }
+        } else if let Some(extension) = self.extensions.get(&variable.var_type) {
+          match extension.calculate(context, &scope, &variable.params) {
+            ExtensionResult::Success(output) => {
+              scope.insert(&variable.name, output);
+            }
+            ExtensionResult::Aborted => {
+              warn!(
+                "rendering was aborted by extension: {}, on var: {}",
+                variable.var_type, variable.name
+              );
+              return RenderResult::Aborted;
+            }
+            ExtensionResult::Error(err) => {
+              warn!(
+                "extension '{}' on var: '{}' reported an error: {}",
+                variable.var_type, variable.name, err
+              );
+              return RenderResult::Error(err);
+            }
+          }
+        } else {
+          error!(
+            "no extension found for variable type: {}",
+            variable.var_type
+          );
+        }
+      }
+
+      // Replace the variables
+      match render_variables(&template.body, &scope) {
+        Ok(output) => output,
+        Err(error) => {
+          return RenderResult::Error(error);
+        }
+      }
+    } else {
+      template.body.clone()
+    };
+
+    // Process the casing style
+    let body_with_casing = match options.casing_style {
+      CasingStyle::None => body,
+      CasingStyle::Uppercase => body.to_uppercase(),
+      CasingStyle::Capitalize => {
+        // Capitalize the first letter
+        let mut v: Vec<char> = body.chars().collect();
+        v[0] = v[0].to_uppercase().next().unwrap();
+        v.into_iter().collect()
+      }
+      CasingStyle::CapitalizeWords => {
+        // Capitalize the first letter of each word
+        WORD_REGEX
+          .replace_all(&body, |caps: &Captures| {
+            if let Some(word_match) = caps.get(0) {
+              let mut v: Vec<char> = word_match.as_str().chars().collect();
+              v[0] = v[0].to_uppercase().next().unwrap();
+              let capitalized_word: String = v.into_iter().collect();
+              capitalized_word
+            } else {
+              "".to_string()
+            }
+          })
+          .to_string()
+      }
+    };
+
+    RenderResult::Success(body_with_casing)
+  }
+}
+
+// TODO: test
+pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
+  let mut replacing_error = None;
+  let output = VAR_REGEX
+    .replace_all(body, |caps: &Captures| {
+      let var_name = caps.name("name").unwrap().as_str();
+      let var_subname = caps.name("subname");
+      match scope.get(var_name) {
+        Some(output) => match output {
+          ExtensionOutput::Single(output) => output,
+          ExtensionOutput::Multiple(results) => match var_subname {
+            Some(var_subname) => {
+              let var_subname = var_subname.as_str();
+              results.get(var_subname).map_or("", |value| &*value)
+            }
+            None => {
+              error!(
+                "nested name missing from multi-value variable: {}",
+                var_name
+              );
+              replacing_error = Some(RendererError::MissingVariable(format!(
+                "nested name missing from multi-value variable: {}",
+                var_name
+              )));
+              ""
+            }
+          },
+        },
+        None => {
+          replacing_error = Some(RendererError::MissingVariable(format!(
+            "variable '{}' is missing",
+            var_name
+          )));
+          ""
+        }
+      }
+    })
+    .to_string();
+
+  if let Some(error) = replacing_error {
+    return Err(error.into());
+  }
+
+  Ok(output)
+}
+
+fn get_matching_template<'a>(
+  variable: &Variable,
+  templates: &'a [&Template],
+) -> Option<&'a Template> {
+  // Find matching template
+  let id = variable.params.get("trigger")?;
+  if let Value::String(id) = id {
+    templates
+      .iter()
+      .find(|template| template.ids.contains(id))
+      .copied()
+  } else {
+    None
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum RendererError {
+  #[error("missing variable: `{0}`")]
+  MissingVariable(String),
+
+  #[error("missing sub match")]
+  MissingSubMatch,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::Params;
+  use std::iter::FromIterator;
+
+  struct MockExtension {}
+
+  impl Extension for MockExtension {
+    fn name(&self) -> &str {
+      "mock"
+    }
+
+    fn calculate(
+      &self,
+      _context: &Context,
+      scope: &Scope,
+      params: &crate::Params,
+    ) -> ExtensionResult {
+      if let Some(Value::String(string)) = params.get("echo") {
+        return ExtensionResult::Success(ExtensionOutput::Single(string.clone()));
+      }
+      // If the "read" param is present, echo the value of the corresponding result in the scope
+      if let Some(Value::String(string)) = params.get("read") {
+        if let Some(ExtensionOutput::Single(value)) = scope.get(string.as_str()) {
+          return ExtensionResult::Success(ExtensionOutput::Single(value.to_string()));
+        }
+      }
+      if params.get("abort").is_some() {
+        return ExtensionResult::Aborted;
+      }
+      if params.get("error").is_some() {
+        return ExtensionResult::Error(
+          RendererError::MissingVariable("missing".to_string()).into(),
+        );
+      }
+      ExtensionResult::Aborted
+    }
+  }
+
+  pub fn get_renderer() -> impl Renderer {
+    DefaultRenderer::new(vec![&MockExtension {}])
+  }
+
+  pub fn template_for_str(str: &str) -> Template {
+    Template {
+      ids: vec!["id".to_string()],
+      body: str.to_string(),
+      vars: Vec::new(),
+    }
+  }
+
+  pub fn template(body: &str, vars: &[(&str, &str)]) -> Template {
+    let vars = vars
+      .iter()
+      .map(|(name, value)| Variable {
+        name: (*name).to_string(),
+        var_type: "mock".to_string(),
+        params: vec![("echo".to_string(), Value::String((*value).to_string()))]
+          .into_iter()
+          .collect::<Params>(),
+      })
+      .collect();
+    Template {
+      ids: vec!["id".to_string()],
+      body: body.to_string(),
+      vars,
+    }
+  }
+
+  #[test]
+  fn no_variable_no_styling() {
+    let renderer = get_renderer();
+    let res = renderer.render(
+      &template_for_str("plain body"),
+      &Default::default(),
+      &Default::default(),
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "plain body"));
+  }
+
+  #[test]
+  fn no_variable_capitalize() {
+    let renderer = get_renderer();
+    let res = renderer.render(
+      &template_for_str("plain body"),
+      &Default::default(),
+      &RenderOptions {
+        casing_style: CasingStyle::Capitalize,
+      },
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "Plain body"));
+  }
+
+  #[test]
+  fn no_variable_capitalize_words() {
+    let renderer = get_renderer();
+    let res = renderer.render(
+      &template_for_str("ordinary least squares, with other.punctuation !Marks"),
+      &Default::default(),
+      &RenderOptions {
+        casing_style: CasingStyle::CapitalizeWords,
+      },
+    );
+    assert!(
+      matches!(res, RenderResult::Success(str) if str == "Ordinary Least Squares, With Other.Punctuation !Marks")
+    );
+  }
+
+  #[test]
+  fn no_variable_uppercase() {
+    let renderer = get_renderer();
+    let res = renderer.render(
+      &template_for_str("plain body"),
+      &Default::default(),
+      &RenderOptions {
+        casing_style: CasingStyle::Uppercase,
+      },
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "PLAIN BODY"));
+  }
+
+  #[test]
+  fn basic_variable() {
+    let renderer = get_renderer();
+    let template = template("hello {{var}}", &[("var", "world")]);
+    let res = renderer.render(&template, &Default::default(), &Default::default());
+    assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
+  }
+
+  #[test]
+  fn missing_variable() {
+    let renderer = get_renderer();
+    let template = template_for_str("hello {{var}}");
+    let res = renderer.render(&template, &Default::default(), &Default::default());
+    assert!(matches!(res, RenderResult::Error(_)));
+  }
+
+  #[test]
+  fn global_variable() {
+    let renderer = get_renderer();
+    let template = template("hello {{var}}", &[]);
+    let res = renderer.render(
+      &template,
+      &Context {
+        global_vars: vec![&Variable {
+          name: "var".to_string(),
+          var_type: "mock".to_string(),
+          params: Params::from_iter(vec![(
+            "echo".to_string(),
+            Value::String("world".to_string()),
+          )]),
+        }],
+        ..Default::default()
+      },
+      &Default::default(),
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
+  }
+
+  #[test]
+  fn global_variable_explicit_ordering() {
+    let renderer = get_renderer();
+    let template = Template {
+      body: "hello {{var}} {{local}}".to_string(),
+      vars: vec![
+        Variable {
+          name: "local".to_string(),
+          var_type: "mock".to_string(),
+          params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
+            .into_iter()
+            .collect::<Params>(),
+        },
+        Variable {
+          name: "var".to_string(),
+          var_type: "global".to_string(),
+          ..Default::default()
+        },
+      ],
+      ..Default::default()
+    };
+    let res = renderer.render(
+      &template,
+      &Context {
+        global_vars: vec![&Variable {
+          name: "var".to_string(),
+          var_type: "mock".to_string(),
+          params: Params::from_iter(vec![(
+            "read".to_string(),
+            Value::String("local".to_string()),
+          )]),
+        }],
+        ..Default::default()
+      },
+      &Default::default(),
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
+  }
+
+  #[test]
+  fn nested_match() {
+    let renderer = get_renderer();
+    let template = Template {
+      body: "hello {{var}}".to_string(),
+      vars: vec![Variable {
+        name: "var".to_string(),
+        var_type: "match".to_string(),
+        params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
+          .into_iter()
+          .collect::<Params>(),
+      }],
+      ..Default::default()
+    };
+    let nested_template = Template {
+      ids: vec!["nested".to_string()],
+      body: "world".to_string(),
+      ..Default::default()
+    };
+    let res = renderer.render(
+      &template,
+      &Context {
+        templates: vec![&nested_template],
+        ..Default::default()
+      },
+      &Default::default(),
+    );
+    assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
+  }
+
+  #[test]
+  fn missing_nested_match() {
+    let renderer = get_renderer();
+    let template = Template {
+      body: "hello {{var}}".to_string(),
+      vars: vec![Variable {
+        name: "var".to_string(),
+        var_type: "match".to_string(),
+        params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
+          .into_iter()
+          .collect::<Params>(),
+      }],
+      ..Default::default()
+    };
+    let res = renderer.render(
+      &template,
+      &Context {
+        ..Default::default()
+      },
+      &Default::default(),
+    );
+    assert!(matches!(res, RenderResult::Error(_)));
+  }
+
+  #[test]
+  fn extension_aborting_propagates() {
+    let renderer = get_renderer();
+    let template = Template {
+      body: "hello {{var}}".to_string(),
+      vars: vec![Variable {
+        name: "var".to_string(),
+        var_type: "mock".to_string(),
+        params: vec![("abort".to_string(), Value::Null)]
+          .into_iter()
+          .collect::<Params>(),
+      }],
+      ..Default::default()
+    };
+    let res = renderer.render(&template, &Default::default(), &Default::default());
+    assert!(matches!(res, RenderResult::Aborted));
+  }
+
+  #[test]
+  fn extension_error_propagates() {
+    let renderer = get_renderer();
+    let template = Template {
+      body: "hello {{var}}".to_string(),
+      vars: vec![Variable {
+        name: "var".to_string(),
+        var_type: "mock".to_string(),
+        params: vec![("error".to_string(), Value::Null)]
+          .into_iter()
+          .collect::<Params>(),
+      }],
+      ..Default::default()
+    };
+    let res = renderer.render(&template, &Default::default(), &Default::default());
+    assert!(matches!(res, RenderResult::Error(_)));
+  }
+}
diff --git a/espanso-render/src/renderer/util.rs b/espanso-render/src/renderer/util.rs
new file mode 100644
index 00000000..e053cf79
--- /dev/null
+++ b/espanso-render/src/renderer/util.rs
@@ -0,0 +1,52 @@
+/*
+ * 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 super::VAR_REGEX;
+use std::collections::HashSet;
+
+pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
+  let mut variables = HashSet::new();
+  for caps in VAR_REGEX.captures_iter(body) {
+    let var_name = caps.name("name").unwrap().as_str();
+    variables.insert(var_name);
+  }
+  variables
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use std::iter::FromIterator;
+
+  #[test]
+  fn get_body_variable_names_no_vars() {
+    assert_eq!(
+      get_body_variable_names("no variables"),
+      HashSet::from_iter(vec![]),
+    );
+  }
+
+  #[test]
+  fn get_body_variable_names_multiple_vars() {
+    assert_eq!(
+      get_body_variable_names("hello {{world}} name {{greet}}"),
+      HashSet::from_iter(vec!["world", "greet"]),
+    );
+  }
+}
diff --git a/espanso-ui/Cargo.toml b/espanso-ui/Cargo.toml
new file mode 100644
index 00000000..1194758a
--- /dev/null
+++ b/espanso-ui/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "espanso-ui"
+version = "0.1.0"
+authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
+edition = "2018"
+build="build.rs"
+
+[features]
+# If enabled, avoid linking with the gdiplus library on Windows, which
+# might conflict with wxWidgets
+avoid-gdi = []
+
+[dependencies]
+log = "0.4.14"
+serde_json = "1.0.61"
+serde = { version = "1.0.123", features = ["derive"] }
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+lazycell = "1.3.0"
+winrt-notification = "0.3.1"
+lazy_static = "1.4.0"
+
+[target.'cfg(target_os="macos")'.dependencies]
+lazycell = "1.3.0"
+
+[target.'cfg(target_os="linux")'.dependencies]
+notify-rust = "4.2.2"
+crossbeam = "0.8.0"
+
+[build-dependencies]
+cc = "1.0.66"
\ No newline at end of file
diff --git a/espanso-ui/build.rs b/espanso-ui/build.rs
new file mode 100644
index 00000000..cf172726
--- /dev/null
+++ b/espanso-ui/build.rs
@@ -0,0 +1,68 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/win32/native.cpp");
+  println!("cargo:rerun-if-changed=src/win32/native.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/win32/native.h")
+    .include("src/win32/json/json.hpp")
+    .file("src/win32/native.cpp")
+    .compile("espansoui");
+
+  println!("cargo:rustc-link-lib=static=espansoui");
+  println!("cargo:rustc-link-lib=dylib=user32");
+  #[cfg(target_env = "gnu")]
+  println!("cargo:rustc-link-lib=dylib=stdc++");
+
+  if cfg!(not(feature = "avoid-gdi")) {
+    println!("cargo:rustc-link-lib=dylib=gdiplus");
+    println!("cargo:rustc-link-lib=dylib=gdi32");
+  }
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+  // Nothing to link on linux
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+  println!("cargo:rerun-if-changed=src/mac/native.mm");
+  println!("cargo:rerun-if-changed=src/mac/native.h");
+  println!("cargo:rerun-if-changed=src/mac/AppDelegate.mm");
+  println!("cargo:rerun-if-changed=src/mac/AppDelegate.h");
+  cc::Build::new()
+    .cpp(true)
+    .include("src/mac/native.h")
+    .include("src/mac/AppDelegate.h")
+    .file("src/mac/native.mm")
+    .file("src/mac/AppDelegate.mm")
+    .compile("espansoui");
+  println!("cargo:rustc-link-lib=dylib=c++");
+  println!("cargo:rustc-link-lib=static=espansoui");
+  println!("cargo:rustc-link-lib=framework=Cocoa");
+  println!("cargo:rustc-link-lib=framework=IOKit");
+}
+
+fn main() {
+  cc_config();
+}
diff --git a/espanso-ui/src/event.rs b/espanso-ui/src/event.rs
new file mode 100644
index 00000000..b3eb580d
--- /dev/null
+++ b/espanso-ui/src/event.rs
@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum UIEvent {
+  TrayIconClick,
+  ContextMenuClick(u32),
+  Heartbeat,
+}
diff --git a/espanso-ui/src/icons.rs b/espanso-ui/src/icons.rs
new file mode 100644
index 00000000..b1adca07
--- /dev/null
+++ b/espanso-ui/src/icons.rs
@@ -0,0 +1,27 @@
+/*
+ * 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/>.
+ */
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum TrayIcon {
+  Normal,
+  Disabled,
+
+  // For example, when macOS activates SecureInput
+  SystemDisabled,
+}
diff --git a/espanso-ui/src/lib.rs b/espanso-ui/src/lib.rs
new file mode 100644
index 00000000..07e92792
--- /dev/null
+++ b/espanso-ui/src/lib.rs
@@ -0,0 +1,101 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use icons::TrayIcon;
+use thiserror::Error;
+
+pub mod event;
+pub mod icons;
+pub mod menu;
+
+#[cfg(target_os = "windows")]
+pub mod win32;
+
+#[cfg(target_os = "linux")]
+pub mod linux;
+
+#[cfg(target_os = "macos")]
+pub mod mac;
+
+pub trait UIRemote: Send {
+  fn update_tray_icon(&self, icon: TrayIcon);
+  fn show_notification(&self, message: &str);
+  fn show_context_menu(&self, menu: &menu::Menu);
+  fn exit(&self);
+}
+
+pub type UIEventCallback = Box<dyn Fn(event::UIEvent)>;
+pub trait UIEventLoop {
+  fn initialize(&mut self) -> Result<()>;
+  fn run(&self, event_callback: UIEventCallback) -> Result<()>;
+}
+
+pub struct UIOptions {
+  pub show_icon: bool,
+  pub icon_paths: Vec<(TrayIcon, String)>,
+  pub notification_icon_path: Option<String>,
+}
+
+impl Default for UIOptions {
+  fn default() -> Self {
+    Self {
+      show_icon: true,
+      icon_paths: Vec::new(),
+      notification_icon_path: None,
+    }
+  }
+}
+
+#[cfg(target_os = "windows")]
+pub fn create_ui(options: UIOptions) -> Result<(Box<dyn UIRemote>, Box<dyn UIEventLoop>)> {
+  let (remote, eventloop) = win32::create(win32::Win32UIOptions {
+    show_icon: options.show_icon,
+    icon_paths: &options.icon_paths,
+    notification_icon_path: options
+      .notification_icon_path
+      .ok_or_else(|| UIError::MissingOption("notification icon".to_string()))?,
+  })?;
+  Ok((Box::new(remote), Box::new(eventloop)))
+}
+
+#[cfg(target_os = "macos")]
+pub fn create_ui(options: UIOptions) -> Result<(Box<dyn UIRemote>, Box<dyn UIEventLoop>)> {
+  let (remote, eventloop) = mac::create(mac::MacUIOptions {
+    show_icon: options.show_icon,
+    icon_paths: &options.icon_paths,
+  })?;
+  Ok((Box::new(remote), Box::new(eventloop)))
+}
+
+#[cfg(target_os = "linux")]
+pub fn create_ui(options: UIOptions) -> Result<(Box<dyn UIRemote>, Box<dyn UIEventLoop>)> {
+  let (remote, eventloop) = linux::create(linux::LinuxUIOptions {
+    notification_icon_path: options
+      .notification_icon_path
+      .ok_or_else(|| UIError::MissingOption("notification icon".to_string()))?,
+  });
+  Ok((Box::new(remote), Box::new(eventloop)))
+}
+
+#[derive(Error, Debug)]
+pub enum UIError {
+  #[error("missing required option for ui: `{0}`")]
+  MissingOption(String),
+}
diff --git a/espanso-ui/src/linux/mod.rs b/espanso-ui/src/linux/mod.rs
new file mode 100644
index 00000000..f51d3195
--- /dev/null
+++ b/espanso-ui/src/linux/mod.rs
@@ -0,0 +1,127 @@
+/*
+ * 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::time::Duration;
+
+use anyhow::Result;
+use crossbeam::{
+  channel::{unbounded, Receiver, Sender},
+  select,
+};
+use log::error;
+use notify_rust::Notification;
+
+use crate::{event::UIEvent, UIEventLoop, UIRemote};
+
+pub struct LinuxUIOptions {
+  pub notification_icon_path: String,
+}
+
+pub fn create(options: LinuxUIOptions) -> (LinuxRemote, LinuxEventLoop) {
+  let (tx, rx) = unbounded();
+  let remote = LinuxRemote::new(tx, options.notification_icon_path);
+  let eventloop = LinuxEventLoop::new(rx);
+  (remote, eventloop)
+}
+
+pub struct LinuxRemote {
+  tx: Sender<()>,
+  notification_icon_path: String,
+}
+
+impl LinuxRemote {
+  pub fn new(tx: Sender<()>, notification_icon_path: String) -> Self {
+    Self {
+      tx,
+      notification_icon_path,
+    }
+  }
+
+  pub fn stop(&self) -> anyhow::Result<()> {
+    Ok(self.tx.send(())?)
+  }
+}
+
+impl UIRemote for LinuxRemote {
+  fn update_tray_icon(&self, _: crate::icons::TrayIcon) {
+    // NOOP on linux
+  }
+
+  fn show_notification(&self, message: &str) {
+    if let Err(error) = Notification::new()
+      .summary("Espanso")
+      .body(message)
+      .icon(&self.notification_icon_path)
+      .show()
+    {
+      error!("Unable to show notification: {}", error);
+    }
+  }
+
+  fn show_context_menu(&self, _: &crate::menu::Menu) {
+    // NOOP on linux
+  }
+
+  fn exit(&self) {
+    self
+      .stop()
+      .expect("unable to send termination signal to ui eventloop");
+  }
+}
+
+pub struct LinuxEventLoop {
+  rx: Receiver<()>,
+}
+
+impl LinuxEventLoop {
+  pub fn new(rx: Receiver<()>) -> Self {
+    Self { rx }
+  }
+}
+
+impl UIEventLoop for LinuxEventLoop {
+  fn initialize(&mut self) -> Result<()> {
+    // NOOP on linux
+    Ok(())
+  }
+
+  fn run(&self, callback: crate::UIEventCallback) -> Result<()> {
+    loop {
+      select! {
+        recv(self.rx) -> result => {
+          // We don't run an event loop on Linux as there is no tray icon or application window needed.
+          // Thad said, we still need a way to block this method, and thus we use a channel
+          match result {
+            Ok(_) => {
+              // remote.exit() called
+              return Ok(());
+            }
+            Err(error) => {
+              error!("Unable to block the LinuxEventLoop: {}", error);
+              return Err(error.into());
+            }
+          }
+        },
+        default(Duration::from_millis(1000)) => {
+          (*callback)(UIEvent::Heartbeat);
+        }
+      }
+    }
+  }
+}
diff --git a/native/libmacbridge/AppDelegate.h b/espanso-ui/src/mac/AppDelegate.h
similarity index 64%
rename from native/libmacbridge/AppDelegate.h
rename to espanso-ui/src/mac/AppDelegate.h
index e37bb71b..2a529828 100644
--- a/native/libmacbridge/AppDelegate.h
+++ b/espanso-ui/src/mac/AppDelegate.h
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -20,16 +20,21 @@
 #import <AppKit/AppKit.h>
 #import <Foundation/Foundation.h>
 
-#include "bridge.h"
+#include "native.h"
 
-@interface AppDelegate : NSObject <NSApplicationDelegate> {
-    @public NSStatusItem *myStatusItem;
+@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate> {
+  @public NSStatusItem *statusItem;
+  @public UIOptions options;
+  @public void *rust_instance;
+  @public EventCallback event_callback;
 }
 
 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
+- (void) setIcon: (int32_t) iconIndex;
+- (void) popupMenu: (NSString *) payload;
+- (void) showNotification: (NSString *) message withDelay:(double) delay;
 - (IBAction) statusIconClick: (id) sender;
 - (IBAction) contextMenuClick: (id) sender;
-- (void) updateIcon: (char *)iconPath;
-- (void) setIcon: (char *)iconPath;
+- (void) heartbeatHandler: (NSTimer *)timer;
 
 @end
\ No newline at end of file
diff --git a/espanso-ui/src/mac/AppDelegate.mm b/espanso-ui/src/mac/AppDelegate.mm
new file mode 100644
index 00000000..4de6c195
--- /dev/null
+++ b/espanso-ui/src/mac/AppDelegate.mm
@@ -0,0 +1,149 @@
+/*
+ * 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/>.
+ */
+
+#import "AppDelegate.h"
+
+void addSeparatorMenu(NSMenu * parent);
+void addSingleMenu(NSMenu * parent, id item);
+void addSubMenu(NSMenu * parent, NSArray * items);
+
+@implementation AppDelegate
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+  if (options.show_icon) {
+    statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
+    [self setIcon: 0];
+  }
+
+  [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
+
+  // Heartbeat timer setup
+  [NSTimer scheduledTimerWithTimeInterval:1.0
+                                  target:self 
+                                selector:@selector(heartbeatHandler:)
+                                userInfo:nil
+                                 repeats:YES];
+}
+
+- (void) setIcon: (int32_t)iconIndex {
+  if (options.show_icon) {
+    char * iconPath = options.icon_paths[iconIndex];
+    NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
+
+    NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
+    [statusImage setTemplate:YES];
+
+    [statusItem.button setImage:statusImage];
+    [statusItem setHighlightMode:YES];
+    [statusItem.button setAction:@selector(statusIconClick:)];
+    [statusItem.button setTarget:self];
+  }
+}
+
+- (IBAction) statusIconClick: (id) sender {
+  UIEvent event = {};
+  event.event_type = UI_EVENT_TYPE_ICON_CLICK;
+  if (event_callback && rust_instance) {
+    event_callback(rust_instance, event);
+  }
+}
+
+- (void) popupMenu: (NSString *) payload {
+  NSError *jsonError;
+  NSData *data = [payload dataUsingEncoding:NSUTF8StringEncoding];
+  NSArray *jsonMenuItems = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
+  NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Espanso"];
+  addSubMenu(menu, jsonMenuItems);
+  [statusItem popUpStatusItemMenu: menu];
+}
+
+- (IBAction) contextMenuClick: (id) sender {
+  NSInteger itemId = [[sender valueForKey:@"tag"] integerValue];
+
+  UIEvent event = {};
+  event.event_type = UI_EVENT_TYPE_CONTEXT_MENU_CLICK;
+  event.context_menu_id = (uint32_t) [itemId intValue];
+  if (event_callback && rust_instance) {
+    event_callback(rust_instance, event);
+  }
+}
+
+- (void) showNotification: (NSString *) message withDelay: (double) delay {
+  NSUserNotification *notification = [[NSUserNotification alloc] init];
+  notification.title = @"Espanso";
+  notification.informativeText = message;
+  notification.soundName = nil;
+  
+  [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
+  [[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay];
+}
+
+- (void) heartbeatHandler: (NSTimer *)timer {
+  UIEvent event = {};
+  event.event_type = UI_EVENT_TYPE_HEARTBEAT;
+  if (event_callback && rust_instance) {
+    event_callback(rust_instance, event);
+  }
+}
+
+@end
+
+// Menu utility methods
+
+void addSeparatorMenu(NSMenu * parent)
+{
+  [parent addItem: [NSMenuItem separatorItem]];
+}
+
+void addSingleMenu(NSMenu * parent, id item)
+{
+  id label = [item objectForKey:@"label"];
+  id raw_id = [item objectForKey:@"id"];
+  if (label == nil || raw_id == nil)
+  {
+    return;
+  }
+  NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:label action:@selector(contextMenuClick:) keyEquivalent:@""];
+  [newMenu setTag:(NSInteger)raw_id];
+  [parent addItem: newMenu]; 
+}
+
+void addSubMenu(NSMenu * parent, NSArray * items)
+{
+  for (id item in items) {
+    id type = [item objectForKey:@"type"];
+    if ([type isEqualToString:@"simple"])
+    {
+      addSingleMenu(parent, item);
+    }
+    else if ([type isEqualToString:@"separator"])
+    {
+      addSeparatorMenu(parent);
+    }
+    else if ([type isEqualToString:@"sub"])
+    {
+      NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item objectForKey:@"label"] action:nil keyEquivalent:@""];
+      NSMenu *subMenu = [[NSMenu alloc] init];
+      [parent addItem: menuItem];
+      addSubMenu(subMenu, [item objectForKey:@"items"]);
+      [menuItem setSubmenu: subMenu];
+    }
+  }
+}
\ No newline at end of file
diff --git a/espanso-ui/src/mac/mod.rs b/espanso-ui/src/mac/mod.rs
new file mode 100644
index 00000000..b6c85692
--- /dev/null
+++ b/espanso-ui/src/mac/mod.rs
@@ -0,0 +1,252 @@
+/*
+ * 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::{cmp::min, collections::HashMap, ffi::CString, os::raw::c_char, thread::ThreadId};
+
+use anyhow::Result;
+use lazycell::LazyCell;
+use log::{error, trace};
+use thiserror::Error;
+
+use crate::{event::UIEvent, icons::TrayIcon, menu::Menu, UIEventLoop, UIRemote};
+
+// IMPORTANT: if you change these, also edit the native.h file.
+const MAX_FILE_PATH: usize = 1024;
+const MAX_ICON_COUNT: usize = 3;
+
+const UI_EVENT_TYPE_ICON_CLICK: i32 = 1;
+const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2;
+const UI_EVENT_TYPE_HEARTBEAT: i32 = 3;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIOptions {
+  pub show_icon: i32,
+
+  pub icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT],
+  pub icon_paths_count: i32,
+}
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIEvent {
+  pub event_type: i32,
+  pub context_menu_id: u32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansoui", kind = "static")]
+extern "C" {
+  pub fn ui_initialize(_self: *const MacEventLoop, options: RawUIOptions);
+  pub fn ui_eventloop(
+    event_callback: extern "C" fn(_self: *mut MacEventLoop, event: RawUIEvent),
+  ) -> i32;
+  pub fn ui_exit();
+  pub fn ui_update_tray_icon(index: i32);
+  pub fn ui_show_notification(message: *const c_char, delay: f64);
+  pub fn ui_show_context_menu(payload: *const c_char);
+}
+
+pub struct MacUIOptions<'a> {
+  pub show_icon: bool,
+  pub icon_paths: &'a Vec<(TrayIcon, String)>,
+}
+
+pub fn create(options: MacUIOptions) -> Result<(MacRemote, MacEventLoop)> {
+  // Validate icons
+  if options.icon_paths.len() > MAX_ICON_COUNT {
+    panic!("MacOS UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more");
+  }
+
+  // Convert the icon paths to the internal representation
+  let mut icon_indexes: HashMap<TrayIcon, usize> = HashMap::new();
+  let mut icons = Vec::new();
+  for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() {
+    icon_indexes.insert(tray_icon.clone(), index);
+    icons.push(path.clone());
+  }
+
+  let eventloop = MacEventLoop::new(icons, options.show_icon);
+  let remote = MacRemote::new(icon_indexes);
+
+  Ok((remote, eventloop))
+}
+
+pub type MacUIEventCallback = Box<dyn Fn(UIEvent)>;
+
+pub struct MacEventLoop {
+  show_icon: bool,
+  icons: Vec<String>,
+
+  // Internal
+  _event_callback: LazyCell<MacUIEventCallback>,
+  _init_thread_id: LazyCell<ThreadId>,
+}
+
+impl MacEventLoop {
+  pub(crate) fn new(icons: Vec<String>, show_icon: bool) -> Self {
+    Self {
+      icons,
+      show_icon,
+      _event_callback: LazyCell::new(),
+      _init_thread_id: LazyCell::new(),
+    }
+  }
+}
+
+impl UIEventLoop for MacEventLoop {
+  fn initialize(&mut self) -> Result<()> {
+    // Convert the icon paths to the raw representation
+    let mut icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT] =
+      [[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
+    for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
+      let c_path = CString::new(self.icons[i].clone())?;
+      let len = min(c_path.as_bytes().len(), MAX_FILE_PATH - 1);
+      icon_path[0..len].clone_from_slice(&c_path.as_bytes()[..len]);
+    }
+
+    let options = RawUIOptions {
+      show_icon: if self.show_icon { 1 } else { 0 },
+      icon_paths,
+      icon_paths_count: self.icons.len() as i32,
+    };
+
+    unsafe { ui_initialize(self as *const MacEventLoop, options) };
+
+    // Make sure the run() method is called in the same thread as initialize()
+    self
+      ._init_thread_id
+      .fill(std::thread::current().id())
+      .expect("Unable to set initialization thread id");
+
+    Ok(())
+  }
+
+  fn run(&self, event_callback: MacUIEventCallback) -> Result<()> {
+    // Make sure the run() method is called in the same thread as initialize()
+    if let Some(init_id) = self._init_thread_id.borrow() {
+      if init_id != &std::thread::current().id() {
+        panic!("MacEventLoop run() and initialize() methods should be called in the same thread");
+      }
+    }
+
+    if self._event_callback.fill(event_callback).is_err() {
+      error!("Unable to set MacEventLoop callback");
+      return Err(MacUIError::InternalError().into());
+    }
+
+    extern "C" fn callback(_self: *mut MacEventLoop, event: RawUIEvent) {
+      if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } {
+        let event: Option<UIEvent> = event.into();
+        if let Some(event) = event {
+          callback(event)
+        } else {
+          trace!("Unable to convert raw event to input event");
+        }
+      }
+    }
+
+    let error_code = unsafe { ui_eventloop(callback) };
+
+    if error_code <= 0 {
+      error!("MacEventLoop exited with <= 0 code");
+      return Err(MacUIError::InternalError().into());
+    }
+
+    Ok(())
+  }
+}
+
+pub struct MacRemote {
+  // Maps icon name to their index
+  icon_indexes: HashMap<TrayIcon, usize>,
+}
+
+impl MacRemote {
+  pub(crate) fn new(icon_indexes: HashMap<TrayIcon, usize>) -> Self {
+    Self { icon_indexes }
+  }
+}
+
+impl UIRemote for MacRemote {
+  fn update_tray_icon(&self, icon: TrayIcon) {
+    if let Some(index) = self.icon_indexes.get(&icon) {
+      unsafe { ui_update_tray_icon((*index) as i32) }
+    } else {
+      error!("Unable to update tray icon, invalid icon id");
+    }
+  }
+
+  fn show_notification(&self, message: &str) {
+    let c_string = CString::new(message);
+    match c_string {
+      Ok(message) => unsafe { ui_show_notification(message.as_ptr(), 3.0) },
+      Err(error) => {
+        error!("Unable to show notification {}", error);
+      }
+    }
+  }
+
+  fn show_context_menu(&self, menu: &Menu) {
+    match menu.to_json() {
+      Ok(payload) => {
+        let c_string = CString::new(payload);
+        match c_string {
+          Ok(c_string) => unsafe { ui_show_context_menu(c_string.as_ptr()) },
+          Err(error) => error!(
+            "Unable to show context menu, impossible to convert payload to c_string: {}",
+            error
+          ),
+        }
+      }
+      Err(error) => {
+        error!("Unable to show context menu, {}", error);
+      }
+    }
+  }
+
+  fn exit(&self) {
+    unsafe { ui_exit() };
+  }
+}
+
+#[allow(clippy::single_match)] // TODO: remove after another match is used
+impl From<RawUIEvent> for Option<UIEvent> {
+  fn from(raw: RawUIEvent) -> Option<UIEvent> {
+    match raw.event_type {
+      UI_EVENT_TYPE_ICON_CLICK => {
+        return Some(UIEvent::TrayIconClick);
+      }
+      UI_EVENT_TYPE_CONTEXT_MENU_CLICK => {
+        return Some(UIEvent::ContextMenuClick(raw.context_menu_id));
+      }
+      UI_EVENT_TYPE_HEARTBEAT => {
+        return Some(UIEvent::Heartbeat);
+      }
+      _ => {}
+    }
+
+    None
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum MacUIError {
+  #[error("internal error")]
+  InternalError(),
+}
diff --git a/espanso-ui/src/mac/native.h b/espanso-ui/src/mac/native.h
new file mode 100644
index 00000000..2e15900a
--- /dev/null
+++ b/espanso-ui/src/mac/native.h
@@ -0,0 +1,77 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_UI_H
+#define ESPANSO_UI_H
+
+#include <stdint.h>
+
+// Explicitly define this constant as we need to use it from the Rust side
+#define MAX_FILE_PATH 1024
+#define MAX_ICON_COUNT 3
+
+#define UI_EVENT_TYPE_ICON_CLICK 1
+#define UI_EVENT_TYPE_CONTEXT_MENU_CLICK 2
+#define UI_EVENT_TYPE_HEARTBEAT 3
+
+typedef struct {
+  int32_t show_icon;
+
+  char icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
+  int32_t icon_paths_count;
+} UIOptions;
+
+typedef struct {
+  int32_t event_type;
+  uint32_t context_menu_id;
+} UIEvent;
+
+typedef void (*EventCallback)(void * self, UIEvent data);
+
+typedef struct
+{
+  UIOptions options;
+
+  // Rust interop
+  void *rust_instance;
+  EventCallback event_callback;
+} UIVariables;
+
+// Initialize the Application delegate.
+extern "C" void ui_initialize(void * self, UIOptions options);
+
+// Run the event loop. Blocking call.
+extern "C" int32_t ui_eventloop(EventCallback callback);
+
+// Stops the application eventloop.
+extern "C" void ui_exit();
+
+// Updates the tray icon to the given one. The method accepts an index that refers to
+// the icon within the UIOptions.icon_paths array.
+extern "C" void ui_update_tray_icon(int32_t index);
+
+// Show a native notification
+extern "C" void ui_show_notification(char * message, double delay);
+
+// Display the context menu on the tray icon.
+// Payload is passed as JSON as given the complex structure, parsing
+// this manually would have been complex.
+extern "C" void ui_show_context_menu(char * payload);
+
+#endif //ESPANSO_UI_H
\ No newline at end of file
diff --git a/espanso-ui/src/mac/native.mm b/espanso-ui/src/mac/native.mm
new file mode 100644
index 00000000..50781266
--- /dev/null
+++ b/espanso-ui/src/mac/native.mm
@@ -0,0 +1,77 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include "AppDelegate.h"
+#import <Foundation/Foundation.h>
+#include <IOKit/IOKitLib.h>
+#include <stdio.h>
+#include <string.h>
+#include <libproc.h>
+
+void ui_initialize(void *_self, UIOptions _options)
+{
+  AppDelegate *delegate = [[AppDelegate alloc] init];
+  delegate->options = _options;
+  delegate->rust_instance = _self;
+
+  NSApplication * application = [NSApplication sharedApplication];
+  [application setDelegate:delegate];
+}
+
+int32_t ui_eventloop(EventCallback _callback)
+{
+  AppDelegate *delegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
+  delegate->event_callback = _callback;
+
+  [NSApp run];
+
+  return 1;
+}
+
+void ui_exit() {
+  [NSApp stop:nil];
+  [NSApp abortModal];
+}
+
+void ui_update_tray_icon(int32_t index)
+{
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    AppDelegate *delegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
+    [delegate setIcon: index];
+  });
+}
+
+void ui_show_notification(char *message, double delay)
+{
+  NSString *nsMessage = [NSString stringWithUTF8String:message];
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    AppDelegate *delegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
+    [delegate showNotification: nsMessage withDelay: delay];
+  });
+}
+
+void ui_show_context_menu(char *payload)
+{
+  NSString *nsPayload = [NSString stringWithUTF8String:payload];
+  dispatch_async(dispatch_get_main_queue(), ^(void) {
+    AppDelegate *delegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
+    [delegate popupMenu: nsPayload];
+  });
+}
\ No newline at end of file
diff --git a/espanso-ui/src/menu.rs b/espanso-ui/src/menu.rs
new file mode 100644
index 00000000..7d663124
--- /dev/null
+++ b/espanso-ui/src/menu.rs
@@ -0,0 +1,89 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug)]
+pub struct Menu {
+  pub items: Vec<MenuItem>,
+}
+
+impl Menu {
+  pub fn to_json(&self) -> Result<String> {
+    Ok(serde_json::to_string(&self.items)?)
+  }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(tag = "type")]
+#[serde(rename_all = "snake_case")]
+pub enum MenuItem {
+  Simple(SimpleMenuItem),
+  Sub(SubMenuItem),
+  Separator,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct SimpleMenuItem {
+  pub id: u32,
+  pub label: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct SubMenuItem {
+  pub label: String,
+  pub items: Vec<MenuItem>,
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_context_menu_serializes_correctly() {
+    let menu = Menu {
+      items: vec![
+        MenuItem::Simple(SimpleMenuItem {
+          id: 0,
+          label: "Open".to_string(),
+        }),
+        MenuItem::Separator,
+        MenuItem::Sub(SubMenuItem {
+          label: "Sub".to_string(),
+          items: vec![
+            MenuItem::Simple(SimpleMenuItem {
+              label: "Sub 1".to_string(),
+              id: 1,
+            }),
+            MenuItem::Simple(SimpleMenuItem {
+              label: "Sub 2".to_string(),
+              id: 2,
+            }),
+          ],
+        }),
+      ],
+    };
+
+    assert_eq!(
+      menu.to_json().unwrap(),
+      r#"[{"type":"simple","id":0,"label":"Open"},{"type":"separator"},{"type":"sub","label":"Sub","items":[{"type":"simple","id":1,"label":"Sub 1"},{"type":"simple","id":2,"label":"Sub 2"}]}]"#
+    );
+  }
+}
diff --git a/espanso-ui/src/win32/json/json.hpp b/espanso-ui/src/win32/json/json.hpp
new file mode 100644
index 00000000..9025ad38
--- /dev/null
+++ b/espanso-ui/src/win32/json/json.hpp
@@ -0,0 +1,25447 @@
+/*
+    __ _____ _____ _____
+ __|  |   __|     |   | |  JSON for Modern C++
+|  |  |__   |  |  | | | |  version 3.9.1
+|_____|_____|_____|_|___|  https://github.com/nlohmann/json
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2013-2019 Niels Lohmann <http://nlohmann.me>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef INCLUDE_NLOHMANN_JSON_HPP_
+#define INCLUDE_NLOHMANN_JSON_HPP_
+
+#define NLOHMANN_JSON_VERSION_MAJOR 3
+#define NLOHMANN_JSON_VERSION_MINOR 9
+#define NLOHMANN_JSON_VERSION_PATCH 1
+
+#include <algorithm> // all_of, find, for_each
+#include <cstddef> // nullptr_t, ptrdiff_t, size_t
+#include <functional> // hash, less
+#include <initializer_list> // initializer_list
+#include <iosfwd> // istream, ostream
+#include <iterator> // random_access_iterator_tag
+#include <memory> // unique_ptr
+#include <numeric> // accumulate
+#include <string> // string, stoi, to_string
+#include <utility> // declval, forward, move, pair, swap
+#include <vector> // vector
+
+// #include <nlohmann/adl_serializer.hpp>
+
+
+#include <utility>
+
+// #include <nlohmann/detail/conversions/from_json.hpp>
+
+
+#include <algorithm> // transform
+#include <array> // array
+#include <forward_list> // forward_list
+#include <iterator> // inserter, front_inserter, end
+#include <map> // map
+#include <string> // string
+#include <tuple> // tuple, make_tuple
+#include <type_traits> // is_arithmetic, is_same, is_enum, underlying_type, is_convertible
+#include <unordered_map> // unordered_map
+#include <utility> // pair, declval
+#include <valarray> // valarray
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+
+#include <exception> // exception
+#include <stdexcept> // runtime_error
+#include <string> // to_string
+
+// #include <nlohmann/detail/input/position_t.hpp>
+
+
+#include <cstddef> // size_t
+
+namespace nlohmann
+{
+namespace detail
+{
+/// struct to capture the start position of the current token
+struct position_t
+{
+    /// the total number of characters read
+    std::size_t chars_read_total = 0;
+    /// the number of characters read in the current line
+    std::size_t chars_read_current_line = 0;
+    /// the number of lines read
+    std::size_t lines_read = 0;
+
+    /// conversion to size_t to preserve SAX interface
+    constexpr operator size_t() const
+    {
+        return chars_read_total;
+    }
+};
+
+} // namespace detail
+} // namespace nlohmann
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+#include <utility> // pair
+// #include <nlohmann/thirdparty/hedley/hedley.hpp>
+/* Hedley - https://nemequ.github.io/hedley
+ * Created by Evan Nemerson <evan@nemerson.com>
+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to
+ * the public domain worldwide. This software is distributed without
+ * any warranty.
+ *
+ * For details, see <http://creativecommons.org/publicdomain/zero/1.0/>.
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 13)
+#if defined(JSON_HEDLEY_VERSION)
+    #undef JSON_HEDLEY_VERSION
+#endif
+#define JSON_HEDLEY_VERSION 13
+
+#if defined(JSON_HEDLEY_STRINGIFY_EX)
+    #undef JSON_HEDLEY_STRINGIFY_EX
+#endif
+#define JSON_HEDLEY_STRINGIFY_EX(x) #x
+
+#if defined(JSON_HEDLEY_STRINGIFY)
+    #undef JSON_HEDLEY_STRINGIFY
+#endif
+#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x)
+
+#if defined(JSON_HEDLEY_CONCAT_EX)
+    #undef JSON_HEDLEY_CONCAT_EX
+#endif
+#define JSON_HEDLEY_CONCAT_EX(a,b) a##b
+
+#if defined(JSON_HEDLEY_CONCAT)
+    #undef JSON_HEDLEY_CONCAT
+#endif
+#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b)
+
+#if defined(JSON_HEDLEY_CONCAT3_EX)
+    #undef JSON_HEDLEY_CONCAT3_EX
+#endif
+#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c
+
+#if defined(JSON_HEDLEY_CONCAT3)
+    #undef JSON_HEDLEY_CONCAT3
+#endif
+#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c)
+
+#if defined(JSON_HEDLEY_VERSION_ENCODE)
+    #undef JSON_HEDLEY_VERSION_ENCODE
+#endif
+#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision))
+
+#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR)
+    #undef JSON_HEDLEY_VERSION_DECODE_MAJOR
+#endif
+#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000)
+
+#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR)
+    #undef JSON_HEDLEY_VERSION_DECODE_MINOR
+#endif
+#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000)
+
+#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION)
+    #undef JSON_HEDLEY_VERSION_DECODE_REVISION
+#endif
+#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000)
+
+#if defined(JSON_HEDLEY_GNUC_VERSION)
+    #undef JSON_HEDLEY_GNUC_VERSION
+#endif
+#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__)
+    #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#elif defined(__GNUC__)
+    #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK)
+    #undef JSON_HEDLEY_GNUC_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_GNUC_VERSION)
+    #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_MSVC_VERSION)
+    #undef JSON_HEDLEY_MSVC_VERSION
+#endif
+#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000)
+    #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100)
+#elif defined(_MSC_FULL_VER)
+    #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10)
+#elif defined(_MSC_VER)
+    #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0)
+#endif
+
+#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK)
+    #undef JSON_HEDLEY_MSVC_VERSION_CHECK
+#endif
+#if !defined(_MSC_VER)
+    #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0)
+#elif defined(_MSC_VER) && (_MSC_VER >= 1400)
+    #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch)))
+#elif defined(_MSC_VER) && (_MSC_VER >= 1200)
+    #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch)))
+#else
+    #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor)))
+#endif
+
+#if defined(JSON_HEDLEY_INTEL_VERSION)
+    #undef JSON_HEDLEY_INTEL_VERSION
+#endif
+#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE)
+    #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE)
+#elif defined(__INTEL_COMPILER)
+    #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0)
+#endif
+
+#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK)
+    #undef JSON_HEDLEY_INTEL_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_INTEL_VERSION)
+    #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_PGI_VERSION)
+    #undef JSON_HEDLEY_PGI_VERSION
+#endif
+#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__)
+    #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__)
+#endif
+
+#if defined(JSON_HEDLEY_PGI_VERSION_CHECK)
+    #undef JSON_HEDLEY_PGI_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_PGI_VERSION)
+    #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_SUNPRO_VERSION)
+    #undef JSON_HEDLEY_SUNPRO_VERSION
+#endif
+#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000)
+    #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10)
+#elif defined(__SUNPRO_C)
+    #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf)
+#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000)
+    #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10)
+#elif defined(__SUNPRO_CC)
+    #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf)
+#endif
+
+#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK)
+    #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_SUNPRO_VERSION)
+    #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION)
+    #undef JSON_HEDLEY_EMSCRIPTEN_VERSION
+#endif
+#if defined(__EMSCRIPTEN__)
+    #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__)
+#endif
+
+#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK)
+    #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION)
+    #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_ARM_VERSION)
+    #undef JSON_HEDLEY_ARM_VERSION
+#endif
+#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION)
+    #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100)
+#elif defined(__CC_ARM) && defined(__ARMCC_VERSION)
+    #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100)
+#endif
+
+#if defined(JSON_HEDLEY_ARM_VERSION_CHECK)
+    #undef JSON_HEDLEY_ARM_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_ARM_VERSION)
+    #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_IBM_VERSION)
+    #undef JSON_HEDLEY_IBM_VERSION
+#endif
+#if defined(__ibmxl__)
+    #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__)
+#elif defined(__xlC__) && defined(__xlC_ver__)
+    #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff)
+#elif defined(__xlC__)
+    #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0)
+#endif
+
+#if defined(JSON_HEDLEY_IBM_VERSION_CHECK)
+    #undef JSON_HEDLEY_IBM_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_IBM_VERSION)
+    #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_VERSION)
+    #undef JSON_HEDLEY_TI_VERSION
+#endif
+#if \
+    defined(__TI_COMPILER_VERSION__) && \
+    ( \
+      defined(__TMS470__) || defined(__TI_ARM__) || \
+      defined(__MSP430__) || \
+      defined(__TMS320C2000__) \
+    )
+#if (__TI_COMPILER_VERSION__ >= 16000000)
+    #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+#endif
+
+#if defined(JSON_HEDLEY_TI_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_VERSION)
+    #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL2000_VERSION)
+    #undef JSON_HEDLEY_TI_CL2000_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__)
+    #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_CL2000_VERSION)
+    #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL430_VERSION)
+    #undef JSON_HEDLEY_TI_CL430_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__)
+    #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_CL430_VERSION)
+    #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_ARMCL_VERSION)
+    #undef JSON_HEDLEY_TI_ARMCL_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__))
+    #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_ARMCL_VERSION)
+    #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL6X_VERSION)
+    #undef JSON_HEDLEY_TI_CL6X_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__)
+    #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_CL6X_VERSION)
+    #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL7X_VERSION)
+    #undef JSON_HEDLEY_TI_CL7X_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__)
+    #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_CL7X_VERSION)
+    #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TI_CLPRU_VERSION)
+    #undef JSON_HEDLEY_TI_CLPRU_VERSION
+#endif
+#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__)
+    #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000))
+#endif
+
+#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK)
+    #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TI_CLPRU_VERSION)
+    #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_CRAY_VERSION)
+    #undef JSON_HEDLEY_CRAY_VERSION
+#endif
+#if defined(_CRAYC)
+    #if defined(_RELEASE_PATCHLEVEL)
+        #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL)
+    #else
+        #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0)
+    #endif
+#endif
+
+#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK)
+    #undef JSON_HEDLEY_CRAY_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_CRAY_VERSION)
+    #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_IAR_VERSION)
+    #undef JSON_HEDLEY_IAR_VERSION
+#endif
+#if defined(__IAR_SYSTEMS_ICC__)
+    #if __VER__ > 1000
+        #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000))
+    #else
+        #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(VER / 100, __VER__ % 100, 0)
+    #endif
+#endif
+
+#if defined(JSON_HEDLEY_IAR_VERSION_CHECK)
+    #undef JSON_HEDLEY_IAR_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_IAR_VERSION)
+    #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_TINYC_VERSION)
+    #undef JSON_HEDLEY_TINYC_VERSION
+#endif
+#if defined(__TINYC__)
+    #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100)
+#endif
+
+#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK)
+    #undef JSON_HEDLEY_TINYC_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_TINYC_VERSION)
+    #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_DMC_VERSION)
+    #undef JSON_HEDLEY_DMC_VERSION
+#endif
+#if defined(__DMC__)
+    #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf)
+#endif
+
+#if defined(JSON_HEDLEY_DMC_VERSION_CHECK)
+    #undef JSON_HEDLEY_DMC_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_DMC_VERSION)
+    #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_COMPCERT_VERSION)
+    #undef JSON_HEDLEY_COMPCERT_VERSION
+#endif
+#if defined(__COMPCERT_VERSION__)
+    #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100)
+#endif
+
+#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK)
+    #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_COMPCERT_VERSION)
+    #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_PELLES_VERSION)
+    #undef JSON_HEDLEY_PELLES_VERSION
+#endif
+#if defined(__POCC__)
+    #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0)
+#endif
+
+#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK)
+    #undef JSON_HEDLEY_PELLES_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_PELLES_VERSION)
+    #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_VERSION)
+    #undef JSON_HEDLEY_GCC_VERSION
+#endif
+#if \
+    defined(JSON_HEDLEY_GNUC_VERSION) && \
+    !defined(__clang__) && \
+    !defined(JSON_HEDLEY_INTEL_VERSION) && \
+    !defined(JSON_HEDLEY_PGI_VERSION) && \
+    !defined(JSON_HEDLEY_ARM_VERSION) && \
+    !defined(JSON_HEDLEY_TI_VERSION) && \
+    !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \
+    !defined(JSON_HEDLEY_TI_CL430_VERSION) && \
+    !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \
+    !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \
+    !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \
+    !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \
+    !defined(__COMPCERT__)
+    #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION
+#endif
+
+#if defined(JSON_HEDLEY_GCC_VERSION_CHECK)
+    #undef JSON_HEDLEY_GCC_VERSION_CHECK
+#endif
+#if defined(JSON_HEDLEY_GCC_VERSION)
+    #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch))
+#else
+    #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_ATTRIBUTE)
+    #undef JSON_HEDLEY_HAS_ATTRIBUTE
+#endif
+#if defined(__has_attribute)
+    #define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute)
+#else
+    #define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE)
+    #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE
+#endif
+#if defined(__has_attribute)
+    #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) __has_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE)
+    #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE
+#endif
+#if defined(__has_attribute)
+    #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) __has_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE)
+    #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE
+#endif
+#if \
+    defined(__has_cpp_attribute) && \
+    defined(__cplusplus) && \
+    (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0))
+    #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute)
+#else
+    #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS)
+    #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS
+#endif
+#if !defined(__cplusplus) || !defined(__has_cpp_attribute)
+    #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0)
+#elif \
+    !defined(JSON_HEDLEY_PGI_VERSION) && \
+    !defined(JSON_HEDLEY_IAR_VERSION) && \
+    (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \
+    (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0))
+    #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute)
+#else
+    #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE)
+    #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE
+#endif
+#if defined(__has_cpp_attribute) && defined(__cplusplus)
+    #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE)
+    #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE
+#endif
+#if defined(__has_cpp_attribute) && defined(__cplusplus)
+    #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_BUILTIN)
+    #undef JSON_HEDLEY_HAS_BUILTIN
+#endif
+#if defined(__has_builtin)
+    #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin)
+#else
+    #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN)
+    #undef JSON_HEDLEY_GNUC_HAS_BUILTIN
+#endif
+#if defined(__has_builtin)
+    #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN)
+    #undef JSON_HEDLEY_GCC_HAS_BUILTIN
+#endif
+#if defined(__has_builtin)
+    #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin)
+#else
+    #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_FEATURE)
+    #undef JSON_HEDLEY_HAS_FEATURE
+#endif
+#if defined(__has_feature)
+    #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature)
+#else
+    #define JSON_HEDLEY_HAS_FEATURE(feature) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE)
+    #undef JSON_HEDLEY_GNUC_HAS_FEATURE
+#endif
+#if defined(__has_feature)
+    #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_FEATURE)
+    #undef JSON_HEDLEY_GCC_HAS_FEATURE
+#endif
+#if defined(__has_feature)
+    #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature)
+#else
+    #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_EXTENSION)
+    #undef JSON_HEDLEY_HAS_EXTENSION
+#endif
+#if defined(__has_extension)
+    #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension)
+#else
+    #define JSON_HEDLEY_HAS_EXTENSION(extension) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION)
+    #undef JSON_HEDLEY_GNUC_HAS_EXTENSION
+#endif
+#if defined(__has_extension)
+    #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION)
+    #undef JSON_HEDLEY_GCC_HAS_EXTENSION
+#endif
+#if defined(__has_extension)
+    #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension)
+#else
+    #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE)
+    #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE
+#endif
+#if defined(__has_declspec_attribute)
+    #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute)
+#else
+    #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE)
+    #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE
+#endif
+#if defined(__has_declspec_attribute)
+    #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE)
+    #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE
+#endif
+#if defined(__has_declspec_attribute)
+    #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute)
+#else
+    #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_HAS_WARNING)
+    #undef JSON_HEDLEY_HAS_WARNING
+#endif
+#if defined(__has_warning)
+    #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning)
+#else
+    #define JSON_HEDLEY_HAS_WARNING(warning) (0)
+#endif
+
+#if defined(JSON_HEDLEY_GNUC_HAS_WARNING)
+    #undef JSON_HEDLEY_GNUC_HAS_WARNING
+#endif
+#if defined(__has_warning)
+    #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning)
+#else
+    #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_GCC_HAS_WARNING)
+    #undef JSON_HEDLEY_GCC_HAS_WARNING
+#endif
+#if defined(__has_warning)
+    #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning)
+#else
+    #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for
+   HEDLEY INTERNAL USE ONLY.  API subject to change without notice. */
+#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_)
+    #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_
+#endif
+#if defined(__cplusplus)
+#  if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat")
+#    if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions")
+#      define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \
+    _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \
+    xpr \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#    else
+#      define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \
+    xpr \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#    endif
+#  endif
+#endif
+#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x
+#endif
+
+#if defined(JSON_HEDLEY_CONST_CAST)
+    #undef JSON_HEDLEY_CONST_CAST
+#endif
+#if defined(__cplusplus)
+#  define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast<T>(expr))
+#elif \
+  JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \
+  JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+#  define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \
+        JSON_HEDLEY_DIAGNOSTIC_PUSH \
+        JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \
+        ((T) (expr)); \
+        JSON_HEDLEY_DIAGNOSTIC_POP \
+    }))
+#else
+#  define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr))
+#endif
+
+#if defined(JSON_HEDLEY_REINTERPRET_CAST)
+    #undef JSON_HEDLEY_REINTERPRET_CAST
+#endif
+#if defined(__cplusplus)
+    #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast<T>(expr))
+#else
+    #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr))
+#endif
+
+#if defined(JSON_HEDLEY_STATIC_CAST)
+    #undef JSON_HEDLEY_STATIC_CAST
+#endif
+#if defined(__cplusplus)
+    #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast<T>(expr))
+#else
+    #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr))
+#endif
+
+#if defined(JSON_HEDLEY_CPP_CAST)
+    #undef JSON_HEDLEY_CPP_CAST
+#endif
+#if defined(__cplusplus)
+#  if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast")
+#    define JSON_HEDLEY_CPP_CAST(T, expr) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \
+    ((T) (expr)) \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#  elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0)
+#    define JSON_HEDLEY_CPP_CAST(T, expr) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("diag_suppress=Pe137") \
+    JSON_HEDLEY_DIAGNOSTIC_POP \
+#  else
+#    define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr))
+#  endif
+#else
+#  define JSON_HEDLEY_CPP_CAST(T, expr) (expr)
+#endif
+
+#if \
+    (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \
+    defined(__clang__) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \
+    JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \
+    JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \
+    (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR))
+    #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value)
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0)
+    #define JSON_HEDLEY_PRAGMA(value) __pragma(value)
+#else
+    #define JSON_HEDLEY_PRAGMA(value)
+#endif
+
+#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH)
+    #undef JSON_HEDLEY_DIAGNOSTIC_PUSH
+#endif
+#if defined(JSON_HEDLEY_DIAGNOSTIC_POP)
+    #undef JSON_HEDLEY_DIAGNOSTIC_POP
+#endif
+#if defined(__clang__)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop")
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)")
+#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push))
+    #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop))
+#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop")
+#elif \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop")
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)")
+    #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)")
+#else
+    #define JSON_HEDLEY_DIAGNOSTIC_PUSH
+    #define JSON_HEDLEY_DIAGNOSTIC_POP
+#endif
+
+#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED)
+    #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations")
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)")
+#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444")
+#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996))
+#elif \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718")
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)")
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)")
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215")
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)")
+#else
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
+#endif
+
+#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS)
+    #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas")
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"")
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)")
+#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675")
+#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068))
+#elif \
+    JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163")
+#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163")
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161")
+#else
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS
+#endif
+
+#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES)
+    #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes")
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"")
+#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030))
+#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097")
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)")
+#elif \
+    JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173")
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097")
+#else
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES
+#endif
+
+#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL)
+    #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual")
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"")
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)")
+#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0)
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"")
+#else
+    #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL
+#endif
+
+#if defined(JSON_HEDLEY_DEPRECATED)
+    #undef JSON_HEDLEY_DEPRECATED
+#endif
+#if defined(JSON_HEDLEY_DEPRECATED_FOR)
+    #undef JSON_HEDLEY_DEPRECATED_FOR
+#endif
+#if JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0)
+    #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since))
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement))
+#elif defined(__cplusplus) && (__cplusplus >= 201402L)
+    #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]])
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]])
+#elif \
+    JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0)
+    #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since)))
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement)))
+#elif \
+    JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__))
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__))
+#elif \
+    JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \
+    JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0)
+    #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated)
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated)
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated")
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated")
+#else
+    #define JSON_HEDLEY_DEPRECATED(since)
+    #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement)
+#endif
+
+#if defined(JSON_HEDLEY_UNAVAILABLE)
+    #undef JSON_HEDLEY_UNAVAILABLE
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since)))
+#else
+    #define JSON_HEDLEY_UNAVAILABLE(available_since)
+#endif
+
+#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT)
+    #undef JSON_HEDLEY_WARN_UNUSED_RESULT
+#endif
+#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG)
+    #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG
+#endif
+#if (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L)
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]])
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]])
+#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard)
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]])
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]])
+#elif \
+    JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \
+    (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__))
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__))
+#elif defined(_Check_return_) /* SAL */
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_
+#else
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT
+    #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg)
+#endif
+
+#if defined(JSON_HEDLEY_SENTINEL)
+    #undef JSON_HEDLEY_SENTINEL
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0)
+    #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position)))
+#else
+    #define JSON_HEDLEY_SENTINEL(position)
+#endif
+
+#if defined(JSON_HEDLEY_NO_RETURN)
+    #undef JSON_HEDLEY_NO_RETURN
+#endif
+#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_NO_RETURN __noreturn
+#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__))
+#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
+    #define JSON_HEDLEY_NO_RETURN _Noreturn
+#elif defined(__cplusplus) && (__cplusplus >= 201103L)
+    #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]])
+#elif \
+    JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__))
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0)
+    #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0)
+    #define JSON_HEDLEY_NO_RETURN __declspec(noreturn)
+#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus)
+    #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;")
+#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0)
+    #define JSON_HEDLEY_NO_RETURN __attribute((noreturn))
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0)
+    #define JSON_HEDLEY_NO_RETURN __declspec(noreturn)
+#else
+    #define JSON_HEDLEY_NO_RETURN
+#endif
+
+#if defined(JSON_HEDLEY_NO_ESCAPE)
+    #undef JSON_HEDLEY_NO_ESCAPE
+#endif
+#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape)
+    #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__))
+#else
+    #define JSON_HEDLEY_NO_ESCAPE
+#endif
+
+#if defined(JSON_HEDLEY_UNREACHABLE)
+    #undef JSON_HEDLEY_UNREACHABLE
+#endif
+#if defined(JSON_HEDLEY_UNREACHABLE_RETURN)
+    #undef JSON_HEDLEY_UNREACHABLE_RETURN
+#endif
+#if defined(JSON_HEDLEY_ASSUME)
+    #undef JSON_HEDLEY_ASSUME
+#endif
+#if \
+    JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_ASSUME(expr) __assume(expr)
+#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume)
+    #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr)
+#elif \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0)
+    #if defined(__cplusplus)
+        #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr)
+    #else
+        #define JSON_HEDLEY_ASSUME(expr) _nassert(expr)
+    #endif
+#endif
+#if \
+    (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5)
+    #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable()
+#elif defined(JSON_HEDLEY_ASSUME)
+    #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0)
+#endif
+#if !defined(JSON_HEDLEY_ASSUME)
+    #if defined(JSON_HEDLEY_UNREACHABLE)
+        #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1)))
+    #else
+        #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr)
+    #endif
+#endif
+#if defined(JSON_HEDLEY_UNREACHABLE)
+    #if  \
+        JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \
+        JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0)
+        #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value))
+    #else
+        #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE()
+    #endif
+#else
+    #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value)
+#endif
+#if !defined(JSON_HEDLEY_UNREACHABLE)
+    #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0)
+#endif
+
+JSON_HEDLEY_DIAGNOSTIC_PUSH
+#if JSON_HEDLEY_HAS_WARNING("-Wpedantic")
+    #pragma clang diagnostic ignored "-Wpedantic"
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus)
+    #pragma clang diagnostic ignored "-Wc++98-compat-pedantic"
+#endif
+#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0)
+    #if defined(__clang__)
+        #pragma clang diagnostic ignored "-Wvariadic-macros"
+    #elif defined(JSON_HEDLEY_GCC_VERSION)
+        #pragma GCC diagnostic ignored "-Wvariadic-macros"
+    #endif
+#endif
+#if defined(JSON_HEDLEY_NON_NULL)
+    #undef JSON_HEDLEY_NON_NULL
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0)
+    #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__)))
+#else
+    #define JSON_HEDLEY_NON_NULL(...)
+#endif
+JSON_HEDLEY_DIAGNOSTIC_POP
+
+#if defined(JSON_HEDLEY_PRINTF_FORMAT)
+    #undef JSON_HEDLEY_PRINTF_FORMAT
+#endif
+#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO)
+    #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check)))
+#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO)
+    #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check)))
+#elif \
+    JSON_HEDLEY_HAS_ATTRIBUTE(format) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check)))
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0)
+    #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check))
+#else
+    #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check)
+#endif
+
+#if defined(JSON_HEDLEY_CONSTEXPR)
+    #undef JSON_HEDLEY_CONSTEXPR
+#endif
+#if defined(__cplusplus)
+    #if __cplusplus >= 201103L
+        #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr)
+    #endif
+#endif
+#if !defined(JSON_HEDLEY_CONSTEXPR)
+    #define JSON_HEDLEY_CONSTEXPR
+#endif
+
+#if defined(JSON_HEDLEY_PREDICT)
+    #undef JSON_HEDLEY_PREDICT
+#endif
+#if defined(JSON_HEDLEY_LIKELY)
+    #undef JSON_HEDLEY_LIKELY
+#endif
+#if defined(JSON_HEDLEY_UNLIKELY)
+    #undef JSON_HEDLEY_UNLIKELY
+#endif
+#if defined(JSON_HEDLEY_UNPREDICTABLE)
+    #undef JSON_HEDLEY_UNPREDICTABLE
+#endif
+#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable)
+    #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr))
+#endif
+#if \
+  JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) || \
+  JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0)
+#  define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability(  (expr), (value), (probability))
+#  define JSON_HEDLEY_PREDICT_TRUE(expr, probability)   __builtin_expect_with_probability(!!(expr),    1   , (probability))
+#  define JSON_HEDLEY_PREDICT_FALSE(expr, probability)  __builtin_expect_with_probability(!!(expr),    0   , (probability))
+#  define JSON_HEDLEY_LIKELY(expr)                      __builtin_expect                 (!!(expr),    1                  )
+#  define JSON_HEDLEY_UNLIKELY(expr)                    __builtin_expect                 (!!(expr),    0                  )
+#elif \
+  JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) || \
+  JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+  (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \
+  JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+  JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+  JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+  JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \
+  JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \
+  JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \
+  JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \
+  JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+  JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \
+  JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \
+  JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0)
+#  define JSON_HEDLEY_PREDICT(expr, expected, probability) \
+    (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)))
+#  define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \
+    (__extension__ ({ \
+        double hedley_probability_ = (probability); \
+        ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \
+    }))
+#  define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \
+    (__extension__ ({ \
+        double hedley_probability_ = (probability); \
+        ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \
+    }))
+#  define JSON_HEDLEY_LIKELY(expr)   __builtin_expect(!!(expr), 1)
+#  define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0)
+#else
+#  define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))
+#  define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr))
+#  define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr))
+#  define JSON_HEDLEY_LIKELY(expr) (!!(expr))
+#  define JSON_HEDLEY_UNLIKELY(expr) (!!(expr))
+#endif
+#if !defined(JSON_HEDLEY_UNPREDICTABLE)
+    #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5)
+#endif
+
+#if defined(JSON_HEDLEY_MALLOC)
+    #undef JSON_HEDLEY_MALLOC
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_MALLOC __attribute__((__malloc__))
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0)
+    #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory")
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(14, 0, 0)
+    #define JSON_HEDLEY_MALLOC __declspec(restrict)
+#else
+    #define JSON_HEDLEY_MALLOC
+#endif
+
+#if defined(JSON_HEDLEY_PURE)
+    #undef JSON_HEDLEY_PURE
+#endif
+#if \
+  JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \
+  JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+  JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+  JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+  JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+  JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+  (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+  (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+  (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+  (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+  JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+  JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \
+  JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+#  define JSON_HEDLEY_PURE __attribute__((__pure__))
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0)
+#  define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data")
+#elif defined(__cplusplus) && \
+    ( \
+      JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \
+      JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \
+      JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \
+    )
+#  define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;")
+#else
+#  define JSON_HEDLEY_PURE
+#endif
+
+#if defined(JSON_HEDLEY_CONST)
+    #undef JSON_HEDLEY_CONST
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(const) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0)
+    #define JSON_HEDLEY_CONST __attribute__((__const__))
+#elif \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0)
+    #define JSON_HEDLEY_CONST _Pragma("no_side_effect")
+#else
+    #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE
+#endif
+
+#if defined(JSON_HEDLEY_RESTRICT)
+    #undef JSON_HEDLEY_RESTRICT
+#endif
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus)
+    #define JSON_HEDLEY_RESTRICT restrict
+#elif \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \
+    JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+    JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \
+    JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \
+    defined(__clang__)
+    #define JSON_HEDLEY_RESTRICT __restrict
+#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus)
+    #define JSON_HEDLEY_RESTRICT _Restrict
+#else
+    #define JSON_HEDLEY_RESTRICT
+#endif
+
+#if defined(JSON_HEDLEY_INLINE)
+    #undef JSON_HEDLEY_INLINE
+#endif
+#if \
+    (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \
+    (defined(__cplusplus) && (__cplusplus >= 199711L))
+    #define JSON_HEDLEY_INLINE inline
+#elif \
+    defined(JSON_HEDLEY_GCC_VERSION) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0)
+    #define JSON_HEDLEY_INLINE __inline__
+#elif \
+    JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_INLINE __inline
+#else
+    #define JSON_HEDLEY_INLINE
+#endif
+
+#if defined(JSON_HEDLEY_ALWAYS_INLINE)
+    #undef JSON_HEDLEY_ALWAYS_INLINE
+#endif
+#if \
+  JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \
+  JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+  JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+  JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+  JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+  JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+  (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+  (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+  (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+  (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+  JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+  JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+  JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+#  define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0)
+#  define JSON_HEDLEY_ALWAYS_INLINE __forceinline
+#elif defined(__cplusplus) && \
+    ( \
+      JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+      JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+      JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+      JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \
+      JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+      JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \
+    )
+#  define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;")
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+#  define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced")
+#else
+#  define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE
+#endif
+
+#if defined(JSON_HEDLEY_NEVER_INLINE)
+    #undef JSON_HEDLEY_NEVER_INLINE
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \
+    JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \
+    (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \
+    (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \
+    (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \
+    (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \
+    JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \
+    JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0)
+    #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__))
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0)
+    #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline)
+#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0)
+    #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline")
+#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus)
+    #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;")
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+    #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never")
+#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0)
+    #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline))
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0)
+    #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline)
+#else
+    #define JSON_HEDLEY_NEVER_INLINE
+#endif
+
+#if defined(JSON_HEDLEY_PRIVATE)
+    #undef JSON_HEDLEY_PRIVATE
+#endif
+#if defined(JSON_HEDLEY_PUBLIC)
+    #undef JSON_HEDLEY_PUBLIC
+#endif
+#if defined(JSON_HEDLEY_IMPORT)
+    #undef JSON_HEDLEY_IMPORT
+#endif
+#if defined(_WIN32) || defined(__CYGWIN__)
+#  define JSON_HEDLEY_PRIVATE
+#  define JSON_HEDLEY_PUBLIC   __declspec(dllexport)
+#  define JSON_HEDLEY_IMPORT   __declspec(dllimport)
+#else
+#  if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \
+    JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \
+    ( \
+      defined(__TI_EABI__) && \
+      ( \
+        (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \
+        JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \
+      ) \
+    )
+#    define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden")))
+#    define JSON_HEDLEY_PUBLIC  __attribute__((__visibility__("default")))
+#  else
+#    define JSON_HEDLEY_PRIVATE
+#    define JSON_HEDLEY_PUBLIC
+#  endif
+#  define JSON_HEDLEY_IMPORT    extern
+#endif
+
+#if defined(JSON_HEDLEY_NO_THROW)
+    #undef JSON_HEDLEY_NO_THROW
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+    #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__))
+#elif \
+    JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0)
+    #define JSON_HEDLEY_NO_THROW __declspec(nothrow)
+#else
+    #define JSON_HEDLEY_NO_THROW
+#endif
+
+#if defined(JSON_HEDLEY_FALL_THROUGH)
+    #undef JSON_HEDLEY_FALL_THROUGH
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0)
+    #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__))
+#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough)
+    #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]])
+#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough)
+    #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]])
+#elif defined(__fallthrough) /* SAL */
+    #define JSON_HEDLEY_FALL_THROUGH __fallthrough
+#else
+    #define JSON_HEDLEY_FALL_THROUGH
+#endif
+
+#if defined(JSON_HEDLEY_RETURNS_NON_NULL)
+    #undef JSON_HEDLEY_RETURNS_NON_NULL
+#endif
+#if \
+    JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0)
+    #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__))
+#elif defined(_Ret_notnull_) /* SAL */
+    #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_
+#else
+    #define JSON_HEDLEY_RETURNS_NON_NULL
+#endif
+
+#if defined(JSON_HEDLEY_ARRAY_PARAM)
+    #undef JSON_HEDLEY_ARRAY_PARAM
+#endif
+#if \
+    defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \
+    !defined(__STDC_NO_VLA__) && \
+    !defined(__cplusplus) && \
+    !defined(JSON_HEDLEY_PGI_VERSION) && \
+    !defined(JSON_HEDLEY_TINYC_VERSION)
+    #define JSON_HEDLEY_ARRAY_PARAM(name) (name)
+#else
+    #define JSON_HEDLEY_ARRAY_PARAM(name)
+#endif
+
+#if defined(JSON_HEDLEY_IS_CONSTANT)
+    #undef JSON_HEDLEY_IS_CONSTANT
+#endif
+#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR)
+    #undef JSON_HEDLEY_REQUIRE_CONSTEXPR
+#endif
+/* JSON_HEDLEY_IS_CONSTEXPR_ is for
+   HEDLEY INTERNAL USE ONLY.  API subject to change without notice. */
+#if defined(JSON_HEDLEY_IS_CONSTEXPR_)
+    #undef JSON_HEDLEY_IS_CONSTEXPR_
+#endif
+#if \
+    JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \
+    JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \
+    JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+    JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \
+    JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \
+    JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \
+    JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \
+    (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \
+    JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0)
+    #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr)
+#endif
+#if !defined(__cplusplus)
+#  if \
+       JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \
+       JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \
+       JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+       JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \
+       JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \
+       JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \
+       JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24)
+#if defined(__INTPTR_TYPE__)
+    #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*)
+#else
+    #include <stdint.h>
+    #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*)
+#endif
+#  elif \
+       ( \
+          defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \
+          !defined(JSON_HEDLEY_SUNPRO_VERSION) && \
+          !defined(JSON_HEDLEY_PGI_VERSION) && \
+          !defined(JSON_HEDLEY_IAR_VERSION)) || \
+       JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) || \
+       JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \
+       JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \
+       JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \
+       JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0)
+#if defined(__INTPTR_TYPE__)
+    #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0)
+#else
+    #include <stdint.h>
+    #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0)
+#endif
+#  elif \
+       defined(JSON_HEDLEY_GCC_VERSION) || \
+       defined(JSON_HEDLEY_INTEL_VERSION) || \
+       defined(JSON_HEDLEY_TINYC_VERSION) || \
+       defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \
+       JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \
+       defined(JSON_HEDLEY_TI_CL2000_VERSION) || \
+       defined(JSON_HEDLEY_TI_CL6X_VERSION) || \
+       defined(JSON_HEDLEY_TI_CL7X_VERSION) || \
+       defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \
+       defined(__clang__)
+#    define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \
+        sizeof(void) != \
+        sizeof(*( \
+                  1 ? \
+                  ((void*) ((expr) * 0L) ) : \
+((struct { char v[sizeof(void) * 2]; } *) 1) \
+                ) \
+              ) \
+                                            )
+#  endif
+#endif
+#if defined(JSON_HEDLEY_IS_CONSTEXPR_)
+    #if !defined(JSON_HEDLEY_IS_CONSTANT)
+        #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr)
+    #endif
+    #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1))
+#else
+    #if !defined(JSON_HEDLEY_IS_CONSTANT)
+        #define JSON_HEDLEY_IS_CONSTANT(expr) (0)
+    #endif
+    #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr)
+#endif
+
+#if defined(JSON_HEDLEY_BEGIN_C_DECLS)
+    #undef JSON_HEDLEY_BEGIN_C_DECLS
+#endif
+#if defined(JSON_HEDLEY_END_C_DECLS)
+    #undef JSON_HEDLEY_END_C_DECLS
+#endif
+#if defined(JSON_HEDLEY_C_DECL)
+    #undef JSON_HEDLEY_C_DECL
+#endif
+#if defined(__cplusplus)
+    #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" {
+    #define JSON_HEDLEY_END_C_DECLS }
+    #define JSON_HEDLEY_C_DECL extern "C"
+#else
+    #define JSON_HEDLEY_BEGIN_C_DECLS
+    #define JSON_HEDLEY_END_C_DECLS
+    #define JSON_HEDLEY_C_DECL
+#endif
+
+#if defined(JSON_HEDLEY_STATIC_ASSERT)
+    #undef JSON_HEDLEY_STATIC_ASSERT
+#endif
+#if \
+  !defined(__cplusplus) && ( \
+      (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \
+      JSON_HEDLEY_HAS_FEATURE(c_static_assert) || \
+      JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \
+      JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \
+      defined(_Static_assert) \
+    )
+#  define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message)
+#elif \
+  (defined(__cplusplus) && (__cplusplus >= 201103L)) || \
+  JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0)
+#  define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message))
+#else
+#  define JSON_HEDLEY_STATIC_ASSERT(expr, message)
+#endif
+
+#if defined(JSON_HEDLEY_NULL)
+    #undef JSON_HEDLEY_NULL
+#endif
+#if defined(__cplusplus)
+    #if __cplusplus >= 201103L
+        #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr)
+    #elif defined(NULL)
+        #define JSON_HEDLEY_NULL NULL
+    #else
+        #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0)
+    #endif
+#elif defined(NULL)
+    #define JSON_HEDLEY_NULL NULL
+#else
+    #define JSON_HEDLEY_NULL ((void*) 0)
+#endif
+
+#if defined(JSON_HEDLEY_MESSAGE)
+    #undef JSON_HEDLEY_MESSAGE
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas")
+#  define JSON_HEDLEY_MESSAGE(msg) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \
+    JSON_HEDLEY_PRAGMA(message msg) \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#elif \
+  JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+#  define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg)
+#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0)
+#  define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg)
+#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0)
+#  define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg))
+#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0)
+#  define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg))
+#else
+#  define JSON_HEDLEY_MESSAGE(msg)
+#endif
+
+#if defined(JSON_HEDLEY_WARNING)
+    #undef JSON_HEDLEY_WARNING
+#endif
+#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas")
+#  define JSON_HEDLEY_WARNING(msg) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \
+    JSON_HEDLEY_PRAGMA(clang warning msg) \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#elif \
+  JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \
+  JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \
+  JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0)
+#  define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg)
+#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0)
+#  define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg))
+#else
+#  define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg)
+#endif
+
+#if defined(JSON_HEDLEY_REQUIRE)
+    #undef JSON_HEDLEY_REQUIRE
+#endif
+#if defined(JSON_HEDLEY_REQUIRE_MSG)
+    #undef JSON_HEDLEY_REQUIRE_MSG
+#endif
+#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if)
+#  if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat")
+#    define JSON_HEDLEY_REQUIRE(expr) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \
+    __attribute__((diagnose_if(!(expr), #expr, "error"))) \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#    define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \
+    JSON_HEDLEY_DIAGNOSTIC_PUSH \
+    _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \
+    __attribute__((diagnose_if(!(expr), msg, "error"))) \
+    JSON_HEDLEY_DIAGNOSTIC_POP
+#  else
+#    define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error")))
+#    define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error")))
+#  endif
+#else
+#  define JSON_HEDLEY_REQUIRE(expr)
+#  define JSON_HEDLEY_REQUIRE_MSG(expr,msg)
+#endif
+
+#if defined(JSON_HEDLEY_FLAGS)
+    #undef JSON_HEDLEY_FLAGS
+#endif
+#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum)
+    #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__))
+#endif
+
+#if defined(JSON_HEDLEY_FLAGS_CAST)
+    #undef JSON_HEDLEY_FLAGS_CAST
+#endif
+#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0)
+#  define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \
+        JSON_HEDLEY_DIAGNOSTIC_PUSH \
+        _Pragma("warning(disable:188)") \
+        ((T) (expr)); \
+        JSON_HEDLEY_DIAGNOSTIC_POP \
+    }))
+#else
+#  define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr)
+#endif
+
+#if defined(JSON_HEDLEY_EMPTY_BASES)
+    #undef JSON_HEDLEY_EMPTY_BASES
+#endif
+#if JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)
+    #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases)
+#else
+    #define JSON_HEDLEY_EMPTY_BASES
+#endif
+
+/* Remaining macros are deprecated. */
+
+#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK)
+    #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK
+#endif
+#if defined(__clang__)
+    #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0)
+#else
+    #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch)
+#endif
+
+#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE)
+    #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE
+#endif
+#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE)
+    #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE
+#endif
+#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN)
+    #undef JSON_HEDLEY_CLANG_HAS_BUILTIN
+#endif
+#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE)
+    #undef JSON_HEDLEY_CLANG_HAS_FEATURE
+#endif
+#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION)
+    #undef JSON_HEDLEY_CLANG_HAS_EXTENSION
+#endif
+#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE)
+    #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE
+#endif
+#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute)
+
+#if defined(JSON_HEDLEY_CLANG_HAS_WARNING)
+    #undef JSON_HEDLEY_CLANG_HAS_WARNING
+#endif
+#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning)
+
+#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */
+
+
+// This file contains all internal macro definitions
+// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them
+
+// exclude unsupported compilers
+#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK)
+    #if defined(__clang__)
+        #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400
+            #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers"
+        #endif
+    #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER))
+        #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800
+            #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers"
+        #endif
+    #endif
+#endif
+
+// C++ language standard detection
+#if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
+    #define JSON_HAS_CPP_20
+    #define JSON_HAS_CPP_17
+    #define JSON_HAS_CPP_14
+#elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464
+    #define JSON_HAS_CPP_17
+    #define JSON_HAS_CPP_14
+#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1)
+    #define JSON_HAS_CPP_14
+#endif
+
+// disable float-equal warnings on GCC/clang
+#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
+    #pragma GCC diagnostic push
+    #pragma GCC diagnostic ignored "-Wfloat-equal"
+#endif
+
+// disable documentation warnings on clang
+#if defined(__clang__)
+    #pragma GCC diagnostic push
+    #pragma GCC diagnostic ignored "-Wdocumentation"
+#endif
+
+// allow to disable exceptions
+#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
+    #define JSON_THROW(exception) throw exception
+    #define JSON_TRY try
+    #define JSON_CATCH(exception) catch(exception)
+    #define JSON_INTERNAL_CATCH(exception) catch(exception)
+#else
+    #include <cstdlib>
+    #define JSON_THROW(exception) std::abort()
+    #define JSON_TRY if(true)
+    #define JSON_CATCH(exception) if(false)
+    #define JSON_INTERNAL_CATCH(exception) if(false)
+#endif
+
+// override exception macros
+#if defined(JSON_THROW_USER)
+    #undef JSON_THROW
+    #define JSON_THROW JSON_THROW_USER
+#endif
+#if defined(JSON_TRY_USER)
+    #undef JSON_TRY
+    #define JSON_TRY JSON_TRY_USER
+#endif
+#if defined(JSON_CATCH_USER)
+    #undef JSON_CATCH
+    #define JSON_CATCH JSON_CATCH_USER
+    #undef JSON_INTERNAL_CATCH
+    #define JSON_INTERNAL_CATCH JSON_CATCH_USER
+#endif
+#if defined(JSON_INTERNAL_CATCH_USER)
+    #undef JSON_INTERNAL_CATCH
+    #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER
+#endif
+
+// allow to override assert
+#if !defined(JSON_ASSERT)
+    #include <cassert> // assert
+    #define JSON_ASSERT(x) assert(x)
+#endif
+
+/*!
+@brief macro to briefly define a mapping between an enum and JSON
+@def NLOHMANN_JSON_SERIALIZE_ENUM
+@since version 3.4.0
+*/
+#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...)                                            \
+    template<typename BasicJsonType>                                                            \
+    inline void to_json(BasicJsonType& j, const ENUM_TYPE& e)                                   \
+    {                                                                                           \
+        static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!");          \
+        static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__;                     \
+        auto it = std::find_if(std::begin(m), std::end(m),                                      \
+                               [e](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool  \
+        {                                                                                       \
+            return ej_pair.first == e;                                                          \
+        });                                                                                     \
+        j = ((it != std::end(m)) ? it : std::begin(m))->second;                                 \
+    }                                                                                           \
+    template<typename BasicJsonType>                                                            \
+    inline void from_json(const BasicJsonType& j, ENUM_TYPE& e)                                 \
+    {                                                                                           \
+        static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!");          \
+        static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__;                     \
+        auto it = std::find_if(std::begin(m), std::end(m),                                      \
+                               [&j](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
+        {                                                                                       \
+            return ej_pair.second == j;                                                         \
+        });                                                                                     \
+        e = ((it != std::end(m)) ? it : std::begin(m))->first;                                  \
+    }
+
+// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
+// may be removed in the future once the class is split.
+
+#define NLOHMANN_BASIC_JSON_TPL_DECLARATION                                \
+    template<template<typename, typename, typename...> class ObjectType,   \
+             template<typename, typename...> class ArrayType,              \
+             class StringType, class BooleanType, class NumberIntegerType, \
+             class NumberUnsignedType, class NumberFloatType,              \
+             template<typename> class AllocatorType,                       \
+             template<typename, typename = void> class JSONSerializer,     \
+             class BinaryType>
+
+#define NLOHMANN_BASIC_JSON_TPL                                            \
+    basic_json<ObjectType, ArrayType, StringType, BooleanType,             \
+    NumberIntegerType, NumberUnsignedType, NumberFloatType,                \
+    AllocatorType, JSONSerializer, BinaryType>
+
+// Macros to simplify conversion from/to types
+
+#define NLOHMANN_JSON_EXPAND( x ) x
+#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME
+#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \
+        NLOHMANN_JSON_PASTE64, \
+        NLOHMANN_JSON_PASTE63, \
+        NLOHMANN_JSON_PASTE62, \
+        NLOHMANN_JSON_PASTE61, \
+        NLOHMANN_JSON_PASTE60, \
+        NLOHMANN_JSON_PASTE59, \
+        NLOHMANN_JSON_PASTE58, \
+        NLOHMANN_JSON_PASTE57, \
+        NLOHMANN_JSON_PASTE56, \
+        NLOHMANN_JSON_PASTE55, \
+        NLOHMANN_JSON_PASTE54, \
+        NLOHMANN_JSON_PASTE53, \
+        NLOHMANN_JSON_PASTE52, \
+        NLOHMANN_JSON_PASTE51, \
+        NLOHMANN_JSON_PASTE50, \
+        NLOHMANN_JSON_PASTE49, \
+        NLOHMANN_JSON_PASTE48, \
+        NLOHMANN_JSON_PASTE47, \
+        NLOHMANN_JSON_PASTE46, \
+        NLOHMANN_JSON_PASTE45, \
+        NLOHMANN_JSON_PASTE44, \
+        NLOHMANN_JSON_PASTE43, \
+        NLOHMANN_JSON_PASTE42, \
+        NLOHMANN_JSON_PASTE41, \
+        NLOHMANN_JSON_PASTE40, \
+        NLOHMANN_JSON_PASTE39, \
+        NLOHMANN_JSON_PASTE38, \
+        NLOHMANN_JSON_PASTE37, \
+        NLOHMANN_JSON_PASTE36, \
+        NLOHMANN_JSON_PASTE35, \
+        NLOHMANN_JSON_PASTE34, \
+        NLOHMANN_JSON_PASTE33, \
+        NLOHMANN_JSON_PASTE32, \
+        NLOHMANN_JSON_PASTE31, \
+        NLOHMANN_JSON_PASTE30, \
+        NLOHMANN_JSON_PASTE29, \
+        NLOHMANN_JSON_PASTE28, \
+        NLOHMANN_JSON_PASTE27, \
+        NLOHMANN_JSON_PASTE26, \
+        NLOHMANN_JSON_PASTE25, \
+        NLOHMANN_JSON_PASTE24, \
+        NLOHMANN_JSON_PASTE23, \
+        NLOHMANN_JSON_PASTE22, \
+        NLOHMANN_JSON_PASTE21, \
+        NLOHMANN_JSON_PASTE20, \
+        NLOHMANN_JSON_PASTE19, \
+        NLOHMANN_JSON_PASTE18, \
+        NLOHMANN_JSON_PASTE17, \
+        NLOHMANN_JSON_PASTE16, \
+        NLOHMANN_JSON_PASTE15, \
+        NLOHMANN_JSON_PASTE14, \
+        NLOHMANN_JSON_PASTE13, \
+        NLOHMANN_JSON_PASTE12, \
+        NLOHMANN_JSON_PASTE11, \
+        NLOHMANN_JSON_PASTE10, \
+        NLOHMANN_JSON_PASTE9, \
+        NLOHMANN_JSON_PASTE8, \
+        NLOHMANN_JSON_PASTE7, \
+        NLOHMANN_JSON_PASTE6, \
+        NLOHMANN_JSON_PASTE5, \
+        NLOHMANN_JSON_PASTE4, \
+        NLOHMANN_JSON_PASTE3, \
+        NLOHMANN_JSON_PASTE2, \
+        NLOHMANN_JSON_PASTE1)(__VA_ARGS__))
+#define NLOHMANN_JSON_PASTE2(func, v1) func(v1)
+#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2)
+#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3)
+#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4)
+#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5)
+#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6)
+#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7)
+#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8)
+#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9)
+#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10)
+#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11)
+#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12)
+#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13)
+#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14)
+#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15)
+#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16)
+#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17)
+#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18)
+#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19)
+#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20)
+#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21)
+#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22)
+#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23)
+#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24)
+#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25)
+#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26)
+#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27)
+#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28)
+#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29)
+#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30)
+#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31)
+#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32)
+#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33)
+#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34)
+#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35)
+#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36)
+#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37)
+#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38)
+#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39)
+#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40)
+#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41)
+#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42)
+#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43)
+#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44)
+#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45)
+#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46)
+#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47)
+#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48)
+#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49)
+#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50)
+#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51)
+#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52)
+#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53)
+#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54)
+#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55)
+#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56)
+#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57)
+#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58)
+#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59)
+#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60)
+#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61)
+#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62)
+#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63)
+
+#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1;
+#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1);
+
+/*!
+@brief macro
+@def NLOHMANN_DEFINE_TYPE_INTRUSIVE
+@since version 3.9.0
+*/
+#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...)  \
+    friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \
+    friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }
+
+/*!
+@brief macro
+@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
+@since version 3.9.0
+*/
+#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...)  \
+    inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \
+    inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }
+
+#ifndef JSON_USE_IMPLICIT_CONVERSIONS
+    #define JSON_USE_IMPLICIT_CONVERSIONS 1
+#endif
+
+#if JSON_USE_IMPLICIT_CONVERSIONS
+    #define JSON_EXPLICIT
+#else
+    #define JSON_EXPLICIT explicit
+#endif
+
+
+namespace nlohmann
+{
+namespace detail
+{
+////////////////
+// exceptions //
+////////////////
+
+/*!
+@brief general exception of the @ref basic_json class
+
+This class is an extension of `std::exception` objects with a member @a id for
+exception ids. It is used as the base class for all exceptions thrown by the
+@ref basic_json class. This class can hence be used as "wildcard" to catch
+exceptions.
+
+Subclasses:
+- @ref parse_error for exceptions indicating a parse error
+- @ref invalid_iterator for exceptions indicating errors with iterators
+- @ref type_error for exceptions indicating executing a member function with
+                  a wrong type
+- @ref out_of_range for exceptions indicating access out of the defined range
+- @ref other_error for exceptions indicating other library errors
+
+@internal
+@note To have nothrow-copy-constructible exceptions, we internally use
+      `std::runtime_error` which can cope with arbitrary-length error messages.
+      Intermediate strings are built with static functions and then passed to
+      the actual constructor.
+@endinternal
+
+@liveexample{The following code shows how arbitrary library exceptions can be
+caught.,exception}
+
+@since version 3.0.0
+*/
+class exception : public std::exception
+{
+  public:
+    /// returns the explanatory string
+    JSON_HEDLEY_RETURNS_NON_NULL
+    const char* what() const noexcept override
+    {
+        return m.what();
+    }
+
+    /// the id of the exception
+    const int id;
+
+  protected:
+    JSON_HEDLEY_NON_NULL(3)
+    exception(int id_, const char* what_arg) : id(id_), m(what_arg) {}
+
+    static std::string name(const std::string& ename, int id_)
+    {
+        return "[json.exception." + ename + "." + std::to_string(id_) + "] ";
+    }
+
+  private:
+    /// an exception object as storage for error messages
+    std::runtime_error m;
+};
+
+/*!
+@brief exception indicating a parse error
+
+This exception is thrown by the library when a parse error occurs. Parse errors
+can occur during the deserialization of JSON text, CBOR, MessagePack, as well
+as when using JSON Patch.
+
+Member @a byte holds the byte index of the last read character in the input
+file.
+
+Exceptions have ids 1xx.
+
+name / id                      | example message | description
+------------------------------ | --------------- | -------------------------
+json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position.
+json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point.
+json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid.
+json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects.
+json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors.
+json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`.
+json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character.
+json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences.
+json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number.
+json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read.
+json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read.
+json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read.
+json.exception.parse_error.114 | parse error: Unsupported BSON record type 0x0F | The parsing of the corresponding BSON record type is not implemented (yet).
+json.exception.parse_error.115 | parse error at byte 5: syntax error while parsing UBJSON high-precision number: invalid number text: 1A | A UBJSON high-precision number could not be parsed.
+
+@note For an input with n bytes, 1 is the index of the first character and n+1
+      is the index of the terminating null byte or the end of file. This also
+      holds true when reading a byte vector (CBOR or MessagePack).
+
+@liveexample{The following code shows how a `parse_error` exception can be
+caught.,parse_error}
+
+@sa - @ref exception for the base class of the library exceptions
+@sa - @ref invalid_iterator for exceptions indicating errors with iterators
+@sa - @ref type_error for exceptions indicating executing a member function with
+                    a wrong type
+@sa - @ref out_of_range for exceptions indicating access out of the defined range
+@sa - @ref other_error for exceptions indicating other library errors
+
+@since version 3.0.0
+*/
+class parse_error : public exception
+{
+  public:
+    /*!
+    @brief create a parse error exception
+    @param[in] id_       the id of the exception
+    @param[in] pos       the position where the error occurred (or with
+                         chars_read_total=0 if the position cannot be
+                         determined)
+    @param[in] what_arg  the explanatory string
+    @return parse_error object
+    */
+    static parse_error create(int id_, const position_t& pos, const std::string& what_arg)
+    {
+        std::string w = exception::name("parse_error", id_) + "parse error" +
+                        position_string(pos) + ": " + what_arg;
+        return parse_error(id_, pos.chars_read_total, w.c_str());
+    }
+
+    static parse_error create(int id_, std::size_t byte_, const std::string& what_arg)
+    {
+        std::string w = exception::name("parse_error", id_) + "parse error" +
+                        (byte_ != 0 ? (" at byte " + std::to_string(byte_)) : "") +
+                        ": " + what_arg;
+        return parse_error(id_, byte_, w.c_str());
+    }
+
+    /*!
+    @brief byte index of the parse error
+
+    The byte index of the last read character in the input file.
+
+    @note For an input with n bytes, 1 is the index of the first character and
+          n+1 is the index of the terminating null byte or the end of file.
+          This also holds true when reading a byte vector (CBOR or MessagePack).
+    */
+    const std::size_t byte;
+
+  private:
+    parse_error(int id_, std::size_t byte_, const char* what_arg)
+        : exception(id_, what_arg), byte(byte_) {}
+
+    static std::string position_string(const position_t& pos)
+    {
+        return " at line " + std::to_string(pos.lines_read + 1) +
+               ", column " + std::to_string(pos.chars_read_current_line);
+    }
+};
+
+/*!
+@brief exception indicating errors with iterators
+
+This exception is thrown if iterators passed to a library function do not match
+the expected semantics.
+
+Exceptions have ids 2xx.
+
+name / id                           | example message | description
+----------------------------------- | --------------- | -------------------------
+json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid.
+json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion.
+json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from.
+json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid.
+json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid.
+json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range.
+json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key.
+json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered.
+json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered.
+json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid.
+json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to.
+json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container.
+json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered.
+json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin().
+
+@liveexample{The following code shows how an `invalid_iterator` exception can be
+caught.,invalid_iterator}
+
+@sa - @ref exception for the base class of the library exceptions
+@sa - @ref parse_error for exceptions indicating a parse error
+@sa - @ref type_error for exceptions indicating executing a member function with
+                    a wrong type
+@sa - @ref out_of_range for exceptions indicating access out of the defined range
+@sa - @ref other_error for exceptions indicating other library errors
+
+@since version 3.0.0
+*/
+class invalid_iterator : public exception
+{
+  public:
+    static invalid_iterator create(int id_, const std::string& what_arg)
+    {
+        std::string w = exception::name("invalid_iterator", id_) + what_arg;
+        return invalid_iterator(id_, w.c_str());
+    }
+
+  private:
+    JSON_HEDLEY_NON_NULL(3)
+    invalid_iterator(int id_, const char* what_arg)
+        : exception(id_, what_arg) {}
+};
+
+/*!
+@brief exception indicating executing a member function with a wrong type
+
+This exception is thrown in case of a type error; that is, a library function is
+executed on a JSON value whose type does not match the expected semantics.
+
+Exceptions have ids 3xx.
+
+name / id                     | example message | description
+----------------------------- | --------------- | -------------------------
+json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead.
+json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types.
+json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t &.
+json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types.
+json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types.
+json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types.
+json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types.
+json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types.
+json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types.
+json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types.
+json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types.
+json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types.
+json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined.
+json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers.
+json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive.
+json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. |
+json.exception.type_error.317 | JSON value cannot be serialized to requested format | The dynamic type of the object cannot be represented in the requested serialization format (e.g. a raw `true` or `null` JSON object cannot be serialized to BSON) |
+
+@liveexample{The following code shows how a `type_error` exception can be
+caught.,type_error}
+
+@sa - @ref exception for the base class of the library exceptions
+@sa - @ref parse_error for exceptions indicating a parse error
+@sa - @ref invalid_iterator for exceptions indicating errors with iterators
+@sa - @ref out_of_range for exceptions indicating access out of the defined range
+@sa - @ref other_error for exceptions indicating other library errors
+
+@since version 3.0.0
+*/
+class type_error : public exception
+{
+  public:
+    static type_error create(int id_, const std::string& what_arg)
+    {
+        std::string w = exception::name("type_error", id_) + what_arg;
+        return type_error(id_, w.c_str());
+    }
+
+  private:
+    JSON_HEDLEY_NON_NULL(3)
+    type_error(int id_, const char* what_arg) : exception(id_, what_arg) {}
+};
+
+/*!
+@brief exception indicating access out of the defined range
+
+This exception is thrown in case a library function is called on an input
+parameter that exceeds the expected range, for instance in case of array
+indices or nonexisting object keys.
+
+Exceptions have ids 4xx.
+
+name / id                       | example message | description
+------------------------------- | --------------- | -------------------------
+json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1.
+json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it.
+json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object.
+json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved.
+json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value.
+json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF.
+json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON and BSON only support integer numbers up to 9223372036854775807. (until version 3.8.0) |
+json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. |
+json.exception.out_of_range.409 | BSON key cannot contain code point U+0000 (at byte 2) | Key identifiers to be serialized to BSON cannot contain code point U+0000, since the key is stored as zero-terminated c-string |
+
+@liveexample{The following code shows how an `out_of_range` exception can be
+caught.,out_of_range}
+
+@sa - @ref exception for the base class of the library exceptions
+@sa - @ref parse_error for exceptions indicating a parse error
+@sa - @ref invalid_iterator for exceptions indicating errors with iterators
+@sa - @ref type_error for exceptions indicating executing a member function with
+                    a wrong type
+@sa - @ref other_error for exceptions indicating other library errors
+
+@since version 3.0.0
+*/
+class out_of_range : public exception
+{
+  public:
+    static out_of_range create(int id_, const std::string& what_arg)
+    {
+        std::string w = exception::name("out_of_range", id_) + what_arg;
+        return out_of_range(id_, w.c_str());
+    }
+
+  private:
+    JSON_HEDLEY_NON_NULL(3)
+    out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {}
+};
+
+/*!
+@brief exception indicating other library errors
+
+This exception is thrown in case of errors that cannot be classified with the
+other exception types.
+
+Exceptions have ids 5xx.
+
+name / id                      | example message | description
+------------------------------ | --------------- | -------------------------
+json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed.
+
+@sa - @ref exception for the base class of the library exceptions
+@sa - @ref parse_error for exceptions indicating a parse error
+@sa - @ref invalid_iterator for exceptions indicating errors with iterators
+@sa - @ref type_error for exceptions indicating executing a member function with
+                    a wrong type
+@sa - @ref out_of_range for exceptions indicating access out of the defined range
+
+@liveexample{The following code shows how an `other_error` exception can be
+caught.,other_error}
+
+@since version 3.0.0
+*/
+class other_error : public exception
+{
+  public:
+    static other_error create(int id_, const std::string& what_arg)
+    {
+        std::string w = exception::name("other_error", id_) + what_arg;
+        return other_error(id_, w.c_str());
+    }
+
+  private:
+    JSON_HEDLEY_NON_NULL(3)
+    other_error(int id_, const char* what_arg) : exception(id_, what_arg) {}
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+
+#include <cstddef> // size_t
+#include <type_traits> // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type
+
+namespace nlohmann
+{
+namespace detail
+{
+// alias templates to reduce boilerplate
+template<bool B, typename T = void>
+using enable_if_t = typename std::enable_if<B, T>::type;
+
+template<typename T>
+using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
+
+// implementation of C++14 index_sequence and affiliates
+// source: https://stackoverflow.com/a/32223343
+template<std::size_t... Ints>
+struct index_sequence
+{
+    using type = index_sequence;
+    using value_type = std::size_t;
+    static constexpr std::size_t size() noexcept
+    {
+        return sizeof...(Ints);
+    }
+};
+
+template<class Sequence1, class Sequence2>
+struct merge_and_renumber;
+
+template<std::size_t... I1, std::size_t... I2>
+struct merge_and_renumber<index_sequence<I1...>, index_sequence<I2...>>
+        : index_sequence < I1..., (sizeof...(I1) + I2)... > {};
+
+template<std::size_t N>
+struct make_index_sequence
+    : merge_and_renumber < typename make_index_sequence < N / 2 >::type,
+      typename make_index_sequence < N - N / 2 >::type > {};
+
+template<> struct make_index_sequence<0> : index_sequence<> {};
+template<> struct make_index_sequence<1> : index_sequence<0> {};
+
+template<typename... Ts>
+using index_sequence_for = make_index_sequence<sizeof...(Ts)>;
+
+// dispatch utility (taken from ranges-v3)
+template<unsigned N> struct priority_tag : priority_tag < N - 1 > {};
+template<> struct priority_tag<0> {};
+
+// taken from ranges-v3
+template<typename T>
+struct static_const
+{
+    static constexpr T value{};
+};
+
+template<typename T>
+constexpr T static_const<T>::value;
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+
+#include <limits> // numeric_limits
+#include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type
+#include <utility> // declval
+
+// #include <nlohmann/detail/iterators/iterator_traits.hpp>
+
+
+#include <iterator> // random_access_iterator_tag
+
+// #include <nlohmann/detail/meta/void_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename ...Ts> struct make_void
+{
+    using type = void;
+};
+template<typename ...Ts> using void_t = typename make_void<Ts...>::type;
+} // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename It, typename = void>
+struct iterator_types {};
+
+template<typename It>
+struct iterator_types <
+    It,
+    void_t<typename It::difference_type, typename It::value_type, typename It::pointer,
+    typename It::reference, typename It::iterator_category >>
+{
+    using difference_type = typename It::difference_type;
+    using value_type = typename It::value_type;
+    using pointer = typename It::pointer;
+    using reference = typename It::reference;
+    using iterator_category = typename It::iterator_category;
+};
+
+// This is required as some compilers implement std::iterator_traits in a way that
+// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341.
+template<typename T, typename = void>
+struct iterator_traits
+{
+};
+
+template<typename T>
+struct iterator_traits < T, enable_if_t < !std::is_pointer<T>::value >>
+            : iterator_types<T>
+{
+};
+
+template<typename T>
+struct iterator_traits<T*, enable_if_t<std::is_object<T>::value>>
+{
+    using iterator_category = std::random_access_iterator_tag;
+    using value_type = T;
+    using difference_type = ptrdiff_t;
+    using pointer = T*;
+    using reference = T&;
+};
+} // namespace detail
+} // namespace nlohmann
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+// #include <nlohmann/detail/meta/detected.hpp>
+
+
+#include <type_traits>
+
+// #include <nlohmann/detail/meta/void_t.hpp>
+
+
+// https://en.cppreference.com/w/cpp/experimental/is_detected
+namespace nlohmann
+{
+namespace detail
+{
+struct nonesuch
+{
+    nonesuch() = delete;
+    ~nonesuch() = delete;
+    nonesuch(nonesuch const&) = delete;
+    nonesuch(nonesuch const&&) = delete;
+    void operator=(nonesuch const&) = delete;
+    void operator=(nonesuch&&) = delete;
+};
+
+template<class Default,
+         class AlwaysVoid,
+         template<class...> class Op,
+         class... Args>
+struct detector
+{
+    using value_t = std::false_type;
+    using type = Default;
+};
+
+template<class Default, template<class...> class Op, class... Args>
+struct detector<Default, void_t<Op<Args...>>, Op, Args...>
+{
+    using value_t = std::true_type;
+    using type = Op<Args...>;
+};
+
+template<template<class...> class Op, class... Args>
+using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
+
+template<template<class...> class Op, class... Args>
+using detected_t = typename detector<nonesuch, void, Op, Args...>::type;
+
+template<class Default, template<class...> class Op, class... Args>
+using detected_or = detector<Default, void, Op, Args...>;
+
+template<class Default, template<class...> class Op, class... Args>
+using detected_or_t = typename detected_or<Default, Op, Args...>::type;
+
+template<class Expected, template<class...> class Op, class... Args>
+using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;
+
+template<class To, template<class...> class Op, class... Args>
+using is_detected_convertible =
+    std::is_convertible<detected_t<Op, Args...>, To>;
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/json_fwd.hpp>
+#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
+#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
+
+#include <cstdint> // int64_t, uint64_t
+#include <map> // map
+#include <memory> // allocator
+#include <string> // string
+#include <vector> // vector
+
+/*!
+@brief namespace for Niels Lohmann
+@see https://github.com/nlohmann
+@since version 1.0.0
+*/
+namespace nlohmann
+{
+/*!
+@brief default JSONSerializer template argument
+
+This serializer ignores the template arguments and uses ADL
+([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
+for serialization.
+*/
+template<typename T = void, typename SFINAE = void>
+struct adl_serializer;
+
+template<template<typename U, typename V, typename... Args> class ObjectType =
+         std::map,
+         template<typename U, typename... Args> class ArrayType = std::vector,
+         class StringType = std::string, class BooleanType = bool,
+         class NumberIntegerType = std::int64_t,
+         class NumberUnsignedType = std::uint64_t,
+         class NumberFloatType = double,
+         template<typename U> class AllocatorType = std::allocator,
+         template<typename T, typename SFINAE = void> class JSONSerializer =
+         adl_serializer,
+         class BinaryType = std::vector<std::uint8_t>>
+class basic_json;
+
+/*!
+@brief JSON Pointer
+
+A JSON pointer defines a string syntax for identifying a specific value
+within a JSON document. It can be used with functions `at` and
+`operator[]`. Furthermore, JSON pointers are the base for JSON patches.
+
+@sa [RFC 6901](https://tools.ietf.org/html/rfc6901)
+
+@since version 2.0.0
+*/
+template<typename BasicJsonType>
+class json_pointer;
+
+/*!
+@brief default JSON class
+
+This type is the default specialization of the @ref basic_json class which
+uses the standard template types.
+
+@since version 1.0.0
+*/
+using json = basic_json<>;
+
+template<class Key, class T, class IgnoredLess, class Allocator>
+struct ordered_map;
+
+/*!
+@brief ordered JSON class
+
+This type preserves the insertion order of object keys.
+
+@since version 3.9.0
+*/
+using ordered_json = basic_json<nlohmann::ordered_map>;
+
+}  // namespace nlohmann
+
+#endif  // INCLUDE_NLOHMANN_JSON_FWD_HPP_
+
+
+namespace nlohmann
+{
+/*!
+@brief detail namespace with internal helper functions
+
+This namespace collects functions that should not be exposed,
+implementations of some @ref basic_json methods, and meta-programming helpers.
+
+@since version 2.1.0
+*/
+namespace detail
+{
+/////////////
+// helpers //
+/////////////
+
+// Note to maintainers:
+//
+// Every trait in this file expects a non CV-qualified type.
+// The only exceptions are in the 'aliases for detected' section
+// (i.e. those of the form: decltype(T::member_function(std::declval<T>())))
+//
+// In this case, T has to be properly CV-qualified to constraint the function arguments
+// (e.g. to_json(BasicJsonType&, const T&))
+
+template<typename> struct is_basic_json : std::false_type {};
+
+NLOHMANN_BASIC_JSON_TPL_DECLARATION
+struct is_basic_json<NLOHMANN_BASIC_JSON_TPL> : std::true_type {};
+
+//////////////////////
+// json_ref helpers //
+//////////////////////
+
+template<typename>
+class json_ref;
+
+template<typename>
+struct is_json_ref : std::false_type {};
+
+template<typename T>
+struct is_json_ref<json_ref<T>> : std::true_type {};
+
+//////////////////////////
+// aliases for detected //
+//////////////////////////
+
+template<typename T>
+using mapped_type_t = typename T::mapped_type;
+
+template<typename T>
+using key_type_t = typename T::key_type;
+
+template<typename T>
+using value_type_t = typename T::value_type;
+
+template<typename T>
+using difference_type_t = typename T::difference_type;
+
+template<typename T>
+using pointer_t = typename T::pointer;
+
+template<typename T>
+using reference_t = typename T::reference;
+
+template<typename T>
+using iterator_category_t = typename T::iterator_category;
+
+template<typename T>
+using iterator_t = typename T::iterator;
+
+template<typename T, typename... Args>
+using to_json_function = decltype(T::to_json(std::declval<Args>()...));
+
+template<typename T, typename... Args>
+using from_json_function = decltype(T::from_json(std::declval<Args>()...));
+
+template<typename T, typename U>
+using get_template_function = decltype(std::declval<T>().template get<U>());
+
+// trait checking if JSONSerializer<T>::from_json(json const&, udt&) exists
+template<typename BasicJsonType, typename T, typename = void>
+struct has_from_json : std::false_type {};
+
+// trait checking if j.get<T> is valid
+// use this trait instead of std::is_constructible or std::is_convertible,
+// both rely on, or make use of implicit conversions, and thus fail when T
+// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958)
+template <typename BasicJsonType, typename T>
+struct is_getable
+{
+    static constexpr bool value = is_detected<get_template_function, const BasicJsonType&, T>::value;
+};
+
+template<typename BasicJsonType, typename T>
+struct has_from_json < BasicJsonType, T,
+           enable_if_t < !is_basic_json<T>::value >>
+{
+    using serializer = typename BasicJsonType::template json_serializer<T, void>;
+
+    static constexpr bool value =
+        is_detected_exact<void, from_json_function, serializer,
+        const BasicJsonType&, T&>::value;
+};
+
+// This trait checks if JSONSerializer<T>::from_json(json const&) exists
+// this overload is used for non-default-constructible user-defined-types
+template<typename BasicJsonType, typename T, typename = void>
+struct has_non_default_from_json : std::false_type {};
+
+template<typename BasicJsonType, typename T>
+struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
+{
+    using serializer = typename BasicJsonType::template json_serializer<T, void>;
+
+    static constexpr bool value =
+        is_detected_exact<T, from_json_function, serializer,
+        const BasicJsonType&>::value;
+};
+
+// This trait checks if BasicJsonType::json_serializer<T>::to_json exists
+// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion.
+template<typename BasicJsonType, typename T, typename = void>
+struct has_to_json : std::false_type {};
+
+template<typename BasicJsonType, typename T>
+struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
+{
+    using serializer = typename BasicJsonType::template json_serializer<T, void>;
+
+    static constexpr bool value =
+        is_detected_exact<void, to_json_function, serializer, BasicJsonType&,
+        T>::value;
+};
+
+
+///////////////////
+// is_ functions //
+///////////////////
+
+template<typename T, typename = void>
+struct is_iterator_traits : std::false_type {};
+
+template<typename T>
+struct is_iterator_traits<iterator_traits<T>>
+{
+  private:
+    using traits = iterator_traits<T>;
+
+  public:
+    static constexpr auto value =
+        is_detected<value_type_t, traits>::value &&
+        is_detected<difference_type_t, traits>::value &&
+        is_detected<pointer_t, traits>::value &&
+        is_detected<iterator_category_t, traits>::value &&
+        is_detected<reference_t, traits>::value;
+};
+
+// source: https://stackoverflow.com/a/37193089/4116453
+
+template<typename T, typename = void>
+struct is_complete_type : std::false_type {};
+
+template<typename T>
+struct is_complete_type<T, decltype(void(sizeof(T)))> : std::true_type {};
+
+template<typename BasicJsonType, typename CompatibleObjectType,
+         typename = void>
+struct is_compatible_object_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename CompatibleObjectType>
+struct is_compatible_object_type_impl <
+    BasicJsonType, CompatibleObjectType,
+    enable_if_t < is_detected<mapped_type_t, CompatibleObjectType>::value&&
+    is_detected<key_type_t, CompatibleObjectType>::value >>
+{
+
+    using object_t = typename BasicJsonType::object_t;
+
+    // macOS's is_constructible does not play well with nonesuch...
+    static constexpr bool value =
+        std::is_constructible<typename object_t::key_type,
+        typename CompatibleObjectType::key_type>::value &&
+        std::is_constructible<typename object_t::mapped_type,
+        typename CompatibleObjectType::mapped_type>::value;
+};
+
+template<typename BasicJsonType, typename CompatibleObjectType>
+struct is_compatible_object_type
+    : is_compatible_object_type_impl<BasicJsonType, CompatibleObjectType> {};
+
+template<typename BasicJsonType, typename ConstructibleObjectType,
+         typename = void>
+struct is_constructible_object_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename ConstructibleObjectType>
+struct is_constructible_object_type_impl <
+    BasicJsonType, ConstructibleObjectType,
+    enable_if_t < is_detected<mapped_type_t, ConstructibleObjectType>::value&&
+    is_detected<key_type_t, ConstructibleObjectType>::value >>
+{
+    using object_t = typename BasicJsonType::object_t;
+
+    static constexpr bool value =
+        (std::is_default_constructible<ConstructibleObjectType>::value &&
+         (std::is_move_assignable<ConstructibleObjectType>::value ||
+          std::is_copy_assignable<ConstructibleObjectType>::value) &&
+         (std::is_constructible<typename ConstructibleObjectType::key_type,
+          typename object_t::key_type>::value &&
+          std::is_same <
+          typename object_t::mapped_type,
+          typename ConstructibleObjectType::mapped_type >::value)) ||
+        (has_from_json<BasicJsonType,
+         typename ConstructibleObjectType::mapped_type>::value ||
+         has_non_default_from_json <
+         BasicJsonType,
+         typename ConstructibleObjectType::mapped_type >::value);
+};
+
+template<typename BasicJsonType, typename ConstructibleObjectType>
+struct is_constructible_object_type
+    : is_constructible_object_type_impl<BasicJsonType,
+      ConstructibleObjectType> {};
+
+template<typename BasicJsonType, typename CompatibleStringType,
+         typename = void>
+struct is_compatible_string_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename CompatibleStringType>
+struct is_compatible_string_type_impl <
+    BasicJsonType, CompatibleStringType,
+    enable_if_t<is_detected_exact<typename BasicJsonType::string_t::value_type,
+    value_type_t, CompatibleStringType>::value >>
+{
+    static constexpr auto value =
+        std::is_constructible<typename BasicJsonType::string_t, CompatibleStringType>::value;
+};
+
+template<typename BasicJsonType, typename ConstructibleStringType>
+struct is_compatible_string_type
+    : is_compatible_string_type_impl<BasicJsonType, ConstructibleStringType> {};
+
+template<typename BasicJsonType, typename ConstructibleStringType,
+         typename = void>
+struct is_constructible_string_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename ConstructibleStringType>
+struct is_constructible_string_type_impl <
+    BasicJsonType, ConstructibleStringType,
+    enable_if_t<is_detected_exact<typename BasicJsonType::string_t::value_type,
+    value_type_t, ConstructibleStringType>::value >>
+{
+    static constexpr auto value =
+        std::is_constructible<ConstructibleStringType,
+        typename BasicJsonType::string_t>::value;
+};
+
+template<typename BasicJsonType, typename ConstructibleStringType>
+struct is_constructible_string_type
+    : is_constructible_string_type_impl<BasicJsonType, ConstructibleStringType> {};
+
+template<typename BasicJsonType, typename CompatibleArrayType, typename = void>
+struct is_compatible_array_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename CompatibleArrayType>
+struct is_compatible_array_type_impl <
+    BasicJsonType, CompatibleArrayType,
+    enable_if_t < is_detected<value_type_t, CompatibleArrayType>::value&&
+    is_detected<iterator_t, CompatibleArrayType>::value&&
+// This is needed because json_reverse_iterator has a ::iterator type...
+// Therefore it is detected as a CompatibleArrayType.
+// The real fix would be to have an Iterable concept.
+    !is_iterator_traits <
+    iterator_traits<CompatibleArrayType >>::value >>
+{
+    static constexpr bool value =
+        std::is_constructible<BasicJsonType,
+        typename CompatibleArrayType::value_type>::value;
+};
+
+template<typename BasicJsonType, typename CompatibleArrayType>
+struct is_compatible_array_type
+    : is_compatible_array_type_impl<BasicJsonType, CompatibleArrayType> {};
+
+template<typename BasicJsonType, typename ConstructibleArrayType, typename = void>
+struct is_constructible_array_type_impl : std::false_type {};
+
+template<typename BasicJsonType, typename ConstructibleArrayType>
+struct is_constructible_array_type_impl <
+    BasicJsonType, ConstructibleArrayType,
+    enable_if_t<std::is_same<ConstructibleArrayType,
+    typename BasicJsonType::value_type>::value >>
+            : std::true_type {};
+
+template<typename BasicJsonType, typename ConstructibleArrayType>
+struct is_constructible_array_type_impl <
+    BasicJsonType, ConstructibleArrayType,
+    enable_if_t < !std::is_same<ConstructibleArrayType,
+    typename BasicJsonType::value_type>::value&&
+    std::is_default_constructible<ConstructibleArrayType>::value&&
+(std::is_move_assignable<ConstructibleArrayType>::value ||
+ std::is_copy_assignable<ConstructibleArrayType>::value)&&
+is_detected<value_type_t, ConstructibleArrayType>::value&&
+is_detected<iterator_t, ConstructibleArrayType>::value&&
+is_complete_type <
+detected_t<value_type_t, ConstructibleArrayType >>::value >>
+{
+    static constexpr bool value =
+        // This is needed because json_reverse_iterator has a ::iterator type,
+        // furthermore, std::back_insert_iterator (and other iterators) have a
+        // base class `iterator`... Therefore it is detected as a
+        // ConstructibleArrayType. The real fix would be to have an Iterable
+        // concept.
+        !is_iterator_traits<iterator_traits<ConstructibleArrayType>>::value &&
+
+        (std::is_same<typename ConstructibleArrayType::value_type,
+         typename BasicJsonType::array_t::value_type>::value ||
+         has_from_json<BasicJsonType,
+         typename ConstructibleArrayType::value_type>::value ||
+         has_non_default_from_json <
+         BasicJsonType, typename ConstructibleArrayType::value_type >::value);
+};
+
+template<typename BasicJsonType, typename ConstructibleArrayType>
+struct is_constructible_array_type
+    : is_constructible_array_type_impl<BasicJsonType, ConstructibleArrayType> {};
+
+template<typename RealIntegerType, typename CompatibleNumberIntegerType,
+         typename = void>
+struct is_compatible_integer_type_impl : std::false_type {};
+
+template<typename RealIntegerType, typename CompatibleNumberIntegerType>
+struct is_compatible_integer_type_impl <
+    RealIntegerType, CompatibleNumberIntegerType,
+    enable_if_t < std::is_integral<RealIntegerType>::value&&
+    std::is_integral<CompatibleNumberIntegerType>::value&&
+    !std::is_same<bool, CompatibleNumberIntegerType>::value >>
+{
+    // is there an assert somewhere on overflows?
+    using RealLimits = std::numeric_limits<RealIntegerType>;
+    using CompatibleLimits = std::numeric_limits<CompatibleNumberIntegerType>;
+
+    static constexpr auto value =
+        std::is_constructible<RealIntegerType,
+        CompatibleNumberIntegerType>::value &&
+        CompatibleLimits::is_integer &&
+        RealLimits::is_signed == CompatibleLimits::is_signed;
+};
+
+template<typename RealIntegerType, typename CompatibleNumberIntegerType>
+struct is_compatible_integer_type
+    : is_compatible_integer_type_impl<RealIntegerType,
+      CompatibleNumberIntegerType> {};
+
+template<typename BasicJsonType, typename CompatibleType, typename = void>
+struct is_compatible_type_impl: std::false_type {};
+
+template<typename BasicJsonType, typename CompatibleType>
+struct is_compatible_type_impl <
+    BasicJsonType, CompatibleType,
+    enable_if_t<is_complete_type<CompatibleType>::value >>
+{
+    static constexpr bool value =
+        has_to_json<BasicJsonType, CompatibleType>::value;
+};
+
+template<typename BasicJsonType, typename CompatibleType>
+struct is_compatible_type
+    : is_compatible_type_impl<BasicJsonType, CompatibleType> {};
+
+// https://en.cppreference.com/w/cpp/types/conjunction
+template<class...> struct conjunction : std::true_type { };
+template<class B1> struct conjunction<B1> : B1 { };
+template<class B1, class... Bn>
+struct conjunction<B1, Bn...>
+: std::conditional<bool(B1::value), conjunction<Bn...>, B1>::type {};
+
+template<typename T1, typename T2>
+struct is_constructible_tuple : std::false_type {};
+
+template<typename T1, typename... Args>
+struct is_constructible_tuple<T1, std::tuple<Args...>> : conjunction<std::is_constructible<T1, Args>...> {};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+#include <array> // array
+#include <cstddef> // size_t
+#include <cstdint> // uint8_t
+#include <string> // string
+
+namespace nlohmann
+{
+namespace detail
+{
+///////////////////////////
+// JSON type enumeration //
+///////////////////////////
+
+/*!
+@brief the JSON type enumeration
+
+This enumeration collects the different JSON types. It is internally used to
+distinguish the stored values, and the functions @ref basic_json::is_null(),
+@ref basic_json::is_object(), @ref basic_json::is_array(),
+@ref basic_json::is_string(), @ref basic_json::is_boolean(),
+@ref basic_json::is_number() (with @ref basic_json::is_number_integer(),
+@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()),
+@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and
+@ref basic_json::is_structured() rely on it.
+
+@note There are three enumeration entries (number_integer, number_unsigned, and
+number_float), because the library distinguishes these three types for numbers:
+@ref basic_json::number_unsigned_t is used for unsigned integers,
+@ref basic_json::number_integer_t is used for signed integers, and
+@ref basic_json::number_float_t is used for floating-point numbers or to
+approximate integers which do not fit in the limits of their respective type.
+
+@sa @ref basic_json::basic_json(const value_t value_type) -- create a JSON
+value with the default value for a given type
+
+@since version 1.0.0
+*/
+enum class value_t : std::uint8_t
+{
+    null,             ///< null value
+    object,           ///< object (unordered set of name/value pairs)
+    array,            ///< array (ordered collection of values)
+    string,           ///< string value
+    boolean,          ///< boolean value
+    number_integer,   ///< number value (signed integer)
+    number_unsigned,  ///< number value (unsigned integer)
+    number_float,     ///< number value (floating-point)
+    binary,           ///< binary array (ordered collection of bytes)
+    discarded         ///< discarded by the parser callback function
+};
+
+/*!
+@brief comparison operator for JSON types
+
+Returns an ordering that is similar to Python:
+- order: null < boolean < number < object < array < string < binary
+- furthermore, each type is not smaller than itself
+- discarded values are not comparable
+- binary is represented as a b"" string in python and directly comparable to a
+  string; however, making a binary array directly comparable with a string would
+  be surprising behavior in a JSON file.
+
+@since version 1.0.0
+*/
+inline bool operator<(const value_t lhs, const value_t rhs) noexcept
+{
+    static constexpr std::array<std::uint8_t, 9> order = {{
+            0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */,
+            1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */,
+            6 /* binary */
+        }
+    };
+
+    const auto l_index = static_cast<std::size_t>(lhs);
+    const auto r_index = static_cast<std::size_t>(rhs);
+    return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index];
+}
+}  // namespace detail
+}  // namespace nlohmann
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename std::nullptr_t& n)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_null()))
+    {
+        JSON_THROW(type_error::create(302, "type must be null, but is " + std::string(j.type_name())));
+    }
+    n = nullptr;
+}
+
+// overloads for basic_json template parameters
+template < typename BasicJsonType, typename ArithmeticType,
+           enable_if_t < std::is_arithmetic<ArithmeticType>::value&&
+                         !std::is_same<ArithmeticType, typename BasicJsonType::boolean_t>::value,
+                         int > = 0 >
+void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val)
+{
+    switch (static_cast<value_t>(j))
+    {
+        case value_t::number_unsigned:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>());
+            break;
+        }
+        case value_t::number_integer:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_integer_t*>());
+            break;
+        }
+        case value_t::number_float:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_float_t*>());
+            break;
+        }
+
+        default:
+            JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name())));
+    }
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_boolean()))
+    {
+        JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(j.type_name())));
+    }
+    b = *j.template get_ptr<const typename BasicJsonType::boolean_t*>();
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_string()))
+    {
+        JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name())));
+    }
+    s = *j.template get_ptr<const typename BasicJsonType::string_t*>();
+}
+
+template <
+    typename BasicJsonType, typename ConstructibleStringType,
+    enable_if_t <
+        is_constructible_string_type<BasicJsonType, ConstructibleStringType>::value&&
+        !std::is_same<typename BasicJsonType::string_t,
+                      ConstructibleStringType>::value,
+        int > = 0 >
+void from_json(const BasicJsonType& j, ConstructibleStringType& s)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_string()))
+    {
+        JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name())));
+    }
+
+    s = *j.template get_ptr<const typename BasicJsonType::string_t*>();
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val)
+{
+    get_arithmetic_value(j, val);
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val)
+{
+    get_arithmetic_value(j, val);
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val)
+{
+    get_arithmetic_value(j, val);
+}
+
+template<typename BasicJsonType, typename EnumType,
+         enable_if_t<std::is_enum<EnumType>::value, int> = 0>
+void from_json(const BasicJsonType& j, EnumType& e)
+{
+    typename std::underlying_type<EnumType>::type val;
+    get_arithmetic_value(j, val);
+    e = static_cast<EnumType>(val);
+}
+
+// forward_list doesn't have an insert method
+template<typename BasicJsonType, typename T, typename Allocator,
+         enable_if_t<is_getable<BasicJsonType, T>::value, int> = 0>
+void from_json(const BasicJsonType& j, std::forward_list<T, Allocator>& l)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
+    {
+        JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name())));
+    }
+    l.clear();
+    std::transform(j.rbegin(), j.rend(),
+                   std::front_inserter(l), [](const BasicJsonType & i)
+    {
+        return i.template get<T>();
+    });
+}
+
+// valarray doesn't have an insert method
+template<typename BasicJsonType, typename T,
+         enable_if_t<is_getable<BasicJsonType, T>::value, int> = 0>
+void from_json(const BasicJsonType& j, std::valarray<T>& l)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
+    {
+        JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name())));
+    }
+    l.resize(j.size());
+    std::transform(j.begin(), j.end(), std::begin(l),
+                   [](const BasicJsonType & elem)
+    {
+        return elem.template get<T>();
+    });
+}
+
+template<typename BasicJsonType, typename T, std::size_t N>
+auto from_json(const BasicJsonType& j, T (&arr)[N])
+-> decltype(j.template get<T>(), void())
+{
+    for (std::size_t i = 0; i < N; ++i)
+    {
+        arr[i] = j.at(i).template get<T>();
+    }
+}
+
+template<typename BasicJsonType>
+void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType::array_t& arr, priority_tag<3> /*unused*/)
+{
+    arr = *j.template get_ptr<const typename BasicJsonType::array_t*>();
+}
+
+template<typename BasicJsonType, typename T, std::size_t N>
+auto from_json_array_impl(const BasicJsonType& j, std::array<T, N>& arr,
+                          priority_tag<2> /*unused*/)
+-> decltype(j.template get<T>(), void())
+{
+    for (std::size_t i = 0; i < N; ++i)
+    {
+        arr[i] = j.at(i).template get<T>();
+    }
+}
+
+template<typename BasicJsonType, typename ConstructibleArrayType>
+auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, priority_tag<1> /*unused*/)
+-> decltype(
+    arr.reserve(std::declval<typename ConstructibleArrayType::size_type>()),
+    j.template get<typename ConstructibleArrayType::value_type>(),
+    void())
+{
+    using std::end;
+
+    ConstructibleArrayType ret;
+    ret.reserve(j.size());
+    std::transform(j.begin(), j.end(),
+                   std::inserter(ret, end(ret)), [](const BasicJsonType & i)
+    {
+        // get<BasicJsonType>() returns *this, this won't call a from_json
+        // method when value_type is BasicJsonType
+        return i.template get<typename ConstructibleArrayType::value_type>();
+    });
+    arr = std::move(ret);
+}
+
+template<typename BasicJsonType, typename ConstructibleArrayType>
+void from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr,
+                          priority_tag<0> /*unused*/)
+{
+    using std::end;
+
+    ConstructibleArrayType ret;
+    std::transform(
+        j.begin(), j.end(), std::inserter(ret, end(ret)),
+        [](const BasicJsonType & i)
+    {
+        // get<BasicJsonType>() returns *this, this won't call a from_json
+        // method when value_type is BasicJsonType
+        return i.template get<typename ConstructibleArrayType::value_type>();
+    });
+    arr = std::move(ret);
+}
+
+template < typename BasicJsonType, typename ConstructibleArrayType,
+           enable_if_t <
+               is_constructible_array_type<BasicJsonType, ConstructibleArrayType>::value&&
+               !is_constructible_object_type<BasicJsonType, ConstructibleArrayType>::value&&
+               !is_constructible_string_type<BasicJsonType, ConstructibleArrayType>::value&&
+               !std::is_same<ConstructibleArrayType, typename BasicJsonType::binary_t>::value&&
+               !is_basic_json<ConstructibleArrayType>::value,
+               int > = 0 >
+auto from_json(const BasicJsonType& j, ConstructibleArrayType& arr)
+-> decltype(from_json_array_impl(j, arr, priority_tag<3> {}),
+j.template get<typename ConstructibleArrayType::value_type>(),
+void())
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
+    {
+        JSON_THROW(type_error::create(302, "type must be array, but is " +
+                                      std::string(j.type_name())));
+    }
+
+    from_json_array_impl(j, arr, priority_tag<3> {});
+}
+
+template<typename BasicJsonType>
+void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& bin)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_binary()))
+    {
+        JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(j.type_name())));
+    }
+
+    bin = *j.template get_ptr<const typename BasicJsonType::binary_t*>();
+}
+
+template<typename BasicJsonType, typename ConstructibleObjectType,
+         enable_if_t<is_constructible_object_type<BasicJsonType, ConstructibleObjectType>::value, int> = 0>
+void from_json(const BasicJsonType& j, ConstructibleObjectType& obj)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_object()))
+    {
+        JSON_THROW(type_error::create(302, "type must be object, but is " + std::string(j.type_name())));
+    }
+
+    ConstructibleObjectType ret;
+    auto inner_object = j.template get_ptr<const typename BasicJsonType::object_t*>();
+    using value_type = typename ConstructibleObjectType::value_type;
+    std::transform(
+        inner_object->begin(), inner_object->end(),
+        std::inserter(ret, ret.begin()),
+        [](typename BasicJsonType::object_t::value_type const & p)
+    {
+        return value_type(p.first, p.second.template get<typename ConstructibleObjectType::mapped_type>());
+    });
+    obj = std::move(ret);
+}
+
+// overload for arithmetic types, not chosen for basic_json template arguments
+// (BooleanType, etc..); note: Is it really necessary to provide explicit
+// overloads for boolean_t etc. in case of a custom BooleanType which is not
+// an arithmetic type?
+template < typename BasicJsonType, typename ArithmeticType,
+           enable_if_t <
+               std::is_arithmetic<ArithmeticType>::value&&
+               !std::is_same<ArithmeticType, typename BasicJsonType::number_unsigned_t>::value&&
+               !std::is_same<ArithmeticType, typename BasicJsonType::number_integer_t>::value&&
+               !std::is_same<ArithmeticType, typename BasicJsonType::number_float_t>::value&&
+               !std::is_same<ArithmeticType, typename BasicJsonType::boolean_t>::value,
+               int > = 0 >
+void from_json(const BasicJsonType& j, ArithmeticType& val)
+{
+    switch (static_cast<value_t>(j))
+    {
+        case value_t::number_unsigned:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>());
+            break;
+        }
+        case value_t::number_integer:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_integer_t*>());
+            break;
+        }
+        case value_t::number_float:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_float_t*>());
+            break;
+        }
+        case value_t::boolean:
+        {
+            val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::boolean_t*>());
+            break;
+        }
+
+        default:
+            JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name())));
+    }
+}
+
+template<typename BasicJsonType, typename A1, typename A2>
+void from_json(const BasicJsonType& j, std::pair<A1, A2>& p)
+{
+    p = {j.at(0).template get<A1>(), j.at(1).template get<A2>()};
+}
+
+template<typename BasicJsonType, typename Tuple, std::size_t... Idx>
+void from_json_tuple_impl(const BasicJsonType& j, Tuple& t, index_sequence<Idx...> /*unused*/)
+{
+    t = std::make_tuple(j.at(Idx).template get<typename std::tuple_element<Idx, Tuple>::type>()...);
+}
+
+template<typename BasicJsonType, typename... Args>
+void from_json(const BasicJsonType& j, std::tuple<Args...>& t)
+{
+    from_json_tuple_impl(j, t, index_sequence_for<Args...> {});
+}
+
+template < typename BasicJsonType, typename Key, typename Value, typename Compare, typename Allocator,
+           typename = enable_if_t < !std::is_constructible <
+                                        typename BasicJsonType::string_t, Key >::value >>
+void from_json(const BasicJsonType& j, std::map<Key, Value, Compare, Allocator>& m)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
+    {
+        JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name())));
+    }
+    m.clear();
+    for (const auto& p : j)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!p.is_array()))
+        {
+            JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name())));
+        }
+        m.emplace(p.at(0).template get<Key>(), p.at(1).template get<Value>());
+    }
+}
+
+template < typename BasicJsonType, typename Key, typename Value, typename Hash, typename KeyEqual, typename Allocator,
+           typename = enable_if_t < !std::is_constructible <
+                                        typename BasicJsonType::string_t, Key >::value >>
+void from_json(const BasicJsonType& j, std::unordered_map<Key, Value, Hash, KeyEqual, Allocator>& m)
+{
+    if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
+    {
+        JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name())));
+    }
+    m.clear();
+    for (const auto& p : j)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!p.is_array()))
+        {
+            JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name())));
+        }
+        m.emplace(p.at(0).template get<Key>(), p.at(1).template get<Value>());
+    }
+}
+
+struct from_json_fn
+{
+    template<typename BasicJsonType, typename T>
+    auto operator()(const BasicJsonType& j, T& val) const
+    noexcept(noexcept(from_json(j, val)))
+    -> decltype(from_json(j, val), void())
+    {
+        return from_json(j, val);
+    }
+};
+}  // namespace detail
+
+/// namespace to hold default `from_json` function
+/// to see why this is required:
+/// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html
+namespace
+{
+constexpr const auto& from_json = detail::static_const<detail::from_json_fn>::value;
+} // namespace
+} // namespace nlohmann
+
+// #include <nlohmann/detail/conversions/to_json.hpp>
+
+
+#include <algorithm> // copy
+#include <iterator> // begin, end
+#include <string> // string
+#include <tuple> // tuple, get
+#include <type_traits> // is_same, is_constructible, is_floating_point, is_enum, underlying_type
+#include <utility> // move, forward, declval, pair
+#include <valarray> // valarray
+#include <vector> // vector
+
+// #include <nlohmann/detail/iterators/iteration_proxy.hpp>
+
+
+#include <cstddef> // size_t
+#include <iterator> // input_iterator_tag
+#include <string> // string, to_string
+#include <tuple> // tuple_size, get, tuple_element
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename string_type>
+void int_to_string( string_type& target, std::size_t value )
+{
+    // For ADL
+    using std::to_string;
+    target = to_string(value);
+}
+template<typename IteratorType> class iteration_proxy_value
+{
+  public:
+    using difference_type = std::ptrdiff_t;
+    using value_type = iteration_proxy_value;
+    using pointer = value_type * ;
+    using reference = value_type & ;
+    using iterator_category = std::input_iterator_tag;
+    using string_type = typename std::remove_cv< typename std::remove_reference<decltype( std::declval<IteratorType>().key() ) >::type >::type;
+
+  private:
+    /// the iterator
+    IteratorType anchor;
+    /// an index for arrays (used to create key names)
+    std::size_t array_index = 0;
+    /// last stringified array index
+    mutable std::size_t array_index_last = 0;
+    /// a string representation of the array index
+    mutable string_type array_index_str = "0";
+    /// an empty string (to return a reference for primitive values)
+    const string_type empty_str = "";
+
+  public:
+    explicit iteration_proxy_value(IteratorType it) noexcept : anchor(it) {}
+
+    /// dereference operator (needed for range-based for)
+    iteration_proxy_value& operator*()
+    {
+        return *this;
+    }
+
+    /// increment operator (needed for range-based for)
+    iteration_proxy_value& operator++()
+    {
+        ++anchor;
+        ++array_index;
+
+        return *this;
+    }
+
+    /// equality operator (needed for InputIterator)
+    bool operator==(const iteration_proxy_value& o) const
+    {
+        return anchor == o.anchor;
+    }
+
+    /// inequality operator (needed for range-based for)
+    bool operator!=(const iteration_proxy_value& o) const
+    {
+        return anchor != o.anchor;
+    }
+
+    /// return key of the iterator
+    const string_type& key() const
+    {
+        JSON_ASSERT(anchor.m_object != nullptr);
+
+        switch (anchor.m_object->type())
+        {
+            // use integer array index as key
+            case value_t::array:
+            {
+                if (array_index != array_index_last)
+                {
+                    int_to_string( array_index_str, array_index );
+                    array_index_last = array_index;
+                }
+                return array_index_str;
+            }
+
+            // use key from the object
+            case value_t::object:
+                return anchor.key();
+
+            // use an empty key for all primitive types
+            default:
+                return empty_str;
+        }
+    }
+
+    /// return value of the iterator
+    typename IteratorType::reference value() const
+    {
+        return anchor.value();
+    }
+};
+
+/// proxy class for the items() function
+template<typename IteratorType> class iteration_proxy
+{
+  private:
+    /// the container to iterate
+    typename IteratorType::reference container;
+
+  public:
+    /// construct iteration proxy from a container
+    explicit iteration_proxy(typename IteratorType::reference cont) noexcept
+        : container(cont) {}
+
+    /// return iterator begin (needed for range-based for)
+    iteration_proxy_value<IteratorType> begin() noexcept
+    {
+        return iteration_proxy_value<IteratorType>(container.begin());
+    }
+
+    /// return iterator end (needed for range-based for)
+    iteration_proxy_value<IteratorType> end() noexcept
+    {
+        return iteration_proxy_value<IteratorType>(container.end());
+    }
+};
+// Structured Bindings Support
+// For further reference see https://blog.tartanllama.xyz/structured-bindings/
+// And see https://github.com/nlohmann/json/pull/1391
+template<std::size_t N, typename IteratorType, enable_if_t<N == 0, int> = 0>
+auto get(const nlohmann::detail::iteration_proxy_value<IteratorType>& i) -> decltype(i.key())
+{
+    return i.key();
+}
+// Structured Bindings Support
+// For further reference see https://blog.tartanllama.xyz/structured-bindings/
+// And see https://github.com/nlohmann/json/pull/1391
+template<std::size_t N, typename IteratorType, enable_if_t<N == 1, int> = 0>
+auto get(const nlohmann::detail::iteration_proxy_value<IteratorType>& i) -> decltype(i.value())
+{
+    return i.value();
+}
+}  // namespace detail
+}  // namespace nlohmann
+
+// The Addition to the STD Namespace is required to add
+// Structured Bindings Support to the iteration_proxy_value class
+// For further reference see https://blog.tartanllama.xyz/structured-bindings/
+// And see https://github.com/nlohmann/json/pull/1391
+namespace std
+{
+#if defined(__clang__)
+    // Fix: https://github.com/nlohmann/json/issues/1401
+    #pragma clang diagnostic push
+    #pragma clang diagnostic ignored "-Wmismatched-tags"
+#endif
+template<typename IteratorType>
+class tuple_size<::nlohmann::detail::iteration_proxy_value<IteratorType>>
+            : public std::integral_constant<std::size_t, 2> {};
+
+template<std::size_t N, typename IteratorType>
+class tuple_element<N, ::nlohmann::detail::iteration_proxy_value<IteratorType >>
+{
+  public:
+    using type = decltype(
+                     get<N>(std::declval <
+                            ::nlohmann::detail::iteration_proxy_value<IteratorType >> ()));
+};
+#if defined(__clang__)
+    #pragma clang diagnostic pop
+#endif
+} // namespace std
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+//////////////////
+// constructors //
+//////////////////
+
+template<value_t> struct external_constructor;
+
+template<>
+struct external_constructor<value_t::boolean>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::boolean_t b) noexcept
+    {
+        j.m_type = value_t::boolean;
+        j.m_value = b;
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::string>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, const typename BasicJsonType::string_t& s)
+    {
+        j.m_type = value_t::string;
+        j.m_value = s;
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::string_t&& s)
+    {
+        j.m_type = value_t::string;
+        j.m_value = std::move(s);
+        j.assert_invariant();
+    }
+
+    template < typename BasicJsonType, typename CompatibleStringType,
+               enable_if_t < !std::is_same<CompatibleStringType, typename BasicJsonType::string_t>::value,
+                             int > = 0 >
+    static void construct(BasicJsonType& j, const CompatibleStringType& str)
+    {
+        j.m_type = value_t::string;
+        j.m_value.string = j.template create<typename BasicJsonType::string_t>(str);
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::binary>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, const typename BasicJsonType::binary_t& b)
+    {
+        j.m_type = value_t::binary;
+        typename BasicJsonType::binary_t value{b};
+        j.m_value = value;
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::binary_t&& b)
+    {
+        j.m_type = value_t::binary;
+        typename BasicJsonType::binary_t value{std::move(b)};
+        j.m_value = value;
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::number_float>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept
+    {
+        j.m_type = value_t::number_float;
+        j.m_value = val;
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::number_unsigned>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::number_unsigned_t val) noexcept
+    {
+        j.m_type = value_t::number_unsigned;
+        j.m_value = val;
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::number_integer>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::number_integer_t val) noexcept
+    {
+        j.m_type = value_t::number_integer;
+        j.m_value = val;
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::array>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, const typename BasicJsonType::array_t& arr)
+    {
+        j.m_type = value_t::array;
+        j.m_value = arr;
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::array_t&& arr)
+    {
+        j.m_type = value_t::array;
+        j.m_value = std::move(arr);
+        j.assert_invariant();
+    }
+
+    template < typename BasicJsonType, typename CompatibleArrayType,
+               enable_if_t < !std::is_same<CompatibleArrayType, typename BasicJsonType::array_t>::value,
+                             int > = 0 >
+    static void construct(BasicJsonType& j, const CompatibleArrayType& arr)
+    {
+        using std::begin;
+        using std::end;
+        j.m_type = value_t::array;
+        j.m_value.array = j.template create<typename BasicJsonType::array_t>(begin(arr), end(arr));
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, const std::vector<bool>& arr)
+    {
+        j.m_type = value_t::array;
+        j.m_value = value_t::array;
+        j.m_value.array->reserve(arr.size());
+        for (const bool x : arr)
+        {
+            j.m_value.array->push_back(x);
+        }
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType, typename T,
+             enable_if_t<std::is_convertible<T, BasicJsonType>::value, int> = 0>
+    static void construct(BasicJsonType& j, const std::valarray<T>& arr)
+    {
+        j.m_type = value_t::array;
+        j.m_value = value_t::array;
+        j.m_value.array->resize(arr.size());
+        if (arr.size() > 0)
+        {
+            std::copy(std::begin(arr), std::end(arr), j.m_value.array->begin());
+        }
+        j.assert_invariant();
+    }
+};
+
+template<>
+struct external_constructor<value_t::object>
+{
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, const typename BasicJsonType::object_t& obj)
+    {
+        j.m_type = value_t::object;
+        j.m_value = obj;
+        j.assert_invariant();
+    }
+
+    template<typename BasicJsonType>
+    static void construct(BasicJsonType& j, typename BasicJsonType::object_t&& obj)
+    {
+        j.m_type = value_t::object;
+        j.m_value = std::move(obj);
+        j.assert_invariant();
+    }
+
+    template < typename BasicJsonType, typename CompatibleObjectType,
+               enable_if_t < !std::is_same<CompatibleObjectType, typename BasicJsonType::object_t>::value, int > = 0 >
+    static void construct(BasicJsonType& j, const CompatibleObjectType& obj)
+    {
+        using std::begin;
+        using std::end;
+
+        j.m_type = value_t::object;
+        j.m_value.object = j.template create<typename BasicJsonType::object_t>(begin(obj), end(obj));
+        j.assert_invariant();
+    }
+};
+
+/////////////
+// to_json //
+/////////////
+
+template<typename BasicJsonType, typename T,
+         enable_if_t<std::is_same<T, typename BasicJsonType::boolean_t>::value, int> = 0>
+void to_json(BasicJsonType& j, T b) noexcept
+{
+    external_constructor<value_t::boolean>::construct(j, b);
+}
+
+template<typename BasicJsonType, typename CompatibleString,
+         enable_if_t<std::is_constructible<typename BasicJsonType::string_t, CompatibleString>::value, int> = 0>
+void to_json(BasicJsonType& j, const CompatibleString& s)
+{
+    external_constructor<value_t::string>::construct(j, s);
+}
+
+template<typename BasicJsonType>
+void to_json(BasicJsonType& j, typename BasicJsonType::string_t&& s)
+{
+    external_constructor<value_t::string>::construct(j, std::move(s));
+}
+
+template<typename BasicJsonType, typename FloatType,
+         enable_if_t<std::is_floating_point<FloatType>::value, int> = 0>
+void to_json(BasicJsonType& j, FloatType val) noexcept
+{
+    external_constructor<value_t::number_float>::construct(j, static_cast<typename BasicJsonType::number_float_t>(val));
+}
+
+template<typename BasicJsonType, typename CompatibleNumberUnsignedType,
+         enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_unsigned_t, CompatibleNumberUnsignedType>::value, int> = 0>
+void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept
+{
+    external_constructor<value_t::number_unsigned>::construct(j, static_cast<typename BasicJsonType::number_unsigned_t>(val));
+}
+
+template<typename BasicJsonType, typename CompatibleNumberIntegerType,
+         enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType>::value, int> = 0>
+void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept
+{
+    external_constructor<value_t::number_integer>::construct(j, static_cast<typename BasicJsonType::number_integer_t>(val));
+}
+
+template<typename BasicJsonType, typename EnumType,
+         enable_if_t<std::is_enum<EnumType>::value, int> = 0>
+void to_json(BasicJsonType& j, EnumType e) noexcept
+{
+    using underlying_type = typename std::underlying_type<EnumType>::type;
+    external_constructor<value_t::number_integer>::construct(j, static_cast<underlying_type>(e));
+}
+
+template<typename BasicJsonType>
+void to_json(BasicJsonType& j, const std::vector<bool>& e)
+{
+    external_constructor<value_t::array>::construct(j, e);
+}
+
+template < typename BasicJsonType, typename CompatibleArrayType,
+           enable_if_t < is_compatible_array_type<BasicJsonType,
+                         CompatibleArrayType>::value&&
+                         !is_compatible_object_type<BasicJsonType, CompatibleArrayType>::value&&
+                         !is_compatible_string_type<BasicJsonType, CompatibleArrayType>::value&&
+                         !std::is_same<typename BasicJsonType::binary_t, CompatibleArrayType>::value&&
+                         !is_basic_json<CompatibleArrayType>::value,
+                         int > = 0 >
+void to_json(BasicJsonType& j, const CompatibleArrayType& arr)
+{
+    external_constructor<value_t::array>::construct(j, arr);
+}
+
+template<typename BasicJsonType>
+void to_json(BasicJsonType& j, const typename BasicJsonType::binary_t& bin)
+{
+    external_constructor<value_t::binary>::construct(j, bin);
+}
+
+template<typename BasicJsonType, typename T,
+         enable_if_t<std::is_convertible<T, BasicJsonType>::value, int> = 0>
+void to_json(BasicJsonType& j, const std::valarray<T>& arr)
+{
+    external_constructor<value_t::array>::construct(j, std::move(arr));
+}
+
+template<typename BasicJsonType>
+void to_json(BasicJsonType& j, typename BasicJsonType::array_t&& arr)
+{
+    external_constructor<value_t::array>::construct(j, std::move(arr));
+}
+
+template < typename BasicJsonType, typename CompatibleObjectType,
+           enable_if_t < is_compatible_object_type<BasicJsonType, CompatibleObjectType>::value&& !is_basic_json<CompatibleObjectType>::value, int > = 0 >
+void to_json(BasicJsonType& j, const CompatibleObjectType& obj)
+{
+    external_constructor<value_t::object>::construct(j, obj);
+}
+
+template<typename BasicJsonType>
+void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj)
+{
+    external_constructor<value_t::object>::construct(j, std::move(obj));
+}
+
+template <
+    typename BasicJsonType, typename T, std::size_t N,
+    enable_if_t < !std::is_constructible<typename BasicJsonType::string_t,
+                  const T(&)[N]>::value,
+                  int > = 0 >
+void to_json(BasicJsonType& j, const T(&arr)[N])
+{
+    external_constructor<value_t::array>::construct(j, arr);
+}
+
+template < typename BasicJsonType, typename T1, typename T2, enable_if_t < std::is_constructible<BasicJsonType, T1>::value&& std::is_constructible<BasicJsonType, T2>::value, int > = 0 >
+void to_json(BasicJsonType& j, const std::pair<T1, T2>& p)
+{
+    j = { p.first, p.second };
+}
+
+// for https://github.com/nlohmann/json/pull/1134
+template<typename BasicJsonType, typename T,
+         enable_if_t<std::is_same<T, iteration_proxy_value<typename BasicJsonType::iterator>>::value, int> = 0>
+void to_json(BasicJsonType& j, const T& b)
+{
+    j = { {b.key(), b.value()} };
+}
+
+template<typename BasicJsonType, typename Tuple, std::size_t... Idx>
+void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence<Idx...> /*unused*/)
+{
+    j = { std::get<Idx>(t)... };
+}
+
+template<typename BasicJsonType, typename T, enable_if_t<is_constructible_tuple<BasicJsonType, T>::value, int > = 0>
+void to_json(BasicJsonType& j, const T& t)
+{
+    to_json_tuple_impl(j, t, make_index_sequence<std::tuple_size<T>::value> {});
+}
+
+struct to_json_fn
+{
+    template<typename BasicJsonType, typename T>
+    auto operator()(BasicJsonType& j, T&& val) const noexcept(noexcept(to_json(j, std::forward<T>(val))))
+    -> decltype(to_json(j, std::forward<T>(val)), void())
+    {
+        return to_json(j, std::forward<T>(val));
+    }
+};
+}  // namespace detail
+
+/// namespace to hold default `to_json` function
+namespace
+{
+constexpr const auto& to_json = detail::static_const<detail::to_json_fn>::value;
+} // namespace
+} // namespace nlohmann
+
+
+namespace nlohmann
+{
+
+template<typename, typename>
+struct adl_serializer
+{
+    /*!
+    @brief convert a JSON value to any value type
+
+    This function is usually called by the `get()` function of the
+    @ref basic_json class (either explicit or via conversion operators).
+
+    @param[in] j        JSON value to read from
+    @param[in,out] val  value to write to
+    */
+    template<typename BasicJsonType, typename ValueType>
+    static auto from_json(BasicJsonType&& j, ValueType& val) noexcept(
+        noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), val)))
+    -> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), val), void())
+    {
+        ::nlohmann::from_json(std::forward<BasicJsonType>(j), val);
+    }
+
+    /*!
+    @brief convert any value type to a JSON value
+
+    This function is usually called by the constructors of the @ref basic_json
+    class.
+
+    @param[in,out] j  JSON value to write to
+    @param[in] val    value to read from
+    */
+    template<typename BasicJsonType, typename ValueType>
+    static auto to_json(BasicJsonType& j, ValueType&& val) noexcept(
+        noexcept(::nlohmann::to_json(j, std::forward<ValueType>(val))))
+    -> decltype(::nlohmann::to_json(j, std::forward<ValueType>(val)), void())
+    {
+        ::nlohmann::to_json(j, std::forward<ValueType>(val));
+    }
+};
+
+}  // namespace nlohmann
+
+// #include <nlohmann/byte_container_with_subtype.hpp>
+
+
+#include <cstdint> // uint8_t
+#include <tuple> // tie
+#include <utility> // move
+
+namespace nlohmann
+{
+
+/*!
+@brief an internal type for a backed binary type
+
+This type extends the template parameter @a BinaryType provided to `basic_json`
+with a subtype used by BSON and MessagePack. This type exists so that the user
+does not have to specify a type themselves with a specific naming scheme in
+order to override the binary type.
+
+@tparam BinaryType container to store bytes (`std::vector<std::uint8_t>` by
+                   default)
+
+@since version 3.8.0
+*/
+template<typename BinaryType>
+class byte_container_with_subtype : public BinaryType
+{
+  public:
+    /// the type of the underlying container
+    using container_type = BinaryType;
+
+    byte_container_with_subtype() noexcept(noexcept(container_type()))
+        : container_type()
+    {}
+
+    byte_container_with_subtype(const container_type& b) noexcept(noexcept(container_type(b)))
+        : container_type(b)
+    {}
+
+    byte_container_with_subtype(container_type&& b) noexcept(noexcept(container_type(std::move(b))))
+        : container_type(std::move(b))
+    {}
+
+    byte_container_with_subtype(const container_type& b, std::uint8_t subtype) noexcept(noexcept(container_type(b)))
+        : container_type(b)
+        , m_subtype(subtype)
+        , m_has_subtype(true)
+    {}
+
+    byte_container_with_subtype(container_type&& b, std::uint8_t subtype) noexcept(noexcept(container_type(std::move(b))))
+        : container_type(std::move(b))
+        , m_subtype(subtype)
+        , m_has_subtype(true)
+    {}
+
+    bool operator==(const byte_container_with_subtype& rhs) const
+    {
+        return std::tie(static_cast<const BinaryType&>(*this), m_subtype, m_has_subtype) ==
+               std::tie(static_cast<const BinaryType&>(rhs), rhs.m_subtype, rhs.m_has_subtype);
+    }
+
+    bool operator!=(const byte_container_with_subtype& rhs) const
+    {
+        return !(rhs == *this);
+    }
+
+    /*!
+    @brief sets the binary subtype
+
+    Sets the binary subtype of the value, also flags a binary JSON value as
+    having a subtype, which has implications for serialization.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @sa @ref subtype() -- return the binary subtype
+    @sa @ref clear_subtype() -- clears the binary subtype
+    @sa @ref has_subtype() -- returns whether or not the binary value has a
+    subtype
+
+    @since version 3.8.0
+    */
+    void set_subtype(std::uint8_t subtype) noexcept
+    {
+        m_subtype = subtype;
+        m_has_subtype = true;
+    }
+
+    /*!
+    @brief return the binary subtype
+
+    Returns the numerical subtype of the value if it has a subtype. If it does
+    not have a subtype, this function will return size_t(-1) as a sentinel
+    value.
+
+    @return the numerical subtype of the binary value
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @sa @ref set_subtype() -- sets the binary subtype
+    @sa @ref clear_subtype() -- clears the binary subtype
+    @sa @ref has_subtype() -- returns whether or not the binary value has a
+    subtype
+
+    @since version 3.8.0
+    */
+    constexpr std::uint8_t subtype() const noexcept
+    {
+        return m_subtype;
+    }
+
+    /*!
+    @brief return whether the value has a subtype
+
+    @return whether the value has a subtype
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @sa @ref subtype() -- return the binary subtype
+    @sa @ref set_subtype() -- sets the binary subtype
+    @sa @ref clear_subtype() -- clears the binary subtype
+
+    @since version 3.8.0
+    */
+    constexpr bool has_subtype() const noexcept
+    {
+        return m_has_subtype;
+    }
+
+    /*!
+    @brief clears the binary subtype
+
+    Clears the binary subtype and flags the value as not having a subtype, which
+    has implications for serialization; for instance MessagePack will prefer the
+    bin family over the ext family.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @sa @ref subtype() -- return the binary subtype
+    @sa @ref set_subtype() -- sets the binary subtype
+    @sa @ref has_subtype() -- returns whether or not the binary value has a
+    subtype
+
+    @since version 3.8.0
+    */
+    void clear_subtype() noexcept
+    {
+        m_subtype = 0;
+        m_has_subtype = false;
+    }
+
+  private:
+    std::uint8_t m_subtype = 0;
+    bool m_has_subtype = false;
+};
+
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/conversions/from_json.hpp>
+
+// #include <nlohmann/detail/conversions/to_json.hpp>
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/hash.hpp>
+
+
+#include <cstddef> // size_t, uint8_t
+#include <functional> // hash
+
+namespace nlohmann
+{
+namespace detail
+{
+
+// boost::hash_combine
+inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
+{
+    seed ^= h + 0x9e3779b9 + (seed << 6U) + (seed >> 2U);
+    return seed;
+}
+
+/*!
+@brief hash a JSON value
+
+The hash function tries to rely on std::hash where possible. Furthermore, the
+type of the JSON value is taken into account to have different hash values for
+null, 0, 0U, and false, etc.
+
+@tparam BasicJsonType basic_json specialization
+@param j JSON value to hash
+@return hash value of j
+*/
+template<typename BasicJsonType>
+std::size_t hash(const BasicJsonType& j)
+{
+    using string_t = typename BasicJsonType::string_t;
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+
+    const auto type = static_cast<std::size_t>(j.type());
+    switch (j.type())
+    {
+        case BasicJsonType::value_t::null:
+        case BasicJsonType::value_t::discarded:
+        {
+            return combine(type, 0);
+        }
+
+        case BasicJsonType::value_t::object:
+        {
+            auto seed = combine(type, j.size());
+            for (const auto& element : j.items())
+            {
+                const auto h = std::hash<string_t> {}(element.key());
+                seed = combine(seed, h);
+                seed = combine(seed, hash(element.value()));
+            }
+            return seed;
+        }
+
+        case BasicJsonType::value_t::array:
+        {
+            auto seed = combine(type, j.size());
+            for (const auto& element : j)
+            {
+                seed = combine(seed, hash(element));
+            }
+            return seed;
+        }
+
+        case BasicJsonType::value_t::string:
+        {
+            const auto h = std::hash<string_t> {}(j.template get_ref<const string_t&>());
+            return combine(type, h);
+        }
+
+        case BasicJsonType::value_t::boolean:
+        {
+            const auto h = std::hash<bool> {}(j.template get<bool>());
+            return combine(type, h);
+        }
+
+        case BasicJsonType::value_t::number_integer:
+        {
+            const auto h = std::hash<number_integer_t> {}(j.template get<number_integer_t>());
+            return combine(type, h);
+        }
+
+        case nlohmann::detail::value_t::number_unsigned:
+        {
+            const auto h = std::hash<number_unsigned_t> {}(j.template get<number_unsigned_t>());
+            return combine(type, h);
+        }
+
+        case nlohmann::detail::value_t::number_float:
+        {
+            const auto h = std::hash<number_float_t> {}(j.template get<number_float_t>());
+            return combine(type, h);
+        }
+
+        case nlohmann::detail::value_t::binary:
+        {
+            auto seed = combine(type, j.get_binary().size());
+            const auto h = std::hash<bool> {}(j.get_binary().has_subtype());
+            seed = combine(seed, h);
+            seed = combine(seed, j.get_binary().subtype());
+            for (const auto byte : j.get_binary())
+            {
+                seed = combine(seed, std::hash<std::uint8_t> {}(byte));
+            }
+            return seed;
+        }
+
+        default: // LCOV_EXCL_LINE
+            JSON_ASSERT(false); // LCOV_EXCL_LINE
+    }
+}
+
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/input/binary_reader.hpp>
+
+
+#include <algorithm> // generate_n
+#include <array> // array
+#include <cmath> // ldexp
+#include <cstddef> // size_t
+#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t
+#include <cstdio> // snprintf
+#include <cstring> // memcpy
+#include <iterator> // back_inserter
+#include <limits> // numeric_limits
+#include <string> // char_traits, string
+#include <utility> // make_pair, move
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/input/input_adapters.hpp>
+
+
+#include <array> // array
+#include <cstddef> // size_t
+#include <cstdio> //FILE *
+#include <cstring> // strlen
+#include <istream> // istream
+#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next
+#include <memory> // shared_ptr, make_shared, addressof
+#include <numeric> // accumulate
+#include <string> // string, char_traits
+#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
+#include <utility> // pair, declval
+
+// #include <nlohmann/detail/iterators/iterator_traits.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+/// the supported input formats
+enum class input_format_t { json, cbor, msgpack, ubjson, bson };
+
+////////////////////
+// input adapters //
+////////////////////
+
+/*!
+Input adapter for stdio file access. This adapter read only 1 byte and do not use any
+ buffer. This adapter is a very low level adapter.
+*/
+class file_input_adapter
+{
+  public:
+    using char_type = char;
+
+    JSON_HEDLEY_NON_NULL(2)
+    explicit file_input_adapter(std::FILE* f) noexcept
+        : m_file(f)
+    {}
+
+    // make class move-only
+    file_input_adapter(const file_input_adapter&) = delete;
+    file_input_adapter(file_input_adapter&&) = default;
+    file_input_adapter& operator=(const file_input_adapter&) = delete;
+    file_input_adapter& operator=(file_input_adapter&&) = delete;
+
+    std::char_traits<char>::int_type get_character() noexcept
+    {
+        return std::fgetc(m_file);
+    }
+
+  private:
+    /// the file pointer to read from
+    std::FILE* m_file;
+};
+
+
+/*!
+Input adapter for a (caching) istream. Ignores a UFT Byte Order Mark at
+beginning of input. Does not support changing the underlying std::streambuf
+in mid-input. Maintains underlying std::istream and std::streambuf to support
+subsequent use of standard std::istream operations to process any input
+characters following those used in parsing the JSON input.  Clears the
+std::istream flags; any input errors (e.g., EOF) will be detected by the first
+subsequent call for input from the std::istream.
+*/
+class input_stream_adapter
+{
+  public:
+    using char_type = char;
+
+    ~input_stream_adapter()
+    {
+        // clear stream flags; we use underlying streambuf I/O, do not
+        // maintain ifstream flags, except eof
+        if (is != nullptr)
+        {
+            is->clear(is->rdstate() & std::ios::eofbit);
+        }
+    }
+
+    explicit input_stream_adapter(std::istream& i)
+        : is(&i), sb(i.rdbuf())
+    {}
+
+    // delete because of pointer members
+    input_stream_adapter(const input_stream_adapter&) = delete;
+    input_stream_adapter& operator=(input_stream_adapter&) = delete;
+    input_stream_adapter& operator=(input_stream_adapter&& rhs) = delete;
+
+    input_stream_adapter(input_stream_adapter&& rhs) noexcept : is(rhs.is), sb(rhs.sb)
+    {
+        rhs.is = nullptr;
+        rhs.sb = nullptr;
+    }
+
+    // std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
+    // ensure that std::char_traits<char>::eof() and the character 0xFF do not
+    // end up as the same value, eg. 0xFFFFFFFF.
+    std::char_traits<char>::int_type get_character()
+    {
+        auto res = sb->sbumpc();
+        // set eof manually, as we don't use the istream interface.
+        if (JSON_HEDLEY_UNLIKELY(res == EOF))
+        {
+            is->clear(is->rdstate() | std::ios::eofbit);
+        }
+        return res;
+    }
+
+  private:
+    /// the associated input stream
+    std::istream* is = nullptr;
+    std::streambuf* sb = nullptr;
+};
+
+// General-purpose iterator-based adapter. It might not be as fast as
+// theoretically possible for some containers, but it is extremely versatile.
+template<typename IteratorType>
+class iterator_input_adapter
+{
+  public:
+    using char_type = typename std::iterator_traits<IteratorType>::value_type;
+
+    iterator_input_adapter(IteratorType first, IteratorType last)
+        : current(std::move(first)), end(std::move(last)) {}
+
+    typename std::char_traits<char_type>::int_type get_character()
+    {
+        if (JSON_HEDLEY_LIKELY(current != end))
+        {
+            auto result = std::char_traits<char_type>::to_int_type(*current);
+            std::advance(current, 1);
+            return result;
+        }
+        else
+        {
+            return std::char_traits<char_type>::eof();
+        }
+    }
+
+  private:
+    IteratorType current;
+    IteratorType end;
+
+    template<typename BaseInputAdapter, size_t T>
+    friend struct wide_string_input_helper;
+
+    bool empty() const
+    {
+        return current == end;
+    }
+
+};
+
+
+template<typename BaseInputAdapter, size_t T>
+struct wide_string_input_helper;
+
+template<typename BaseInputAdapter>
+struct wide_string_input_helper<BaseInputAdapter, 4>
+{
+    // UTF-32
+    static void fill_buffer(BaseInputAdapter& input,
+                            std::array<std::char_traits<char>::int_type, 4>& utf8_bytes,
+                            size_t& utf8_bytes_index,
+                            size_t& utf8_bytes_filled)
+    {
+        utf8_bytes_index = 0;
+
+        if (JSON_HEDLEY_UNLIKELY(input.empty()))
+        {
+            utf8_bytes[0] = std::char_traits<char>::eof();
+            utf8_bytes_filled = 1;
+        }
+        else
+        {
+            // get the current character
+            const auto wc = input.get_character();
+
+            // UTF-32 to UTF-8 encoding
+            if (wc < 0x80)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(wc);
+                utf8_bytes_filled = 1;
+            }
+            else if (wc <= 0x7FF)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xC0u | ((static_cast<unsigned int>(wc) >> 6u) & 0x1Fu));
+                utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | (static_cast<unsigned int>(wc) & 0x3Fu));
+                utf8_bytes_filled = 2;
+            }
+            else if (wc <= 0xFFFF)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xE0u | ((static_cast<unsigned int>(wc) >> 12u) & 0x0Fu));
+                utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | ((static_cast<unsigned int>(wc) >> 6u) & 0x3Fu));
+                utf8_bytes[2] = static_cast<std::char_traits<char>::int_type>(0x80u | (static_cast<unsigned int>(wc) & 0x3Fu));
+                utf8_bytes_filled = 3;
+            }
+            else if (wc <= 0x10FFFF)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xF0u | ((static_cast<unsigned int>(wc) >> 18u) & 0x07u));
+                utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | ((static_cast<unsigned int>(wc) >> 12u) & 0x3Fu));
+                utf8_bytes[2] = static_cast<std::char_traits<char>::int_type>(0x80u | ((static_cast<unsigned int>(wc) >> 6u) & 0x3Fu));
+                utf8_bytes[3] = static_cast<std::char_traits<char>::int_type>(0x80u | (static_cast<unsigned int>(wc) & 0x3Fu));
+                utf8_bytes_filled = 4;
+            }
+            else
+            {
+                // unknown character
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(wc);
+                utf8_bytes_filled = 1;
+            }
+        }
+    }
+};
+
+template<typename BaseInputAdapter>
+struct wide_string_input_helper<BaseInputAdapter, 2>
+{
+    // UTF-16
+    static void fill_buffer(BaseInputAdapter& input,
+                            std::array<std::char_traits<char>::int_type, 4>& utf8_bytes,
+                            size_t& utf8_bytes_index,
+                            size_t& utf8_bytes_filled)
+    {
+        utf8_bytes_index = 0;
+
+        if (JSON_HEDLEY_UNLIKELY(input.empty()))
+        {
+            utf8_bytes[0] = std::char_traits<char>::eof();
+            utf8_bytes_filled = 1;
+        }
+        else
+        {
+            // get the current character
+            const auto wc = input.get_character();
+
+            // UTF-16 to UTF-8 encoding
+            if (wc < 0x80)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(wc);
+                utf8_bytes_filled = 1;
+            }
+            else if (wc <= 0x7FF)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xC0u | ((static_cast<unsigned int>(wc) >> 6u)));
+                utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | (static_cast<unsigned int>(wc) & 0x3Fu));
+                utf8_bytes_filled = 2;
+            }
+            else if (0xD800 > wc || wc >= 0xE000)
+            {
+                utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xE0u | ((static_cast<unsigned int>(wc) >> 12u)));
+                utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | ((static_cast<unsigned int>(wc) >> 6u) & 0x3Fu));
+                utf8_bytes[2] = static_cast<std::char_traits<char>::int_type>(0x80u | (static_cast<unsigned int>(wc) & 0x3Fu));
+                utf8_bytes_filled = 3;
+            }
+            else
+            {
+                if (JSON_HEDLEY_UNLIKELY(!input.empty()))
+                {
+                    const auto wc2 = static_cast<unsigned int>(input.get_character());
+                    const auto charcode = 0x10000u + (((static_cast<unsigned int>(wc) & 0x3FFu) << 10u) | (wc2 & 0x3FFu));
+                    utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(0xF0u | (charcode >> 18u));
+                    utf8_bytes[1] = static_cast<std::char_traits<char>::int_type>(0x80u | ((charcode >> 12u) & 0x3Fu));
+                    utf8_bytes[2] = static_cast<std::char_traits<char>::int_type>(0x80u | ((charcode >> 6u) & 0x3Fu));
+                    utf8_bytes[3] = static_cast<std::char_traits<char>::int_type>(0x80u | (charcode & 0x3Fu));
+                    utf8_bytes_filled = 4;
+                }
+                else
+                {
+                    utf8_bytes[0] = static_cast<std::char_traits<char>::int_type>(wc);
+                    utf8_bytes_filled = 1;
+                }
+            }
+        }
+    }
+};
+
+// Wraps another input apdater to convert wide character types into individual bytes.
+template<typename BaseInputAdapter, typename WideCharType>
+class wide_string_input_adapter
+{
+  public:
+    using char_type = char;
+
+    wide_string_input_adapter(BaseInputAdapter base)
+        : base_adapter(base) {}
+
+    typename std::char_traits<char>::int_type get_character() noexcept
+    {
+        // check if buffer needs to be filled
+        if (utf8_bytes_index == utf8_bytes_filled)
+        {
+            fill_buffer<sizeof(WideCharType)>();
+
+            JSON_ASSERT(utf8_bytes_filled > 0);
+            JSON_ASSERT(utf8_bytes_index == 0);
+        }
+
+        // use buffer
+        JSON_ASSERT(utf8_bytes_filled > 0);
+        JSON_ASSERT(utf8_bytes_index < utf8_bytes_filled);
+        return utf8_bytes[utf8_bytes_index++];
+    }
+
+  private:
+    BaseInputAdapter base_adapter;
+
+    template<size_t T>
+    void fill_buffer()
+    {
+        wide_string_input_helper<BaseInputAdapter, T>::fill_buffer(base_adapter, utf8_bytes, utf8_bytes_index, utf8_bytes_filled);
+    }
+
+    /// a buffer for UTF-8 bytes
+    std::array<std::char_traits<char>::int_type, 4> utf8_bytes = {{0, 0, 0, 0}};
+
+    /// index to the utf8_codes array for the next valid byte
+    std::size_t utf8_bytes_index = 0;
+    /// number of valid bytes in the utf8_codes array
+    std::size_t utf8_bytes_filled = 0;
+};
+
+
+template<typename IteratorType, typename Enable = void>
+struct iterator_input_adapter_factory
+{
+    using iterator_type = IteratorType;
+    using char_type = typename std::iterator_traits<iterator_type>::value_type;
+    using adapter_type = iterator_input_adapter<iterator_type>;
+
+    static adapter_type create(IteratorType first, IteratorType last)
+    {
+        return adapter_type(std::move(first), std::move(last));
+    }
+};
+
+template<typename T>
+struct is_iterator_of_multibyte
+{
+    using value_type = typename std::iterator_traits<T>::value_type;
+    enum
+    {
+        value = sizeof(value_type) > 1
+    };
+};
+
+template<typename IteratorType>
+struct iterator_input_adapter_factory<IteratorType, enable_if_t<is_iterator_of_multibyte<IteratorType>::value>>
+{
+    using iterator_type = IteratorType;
+    using char_type = typename std::iterator_traits<iterator_type>::value_type;
+    using base_adapter_type = iterator_input_adapter<iterator_type>;
+    using adapter_type = wide_string_input_adapter<base_adapter_type, char_type>;
+
+    static adapter_type create(IteratorType first, IteratorType last)
+    {
+        return adapter_type(base_adapter_type(std::move(first), std::move(last)));
+    }
+};
+
+// General purpose iterator-based input
+template<typename IteratorType>
+typename iterator_input_adapter_factory<IteratorType>::adapter_type input_adapter(IteratorType first, IteratorType last)
+{
+    using factory_type = iterator_input_adapter_factory<IteratorType>;
+    return factory_type::create(first, last);
+}
+
+// Convenience shorthand from container to iterator
+template<typename ContainerType>
+auto input_adapter(const ContainerType& container) -> decltype(input_adapter(begin(container), end(container)))
+{
+    // Enable ADL
+    using std::begin;
+    using std::end;
+
+    return input_adapter(begin(container), end(container));
+}
+
+// Special cases with fast paths
+inline file_input_adapter input_adapter(std::FILE* file)
+{
+    return file_input_adapter(file);
+}
+
+inline input_stream_adapter input_adapter(std::istream& stream)
+{
+    return input_stream_adapter(stream);
+}
+
+inline input_stream_adapter input_adapter(std::istream&& stream)
+{
+    return input_stream_adapter(stream);
+}
+
+using contiguous_bytes_input_adapter = decltype(input_adapter(std::declval<const char*>(), std::declval<const char*>()));
+
+// Null-delimited strings, and the like.
+template < typename CharT,
+           typename std::enable_if <
+               std::is_pointer<CharT>::value&&
+               !std::is_array<CharT>::value&&
+               std::is_integral<typename std::remove_pointer<CharT>::type>::value&&
+               sizeof(typename std::remove_pointer<CharT>::type) == 1,
+               int >::type = 0 >
+contiguous_bytes_input_adapter input_adapter(CharT b)
+{
+    auto length = std::strlen(reinterpret_cast<const char*>(b));
+    const auto* ptr = reinterpret_cast<const char*>(b);
+    return input_adapter(ptr, ptr + length);
+}
+
+template<typename T, std::size_t N>
+auto input_adapter(T (&array)[N]) -> decltype(input_adapter(array, array + N))
+{
+    return input_adapter(array, array + N);
+}
+
+// This class only handles inputs of input_buffer_adapter type.
+// It's required so that expressions like {ptr, len} can be implicitely casted
+// to the correct adapter.
+class span_input_adapter
+{
+  public:
+    template < typename CharT,
+               typename std::enable_if <
+                   std::is_pointer<CharT>::value&&
+                   std::is_integral<typename std::remove_pointer<CharT>::type>::value&&
+                   sizeof(typename std::remove_pointer<CharT>::type) == 1,
+                   int >::type = 0 >
+    span_input_adapter(CharT b, std::size_t l)
+        : ia(reinterpret_cast<const char*>(b), reinterpret_cast<const char*>(b) + l) {}
+
+    template<class IteratorType,
+             typename std::enable_if<
+                 std::is_same<typename iterator_traits<IteratorType>::iterator_category, std::random_access_iterator_tag>::value,
+                 int>::type = 0>
+    span_input_adapter(IteratorType first, IteratorType last)
+        : ia(input_adapter(first, last)) {}
+
+    contiguous_bytes_input_adapter&& get()
+    {
+        return std::move(ia);
+    }
+
+  private:
+    contiguous_bytes_input_adapter ia;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/input/json_sax.hpp>
+
+
+#include <cstddef>
+#include <string> // string
+#include <utility> // move
+#include <vector> // vector
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+namespace nlohmann
+{
+
+/*!
+@brief SAX interface
+
+This class describes the SAX interface used by @ref nlohmann::json::sax_parse.
+Each function is called in different situations while the input is parsed. The
+boolean return value informs the parser whether to continue processing the
+input.
+*/
+template<typename BasicJsonType>
+struct json_sax
+{
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+
+    /*!
+    @brief a null value was read
+    @return whether parsing should proceed
+    */
+    virtual bool null() = 0;
+
+    /*!
+    @brief a boolean value was read
+    @param[in] val  boolean value
+    @return whether parsing should proceed
+    */
+    virtual bool boolean(bool val) = 0;
+
+    /*!
+    @brief an integer number was read
+    @param[in] val  integer value
+    @return whether parsing should proceed
+    */
+    virtual bool number_integer(number_integer_t val) = 0;
+
+    /*!
+    @brief an unsigned integer number was read
+    @param[in] val  unsigned integer value
+    @return whether parsing should proceed
+    */
+    virtual bool number_unsigned(number_unsigned_t val) = 0;
+
+    /*!
+    @brief an floating-point number was read
+    @param[in] val  floating-point value
+    @param[in] s    raw token value
+    @return whether parsing should proceed
+    */
+    virtual bool number_float(number_float_t val, const string_t& s) = 0;
+
+    /*!
+    @brief a string was read
+    @param[in] val  string value
+    @return whether parsing should proceed
+    @note It is safe to move the passed string.
+    */
+    virtual bool string(string_t& val) = 0;
+
+    /*!
+    @brief a binary string was read
+    @param[in] val  binary value
+    @return whether parsing should proceed
+    @note It is safe to move the passed binary.
+    */
+    virtual bool binary(binary_t& val) = 0;
+
+    /*!
+    @brief the beginning of an object was read
+    @param[in] elements  number of object elements or -1 if unknown
+    @return whether parsing should proceed
+    @note binary formats may report the number of elements
+    */
+    virtual bool start_object(std::size_t elements) = 0;
+
+    /*!
+    @brief an object key was read
+    @param[in] val  object key
+    @return whether parsing should proceed
+    @note It is safe to move the passed string.
+    */
+    virtual bool key(string_t& val) = 0;
+
+    /*!
+    @brief the end of an object was read
+    @return whether parsing should proceed
+    */
+    virtual bool end_object() = 0;
+
+    /*!
+    @brief the beginning of an array was read
+    @param[in] elements  number of array elements or -1 if unknown
+    @return whether parsing should proceed
+    @note binary formats may report the number of elements
+    */
+    virtual bool start_array(std::size_t elements) = 0;
+
+    /*!
+    @brief the end of an array was read
+    @return whether parsing should proceed
+    */
+    virtual bool end_array() = 0;
+
+    /*!
+    @brief a parse error occurred
+    @param[in] position    the position in the input where the error occurs
+    @param[in] last_token  the last read token
+    @param[in] ex          an exception object describing the error
+    @return whether parsing should proceed (must return false)
+    */
+    virtual bool parse_error(std::size_t position,
+                             const std::string& last_token,
+                             const detail::exception& ex) = 0;
+
+    virtual ~json_sax() = default;
+};
+
+
+namespace detail
+{
+/*!
+@brief SAX implementation to create a JSON value from SAX events
+
+This class implements the @ref json_sax interface and processes the SAX events
+to create a JSON value which makes it basically a DOM parser. The structure or
+hierarchy of the JSON value is managed by the stack `ref_stack` which contains
+a pointer to the respective array or object for each recursion depth.
+
+After successful parsing, the value that is passed by reference to the
+constructor contains the parsed value.
+
+@tparam BasicJsonType  the JSON type
+*/
+template<typename BasicJsonType>
+class json_sax_dom_parser
+{
+  public:
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+
+    /*!
+    @param[in, out] r  reference to a JSON value that is manipulated while
+                       parsing
+    @param[in] allow_exceptions_  whether parse errors yield exceptions
+    */
+    explicit json_sax_dom_parser(BasicJsonType& r, const bool allow_exceptions_ = true)
+        : root(r), allow_exceptions(allow_exceptions_)
+    {}
+
+    // make class move-only
+    json_sax_dom_parser(const json_sax_dom_parser&) = delete;
+    json_sax_dom_parser(json_sax_dom_parser&&) = default;
+    json_sax_dom_parser& operator=(const json_sax_dom_parser&) = delete;
+    json_sax_dom_parser& operator=(json_sax_dom_parser&&) = default;
+    ~json_sax_dom_parser() = default;
+
+    bool null()
+    {
+        handle_value(nullptr);
+        return true;
+    }
+
+    bool boolean(bool val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_integer(number_integer_t val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_unsigned(number_unsigned_t val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_float(number_float_t val, const string_t& /*unused*/)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool string(string_t& val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool binary(binary_t& val)
+    {
+        handle_value(std::move(val));
+        return true;
+    }
+
+    bool start_object(std::size_t len)
+    {
+        ref_stack.push_back(handle_value(BasicJsonType::value_t::object));
+
+        if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size()))
+        {
+            JSON_THROW(out_of_range::create(408,
+                                            "excessive object size: " + std::to_string(len)));
+        }
+
+        return true;
+    }
+
+    bool key(string_t& val)
+    {
+        // add null at given key and store the reference for later
+        object_element = &(ref_stack.back()->m_value.object->operator[](val));
+        return true;
+    }
+
+    bool end_object()
+    {
+        ref_stack.pop_back();
+        return true;
+    }
+
+    bool start_array(std::size_t len)
+    {
+        ref_stack.push_back(handle_value(BasicJsonType::value_t::array));
+
+        if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size()))
+        {
+            JSON_THROW(out_of_range::create(408,
+                                            "excessive array size: " + std::to_string(len)));
+        }
+
+        return true;
+    }
+
+    bool end_array()
+    {
+        ref_stack.pop_back();
+        return true;
+    }
+
+    template<class Exception>
+    bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/,
+                     const Exception& ex)
+    {
+        errored = true;
+        static_cast<void>(ex);
+        if (allow_exceptions)
+        {
+            JSON_THROW(ex);
+        }
+        return false;
+    }
+
+    constexpr bool is_errored() const
+    {
+        return errored;
+    }
+
+  private:
+    /*!
+    @invariant If the ref stack is empty, then the passed value will be the new
+               root.
+    @invariant If the ref stack contains a value, then it is an array or an
+               object to which we can add elements
+    */
+    template<typename Value>
+    JSON_HEDLEY_RETURNS_NON_NULL
+    BasicJsonType* handle_value(Value&& v)
+    {
+        if (ref_stack.empty())
+        {
+            root = BasicJsonType(std::forward<Value>(v));
+            return &root;
+        }
+
+        JSON_ASSERT(ref_stack.back()->is_array() || ref_stack.back()->is_object());
+
+        if (ref_stack.back()->is_array())
+        {
+            ref_stack.back()->m_value.array->emplace_back(std::forward<Value>(v));
+            return &(ref_stack.back()->m_value.array->back());
+        }
+
+        JSON_ASSERT(ref_stack.back()->is_object());
+        JSON_ASSERT(object_element);
+        *object_element = BasicJsonType(std::forward<Value>(v));
+        return object_element;
+    }
+
+    /// the parsed JSON value
+    BasicJsonType& root;
+    /// stack to model hierarchy of values
+    std::vector<BasicJsonType*> ref_stack {};
+    /// helper to hold the reference for the next object element
+    BasicJsonType* object_element = nullptr;
+    /// whether a syntax error occurred
+    bool errored = false;
+    /// whether to throw exceptions in case of errors
+    const bool allow_exceptions = true;
+};
+
+template<typename BasicJsonType>
+class json_sax_dom_callback_parser
+{
+  public:
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+    using parser_callback_t = typename BasicJsonType::parser_callback_t;
+    using parse_event_t = typename BasicJsonType::parse_event_t;
+
+    json_sax_dom_callback_parser(BasicJsonType& r,
+                                 const parser_callback_t cb,
+                                 const bool allow_exceptions_ = true)
+        : root(r), callback(cb), allow_exceptions(allow_exceptions_)
+    {
+        keep_stack.push_back(true);
+    }
+
+    // make class move-only
+    json_sax_dom_callback_parser(const json_sax_dom_callback_parser&) = delete;
+    json_sax_dom_callback_parser(json_sax_dom_callback_parser&&) = default;
+    json_sax_dom_callback_parser& operator=(const json_sax_dom_callback_parser&) = delete;
+    json_sax_dom_callback_parser& operator=(json_sax_dom_callback_parser&&) = default;
+    ~json_sax_dom_callback_parser() = default;
+
+    bool null()
+    {
+        handle_value(nullptr);
+        return true;
+    }
+
+    bool boolean(bool val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_integer(number_integer_t val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_unsigned(number_unsigned_t val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool number_float(number_float_t val, const string_t& /*unused*/)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool string(string_t& val)
+    {
+        handle_value(val);
+        return true;
+    }
+
+    bool binary(binary_t& val)
+    {
+        handle_value(std::move(val));
+        return true;
+    }
+
+    bool start_object(std::size_t len)
+    {
+        // check callback for object start
+        const bool keep = callback(static_cast<int>(ref_stack.size()), parse_event_t::object_start, discarded);
+        keep_stack.push_back(keep);
+
+        auto val = handle_value(BasicJsonType::value_t::object, true);
+        ref_stack.push_back(val.second);
+
+        // check object limit
+        if (ref_stack.back() && JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size()))
+        {
+            JSON_THROW(out_of_range::create(408, "excessive object size: " + std::to_string(len)));
+        }
+
+        return true;
+    }
+
+    bool key(string_t& val)
+    {
+        BasicJsonType k = BasicJsonType(val);
+
+        // check callback for key
+        const bool keep = callback(static_cast<int>(ref_stack.size()), parse_event_t::key, k);
+        key_keep_stack.push_back(keep);
+
+        // add discarded value at given key and store the reference for later
+        if (keep && ref_stack.back())
+        {
+            object_element = &(ref_stack.back()->m_value.object->operator[](val) = discarded);
+        }
+
+        return true;
+    }
+
+    bool end_object()
+    {
+        if (ref_stack.back() && !callback(static_cast<int>(ref_stack.size()) - 1, parse_event_t::object_end, *ref_stack.back()))
+        {
+            // discard object
+            *ref_stack.back() = discarded;
+        }
+
+        JSON_ASSERT(!ref_stack.empty());
+        JSON_ASSERT(!keep_stack.empty());
+        ref_stack.pop_back();
+        keep_stack.pop_back();
+
+        if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured())
+        {
+            // remove discarded value
+            for (auto it = ref_stack.back()->begin(); it != ref_stack.back()->end(); ++it)
+            {
+                if (it->is_discarded())
+                {
+                    ref_stack.back()->erase(it);
+                    break;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    bool start_array(std::size_t len)
+    {
+        const bool keep = callback(static_cast<int>(ref_stack.size()), parse_event_t::array_start, discarded);
+        keep_stack.push_back(keep);
+
+        auto val = handle_value(BasicJsonType::value_t::array, true);
+        ref_stack.push_back(val.second);
+
+        // check array limit
+        if (ref_stack.back() && JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size()))
+        {
+            JSON_THROW(out_of_range::create(408, "excessive array size: " + std::to_string(len)));
+        }
+
+        return true;
+    }
+
+    bool end_array()
+    {
+        bool keep = true;
+
+        if (ref_stack.back())
+        {
+            keep = callback(static_cast<int>(ref_stack.size()) - 1, parse_event_t::array_end, *ref_stack.back());
+            if (!keep)
+            {
+                // discard array
+                *ref_stack.back() = discarded;
+            }
+        }
+
+        JSON_ASSERT(!ref_stack.empty());
+        JSON_ASSERT(!keep_stack.empty());
+        ref_stack.pop_back();
+        keep_stack.pop_back();
+
+        // remove discarded value
+        if (!keep && !ref_stack.empty() && ref_stack.back()->is_array())
+        {
+            ref_stack.back()->m_value.array->pop_back();
+        }
+
+        return true;
+    }
+
+    template<class Exception>
+    bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/,
+                     const Exception& ex)
+    {
+        errored = true;
+        static_cast<void>(ex);
+        if (allow_exceptions)
+        {
+            JSON_THROW(ex);
+        }
+        return false;
+    }
+
+    constexpr bool is_errored() const
+    {
+        return errored;
+    }
+
+  private:
+    /*!
+    @param[in] v  value to add to the JSON value we build during parsing
+    @param[in] skip_callback  whether we should skip calling the callback
+               function; this is required after start_array() and
+               start_object() SAX events, because otherwise we would call the
+               callback function with an empty array or object, respectively.
+
+    @invariant If the ref stack is empty, then the passed value will be the new
+               root.
+    @invariant If the ref stack contains a value, then it is an array or an
+               object to which we can add elements
+
+    @return pair of boolean (whether value should be kept) and pointer (to the
+            passed value in the ref_stack hierarchy; nullptr if not kept)
+    */
+    template<typename Value>
+    std::pair<bool, BasicJsonType*> handle_value(Value&& v, const bool skip_callback = false)
+    {
+        JSON_ASSERT(!keep_stack.empty());
+
+        // do not handle this value if we know it would be added to a discarded
+        // container
+        if (!keep_stack.back())
+        {
+            return {false, nullptr};
+        }
+
+        // create value
+        auto value = BasicJsonType(std::forward<Value>(v));
+
+        // check callback
+        const bool keep = skip_callback || callback(static_cast<int>(ref_stack.size()), parse_event_t::value, value);
+
+        // do not handle this value if we just learnt it shall be discarded
+        if (!keep)
+        {
+            return {false, nullptr};
+        }
+
+        if (ref_stack.empty())
+        {
+            root = std::move(value);
+            return {true, &root};
+        }
+
+        // skip this value if we already decided to skip the parent
+        // (https://github.com/nlohmann/json/issues/971#issuecomment-413678360)
+        if (!ref_stack.back())
+        {
+            return {false, nullptr};
+        }
+
+        // we now only expect arrays and objects
+        JSON_ASSERT(ref_stack.back()->is_array() || ref_stack.back()->is_object());
+
+        // array
+        if (ref_stack.back()->is_array())
+        {
+            ref_stack.back()->m_value.array->push_back(std::move(value));
+            return {true, &(ref_stack.back()->m_value.array->back())};
+        }
+
+        // object
+        JSON_ASSERT(ref_stack.back()->is_object());
+        // check if we should store an element for the current key
+        JSON_ASSERT(!key_keep_stack.empty());
+        const bool store_element = key_keep_stack.back();
+        key_keep_stack.pop_back();
+
+        if (!store_element)
+        {
+            return {false, nullptr};
+        }
+
+        JSON_ASSERT(object_element);
+        *object_element = std::move(value);
+        return {true, object_element};
+    }
+
+    /// the parsed JSON value
+    BasicJsonType& root;
+    /// stack to model hierarchy of values
+    std::vector<BasicJsonType*> ref_stack {};
+    /// stack to manage which values to keep
+    std::vector<bool> keep_stack {};
+    /// stack to manage which object keys to keep
+    std::vector<bool> key_keep_stack {};
+    /// helper to hold the reference for the next object element
+    BasicJsonType* object_element = nullptr;
+    /// whether a syntax error occurred
+    bool errored = false;
+    /// callback function
+    const parser_callback_t callback = nullptr;
+    /// whether to throw exceptions in case of errors
+    const bool allow_exceptions = true;
+    /// a discarded value for the callback
+    BasicJsonType discarded = BasicJsonType::value_t::discarded;
+};
+
+template<typename BasicJsonType>
+class json_sax_acceptor
+{
+  public:
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+
+    bool null()
+    {
+        return true;
+    }
+
+    bool boolean(bool /*unused*/)
+    {
+        return true;
+    }
+
+    bool number_integer(number_integer_t /*unused*/)
+    {
+        return true;
+    }
+
+    bool number_unsigned(number_unsigned_t /*unused*/)
+    {
+        return true;
+    }
+
+    bool number_float(number_float_t /*unused*/, const string_t& /*unused*/)
+    {
+        return true;
+    }
+
+    bool string(string_t& /*unused*/)
+    {
+        return true;
+    }
+
+    bool binary(binary_t& /*unused*/)
+    {
+        return true;
+    }
+
+    bool start_object(std::size_t /*unused*/ = std::size_t(-1))
+    {
+        return true;
+    }
+
+    bool key(string_t& /*unused*/)
+    {
+        return true;
+    }
+
+    bool end_object()
+    {
+        return true;
+    }
+
+    bool start_array(std::size_t /*unused*/ = std::size_t(-1))
+    {
+        return true;
+    }
+
+    bool end_array()
+    {
+        return true;
+    }
+
+    bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const detail::exception& /*unused*/)
+    {
+        return false;
+    }
+};
+}  // namespace detail
+
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/input/lexer.hpp>
+
+
+#include <array> // array
+#include <clocale> // localeconv
+#include <cstddef> // size_t
+#include <cstdio> // snprintf
+#include <cstdlib> // strtof, strtod, strtold, strtoll, strtoull
+#include <initializer_list> // initializer_list
+#include <string> // char_traits, string
+#include <utility> // move
+#include <vector> // vector
+
+// #include <nlohmann/detail/input/input_adapters.hpp>
+
+// #include <nlohmann/detail/input/position_t.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+///////////
+// lexer //
+///////////
+
+template<typename BasicJsonType>
+class lexer_base
+{
+  public:
+    /// token types for the parser
+    enum class token_type
+    {
+        uninitialized,    ///< indicating the scanner is uninitialized
+        literal_true,     ///< the `true` literal
+        literal_false,    ///< the `false` literal
+        literal_null,     ///< the `null` literal
+        value_string,     ///< a string -- use get_string() for actual value
+        value_unsigned,   ///< an unsigned integer -- use get_number_unsigned() for actual value
+        value_integer,    ///< a signed integer -- use get_number_integer() for actual value
+        value_float,      ///< an floating point number -- use get_number_float() for actual value
+        begin_array,      ///< the character for array begin `[`
+        begin_object,     ///< the character for object begin `{`
+        end_array,        ///< the character for array end `]`
+        end_object,       ///< the character for object end `}`
+        name_separator,   ///< the name separator `:`
+        value_separator,  ///< the value separator `,`
+        parse_error,      ///< indicating a parse error
+        end_of_input,     ///< indicating the end of the input buffer
+        literal_or_value  ///< a literal or the begin of a value (only for diagnostics)
+    };
+
+    /// return name of values of type token_type (only used for errors)
+    JSON_HEDLEY_RETURNS_NON_NULL
+    JSON_HEDLEY_CONST
+    static const char* token_type_name(const token_type t) noexcept
+    {
+        switch (t)
+        {
+            case token_type::uninitialized:
+                return "<uninitialized>";
+            case token_type::literal_true:
+                return "true literal";
+            case token_type::literal_false:
+                return "false literal";
+            case token_type::literal_null:
+                return "null literal";
+            case token_type::value_string:
+                return "string literal";
+            case token_type::value_unsigned:
+            case token_type::value_integer:
+            case token_type::value_float:
+                return "number literal";
+            case token_type::begin_array:
+                return "'['";
+            case token_type::begin_object:
+                return "'{'";
+            case token_type::end_array:
+                return "']'";
+            case token_type::end_object:
+                return "'}'";
+            case token_type::name_separator:
+                return "':'";
+            case token_type::value_separator:
+                return "','";
+            case token_type::parse_error:
+                return "<parse error>";
+            case token_type::end_of_input:
+                return "end of input";
+            case token_type::literal_or_value:
+                return "'[', '{', or a literal";
+            // LCOV_EXCL_START
+            default: // catch non-enum values
+                return "unknown token";
+                // LCOV_EXCL_STOP
+        }
+    }
+};
+/*!
+@brief lexical analysis
+
+This class organizes the lexical analysis during JSON deserialization.
+*/
+template<typename BasicJsonType, typename InputAdapterType>
+class lexer : public lexer_base<BasicJsonType>
+{
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using char_type = typename InputAdapterType::char_type;
+    using char_int_type = typename std::char_traits<char_type>::int_type;
+
+  public:
+    using token_type = typename lexer_base<BasicJsonType>::token_type;
+
+    explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false)
+        : ia(std::move(adapter))
+        , ignore_comments(ignore_comments_)
+        , decimal_point_char(static_cast<char_int_type>(get_decimal_point()))
+    {}
+
+    // delete because of pointer members
+    lexer(const lexer&) = delete;
+    lexer(lexer&&) = default;
+    lexer& operator=(lexer&) = delete;
+    lexer& operator=(lexer&&) = default;
+    ~lexer() = default;
+
+  private:
+    /////////////////////
+    // locales
+    /////////////////////
+
+    /// return the locale-dependent decimal point
+    JSON_HEDLEY_PURE
+    static char get_decimal_point() noexcept
+    {
+        const auto* loc = localeconv();
+        JSON_ASSERT(loc != nullptr);
+        return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point);
+    }
+
+    /////////////////////
+    // scan functions
+    /////////////////////
+
+    /*!
+    @brief get codepoint from 4 hex characters following `\u`
+
+    For input "\u c1 c2 c3 c4" the codepoint is:
+      (c1 * 0x1000) + (c2 * 0x0100) + (c3 * 0x0010) + c4
+    = (c1 << 12) + (c2 << 8) + (c3 << 4) + (c4 << 0)
+
+    Furthermore, the possible characters '0'..'9', 'A'..'F', and 'a'..'f'
+    must be converted to the integers 0x0..0x9, 0xA..0xF, 0xA..0xF, resp. The
+    conversion is done by subtracting the offset (0x30, 0x37, and 0x57)
+    between the ASCII value of the character and the desired integer value.
+
+    @return codepoint (0x0000..0xFFFF) or -1 in case of an error (e.g. EOF or
+            non-hex character)
+    */
+    int get_codepoint()
+    {
+        // this function only makes sense after reading `\u`
+        JSON_ASSERT(current == 'u');
+        int codepoint = 0;
+
+        const auto factors = { 12u, 8u, 4u, 0u };
+        for (const auto factor : factors)
+        {
+            get();
+
+            if (current >= '0' && current <= '9')
+            {
+                codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x30u) << factor);
+            }
+            else if (current >= 'A' && current <= 'F')
+            {
+                codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x37u) << factor);
+            }
+            else if (current >= 'a' && current <= 'f')
+            {
+                codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x57u) << factor);
+            }
+            else
+            {
+                return -1;
+            }
+        }
+
+        JSON_ASSERT(0x0000 <= codepoint && codepoint <= 0xFFFF);
+        return codepoint;
+    }
+
+    /*!
+    @brief check if the next byte(s) are inside a given range
+
+    Adds the current byte and, for each passed range, reads a new byte and
+    checks if it is inside the range. If a violation was detected, set up an
+    error message and return false. Otherwise, return true.
+
+    @param[in] ranges  list of integers; interpreted as list of pairs of
+                       inclusive lower and upper bound, respectively
+
+    @pre The passed list @a ranges must have 2, 4, or 6 elements; that is,
+         1, 2, or 3 pairs. This precondition is enforced by an assertion.
+
+    @return true if and only if no range violation was detected
+    */
+    bool next_byte_in_range(std::initializer_list<char_int_type> ranges)
+    {
+        JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
+        add(current);
+
+        for (auto range = ranges.begin(); range != ranges.end(); ++range)
+        {
+            get();
+            if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range)))
+            {
+                add(current);
+            }
+            else
+            {
+                error_message = "invalid string: ill-formed UTF-8 byte";
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /*!
+    @brief scan a string literal
+
+    This function scans a string according to Sect. 7 of RFC 7159. While
+    scanning, bytes are escaped and copied into buffer token_buffer. Then the
+    function returns successfully, token_buffer is *not* null-terminated (as it
+    may contain \0 bytes), and token_buffer.size() is the number of bytes in the
+    string.
+
+    @return token_type::value_string if string could be successfully scanned,
+            token_type::parse_error otherwise
+
+    @note In case of errors, variable error_message contains a textual
+          description.
+    */
+    token_type scan_string()
+    {
+        // reset token_buffer (ignore opening quote)
+        reset();
+
+        // we entered the function by reading an open quote
+        JSON_ASSERT(current == '\"');
+
+        while (true)
+        {
+            // get next character
+            switch (get())
+            {
+                // end of file while parsing string
+                case std::char_traits<char_type>::eof():
+                {
+                    error_message = "invalid string: missing closing quote";
+                    return token_type::parse_error;
+                }
+
+                // closing quote
+                case '\"':
+                {
+                    return token_type::value_string;
+                }
+
+                // escapes
+                case '\\':
+                {
+                    switch (get())
+                    {
+                        // quotation mark
+                        case '\"':
+                            add('\"');
+                            break;
+                        // reverse solidus
+                        case '\\':
+                            add('\\');
+                            break;
+                        // solidus
+                        case '/':
+                            add('/');
+                            break;
+                        // backspace
+                        case 'b':
+                            add('\b');
+                            break;
+                        // form feed
+                        case 'f':
+                            add('\f');
+                            break;
+                        // line feed
+                        case 'n':
+                            add('\n');
+                            break;
+                        // carriage return
+                        case 'r':
+                            add('\r');
+                            break;
+                        // tab
+                        case 't':
+                            add('\t');
+                            break;
+
+                        // unicode escapes
+                        case 'u':
+                        {
+                            const int codepoint1 = get_codepoint();
+                            int codepoint = codepoint1; // start with codepoint1
+
+                            if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1))
+                            {
+                                error_message = "invalid string: '\\u' must be followed by 4 hex digits";
+                                return token_type::parse_error;
+                            }
+
+                            // check if code point is a high surrogate
+                            if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF)
+                            {
+                                // expect next \uxxxx entry
+                                if (JSON_HEDLEY_LIKELY(get() == '\\' && get() == 'u'))
+                                {
+                                    const int codepoint2 = get_codepoint();
+
+                                    if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1))
+                                    {
+                                        error_message = "invalid string: '\\u' must be followed by 4 hex digits";
+                                        return token_type::parse_error;
+                                    }
+
+                                    // check if codepoint2 is a low surrogate
+                                    if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 && codepoint2 <= 0xDFFF))
+                                    {
+                                        // overwrite codepoint
+                                        codepoint = static_cast<int>(
+                                                        // high surrogate occupies the most significant 22 bits
+                                                        (static_cast<unsigned int>(codepoint1) << 10u)
+                                                        // low surrogate occupies the least significant 15 bits
+                                                        + static_cast<unsigned int>(codepoint2)
+                                                        // there is still the 0xD800, 0xDC00 and 0x10000 noise
+                                                        // in the result so we have to subtract with:
+                                                        // (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00
+                                                        - 0x35FDC00u);
+                                    }
+                                    else
+                                    {
+                                        error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
+                                        return token_type::parse_error;
+                                    }
+                                }
+                                else
+                                {
+                                    error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
+                                    return token_type::parse_error;
+                                }
+                            }
+                            else
+                            {
+                                if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF))
+                                {
+                                    error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF";
+                                    return token_type::parse_error;
+                                }
+                            }
+
+                            // result of the above calculation yields a proper codepoint
+                            JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
+
+                            // translate codepoint into bytes
+                            if (codepoint < 0x80)
+                            {
+                                // 1-byte characters: 0xxxxxxx (ASCII)
+                                add(static_cast<char_int_type>(codepoint));
+                            }
+                            else if (codepoint <= 0x7FF)
+                            {
+                                // 2-byte characters: 110xxxxx 10xxxxxx
+                                add(static_cast<char_int_type>(0xC0u | (static_cast<unsigned int>(codepoint) >> 6u)));
+                                add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
+                            }
+                            else if (codepoint <= 0xFFFF)
+                            {
+                                // 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx
+                                add(static_cast<char_int_type>(0xE0u | (static_cast<unsigned int>(codepoint) >> 12u)));
+                                add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 6u) & 0x3Fu)));
+                                add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
+                            }
+                            else
+                            {
+                                // 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+                                add(static_cast<char_int_type>(0xF0u | (static_cast<unsigned int>(codepoint) >> 18u)));
+                                add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 12u) & 0x3Fu)));
+                                add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 6u) & 0x3Fu)));
+                                add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
+                            }
+
+                            break;
+                        }
+
+                        // other characters after escape
+                        default:
+                            error_message = "invalid string: forbidden character after backslash";
+                            return token_type::parse_error;
+                    }
+
+                    break;
+                }
+
+                // invalid control characters
+                case 0x00:
+                {
+                    error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000";
+                    return token_type::parse_error;
+                }
+
+                case 0x01:
+                {
+                    error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001";
+                    return token_type::parse_error;
+                }
+
+                case 0x02:
+                {
+                    error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002";
+                    return token_type::parse_error;
+                }
+
+                case 0x03:
+                {
+                    error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003";
+                    return token_type::parse_error;
+                }
+
+                case 0x04:
+                {
+                    error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004";
+                    return token_type::parse_error;
+                }
+
+                case 0x05:
+                {
+                    error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005";
+                    return token_type::parse_error;
+                }
+
+                case 0x06:
+                {
+                    error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006";
+                    return token_type::parse_error;
+                }
+
+                case 0x07:
+                {
+                    error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007";
+                    return token_type::parse_error;
+                }
+
+                case 0x08:
+                {
+                    error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b";
+                    return token_type::parse_error;
+                }
+
+                case 0x09:
+                {
+                    error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t";
+                    return token_type::parse_error;
+                }
+
+                case 0x0A:
+                {
+                    error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n";
+                    return token_type::parse_error;
+                }
+
+                case 0x0B:
+                {
+                    error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B";
+                    return token_type::parse_error;
+                }
+
+                case 0x0C:
+                {
+                    error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f";
+                    return token_type::parse_error;
+                }
+
+                case 0x0D:
+                {
+                    error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r";
+                    return token_type::parse_error;
+                }
+
+                case 0x0E:
+                {
+                    error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E";
+                    return token_type::parse_error;
+                }
+
+                case 0x0F:
+                {
+                    error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F";
+                    return token_type::parse_error;
+                }
+
+                case 0x10:
+                {
+                    error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010";
+                    return token_type::parse_error;
+                }
+
+                case 0x11:
+                {
+                    error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011";
+                    return token_type::parse_error;
+                }
+
+                case 0x12:
+                {
+                    error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012";
+                    return token_type::parse_error;
+                }
+
+                case 0x13:
+                {
+                    error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013";
+                    return token_type::parse_error;
+                }
+
+                case 0x14:
+                {
+                    error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014";
+                    return token_type::parse_error;
+                }
+
+                case 0x15:
+                {
+                    error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015";
+                    return token_type::parse_error;
+                }
+
+                case 0x16:
+                {
+                    error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016";
+                    return token_type::parse_error;
+                }
+
+                case 0x17:
+                {
+                    error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017";
+                    return token_type::parse_error;
+                }
+
+                case 0x18:
+                {
+                    error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018";
+                    return token_type::parse_error;
+                }
+
+                case 0x19:
+                {
+                    error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019";
+                    return token_type::parse_error;
+                }
+
+                case 0x1A:
+                {
+                    error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A";
+                    return token_type::parse_error;
+                }
+
+                case 0x1B:
+                {
+                    error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B";
+                    return token_type::parse_error;
+                }
+
+                case 0x1C:
+                {
+                    error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C";
+                    return token_type::parse_error;
+                }
+
+                case 0x1D:
+                {
+                    error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D";
+                    return token_type::parse_error;
+                }
+
+                case 0x1E:
+                {
+                    error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E";
+                    return token_type::parse_error;
+                }
+
+                case 0x1F:
+                {
+                    error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F";
+                    return token_type::parse_error;
+                }
+
+                // U+0020..U+007F (except U+0022 (quote) and U+005C (backspace))
+                case 0x20:
+                case 0x21:
+                case 0x23:
+                case 0x24:
+                case 0x25:
+                case 0x26:
+                case 0x27:
+                case 0x28:
+                case 0x29:
+                case 0x2A:
+                case 0x2B:
+                case 0x2C:
+                case 0x2D:
+                case 0x2E:
+                case 0x2F:
+                case 0x30:
+                case 0x31:
+                case 0x32:
+                case 0x33:
+                case 0x34:
+                case 0x35:
+                case 0x36:
+                case 0x37:
+                case 0x38:
+                case 0x39:
+                case 0x3A:
+                case 0x3B:
+                case 0x3C:
+                case 0x3D:
+                case 0x3E:
+                case 0x3F:
+                case 0x40:
+                case 0x41:
+                case 0x42:
+                case 0x43:
+                case 0x44:
+                case 0x45:
+                case 0x46:
+                case 0x47:
+                case 0x48:
+                case 0x49:
+                case 0x4A:
+                case 0x4B:
+                case 0x4C:
+                case 0x4D:
+                case 0x4E:
+                case 0x4F:
+                case 0x50:
+                case 0x51:
+                case 0x52:
+                case 0x53:
+                case 0x54:
+                case 0x55:
+                case 0x56:
+                case 0x57:
+                case 0x58:
+                case 0x59:
+                case 0x5A:
+                case 0x5B:
+                case 0x5D:
+                case 0x5E:
+                case 0x5F:
+                case 0x60:
+                case 0x61:
+                case 0x62:
+                case 0x63:
+                case 0x64:
+                case 0x65:
+                case 0x66:
+                case 0x67:
+                case 0x68:
+                case 0x69:
+                case 0x6A:
+                case 0x6B:
+                case 0x6C:
+                case 0x6D:
+                case 0x6E:
+                case 0x6F:
+                case 0x70:
+                case 0x71:
+                case 0x72:
+                case 0x73:
+                case 0x74:
+                case 0x75:
+                case 0x76:
+                case 0x77:
+                case 0x78:
+                case 0x79:
+                case 0x7A:
+                case 0x7B:
+                case 0x7C:
+                case 0x7D:
+                case 0x7E:
+                case 0x7F:
+                {
+                    add(current);
+                    break;
+                }
+
+                // U+0080..U+07FF: bytes C2..DF 80..BF
+                case 0xC2:
+                case 0xC3:
+                case 0xC4:
+                case 0xC5:
+                case 0xC6:
+                case 0xC7:
+                case 0xC8:
+                case 0xC9:
+                case 0xCA:
+                case 0xCB:
+                case 0xCC:
+                case 0xCD:
+                case 0xCE:
+                case 0xCF:
+                case 0xD0:
+                case 0xD1:
+                case 0xD2:
+                case 0xD3:
+                case 0xD4:
+                case 0xD5:
+                case 0xD6:
+                case 0xD7:
+                case 0xD8:
+                case 0xD9:
+                case 0xDA:
+                case 0xDB:
+                case 0xDC:
+                case 0xDD:
+                case 0xDE:
+                case 0xDF:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF})))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+0800..U+0FFF: bytes E0 A0..BF 80..BF
+                case 0xE0:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+1000..U+CFFF: bytes E1..EC 80..BF 80..BF
+                // U+E000..U+FFFF: bytes EE..EF 80..BF 80..BF
+                case 0xE1:
+                case 0xE2:
+                case 0xE3:
+                case 0xE4:
+                case 0xE5:
+                case 0xE6:
+                case 0xE7:
+                case 0xE8:
+                case 0xE9:
+                case 0xEA:
+                case 0xEB:
+                case 0xEC:
+                case 0xEE:
+                case 0xEF:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+D000..U+D7FF: bytes ED 80..9F 80..BF
+                case 0xED:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
+                case 0xF0:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
+                case 0xF1:
+                case 0xF2:
+                case 0xF3:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
+                case 0xF4:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
+                    {
+                        return token_type::parse_error;
+                    }
+                    break;
+                }
+
+                // remaining bytes (80..C1 and F5..FF) are ill-formed
+                default:
+                {
+                    error_message = "invalid string: ill-formed UTF-8 byte";
+                    return token_type::parse_error;
+                }
+            }
+        }
+    }
+
+    /*!
+     * @brief scan a comment
+     * @return whether comment could be scanned successfully
+     */
+    bool scan_comment()
+    {
+        switch (get())
+        {
+            // single-line comments skip input until a newline or EOF is read
+            case '/':
+            {
+                while (true)
+                {
+                    switch (get())
+                    {
+                        case '\n':
+                        case '\r':
+                        case std::char_traits<char_type>::eof():
+                        case '\0':
+                            return true;
+
+                        default:
+                            break;
+                    }
+                }
+            }
+
+            // multi-line comments skip input until */ is read
+            case '*':
+            {
+                while (true)
+                {
+                    switch (get())
+                    {
+                        case std::char_traits<char_type>::eof():
+                        case '\0':
+                        {
+                            error_message = "invalid comment; missing closing '*/'";
+                            return false;
+                        }
+
+                        case '*':
+                        {
+                            switch (get())
+                            {
+                                case '/':
+                                    return true;
+
+                                default:
+                                {
+                                    unget();
+                                    continue;
+                                }
+                            }
+                        }
+
+                        default:
+                            continue;
+                    }
+                }
+            }
+
+            // unexpected character after reading '/'
+            default:
+            {
+                error_message = "invalid comment; expecting '/' or '*' after '/'";
+                return false;
+            }
+        }
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    static void strtof(float& f, const char* str, char** endptr) noexcept
+    {
+        f = std::strtof(str, endptr);
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    static void strtof(double& f, const char* str, char** endptr) noexcept
+    {
+        f = std::strtod(str, endptr);
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    static void strtof(long double& f, const char* str, char** endptr) noexcept
+    {
+        f = std::strtold(str, endptr);
+    }
+
+    /*!
+    @brief scan a number literal
+
+    This function scans a string according to Sect. 6 of RFC 7159.
+
+    The function is realized with a deterministic finite state machine derived
+    from the grammar described in RFC 7159. Starting in state "init", the
+    input is read and used to determined the next state. Only state "done"
+    accepts the number. State "error" is a trap state to model errors. In the
+    table below, "anything" means any character but the ones listed before.
+
+    state    | 0        | 1-9      | e E      | +       | -       | .        | anything
+    ---------|----------|----------|----------|---------|---------|----------|-----------
+    init     | zero     | any1     | [error]  | [error] | minus   | [error]  | [error]
+    minus    | zero     | any1     | [error]  | [error] | [error] | [error]  | [error]
+    zero     | done     | done     | exponent | done    | done    | decimal1 | done
+    any1     | any1     | any1     | exponent | done    | done    | decimal1 | done
+    decimal1 | decimal2 | decimal2 | [error]  | [error] | [error] | [error]  | [error]
+    decimal2 | decimal2 | decimal2 | exponent | done    | done    | done     | done
+    exponent | any2     | any2     | [error]  | sign    | sign    | [error]  | [error]
+    sign     | any2     | any2     | [error]  | [error] | [error] | [error]  | [error]
+    any2     | any2     | any2     | done     | done    | done    | done     | done
+
+    The state machine is realized with one label per state (prefixed with
+    "scan_number_") and `goto` statements between them. The state machine
+    contains cycles, but any cycle can be left when EOF is read. Therefore,
+    the function is guaranteed to terminate.
+
+    During scanning, the read bytes are stored in token_buffer. This string is
+    then converted to a signed integer, an unsigned integer, or a
+    floating-point number.
+
+    @return token_type::value_unsigned, token_type::value_integer, or
+            token_type::value_float if number could be successfully scanned,
+            token_type::parse_error otherwise
+
+    @note The scanner is independent of the current locale. Internally, the
+          locale's decimal point is used instead of `.` to work with the
+          locale-dependent converters.
+    */
+    token_type scan_number()  // lgtm [cpp/use-of-goto]
+    {
+        // reset token_buffer to store the number's bytes
+        reset();
+
+        // the type of the parsed number; initially set to unsigned; will be
+        // changed if minus sign, decimal point or exponent is read
+        token_type number_type = token_type::value_unsigned;
+
+        // state (init): we just found out we need to scan a number
+        switch (current)
+        {
+            case '-':
+            {
+                add(current);
+                goto scan_number_minus;
+            }
+
+            case '0':
+            {
+                add(current);
+                goto scan_number_zero;
+            }
+
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any1;
+            }
+
+            // all other characters are rejected outside scan_number()
+            default:            // LCOV_EXCL_LINE
+                JSON_ASSERT(false);  // LCOV_EXCL_LINE
+        }
+
+scan_number_minus:
+        // state: we just parsed a leading minus sign
+        number_type = token_type::value_integer;
+        switch (get())
+        {
+            case '0':
+            {
+                add(current);
+                goto scan_number_zero;
+            }
+
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any1;
+            }
+
+            default:
+            {
+                error_message = "invalid number; expected digit after '-'";
+                return token_type::parse_error;
+            }
+        }
+
+scan_number_zero:
+        // state: we just parse a zero (maybe with a leading minus sign)
+        switch (get())
+        {
+            case '.':
+            {
+                add(decimal_point_char);
+                goto scan_number_decimal1;
+            }
+
+            case 'e':
+            case 'E':
+            {
+                add(current);
+                goto scan_number_exponent;
+            }
+
+            default:
+                goto scan_number_done;
+        }
+
+scan_number_any1:
+        // state: we just parsed a number 0-9 (maybe with a leading minus sign)
+        switch (get())
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any1;
+            }
+
+            case '.':
+            {
+                add(decimal_point_char);
+                goto scan_number_decimal1;
+            }
+
+            case 'e':
+            case 'E':
+            {
+                add(current);
+                goto scan_number_exponent;
+            }
+
+            default:
+                goto scan_number_done;
+        }
+
+scan_number_decimal1:
+        // state: we just parsed a decimal point
+        number_type = token_type::value_float;
+        switch (get())
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_decimal2;
+            }
+
+            default:
+            {
+                error_message = "invalid number; expected digit after '.'";
+                return token_type::parse_error;
+            }
+        }
+
+scan_number_decimal2:
+        // we just parsed at least one number after a decimal point
+        switch (get())
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_decimal2;
+            }
+
+            case 'e':
+            case 'E':
+            {
+                add(current);
+                goto scan_number_exponent;
+            }
+
+            default:
+                goto scan_number_done;
+        }
+
+scan_number_exponent:
+        // we just parsed an exponent
+        number_type = token_type::value_float;
+        switch (get())
+        {
+            case '+':
+            case '-':
+            {
+                add(current);
+                goto scan_number_sign;
+            }
+
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any2;
+            }
+
+            default:
+            {
+                error_message =
+                    "invalid number; expected '+', '-', or digit after exponent";
+                return token_type::parse_error;
+            }
+        }
+
+scan_number_sign:
+        // we just parsed an exponent sign
+        switch (get())
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any2;
+            }
+
+            default:
+            {
+                error_message = "invalid number; expected digit after exponent sign";
+                return token_type::parse_error;
+            }
+        }
+
+scan_number_any2:
+        // we just parsed a number after the exponent or exponent sign
+        switch (get())
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            {
+                add(current);
+                goto scan_number_any2;
+            }
+
+            default:
+                goto scan_number_done;
+        }
+
+scan_number_done:
+        // unget the character after the number (we only read it to know that
+        // we are done scanning a number)
+        unget();
+
+        char* endptr = nullptr;
+        errno = 0;
+
+        // try to parse integers first and fall back to floats
+        if (number_type == token_type::value_unsigned)
+        {
+            const auto x = std::strtoull(token_buffer.data(), &endptr, 10);
+
+            // we checked the number format before
+            JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size());
+
+            if (errno == 0)
+            {
+                value_unsigned = static_cast<number_unsigned_t>(x);
+                if (value_unsigned == x)
+                {
+                    return token_type::value_unsigned;
+                }
+            }
+        }
+        else if (number_type == token_type::value_integer)
+        {
+            const auto x = std::strtoll(token_buffer.data(), &endptr, 10);
+
+            // we checked the number format before
+            JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size());
+
+            if (errno == 0)
+            {
+                value_integer = static_cast<number_integer_t>(x);
+                if (value_integer == x)
+                {
+                    return token_type::value_integer;
+                }
+            }
+        }
+
+        // this code is reached if we parse a floating-point number or if an
+        // integer conversion above failed
+        strtof(value_float, token_buffer.data(), &endptr);
+
+        // we checked the number format before
+        JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size());
+
+        return token_type::value_float;
+    }
+
+    /*!
+    @param[in] literal_text  the literal text to expect
+    @param[in] length        the length of the passed literal text
+    @param[in] return_type   the token type to return on success
+    */
+    JSON_HEDLEY_NON_NULL(2)
+    token_type scan_literal(const char_type* literal_text, const std::size_t length,
+                            token_type return_type)
+    {
+        JSON_ASSERT(std::char_traits<char_type>::to_char_type(current) == literal_text[0]);
+        for (std::size_t i = 1; i < length; ++i)
+        {
+            if (JSON_HEDLEY_UNLIKELY(std::char_traits<char_type>::to_char_type(get()) != literal_text[i]))
+            {
+                error_message = "invalid literal";
+                return token_type::parse_error;
+            }
+        }
+        return return_type;
+    }
+
+    /////////////////////
+    // input management
+    /////////////////////
+
+    /// reset token_buffer; current character is beginning of token
+    void reset() noexcept
+    {
+        token_buffer.clear();
+        token_string.clear();
+        token_string.push_back(std::char_traits<char_type>::to_char_type(current));
+    }
+
+    /*
+    @brief get next character from the input
+
+    This function provides the interface to the used input adapter. It does
+    not throw in case the input reached EOF, but returns a
+    `std::char_traits<char>::eof()` in that case.  Stores the scanned characters
+    for use in error messages.
+
+    @return character read from the input
+    */
+    char_int_type get()
+    {
+        ++position.chars_read_total;
+        ++position.chars_read_current_line;
+
+        if (next_unget)
+        {
+            // just reset the next_unget variable and work with current
+            next_unget = false;
+        }
+        else
+        {
+            current = ia.get_character();
+        }
+
+        if (JSON_HEDLEY_LIKELY(current != std::char_traits<char_type>::eof()))
+        {
+            token_string.push_back(std::char_traits<char_type>::to_char_type(current));
+        }
+
+        if (current == '\n')
+        {
+            ++position.lines_read;
+            position.chars_read_current_line = 0;
+        }
+
+        return current;
+    }
+
+    /*!
+    @brief unget current character (read it again on next get)
+
+    We implement unget by setting variable next_unget to true. The input is not
+    changed - we just simulate ungetting by modifying chars_read_total,
+    chars_read_current_line, and token_string. The next call to get() will
+    behave as if the unget character is read again.
+    */
+    void unget()
+    {
+        next_unget = true;
+
+        --position.chars_read_total;
+
+        // in case we "unget" a newline, we have to also decrement the lines_read
+        if (position.chars_read_current_line == 0)
+        {
+            if (position.lines_read > 0)
+            {
+                --position.lines_read;
+            }
+        }
+        else
+        {
+            --position.chars_read_current_line;
+        }
+
+        if (JSON_HEDLEY_LIKELY(current != std::char_traits<char_type>::eof()))
+        {
+            JSON_ASSERT(!token_string.empty());
+            token_string.pop_back();
+        }
+    }
+
+    /// add a character to token_buffer
+    void add(char_int_type c)
+    {
+        token_buffer.push_back(static_cast<typename string_t::value_type>(c));
+    }
+
+  public:
+    /////////////////////
+    // value getters
+    /////////////////////
+
+    /// return integer value
+    constexpr number_integer_t get_number_integer() const noexcept
+    {
+        return value_integer;
+    }
+
+    /// return unsigned integer value
+    constexpr number_unsigned_t get_number_unsigned() const noexcept
+    {
+        return value_unsigned;
+    }
+
+    /// return floating-point value
+    constexpr number_float_t get_number_float() const noexcept
+    {
+        return value_float;
+    }
+
+    /// return current string value (implicitly resets the token; useful only once)
+    string_t& get_string()
+    {
+        return token_buffer;
+    }
+
+    /////////////////////
+    // diagnostics
+    /////////////////////
+
+    /// return position of last read token
+    constexpr position_t get_position() const noexcept
+    {
+        return position;
+    }
+
+    /// return the last read token (for errors only).  Will never contain EOF
+    /// (an arbitrary value that is not a valid char value, often -1), because
+    /// 255 may legitimately occur.  May contain NUL, which should be escaped.
+    std::string get_token_string() const
+    {
+        // escape control characters
+        std::string result;
+        for (const auto c : token_string)
+        {
+            if (static_cast<unsigned char>(c) <= '\x1F')
+            {
+                // escape control characters
+                std::array<char, 9> cs{{}};
+                (std::snprintf)(cs.data(), cs.size(), "<U+%.4X>", static_cast<unsigned char>(c));
+                result += cs.data();
+            }
+            else
+            {
+                // add character as is
+                result.push_back(static_cast<std::string::value_type>(c));
+            }
+        }
+
+        return result;
+    }
+
+    /// return syntax error message
+    JSON_HEDLEY_RETURNS_NON_NULL
+    constexpr const char* get_error_message() const noexcept
+    {
+        return error_message;
+    }
+
+    /////////////////////
+    // actual scanner
+    /////////////////////
+
+    /*!
+    @brief skip the UTF-8 byte order mark
+    @return true iff there is no BOM or the correct BOM has been skipped
+    */
+    bool skip_bom()
+    {
+        if (get() == 0xEF)
+        {
+            // check if we completely parse the BOM
+            return get() == 0xBB && get() == 0xBF;
+        }
+
+        // the first character is not the beginning of the BOM; unget it to
+        // process is later
+        unget();
+        return true;
+    }
+
+    void skip_whitespace()
+    {
+        do
+        {
+            get();
+        }
+        while (current == ' ' || current == '\t' || current == '\n' || current == '\r');
+    }
+
+    token_type scan()
+    {
+        // initially, skip the BOM
+        if (position.chars_read_total == 0 && !skip_bom())
+        {
+            error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
+            return token_type::parse_error;
+        }
+
+        // read next character and ignore whitespace
+        skip_whitespace();
+
+        // ignore comments
+        while (ignore_comments && current == '/')
+        {
+            if (!scan_comment())
+            {
+                return token_type::parse_error;
+            }
+
+            // skip following whitespace
+            skip_whitespace();
+        }
+
+        switch (current)
+        {
+            // structural characters
+            case '[':
+                return token_type::begin_array;
+            case ']':
+                return token_type::end_array;
+            case '{':
+                return token_type::begin_object;
+            case '}':
+                return token_type::end_object;
+            case ':':
+                return token_type::name_separator;
+            case ',':
+                return token_type::value_separator;
+
+            // literals
+            case 't':
+            {
+                std::array<char_type, 4> true_literal = {{'t', 'r', 'u', 'e'}};
+                return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
+            }
+            case 'f':
+            {
+                std::array<char_type, 5> false_literal = {{'f', 'a', 'l', 's', 'e'}};
+                return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
+            }
+            case 'n':
+            {
+                std::array<char_type, 4> null_literal = {{'n', 'u', 'l', 'l'}};
+                return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
+            }
+
+            // string
+            case '\"':
+                return scan_string();
+
+            // number
+            case '-':
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                return scan_number();
+
+            // end of input (the null byte is needed when parsing from
+            // string literals)
+            case '\0':
+            case std::char_traits<char_type>::eof():
+                return token_type::end_of_input;
+
+            // error
+            default:
+                error_message = "invalid literal";
+                return token_type::parse_error;
+        }
+    }
+
+  private:
+    /// input adapter
+    InputAdapterType ia;
+
+    /// whether comments should be ignored (true) or signaled as errors (false)
+    const bool ignore_comments = false;
+
+    /// the current character
+    char_int_type current = std::char_traits<char_type>::eof();
+
+    /// whether the next get() call should just return current
+    bool next_unget = false;
+
+    /// the start position of the current token
+    position_t position {};
+
+    /// raw input token string (for error messages)
+    std::vector<char_type> token_string {};
+
+    /// buffer for variable-length tokens (numbers, strings)
+    string_t token_buffer {};
+
+    /// a description of occurred lexer errors
+    const char* error_message = "";
+
+    // number values
+    number_integer_t value_integer = 0;
+    number_unsigned_t value_unsigned = 0;
+    number_float_t value_float = 0;
+
+    /// the decimal point
+    const char_int_type decimal_point_char = '.';
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/is_sax.hpp>
+
+
+#include <cstdint> // size_t
+#include <utility> // declval
+#include <string> // string
+
+// #include <nlohmann/detail/meta/detected.hpp>
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename T>
+using null_function_t = decltype(std::declval<T&>().null());
+
+template<typename T>
+using boolean_function_t =
+    decltype(std::declval<T&>().boolean(std::declval<bool>()));
+
+template<typename T, typename Integer>
+using number_integer_function_t =
+    decltype(std::declval<T&>().number_integer(std::declval<Integer>()));
+
+template<typename T, typename Unsigned>
+using number_unsigned_function_t =
+    decltype(std::declval<T&>().number_unsigned(std::declval<Unsigned>()));
+
+template<typename T, typename Float, typename String>
+using number_float_function_t = decltype(std::declval<T&>().number_float(
+                                    std::declval<Float>(), std::declval<const String&>()));
+
+template<typename T, typename String>
+using string_function_t =
+    decltype(std::declval<T&>().string(std::declval<String&>()));
+
+template<typename T, typename Binary>
+using binary_function_t =
+    decltype(std::declval<T&>().binary(std::declval<Binary&>()));
+
+template<typename T>
+using start_object_function_t =
+    decltype(std::declval<T&>().start_object(std::declval<std::size_t>()));
+
+template<typename T, typename String>
+using key_function_t =
+    decltype(std::declval<T&>().key(std::declval<String&>()));
+
+template<typename T>
+using end_object_function_t = decltype(std::declval<T&>().end_object());
+
+template<typename T>
+using start_array_function_t =
+    decltype(std::declval<T&>().start_array(std::declval<std::size_t>()));
+
+template<typename T>
+using end_array_function_t = decltype(std::declval<T&>().end_array());
+
+template<typename T, typename Exception>
+using parse_error_function_t = decltype(std::declval<T&>().parse_error(
+        std::declval<std::size_t>(), std::declval<const std::string&>(),
+        std::declval<const Exception&>()));
+
+template<typename SAX, typename BasicJsonType>
+struct is_sax
+{
+  private:
+    static_assert(is_basic_json<BasicJsonType>::value,
+                  "BasicJsonType must be of type basic_json<...>");
+
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+    using exception_t = typename BasicJsonType::exception;
+
+  public:
+    static constexpr bool value =
+        is_detected_exact<bool, null_function_t, SAX>::value &&
+        is_detected_exact<bool, boolean_function_t, SAX>::value &&
+        is_detected_exact<bool, number_integer_function_t, SAX, number_integer_t>::value &&
+        is_detected_exact<bool, number_unsigned_function_t, SAX, number_unsigned_t>::value &&
+        is_detected_exact<bool, number_float_function_t, SAX, number_float_t, string_t>::value &&
+        is_detected_exact<bool, string_function_t, SAX, string_t>::value &&
+        is_detected_exact<bool, binary_function_t, SAX, binary_t>::value &&
+        is_detected_exact<bool, start_object_function_t, SAX>::value &&
+        is_detected_exact<bool, key_function_t, SAX, string_t>::value &&
+        is_detected_exact<bool, end_object_function_t, SAX>::value &&
+        is_detected_exact<bool, start_array_function_t, SAX>::value &&
+        is_detected_exact<bool, end_array_function_t, SAX>::value &&
+        is_detected_exact<bool, parse_error_function_t, SAX, exception_t>::value;
+};
+
+template<typename SAX, typename BasicJsonType>
+struct is_sax_static_asserts
+{
+  private:
+    static_assert(is_basic_json<BasicJsonType>::value,
+                  "BasicJsonType must be of type basic_json<...>");
+
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+    using exception_t = typename BasicJsonType::exception;
+
+  public:
+    static_assert(is_detected_exact<bool, null_function_t, SAX>::value,
+                  "Missing/invalid function: bool null()");
+    static_assert(is_detected_exact<bool, boolean_function_t, SAX>::value,
+                  "Missing/invalid function: bool boolean(bool)");
+    static_assert(is_detected_exact<bool, boolean_function_t, SAX>::value,
+                  "Missing/invalid function: bool boolean(bool)");
+    static_assert(
+        is_detected_exact<bool, number_integer_function_t, SAX,
+        number_integer_t>::value,
+        "Missing/invalid function: bool number_integer(number_integer_t)");
+    static_assert(
+        is_detected_exact<bool, number_unsigned_function_t, SAX,
+        number_unsigned_t>::value,
+        "Missing/invalid function: bool number_unsigned(number_unsigned_t)");
+    static_assert(is_detected_exact<bool, number_float_function_t, SAX,
+                  number_float_t, string_t>::value,
+                  "Missing/invalid function: bool number_float(number_float_t, const string_t&)");
+    static_assert(
+        is_detected_exact<bool, string_function_t, SAX, string_t>::value,
+        "Missing/invalid function: bool string(string_t&)");
+    static_assert(
+        is_detected_exact<bool, binary_function_t, SAX, binary_t>::value,
+        "Missing/invalid function: bool binary(binary_t&)");
+    static_assert(is_detected_exact<bool, start_object_function_t, SAX>::value,
+                  "Missing/invalid function: bool start_object(std::size_t)");
+    static_assert(is_detected_exact<bool, key_function_t, SAX, string_t>::value,
+                  "Missing/invalid function: bool key(string_t&)");
+    static_assert(is_detected_exact<bool, end_object_function_t, SAX>::value,
+                  "Missing/invalid function: bool end_object()");
+    static_assert(is_detected_exact<bool, start_array_function_t, SAX>::value,
+                  "Missing/invalid function: bool start_array(std::size_t)");
+    static_assert(is_detected_exact<bool, end_array_function_t, SAX>::value,
+                  "Missing/invalid function: bool end_array()");
+    static_assert(
+        is_detected_exact<bool, parse_error_function_t, SAX, exception_t>::value,
+        "Missing/invalid function: bool parse_error(std::size_t, const "
+        "std::string&, const exception&)");
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+
+/// how to treat CBOR tags
+enum class cbor_tag_handler_t
+{
+    error,  ///< throw a parse_error exception in case of a tag
+    ignore   ///< ignore tags
+};
+
+/*!
+@brief determine system byte order
+
+@return true if and only if system's byte order is little endian
+
+@note from https://stackoverflow.com/a/1001328/266378
+*/
+static inline bool little_endianess(int num = 1) noexcept
+{
+    return *reinterpret_cast<char*>(&num) == 1;
+}
+
+
+///////////////////
+// binary reader //
+///////////////////
+
+/*!
+@brief deserialization of CBOR, MessagePack, and UBJSON values
+*/
+template<typename BasicJsonType, typename InputAdapterType, typename SAX = json_sax_dom_parser<BasicJsonType>>
+class binary_reader
+{
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+    using json_sax_t = SAX;
+    using char_type = typename InputAdapterType::char_type;
+    using char_int_type = typename std::char_traits<char_type>::int_type;
+
+  public:
+    /*!
+    @brief create a binary reader
+
+    @param[in] adapter  input adapter to read from
+    */
+    explicit binary_reader(InputAdapterType&& adapter) : ia(std::move(adapter))
+    {
+        (void)detail::is_sax_static_asserts<SAX, BasicJsonType> {};
+    }
+
+    // make class move-only
+    binary_reader(const binary_reader&) = delete;
+    binary_reader(binary_reader&&) = default;
+    binary_reader& operator=(const binary_reader&) = delete;
+    binary_reader& operator=(binary_reader&&) = default;
+    ~binary_reader() = default;
+
+    /*!
+    @param[in] format  the binary format to parse
+    @param[in] sax_    a SAX event processor
+    @param[in] strict  whether to expect the input to be consumed completed
+    @param[in] tag_handler  how to treat CBOR tags
+
+    @return
+    */
+    JSON_HEDLEY_NON_NULL(3)
+    bool sax_parse(const input_format_t format,
+                   json_sax_t* sax_,
+                   const bool strict = true,
+                   const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
+    {
+        sax = sax_;
+        bool result = false;
+
+        switch (format)
+        {
+            case input_format_t::bson:
+                result = parse_bson_internal();
+                break;
+
+            case input_format_t::cbor:
+                result = parse_cbor_internal(true, tag_handler);
+                break;
+
+            case input_format_t::msgpack:
+                result = parse_msgpack_internal();
+                break;
+
+            case input_format_t::ubjson:
+                result = parse_ubjson_internal();
+                break;
+
+            default:            // LCOV_EXCL_LINE
+                JSON_ASSERT(false);  // LCOV_EXCL_LINE
+        }
+
+        // strict mode: next byte must be EOF
+        if (result && strict)
+        {
+            if (format == input_format_t::ubjson)
+            {
+                get_ignore_noop();
+            }
+            else
+            {
+                get();
+            }
+
+            if (JSON_HEDLEY_UNLIKELY(current != std::char_traits<char_type>::eof()))
+            {
+                return sax->parse_error(chars_read, get_token_string(),
+                                        parse_error::create(110, chars_read, exception_message(format, "expected end of input; last byte: 0x" + get_token_string(), "value")));
+            }
+        }
+
+        return result;
+    }
+
+  private:
+    //////////
+    // BSON //
+    //////////
+
+    /*!
+    @brief Reads in a BSON-object and passes it to the SAX-parser.
+    @return whether a valid BSON-value was passed to the SAX parser
+    */
+    bool parse_bson_internal()
+    {
+        std::int32_t document_size{};
+        get_number<std::int32_t, true>(input_format_t::bson, document_size);
+
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1))))
+        {
+            return false;
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/false)))
+        {
+            return false;
+        }
+
+        return sax->end_object();
+    }
+
+    /*!
+    @brief Parses a C-style string from the BSON input.
+    @param[in, out] result  A reference to the string variable where the read
+                            string is to be stored.
+    @return `true` if the \x00-byte indicating the end of the string was
+             encountered before the EOF; false` indicates an unexpected EOF.
+    */
+    bool get_bson_cstr(string_t& result)
+    {
+        auto out = std::back_inserter(result);
+        while (true)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "cstring")))
+            {
+                return false;
+            }
+            if (current == 0x00)
+            {
+                return true;
+            }
+            *out++ = static_cast<typename string_t::value_type>(current);
+        }
+    }
+
+    /*!
+    @brief Parses a zero-terminated string of length @a len from the BSON
+           input.
+    @param[in] len  The length (including the zero-byte at the end) of the
+                    string to be read.
+    @param[in, out] result  A reference to the string variable where the read
+                            string is to be stored.
+    @tparam NumberType The type of the length @a len
+    @pre len >= 1
+    @return `true` if the string was successfully parsed
+    */
+    template<typename NumberType>
+    bool get_bson_string(const NumberType len, string_t& result)
+    {
+        if (JSON_HEDLEY_UNLIKELY(len < 1))
+        {
+            auto last_token = get_token_string();
+            return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::bson, "string length must be at least 1, is " + std::to_string(len), "string")));
+        }
+
+        return get_string(input_format_t::bson, len - static_cast<NumberType>(1), result) && get() != std::char_traits<char_type>::eof();
+    }
+
+    /*!
+    @brief Parses a byte array input of length @a len from the BSON input.
+    @param[in] len  The length of the byte array to be read.
+    @param[in, out] result  A reference to the binary variable where the read
+                            array is to be stored.
+    @tparam NumberType The type of the length @a len
+    @pre len >= 0
+    @return `true` if the byte array was successfully parsed
+    */
+    template<typename NumberType>
+    bool get_bson_binary(const NumberType len, binary_t& result)
+    {
+        if (JSON_HEDLEY_UNLIKELY(len < 0))
+        {
+            auto last_token = get_token_string();
+            return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::bson, "byte array length cannot be negative, is " + std::to_string(len), "binary")));
+        }
+
+        // All BSON binary values have a subtype
+        std::uint8_t subtype{};
+        get_number<std::uint8_t>(input_format_t::bson, subtype);
+        result.set_subtype(subtype);
+
+        return get_binary(input_format_t::bson, len, result);
+    }
+
+    /*!
+    @brief Read a BSON document element of the given @a element_type.
+    @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
+    @param[in] element_type_parse_position The position in the input stream,
+               where the `element_type` was read.
+    @warning Not all BSON element types are supported yet. An unsupported
+             @a element_type will give rise to a parse_error.114:
+             Unsupported BSON record type 0x...
+    @return whether a valid BSON-object/array was passed to the SAX parser
+    */
+    bool parse_bson_element_internal(const char_int_type element_type,
+                                     const std::size_t element_type_parse_position)
+    {
+        switch (element_type)
+        {
+            case 0x01: // double
+            {
+                double number{};
+                return get_number<double, true>(input_format_t::bson, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 0x02: // string
+            {
+                std::int32_t len{};
+                string_t value;
+                return get_number<std::int32_t, true>(input_format_t::bson, len) && get_bson_string(len, value) && sax->string(value);
+            }
+
+            case 0x03: // object
+            {
+                return parse_bson_internal();
+            }
+
+            case 0x04: // array
+            {
+                return parse_bson_array();
+            }
+
+            case 0x05: // binary
+            {
+                std::int32_t len{};
+                binary_t value;
+                return get_number<std::int32_t, true>(input_format_t::bson, len) && get_bson_binary(len, value) && sax->binary(value);
+            }
+
+            case 0x08: // boolean
+            {
+                return sax->boolean(get() != 0);
+            }
+
+            case 0x0A: // null
+            {
+                return sax->null();
+            }
+
+            case 0x10: // int32
+            {
+                std::int32_t value{};
+                return get_number<std::int32_t, true>(input_format_t::bson, value) && sax->number_integer(value);
+            }
+
+            case 0x12: // int64
+            {
+                std::int64_t value{};
+                return get_number<std::int64_t, true>(input_format_t::bson, value) && sax->number_integer(value);
+            }
+
+            default: // anything else not supported (yet)
+            {
+                std::array<char, 3> cr{{}};
+                (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast<unsigned char>(element_type));
+                return sax->parse_error(element_type_parse_position, std::string(cr.data()), parse_error::create(114, element_type_parse_position, "Unsupported BSON record type 0x" + std::string(cr.data())));
+            }
+        }
+    }
+
+    /*!
+    @brief Read a BSON element list (as specified in the BSON-spec)
+
+    The same binary layout is used for objects and arrays, hence it must be
+    indicated with the argument @a is_array which one is expected
+    (true --> array, false --> object).
+
+    @param[in] is_array Determines if the element list being read is to be
+                        treated as an object (@a is_array == false), or as an
+                        array (@a is_array == true).
+    @return whether a valid BSON-object/array was passed to the SAX parser
+    */
+    bool parse_bson_element_list(const bool is_array)
+    {
+        string_t key;
+
+        while (auto element_type = get())
+        {
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list")))
+            {
+                return false;
+            }
+
+            const std::size_t element_type_parse_position = chars_read;
+            if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
+            {
+                return false;
+            }
+
+            if (!is_array && !sax->key(key))
+            {
+                return false;
+            }
+
+            if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position)))
+            {
+                return false;
+            }
+
+            // get_bson_cstr only appends
+            key.clear();
+        }
+
+        return true;
+    }
+
+    /*!
+    @brief Reads an array from the BSON input and passes it to the SAX-parser.
+    @return whether a valid BSON-array was passed to the SAX parser
+    */
+    bool parse_bson_array()
+    {
+        std::int32_t document_size{};
+        get_number<std::int32_t, true>(input_format_t::bson, document_size);
+
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1))))
+        {
+            return false;
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/true)))
+        {
+            return false;
+        }
+
+        return sax->end_array();
+    }
+
+    //////////
+    // CBOR //
+    //////////
+
+    /*!
+    @param[in] get_char  whether a new character should be retrieved from the
+                         input (true) or whether the last read character should
+                         be considered instead (false)
+    @param[in] tag_handler how CBOR tags should be treated
+
+    @return whether a valid CBOR value was passed to the SAX parser
+    */
+    bool parse_cbor_internal(const bool get_char,
+                             const cbor_tag_handler_t tag_handler)
+    {
+        switch (get_char ? get() : current)
+        {
+            // EOF
+            case std::char_traits<char_type>::eof():
+                return unexpect_eof(input_format_t::cbor, "value");
+
+            // Integer 0x00..0x17 (0..23)
+            case 0x00:
+            case 0x01:
+            case 0x02:
+            case 0x03:
+            case 0x04:
+            case 0x05:
+            case 0x06:
+            case 0x07:
+            case 0x08:
+            case 0x09:
+            case 0x0A:
+            case 0x0B:
+            case 0x0C:
+            case 0x0D:
+            case 0x0E:
+            case 0x0F:
+            case 0x10:
+            case 0x11:
+            case 0x12:
+            case 0x13:
+            case 0x14:
+            case 0x15:
+            case 0x16:
+            case 0x17:
+                return sax->number_unsigned(static_cast<number_unsigned_t>(current));
+
+            case 0x18: // Unsigned integer (one-byte uint8_t follows)
+            {
+                std::uint8_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_unsigned(number);
+            }
+
+            case 0x19: // Unsigned integer (two-byte uint16_t follows)
+            {
+                std::uint16_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_unsigned(number);
+            }
+
+            case 0x1A: // Unsigned integer (four-byte uint32_t follows)
+            {
+                std::uint32_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_unsigned(number);
+            }
+
+            case 0x1B: // Unsigned integer (eight-byte uint64_t follows)
+            {
+                std::uint64_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_unsigned(number);
+            }
+
+            // Negative integer -1-0x00..-1-0x17 (-1..-24)
+            case 0x20:
+            case 0x21:
+            case 0x22:
+            case 0x23:
+            case 0x24:
+            case 0x25:
+            case 0x26:
+            case 0x27:
+            case 0x28:
+            case 0x29:
+            case 0x2A:
+            case 0x2B:
+            case 0x2C:
+            case 0x2D:
+            case 0x2E:
+            case 0x2F:
+            case 0x30:
+            case 0x31:
+            case 0x32:
+            case 0x33:
+            case 0x34:
+            case 0x35:
+            case 0x36:
+            case 0x37:
+                return sax->number_integer(static_cast<std::int8_t>(0x20 - 1 - current));
+
+            case 0x38: // Negative integer (one-byte uint8_t follows)
+            {
+                std::uint8_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast<number_integer_t>(-1) - number);
+            }
+
+            case 0x39: // Negative integer -1-n (two-byte uint16_t follows)
+            {
+                std::uint16_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast<number_integer_t>(-1) - number);
+            }
+
+            case 0x3A: // Negative integer -1-n (four-byte uint32_t follows)
+            {
+                std::uint32_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast<number_integer_t>(-1) - number);
+            }
+
+            case 0x3B: // Negative integer -1-n (eight-byte uint64_t follows)
+            {
+                std::uint64_t number{};
+                return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast<number_integer_t>(-1)
+                        - static_cast<number_integer_t>(number));
+            }
+
+            // Binary data (0x00..0x17 bytes follow)
+            case 0x40:
+            case 0x41:
+            case 0x42:
+            case 0x43:
+            case 0x44:
+            case 0x45:
+            case 0x46:
+            case 0x47:
+            case 0x48:
+            case 0x49:
+            case 0x4A:
+            case 0x4B:
+            case 0x4C:
+            case 0x4D:
+            case 0x4E:
+            case 0x4F:
+            case 0x50:
+            case 0x51:
+            case 0x52:
+            case 0x53:
+            case 0x54:
+            case 0x55:
+            case 0x56:
+            case 0x57:
+            case 0x58: // Binary data (one-byte uint8_t for n follows)
+            case 0x59: // Binary data (two-byte uint16_t for n follow)
+            case 0x5A: // Binary data (four-byte uint32_t for n follow)
+            case 0x5B: // Binary data (eight-byte uint64_t for n follow)
+            case 0x5F: // Binary data (indefinite length)
+            {
+                binary_t b;
+                return get_cbor_binary(b) && sax->binary(b);
+            }
+
+            // UTF-8 string (0x00..0x17 bytes follow)
+            case 0x60:
+            case 0x61:
+            case 0x62:
+            case 0x63:
+            case 0x64:
+            case 0x65:
+            case 0x66:
+            case 0x67:
+            case 0x68:
+            case 0x69:
+            case 0x6A:
+            case 0x6B:
+            case 0x6C:
+            case 0x6D:
+            case 0x6E:
+            case 0x6F:
+            case 0x70:
+            case 0x71:
+            case 0x72:
+            case 0x73:
+            case 0x74:
+            case 0x75:
+            case 0x76:
+            case 0x77:
+            case 0x78: // UTF-8 string (one-byte uint8_t for n follows)
+            case 0x79: // UTF-8 string (two-byte uint16_t for n follow)
+            case 0x7A: // UTF-8 string (four-byte uint32_t for n follow)
+            case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow)
+            case 0x7F: // UTF-8 string (indefinite length)
+            {
+                string_t s;
+                return get_cbor_string(s) && sax->string(s);
+            }
+
+            // array (0x00..0x17 data items follow)
+            case 0x80:
+            case 0x81:
+            case 0x82:
+            case 0x83:
+            case 0x84:
+            case 0x85:
+            case 0x86:
+            case 0x87:
+            case 0x88:
+            case 0x89:
+            case 0x8A:
+            case 0x8B:
+            case 0x8C:
+            case 0x8D:
+            case 0x8E:
+            case 0x8F:
+            case 0x90:
+            case 0x91:
+            case 0x92:
+            case 0x93:
+            case 0x94:
+            case 0x95:
+            case 0x96:
+            case 0x97:
+                return get_cbor_array(static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
+
+            case 0x98: // array (one-byte uint8_t for n follows)
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0x99: // array (two-byte uint16_t for n follow)
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0x9A: // array (four-byte uint32_t for n follow)
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0x9B: // array (eight-byte uint64_t for n follow)
+            {
+                std::uint64_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0x9F: // array (indefinite length)
+                return get_cbor_array(std::size_t(-1), tag_handler);
+
+            // map (0x00..0x17 pairs of data items follow)
+            case 0xA0:
+            case 0xA1:
+            case 0xA2:
+            case 0xA3:
+            case 0xA4:
+            case 0xA5:
+            case 0xA6:
+            case 0xA7:
+            case 0xA8:
+            case 0xA9:
+            case 0xAA:
+            case 0xAB:
+            case 0xAC:
+            case 0xAD:
+            case 0xAE:
+            case 0xAF:
+            case 0xB0:
+            case 0xB1:
+            case 0xB2:
+            case 0xB3:
+            case 0xB4:
+            case 0xB5:
+            case 0xB6:
+            case 0xB7:
+                return get_cbor_object(static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
+
+            case 0xB8: // map (one-byte uint8_t for n follows)
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0xB9: // map (two-byte uint16_t for n follow)
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0xBA: // map (four-byte uint32_t for n follow)
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0xBB: // map (eight-byte uint64_t for n follow)
+            {
+                std::uint64_t len{};
+                return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler);
+            }
+
+            case 0xBF: // map (indefinite length)
+                return get_cbor_object(std::size_t(-1), tag_handler);
+
+            case 0xC6: // tagged item
+            case 0xC7:
+            case 0xC8:
+            case 0xC9:
+            case 0xCA:
+            case 0xCB:
+            case 0xCC:
+            case 0xCD:
+            case 0xCE:
+            case 0xCF:
+            case 0xD0:
+            case 0xD1:
+            case 0xD2:
+            case 0xD3:
+            case 0xD4:
+            case 0xD8: // tagged item (1 bytes follow)
+            case 0xD9: // tagged item (2 bytes follow)
+            case 0xDA: // tagged item (4 bytes follow)
+            case 0xDB: // tagged item (8 bytes follow)
+            {
+                switch (tag_handler)
+                {
+                    case cbor_tag_handler_t::error:
+                    {
+                        auto last_token = get_token_string();
+                        return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::cbor, "invalid byte: 0x" + last_token, "value")));
+                    }
+
+                    case cbor_tag_handler_t::ignore:
+                    {
+                        switch (current)
+                        {
+                            case 0xD8:
+                            {
+                                std::uint8_t len{};
+                                get_number(input_format_t::cbor, len);
+                                break;
+                            }
+                            case 0xD9:
+                            {
+                                std::uint16_t len{};
+                                get_number(input_format_t::cbor, len);
+                                break;
+                            }
+                            case 0xDA:
+                            {
+                                std::uint32_t len{};
+                                get_number(input_format_t::cbor, len);
+                                break;
+                            }
+                            case 0xDB:
+                            {
+                                std::uint64_t len{};
+                                get_number(input_format_t::cbor, len);
+                                break;
+                            }
+                            default:
+                                break;
+                        }
+                        return parse_cbor_internal(true, tag_handler);
+                    }
+
+                    default:            // LCOV_EXCL_LINE
+                        JSON_ASSERT(false);  // LCOV_EXCL_LINE
+                }
+            }
+
+            case 0xF4: // false
+                return sax->boolean(false);
+
+            case 0xF5: // true
+                return sax->boolean(true);
+
+            case 0xF6: // null
+                return sax->null();
+
+            case 0xF9: // Half-Precision Float (two-byte IEEE 754)
+            {
+                const auto byte1_raw = get();
+                if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "number")))
+                {
+                    return false;
+                }
+                const auto byte2_raw = get();
+                if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "number")))
+                {
+                    return false;
+                }
+
+                const auto byte1 = static_cast<unsigned char>(byte1_raw);
+                const auto byte2 = static_cast<unsigned char>(byte2_raw);
+
+                // code from RFC 7049, Appendix D, Figure 3:
+                // As half-precision floating-point numbers were only added
+                // to IEEE 754 in 2008, today's programming platforms often
+                // still only have limited support for them. It is very
+                // easy to include at least decoding support for them even
+                // without such support. An example of a small decoder for
+                // half-precision floating-point numbers in the C language
+                // is shown in Fig. 3.
+                const auto half = static_cast<unsigned int>((byte1 << 8u) + byte2);
+                const double val = [&half]
+                {
+                    const int exp = (half >> 10u) & 0x1Fu;
+                    const unsigned int mant = half & 0x3FFu;
+                    JSON_ASSERT(0 <= exp&& exp <= 32);
+                    JSON_ASSERT(mant <= 1024);
+                    switch (exp)
+                    {
+                        case 0:
+                            return std::ldexp(mant, -24);
+                        case 31:
+                            return (mant == 0)
+                            ? std::numeric_limits<double>::infinity()
+                            : std::numeric_limits<double>::quiet_NaN();
+                        default:
+                            return std::ldexp(mant + 1024, exp - 25);
+                    }
+                }();
+                return sax->number_float((half & 0x8000u) != 0
+                                         ? static_cast<number_float_t>(-val)
+                                         : static_cast<number_float_t>(val), "");
+            }
+
+            case 0xFA: // Single-Precision Float (four-byte IEEE 754)
+            {
+                float number{};
+                return get_number(input_format_t::cbor, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 0xFB: // Double-Precision Float (eight-byte IEEE 754)
+            {
+                double number{};
+                return get_number(input_format_t::cbor, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            default: // anything else (0xFF is handled inside the other types)
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::cbor, "invalid byte: 0x" + last_token, "value")));
+            }
+        }
+    }
+
+    /*!
+    @brief reads a CBOR string
+
+    This function first reads starting bytes to determine the expected
+    string length and then copies this number of bytes into a string.
+    Additionally, CBOR's strings with indefinite lengths are supported.
+
+    @param[out] result  created string
+
+    @return whether string creation completed
+    */
+    bool get_cbor_string(string_t& result)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "string")))
+        {
+            return false;
+        }
+
+        switch (current)
+        {
+            // UTF-8 string (0x00..0x17 bytes follow)
+            case 0x60:
+            case 0x61:
+            case 0x62:
+            case 0x63:
+            case 0x64:
+            case 0x65:
+            case 0x66:
+            case 0x67:
+            case 0x68:
+            case 0x69:
+            case 0x6A:
+            case 0x6B:
+            case 0x6C:
+            case 0x6D:
+            case 0x6E:
+            case 0x6F:
+            case 0x70:
+            case 0x71:
+            case 0x72:
+            case 0x73:
+            case 0x74:
+            case 0x75:
+            case 0x76:
+            case 0x77:
+            {
+                return get_string(input_format_t::cbor, static_cast<unsigned int>(current) & 0x1Fu, result);
+            }
+
+            case 0x78: // UTF-8 string (one-byte uint8_t for n follows)
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result);
+            }
+
+            case 0x79: // UTF-8 string (two-byte uint16_t for n follow)
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result);
+            }
+
+            case 0x7A: // UTF-8 string (four-byte uint32_t for n follow)
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result);
+            }
+
+            case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow)
+            {
+                std::uint64_t len{};
+                return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result);
+            }
+
+            case 0x7F: // UTF-8 string (indefinite length)
+            {
+                while (get() != 0xFF)
+                {
+                    string_t chunk;
+                    if (!get_cbor_string(chunk))
+                    {
+                        return false;
+                    }
+                    result.append(chunk);
+                }
+                return true;
+            }
+
+            default:
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::cbor, "expected length specification (0x60-0x7B) or indefinite string type (0x7F); last byte: 0x" + last_token, "string")));
+            }
+        }
+    }
+
+    /*!
+    @brief reads a CBOR byte array
+
+    This function first reads starting bytes to determine the expected
+    byte array length and then copies this number of bytes into the byte array.
+    Additionally, CBOR's byte arrays with indefinite lengths are supported.
+
+    @param[out] result  created byte array
+
+    @return whether byte array creation completed
+    */
+    bool get_cbor_binary(binary_t& result)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "binary")))
+        {
+            return false;
+        }
+
+        switch (current)
+        {
+            // Binary data (0x00..0x17 bytes follow)
+            case 0x40:
+            case 0x41:
+            case 0x42:
+            case 0x43:
+            case 0x44:
+            case 0x45:
+            case 0x46:
+            case 0x47:
+            case 0x48:
+            case 0x49:
+            case 0x4A:
+            case 0x4B:
+            case 0x4C:
+            case 0x4D:
+            case 0x4E:
+            case 0x4F:
+            case 0x50:
+            case 0x51:
+            case 0x52:
+            case 0x53:
+            case 0x54:
+            case 0x55:
+            case 0x56:
+            case 0x57:
+            {
+                return get_binary(input_format_t::cbor, static_cast<unsigned int>(current) & 0x1Fu, result);
+            }
+
+            case 0x58: // Binary data (one-byte uint8_t for n follows)
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::cbor, len) &&
+                       get_binary(input_format_t::cbor, len, result);
+            }
+
+            case 0x59: // Binary data (two-byte uint16_t for n follow)
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::cbor, len) &&
+                       get_binary(input_format_t::cbor, len, result);
+            }
+
+            case 0x5A: // Binary data (four-byte uint32_t for n follow)
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::cbor, len) &&
+                       get_binary(input_format_t::cbor, len, result);
+            }
+
+            case 0x5B: // Binary data (eight-byte uint64_t for n follow)
+            {
+                std::uint64_t len{};
+                return get_number(input_format_t::cbor, len) &&
+                       get_binary(input_format_t::cbor, len, result);
+            }
+
+            case 0x5F: // Binary data (indefinite length)
+            {
+                while (get() != 0xFF)
+                {
+                    binary_t chunk;
+                    if (!get_cbor_binary(chunk))
+                    {
+                        return false;
+                    }
+                    result.insert(result.end(), chunk.begin(), chunk.end());
+                }
+                return true;
+            }
+
+            default:
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::cbor, "expected length specification (0x40-0x5B) or indefinite binary array type (0x5F); last byte: 0x" + last_token, "binary")));
+            }
+        }
+    }
+
+    /*!
+    @param[in] len  the length of the array or std::size_t(-1) for an
+                    array of indefinite size
+    @param[in] tag_handler how CBOR tags should be treated
+    @return whether array creation completed
+    */
+    bool get_cbor_array(const std::size_t len,
+                        const cbor_tag_handler_t tag_handler)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len)))
+        {
+            return false;
+        }
+
+        if (len != std::size_t(-1))
+        {
+            for (std::size_t i = 0; i < len; ++i)
+            {
+                if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
+                {
+                    return false;
+                }
+            }
+        }
+        else
+        {
+            while (get() != 0xFF)
+            {
+                if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(false, tag_handler)))
+                {
+                    return false;
+                }
+            }
+        }
+
+        return sax->end_array();
+    }
+
+    /*!
+    @param[in] len  the length of the object or std::size_t(-1) for an
+                    object of indefinite size
+    @param[in] tag_handler how CBOR tags should be treated
+    @return whether object creation completed
+    */
+    bool get_cbor_object(const std::size_t len,
+                         const cbor_tag_handler_t tag_handler)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
+        {
+            return false;
+        }
+
+        string_t key;
+        if (len != std::size_t(-1))
+        {
+            for (std::size_t i = 0; i < len; ++i)
+            {
+                get();
+                if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
+                {
+                    return false;
+                }
+
+                if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
+                {
+                    return false;
+                }
+                key.clear();
+            }
+        }
+        else
+        {
+            while (get() != 0xFF)
+            {
+                if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
+                {
+                    return false;
+                }
+
+                if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
+                {
+                    return false;
+                }
+                key.clear();
+            }
+        }
+
+        return sax->end_object();
+    }
+
+    /////////////
+    // MsgPack //
+    /////////////
+
+    /*!
+    @return whether a valid MessagePack value was passed to the SAX parser
+    */
+    bool parse_msgpack_internal()
+    {
+        switch (get())
+        {
+            // EOF
+            case std::char_traits<char_type>::eof():
+                return unexpect_eof(input_format_t::msgpack, "value");
+
+            // positive fixint
+            case 0x00:
+            case 0x01:
+            case 0x02:
+            case 0x03:
+            case 0x04:
+            case 0x05:
+            case 0x06:
+            case 0x07:
+            case 0x08:
+            case 0x09:
+            case 0x0A:
+            case 0x0B:
+            case 0x0C:
+            case 0x0D:
+            case 0x0E:
+            case 0x0F:
+            case 0x10:
+            case 0x11:
+            case 0x12:
+            case 0x13:
+            case 0x14:
+            case 0x15:
+            case 0x16:
+            case 0x17:
+            case 0x18:
+            case 0x19:
+            case 0x1A:
+            case 0x1B:
+            case 0x1C:
+            case 0x1D:
+            case 0x1E:
+            case 0x1F:
+            case 0x20:
+            case 0x21:
+            case 0x22:
+            case 0x23:
+            case 0x24:
+            case 0x25:
+            case 0x26:
+            case 0x27:
+            case 0x28:
+            case 0x29:
+            case 0x2A:
+            case 0x2B:
+            case 0x2C:
+            case 0x2D:
+            case 0x2E:
+            case 0x2F:
+            case 0x30:
+            case 0x31:
+            case 0x32:
+            case 0x33:
+            case 0x34:
+            case 0x35:
+            case 0x36:
+            case 0x37:
+            case 0x38:
+            case 0x39:
+            case 0x3A:
+            case 0x3B:
+            case 0x3C:
+            case 0x3D:
+            case 0x3E:
+            case 0x3F:
+            case 0x40:
+            case 0x41:
+            case 0x42:
+            case 0x43:
+            case 0x44:
+            case 0x45:
+            case 0x46:
+            case 0x47:
+            case 0x48:
+            case 0x49:
+            case 0x4A:
+            case 0x4B:
+            case 0x4C:
+            case 0x4D:
+            case 0x4E:
+            case 0x4F:
+            case 0x50:
+            case 0x51:
+            case 0x52:
+            case 0x53:
+            case 0x54:
+            case 0x55:
+            case 0x56:
+            case 0x57:
+            case 0x58:
+            case 0x59:
+            case 0x5A:
+            case 0x5B:
+            case 0x5C:
+            case 0x5D:
+            case 0x5E:
+            case 0x5F:
+            case 0x60:
+            case 0x61:
+            case 0x62:
+            case 0x63:
+            case 0x64:
+            case 0x65:
+            case 0x66:
+            case 0x67:
+            case 0x68:
+            case 0x69:
+            case 0x6A:
+            case 0x6B:
+            case 0x6C:
+            case 0x6D:
+            case 0x6E:
+            case 0x6F:
+            case 0x70:
+            case 0x71:
+            case 0x72:
+            case 0x73:
+            case 0x74:
+            case 0x75:
+            case 0x76:
+            case 0x77:
+            case 0x78:
+            case 0x79:
+            case 0x7A:
+            case 0x7B:
+            case 0x7C:
+            case 0x7D:
+            case 0x7E:
+            case 0x7F:
+                return sax->number_unsigned(static_cast<number_unsigned_t>(current));
+
+            // fixmap
+            case 0x80:
+            case 0x81:
+            case 0x82:
+            case 0x83:
+            case 0x84:
+            case 0x85:
+            case 0x86:
+            case 0x87:
+            case 0x88:
+            case 0x89:
+            case 0x8A:
+            case 0x8B:
+            case 0x8C:
+            case 0x8D:
+            case 0x8E:
+            case 0x8F:
+                return get_msgpack_object(static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
+
+            // fixarray
+            case 0x90:
+            case 0x91:
+            case 0x92:
+            case 0x93:
+            case 0x94:
+            case 0x95:
+            case 0x96:
+            case 0x97:
+            case 0x98:
+            case 0x99:
+            case 0x9A:
+            case 0x9B:
+            case 0x9C:
+            case 0x9D:
+            case 0x9E:
+            case 0x9F:
+                return get_msgpack_array(static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
+
+            // fixstr
+            case 0xA0:
+            case 0xA1:
+            case 0xA2:
+            case 0xA3:
+            case 0xA4:
+            case 0xA5:
+            case 0xA6:
+            case 0xA7:
+            case 0xA8:
+            case 0xA9:
+            case 0xAA:
+            case 0xAB:
+            case 0xAC:
+            case 0xAD:
+            case 0xAE:
+            case 0xAF:
+            case 0xB0:
+            case 0xB1:
+            case 0xB2:
+            case 0xB3:
+            case 0xB4:
+            case 0xB5:
+            case 0xB6:
+            case 0xB7:
+            case 0xB8:
+            case 0xB9:
+            case 0xBA:
+            case 0xBB:
+            case 0xBC:
+            case 0xBD:
+            case 0xBE:
+            case 0xBF:
+            case 0xD9: // str 8
+            case 0xDA: // str 16
+            case 0xDB: // str 32
+            {
+                string_t s;
+                return get_msgpack_string(s) && sax->string(s);
+            }
+
+            case 0xC0: // nil
+                return sax->null();
+
+            case 0xC2: // false
+                return sax->boolean(false);
+
+            case 0xC3: // true
+                return sax->boolean(true);
+
+            case 0xC4: // bin 8
+            case 0xC5: // bin 16
+            case 0xC6: // bin 32
+            case 0xC7: // ext 8
+            case 0xC8: // ext 16
+            case 0xC9: // ext 32
+            case 0xD4: // fixext 1
+            case 0xD5: // fixext 2
+            case 0xD6: // fixext 4
+            case 0xD7: // fixext 8
+            case 0xD8: // fixext 16
+            {
+                binary_t b;
+                return get_msgpack_binary(b) && sax->binary(b);
+            }
+
+            case 0xCA: // float 32
+            {
+                float number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 0xCB: // float 64
+            {
+                double number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 0xCC: // uint 8
+            {
+                std::uint8_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number);
+            }
+
+            case 0xCD: // uint 16
+            {
+                std::uint16_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number);
+            }
+
+            case 0xCE: // uint 32
+            {
+                std::uint32_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number);
+            }
+
+            case 0xCF: // uint 64
+            {
+                std::uint64_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number);
+            }
+
+            case 0xD0: // int 8
+            {
+                std::int8_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_integer(number);
+            }
+
+            case 0xD1: // int 16
+            {
+                std::int16_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_integer(number);
+            }
+
+            case 0xD2: // int 32
+            {
+                std::int32_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_integer(number);
+            }
+
+            case 0xD3: // int 64
+            {
+                std::int64_t number{};
+                return get_number(input_format_t::msgpack, number) && sax->number_integer(number);
+            }
+
+            case 0xDC: // array 16
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast<std::size_t>(len));
+            }
+
+            case 0xDD: // array 32
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast<std::size_t>(len));
+            }
+
+            case 0xDE: // map 16
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast<std::size_t>(len));
+            }
+
+            case 0xDF: // map 32
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast<std::size_t>(len));
+            }
+
+            // negative fixint
+            case 0xE0:
+            case 0xE1:
+            case 0xE2:
+            case 0xE3:
+            case 0xE4:
+            case 0xE5:
+            case 0xE6:
+            case 0xE7:
+            case 0xE8:
+            case 0xE9:
+            case 0xEA:
+            case 0xEB:
+            case 0xEC:
+            case 0xED:
+            case 0xEE:
+            case 0xEF:
+            case 0xF0:
+            case 0xF1:
+            case 0xF2:
+            case 0xF3:
+            case 0xF4:
+            case 0xF5:
+            case 0xF6:
+            case 0xF7:
+            case 0xF8:
+            case 0xF9:
+            case 0xFA:
+            case 0xFB:
+            case 0xFC:
+            case 0xFD:
+            case 0xFE:
+            case 0xFF:
+                return sax->number_integer(static_cast<std::int8_t>(current));
+
+            default: // anything else
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::msgpack, "invalid byte: 0x" + last_token, "value")));
+            }
+        }
+    }
+
+    /*!
+    @brief reads a MessagePack string
+
+    This function first reads starting bytes to determine the expected
+    string length and then copies this number of bytes into a string.
+
+    @param[out] result  created string
+
+    @return whether string creation completed
+    */
+    bool get_msgpack_string(string_t& result)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::msgpack, "string")))
+        {
+            return false;
+        }
+
+        switch (current)
+        {
+            // fixstr
+            case 0xA0:
+            case 0xA1:
+            case 0xA2:
+            case 0xA3:
+            case 0xA4:
+            case 0xA5:
+            case 0xA6:
+            case 0xA7:
+            case 0xA8:
+            case 0xA9:
+            case 0xAA:
+            case 0xAB:
+            case 0xAC:
+            case 0xAD:
+            case 0xAE:
+            case 0xAF:
+            case 0xB0:
+            case 0xB1:
+            case 0xB2:
+            case 0xB3:
+            case 0xB4:
+            case 0xB5:
+            case 0xB6:
+            case 0xB7:
+            case 0xB8:
+            case 0xB9:
+            case 0xBA:
+            case 0xBB:
+            case 0xBC:
+            case 0xBD:
+            case 0xBE:
+            case 0xBF:
+            {
+                return get_string(input_format_t::msgpack, static_cast<unsigned int>(current) & 0x1Fu, result);
+            }
+
+            case 0xD9: // str 8
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result);
+            }
+
+            case 0xDA: // str 16
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result);
+            }
+
+            case 0xDB: // str 32
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result);
+            }
+
+            default:
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::msgpack, "expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x" + last_token, "string")));
+            }
+        }
+    }
+
+    /*!
+    @brief reads a MessagePack byte array
+
+    This function first reads starting bytes to determine the expected
+    byte array length and then copies this number of bytes into a byte array.
+
+    @param[out] result  created byte array
+
+    @return whether byte array creation completed
+    */
+    bool get_msgpack_binary(binary_t& result)
+    {
+        // helper function to set the subtype
+        auto assign_and_return_true = [&result](std::int8_t subtype)
+        {
+            result.set_subtype(static_cast<std::uint8_t>(subtype));
+            return true;
+        };
+
+        switch (current)
+        {
+            case 0xC4: // bin 8
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_binary(input_format_t::msgpack, len, result);
+            }
+
+            case 0xC5: // bin 16
+            {
+                std::uint16_t len{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_binary(input_format_t::msgpack, len, result);
+            }
+
+            case 0xC6: // bin 32
+            {
+                std::uint32_t len{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_binary(input_format_t::msgpack, len, result);
+            }
+
+            case 0xC7: // ext 8
+            {
+                std::uint8_t len{};
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, len, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xC8: // ext 16
+            {
+                std::uint16_t len{};
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, len, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xC9: // ext 32
+            {
+                std::uint32_t len{};
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, len) &&
+                       get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, len, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xD4: // fixext 1
+            {
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, 1, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xD5: // fixext 2
+            {
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, 2, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xD6: // fixext 4
+            {
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, 4, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xD7: // fixext 8
+            {
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, 8, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            case 0xD8: // fixext 16
+            {
+                std::int8_t subtype{};
+                return get_number(input_format_t::msgpack, subtype) &&
+                       get_binary(input_format_t::msgpack, 16, result) &&
+                       assign_and_return_true(subtype);
+            }
+
+            default:           // LCOV_EXCL_LINE
+                return false;  // LCOV_EXCL_LINE
+        }
+    }
+
+    /*!
+    @param[in] len  the length of the array
+    @return whether array creation completed
+    */
+    bool get_msgpack_array(const std::size_t len)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len)))
+        {
+            return false;
+        }
+
+        for (std::size_t i = 0; i < len; ++i)
+        {
+            if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
+            {
+                return false;
+            }
+        }
+
+        return sax->end_array();
+    }
+
+    /*!
+    @param[in] len  the length of the object
+    @return whether object creation completed
+    */
+    bool get_msgpack_object(const std::size_t len)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
+        {
+            return false;
+        }
+
+        string_t key;
+        for (std::size_t i = 0; i < len; ++i)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key)))
+            {
+                return false;
+            }
+
+            if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
+            {
+                return false;
+            }
+            key.clear();
+        }
+
+        return sax->end_object();
+    }
+
+    ////////////
+    // UBJSON //
+    ////////////
+
+    /*!
+    @param[in] get_char  whether a new character should be retrieved from the
+                         input (true, default) or whether the last read
+                         character should be considered instead
+
+    @return whether a valid UBJSON value was passed to the SAX parser
+    */
+    bool parse_ubjson_internal(const bool get_char = true)
+    {
+        return get_ubjson_value(get_char ? get_ignore_noop() : current);
+    }
+
+    /*!
+    @brief reads a UBJSON string
+
+    This function is either called after reading the 'S' byte explicitly
+    indicating a string, or in case of an object key where the 'S' byte can be
+    left out.
+
+    @param[out] result   created string
+    @param[in] get_char  whether a new character should be retrieved from the
+                         input (true, default) or whether the last read
+                         character should be considered instead
+
+    @return whether string creation completed
+    */
+    bool get_ubjson_string(string_t& result, const bool get_char = true)
+    {
+        if (get_char)
+        {
+            get();
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "value")))
+        {
+            return false;
+        }
+
+        switch (current)
+        {
+            case 'U':
+            {
+                std::uint8_t len{};
+                return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result);
+            }
+
+            case 'i':
+            {
+                std::int8_t len{};
+                return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result);
+            }
+
+            case 'I':
+            {
+                std::int16_t len{};
+                return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result);
+            }
+
+            case 'l':
+            {
+                std::int32_t len{};
+                return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result);
+            }
+
+            case 'L':
+            {
+                std::int64_t len{};
+                return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result);
+            }
+
+            default:
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L); last byte: 0x" + last_token, "string")));
+        }
+    }
+
+    /*!
+    @param[out] result  determined size
+    @return whether size determination completed
+    */
+    bool get_ubjson_size_value(std::size_t& result)
+    {
+        switch (get_ignore_noop())
+        {
+            case 'U':
+            {
+                std::uint8_t number{};
+                if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number)))
+                {
+                    return false;
+                }
+                result = static_cast<std::size_t>(number);
+                return true;
+            }
+
+            case 'i':
+            {
+                std::int8_t number{};
+                if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number)))
+                {
+                    return false;
+                }
+                result = static_cast<std::size_t>(number);
+                return true;
+            }
+
+            case 'I':
+            {
+                std::int16_t number{};
+                if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number)))
+                {
+                    return false;
+                }
+                result = static_cast<std::size_t>(number);
+                return true;
+            }
+
+            case 'l':
+            {
+                std::int32_t number{};
+                if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number)))
+                {
+                    return false;
+                }
+                result = static_cast<std::size_t>(number);
+                return true;
+            }
+
+            case 'L':
+            {
+                std::int64_t number{};
+                if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number)))
+                {
+                    return false;
+                }
+                result = static_cast<std::size_t>(number);
+                return true;
+            }
+
+            default:
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L) after '#'; last byte: 0x" + last_token, "size")));
+            }
+        }
+    }
+
+    /*!
+    @brief determine the type and size for a container
+
+    In the optimized UBJSON format, a type and a size can be provided to allow
+    for a more compact representation.
+
+    @param[out] result  pair of the size and the type
+
+    @return whether pair creation completed
+    */
+    bool get_ubjson_size_type(std::pair<std::size_t, char_int_type>& result)
+    {
+        result.first = string_t::npos; // size
+        result.second = 0; // type
+
+        get_ignore_noop();
+
+        if (current == '$')
+        {
+            result.second = get();  // must not ignore 'N', because 'N' maybe the type
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "type")))
+            {
+                return false;
+            }
+
+            get_ignore_noop();
+            if (JSON_HEDLEY_UNLIKELY(current != '#'))
+            {
+                if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "value")))
+                {
+                    return false;
+                }
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "expected '#' after type information; last byte: 0x" + last_token, "size")));
+            }
+
+            return get_ubjson_size_value(result.first);
+        }
+
+        if (current == '#')
+        {
+            return get_ubjson_size_value(result.first);
+        }
+
+        return true;
+    }
+
+    /*!
+    @param prefix  the previously read or set type prefix
+    @return whether value creation completed
+    */
+    bool get_ubjson_value(const char_int_type prefix)
+    {
+        switch (prefix)
+        {
+            case std::char_traits<char_type>::eof():  // EOF
+                return unexpect_eof(input_format_t::ubjson, "value");
+
+            case 'T':  // true
+                return sax->boolean(true);
+            case 'F':  // false
+                return sax->boolean(false);
+
+            case 'Z':  // null
+                return sax->null();
+
+            case 'U':
+            {
+                std::uint8_t number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_unsigned(number);
+            }
+
+            case 'i':
+            {
+                std::int8_t number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_integer(number);
+            }
+
+            case 'I':
+            {
+                std::int16_t number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_integer(number);
+            }
+
+            case 'l':
+            {
+                std::int32_t number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_integer(number);
+            }
+
+            case 'L':
+            {
+                std::int64_t number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_integer(number);
+            }
+
+            case 'd':
+            {
+                float number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 'D':
+            {
+                double number{};
+                return get_number(input_format_t::ubjson, number) && sax->number_float(static_cast<number_float_t>(number), "");
+            }
+
+            case 'H':
+            {
+                return get_ubjson_high_precision_number();
+            }
+
+            case 'C':  // char
+            {
+                get();
+                if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "char")))
+                {
+                    return false;
+                }
+                if (JSON_HEDLEY_UNLIKELY(current > 127))
+                {
+                    auto last_token = get_token_string();
+                    return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "byte after 'C' must be in range 0x00..0x7F; last byte: 0x" + last_token, "char")));
+                }
+                string_t s(1, static_cast<typename string_t::value_type>(current));
+                return sax->string(s);
+            }
+
+            case 'S':  // string
+            {
+                string_t s;
+                return get_ubjson_string(s) && sax->string(s);
+            }
+
+            case '[':  // array
+                return get_ubjson_array();
+
+            case '{':  // object
+                return get_ubjson_object();
+
+            default: // anything else
+            {
+                auto last_token = get_token_string();
+                return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "invalid byte: 0x" + last_token, "value")));
+            }
+        }
+    }
+
+    /*!
+    @return whether array creation completed
+    */
+    bool get_ubjson_array()
+    {
+        std::pair<std::size_t, char_int_type> size_and_type;
+        if (JSON_HEDLEY_UNLIKELY(!get_ubjson_size_type(size_and_type)))
+        {
+            return false;
+        }
+
+        if (size_and_type.first != string_t::npos)
+        {
+            if (JSON_HEDLEY_UNLIKELY(!sax->start_array(size_and_type.first)))
+            {
+                return false;
+            }
+
+            if (size_and_type.second != 0)
+            {
+                if (size_and_type.second != 'N')
+                {
+                    for (std::size_t i = 0; i < size_and_type.first; ++i)
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
+                        {
+                            return false;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                for (std::size_t i = 0; i < size_and_type.first; ++i)
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
+                    {
+                        return false;
+                    }
+                }
+            }
+        }
+        else
+        {
+            if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1))))
+            {
+                return false;
+            }
+
+            while (current != ']')
+            {
+                if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal(false)))
+                {
+                    return false;
+                }
+                get_ignore_noop();
+            }
+        }
+
+        return sax->end_array();
+    }
+
+    /*!
+    @return whether object creation completed
+    */
+    bool get_ubjson_object()
+    {
+        std::pair<std::size_t, char_int_type> size_and_type;
+        if (JSON_HEDLEY_UNLIKELY(!get_ubjson_size_type(size_and_type)))
+        {
+            return false;
+        }
+
+        string_t key;
+        if (size_and_type.first != string_t::npos)
+        {
+            if (JSON_HEDLEY_UNLIKELY(!sax->start_object(size_and_type.first)))
+            {
+                return false;
+            }
+
+            if (size_and_type.second != 0)
+            {
+                for (std::size_t i = 0; i < size_and_type.first; ++i)
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
+                    {
+                        return false;
+                    }
+                    if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
+                    {
+                        return false;
+                    }
+                    key.clear();
+                }
+            }
+            else
+            {
+                for (std::size_t i = 0; i < size_and_type.first; ++i)
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
+                    {
+                        return false;
+                    }
+                    if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
+                    {
+                        return false;
+                    }
+                    key.clear();
+                }
+            }
+        }
+        else
+        {
+            if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1))))
+            {
+                return false;
+            }
+
+            while (current != '}')
+            {
+                if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key)))
+                {
+                    return false;
+                }
+                if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
+                {
+                    return false;
+                }
+                get_ignore_noop();
+                key.clear();
+            }
+        }
+
+        return sax->end_object();
+    }
+
+    // Note, no reader for UBJSON binary types is implemented because they do
+    // not exist
+
+    bool get_ubjson_high_precision_number()
+    {
+        // get size of following number string
+        std::size_t size{};
+        auto res = get_ubjson_size_value(size);
+        if (JSON_HEDLEY_UNLIKELY(!res))
+        {
+            return res;
+        }
+
+        // get number string
+        std::vector<char> number_vector;
+        for (std::size_t i = 0; i < size; ++i)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "number")))
+            {
+                return false;
+            }
+            number_vector.push_back(static_cast<char>(current));
+        }
+
+        // parse number string
+        auto number_ia = detail::input_adapter(std::forward<decltype(number_vector)>(number_vector));
+        auto number_lexer = detail::lexer<BasicJsonType, decltype(number_ia)>(std::move(number_ia), false);
+        const auto result_number = number_lexer.scan();
+        const auto number_string = number_lexer.get_token_string();
+        const auto result_remainder = number_lexer.scan();
+
+        using token_type = typename detail::lexer_base<BasicJsonType>::token_type;
+
+        if (JSON_HEDLEY_UNLIKELY(result_remainder != token_type::end_of_input))
+        {
+            return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, exception_message(input_format_t::ubjson, "invalid number text: " + number_lexer.get_token_string(), "high-precision number")));
+        }
+
+        switch (result_number)
+        {
+            case token_type::value_integer:
+                return sax->number_integer(number_lexer.get_number_integer());
+            case token_type::value_unsigned:
+                return sax->number_unsigned(number_lexer.get_number_unsigned());
+            case token_type::value_float:
+                return sax->number_float(number_lexer.get_number_float(), std::move(number_string));
+            default:
+                return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, exception_message(input_format_t::ubjson, "invalid number text: " + number_lexer.get_token_string(), "high-precision number")));
+        }
+    }
+
+    ///////////////////////
+    // Utility functions //
+    ///////////////////////
+
+    /*!
+    @brief get next character from the input
+
+    This function provides the interface to the used input adapter. It does
+    not throw in case the input reached EOF, but returns a -'ve valued
+    `std::char_traits<char_type>::eof()` in that case.
+
+    @return character read from the input
+    */
+    char_int_type get()
+    {
+        ++chars_read;
+        return current = ia.get_character();
+    }
+
+    /*!
+    @return character read from the input after ignoring all 'N' entries
+    */
+    char_int_type get_ignore_noop()
+    {
+        do
+        {
+            get();
+        }
+        while (current == 'N');
+
+        return current;
+    }
+
+    /*
+    @brief read a number from the input
+
+    @tparam NumberType the type of the number
+    @param[in] format   the current format (for diagnostics)
+    @param[out] result  number of type @a NumberType
+
+    @return whether conversion completed
+
+    @note This function needs to respect the system's endianess, because
+          bytes in CBOR, MessagePack, and UBJSON are stored in network order
+          (big endian) and therefore need reordering on little endian systems.
+    */
+    template<typename NumberType, bool InputIsLittleEndian = false>
+    bool get_number(const input_format_t format, NumberType& result)
+    {
+        // step 1: read input into array with system's byte order
+        std::array<std::uint8_t, sizeof(NumberType)> vec;
+        for (std::size_t i = 0; i < sizeof(NumberType); ++i)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "number")))
+            {
+                return false;
+            }
+
+            // reverse byte order prior to conversion if necessary
+            if (is_little_endian != InputIsLittleEndian)
+            {
+                vec[sizeof(NumberType) - i - 1] = static_cast<std::uint8_t>(current);
+            }
+            else
+            {
+                vec[i] = static_cast<std::uint8_t>(current); // LCOV_EXCL_LINE
+            }
+        }
+
+        // step 2: convert array into number of type T and return
+        std::memcpy(&result, vec.data(), sizeof(NumberType));
+        return true;
+    }
+
+    /*!
+    @brief create a string by reading characters from the input
+
+    @tparam NumberType the type of the number
+    @param[in] format the current format (for diagnostics)
+    @param[in] len number of characters to read
+    @param[out] result string created by reading @a len bytes
+
+    @return whether string creation completed
+
+    @note We can not reserve @a len bytes for the result, because @a len
+          may be too large. Usually, @ref unexpect_eof() detects the end of
+          the input before we run out of string memory.
+    */
+    template<typename NumberType>
+    bool get_string(const input_format_t format,
+                    const NumberType len,
+                    string_t& result)
+    {
+        bool success = true;
+        for (NumberType i = 0; i < len; i++)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string")))
+            {
+                success = false;
+                break;
+            }
+            result.push_back(static_cast<typename string_t::value_type>(current));
+        };
+        return success;
+    }
+
+    /*!
+    @brief create a byte array by reading bytes from the input
+
+    @tparam NumberType the type of the number
+    @param[in] format the current format (for diagnostics)
+    @param[in] len number of bytes to read
+    @param[out] result byte array created by reading @a len bytes
+
+    @return whether byte array creation completed
+
+    @note We can not reserve @a len bytes for the result, because @a len
+          may be too large. Usually, @ref unexpect_eof() detects the end of
+          the input before we run out of memory.
+    */
+    template<typename NumberType>
+    bool get_binary(const input_format_t format,
+                    const NumberType len,
+                    binary_t& result)
+    {
+        bool success = true;
+        for (NumberType i = 0; i < len; i++)
+        {
+            get();
+            if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
+            {
+                success = false;
+                break;
+            }
+            result.push_back(static_cast<std::uint8_t>(current));
+        }
+        return success;
+    }
+
+    /*!
+    @param[in] format   the current format (for diagnostics)
+    @param[in] context  further context information (for diagnostics)
+    @return whether the last read character is not EOF
+    */
+    JSON_HEDLEY_NON_NULL(3)
+    bool unexpect_eof(const input_format_t format, const char* context) const
+    {
+        if (JSON_HEDLEY_UNLIKELY(current == std::char_traits<char_type>::eof()))
+        {
+            return sax->parse_error(chars_read, "<end of file>",
+                                    parse_error::create(110, chars_read, exception_message(format, "unexpected end of input", context)));
+        }
+        return true;
+    }
+
+    /*!
+    @return a string representation of the last read byte
+    */
+    std::string get_token_string() const
+    {
+        std::array<char, 3> cr{{}};
+        (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast<unsigned char>(current));
+        return std::string{cr.data()};
+    }
+
+    /*!
+    @param[in] format   the current format
+    @param[in] detail   a detailed error message
+    @param[in] context  further context information
+    @return a message string to use in the parse_error exceptions
+    */
+    std::string exception_message(const input_format_t format,
+                                  const std::string& detail,
+                                  const std::string& context) const
+    {
+        std::string error_msg = "syntax error while parsing ";
+
+        switch (format)
+        {
+            case input_format_t::cbor:
+                error_msg += "CBOR";
+                break;
+
+            case input_format_t::msgpack:
+                error_msg += "MessagePack";
+                break;
+
+            case input_format_t::ubjson:
+                error_msg += "UBJSON";
+                break;
+
+            case input_format_t::bson:
+                error_msg += "BSON";
+                break;
+
+            default:            // LCOV_EXCL_LINE
+                JSON_ASSERT(false);  // LCOV_EXCL_LINE
+        }
+
+        return error_msg + " " + context + ": " + detail;
+    }
+
+  private:
+    /// input adapter
+    InputAdapterType ia;
+
+    /// the current character
+    char_int_type current = std::char_traits<char_type>::eof();
+
+    /// the number of characters read
+    std::size_t chars_read = 0;
+
+    /// whether we can assume little endianess
+    const bool is_little_endian = little_endianess();
+
+    /// the SAX parser
+    json_sax_t* sax = nullptr;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/input/input_adapters.hpp>
+
+// #include <nlohmann/detail/input/lexer.hpp>
+
+// #include <nlohmann/detail/input/parser.hpp>
+
+
+#include <cmath> // isfinite
+#include <cstdint> // uint8_t
+#include <functional> // function
+#include <string> // string
+#include <utility> // move
+#include <vector> // vector
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/input/input_adapters.hpp>
+
+// #include <nlohmann/detail/input/json_sax.hpp>
+
+// #include <nlohmann/detail/input/lexer.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/is_sax.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+////////////
+// parser //
+////////////
+
+enum class parse_event_t : uint8_t
+{
+    /// the parser read `{` and started to process a JSON object
+    object_start,
+    /// the parser read `}` and finished processing a JSON object
+    object_end,
+    /// the parser read `[` and started to process a JSON array
+    array_start,
+    /// the parser read `]` and finished processing a JSON array
+    array_end,
+    /// the parser read a key of a value in an object
+    key,
+    /// the parser finished reading a JSON value
+    value
+};
+
+template<typename BasicJsonType>
+using parser_callback_t =
+    std::function<bool(int depth, parse_event_t event, BasicJsonType& parsed)>;
+
+/*!
+@brief syntax analysis
+
+This class implements a recursive descent parser.
+*/
+template<typename BasicJsonType, typename InputAdapterType>
+class parser
+{
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using string_t = typename BasicJsonType::string_t;
+    using lexer_t = lexer<BasicJsonType, InputAdapterType>;
+    using token_type = typename lexer_t::token_type;
+
+  public:
+    /// a parser reading from an input adapter
+    explicit parser(InputAdapterType&& adapter,
+                    const parser_callback_t<BasicJsonType> cb = nullptr,
+                    const bool allow_exceptions_ = true,
+                    const bool skip_comments = false)
+        : callback(cb)
+        , m_lexer(std::move(adapter), skip_comments)
+        , allow_exceptions(allow_exceptions_)
+    {
+        // read first token
+        get_token();
+    }
+
+    /*!
+    @brief public parser interface
+
+    @param[in] strict      whether to expect the last token to be EOF
+    @param[in,out] result  parsed JSON value
+
+    @throw parse_error.101 in case of an unexpected token
+    @throw parse_error.102 if to_unicode fails or surrogate error
+    @throw parse_error.103 if to_unicode fails
+    */
+    void parse(const bool strict, BasicJsonType& result)
+    {
+        if (callback)
+        {
+            json_sax_dom_callback_parser<BasicJsonType> sdp(result, callback, allow_exceptions);
+            sax_parse_internal(&sdp);
+            result.assert_invariant();
+
+            // in strict mode, input must be completely read
+            if (strict && (get_token() != token_type::end_of_input))
+            {
+                sdp.parse_error(m_lexer.get_position(),
+                                m_lexer.get_token_string(),
+                                parse_error::create(101, m_lexer.get_position(),
+                                                    exception_message(token_type::end_of_input, "value")));
+            }
+
+            // in case of an error, return discarded value
+            if (sdp.is_errored())
+            {
+                result = value_t::discarded;
+                return;
+            }
+
+            // set top-level value to null if it was discarded by the callback
+            // function
+            if (result.is_discarded())
+            {
+                result = nullptr;
+            }
+        }
+        else
+        {
+            json_sax_dom_parser<BasicJsonType> sdp(result, allow_exceptions);
+            sax_parse_internal(&sdp);
+            result.assert_invariant();
+
+            // in strict mode, input must be completely read
+            if (strict && (get_token() != token_type::end_of_input))
+            {
+                sdp.parse_error(m_lexer.get_position(),
+                                m_lexer.get_token_string(),
+                                parse_error::create(101, m_lexer.get_position(),
+                                                    exception_message(token_type::end_of_input, "value")));
+            }
+
+            // in case of an error, return discarded value
+            if (sdp.is_errored())
+            {
+                result = value_t::discarded;
+                return;
+            }
+        }
+    }
+
+    /*!
+    @brief public accept interface
+
+    @param[in] strict  whether to expect the last token to be EOF
+    @return whether the input is a proper JSON text
+    */
+    bool accept(const bool strict = true)
+    {
+        json_sax_acceptor<BasicJsonType> sax_acceptor;
+        return sax_parse(&sax_acceptor, strict);
+    }
+
+    template<typename SAX>
+    JSON_HEDLEY_NON_NULL(2)
+    bool sax_parse(SAX* sax, const bool strict = true)
+    {
+        (void)detail::is_sax_static_asserts<SAX, BasicJsonType> {};
+        const bool result = sax_parse_internal(sax);
+
+        // strict mode: next byte must be EOF
+        if (result && strict && (get_token() != token_type::end_of_input))
+        {
+            return sax->parse_error(m_lexer.get_position(),
+                                    m_lexer.get_token_string(),
+                                    parse_error::create(101, m_lexer.get_position(),
+                                            exception_message(token_type::end_of_input, "value")));
+        }
+
+        return result;
+    }
+
+  private:
+    template<typename SAX>
+    JSON_HEDLEY_NON_NULL(2)
+    bool sax_parse_internal(SAX* sax)
+    {
+        // stack to remember the hierarchy of structured values we are parsing
+        // true = array; false = object
+        std::vector<bool> states;
+        // value to avoid a goto (see comment where set to true)
+        bool skip_to_state_evaluation = false;
+
+        while (true)
+        {
+            if (!skip_to_state_evaluation)
+            {
+                // invariant: get_token() was called before each iteration
+                switch (last_token)
+                {
+                    case token_type::begin_object:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1))))
+                        {
+                            return false;
+                        }
+
+                        // closing } -> we are done
+                        if (get_token() == token_type::end_object)
+                        {
+                            if (JSON_HEDLEY_UNLIKELY(!sax->end_object()))
+                            {
+                                return false;
+                            }
+                            break;
+                        }
+
+                        // parse key
+                        if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
+                        {
+                            return sax->parse_error(m_lexer.get_position(),
+                                                    m_lexer.get_token_string(),
+                                                    parse_error::create(101, m_lexer.get_position(),
+                                                            exception_message(token_type::value_string, "object key")));
+                        }
+                        if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
+                        {
+                            return false;
+                        }
+
+                        // parse separator (:)
+                        if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator))
+                        {
+                            return sax->parse_error(m_lexer.get_position(),
+                                                    m_lexer.get_token_string(),
+                                                    parse_error::create(101, m_lexer.get_position(),
+                                                            exception_message(token_type::name_separator, "object separator")));
+                        }
+
+                        // remember we are now inside an object
+                        states.push_back(false);
+
+                        // parse values
+                        get_token();
+                        continue;
+                    }
+
+                    case token_type::begin_array:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1))))
+                        {
+                            return false;
+                        }
+
+                        // closing ] -> we are done
+                        if (get_token() == token_type::end_array)
+                        {
+                            if (JSON_HEDLEY_UNLIKELY(!sax->end_array()))
+                            {
+                                return false;
+                            }
+                            break;
+                        }
+
+                        // remember we are now inside an array
+                        states.push_back(true);
+
+                        // parse values (no need to call get_token)
+                        continue;
+                    }
+
+                    case token_type::value_float:
+                    {
+                        const auto res = m_lexer.get_number_float();
+
+                        if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res)))
+                        {
+                            return sax->parse_error(m_lexer.get_position(),
+                                                    m_lexer.get_token_string(),
+                                                    out_of_range::create(406, "number overflow parsing '" + m_lexer.get_token_string() + "'"));
+                        }
+
+                        if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string())))
+                        {
+                            return false;
+                        }
+
+                        break;
+                    }
+
+                    case token_type::literal_false:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->boolean(false)))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::literal_null:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->null()))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::literal_true:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->boolean(true)))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::value_integer:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->number_integer(m_lexer.get_number_integer())))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::value_string:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string())))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::value_unsigned:
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(m_lexer.get_number_unsigned())))
+                        {
+                            return false;
+                        }
+                        break;
+                    }
+
+                    case token_type::parse_error:
+                    {
+                        // using "uninitialized" to avoid "expected" message
+                        return sax->parse_error(m_lexer.get_position(),
+                                                m_lexer.get_token_string(),
+                                                parse_error::create(101, m_lexer.get_position(),
+                                                        exception_message(token_type::uninitialized, "value")));
+                    }
+
+                    default: // the last token was unexpected
+                    {
+                        return sax->parse_error(m_lexer.get_position(),
+                                                m_lexer.get_token_string(),
+                                                parse_error::create(101, m_lexer.get_position(),
+                                                        exception_message(token_type::literal_or_value, "value")));
+                    }
+                }
+            }
+            else
+            {
+                skip_to_state_evaluation = false;
+            }
+
+            // we reached this line after we successfully parsed a value
+            if (states.empty())
+            {
+                // empty stack: we reached the end of the hierarchy: done
+                return true;
+            }
+
+            if (states.back())  // array
+            {
+                // comma -> next value
+                if (get_token() == token_type::value_separator)
+                {
+                    // parse a new value
+                    get_token();
+                    continue;
+                }
+
+                // closing ]
+                if (JSON_HEDLEY_LIKELY(last_token == token_type::end_array))
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!sax->end_array()))
+                    {
+                        return false;
+                    }
+
+                    // We are done with this array. Before we can parse a
+                    // new value, we need to evaluate the new state first.
+                    // By setting skip_to_state_evaluation to false, we
+                    // are effectively jumping to the beginning of this if.
+                    JSON_ASSERT(!states.empty());
+                    states.pop_back();
+                    skip_to_state_evaluation = true;
+                    continue;
+                }
+
+                return sax->parse_error(m_lexer.get_position(),
+                                        m_lexer.get_token_string(),
+                                        parse_error::create(101, m_lexer.get_position(),
+                                                exception_message(token_type::end_array, "array")));
+            }
+            else  // object
+            {
+                // comma -> next value
+                if (get_token() == token_type::value_separator)
+                {
+                    // parse key
+                    if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::value_string))
+                    {
+                        return sax->parse_error(m_lexer.get_position(),
+                                                m_lexer.get_token_string(),
+                                                parse_error::create(101, m_lexer.get_position(),
+                                                        exception_message(token_type::value_string, "object key")));
+                    }
+
+                    if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
+                    {
+                        return false;
+                    }
+
+                    // parse separator (:)
+                    if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator))
+                    {
+                        return sax->parse_error(m_lexer.get_position(),
+                                                m_lexer.get_token_string(),
+                                                parse_error::create(101, m_lexer.get_position(),
+                                                        exception_message(token_type::name_separator, "object separator")));
+                    }
+
+                    // parse values
+                    get_token();
+                    continue;
+                }
+
+                // closing }
+                if (JSON_HEDLEY_LIKELY(last_token == token_type::end_object))
+                {
+                    if (JSON_HEDLEY_UNLIKELY(!sax->end_object()))
+                    {
+                        return false;
+                    }
+
+                    // We are done with this object. Before we can parse a
+                    // new value, we need to evaluate the new state first.
+                    // By setting skip_to_state_evaluation to false, we
+                    // are effectively jumping to the beginning of this if.
+                    JSON_ASSERT(!states.empty());
+                    states.pop_back();
+                    skip_to_state_evaluation = true;
+                    continue;
+                }
+
+                return sax->parse_error(m_lexer.get_position(),
+                                        m_lexer.get_token_string(),
+                                        parse_error::create(101, m_lexer.get_position(),
+                                                exception_message(token_type::end_object, "object")));
+            }
+        }
+    }
+
+    /// get next token from lexer
+    token_type get_token()
+    {
+        return last_token = m_lexer.scan();
+    }
+
+    std::string exception_message(const token_type expected, const std::string& context)
+    {
+        std::string error_msg = "syntax error ";
+
+        if (!context.empty())
+        {
+            error_msg += "while parsing " + context + " ";
+        }
+
+        error_msg += "- ";
+
+        if (last_token == token_type::parse_error)
+        {
+            error_msg += std::string(m_lexer.get_error_message()) + "; last read: '" +
+                         m_lexer.get_token_string() + "'";
+        }
+        else
+        {
+            error_msg += "unexpected " + std::string(lexer_t::token_type_name(last_token));
+        }
+
+        if (expected != token_type::uninitialized)
+        {
+            error_msg += "; expected " + std::string(lexer_t::token_type_name(expected));
+        }
+
+        return error_msg;
+    }
+
+  private:
+    /// callback function
+    const parser_callback_t<BasicJsonType> callback = nullptr;
+    /// the type of the last read token
+    token_type last_token = token_type::uninitialized;
+    /// the lexer
+    lexer_t m_lexer;
+    /// whether to throw exceptions in case of errors
+    const bool allow_exceptions = true;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/iterators/internal_iterator.hpp>
+
+
+// #include <nlohmann/detail/iterators/primitive_iterator.hpp>
+
+
+#include <cstddef> // ptrdiff_t
+#include <limits>  // numeric_limits
+
+namespace nlohmann
+{
+namespace detail
+{
+/*
+@brief an iterator for primitive JSON types
+
+This class models an iterator for primitive JSON types (boolean, number,
+string). It's only purpose is to allow the iterator/const_iterator classes
+to "iterate" over primitive values. Internally, the iterator is modeled by
+a `difference_type` variable. Value begin_value (`0`) models the begin,
+end_value (`1`) models past the end.
+*/
+class primitive_iterator_t
+{
+  private:
+    using difference_type = std::ptrdiff_t;
+    static constexpr difference_type begin_value = 0;
+    static constexpr difference_type end_value = begin_value + 1;
+
+    /// iterator as signed integer type
+    difference_type m_it = (std::numeric_limits<std::ptrdiff_t>::min)();
+
+  public:
+    constexpr difference_type get_value() const noexcept
+    {
+        return m_it;
+    }
+
+    /// set iterator to a defined beginning
+    void set_begin() noexcept
+    {
+        m_it = begin_value;
+    }
+
+    /// set iterator to a defined past the end
+    void set_end() noexcept
+    {
+        m_it = end_value;
+    }
+
+    /// return whether the iterator can be dereferenced
+    constexpr bool is_begin() const noexcept
+    {
+        return m_it == begin_value;
+    }
+
+    /// return whether the iterator is at end
+    constexpr bool is_end() const noexcept
+    {
+        return m_it == end_value;
+    }
+
+    friend constexpr bool operator==(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept
+    {
+        return lhs.m_it == rhs.m_it;
+    }
+
+    friend constexpr bool operator<(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept
+    {
+        return lhs.m_it < rhs.m_it;
+    }
+
+    primitive_iterator_t operator+(difference_type n) noexcept
+    {
+        auto result = *this;
+        result += n;
+        return result;
+    }
+
+    friend constexpr difference_type operator-(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept
+    {
+        return lhs.m_it - rhs.m_it;
+    }
+
+    primitive_iterator_t& operator++() noexcept
+    {
+        ++m_it;
+        return *this;
+    }
+
+    primitive_iterator_t const operator++(int) noexcept
+    {
+        auto result = *this;
+        ++m_it;
+        return result;
+    }
+
+    primitive_iterator_t& operator--() noexcept
+    {
+        --m_it;
+        return *this;
+    }
+
+    primitive_iterator_t const operator--(int) noexcept
+    {
+        auto result = *this;
+        --m_it;
+        return result;
+    }
+
+    primitive_iterator_t& operator+=(difference_type n) noexcept
+    {
+        m_it += n;
+        return *this;
+    }
+
+    primitive_iterator_t& operator-=(difference_type n) noexcept
+    {
+        m_it -= n;
+        return *this;
+    }
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+
+namespace nlohmann
+{
+namespace detail
+{
+/*!
+@brief an iterator value
+
+@note This structure could easily be a union, but MSVC currently does not allow
+unions members with complex constructors, see https://github.com/nlohmann/json/pull/105.
+*/
+template<typename BasicJsonType> struct internal_iterator
+{
+    /// iterator for JSON objects
+    typename BasicJsonType::object_t::iterator object_iterator {};
+    /// iterator for JSON arrays
+    typename BasicJsonType::array_t::iterator array_iterator {};
+    /// generic iterator for all other types
+    primitive_iterator_t primitive_iterator {};
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/iterators/iter_impl.hpp>
+
+
+#include <iterator> // iterator, random_access_iterator_tag, bidirectional_iterator_tag, advance, next
+#include <type_traits> // conditional, is_const, remove_const
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/iterators/internal_iterator.hpp>
+
+// #include <nlohmann/detail/iterators/primitive_iterator.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+// forward declare, to be able to friend it later on
+template<typename IteratorType> class iteration_proxy;
+template<typename IteratorType> class iteration_proxy_value;
+
+/*!
+@brief a template for a bidirectional iterator for the @ref basic_json class
+This class implements a both iterators (iterator and const_iterator) for the
+@ref basic_json class.
+@note An iterator is called *initialized* when a pointer to a JSON value has
+      been set (e.g., by a constructor or a copy assignment). If the iterator is
+      default-constructed, it is *uninitialized* and most methods are undefined.
+      **The library uses assertions to detect calls on uninitialized iterators.**
+@requirement The class satisfies the following concept requirements:
+-
+[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator):
+  The iterator that can be moved can be moved in both directions (i.e.
+  incremented and decremented).
+@since version 1.0.0, simplified in version 2.0.9, change to bidirectional
+       iterators in version 3.0.0 (see https://github.com/nlohmann/json/issues/593)
+*/
+template<typename BasicJsonType>
+class iter_impl
+{
+    /// allow basic_json to access private members
+    friend iter_impl<typename std::conditional<std::is_const<BasicJsonType>::value, typename std::remove_const<BasicJsonType>::type, const BasicJsonType>::type>;
+    friend BasicJsonType;
+    friend iteration_proxy<iter_impl>;
+    friend iteration_proxy_value<iter_impl>;
+
+    using object_t = typename BasicJsonType::object_t;
+    using array_t = typename BasicJsonType::array_t;
+    // make sure BasicJsonType is basic_json or const basic_json
+    static_assert(is_basic_json<typename std::remove_const<BasicJsonType>::type>::value,
+                  "iter_impl only accepts (const) basic_json");
+
+  public:
+
+    /// The std::iterator class template (used as a base class to provide typedefs) is deprecated in C++17.
+    /// The C++ Standard has never required user-defined iterators to derive from std::iterator.
+    /// A user-defined iterator should provide publicly accessible typedefs named
+    /// iterator_category, value_type, difference_type, pointer, and reference.
+    /// Note that value_type is required to be non-const, even for constant iterators.
+    using iterator_category = std::bidirectional_iterator_tag;
+
+    /// the type of the values when the iterator is dereferenced
+    using value_type = typename BasicJsonType::value_type;
+    /// a type to represent differences between iterators
+    using difference_type = typename BasicJsonType::difference_type;
+    /// defines a pointer to the type iterated over (value_type)
+    using pointer = typename std::conditional<std::is_const<BasicJsonType>::value,
+          typename BasicJsonType::const_pointer,
+          typename BasicJsonType::pointer>::type;
+    /// defines a reference to the type iterated over (value_type)
+    using reference =
+        typename std::conditional<std::is_const<BasicJsonType>::value,
+        typename BasicJsonType::const_reference,
+        typename BasicJsonType::reference>::type;
+
+    /// default constructor
+    iter_impl() = default;
+
+    /*!
+    @brief constructor for a given JSON instance
+    @param[in] object  pointer to a JSON object for this iterator
+    @pre object != nullptr
+    @post The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    explicit iter_impl(pointer object) noexcept : m_object(object)
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                m_it.object_iterator = typename object_t::iterator();
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_it.array_iterator = typename array_t::iterator();
+                break;
+            }
+
+            default:
+            {
+                m_it.primitive_iterator = primitive_iterator_t();
+                break;
+            }
+        }
+    }
+
+    /*!
+    @note The conventional copy constructor and copy assignment are implicitly
+          defined. Combined with the following converting constructor and
+          assignment, they support: (1) copy from iterator to iterator, (2)
+          copy from const iterator to const iterator, and (3) conversion from
+          iterator to const iterator. However conversion from const iterator
+          to iterator is not defined.
+    */
+
+    /*!
+    @brief const copy constructor
+    @param[in] other const iterator to copy from
+    @note This copy constructor had to be defined explicitly to circumvent a bug
+          occurring on msvc v19.0 compiler (VS 2015) debug build. For more
+          information refer to: https://github.com/nlohmann/json/issues/1608
+    */
+    iter_impl(const iter_impl<const BasicJsonType>& other) noexcept
+        : m_object(other.m_object), m_it(other.m_it)
+    {}
+
+    /*!
+    @brief converting assignment
+    @param[in] other const iterator to copy from
+    @return const/non-const iterator
+    @note It is not checked whether @a other is initialized.
+    */
+    iter_impl& operator=(const iter_impl<const BasicJsonType>& other) noexcept
+    {
+        m_object = other.m_object;
+        m_it = other.m_it;
+        return *this;
+    }
+
+    /*!
+    @brief converting constructor
+    @param[in] other  non-const iterator to copy from
+    @note It is not checked whether @a other is initialized.
+    */
+    iter_impl(const iter_impl<typename std::remove_const<BasicJsonType>::type>& other) noexcept
+        : m_object(other.m_object), m_it(other.m_it)
+    {}
+
+    /*!
+    @brief converting assignment
+    @param[in] other  non-const iterator to copy from
+    @return const/non-const iterator
+    @note It is not checked whether @a other is initialized.
+    */
+    iter_impl& operator=(const iter_impl<typename std::remove_const<BasicJsonType>::type>& other) noexcept
+    {
+        m_object = other.m_object;
+        m_it = other.m_it;
+        return *this;
+    }
+
+  private:
+    /*!
+    @brief set the iterator to the first value
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    void set_begin() noexcept
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                m_it.object_iterator = m_object->m_value.object->begin();
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_it.array_iterator = m_object->m_value.array->begin();
+                break;
+            }
+
+            case value_t::null:
+            {
+                // set to end so begin()==end() is true: null is empty
+                m_it.primitive_iterator.set_end();
+                break;
+            }
+
+            default:
+            {
+                m_it.primitive_iterator.set_begin();
+                break;
+            }
+        }
+    }
+
+    /*!
+    @brief set the iterator past the last value
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    void set_end() noexcept
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                m_it.object_iterator = m_object->m_value.object->end();
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_it.array_iterator = m_object->m_value.array->end();
+                break;
+            }
+
+            default:
+            {
+                m_it.primitive_iterator.set_end();
+                break;
+            }
+        }
+    }
+
+  public:
+    /*!
+    @brief return a reference to the value pointed to by the iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    reference operator*() const
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end());
+                return m_it.object_iterator->second;
+            }
+
+            case value_t::array:
+            {
+                JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end());
+                return *m_it.array_iterator;
+            }
+
+            case value_t::null:
+                JSON_THROW(invalid_iterator::create(214, "cannot get value"));
+
+            default:
+            {
+                if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
+                {
+                    return *m_object;
+                }
+
+                JSON_THROW(invalid_iterator::create(214, "cannot get value"));
+            }
+        }
+    }
+
+    /*!
+    @brief dereference the iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    pointer operator->() const
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end());
+                return &(m_it.object_iterator->second);
+            }
+
+            case value_t::array:
+            {
+                JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end());
+                return &*m_it.array_iterator;
+            }
+
+            default:
+            {
+                if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
+                {
+                    return m_object;
+                }
+
+                JSON_THROW(invalid_iterator::create(214, "cannot get value"));
+            }
+        }
+    }
+
+    /*!
+    @brief post-increment (it++)
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl const operator++(int)
+    {
+        auto result = *this;
+        ++(*this);
+        return result;
+    }
+
+    /*!
+    @brief pre-increment (++it)
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl& operator++()
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                std::advance(m_it.object_iterator, 1);
+                break;
+            }
+
+            case value_t::array:
+            {
+                std::advance(m_it.array_iterator, 1);
+                break;
+            }
+
+            default:
+            {
+                ++m_it.primitive_iterator;
+                break;
+            }
+        }
+
+        return *this;
+    }
+
+    /*!
+    @brief post-decrement (it--)
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl const operator--(int)
+    {
+        auto result = *this;
+        --(*this);
+        return result;
+    }
+
+    /*!
+    @brief pre-decrement (--it)
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl& operator--()
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+            {
+                std::advance(m_it.object_iterator, -1);
+                break;
+            }
+
+            case value_t::array:
+            {
+                std::advance(m_it.array_iterator, -1);
+                break;
+            }
+
+            default:
+            {
+                --m_it.primitive_iterator;
+                break;
+            }
+        }
+
+        return *this;
+    }
+
+    /*!
+    @brief  comparison: equal
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator==(const iter_impl& other) const
+    {
+        // if objects are not the same, the comparison is undefined
+        if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers"));
+        }
+
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+                return (m_it.object_iterator == other.m_it.object_iterator);
+
+            case value_t::array:
+                return (m_it.array_iterator == other.m_it.array_iterator);
+
+            default:
+                return (m_it.primitive_iterator == other.m_it.primitive_iterator);
+        }
+    }
+
+    /*!
+    @brief  comparison: not equal
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator!=(const iter_impl& other) const
+    {
+        return !operator==(other);
+    }
+
+    /*!
+    @brief  comparison: smaller
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator<(const iter_impl& other) const
+    {
+        // if objects are not the same, the comparison is undefined
+        if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers"));
+        }
+
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+                JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators"));
+
+            case value_t::array:
+                return (m_it.array_iterator < other.m_it.array_iterator);
+
+            default:
+                return (m_it.primitive_iterator < other.m_it.primitive_iterator);
+        }
+    }
+
+    /*!
+    @brief  comparison: less than or equal
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator<=(const iter_impl& other) const
+    {
+        return !other.operator < (*this);
+    }
+
+    /*!
+    @brief  comparison: greater than
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator>(const iter_impl& other) const
+    {
+        return !operator<=(other);
+    }
+
+    /*!
+    @brief  comparison: greater than or equal
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    bool operator>=(const iter_impl& other) const
+    {
+        return !operator<(other);
+    }
+
+    /*!
+    @brief  add to iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl& operator+=(difference_type i)
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+                JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators"));
+
+            case value_t::array:
+            {
+                std::advance(m_it.array_iterator, i);
+                break;
+            }
+
+            default:
+            {
+                m_it.primitive_iterator += i;
+                break;
+            }
+        }
+
+        return *this;
+    }
+
+    /*!
+    @brief  subtract from iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl& operator-=(difference_type i)
+    {
+        return operator+=(-i);
+    }
+
+    /*!
+    @brief  add to iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl operator+(difference_type i) const
+    {
+        auto result = *this;
+        result += i;
+        return result;
+    }
+
+    /*!
+    @brief  addition of distance and iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    friend iter_impl operator+(difference_type i, const iter_impl& it)
+    {
+        auto result = it;
+        result += i;
+        return result;
+    }
+
+    /*!
+    @brief  subtract from iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    iter_impl operator-(difference_type i) const
+    {
+        auto result = *this;
+        result -= i;
+        return result;
+    }
+
+    /*!
+    @brief  return difference
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    difference_type operator-(const iter_impl& other) const
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+                JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators"));
+
+            case value_t::array:
+                return m_it.array_iterator - other.m_it.array_iterator;
+
+            default:
+                return m_it.primitive_iterator - other.m_it.primitive_iterator;
+        }
+    }
+
+    /*!
+    @brief  access to successor
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    reference operator[](difference_type n) const
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        switch (m_object->m_type)
+        {
+            case value_t::object:
+                JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators"));
+
+            case value_t::array:
+                return *std::next(m_it.array_iterator, n);
+
+            case value_t::null:
+                JSON_THROW(invalid_iterator::create(214, "cannot get value"));
+
+            default:
+            {
+                if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.get_value() == -n))
+                {
+                    return *m_object;
+                }
+
+                JSON_THROW(invalid_iterator::create(214, "cannot get value"));
+            }
+        }
+    }
+
+    /*!
+    @brief  return the key of an object iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    const typename object_t::key_type& key() const
+    {
+        JSON_ASSERT(m_object != nullptr);
+
+        if (JSON_HEDLEY_LIKELY(m_object->is_object()))
+        {
+            return m_it.object_iterator->first;
+        }
+
+        JSON_THROW(invalid_iterator::create(207, "cannot use key() for non-object iterators"));
+    }
+
+    /*!
+    @brief  return the value of an iterator
+    @pre The iterator is initialized; i.e. `m_object != nullptr`.
+    */
+    reference value() const
+    {
+        return operator*();
+    }
+
+  private:
+    /// associated JSON instance
+    pointer m_object = nullptr;
+    /// the actual iterator of the associated instance
+    internal_iterator<typename std::remove_const<BasicJsonType>::type> m_it {};
+};
+} // namespace detail
+} // namespace nlohmann
+
+// #include <nlohmann/detail/iterators/iteration_proxy.hpp>
+
+// #include <nlohmann/detail/iterators/json_reverse_iterator.hpp>
+
+
+#include <cstddef> // ptrdiff_t
+#include <iterator> // reverse_iterator
+#include <utility> // declval
+
+namespace nlohmann
+{
+namespace detail
+{
+//////////////////////
+// reverse_iterator //
+//////////////////////
+
+/*!
+@brief a template for a reverse iterator class
+
+@tparam Base the base iterator type to reverse. Valid types are @ref
+iterator (to create @ref reverse_iterator) and @ref const_iterator (to
+create @ref const_reverse_iterator).
+
+@requirement The class satisfies the following concept requirements:
+-
+[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator):
+  The iterator that can be moved can be moved in both directions (i.e.
+  incremented and decremented).
+- [OutputIterator](https://en.cppreference.com/w/cpp/named_req/OutputIterator):
+  It is possible to write to the pointed-to element (only if @a Base is
+  @ref iterator).
+
+@since version 1.0.0
+*/
+template<typename Base>
+class json_reverse_iterator : public std::reverse_iterator<Base>
+{
+  public:
+    using difference_type = std::ptrdiff_t;
+    /// shortcut to the reverse iterator adapter
+    using base_iterator = std::reverse_iterator<Base>;
+    /// the reference type for the pointed-to element
+    using reference = typename Base::reference;
+
+    /// create reverse iterator from iterator
+    explicit json_reverse_iterator(const typename base_iterator::iterator_type& it) noexcept
+        : base_iterator(it) {}
+
+    /// create reverse iterator from base class
+    explicit json_reverse_iterator(const base_iterator& it) noexcept : base_iterator(it) {}
+
+    /// post-increment (it++)
+    json_reverse_iterator const operator++(int)
+    {
+        return static_cast<json_reverse_iterator>(base_iterator::operator++(1));
+    }
+
+    /// pre-increment (++it)
+    json_reverse_iterator& operator++()
+    {
+        return static_cast<json_reverse_iterator&>(base_iterator::operator++());
+    }
+
+    /// post-decrement (it--)
+    json_reverse_iterator const operator--(int)
+    {
+        return static_cast<json_reverse_iterator>(base_iterator::operator--(1));
+    }
+
+    /// pre-decrement (--it)
+    json_reverse_iterator& operator--()
+    {
+        return static_cast<json_reverse_iterator&>(base_iterator::operator--());
+    }
+
+    /// add to iterator
+    json_reverse_iterator& operator+=(difference_type i)
+    {
+        return static_cast<json_reverse_iterator&>(base_iterator::operator+=(i));
+    }
+
+    /// add to iterator
+    json_reverse_iterator operator+(difference_type i) const
+    {
+        return static_cast<json_reverse_iterator>(base_iterator::operator+(i));
+    }
+
+    /// subtract from iterator
+    json_reverse_iterator operator-(difference_type i) const
+    {
+        return static_cast<json_reverse_iterator>(base_iterator::operator-(i));
+    }
+
+    /// return difference
+    difference_type operator-(const json_reverse_iterator& other) const
+    {
+        return base_iterator(*this) - base_iterator(other);
+    }
+
+    /// access to successor
+    reference operator[](difference_type n) const
+    {
+        return *(this->operator+(n));
+    }
+
+    /// return the key of an object iterator
+    auto key() const -> decltype(std::declval<Base>().key())
+    {
+        auto it = --this->base();
+        return it.key();
+    }
+
+    /// return the value of an iterator
+    reference value() const
+    {
+        auto it = --this->base();
+        return it.operator * ();
+    }
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/iterators/primitive_iterator.hpp>
+
+// #include <nlohmann/detail/json_pointer.hpp>
+
+
+#include <algorithm> // all_of
+#include <cctype> // isdigit
+#include <limits> // max
+#include <numeric> // accumulate
+#include <string> // string
+#include <utility> // move
+#include <vector> // vector
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+template<typename BasicJsonType>
+class json_pointer
+{
+    // allow basic_json to access private members
+    NLOHMANN_BASIC_JSON_TPL_DECLARATION
+    friend class basic_json;
+
+  public:
+    /*!
+    @brief create JSON pointer
+
+    Create a JSON pointer according to the syntax described in
+    [Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3).
+
+    @param[in] s  string representing the JSON pointer; if omitted, the empty
+                  string is assumed which references the whole JSON value
+
+    @throw parse_error.107 if the given JSON pointer @a s is nonempty and does
+                           not begin with a slash (`/`); see example below
+
+    @throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s is
+    not followed by `0` (representing `~`) or `1` (representing `/`); see
+    example below
+
+    @liveexample{The example shows the construction several valid JSON pointers
+    as well as the exceptional behavior.,json_pointer}
+
+    @since version 2.0.0
+    */
+    explicit json_pointer(const std::string& s = "")
+        : reference_tokens(split(s))
+    {}
+
+    /*!
+    @brief return a string representation of the JSON pointer
+
+    @invariant For each JSON pointer `ptr`, it holds:
+    @code {.cpp}
+    ptr == json_pointer(ptr.to_string());
+    @endcode
+
+    @return a string representation of the JSON pointer
+
+    @liveexample{The example shows the result of `to_string`.,json_pointer__to_string}
+
+    @since version 2.0.0
+    */
+    std::string to_string() const
+    {
+        return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
+                               std::string{},
+                               [](const std::string & a, const std::string & b)
+        {
+            return a + "/" + escape(b);
+        });
+    }
+
+    /// @copydoc to_string()
+    operator std::string() const
+    {
+        return to_string();
+    }
+
+    /*!
+    @brief append another JSON pointer at the end of this JSON pointer
+
+    @param[in] ptr  JSON pointer to append
+    @return JSON pointer with @a ptr appended
+
+    @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
+
+    @complexity Linear in the length of @a ptr.
+
+    @sa @ref operator/=(std::string) to append a reference token
+    @sa @ref operator/=(std::size_t) to append an array index
+    @sa @ref operator/(const json_pointer&, const json_pointer&) for a binary operator
+
+    @since version 3.6.0
+    */
+    json_pointer& operator/=(const json_pointer& ptr)
+    {
+        reference_tokens.insert(reference_tokens.end(),
+                                ptr.reference_tokens.begin(),
+                                ptr.reference_tokens.end());
+        return *this;
+    }
+
+    /*!
+    @brief append an unescaped reference token at the end of this JSON pointer
+
+    @param[in] token  reference token to append
+    @return JSON pointer with @a token appended without escaping @a token
+
+    @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
+
+    @complexity Amortized constant.
+
+    @sa @ref operator/=(const json_pointer&) to append a JSON pointer
+    @sa @ref operator/=(std::size_t) to append an array index
+    @sa @ref operator/(const json_pointer&, std::size_t) for a binary operator
+
+    @since version 3.6.0
+    */
+    json_pointer& operator/=(std::string token)
+    {
+        push_back(std::move(token));
+        return *this;
+    }
+
+    /*!
+    @brief append an array index at the end of this JSON pointer
+
+    @param[in] array_idx  array index to append
+    @return JSON pointer with @a array_idx appended
+
+    @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add}
+
+    @complexity Amortized constant.
+
+    @sa @ref operator/=(const json_pointer&) to append a JSON pointer
+    @sa @ref operator/=(std::string) to append a reference token
+    @sa @ref operator/(const json_pointer&, std::string) for a binary operator
+
+    @since version 3.6.0
+    */
+    json_pointer& operator/=(std::size_t array_idx)
+    {
+        return *this /= std::to_string(array_idx);
+    }
+
+    /*!
+    @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer
+
+    @param[in] lhs  JSON pointer
+    @param[in] rhs  JSON pointer
+    @return a new JSON pointer with @a rhs appended to @a lhs
+
+    @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
+
+    @complexity Linear in the length of @a lhs and @a rhs.
+
+    @sa @ref operator/=(const json_pointer&) to append a JSON pointer
+
+    @since version 3.6.0
+    */
+    friend json_pointer operator/(const json_pointer& lhs,
+                                  const json_pointer& rhs)
+    {
+        return json_pointer(lhs) /= rhs;
+    }
+
+    /*!
+    @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
+
+    @param[in] ptr  JSON pointer
+    @param[in] token  reference token
+    @return a new JSON pointer with unescaped @a token appended to @a ptr
+
+    @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
+
+    @complexity Linear in the length of @a ptr.
+
+    @sa @ref operator/=(std::string) to append a reference token
+
+    @since version 3.6.0
+    */
+    friend json_pointer operator/(const json_pointer& ptr, std::string token)
+    {
+        return json_pointer(ptr) /= std::move(token);
+    }
+
+    /*!
+    @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
+
+    @param[in] ptr  JSON pointer
+    @param[in] array_idx  array index
+    @return a new JSON pointer with @a array_idx appended to @a ptr
+
+    @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary}
+
+    @complexity Linear in the length of @a ptr.
+
+    @sa @ref operator/=(std::size_t) to append an array index
+
+    @since version 3.6.0
+    */
+    friend json_pointer operator/(const json_pointer& ptr, std::size_t array_idx)
+    {
+        return json_pointer(ptr) /= array_idx;
+    }
+
+    /*!
+    @brief returns the parent of this JSON pointer
+
+    @return parent of this JSON pointer; in case this JSON pointer is the root,
+            the root itself is returned
+
+    @complexity Linear in the length of the JSON pointer.
+
+    @liveexample{The example shows the result of `parent_pointer` for different
+    JSON Pointers.,json_pointer__parent_pointer}
+
+    @since version 3.6.0
+    */
+    json_pointer parent_pointer() const
+    {
+        if (empty())
+        {
+            return *this;
+        }
+
+        json_pointer res = *this;
+        res.pop_back();
+        return res;
+    }
+
+    /*!
+    @brief remove last reference token
+
+    @pre not `empty()`
+
+    @liveexample{The example shows the usage of `pop_back`.,json_pointer__pop_back}
+
+    @complexity Constant.
+
+    @throw out_of_range.405 if JSON pointer has no parent
+
+    @since version 3.6.0
+    */
+    void pop_back()
+    {
+        if (JSON_HEDLEY_UNLIKELY(empty()))
+        {
+            JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
+        }
+
+        reference_tokens.pop_back();
+    }
+
+    /*!
+    @brief return last reference token
+
+    @pre not `empty()`
+    @return last reference token
+
+    @liveexample{The example shows the usage of `back`.,json_pointer__back}
+
+    @complexity Constant.
+
+    @throw out_of_range.405 if JSON pointer has no parent
+
+    @since version 3.6.0
+    */
+    const std::string& back() const
+    {
+        if (JSON_HEDLEY_UNLIKELY(empty()))
+        {
+            JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
+        }
+
+        return reference_tokens.back();
+    }
+
+    /*!
+    @brief append an unescaped token at the end of the reference pointer
+
+    @param[in] token  token to add
+
+    @complexity Amortized constant.
+
+    @liveexample{The example shows the result of `push_back` for different
+    JSON Pointers.,json_pointer__push_back}
+
+    @since version 3.6.0
+    */
+    void push_back(const std::string& token)
+    {
+        reference_tokens.push_back(token);
+    }
+
+    /// @copydoc push_back(const std::string&)
+    void push_back(std::string&& token)
+    {
+        reference_tokens.push_back(std::move(token));
+    }
+
+    /*!
+    @brief return whether pointer points to the root document
+
+    @return true iff the JSON pointer points to the root document
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example shows the result of `empty` for different JSON
+    Pointers.,json_pointer__empty}
+
+    @since version 3.6.0
+    */
+    bool empty() const noexcept
+    {
+        return reference_tokens.empty();
+    }
+
+  private:
+    /*!
+    @param[in] s  reference token to be converted into an array index
+
+    @return integer representation of @a s
+
+    @throw parse_error.106  if an array index begins with '0'
+    @throw parse_error.109  if an array index begins not with a digit
+    @throw out_of_range.404 if string @a s could not be converted to an integer
+    @throw out_of_range.410 if an array index exceeds size_type
+    */
+    static typename BasicJsonType::size_type array_index(const std::string& s)
+    {
+        using size_type = typename BasicJsonType::size_type;
+
+        // error condition (cf. RFC 6901, Sect. 4)
+        if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0'))
+        {
+            JSON_THROW(detail::parse_error::create(106, 0,
+                                                   "array index '" + s +
+                                                   "' must not begin with '0'"));
+        }
+
+        // error condition (cf. RFC 6901, Sect. 4)
+        if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9')))
+        {
+            JSON_THROW(detail::parse_error::create(109, 0, "array index '" + s + "' is not a number"));
+        }
+
+        std::size_t processed_chars = 0;
+        unsigned long long res = 0;
+        JSON_TRY
+        {
+            res = std::stoull(s, &processed_chars);
+        }
+        JSON_CATCH(std::out_of_range&)
+        {
+            JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
+        }
+
+        // check if the string was completely read
+        if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size()))
+        {
+            JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
+        }
+
+        // only triggered on special platforms (like 32bit), see also
+        // https://github.com/nlohmann/json/pull/2203
+        if (res >= static_cast<unsigned long long>((std::numeric_limits<size_type>::max)()))
+        {
+            JSON_THROW(detail::out_of_range::create(410, "array index " + s + " exceeds size_type")); // LCOV_EXCL_LINE
+        }
+
+        return static_cast<size_type>(res);
+    }
+
+    json_pointer top() const
+    {
+        if (JSON_HEDLEY_UNLIKELY(empty()))
+        {
+            JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
+        }
+
+        json_pointer result = *this;
+        result.reference_tokens = {reference_tokens[0]};
+        return result;
+    }
+
+    /*!
+    @brief create and return a reference to the pointed to value
+
+    @complexity Linear in the number of reference tokens.
+
+    @throw parse_error.109 if array index is not a number
+    @throw type_error.313 if value cannot be unflattened
+    */
+    BasicJsonType& get_and_create(BasicJsonType& j) const
+    {
+        auto result = &j;
+
+        // in case no reference tokens exist, return a reference to the JSON value
+        // j which will be overwritten by a primitive value
+        for (const auto& reference_token : reference_tokens)
+        {
+            switch (result->type())
+            {
+                case detail::value_t::null:
+                {
+                    if (reference_token == "0")
+                    {
+                        // start a new array if reference token is 0
+                        result = &result->operator[](0);
+                    }
+                    else
+                    {
+                        // start a new object otherwise
+                        result = &result->operator[](reference_token);
+                    }
+                    break;
+                }
+
+                case detail::value_t::object:
+                {
+                    // create an entry in the object
+                    result = &result->operator[](reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    // create an entry in the array
+                    result = &result->operator[](array_index(reference_token));
+                    break;
+                }
+
+                /*
+                The following code is only reached if there exists a reference
+                token _and_ the current value is primitive. In this case, we have
+                an error situation, because primitive values may only occur as
+                single value; that is, with an empty list of reference tokens.
+                */
+                default:
+                    JSON_THROW(detail::type_error::create(313, "invalid value to unflatten"));
+            }
+        }
+
+        return *result;
+    }
+
+    /*!
+    @brief return a reference to the pointed to value
+
+    @note This version does not throw if a value is not present, but tries to
+          create nested values instead. For instance, calling this function
+          with pointer `"/this/that"` on a null value is equivalent to calling
+          `operator[]("this").operator[]("that")` on that value, effectively
+          changing the null value to an object.
+
+    @param[in] ptr  a JSON value
+
+    @return reference to the JSON value pointed to by the JSON pointer
+
+    @complexity Linear in the length of the JSON pointer.
+
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+    */
+    BasicJsonType& get_unchecked(BasicJsonType* ptr) const
+    {
+        for (const auto& reference_token : reference_tokens)
+        {
+            // convert null values to arrays or objects before continuing
+            if (ptr->is_null())
+            {
+                // check if reference token is a number
+                const bool nums =
+                    std::all_of(reference_token.begin(), reference_token.end(),
+                                [](const unsigned char x)
+                {
+                    return std::isdigit(x);
+                });
+
+                // change value to array for numbers or "-" or to object otherwise
+                *ptr = (nums || reference_token == "-")
+                       ? detail::value_t::array
+                       : detail::value_t::object;
+            }
+
+            switch (ptr->type())
+            {
+                case detail::value_t::object:
+                {
+                    // use unchecked object access
+                    ptr = &ptr->operator[](reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    if (reference_token == "-")
+                    {
+                        // explicitly treat "-" as index beyond the end
+                        ptr = &ptr->operator[](ptr->m_value.array->size());
+                    }
+                    else
+                    {
+                        // convert array index to number; unchecked access
+                        ptr = &ptr->operator[](array_index(reference_token));
+                    }
+                    break;
+                }
+
+                default:
+                    JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
+            }
+        }
+
+        return *ptr;
+    }
+
+    /*!
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.402  if the array index '-' is used
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+    */
+    BasicJsonType& get_checked(BasicJsonType* ptr) const
+    {
+        for (const auto& reference_token : reference_tokens)
+        {
+            switch (ptr->type())
+            {
+                case detail::value_t::object:
+                {
+                    // note: at performs range check
+                    ptr = &ptr->at(reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
+                    {
+                        // "-" always fails the range check
+                        JSON_THROW(detail::out_of_range::create(402,
+                                                                "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
+                                                                ") is out of range"));
+                    }
+
+                    // note: at performs range check
+                    ptr = &ptr->at(array_index(reference_token));
+                    break;
+                }
+
+                default:
+                    JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
+            }
+        }
+
+        return *ptr;
+    }
+
+    /*!
+    @brief return a const reference to the pointed to value
+
+    @param[in] ptr  a JSON value
+
+    @return const reference to the JSON value pointed to by the JSON
+    pointer
+
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.402  if the array index '-' is used
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+    */
+    const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const
+    {
+        for (const auto& reference_token : reference_tokens)
+        {
+            switch (ptr->type())
+            {
+                case detail::value_t::object:
+                {
+                    // use unchecked object access
+                    ptr = &ptr->operator[](reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
+                    {
+                        // "-" cannot be used for const access
+                        JSON_THROW(detail::out_of_range::create(402,
+                                                                "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
+                                                                ") is out of range"));
+                    }
+
+                    // use unchecked array access
+                    ptr = &ptr->operator[](array_index(reference_token));
+                    break;
+                }
+
+                default:
+                    JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
+            }
+        }
+
+        return *ptr;
+    }
+
+    /*!
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.402  if the array index '-' is used
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+    */
+    const BasicJsonType& get_checked(const BasicJsonType* ptr) const
+    {
+        for (const auto& reference_token : reference_tokens)
+        {
+            switch (ptr->type())
+            {
+                case detail::value_t::object:
+                {
+                    // note: at performs range check
+                    ptr = &ptr->at(reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
+                    {
+                        // "-" always fails the range check
+                        JSON_THROW(detail::out_of_range::create(402,
+                                                                "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
+                                                                ") is out of range"));
+                    }
+
+                    // note: at performs range check
+                    ptr = &ptr->at(array_index(reference_token));
+                    break;
+                }
+
+                default:
+                    JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
+            }
+        }
+
+        return *ptr;
+    }
+
+    /*!
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    */
+    bool contains(const BasicJsonType* ptr) const
+    {
+        for (const auto& reference_token : reference_tokens)
+        {
+            switch (ptr->type())
+            {
+                case detail::value_t::object:
+                {
+                    if (!ptr->contains(reference_token))
+                    {
+                        // we did not find the key in the object
+                        return false;
+                    }
+
+                    ptr = &ptr->operator[](reference_token);
+                    break;
+                }
+
+                case detail::value_t::array:
+                {
+                    if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
+                    {
+                        // "-" always fails the range check
+                        return false;
+                    }
+                    if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9")))
+                    {
+                        // invalid char
+                        return false;
+                    }
+                    if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1))
+                    {
+                        if (JSON_HEDLEY_UNLIKELY(!('1' <= reference_token[0] && reference_token[0] <= '9')))
+                        {
+                            // first char should be between '1' and '9'
+                            return false;
+                        }
+                        for (std::size_t i = 1; i < reference_token.size(); i++)
+                        {
+                            if (JSON_HEDLEY_UNLIKELY(!('0' <= reference_token[i] && reference_token[i] <= '9')))
+                            {
+                                // other char should be between '0' and '9'
+                                return false;
+                            }
+                        }
+                    }
+
+                    const auto idx = array_index(reference_token);
+                    if (idx >= ptr->size())
+                    {
+                        // index out of range
+                        return false;
+                    }
+
+                    ptr = &ptr->operator[](idx);
+                    break;
+                }
+
+                default:
+                {
+                    // we do not expect primitive values if there is still a
+                    // reference token to process
+                    return false;
+                }
+            }
+        }
+
+        // no reference token left means we found a primitive value
+        return true;
+    }
+
+    /*!
+    @brief split the string input to reference tokens
+
+    @note This function is only called by the json_pointer constructor.
+          All exceptions below are documented there.
+
+    @throw parse_error.107  if the pointer is not empty or begins with '/'
+    @throw parse_error.108  if character '~' is not followed by '0' or '1'
+    */
+    static std::vector<std::string> split(const std::string& reference_string)
+    {
+        std::vector<std::string> result;
+
+        // special case: empty reference string -> no reference tokens
+        if (reference_string.empty())
+        {
+            return result;
+        }
+
+        // check if nonempty reference string begins with slash
+        if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
+        {
+            JSON_THROW(detail::parse_error::create(107, 1,
+                                                   "JSON pointer must be empty or begin with '/' - was: '" +
+                                                   reference_string + "'"));
+        }
+
+        // extract the reference tokens:
+        // - slash: position of the last read slash (or end of string)
+        // - start: position after the previous slash
+        for (
+            // search for the first slash after the first character
+            std::size_t slash = reference_string.find_first_of('/', 1),
+            // set the beginning of the first reference token
+            start = 1;
+            // we can stop if start == 0 (if slash == std::string::npos)
+            start != 0;
+            // set the beginning of the next reference token
+            // (will eventually be 0 if slash == std::string::npos)
+            start = (slash == std::string::npos) ? 0 : slash + 1,
+            // find next slash
+            slash = reference_string.find_first_of('/', start))
+        {
+            // use the text between the beginning of the reference token
+            // (start) and the last slash (slash).
+            auto reference_token = reference_string.substr(start, slash - start);
+
+            // check reference tokens are properly escaped
+            for (std::size_t pos = reference_token.find_first_of('~');
+                    pos != std::string::npos;
+                    pos = reference_token.find_first_of('~', pos + 1))
+            {
+                JSON_ASSERT(reference_token[pos] == '~');
+
+                // ~ must be followed by 0 or 1
+                if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 ||
+                                         (reference_token[pos + 1] != '0' &&
+                                          reference_token[pos + 1] != '1')))
+                {
+                    JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'"));
+                }
+            }
+
+            // finally, store the reference token
+            unescape(reference_token);
+            result.push_back(reference_token);
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief replace all occurrences of a substring by another string
+
+    @param[in,out] s  the string to manipulate; changed so that all
+                   occurrences of @a f are replaced with @a t
+    @param[in]     f  the substring to replace with @a t
+    @param[in]     t  the string to replace @a f
+
+    @pre The search string @a f must not be empty. **This precondition is
+    enforced with an assertion.**
+
+    @since version 2.0.0
+    */
+    static void replace_substring(std::string& s, const std::string& f,
+                                  const std::string& t)
+    {
+        JSON_ASSERT(!f.empty());
+        for (auto pos = s.find(f);                // find first occurrence of f
+                pos != std::string::npos;         // make sure f was found
+                s.replace(pos, f.size(), t),      // replace with t, and
+                pos = s.find(f, pos + t.size()))  // find next occurrence of f
+        {}
+    }
+
+    /// escape "~" to "~0" and "/" to "~1"
+    static std::string escape(std::string s)
+    {
+        replace_substring(s, "~", "~0");
+        replace_substring(s, "/", "~1");
+        return s;
+    }
+
+    /// unescape "~1" to tilde and "~0" to slash (order is important!)
+    static void unescape(std::string& s)
+    {
+        replace_substring(s, "~1", "/");
+        replace_substring(s, "~0", "~");
+    }
+
+    /*!
+    @param[in] reference_string  the reference string to the current value
+    @param[in] value             the value to consider
+    @param[in,out] result        the result object to insert values to
+
+    @note Empty objects or arrays are flattened to `null`.
+    */
+    static void flatten(const std::string& reference_string,
+                        const BasicJsonType& value,
+                        BasicJsonType& result)
+    {
+        switch (value.type())
+        {
+            case detail::value_t::array:
+            {
+                if (value.m_value.array->empty())
+                {
+                    // flatten empty array as null
+                    result[reference_string] = nullptr;
+                }
+                else
+                {
+                    // iterate array and use index as reference string
+                    for (std::size_t i = 0; i < value.m_value.array->size(); ++i)
+                    {
+                        flatten(reference_string + "/" + std::to_string(i),
+                                value.m_value.array->operator[](i), result);
+                    }
+                }
+                break;
+            }
+
+            case detail::value_t::object:
+            {
+                if (value.m_value.object->empty())
+                {
+                    // flatten empty object as null
+                    result[reference_string] = nullptr;
+                }
+                else
+                {
+                    // iterate object and use keys as reference string
+                    for (const auto& element : *value.m_value.object)
+                    {
+                        flatten(reference_string + "/" + escape(element.first), element.second, result);
+                    }
+                }
+                break;
+            }
+
+            default:
+            {
+                // add primitive value with its reference string
+                result[reference_string] = value;
+                break;
+            }
+        }
+    }
+
+    /*!
+    @param[in] value  flattened JSON
+
+    @return unflattened JSON
+
+    @throw parse_error.109 if array index is not a number
+    @throw type_error.314  if value is not an object
+    @throw type_error.315  if object values are not primitive
+    @throw type_error.313  if value cannot be unflattened
+    */
+    static BasicJsonType
+    unflatten(const BasicJsonType& value)
+    {
+        if (JSON_HEDLEY_UNLIKELY(!value.is_object()))
+        {
+            JSON_THROW(detail::type_error::create(314, "only objects can be unflattened"));
+        }
+
+        BasicJsonType result;
+
+        // iterate the JSON object values
+        for (const auto& element : *value.m_value.object)
+        {
+            if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
+            {
+                JSON_THROW(detail::type_error::create(315, "values in object must be primitive"));
+            }
+
+            // assign value to reference pointed to by JSON pointer; Note that if
+            // the JSON pointer is "" (i.e., points to the whole value), function
+            // get_and_create returns a reference to result itself. An assignment
+            // will then create a primitive value.
+            json_pointer(element.first).get_and_create(result) = element.second;
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief compares two JSON pointers for equality
+
+    @param[in] lhs  JSON pointer to compare
+    @param[in] rhs  JSON pointer to compare
+    @return whether @a lhs is equal to @a rhs
+
+    @complexity Linear in the length of the JSON pointer
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+    */
+    friend bool operator==(json_pointer const& lhs,
+                           json_pointer const& rhs) noexcept
+    {
+        return lhs.reference_tokens == rhs.reference_tokens;
+    }
+
+    /*!
+    @brief compares two JSON pointers for inequality
+
+    @param[in] lhs  JSON pointer to compare
+    @param[in] rhs  JSON pointer to compare
+    @return whether @a lhs is not equal @a rhs
+
+    @complexity Linear in the length of the JSON pointer
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+    */
+    friend bool operator!=(json_pointer const& lhs,
+                           json_pointer const& rhs) noexcept
+    {
+        return !(lhs == rhs);
+    }
+
+    /// the reference tokens
+    std::vector<std::string> reference_tokens;
+};
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/json_ref.hpp>
+
+
+#include <initializer_list>
+#include <utility>
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+template<typename BasicJsonType>
+class json_ref
+{
+  public:
+    using value_type = BasicJsonType;
+
+    json_ref(value_type&& value)
+        : owned_value(std::move(value))
+        , value_ref(&owned_value)
+        , is_rvalue(true)
+    {}
+
+    json_ref(const value_type& value)
+        : value_ref(const_cast<value_type*>(&value))
+        , is_rvalue(false)
+    {}
+
+    json_ref(std::initializer_list<json_ref> init)
+        : owned_value(init)
+        , value_ref(&owned_value)
+        , is_rvalue(true)
+    {}
+
+    template <
+        class... Args,
+        enable_if_t<std::is_constructible<value_type, Args...>::value, int> = 0 >
+    json_ref(Args && ... args)
+        : owned_value(std::forward<Args>(args)...)
+        , value_ref(&owned_value)
+        , is_rvalue(true)
+    {}
+
+    // class should be movable only
+    json_ref(json_ref&&) = default;
+    json_ref(const json_ref&) = delete;
+    json_ref& operator=(const json_ref&) = delete;
+    json_ref& operator=(json_ref&&) = delete;
+    ~json_ref() = default;
+
+    value_type moved_or_copied() const
+    {
+        if (is_rvalue)
+        {
+            return std::move(*value_ref);
+        }
+        return *value_ref;
+    }
+
+    value_type const& operator*() const
+    {
+        return *static_cast<value_type const*>(value_ref);
+    }
+
+    value_type const* operator->() const
+    {
+        return static_cast<value_type const*>(value_ref);
+    }
+
+  private:
+    mutable value_type owned_value = nullptr;
+    value_type* value_ref = nullptr;
+    const bool is_rvalue = true;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+// #include <nlohmann/detail/meta/type_traits.hpp>
+
+// #include <nlohmann/detail/output/binary_writer.hpp>
+
+
+#include <algorithm> // reverse
+#include <array> // array
+#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t
+#include <cstring> // memcpy
+#include <limits> // numeric_limits
+#include <string> // string
+#include <cmath> // isnan, isinf
+
+// #include <nlohmann/detail/input/binary_reader.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/output/output_adapters.hpp>
+
+
+#include <algorithm> // copy
+#include <cstddef> // size_t
+#include <ios> // streamsize
+#include <iterator> // back_inserter
+#include <memory> // shared_ptr, make_shared
+#include <ostream> // basic_ostream
+#include <string> // basic_string
+#include <vector> // vector
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+/// abstract output adapter interface
+template<typename CharType> struct output_adapter_protocol
+{
+    virtual void write_character(CharType c) = 0;
+    virtual void write_characters(const CharType* s, std::size_t length) = 0;
+    virtual ~output_adapter_protocol() = default;
+};
+
+/// a type to simplify interfaces
+template<typename CharType>
+using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
+
+/// output adapter for byte vectors
+template<typename CharType>
+class output_vector_adapter : public output_adapter_protocol<CharType>
+{
+  public:
+    explicit output_vector_adapter(std::vector<CharType>& vec) noexcept
+        : v(vec)
+    {}
+
+    void write_character(CharType c) override
+    {
+        v.push_back(c);
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    void write_characters(const CharType* s, std::size_t length) override
+    {
+        std::copy(s, s + length, std::back_inserter(v));
+    }
+
+  private:
+    std::vector<CharType>& v;
+};
+
+/// output adapter for output streams
+template<typename CharType>
+class output_stream_adapter : public output_adapter_protocol<CharType>
+{
+  public:
+    explicit output_stream_adapter(std::basic_ostream<CharType>& s) noexcept
+        : stream(s)
+    {}
+
+    void write_character(CharType c) override
+    {
+        stream.put(c);
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    void write_characters(const CharType* s, std::size_t length) override
+    {
+        stream.write(s, static_cast<std::streamsize>(length));
+    }
+
+  private:
+    std::basic_ostream<CharType>& stream;
+};
+
+/// output adapter for basic_string
+template<typename CharType, typename StringType = std::basic_string<CharType>>
+class output_string_adapter : public output_adapter_protocol<CharType>
+{
+  public:
+    explicit output_string_adapter(StringType& s) noexcept
+        : str(s)
+    {}
+
+    void write_character(CharType c) override
+    {
+        str.push_back(c);
+    }
+
+    JSON_HEDLEY_NON_NULL(2)
+    void write_characters(const CharType* s, std::size_t length) override
+    {
+        str.append(s, length);
+    }
+
+  private:
+    StringType& str;
+};
+
+template<typename CharType, typename StringType = std::basic_string<CharType>>
+class output_adapter
+{
+  public:
+    output_adapter(std::vector<CharType>& vec)
+        : oa(std::make_shared<output_vector_adapter<CharType>>(vec)) {}
+
+    output_adapter(std::basic_ostream<CharType>& s)
+        : oa(std::make_shared<output_stream_adapter<CharType>>(s)) {}
+
+    output_adapter(StringType& s)
+        : oa(std::make_shared<output_string_adapter<CharType, StringType>>(s)) {}
+
+    operator output_adapter_t<CharType>()
+    {
+        return oa;
+    }
+
+  private:
+    output_adapter_t<CharType> oa = nullptr;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+
+namespace nlohmann
+{
+namespace detail
+{
+///////////////////
+// binary writer //
+///////////////////
+
+/*!
+@brief serialization to CBOR and MessagePack values
+*/
+template<typename BasicJsonType, typename CharType>
+class binary_writer
+{
+    using string_t = typename BasicJsonType::string_t;
+    using binary_t = typename BasicJsonType::binary_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+
+  public:
+    /*!
+    @brief create a binary writer
+
+    @param[in] adapter  output adapter to write to
+    */
+    explicit binary_writer(output_adapter_t<CharType> adapter) : oa(adapter)
+    {
+        JSON_ASSERT(oa);
+    }
+
+    /*!
+    @param[in] j  JSON value to serialize
+    @pre       j.type() == value_t::object
+    */
+    void write_bson(const BasicJsonType& j)
+    {
+        switch (j.type())
+        {
+            case value_t::object:
+            {
+                write_bson_object(*j.m_value.object);
+                break;
+            }
+
+            default:
+            {
+                JSON_THROW(type_error::create(317, "to serialize to BSON, top-level type must be object, but is " + std::string(j.type_name())));
+            }
+        }
+    }
+
+    /*!
+    @param[in] j  JSON value to serialize
+    */
+    void write_cbor(const BasicJsonType& j)
+    {
+        switch (j.type())
+        {
+            case value_t::null:
+            {
+                oa->write_character(to_char_type(0xF6));
+                break;
+            }
+
+            case value_t::boolean:
+            {
+                oa->write_character(j.m_value.boolean
+                                    ? to_char_type(0xF5)
+                                    : to_char_type(0xF4));
+                break;
+            }
+
+            case value_t::number_integer:
+            {
+                if (j.m_value.number_integer >= 0)
+                {
+                    // CBOR does not differentiate between positive signed
+                    // integers and unsigned integers. Therefore, we used the
+                    // code from the value_t::number_unsigned case here.
+                    if (j.m_value.number_integer <= 0x17)
+                    {
+                        write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer <= (std::numeric_limits<std::uint8_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x18));
+                        write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer <= (std::numeric_limits<std::uint16_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x19));
+                        write_number(static_cast<std::uint16_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer <= (std::numeric_limits<std::uint32_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x1A));
+                        write_number(static_cast<std::uint32_t>(j.m_value.number_integer));
+                    }
+                    else
+                    {
+                        oa->write_character(to_char_type(0x1B));
+                        write_number(static_cast<std::uint64_t>(j.m_value.number_integer));
+                    }
+                }
+                else
+                {
+                    // The conversions below encode the sign in the first
+                    // byte, and the value is converted to a positive number.
+                    const auto positive_number = -1 - j.m_value.number_integer;
+                    if (j.m_value.number_integer >= -24)
+                    {
+                        write_number(static_cast<std::uint8_t>(0x20 + positive_number));
+                    }
+                    else if (positive_number <= (std::numeric_limits<std::uint8_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x38));
+                        write_number(static_cast<std::uint8_t>(positive_number));
+                    }
+                    else if (positive_number <= (std::numeric_limits<std::uint16_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x39));
+                        write_number(static_cast<std::uint16_t>(positive_number));
+                    }
+                    else if (positive_number <= (std::numeric_limits<std::uint32_t>::max)())
+                    {
+                        oa->write_character(to_char_type(0x3A));
+                        write_number(static_cast<std::uint32_t>(positive_number));
+                    }
+                    else
+                    {
+                        oa->write_character(to_char_type(0x3B));
+                        write_number(static_cast<std::uint64_t>(positive_number));
+                    }
+                }
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                if (j.m_value.number_unsigned <= 0x17)
+                {
+                    write_number(static_cast<std::uint8_t>(j.m_value.number_unsigned));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x18));
+                    write_number(static_cast<std::uint8_t>(j.m_value.number_unsigned));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x19));
+                    write_number(static_cast<std::uint16_t>(j.m_value.number_unsigned));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x1A));
+                    write_number(static_cast<std::uint32_t>(j.m_value.number_unsigned));
+                }
+                else
+                {
+                    oa->write_character(to_char_type(0x1B));
+                    write_number(static_cast<std::uint64_t>(j.m_value.number_unsigned));
+                }
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                if (std::isnan(j.m_value.number_float))
+                {
+                    // NaN is 0xf97e00 in CBOR
+                    oa->write_character(to_char_type(0xF9));
+                    oa->write_character(to_char_type(0x7E));
+                    oa->write_character(to_char_type(0x00));
+                }
+                else if (std::isinf(j.m_value.number_float))
+                {
+                    // Infinity is 0xf97c00, -Infinity is 0xf9fc00
+                    oa->write_character(to_char_type(0xf9));
+                    oa->write_character(j.m_value.number_float > 0 ? to_char_type(0x7C) : to_char_type(0xFC));
+                    oa->write_character(to_char_type(0x00));
+                }
+                else
+                {
+                    write_compact_float(j.m_value.number_float, detail::input_format_t::cbor);
+                }
+                break;
+            }
+
+            case value_t::string:
+            {
+                // step 1: write control byte and the string length
+                const auto N = j.m_value.string->size();
+                if (N <= 0x17)
+                {
+                    write_number(static_cast<std::uint8_t>(0x60 + N));
+                }
+                else if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x78));
+                    write_number(static_cast<std::uint8_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x79));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x7A));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+                // LCOV_EXCL_START
+                else if (N <= (std::numeric_limits<std::uint64_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x7B));
+                    write_number(static_cast<std::uint64_t>(N));
+                }
+                // LCOV_EXCL_STOP
+
+                // step 2: write the string
+                oa->write_characters(
+                    reinterpret_cast<const CharType*>(j.m_value.string->c_str()),
+                    j.m_value.string->size());
+                break;
+            }
+
+            case value_t::array:
+            {
+                // step 1: write control byte and the array size
+                const auto N = j.m_value.array->size();
+                if (N <= 0x17)
+                {
+                    write_number(static_cast<std::uint8_t>(0x80 + N));
+                }
+                else if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x98));
+                    write_number(static_cast<std::uint8_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x99));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x9A));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+                // LCOV_EXCL_START
+                else if (N <= (std::numeric_limits<std::uint64_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x9B));
+                    write_number(static_cast<std::uint64_t>(N));
+                }
+                // LCOV_EXCL_STOP
+
+                // step 2: write each element
+                for (const auto& el : *j.m_value.array)
+                {
+                    write_cbor(el);
+                }
+                break;
+            }
+
+            case value_t::binary:
+            {
+                if (j.m_value.binary->has_subtype())
+                {
+                    write_number(static_cast<std::uint8_t>(0xd8));
+                    write_number(j.m_value.binary->subtype());
+                }
+
+                // step 1: write control byte and the binary array size
+                const auto N = j.m_value.binary->size();
+                if (N <= 0x17)
+                {
+                    write_number(static_cast<std::uint8_t>(0x40 + N));
+                }
+                else if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x58));
+                    write_number(static_cast<std::uint8_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x59));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x5A));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+                // LCOV_EXCL_START
+                else if (N <= (std::numeric_limits<std::uint64_t>::max)())
+                {
+                    oa->write_character(to_char_type(0x5B));
+                    write_number(static_cast<std::uint64_t>(N));
+                }
+                // LCOV_EXCL_STOP
+
+                // step 2: write each element
+                oa->write_characters(
+                    reinterpret_cast<const CharType*>(j.m_value.binary->data()),
+                    N);
+
+                break;
+            }
+
+            case value_t::object:
+            {
+                // step 1: write control byte and the object size
+                const auto N = j.m_value.object->size();
+                if (N <= 0x17)
+                {
+                    write_number(static_cast<std::uint8_t>(0xA0 + N));
+                }
+                else if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    oa->write_character(to_char_type(0xB8));
+                    write_number(static_cast<std::uint8_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    oa->write_character(to_char_type(0xB9));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    oa->write_character(to_char_type(0xBA));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+                // LCOV_EXCL_START
+                else if (N <= (std::numeric_limits<std::uint64_t>::max)())
+                {
+                    oa->write_character(to_char_type(0xBB));
+                    write_number(static_cast<std::uint64_t>(N));
+                }
+                // LCOV_EXCL_STOP
+
+                // step 2: write each element
+                for (const auto& el : *j.m_value.object)
+                {
+                    write_cbor(el.first);
+                    write_cbor(el.second);
+                }
+                break;
+            }
+
+            default:
+                break;
+        }
+    }
+
+    /*!
+    @param[in] j  JSON value to serialize
+    */
+    void write_msgpack(const BasicJsonType& j)
+    {
+        switch (j.type())
+        {
+            case value_t::null: // nil
+            {
+                oa->write_character(to_char_type(0xC0));
+                break;
+            }
+
+            case value_t::boolean: // true and false
+            {
+                oa->write_character(j.m_value.boolean
+                                    ? to_char_type(0xC3)
+                                    : to_char_type(0xC2));
+                break;
+            }
+
+            case value_t::number_integer:
+            {
+                if (j.m_value.number_integer >= 0)
+                {
+                    // MessagePack does not differentiate between positive
+                    // signed integers and unsigned integers. Therefore, we used
+                    // the code from the value_t::number_unsigned case here.
+                    if (j.m_value.number_unsigned < 128)
+                    {
+                        // positive fixnum
+                        write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint8_t>::max)())
+                    {
+                        // uint 8
+                        oa->write_character(to_char_type(0xCC));
+                        write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint16_t>::max)())
+                    {
+                        // uint 16
+                        oa->write_character(to_char_type(0xCD));
+                        write_number(static_cast<std::uint16_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint32_t>::max)())
+                    {
+                        // uint 32
+                        oa->write_character(to_char_type(0xCE));
+                        write_number(static_cast<std::uint32_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint64_t>::max)())
+                    {
+                        // uint 64
+                        oa->write_character(to_char_type(0xCF));
+                        write_number(static_cast<std::uint64_t>(j.m_value.number_integer));
+                    }
+                }
+                else
+                {
+                    if (j.m_value.number_integer >= -32)
+                    {
+                        // negative fixnum
+                        write_number(static_cast<std::int8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer >= (std::numeric_limits<std::int8_t>::min)() &&
+                             j.m_value.number_integer <= (std::numeric_limits<std::int8_t>::max)())
+                    {
+                        // int 8
+                        oa->write_character(to_char_type(0xD0));
+                        write_number(static_cast<std::int8_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer >= (std::numeric_limits<std::int16_t>::min)() &&
+                             j.m_value.number_integer <= (std::numeric_limits<std::int16_t>::max)())
+                    {
+                        // int 16
+                        oa->write_character(to_char_type(0xD1));
+                        write_number(static_cast<std::int16_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer >= (std::numeric_limits<std::int32_t>::min)() &&
+                             j.m_value.number_integer <= (std::numeric_limits<std::int32_t>::max)())
+                    {
+                        // int 32
+                        oa->write_character(to_char_type(0xD2));
+                        write_number(static_cast<std::int32_t>(j.m_value.number_integer));
+                    }
+                    else if (j.m_value.number_integer >= (std::numeric_limits<std::int64_t>::min)() &&
+                             j.m_value.number_integer <= (std::numeric_limits<std::int64_t>::max)())
+                    {
+                        // int 64
+                        oa->write_character(to_char_type(0xD3));
+                        write_number(static_cast<std::int64_t>(j.m_value.number_integer));
+                    }
+                }
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                if (j.m_value.number_unsigned < 128)
+                {
+                    // positive fixnum
+                    write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    // uint 8
+                    oa->write_character(to_char_type(0xCC));
+                    write_number(static_cast<std::uint8_t>(j.m_value.number_integer));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    // uint 16
+                    oa->write_character(to_char_type(0xCD));
+                    write_number(static_cast<std::uint16_t>(j.m_value.number_integer));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    // uint 32
+                    oa->write_character(to_char_type(0xCE));
+                    write_number(static_cast<std::uint32_t>(j.m_value.number_integer));
+                }
+                else if (j.m_value.number_unsigned <= (std::numeric_limits<std::uint64_t>::max)())
+                {
+                    // uint 64
+                    oa->write_character(to_char_type(0xCF));
+                    write_number(static_cast<std::uint64_t>(j.m_value.number_integer));
+                }
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                write_compact_float(j.m_value.number_float, detail::input_format_t::msgpack);
+                break;
+            }
+
+            case value_t::string:
+            {
+                // step 1: write control byte and the string length
+                const auto N = j.m_value.string->size();
+                if (N <= 31)
+                {
+                    // fixstr
+                    write_number(static_cast<std::uint8_t>(0xA0 | N));
+                }
+                else if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    // str 8
+                    oa->write_character(to_char_type(0xD9));
+                    write_number(static_cast<std::uint8_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    // str 16
+                    oa->write_character(to_char_type(0xDA));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    // str 32
+                    oa->write_character(to_char_type(0xDB));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+
+                // step 2: write the string
+                oa->write_characters(
+                    reinterpret_cast<const CharType*>(j.m_value.string->c_str()),
+                    j.m_value.string->size());
+                break;
+            }
+
+            case value_t::array:
+            {
+                // step 1: write control byte and the array size
+                const auto N = j.m_value.array->size();
+                if (N <= 15)
+                {
+                    // fixarray
+                    write_number(static_cast<std::uint8_t>(0x90 | N));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    // array 16
+                    oa->write_character(to_char_type(0xDC));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    // array 32
+                    oa->write_character(to_char_type(0xDD));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+
+                // step 2: write each element
+                for (const auto& el : *j.m_value.array)
+                {
+                    write_msgpack(el);
+                }
+                break;
+            }
+
+            case value_t::binary:
+            {
+                // step 0: determine if the binary type has a set subtype to
+                // determine whether or not to use the ext or fixext types
+                const bool use_ext = j.m_value.binary->has_subtype();
+
+                // step 1: write control byte and the byte string length
+                const auto N = j.m_value.binary->size();
+                if (N <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    std::uint8_t output_type{};
+                    bool fixed = true;
+                    if (use_ext)
+                    {
+                        switch (N)
+                        {
+                            case 1:
+                                output_type = 0xD4; // fixext 1
+                                break;
+                            case 2:
+                                output_type = 0xD5; // fixext 2
+                                break;
+                            case 4:
+                                output_type = 0xD6; // fixext 4
+                                break;
+                            case 8:
+                                output_type = 0xD7; // fixext 8
+                                break;
+                            case 16:
+                                output_type = 0xD8; // fixext 16
+                                break;
+                            default:
+                                output_type = 0xC7; // ext 8
+                                fixed = false;
+                                break;
+                        }
+
+                    }
+                    else
+                    {
+                        output_type = 0xC4; // bin 8
+                        fixed = false;
+                    }
+
+                    oa->write_character(to_char_type(output_type));
+                    if (!fixed)
+                    {
+                        write_number(static_cast<std::uint8_t>(N));
+                    }
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    std::uint8_t output_type = use_ext
+                                               ? 0xC8 // ext 16
+                                               : 0xC5; // bin 16
+
+                    oa->write_character(to_char_type(output_type));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    std::uint8_t output_type = use_ext
+                                               ? 0xC9 // ext 32
+                                               : 0xC6; // bin 32
+
+                    oa->write_character(to_char_type(output_type));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+
+                // step 1.5: if this is an ext type, write the subtype
+                if (use_ext)
+                {
+                    write_number(static_cast<std::int8_t>(j.m_value.binary->subtype()));
+                }
+
+                // step 2: write the byte string
+                oa->write_characters(
+                    reinterpret_cast<const CharType*>(j.m_value.binary->data()),
+                    N);
+
+                break;
+            }
+
+            case value_t::object:
+            {
+                // step 1: write control byte and the object size
+                const auto N = j.m_value.object->size();
+                if (N <= 15)
+                {
+                    // fixmap
+                    write_number(static_cast<std::uint8_t>(0x80 | (N & 0xF)));
+                }
+                else if (N <= (std::numeric_limits<std::uint16_t>::max)())
+                {
+                    // map 16
+                    oa->write_character(to_char_type(0xDE));
+                    write_number(static_cast<std::uint16_t>(N));
+                }
+                else if (N <= (std::numeric_limits<std::uint32_t>::max)())
+                {
+                    // map 32
+                    oa->write_character(to_char_type(0xDF));
+                    write_number(static_cast<std::uint32_t>(N));
+                }
+
+                // step 2: write each element
+                for (const auto& el : *j.m_value.object)
+                {
+                    write_msgpack(el.first);
+                    write_msgpack(el.second);
+                }
+                break;
+            }
+
+            default:
+                break;
+        }
+    }
+
+    /*!
+    @param[in] j  JSON value to serialize
+    @param[in] use_count   whether to use '#' prefixes (optimized format)
+    @param[in] use_type    whether to use '$' prefixes (optimized format)
+    @param[in] add_prefix  whether prefixes need to be used for this value
+    */
+    void write_ubjson(const BasicJsonType& j, const bool use_count,
+                      const bool use_type, const bool add_prefix = true)
+    {
+        switch (j.type())
+        {
+            case value_t::null:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(to_char_type('Z'));
+                }
+                break;
+            }
+
+            case value_t::boolean:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(j.m_value.boolean
+                                        ? to_char_type('T')
+                                        : to_char_type('F'));
+                }
+                break;
+            }
+
+            case value_t::number_integer:
+            {
+                write_number_with_ubjson_prefix(j.m_value.number_integer, add_prefix);
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                write_number_with_ubjson_prefix(j.m_value.number_unsigned, add_prefix);
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                write_number_with_ubjson_prefix(j.m_value.number_float, add_prefix);
+                break;
+            }
+
+            case value_t::string:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(to_char_type('S'));
+                }
+                write_number_with_ubjson_prefix(j.m_value.string->size(), true);
+                oa->write_characters(
+                    reinterpret_cast<const CharType*>(j.m_value.string->c_str()),
+                    j.m_value.string->size());
+                break;
+            }
+
+            case value_t::array:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(to_char_type('['));
+                }
+
+                bool prefix_required = true;
+                if (use_type && !j.m_value.array->empty())
+                {
+                    JSON_ASSERT(use_count);
+                    const CharType first_prefix = ubjson_prefix(j.front());
+                    const bool same_prefix = std::all_of(j.begin() + 1, j.end(),
+                                                         [this, first_prefix](const BasicJsonType & v)
+                    {
+                        return ubjson_prefix(v) == first_prefix;
+                    });
+
+                    if (same_prefix)
+                    {
+                        prefix_required = false;
+                        oa->write_character(to_char_type('$'));
+                        oa->write_character(first_prefix);
+                    }
+                }
+
+                if (use_count)
+                {
+                    oa->write_character(to_char_type('#'));
+                    write_number_with_ubjson_prefix(j.m_value.array->size(), true);
+                }
+
+                for (const auto& el : *j.m_value.array)
+                {
+                    write_ubjson(el, use_count, use_type, prefix_required);
+                }
+
+                if (!use_count)
+                {
+                    oa->write_character(to_char_type(']'));
+                }
+
+                break;
+            }
+
+            case value_t::binary:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(to_char_type('['));
+                }
+
+                if (use_type && !j.m_value.binary->empty())
+                {
+                    JSON_ASSERT(use_count);
+                    oa->write_character(to_char_type('$'));
+                    oa->write_character('U');
+                }
+
+                if (use_count)
+                {
+                    oa->write_character(to_char_type('#'));
+                    write_number_with_ubjson_prefix(j.m_value.binary->size(), true);
+                }
+
+                if (use_type)
+                {
+                    oa->write_characters(
+                        reinterpret_cast<const CharType*>(j.m_value.binary->data()),
+                        j.m_value.binary->size());
+                }
+                else
+                {
+                    for (size_t i = 0; i < j.m_value.binary->size(); ++i)
+                    {
+                        oa->write_character(to_char_type('U'));
+                        oa->write_character(j.m_value.binary->data()[i]);
+                    }
+                }
+
+                if (!use_count)
+                {
+                    oa->write_character(to_char_type(']'));
+                }
+
+                break;
+            }
+
+            case value_t::object:
+            {
+                if (add_prefix)
+                {
+                    oa->write_character(to_char_type('{'));
+                }
+
+                bool prefix_required = true;
+                if (use_type && !j.m_value.object->empty())
+                {
+                    JSON_ASSERT(use_count);
+                    const CharType first_prefix = ubjson_prefix(j.front());
+                    const bool same_prefix = std::all_of(j.begin(), j.end(),
+                                                         [this, first_prefix](const BasicJsonType & v)
+                    {
+                        return ubjson_prefix(v) == first_prefix;
+                    });
+
+                    if (same_prefix)
+                    {
+                        prefix_required = false;
+                        oa->write_character(to_char_type('$'));
+                        oa->write_character(first_prefix);
+                    }
+                }
+
+                if (use_count)
+                {
+                    oa->write_character(to_char_type('#'));
+                    write_number_with_ubjson_prefix(j.m_value.object->size(), true);
+                }
+
+                for (const auto& el : *j.m_value.object)
+                {
+                    write_number_with_ubjson_prefix(el.first.size(), true);
+                    oa->write_characters(
+                        reinterpret_cast<const CharType*>(el.first.c_str()),
+                        el.first.size());
+                    write_ubjson(el.second, use_count, use_type, prefix_required);
+                }
+
+                if (!use_count)
+                {
+                    oa->write_character(to_char_type('}'));
+                }
+
+                break;
+            }
+
+            default:
+                break;
+        }
+    }
+
+  private:
+    //////////
+    // BSON //
+    //////////
+
+    /*!
+    @return The size of a BSON document entry header, including the id marker
+            and the entry name size (and its null-terminator).
+    */
+    static std::size_t calc_bson_entry_header_size(const string_t& name)
+    {
+        const auto it = name.find(static_cast<typename string_t::value_type>(0));
+        if (JSON_HEDLEY_UNLIKELY(it != BasicJsonType::string_t::npos))
+        {
+            JSON_THROW(out_of_range::create(409,
+                                            "BSON key cannot contain code point U+0000 (at byte " + std::to_string(it) + ")"));
+        }
+
+        return /*id*/ 1ul + name.size() + /*zero-terminator*/1u;
+    }
+
+    /*!
+    @brief Writes the given @a element_type and @a name to the output adapter
+    */
+    void write_bson_entry_header(const string_t& name,
+                                 const std::uint8_t element_type)
+    {
+        oa->write_character(to_char_type(element_type)); // boolean
+        oa->write_characters(
+            reinterpret_cast<const CharType*>(name.c_str()),
+            name.size() + 1u);
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and boolean value @a value
+    */
+    void write_bson_boolean(const string_t& name,
+                            const bool value)
+    {
+        write_bson_entry_header(name, 0x08);
+        oa->write_character(value ? to_char_type(0x01) : to_char_type(0x00));
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and double value @a value
+    */
+    void write_bson_double(const string_t& name,
+                           const double value)
+    {
+        write_bson_entry_header(name, 0x01);
+        write_number<double, true>(value);
+    }
+
+    /*!
+    @return The size of the BSON-encoded string in @a value
+    */
+    static std::size_t calc_bson_string_size(const string_t& value)
+    {
+        return sizeof(std::int32_t) + value.size() + 1ul;
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and string value @a value
+    */
+    void write_bson_string(const string_t& name,
+                           const string_t& value)
+    {
+        write_bson_entry_header(name, 0x02);
+
+        write_number<std::int32_t, true>(static_cast<std::int32_t>(value.size() + 1ul));
+        oa->write_characters(
+            reinterpret_cast<const CharType*>(value.c_str()),
+            value.size() + 1);
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and null value
+    */
+    void write_bson_null(const string_t& name)
+    {
+        write_bson_entry_header(name, 0x0A);
+    }
+
+    /*!
+    @return The size of the BSON-encoded integer @a value
+    */
+    static std::size_t calc_bson_integer_size(const std::int64_t value)
+    {
+        return (std::numeric_limits<std::int32_t>::min)() <= value && value <= (std::numeric_limits<std::int32_t>::max)()
+               ? sizeof(std::int32_t)
+               : sizeof(std::int64_t);
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and integer @a value
+    */
+    void write_bson_integer(const string_t& name,
+                            const std::int64_t value)
+    {
+        if ((std::numeric_limits<std::int32_t>::min)() <= value && value <= (std::numeric_limits<std::int32_t>::max)())
+        {
+            write_bson_entry_header(name, 0x10); // int32
+            write_number<std::int32_t, true>(static_cast<std::int32_t>(value));
+        }
+        else
+        {
+            write_bson_entry_header(name, 0x12); // int64
+            write_number<std::int64_t, true>(static_cast<std::int64_t>(value));
+        }
+    }
+
+    /*!
+    @return The size of the BSON-encoded unsigned integer in @a j
+    */
+    static constexpr std::size_t calc_bson_unsigned_size(const std::uint64_t value) noexcept
+    {
+        return (value <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
+               ? sizeof(std::int32_t)
+               : sizeof(std::int64_t);
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and unsigned @a value
+    */
+    void write_bson_unsigned(const string_t& name,
+                             const std::uint64_t value)
+    {
+        if (value <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
+        {
+            write_bson_entry_header(name, 0x10 /* int32 */);
+            write_number<std::int32_t, true>(static_cast<std::int32_t>(value));
+        }
+        else if (value <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
+        {
+            write_bson_entry_header(name, 0x12 /* int64 */);
+            write_number<std::int64_t, true>(static_cast<std::int64_t>(value));
+        }
+        else
+        {
+            JSON_THROW(out_of_range::create(407, "integer number " + std::to_string(value) + " cannot be represented by BSON as it does not fit int64"));
+        }
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and object @a value
+    */
+    void write_bson_object_entry(const string_t& name,
+                                 const typename BasicJsonType::object_t& value)
+    {
+        write_bson_entry_header(name, 0x03); // object
+        write_bson_object(value);
+    }
+
+    /*!
+    @return The size of the BSON-encoded array @a value
+    */
+    static std::size_t calc_bson_array_size(const typename BasicJsonType::array_t& value)
+    {
+        std::size_t array_index = 0ul;
+
+        const std::size_t embedded_document_size = std::accumulate(std::begin(value), std::end(value), std::size_t(0), [&array_index](std::size_t result, const typename BasicJsonType::array_t::value_type & el)
+        {
+            return result + calc_bson_element_size(std::to_string(array_index++), el);
+        });
+
+        return sizeof(std::int32_t) + embedded_document_size + 1ul;
+    }
+
+    /*!
+    @return The size of the BSON-encoded binary array @a value
+    */
+    static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value)
+    {
+        return sizeof(std::int32_t) + value.size() + 1ul;
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and array @a value
+    */
+    void write_bson_array(const string_t& name,
+                          const typename BasicJsonType::array_t& value)
+    {
+        write_bson_entry_header(name, 0x04); // array
+        write_number<std::int32_t, true>(static_cast<std::int32_t>(calc_bson_array_size(value)));
+
+        std::size_t array_index = 0ul;
+
+        for (const auto& el : value)
+        {
+            write_bson_element(std::to_string(array_index++), el);
+        }
+
+        oa->write_character(to_char_type(0x00));
+    }
+
+    /*!
+    @brief Writes a BSON element with key @a name and binary value @a value
+    */
+    void write_bson_binary(const string_t& name,
+                           const binary_t& value)
+    {
+        write_bson_entry_header(name, 0x05);
+
+        write_number<std::int32_t, true>(static_cast<std::int32_t>(value.size()));
+        write_number(value.has_subtype() ? value.subtype() : std::uint8_t(0x00));
+
+        oa->write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());
+    }
+
+    /*!
+    @brief Calculates the size necessary to serialize the JSON value @a j with its @a name
+    @return The calculated size for the BSON document entry for @a j with the given @a name.
+    */
+    static std::size_t calc_bson_element_size(const string_t& name,
+            const BasicJsonType& j)
+    {
+        const auto header_size = calc_bson_entry_header_size(name);
+        switch (j.type())
+        {
+            case value_t::object:
+                return header_size + calc_bson_object_size(*j.m_value.object);
+
+            case value_t::array:
+                return header_size + calc_bson_array_size(*j.m_value.array);
+
+            case value_t::binary:
+                return header_size + calc_bson_binary_size(*j.m_value.binary);
+
+            case value_t::boolean:
+                return header_size + 1ul;
+
+            case value_t::number_float:
+                return header_size + 8ul;
+
+            case value_t::number_integer:
+                return header_size + calc_bson_integer_size(j.m_value.number_integer);
+
+            case value_t::number_unsigned:
+                return header_size + calc_bson_unsigned_size(j.m_value.number_unsigned);
+
+            case value_t::string:
+                return header_size + calc_bson_string_size(*j.m_value.string);
+
+            case value_t::null:
+                return header_size + 0ul;
+
+            // LCOV_EXCL_START
+            default:
+                JSON_ASSERT(false);
+                return 0ul;
+                // LCOV_EXCL_STOP
+        }
+    }
+
+    /*!
+    @brief Serializes the JSON value @a j to BSON and associates it with the
+           key @a name.
+    @param name The name to associate with the JSON entity @a j within the
+                current BSON document
+    @return The size of the BSON entry
+    */
+    void write_bson_element(const string_t& name,
+                            const BasicJsonType& j)
+    {
+        switch (j.type())
+        {
+            case value_t::object:
+                return write_bson_object_entry(name, *j.m_value.object);
+
+            case value_t::array:
+                return write_bson_array(name, *j.m_value.array);
+
+            case value_t::binary:
+                return write_bson_binary(name, *j.m_value.binary);
+
+            case value_t::boolean:
+                return write_bson_boolean(name, j.m_value.boolean);
+
+            case value_t::number_float:
+                return write_bson_double(name, j.m_value.number_float);
+
+            case value_t::number_integer:
+                return write_bson_integer(name, j.m_value.number_integer);
+
+            case value_t::number_unsigned:
+                return write_bson_unsigned(name, j.m_value.number_unsigned);
+
+            case value_t::string:
+                return write_bson_string(name, *j.m_value.string);
+
+            case value_t::null:
+                return write_bson_null(name);
+
+            // LCOV_EXCL_START
+            default:
+                JSON_ASSERT(false);
+                return;
+                // LCOV_EXCL_STOP
+        }
+    }
+
+    /*!
+    @brief Calculates the size of the BSON serialization of the given
+           JSON-object @a j.
+    @param[in] j  JSON value to serialize
+    @pre       j.type() == value_t::object
+    */
+    static std::size_t calc_bson_object_size(const typename BasicJsonType::object_t& value)
+    {
+        std::size_t document_size = std::accumulate(value.begin(), value.end(), std::size_t(0),
+                                    [](size_t result, const typename BasicJsonType::object_t::value_type & el)
+        {
+            return result += calc_bson_element_size(el.first, el.second);
+        });
+
+        return sizeof(std::int32_t) + document_size + 1ul;
+    }
+
+    /*!
+    @param[in] j  JSON value to serialize
+    @pre       j.type() == value_t::object
+    */
+    void write_bson_object(const typename BasicJsonType::object_t& value)
+    {
+        write_number<std::int32_t, true>(static_cast<std::int32_t>(calc_bson_object_size(value)));
+
+        for (const auto& el : value)
+        {
+            write_bson_element(el.first, el.second);
+        }
+
+        oa->write_character(to_char_type(0x00));
+    }
+
+    //////////
+    // CBOR //
+    //////////
+
+    static constexpr CharType get_cbor_float_prefix(float /*unused*/)
+    {
+        return to_char_type(0xFA);  // Single-Precision Float
+    }
+
+    static constexpr CharType get_cbor_float_prefix(double /*unused*/)
+    {
+        return to_char_type(0xFB);  // Double-Precision Float
+    }
+
+    /////////////
+    // MsgPack //
+    /////////////
+
+    static constexpr CharType get_msgpack_float_prefix(float /*unused*/)
+    {
+        return to_char_type(0xCA);  // float 32
+    }
+
+    static constexpr CharType get_msgpack_float_prefix(double /*unused*/)
+    {
+        return to_char_type(0xCB);  // float 64
+    }
+
+    ////////////
+    // UBJSON //
+    ////////////
+
+    // UBJSON: write number (floating point)
+    template<typename NumberType, typename std::enable_if<
+                 std::is_floating_point<NumberType>::value, int>::type = 0>
+    void write_number_with_ubjson_prefix(const NumberType n,
+                                         const bool add_prefix)
+    {
+        if (add_prefix)
+        {
+            oa->write_character(get_ubjson_float_prefix(n));
+        }
+        write_number(n);
+    }
+
+    // UBJSON: write number (unsigned integer)
+    template<typename NumberType, typename std::enable_if<
+                 std::is_unsigned<NumberType>::value, int>::type = 0>
+    void write_number_with_ubjson_prefix(const NumberType n,
+                                         const bool add_prefix)
+    {
+        if (n <= static_cast<std::uint64_t>((std::numeric_limits<std::int8_t>::max)()))
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('i'));  // int8
+            }
+            write_number(static_cast<std::uint8_t>(n));
+        }
+        else if (n <= (std::numeric_limits<std::uint8_t>::max)())
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('U'));  // uint8
+            }
+            write_number(static_cast<std::uint8_t>(n));
+        }
+        else if (n <= static_cast<std::uint64_t>((std::numeric_limits<std::int16_t>::max)()))
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('I'));  // int16
+            }
+            write_number(static_cast<std::int16_t>(n));
+        }
+        else if (n <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('l'));  // int32
+            }
+            write_number(static_cast<std::int32_t>(n));
+        }
+        else if (n <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('L'));  // int64
+            }
+            write_number(static_cast<std::int64_t>(n));
+        }
+        else
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('H'));  // high-precision number
+            }
+
+            const auto number = BasicJsonType(n).dump();
+            write_number_with_ubjson_prefix(number.size(), true);
+            for (std::size_t i = 0; i < number.size(); ++i)
+            {
+                oa->write_character(to_char_type(static_cast<std::uint8_t>(number[i])));
+            }
+        }
+    }
+
+    // UBJSON: write number (signed integer)
+    template < typename NumberType, typename std::enable_if <
+                   std::is_signed<NumberType>::value&&
+                   !std::is_floating_point<NumberType>::value, int >::type = 0 >
+    void write_number_with_ubjson_prefix(const NumberType n,
+                                         const bool add_prefix)
+    {
+        if ((std::numeric_limits<std::int8_t>::min)() <= n && n <= (std::numeric_limits<std::int8_t>::max)())
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('i'));  // int8
+            }
+            write_number(static_cast<std::int8_t>(n));
+        }
+        else if (static_cast<std::int64_t>((std::numeric_limits<std::uint8_t>::min)()) <= n && n <= static_cast<std::int64_t>((std::numeric_limits<std::uint8_t>::max)()))
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('U'));  // uint8
+            }
+            write_number(static_cast<std::uint8_t>(n));
+        }
+        else if ((std::numeric_limits<std::int16_t>::min)() <= n && n <= (std::numeric_limits<std::int16_t>::max)())
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('I'));  // int16
+            }
+            write_number(static_cast<std::int16_t>(n));
+        }
+        else if ((std::numeric_limits<std::int32_t>::min)() <= n && n <= (std::numeric_limits<std::int32_t>::max)())
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('l'));  // int32
+            }
+            write_number(static_cast<std::int32_t>(n));
+        }
+        else if ((std::numeric_limits<std::int64_t>::min)() <= n && n <= (std::numeric_limits<std::int64_t>::max)())
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('L'));  // int64
+            }
+            write_number(static_cast<std::int64_t>(n));
+        }
+        // LCOV_EXCL_START
+        else
+        {
+            if (add_prefix)
+            {
+                oa->write_character(to_char_type('H'));  // high-precision number
+            }
+
+            const auto number = BasicJsonType(n).dump();
+            write_number_with_ubjson_prefix(number.size(), true);
+            for (std::size_t i = 0; i < number.size(); ++i)
+            {
+                oa->write_character(to_char_type(static_cast<std::uint8_t>(number[i])));
+            }
+        }
+        // LCOV_EXCL_STOP
+    }
+
+    /*!
+    @brief determine the type prefix of container values
+    */
+    CharType ubjson_prefix(const BasicJsonType& j) const noexcept
+    {
+        switch (j.type())
+        {
+            case value_t::null:
+                return 'Z';
+
+            case value_t::boolean:
+                return j.m_value.boolean ? 'T' : 'F';
+
+            case value_t::number_integer:
+            {
+                if ((std::numeric_limits<std::int8_t>::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits<std::int8_t>::max)())
+                {
+                    return 'i';
+                }
+                if ((std::numeric_limits<std::uint8_t>::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits<std::uint8_t>::max)())
+                {
+                    return 'U';
+                }
+                if ((std::numeric_limits<std::int16_t>::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits<std::int16_t>::max)())
+                {
+                    return 'I';
+                }
+                if ((std::numeric_limits<std::int32_t>::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits<std::int32_t>::max)())
+                {
+                    return 'l';
+                }
+                if ((std::numeric_limits<std::int64_t>::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits<std::int64_t>::max)())
+                {
+                    return 'L';
+                }
+                // anything else is treated as high-precision number
+                return 'H'; // LCOV_EXCL_LINE
+            }
+
+            case value_t::number_unsigned:
+            {
+                if (j.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int8_t>::max)()))
+                {
+                    return 'i';
+                }
+                if (j.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::uint8_t>::max)()))
+                {
+                    return 'U';
+                }
+                if (j.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int16_t>::max)()))
+                {
+                    return 'I';
+                }
+                if (j.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
+                {
+                    return 'l';
+                }
+                if (j.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
+                {
+                    return 'L';
+                }
+                // anything else is treated as high-precision number
+                return 'H'; // LCOV_EXCL_LINE
+            }
+
+            case value_t::number_float:
+                return get_ubjson_float_prefix(j.m_value.number_float);
+
+            case value_t::string:
+                return 'S';
+
+            case value_t::array: // fallthrough
+            case value_t::binary:
+                return '[';
+
+            case value_t::object:
+                return '{';
+
+            default:  // discarded values
+                return 'N';
+        }
+    }
+
+    static constexpr CharType get_ubjson_float_prefix(float /*unused*/)
+    {
+        return 'd';  // float 32
+    }
+
+    static constexpr CharType get_ubjson_float_prefix(double /*unused*/)
+    {
+        return 'D';  // float 64
+    }
+
+    ///////////////////////
+    // Utility functions //
+    ///////////////////////
+
+    /*
+    @brief write a number to output input
+    @param[in] n number of type @a NumberType
+    @tparam NumberType the type of the number
+    @tparam OutputIsLittleEndian Set to true if output data is
+                                 required to be little endian
+
+    @note This function needs to respect the system's endianess, because bytes
+          in CBOR, MessagePack, and UBJSON are stored in network order (big
+          endian) and therefore need reordering on little endian systems.
+    */
+    template<typename NumberType, bool OutputIsLittleEndian = false>
+    void write_number(const NumberType n)
+    {
+        // step 1: write number to array of length NumberType
+        std::array<CharType, sizeof(NumberType)> vec;
+        std::memcpy(vec.data(), &n, sizeof(NumberType));
+
+        // step 2: write array to output (with possible reordering)
+        if (is_little_endian != OutputIsLittleEndian)
+        {
+            // reverse byte order prior to conversion if necessary
+            std::reverse(vec.begin(), vec.end());
+        }
+
+        oa->write_characters(vec.data(), sizeof(NumberType));
+    }
+
+    void write_compact_float(const number_float_t n, detail::input_format_t format)
+    {
+        if (static_cast<double>(n) >= static_cast<double>(std::numeric_limits<float>::lowest()) &&
+                static_cast<double>(n) <= static_cast<double>((std::numeric_limits<float>::max)()) &&
+                static_cast<double>(static_cast<float>(n)) == static_cast<double>(n))
+        {
+            oa->write_character(format == detail::input_format_t::cbor
+                                ? get_cbor_float_prefix(static_cast<float>(n))
+                                : get_msgpack_float_prefix(static_cast<float>(n)));
+            write_number(static_cast<float>(n));
+        }
+        else
+        {
+            oa->write_character(format == detail::input_format_t::cbor
+                                ? get_cbor_float_prefix(n)
+                                : get_msgpack_float_prefix(n));
+            write_number(n);
+        }
+    }
+
+  public:
+    // The following to_char_type functions are implement the conversion
+    // between uint8_t and CharType. In case CharType is not unsigned,
+    // such a conversion is required to allow values greater than 128.
+    // See <https://github.com/nlohmann/json/issues/1286> for a discussion.
+    template < typename C = CharType,
+               enable_if_t < std::is_signed<C>::value && std::is_signed<char>::value > * = nullptr >
+    static constexpr CharType to_char_type(std::uint8_t x) noexcept
+    {
+        return *reinterpret_cast<char*>(&x);
+    }
+
+    template < typename C = CharType,
+               enable_if_t < std::is_signed<C>::value && std::is_unsigned<char>::value > * = nullptr >
+    static CharType to_char_type(std::uint8_t x) noexcept
+    {
+        static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t");
+        static_assert(std::is_trivial<CharType>::value, "CharType must be trivial");
+        CharType result;
+        std::memcpy(&result, &x, sizeof(x));
+        return result;
+    }
+
+    template<typename C = CharType,
+             enable_if_t<std::is_unsigned<C>::value>* = nullptr>
+    static constexpr CharType to_char_type(std::uint8_t x) noexcept
+    {
+        return x;
+    }
+
+    template < typename InputCharType, typename C = CharType,
+               enable_if_t <
+                   std::is_signed<C>::value &&
+                   std::is_signed<char>::value &&
+                   std::is_same<char, typename std::remove_cv<InputCharType>::type>::value
+                   > * = nullptr >
+    static constexpr CharType to_char_type(InputCharType x) noexcept
+    {
+        return x;
+    }
+
+  private:
+    /// whether we can assume little endianess
+    const bool is_little_endian = little_endianess();
+
+    /// the output
+    output_adapter_t<CharType> oa = nullptr;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/output/output_adapters.hpp>
+
+// #include <nlohmann/detail/output/serializer.hpp>
+
+
+#include <algorithm> // reverse, remove, fill, find, none_of
+#include <array> // array
+#include <clocale> // localeconv, lconv
+#include <cmath> // labs, isfinite, isnan, signbit
+#include <cstddef> // size_t, ptrdiff_t
+#include <cstdint> // uint8_t
+#include <cstdio> // snprintf
+#include <limits> // numeric_limits
+#include <string> // string, char_traits
+#include <type_traits> // is_same
+#include <utility> // move
+
+// #include <nlohmann/detail/conversions/to_chars.hpp>
+
+
+#include <array> // array
+#include <cmath>   // signbit, isfinite
+#include <cstdint> // intN_t, uintN_t
+#include <cstring> // memcpy, memmove
+#include <limits> // numeric_limits
+#include <type_traits> // conditional
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+
+/*!
+@brief implements the Grisu2 algorithm for binary to decimal floating-point
+conversion.
+
+This implementation is a slightly modified version of the reference
+implementation which may be obtained from
+http://florian.loitsch.com/publications (bench.tar.gz).
+
+The code is distributed under the MIT license, Copyright (c) 2009 Florian Loitsch.
+
+For a detailed description of the algorithm see:
+
+[1] Loitsch, "Printing Floating-Point Numbers Quickly and Accurately with
+    Integers", Proceedings of the ACM SIGPLAN 2010 Conference on Programming
+    Language Design and Implementation, PLDI 2010
+[2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and Accurately",
+    Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language
+    Design and Implementation, PLDI 1996
+*/
+namespace dtoa_impl
+{
+
+template<typename Target, typename Source>
+Target reinterpret_bits(const Source source)
+{
+    static_assert(sizeof(Target) == sizeof(Source), "size mismatch");
+
+    Target target;
+    std::memcpy(&target, &source, sizeof(Source));
+    return target;
+}
+
+struct diyfp // f * 2^e
+{
+    static constexpr int kPrecision = 64; // = q
+
+    std::uint64_t f = 0;
+    int e = 0;
+
+    constexpr diyfp(std::uint64_t f_, int e_) noexcept : f(f_), e(e_) {}
+
+    /*!
+    @brief returns x - y
+    @pre x.e == y.e and x.f >= y.f
+    */
+    static diyfp sub(const diyfp& x, const diyfp& y) noexcept
+    {
+        JSON_ASSERT(x.e == y.e);
+        JSON_ASSERT(x.f >= y.f);
+
+        return {x.f - y.f, x.e};
+    }
+
+    /*!
+    @brief returns x * y
+    @note The result is rounded. (Only the upper q bits are returned.)
+    */
+    static diyfp mul(const diyfp& x, const diyfp& y) noexcept
+    {
+        static_assert(kPrecision == 64, "internal error");
+
+        // Computes:
+        //  f = round((x.f * y.f) / 2^q)
+        //  e = x.e + y.e + q
+
+        // Emulate the 64-bit * 64-bit multiplication:
+        //
+        // p = u * v
+        //   = (u_lo + 2^32 u_hi) (v_lo + 2^32 v_hi)
+        //   = (u_lo v_lo         ) + 2^32 ((u_lo v_hi         ) + (u_hi v_lo         )) + 2^64 (u_hi v_hi         )
+        //   = (p0                ) + 2^32 ((p1                ) + (p2                )) + 2^64 (p3                )
+        //   = (p0_lo + 2^32 p0_hi) + 2^32 ((p1_lo + 2^32 p1_hi) + (p2_lo + 2^32 p2_hi)) + 2^64 (p3                )
+        //   = (p0_lo             ) + 2^32 (p0_hi + p1_lo + p2_lo                      ) + 2^64 (p1_hi + p2_hi + p3)
+        //   = (p0_lo             ) + 2^32 (Q                                          ) + 2^64 (H                 )
+        //   = (p0_lo             ) + 2^32 (Q_lo + 2^32 Q_hi                           ) + 2^64 (H                 )
+        //
+        // (Since Q might be larger than 2^32 - 1)
+        //
+        //   = (p0_lo + 2^32 Q_lo) + 2^64 (Q_hi + H)
+        //
+        // (Q_hi + H does not overflow a 64-bit int)
+        //
+        //   = p_lo + 2^64 p_hi
+
+        const std::uint64_t u_lo = x.f & 0xFFFFFFFFu;
+        const std::uint64_t u_hi = x.f >> 32u;
+        const std::uint64_t v_lo = y.f & 0xFFFFFFFFu;
+        const std::uint64_t v_hi = y.f >> 32u;
+
+        const std::uint64_t p0 = u_lo * v_lo;
+        const std::uint64_t p1 = u_lo * v_hi;
+        const std::uint64_t p2 = u_hi * v_lo;
+        const std::uint64_t p3 = u_hi * v_hi;
+
+        const std::uint64_t p0_hi = p0 >> 32u;
+        const std::uint64_t p1_lo = p1 & 0xFFFFFFFFu;
+        const std::uint64_t p1_hi = p1 >> 32u;
+        const std::uint64_t p2_lo = p2 & 0xFFFFFFFFu;
+        const std::uint64_t p2_hi = p2 >> 32u;
+
+        std::uint64_t Q = p0_hi + p1_lo + p2_lo;
+
+        // The full product might now be computed as
+        //
+        // p_hi = p3 + p2_hi + p1_hi + (Q >> 32)
+        // p_lo = p0_lo + (Q << 32)
+        //
+        // But in this particular case here, the full p_lo is not required.
+        // Effectively we only need to add the highest bit in p_lo to p_hi (and
+        // Q_hi + 1 does not overflow).
+
+        Q += std::uint64_t{1} << (64u - 32u - 1u); // round, ties up
+
+        const std::uint64_t h = p3 + p2_hi + p1_hi + (Q >> 32u);
+
+        return {h, x.e + y.e + 64};
+    }
+
+    /*!
+    @brief normalize x such that the significand is >= 2^(q-1)
+    @pre x.f != 0
+    */
+    static diyfp normalize(diyfp x) noexcept
+    {
+        JSON_ASSERT(x.f != 0);
+
+        while ((x.f >> 63u) == 0)
+        {
+            x.f <<= 1u;
+            x.e--;
+        }
+
+        return x;
+    }
+
+    /*!
+    @brief normalize x such that the result has the exponent E
+    @pre e >= x.e and the upper e - x.e bits of x.f must be zero.
+    */
+    static diyfp normalize_to(const diyfp& x, const int target_exponent) noexcept
+    {
+        const int delta = x.e - target_exponent;
+
+        JSON_ASSERT(delta >= 0);
+        JSON_ASSERT(((x.f << delta) >> delta) == x.f);
+
+        return {x.f << delta, target_exponent};
+    }
+};
+
+struct boundaries
+{
+    diyfp w;
+    diyfp minus;
+    diyfp plus;
+};
+
+/*!
+Compute the (normalized) diyfp representing the input number 'value' and its
+boundaries.
+
+@pre value must be finite and positive
+*/
+template<typename FloatType>
+boundaries compute_boundaries(FloatType value)
+{
+    JSON_ASSERT(std::isfinite(value));
+    JSON_ASSERT(value > 0);
+
+    // Convert the IEEE representation into a diyfp.
+    //
+    // If v is denormal:
+    //      value = 0.F * 2^(1 - bias) = (          F) * 2^(1 - bias - (p-1))
+    // If v is normalized:
+    //      value = 1.F * 2^(E - bias) = (2^(p-1) + F) * 2^(E - bias - (p-1))
+
+    static_assert(std::numeric_limits<FloatType>::is_iec559,
+                  "internal error: dtoa_short requires an IEEE-754 floating-point implementation");
+
+    constexpr int      kPrecision = std::numeric_limits<FloatType>::digits; // = p (includes the hidden bit)
+    constexpr int      kBias      = std::numeric_limits<FloatType>::max_exponent - 1 + (kPrecision - 1);
+    constexpr int      kMinExp    = 1 - kBias;
+    constexpr std::uint64_t kHiddenBit = std::uint64_t{1} << (kPrecision - 1); // = 2^(p-1)
+
+    using bits_type = typename std::conditional<kPrecision == 24, std::uint32_t, std::uint64_t >::type;
+
+    const std::uint64_t bits = reinterpret_bits<bits_type>(value);
+    const std::uint64_t E = bits >> (kPrecision - 1);
+    const std::uint64_t F = bits & (kHiddenBit - 1);
+
+    const bool is_denormal = E == 0;
+    const diyfp v = is_denormal
+                    ? diyfp(F, kMinExp)
+                    : diyfp(F + kHiddenBit, static_cast<int>(E) - kBias);
+
+    // Compute the boundaries m- and m+ of the floating-point value
+    // v = f * 2^e.
+    //
+    // Determine v- and v+, the floating-point predecessor and successor if v,
+    // respectively.
+    //
+    //      v- = v - 2^e        if f != 2^(p-1) or e == e_min                (A)
+    //         = v - 2^(e-1)    if f == 2^(p-1) and e > e_min                (B)
+    //
+    //      v+ = v + 2^e
+    //
+    // Let m- = (v- + v) / 2 and m+ = (v + v+) / 2. All real numbers _strictly_
+    // between m- and m+ round to v, regardless of how the input rounding
+    // algorithm breaks ties.
+    //
+    //      ---+-------------+-------------+-------------+-------------+---  (A)
+    //         v-            m-            v             m+            v+
+    //
+    //      -----------------+------+------+-------------+-------------+---  (B)
+    //                       v-     m-     v             m+            v+
+
+    const bool lower_boundary_is_closer = F == 0 && E > 1;
+    const diyfp m_plus = diyfp(2 * v.f + 1, v.e - 1);
+    const diyfp m_minus = lower_boundary_is_closer
+                          ? diyfp(4 * v.f - 1, v.e - 2)  // (B)
+                          : diyfp(2 * v.f - 1, v.e - 1); // (A)
+
+    // Determine the normalized w+ = m+.
+    const diyfp w_plus = diyfp::normalize(m_plus);
+
+    // Determine w- = m- such that e_(w-) = e_(w+).
+    const diyfp w_minus = diyfp::normalize_to(m_minus, w_plus.e);
+
+    return {diyfp::normalize(v), w_minus, w_plus};
+}
+
+// Given normalized diyfp w, Grisu needs to find a (normalized) cached
+// power-of-ten c, such that the exponent of the product c * w = f * 2^e lies
+// within a certain range [alpha, gamma] (Definition 3.2 from [1])
+//
+//      alpha <= e = e_c + e_w + q <= gamma
+//
+// or
+//
+//      f_c * f_w * 2^alpha <= f_c 2^(e_c) * f_w 2^(e_w) * 2^q
+//                          <= f_c * f_w * 2^gamma
+//
+// Since c and w are normalized, i.e. 2^(q-1) <= f < 2^q, this implies
+//
+//      2^(q-1) * 2^(q-1) * 2^alpha <= c * w * 2^q < 2^q * 2^q * 2^gamma
+//
+// or
+//
+//      2^(q - 2 + alpha) <= c * w < 2^(q + gamma)
+//
+// The choice of (alpha,gamma) determines the size of the table and the form of
+// the digit generation procedure. Using (alpha,gamma)=(-60,-32) works out well
+// in practice:
+//
+// The idea is to cut the number c * w = f * 2^e into two parts, which can be
+// processed independently: An integral part p1, and a fractional part p2:
+//
+//      f * 2^e = ( (f div 2^-e) * 2^-e + (f mod 2^-e) ) * 2^e
+//              = (f div 2^-e) + (f mod 2^-e) * 2^e
+//              = p1 + p2 * 2^e
+//
+// The conversion of p1 into decimal form requires a series of divisions and
+// modulos by (a power of) 10. These operations are faster for 32-bit than for
+// 64-bit integers, so p1 should ideally fit into a 32-bit integer. This can be
+// achieved by choosing
+//
+//      -e >= 32   or   e <= -32 := gamma
+//
+// In order to convert the fractional part
+//
+//      p2 * 2^e = p2 / 2^-e = d[-1] / 10^1 + d[-2] / 10^2 + ...
+//
+// into decimal form, the fraction is repeatedly multiplied by 10 and the digits
+// d[-i] are extracted in order:
+//
+//      (10 * p2) div 2^-e = d[-1]
+//      (10 * p2) mod 2^-e = d[-2] / 10^1 + ...
+//
+// The multiplication by 10 must not overflow. It is sufficient to choose
+//
+//      10 * p2 < 16 * p2 = 2^4 * p2 <= 2^64.
+//
+// Since p2 = f mod 2^-e < 2^-e,
+//
+//      -e <= 60   or   e >= -60 := alpha
+
+constexpr int kAlpha = -60;
+constexpr int kGamma = -32;
+
+struct cached_power // c = f * 2^e ~= 10^k
+{
+    std::uint64_t f;
+    int e;
+    int k;
+};
+
+/*!
+For a normalized diyfp w = f * 2^e, this function returns a (normalized) cached
+power-of-ten c = f_c * 2^e_c, such that the exponent of the product w * c
+satisfies (Definition 3.2 from [1])
+
+     alpha <= e_c + e + q <= gamma.
+*/
+inline cached_power get_cached_power_for_binary_exponent(int e)
+{
+    // Now
+    //
+    //      alpha <= e_c + e + q <= gamma                                    (1)
+    //      ==> f_c * 2^alpha <= c * 2^e * 2^q
+    //
+    // and since the c's are normalized, 2^(q-1) <= f_c,
+    //
+    //      ==> 2^(q - 1 + alpha) <= c * 2^(e + q)
+    //      ==> 2^(alpha - e - 1) <= c
+    //
+    // If c were an exact power of ten, i.e. c = 10^k, one may determine k as
+    //
+    //      k = ceil( log_10( 2^(alpha - e - 1) ) )
+    //        = ceil( (alpha - e - 1) * log_10(2) )
+    //
+    // From the paper:
+    // "In theory the result of the procedure could be wrong since c is rounded,
+    //  and the computation itself is approximated [...]. In practice, however,
+    //  this simple function is sufficient."
+    //
+    // For IEEE double precision floating-point numbers converted into
+    // normalized diyfp's w = f * 2^e, with q = 64,
+    //
+    //      e >= -1022      (min IEEE exponent)
+    //           -52        (p - 1)
+    //           -52        (p - 1, possibly normalize denormal IEEE numbers)
+    //           -11        (normalize the diyfp)
+    //         = -1137
+    //
+    // and
+    //
+    //      e <= +1023      (max IEEE exponent)
+    //           -52        (p - 1)
+    //           -11        (normalize the diyfp)
+    //         = 960
+    //
+    // This binary exponent range [-1137,960] results in a decimal exponent
+    // range [-307,324]. One does not need to store a cached power for each
+    // k in this range. For each such k it suffices to find a cached power
+    // such that the exponent of the product lies in [alpha,gamma].
+    // This implies that the difference of the decimal exponents of adjacent
+    // table entries must be less than or equal to
+    //
+    //      floor( (gamma - alpha) * log_10(2) ) = 8.
+    //
+    // (A smaller distance gamma-alpha would require a larger table.)
+
+    // NB:
+    // Actually this function returns c, such that -60 <= e_c + e + 64 <= -34.
+
+    constexpr int kCachedPowersMinDecExp = -300;
+    constexpr int kCachedPowersDecStep = 8;
+
+    static constexpr std::array<cached_power, 79> kCachedPowers =
+    {
+        {
+            { 0xAB70FE17C79AC6CA, -1060, -300 },
+            { 0xFF77B1FCBEBCDC4F, -1034, -292 },
+            { 0xBE5691EF416BD60C, -1007, -284 },
+            { 0x8DD01FAD907FFC3C,  -980, -276 },
+            { 0xD3515C2831559A83,  -954, -268 },
+            { 0x9D71AC8FADA6C9B5,  -927, -260 },
+            { 0xEA9C227723EE8BCB,  -901, -252 },
+            { 0xAECC49914078536D,  -874, -244 },
+            { 0x823C12795DB6CE57,  -847, -236 },
+            { 0xC21094364DFB5637,  -821, -228 },
+            { 0x9096EA6F3848984F,  -794, -220 },
+            { 0xD77485CB25823AC7,  -768, -212 },
+            { 0xA086CFCD97BF97F4,  -741, -204 },
+            { 0xEF340A98172AACE5,  -715, -196 },
+            { 0xB23867FB2A35B28E,  -688, -188 },
+            { 0x84C8D4DFD2C63F3B,  -661, -180 },
+            { 0xC5DD44271AD3CDBA,  -635, -172 },
+            { 0x936B9FCEBB25C996,  -608, -164 },
+            { 0xDBAC6C247D62A584,  -582, -156 },
+            { 0xA3AB66580D5FDAF6,  -555, -148 },
+            { 0xF3E2F893DEC3F126,  -529, -140 },
+            { 0xB5B5ADA8AAFF80B8,  -502, -132 },
+            { 0x87625F056C7C4A8B,  -475, -124 },
+            { 0xC9BCFF6034C13053,  -449, -116 },
+            { 0x964E858C91BA2655,  -422, -108 },
+            { 0xDFF9772470297EBD,  -396, -100 },
+            { 0xA6DFBD9FB8E5B88F,  -369,  -92 },
+            { 0xF8A95FCF88747D94,  -343,  -84 },
+            { 0xB94470938FA89BCF,  -316,  -76 },
+            { 0x8A08F0F8BF0F156B,  -289,  -68 },
+            { 0xCDB02555653131B6,  -263,  -60 },
+            { 0x993FE2C6D07B7FAC,  -236,  -52 },
+            { 0xE45C10C42A2B3B06,  -210,  -44 },
+            { 0xAA242499697392D3,  -183,  -36 },
+            { 0xFD87B5F28300CA0E,  -157,  -28 },
+            { 0xBCE5086492111AEB,  -130,  -20 },
+            { 0x8CBCCC096F5088CC,  -103,  -12 },
+            { 0xD1B71758E219652C,   -77,   -4 },
+            { 0x9C40000000000000,   -50,    4 },
+            { 0xE8D4A51000000000,   -24,   12 },
+            { 0xAD78EBC5AC620000,     3,   20 },
+            { 0x813F3978F8940984,    30,   28 },
+            { 0xC097CE7BC90715B3,    56,   36 },
+            { 0x8F7E32CE7BEA5C70,    83,   44 },
+            { 0xD5D238A4ABE98068,   109,   52 },
+            { 0x9F4F2726179A2245,   136,   60 },
+            { 0xED63A231D4C4FB27,   162,   68 },
+            { 0xB0DE65388CC8ADA8,   189,   76 },
+            { 0x83C7088E1AAB65DB,   216,   84 },
+            { 0xC45D1DF942711D9A,   242,   92 },
+            { 0x924D692CA61BE758,   269,  100 },
+            { 0xDA01EE641A708DEA,   295,  108 },
+            { 0xA26DA3999AEF774A,   322,  116 },
+            { 0xF209787BB47D6B85,   348,  124 },
+            { 0xB454E4A179DD1877,   375,  132 },
+            { 0x865B86925B9BC5C2,   402,  140 },
+            { 0xC83553C5C8965D3D,   428,  148 },
+            { 0x952AB45CFA97A0B3,   455,  156 },
+            { 0xDE469FBD99A05FE3,   481,  164 },
+            { 0xA59BC234DB398C25,   508,  172 },
+            { 0xF6C69A72A3989F5C,   534,  180 },
+            { 0xB7DCBF5354E9BECE,   561,  188 },
+            { 0x88FCF317F22241E2,   588,  196 },
+            { 0xCC20CE9BD35C78A5,   614,  204 },
+            { 0x98165AF37B2153DF,   641,  212 },
+            { 0xE2A0B5DC971F303A,   667,  220 },
+            { 0xA8D9D1535CE3B396,   694,  228 },
+            { 0xFB9B7CD9A4A7443C,   720,  236 },
+            { 0xBB764C4CA7A44410,   747,  244 },
+            { 0x8BAB8EEFB6409C1A,   774,  252 },
+            { 0xD01FEF10A657842C,   800,  260 },
+            { 0x9B10A4E5E9913129,   827,  268 },
+            { 0xE7109BFBA19C0C9D,   853,  276 },
+            { 0xAC2820D9623BF429,   880,  284 },
+            { 0x80444B5E7AA7CF85,   907,  292 },
+            { 0xBF21E44003ACDD2D,   933,  300 },
+            { 0x8E679C2F5E44FF8F,   960,  308 },
+            { 0xD433179D9C8CB841,   986,  316 },
+            { 0x9E19DB92B4E31BA9,  1013,  324 },
+        }
+    };
+
+    // This computation gives exactly the same results for k as
+    //      k = ceil((kAlpha - e - 1) * 0.30102999566398114)
+    // for |e| <= 1500, but doesn't require floating-point operations.
+    // NB: log_10(2) ~= 78913 / 2^18
+    JSON_ASSERT(e >= -1500);
+    JSON_ASSERT(e <=  1500);
+    const int f = kAlpha - e - 1;
+    const int k = (f * 78913) / (1 << 18) + static_cast<int>(f > 0);
+
+    const int index = (-kCachedPowersMinDecExp + k + (kCachedPowersDecStep - 1)) / kCachedPowersDecStep;
+    JSON_ASSERT(index >= 0);
+    JSON_ASSERT(static_cast<std::size_t>(index) < kCachedPowers.size());
+
+    const cached_power cached = kCachedPowers[static_cast<std::size_t>(index)];
+    JSON_ASSERT(kAlpha <= cached.e + e + 64);
+    JSON_ASSERT(kGamma >= cached.e + e + 64);
+
+    return cached;
+}
+
+/*!
+For n != 0, returns k, such that pow10 := 10^(k-1) <= n < 10^k.
+For n == 0, returns 1 and sets pow10 := 1.
+*/
+inline int find_largest_pow10(const std::uint32_t n, std::uint32_t& pow10)
+{
+    // LCOV_EXCL_START
+    if (n >= 1000000000)
+    {
+        pow10 = 1000000000;
+        return 10;
+    }
+    // LCOV_EXCL_STOP
+    else if (n >= 100000000)
+    {
+        pow10 = 100000000;
+        return  9;
+    }
+    else if (n >= 10000000)
+    {
+        pow10 = 10000000;
+        return  8;
+    }
+    else if (n >= 1000000)
+    {
+        pow10 = 1000000;
+        return  7;
+    }
+    else if (n >= 100000)
+    {
+        pow10 = 100000;
+        return  6;
+    }
+    else if (n >= 10000)
+    {
+        pow10 = 10000;
+        return  5;
+    }
+    else if (n >= 1000)
+    {
+        pow10 = 1000;
+        return  4;
+    }
+    else if (n >= 100)
+    {
+        pow10 = 100;
+        return  3;
+    }
+    else if (n >= 10)
+    {
+        pow10 = 10;
+        return  2;
+    }
+    else
+    {
+        pow10 = 1;
+        return 1;
+    }
+}
+
+inline void grisu2_round(char* buf, int len, std::uint64_t dist, std::uint64_t delta,
+                         std::uint64_t rest, std::uint64_t ten_k)
+{
+    JSON_ASSERT(len >= 1);
+    JSON_ASSERT(dist <= delta);
+    JSON_ASSERT(rest <= delta);
+    JSON_ASSERT(ten_k > 0);
+
+    //               <--------------------------- delta ---->
+    //                                  <---- dist --------->
+    // --------------[------------------+-------------------]--------------
+    //               M-                 w                   M+
+    //
+    //                                  ten_k
+    //                                <------>
+    //                                       <---- rest ---->
+    // --------------[------------------+----+--------------]--------------
+    //                                  w    V
+    //                                       = buf * 10^k
+    //
+    // ten_k represents a unit-in-the-last-place in the decimal representation
+    // stored in buf.
+    // Decrement buf by ten_k while this takes buf closer to w.
+
+    // The tests are written in this order to avoid overflow in unsigned
+    // integer arithmetic.
+
+    while (rest < dist
+            && delta - rest >= ten_k
+            && (rest + ten_k < dist || dist - rest > rest + ten_k - dist))
+    {
+        JSON_ASSERT(buf[len - 1] != '0');
+        buf[len - 1]--;
+        rest += ten_k;
+    }
+}
+
+/*!
+Generates V = buffer * 10^decimal_exponent, such that M- <= V <= M+.
+M- and M+ must be normalized and share the same exponent -60 <= e <= -32.
+*/
+inline void grisu2_digit_gen(char* buffer, int& length, int& decimal_exponent,
+                             diyfp M_minus, diyfp w, diyfp M_plus)
+{
+    static_assert(kAlpha >= -60, "internal error");
+    static_assert(kGamma <= -32, "internal error");
+
+    // Generates the digits (and the exponent) of a decimal floating-point
+    // number V = buffer * 10^decimal_exponent in the range [M-, M+]. The diyfp's
+    // w, M- and M+ share the same exponent e, which satisfies alpha <= e <= gamma.
+    //
+    //               <--------------------------- delta ---->
+    //                                  <---- dist --------->
+    // --------------[------------------+-------------------]--------------
+    //               M-                 w                   M+
+    //
+    // Grisu2 generates the digits of M+ from left to right and stops as soon as
+    // V is in [M-,M+].
+
+    JSON_ASSERT(M_plus.e >= kAlpha);
+    JSON_ASSERT(M_plus.e <= kGamma);
+
+    std::uint64_t delta = diyfp::sub(M_plus, M_minus).f; // (significand of (M+ - M-), implicit exponent is e)
+    std::uint64_t dist  = diyfp::sub(M_plus, w      ).f; // (significand of (M+ - w ), implicit exponent is e)
+
+    // Split M+ = f * 2^e into two parts p1 and p2 (note: e < 0):
+    //
+    //      M+ = f * 2^e
+    //         = ((f div 2^-e) * 2^-e + (f mod 2^-e)) * 2^e
+    //         = ((p1        ) * 2^-e + (p2        )) * 2^e
+    //         = p1 + p2 * 2^e
+
+    const diyfp one(std::uint64_t{1} << -M_plus.e, M_plus.e);
+
+    auto p1 = static_cast<std::uint32_t>(M_plus.f >> -one.e); // p1 = f div 2^-e (Since -e >= 32, p1 fits into a 32-bit int.)
+    std::uint64_t p2 = M_plus.f & (one.f - 1);                    // p2 = f mod 2^-e
+
+    // 1)
+    //
+    // Generate the digits of the integral part p1 = d[n-1]...d[1]d[0]
+
+    JSON_ASSERT(p1 > 0);
+
+    std::uint32_t pow10;
+    const int k = find_largest_pow10(p1, pow10);
+
+    //      10^(k-1) <= p1 < 10^k, pow10 = 10^(k-1)
+    //
+    //      p1 = (p1 div 10^(k-1)) * 10^(k-1) + (p1 mod 10^(k-1))
+    //         = (d[k-1]         ) * 10^(k-1) + (p1 mod 10^(k-1))
+    //
+    //      M+ = p1                                             + p2 * 2^e
+    //         = d[k-1] * 10^(k-1) + (p1 mod 10^(k-1))          + p2 * 2^e
+    //         = d[k-1] * 10^(k-1) + ((p1 mod 10^(k-1)) * 2^-e + p2) * 2^e
+    //         = d[k-1] * 10^(k-1) + (                         rest) * 2^e
+    //
+    // Now generate the digits d[n] of p1 from left to right (n = k-1,...,0)
+    //
+    //      p1 = d[k-1]...d[n] * 10^n + d[n-1]...d[0]
+    //
+    // but stop as soon as
+    //
+    //      rest * 2^e = (d[n-1]...d[0] * 2^-e + p2) * 2^e <= delta * 2^e
+
+    int n = k;
+    while (n > 0)
+    {
+        // Invariants:
+        //      M+ = buffer * 10^n + (p1 + p2 * 2^e)    (buffer = 0 for n = k)
+        //      pow10 = 10^(n-1) <= p1 < 10^n
+        //
+        const std::uint32_t d = p1 / pow10;  // d = p1 div 10^(n-1)
+        const std::uint32_t r = p1 % pow10;  // r = p1 mod 10^(n-1)
+        //
+        //      M+ = buffer * 10^n + (d * 10^(n-1) + r) + p2 * 2^e
+        //         = (buffer * 10 + d) * 10^(n-1) + (r + p2 * 2^e)
+        //
+        JSON_ASSERT(d <= 9);
+        buffer[length++] = static_cast<char>('0' + d); // buffer := buffer * 10 + d
+        //
+        //      M+ = buffer * 10^(n-1) + (r + p2 * 2^e)
+        //
+        p1 = r;
+        n--;
+        //
+        //      M+ = buffer * 10^n + (p1 + p2 * 2^e)
+        //      pow10 = 10^n
+        //
+
+        // Now check if enough digits have been generated.
+        // Compute
+        //
+        //      p1 + p2 * 2^e = (p1 * 2^-e + p2) * 2^e = rest * 2^e
+        //
+        // Note:
+        // Since rest and delta share the same exponent e, it suffices to
+        // compare the significands.
+        const std::uint64_t rest = (std::uint64_t{p1} << -one.e) + p2;
+        if (rest <= delta)
+        {
+            // V = buffer * 10^n, with M- <= V <= M+.
+
+            decimal_exponent += n;
+
+            // We may now just stop. But instead look if the buffer could be
+            // decremented to bring V closer to w.
+            //
+            // pow10 = 10^n is now 1 ulp in the decimal representation V.
+            // The rounding procedure works with diyfp's with an implicit
+            // exponent of e.
+            //
+            //      10^n = (10^n * 2^-e) * 2^e = ulp * 2^e
+            //
+            const std::uint64_t ten_n = std::uint64_t{pow10} << -one.e;
+            grisu2_round(buffer, length, dist, delta, rest, ten_n);
+
+            return;
+        }
+
+        pow10 /= 10;
+        //
+        //      pow10 = 10^(n-1) <= p1 < 10^n
+        // Invariants restored.
+    }
+
+    // 2)
+    //
+    // The digits of the integral part have been generated:
+    //
+    //      M+ = d[k-1]...d[1]d[0] + p2 * 2^e
+    //         = buffer            + p2 * 2^e
+    //
+    // Now generate the digits of the fractional part p2 * 2^e.
+    //
+    // Note:
+    // No decimal point is generated: the exponent is adjusted instead.
+    //
+    // p2 actually represents the fraction
+    //
+    //      p2 * 2^e
+    //          = p2 / 2^-e
+    //          = d[-1] / 10^1 + d[-2] / 10^2 + ...
+    //
+    // Now generate the digits d[-m] of p1 from left to right (m = 1,2,...)
+    //
+    //      p2 * 2^e = d[-1]d[-2]...d[-m] * 10^-m
+    //                      + 10^-m * (d[-m-1] / 10^1 + d[-m-2] / 10^2 + ...)
+    //
+    // using
+    //
+    //      10^m * p2 = ((10^m * p2) div 2^-e) * 2^-e + ((10^m * p2) mod 2^-e)
+    //                = (                   d) * 2^-e + (                   r)
+    //
+    // or
+    //      10^m * p2 * 2^e = d + r * 2^e
+    //
+    // i.e.
+    //
+    //      M+ = buffer + p2 * 2^e
+    //         = buffer + 10^-m * (d + r * 2^e)
+    //         = (buffer * 10^m + d) * 10^-m + 10^-m * r * 2^e
+    //
+    // and stop as soon as 10^-m * r * 2^e <= delta * 2^e
+
+    JSON_ASSERT(p2 > delta);
+
+    int m = 0;
+    for (;;)
+    {
+        // Invariant:
+        //      M+ = buffer * 10^-m + 10^-m * (d[-m-1] / 10 + d[-m-2] / 10^2 + ...) * 2^e
+        //         = buffer * 10^-m + 10^-m * (p2                                 ) * 2^e
+        //         = buffer * 10^-m + 10^-m * (1/10 * (10 * p2)                   ) * 2^e
+        //         = buffer * 10^-m + 10^-m * (1/10 * ((10*p2 div 2^-e) * 2^-e + (10*p2 mod 2^-e)) * 2^e
+        //
+        JSON_ASSERT(p2 <= (std::numeric_limits<std::uint64_t>::max)() / 10);
+        p2 *= 10;
+        const std::uint64_t d = p2 >> -one.e;     // d = (10 * p2) div 2^-e
+        const std::uint64_t r = p2 & (one.f - 1); // r = (10 * p2) mod 2^-e
+        //
+        //      M+ = buffer * 10^-m + 10^-m * (1/10 * (d * 2^-e + r) * 2^e
+        //         = buffer * 10^-m + 10^-m * (1/10 * (d + r * 2^e))
+        //         = (buffer * 10 + d) * 10^(-m-1) + 10^(-m-1) * r * 2^e
+        //
+        JSON_ASSERT(d <= 9);
+        buffer[length++] = static_cast<char>('0' + d); // buffer := buffer * 10 + d
+        //
+        //      M+ = buffer * 10^(-m-1) + 10^(-m-1) * r * 2^e
+        //
+        p2 = r;
+        m++;
+        //
+        //      M+ = buffer * 10^-m + 10^-m * p2 * 2^e
+        // Invariant restored.
+
+        // Check if enough digits have been generated.
+        //
+        //      10^-m * p2 * 2^e <= delta * 2^e
+        //              p2 * 2^e <= 10^m * delta * 2^e
+        //                    p2 <= 10^m * delta
+        delta *= 10;
+        dist  *= 10;
+        if (p2 <= delta)
+        {
+            break;
+        }
+    }
+
+    // V = buffer * 10^-m, with M- <= V <= M+.
+
+    decimal_exponent -= m;
+
+    // 1 ulp in the decimal representation is now 10^-m.
+    // Since delta and dist are now scaled by 10^m, we need to do the
+    // same with ulp in order to keep the units in sync.
+    //
+    //      10^m * 10^-m = 1 = 2^-e * 2^e = ten_m * 2^e
+    //
+    const std::uint64_t ten_m = one.f;
+    grisu2_round(buffer, length, dist, delta, p2, ten_m);
+
+    // By construction this algorithm generates the shortest possible decimal
+    // number (Loitsch, Theorem 6.2) which rounds back to w.
+    // For an input number of precision p, at least
+    //
+    //      N = 1 + ceil(p * log_10(2))
+    //
+    // decimal digits are sufficient to identify all binary floating-point
+    // numbers (Matula, "In-and-Out conversions").
+    // This implies that the algorithm does not produce more than N decimal
+    // digits.
+    //
+    //      N = 17 for p = 53 (IEEE double precision)
+    //      N = 9  for p = 24 (IEEE single precision)
+}
+
+/*!
+v = buf * 10^decimal_exponent
+len is the length of the buffer (number of decimal digits)
+The buffer must be large enough, i.e. >= max_digits10.
+*/
+JSON_HEDLEY_NON_NULL(1)
+inline void grisu2(char* buf, int& len, int& decimal_exponent,
+                   diyfp m_minus, diyfp v, diyfp m_plus)
+{
+    JSON_ASSERT(m_plus.e == m_minus.e);
+    JSON_ASSERT(m_plus.e == v.e);
+
+    //  --------(-----------------------+-----------------------)--------    (A)
+    //          m-                      v                       m+
+    //
+    //  --------------------(-----------+-----------------------)--------    (B)
+    //                      m-          v                       m+
+    //
+    // First scale v (and m- and m+) such that the exponent is in the range
+    // [alpha, gamma].
+
+    const cached_power cached = get_cached_power_for_binary_exponent(m_plus.e);
+
+    const diyfp c_minus_k(cached.f, cached.e); // = c ~= 10^-k
+
+    // The exponent of the products is = v.e + c_minus_k.e + q and is in the range [alpha,gamma]
+    const diyfp w       = diyfp::mul(v,       c_minus_k);
+    const diyfp w_minus = diyfp::mul(m_minus, c_minus_k);
+    const diyfp w_plus  = diyfp::mul(m_plus,  c_minus_k);
+
+    //  ----(---+---)---------------(---+---)---------------(---+---)----
+    //          w-                      w                       w+
+    //          = c*m-                  = c*v                   = c*m+
+    //
+    // diyfp::mul rounds its result and c_minus_k is approximated too. w, w- and
+    // w+ are now off by a small amount.
+    // In fact:
+    //
+    //      w - v * 10^k < 1 ulp
+    //
+    // To account for this inaccuracy, add resp. subtract 1 ulp.
+    //
+    //  --------+---[---------------(---+---)---------------]---+--------
+    //          w-  M-                  w                   M+  w+
+    //
+    // Now any number in [M-, M+] (bounds included) will round to w when input,
+    // regardless of how the input rounding algorithm breaks ties.
+    //
+    // And digit_gen generates the shortest possible such number in [M-, M+].
+    // Note that this does not mean that Grisu2 always generates the shortest
+    // possible number in the interval (m-, m+).
+    const diyfp M_minus(w_minus.f + 1, w_minus.e);
+    const diyfp M_plus (w_plus.f  - 1, w_plus.e );
+
+    decimal_exponent = -cached.k; // = -(-k) = k
+
+    grisu2_digit_gen(buf, len, decimal_exponent, M_minus, w, M_plus);
+}
+
+/*!
+v = buf * 10^decimal_exponent
+len is the length of the buffer (number of decimal digits)
+The buffer must be large enough, i.e. >= max_digits10.
+*/
+template<typename FloatType>
+JSON_HEDLEY_NON_NULL(1)
+void grisu2(char* buf, int& len, int& decimal_exponent, FloatType value)
+{
+    static_assert(diyfp::kPrecision >= std::numeric_limits<FloatType>::digits + 3,
+                  "internal error: not enough precision");
+
+    JSON_ASSERT(std::isfinite(value));
+    JSON_ASSERT(value > 0);
+
+    // If the neighbors (and boundaries) of 'value' are always computed for double-precision
+    // numbers, all float's can be recovered using strtod (and strtof). However, the resulting
+    // decimal representations are not exactly "short".
+    //
+    // The documentation for 'std::to_chars' (https://en.cppreference.com/w/cpp/utility/to_chars)
+    // says "value is converted to a string as if by std::sprintf in the default ("C") locale"
+    // and since sprintf promotes float's to double's, I think this is exactly what 'std::to_chars'
+    // does.
+    // On the other hand, the documentation for 'std::to_chars' requires that "parsing the
+    // representation using the corresponding std::from_chars function recovers value exactly". That
+    // indicates that single precision floating-point numbers should be recovered using
+    // 'std::strtof'.
+    //
+    // NB: If the neighbors are computed for single-precision numbers, there is a single float
+    //     (7.0385307e-26f) which can't be recovered using strtod. The resulting double precision
+    //     value is off by 1 ulp.
+#if 0
+    const boundaries w = compute_boundaries(static_cast<double>(value));
+#else
+    const boundaries w = compute_boundaries(value);
+#endif
+
+    grisu2(buf, len, decimal_exponent, w.minus, w.w, w.plus);
+}
+
+/*!
+@brief appends a decimal representation of e to buf
+@return a pointer to the element following the exponent.
+@pre -1000 < e < 1000
+*/
+JSON_HEDLEY_NON_NULL(1)
+JSON_HEDLEY_RETURNS_NON_NULL
+inline char* append_exponent(char* buf, int e)
+{
+    JSON_ASSERT(e > -1000);
+    JSON_ASSERT(e <  1000);
+
+    if (e < 0)
+    {
+        e = -e;
+        *buf++ = '-';
+    }
+    else
+    {
+        *buf++ = '+';
+    }
+
+    auto k = static_cast<std::uint32_t>(e);
+    if (k < 10)
+    {
+        // Always print at least two digits in the exponent.
+        // This is for compatibility with printf("%g").
+        *buf++ = '0';
+        *buf++ = static_cast<char>('0' + k);
+    }
+    else if (k < 100)
+    {
+        *buf++ = static_cast<char>('0' + k / 10);
+        k %= 10;
+        *buf++ = static_cast<char>('0' + k);
+    }
+    else
+    {
+        *buf++ = static_cast<char>('0' + k / 100);
+        k %= 100;
+        *buf++ = static_cast<char>('0' + k / 10);
+        k %= 10;
+        *buf++ = static_cast<char>('0' + k);
+    }
+
+    return buf;
+}
+
+/*!
+@brief prettify v = buf * 10^decimal_exponent
+
+If v is in the range [10^min_exp, 10^max_exp) it will be printed in fixed-point
+notation. Otherwise it will be printed in exponential notation.
+
+@pre min_exp < 0
+@pre max_exp > 0
+*/
+JSON_HEDLEY_NON_NULL(1)
+JSON_HEDLEY_RETURNS_NON_NULL
+inline char* format_buffer(char* buf, int len, int decimal_exponent,
+                           int min_exp, int max_exp)
+{
+    JSON_ASSERT(min_exp < 0);
+    JSON_ASSERT(max_exp > 0);
+
+    const int k = len;
+    const int n = len + decimal_exponent;
+
+    // v = buf * 10^(n-k)
+    // k is the length of the buffer (number of decimal digits)
+    // n is the position of the decimal point relative to the start of the buffer.
+
+    if (k <= n && n <= max_exp)
+    {
+        // digits[000]
+        // len <= max_exp + 2
+
+        std::memset(buf + k, '0', static_cast<size_t>(n) - static_cast<size_t>(k));
+        // Make it look like a floating-point number (#362, #378)
+        buf[n + 0] = '.';
+        buf[n + 1] = '0';
+        return buf + (static_cast<size_t>(n) + 2);
+    }
+
+    if (0 < n && n <= max_exp)
+    {
+        // dig.its
+        // len <= max_digits10 + 1
+
+        JSON_ASSERT(k > n);
+
+        std::memmove(buf + (static_cast<size_t>(n) + 1), buf + n, static_cast<size_t>(k) - static_cast<size_t>(n));
+        buf[n] = '.';
+        return buf + (static_cast<size_t>(k) + 1U);
+    }
+
+    if (min_exp < n && n <= 0)
+    {
+        // 0.[000]digits
+        // len <= 2 + (-min_exp - 1) + max_digits10
+
+        std::memmove(buf + (2 + static_cast<size_t>(-n)), buf, static_cast<size_t>(k));
+        buf[0] = '0';
+        buf[1] = '.';
+        std::memset(buf + 2, '0', static_cast<size_t>(-n));
+        return buf + (2U + static_cast<size_t>(-n) + static_cast<size_t>(k));
+    }
+
+    if (k == 1)
+    {
+        // dE+123
+        // len <= 1 + 5
+
+        buf += 1;
+    }
+    else
+    {
+        // d.igitsE+123
+        // len <= max_digits10 + 1 + 5
+
+        std::memmove(buf + 2, buf + 1, static_cast<size_t>(k) - 1);
+        buf[1] = '.';
+        buf += 1 + static_cast<size_t>(k);
+    }
+
+    *buf++ = 'e';
+    return append_exponent(buf, n - 1);
+}
+
+} // namespace dtoa_impl
+
+/*!
+@brief generates a decimal representation of the floating-point number value in [first, last).
+
+The format of the resulting decimal representation is similar to printf's %g
+format. Returns an iterator pointing past-the-end of the decimal representation.
+
+@note The input number must be finite, i.e. NaN's and Inf's are not supported.
+@note The buffer must be large enough.
+@note The result is NOT null-terminated.
+*/
+template<typename FloatType>
+JSON_HEDLEY_NON_NULL(1, 2)
+JSON_HEDLEY_RETURNS_NON_NULL
+char* to_chars(char* first, const char* last, FloatType value)
+{
+    static_cast<void>(last); // maybe unused - fix warning
+    JSON_ASSERT(std::isfinite(value));
+
+    // Use signbit(value) instead of (value < 0) since signbit works for -0.
+    if (std::signbit(value))
+    {
+        value = -value;
+        *first++ = '-';
+    }
+
+    if (value == 0) // +-0
+    {
+        *first++ = '0';
+        // Make it look like a floating-point number (#362, #378)
+        *first++ = '.';
+        *first++ = '0';
+        return first;
+    }
+
+    JSON_ASSERT(last - first >= std::numeric_limits<FloatType>::max_digits10);
+
+    // Compute v = buffer * 10^decimal_exponent.
+    // The decimal digits are stored in the buffer, which needs to be interpreted
+    // as an unsigned decimal integer.
+    // len is the length of the buffer, i.e. the number of decimal digits.
+    int len = 0;
+    int decimal_exponent = 0;
+    dtoa_impl::grisu2(first, len, decimal_exponent, value);
+
+    JSON_ASSERT(len <= std::numeric_limits<FloatType>::max_digits10);
+
+    // Format the buffer like printf("%.*g", prec, value)
+    constexpr int kMinExp = -4;
+    // Use digits10 here to increase compatibility with version 2.
+    constexpr int kMaxExp = std::numeric_limits<FloatType>::digits10;
+
+    JSON_ASSERT(last - first >= kMaxExp + 2);
+    JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits<FloatType>::max_digits10);
+    JSON_ASSERT(last - first >= std::numeric_limits<FloatType>::max_digits10 + 6);
+
+    return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp);
+}
+
+} // namespace detail
+} // namespace nlohmann
+
+// #include <nlohmann/detail/exceptions.hpp>
+
+// #include <nlohmann/detail/macro_scope.hpp>
+
+// #include <nlohmann/detail/meta/cpp_future.hpp>
+
+// #include <nlohmann/detail/output/binary_writer.hpp>
+
+// #include <nlohmann/detail/output/output_adapters.hpp>
+
+// #include <nlohmann/detail/value_t.hpp>
+
+
+namespace nlohmann
+{
+namespace detail
+{
+///////////////////
+// serialization //
+///////////////////
+
+/// how to treat decoding errors
+enum class error_handler_t
+{
+    strict,  ///< throw a type_error exception in case of invalid UTF-8
+    replace, ///< replace invalid UTF-8 sequences with U+FFFD
+    ignore   ///< ignore invalid UTF-8 sequences
+};
+
+template<typename BasicJsonType>
+class serializer
+{
+    using string_t = typename BasicJsonType::string_t;
+    using number_float_t = typename BasicJsonType::number_float_t;
+    using number_integer_t = typename BasicJsonType::number_integer_t;
+    using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+    using binary_char_t = typename BasicJsonType::binary_t::value_type;
+    static constexpr std::uint8_t UTF8_ACCEPT = 0;
+    static constexpr std::uint8_t UTF8_REJECT = 1;
+
+  public:
+    /*!
+    @param[in] s  output stream to serialize to
+    @param[in] ichar  indentation character to use
+    @param[in] error_handler_  how to react on decoding errors
+    */
+    serializer(output_adapter_t<char> s, const char ichar,
+               error_handler_t error_handler_ = error_handler_t::strict)
+        : o(std::move(s))
+        , loc(std::localeconv())
+        , thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->thousands_sep)))
+        , decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->decimal_point)))
+        , indent_char(ichar)
+        , indent_string(512, indent_char)
+        , error_handler(error_handler_)
+    {}
+
+    // delete because of pointer members
+    serializer(const serializer&) = delete;
+    serializer& operator=(const serializer&) = delete;
+    serializer(serializer&&) = delete;
+    serializer& operator=(serializer&&) = delete;
+    ~serializer() = default;
+
+    /*!
+    @brief internal implementation of the serialization function
+
+    This function is called by the public member function dump and organizes
+    the serialization internally. The indentation level is propagated as
+    additional parameter. In case of arrays and objects, the function is
+    called recursively.
+
+    - strings and object keys are escaped using `escape_string()`
+    - integer numbers are converted implicitly via `operator<<`
+    - floating-point numbers are converted to a string using `"%g"` format
+    - binary values are serialized as objects containing the subtype and the
+      byte array
+
+    @param[in] val               value to serialize
+    @param[in] pretty_print      whether the output shall be pretty-printed
+    @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters
+    in the output are escaped with `\uXXXX` sequences, and the result consists
+    of ASCII characters only.
+    @param[in] indent_step       the indent level
+    @param[in] current_indent    the current indent level (only used internally)
+    */
+    void dump(const BasicJsonType& val,
+              const bool pretty_print,
+              const bool ensure_ascii,
+              const unsigned int indent_step,
+              const unsigned int current_indent = 0)
+    {
+        switch (val.m_type)
+        {
+            case value_t::object:
+            {
+                if (val.m_value.object->empty())
+                {
+                    o->write_characters("{}", 2);
+                    return;
+                }
+
+                if (pretty_print)
+                {
+                    o->write_characters("{\n", 2);
+
+                    // variable to hold indentation for recursive calls
+                    const auto new_indent = current_indent + indent_step;
+                    if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
+                    {
+                        indent_string.resize(indent_string.size() * 2, ' ');
+                    }
+
+                    // first n-1 elements
+                    auto i = val.m_value.object->cbegin();
+                    for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i)
+                    {
+                        o->write_characters(indent_string.c_str(), new_indent);
+                        o->write_character('\"');
+                        dump_escaped(i->first, ensure_ascii);
+                        o->write_characters("\": ", 3);
+                        dump(i->second, true, ensure_ascii, indent_step, new_indent);
+                        o->write_characters(",\n", 2);
+                    }
+
+                    // last element
+                    JSON_ASSERT(i != val.m_value.object->cend());
+                    JSON_ASSERT(std::next(i) == val.m_value.object->cend());
+                    o->write_characters(indent_string.c_str(), new_indent);
+                    o->write_character('\"');
+                    dump_escaped(i->first, ensure_ascii);
+                    o->write_characters("\": ", 3);
+                    dump(i->second, true, ensure_ascii, indent_step, new_indent);
+
+                    o->write_character('\n');
+                    o->write_characters(indent_string.c_str(), current_indent);
+                    o->write_character('}');
+                }
+                else
+                {
+                    o->write_character('{');
+
+                    // first n-1 elements
+                    auto i = val.m_value.object->cbegin();
+                    for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i)
+                    {
+                        o->write_character('\"');
+                        dump_escaped(i->first, ensure_ascii);
+                        o->write_characters("\":", 2);
+                        dump(i->second, false, ensure_ascii, indent_step, current_indent);
+                        o->write_character(',');
+                    }
+
+                    // last element
+                    JSON_ASSERT(i != val.m_value.object->cend());
+                    JSON_ASSERT(std::next(i) == val.m_value.object->cend());
+                    o->write_character('\"');
+                    dump_escaped(i->first, ensure_ascii);
+                    o->write_characters("\":", 2);
+                    dump(i->second, false, ensure_ascii, indent_step, current_indent);
+
+                    o->write_character('}');
+                }
+
+                return;
+            }
+
+            case value_t::array:
+            {
+                if (val.m_value.array->empty())
+                {
+                    o->write_characters("[]", 2);
+                    return;
+                }
+
+                if (pretty_print)
+                {
+                    o->write_characters("[\n", 2);
+
+                    // variable to hold indentation for recursive calls
+                    const auto new_indent = current_indent + indent_step;
+                    if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
+                    {
+                        indent_string.resize(indent_string.size() * 2, ' ');
+                    }
+
+                    // first n-1 elements
+                    for (auto i = val.m_value.array->cbegin();
+                            i != val.m_value.array->cend() - 1; ++i)
+                    {
+                        o->write_characters(indent_string.c_str(), new_indent);
+                        dump(*i, true, ensure_ascii, indent_step, new_indent);
+                        o->write_characters(",\n", 2);
+                    }
+
+                    // last element
+                    JSON_ASSERT(!val.m_value.array->empty());
+                    o->write_characters(indent_string.c_str(), new_indent);
+                    dump(val.m_value.array->back(), true, ensure_ascii, indent_step, new_indent);
+
+                    o->write_character('\n');
+                    o->write_characters(indent_string.c_str(), current_indent);
+                    o->write_character(']');
+                }
+                else
+                {
+                    o->write_character('[');
+
+                    // first n-1 elements
+                    for (auto i = val.m_value.array->cbegin();
+                            i != val.m_value.array->cend() - 1; ++i)
+                    {
+                        dump(*i, false, ensure_ascii, indent_step, current_indent);
+                        o->write_character(',');
+                    }
+
+                    // last element
+                    JSON_ASSERT(!val.m_value.array->empty());
+                    dump(val.m_value.array->back(), false, ensure_ascii, indent_step, current_indent);
+
+                    o->write_character(']');
+                }
+
+                return;
+            }
+
+            case value_t::string:
+            {
+                o->write_character('\"');
+                dump_escaped(*val.m_value.string, ensure_ascii);
+                o->write_character('\"');
+                return;
+            }
+
+            case value_t::binary:
+            {
+                if (pretty_print)
+                {
+                    o->write_characters("{\n", 2);
+
+                    // variable to hold indentation for recursive calls
+                    const auto new_indent = current_indent + indent_step;
+                    if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
+                    {
+                        indent_string.resize(indent_string.size() * 2, ' ');
+                    }
+
+                    o->write_characters(indent_string.c_str(), new_indent);
+
+                    o->write_characters("\"bytes\": [", 10);
+
+                    if (!val.m_value.binary->empty())
+                    {
+                        for (auto i = val.m_value.binary->cbegin();
+                                i != val.m_value.binary->cend() - 1; ++i)
+                        {
+                            dump_integer(*i);
+                            o->write_characters(", ", 2);
+                        }
+                        dump_integer(val.m_value.binary->back());
+                    }
+
+                    o->write_characters("],\n", 3);
+                    o->write_characters(indent_string.c_str(), new_indent);
+
+                    o->write_characters("\"subtype\": ", 11);
+                    if (val.m_value.binary->has_subtype())
+                    {
+                        dump_integer(val.m_value.binary->subtype());
+                    }
+                    else
+                    {
+                        o->write_characters("null", 4);
+                    }
+                    o->write_character('\n');
+                    o->write_characters(indent_string.c_str(), current_indent);
+                    o->write_character('}');
+                }
+                else
+                {
+                    o->write_characters("{\"bytes\":[", 10);
+
+                    if (!val.m_value.binary->empty())
+                    {
+                        for (auto i = val.m_value.binary->cbegin();
+                                i != val.m_value.binary->cend() - 1; ++i)
+                        {
+                            dump_integer(*i);
+                            o->write_character(',');
+                        }
+                        dump_integer(val.m_value.binary->back());
+                    }
+
+                    o->write_characters("],\"subtype\":", 12);
+                    if (val.m_value.binary->has_subtype())
+                    {
+                        dump_integer(val.m_value.binary->subtype());
+                        o->write_character('}');
+                    }
+                    else
+                    {
+                        o->write_characters("null}", 5);
+                    }
+                }
+                return;
+            }
+
+            case value_t::boolean:
+            {
+                if (val.m_value.boolean)
+                {
+                    o->write_characters("true", 4);
+                }
+                else
+                {
+                    o->write_characters("false", 5);
+                }
+                return;
+            }
+
+            case value_t::number_integer:
+            {
+                dump_integer(val.m_value.number_integer);
+                return;
+            }
+
+            case value_t::number_unsigned:
+            {
+                dump_integer(val.m_value.number_unsigned);
+                return;
+            }
+
+            case value_t::number_float:
+            {
+                dump_float(val.m_value.number_float);
+                return;
+            }
+
+            case value_t::discarded:
+            {
+                o->write_characters("<discarded>", 11);
+                return;
+            }
+
+            case value_t::null:
+            {
+                o->write_characters("null", 4);
+                return;
+            }
+
+            default:            // LCOV_EXCL_LINE
+                JSON_ASSERT(false);  // LCOV_EXCL_LINE
+        }
+    }
+
+  private:
+    /*!
+    @brief dump escaped string
+
+    Escape a string by replacing certain special characters by a sequence of an
+    escape character (backslash) and another character and other control
+    characters by a sequence of "\u" followed by a four-digit hex
+    representation. The escaped string is written to output stream @a o.
+
+    @param[in] s  the string to escape
+    @param[in] ensure_ascii  whether to escape non-ASCII characters with
+                             \uXXXX sequences
+
+    @complexity Linear in the length of string @a s.
+    */
+    void dump_escaped(const string_t& s, const bool ensure_ascii)
+    {
+        std::uint32_t codepoint;
+        std::uint8_t state = UTF8_ACCEPT;
+        std::size_t bytes = 0;  // number of bytes written to string_buffer
+
+        // number of bytes written at the point of the last valid byte
+        std::size_t bytes_after_last_accept = 0;
+        std::size_t undumped_chars = 0;
+
+        for (std::size_t i = 0; i < s.size(); ++i)
+        {
+            const auto byte = static_cast<uint8_t>(s[i]);
+
+            switch (decode(state, codepoint, byte))
+            {
+                case UTF8_ACCEPT:  // decode found a new code point
+                {
+                    switch (codepoint)
+                    {
+                        case 0x08: // backspace
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = 'b';
+                            break;
+                        }
+
+                        case 0x09: // horizontal tab
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = 't';
+                            break;
+                        }
+
+                        case 0x0A: // newline
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = 'n';
+                            break;
+                        }
+
+                        case 0x0C: // formfeed
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = 'f';
+                            break;
+                        }
+
+                        case 0x0D: // carriage return
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = 'r';
+                            break;
+                        }
+
+                        case 0x22: // quotation mark
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = '\"';
+                            break;
+                        }
+
+                        case 0x5C: // reverse solidus
+                        {
+                            string_buffer[bytes++] = '\\';
+                            string_buffer[bytes++] = '\\';
+                            break;
+                        }
+
+                        default:
+                        {
+                            // escape control characters (0x00..0x1F) or, if
+                            // ensure_ascii parameter is used, non-ASCII characters
+                            if ((codepoint <= 0x1F) || (ensure_ascii && (codepoint >= 0x7F)))
+                            {
+                                if (codepoint <= 0xFFFF)
+                                {
+                                    (std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x",
+                                                    static_cast<std::uint16_t>(codepoint));
+                                    bytes += 6;
+                                }
+                                else
+                                {
+                                    (std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x",
+                                                    static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)),
+                                                    static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
+                                    bytes += 12;
+                                }
+                            }
+                            else
+                            {
+                                // copy byte to buffer (all previous bytes
+                                // been copied have in default case above)
+                                string_buffer[bytes++] = s[i];
+                            }
+                            break;
+                        }
+                    }
+
+                    // write buffer and reset index; there must be 13 bytes
+                    // left, as this is the maximal number of bytes to be
+                    // written ("\uxxxx\uxxxx\0") for one code point
+                    if (string_buffer.size() - bytes < 13)
+                    {
+                        o->write_characters(string_buffer.data(), bytes);
+                        bytes = 0;
+                    }
+
+                    // remember the byte position of this accept
+                    bytes_after_last_accept = bytes;
+                    undumped_chars = 0;
+                    break;
+                }
+
+                case UTF8_REJECT:  // decode found invalid UTF-8 byte
+                {
+                    switch (error_handler)
+                    {
+                        case error_handler_t::strict:
+                        {
+                            std::string sn(3, '\0');
+                            (std::snprintf)(&sn[0], sn.size(), "%.2X", byte);
+                            JSON_THROW(type_error::create(316, "invalid UTF-8 byte at index " + std::to_string(i) + ": 0x" + sn));
+                        }
+
+                        case error_handler_t::ignore:
+                        case error_handler_t::replace:
+                        {
+                            // in case we saw this character the first time, we
+                            // would like to read it again, because the byte
+                            // may be OK for itself, but just not OK for the
+                            // previous sequence
+                            if (undumped_chars > 0)
+                            {
+                                --i;
+                            }
+
+                            // reset length buffer to the last accepted index;
+                            // thus removing/ignoring the invalid characters
+                            bytes = bytes_after_last_accept;
+
+                            if (error_handler == error_handler_t::replace)
+                            {
+                                // add a replacement character
+                                if (ensure_ascii)
+                                {
+                                    string_buffer[bytes++] = '\\';
+                                    string_buffer[bytes++] = 'u';
+                                    string_buffer[bytes++] = 'f';
+                                    string_buffer[bytes++] = 'f';
+                                    string_buffer[bytes++] = 'f';
+                                    string_buffer[bytes++] = 'd';
+                                }
+                                else
+                                {
+                                    string_buffer[bytes++] = detail::binary_writer<BasicJsonType, char>::to_char_type('\xEF');
+                                    string_buffer[bytes++] = detail::binary_writer<BasicJsonType, char>::to_char_type('\xBF');
+                                    string_buffer[bytes++] = detail::binary_writer<BasicJsonType, char>::to_char_type('\xBD');
+                                }
+
+                                // write buffer and reset index; there must be 13 bytes
+                                // left, as this is the maximal number of bytes to be
+                                // written ("\uxxxx\uxxxx\0") for one code point
+                                if (string_buffer.size() - bytes < 13)
+                                {
+                                    o->write_characters(string_buffer.data(), bytes);
+                                    bytes = 0;
+                                }
+
+                                bytes_after_last_accept = bytes;
+                            }
+
+                            undumped_chars = 0;
+
+                            // continue processing the string
+                            state = UTF8_ACCEPT;
+                            break;
+                        }
+
+                        default:            // LCOV_EXCL_LINE
+                            JSON_ASSERT(false);  // LCOV_EXCL_LINE
+                    }
+                    break;
+                }
+
+                default:  // decode found yet incomplete multi-byte code point
+                {
+                    if (!ensure_ascii)
+                    {
+                        // code point will not be escaped - copy byte to buffer
+                        string_buffer[bytes++] = s[i];
+                    }
+                    ++undumped_chars;
+                    break;
+                }
+            }
+        }
+
+        // we finished processing the string
+        if (JSON_HEDLEY_LIKELY(state == UTF8_ACCEPT))
+        {
+            // write buffer
+            if (bytes > 0)
+            {
+                o->write_characters(string_buffer.data(), bytes);
+            }
+        }
+        else
+        {
+            // we finish reading, but do not accept: string was incomplete
+            switch (error_handler)
+            {
+                case error_handler_t::strict:
+                {
+                    std::string sn(3, '\0');
+                    (std::snprintf)(&sn[0], sn.size(), "%.2X", static_cast<std::uint8_t>(s.back()));
+                    JSON_THROW(type_error::create(316, "incomplete UTF-8 string; last byte: 0x" + sn));
+                }
+
+                case error_handler_t::ignore:
+                {
+                    // write all accepted bytes
+                    o->write_characters(string_buffer.data(), bytes_after_last_accept);
+                    break;
+                }
+
+                case error_handler_t::replace:
+                {
+                    // write all accepted bytes
+                    o->write_characters(string_buffer.data(), bytes_after_last_accept);
+                    // add a replacement character
+                    if (ensure_ascii)
+                    {
+                        o->write_characters("\\ufffd", 6);
+                    }
+                    else
+                    {
+                        o->write_characters("\xEF\xBF\xBD", 3);
+                    }
+                    break;
+                }
+
+                default:            // LCOV_EXCL_LINE
+                    JSON_ASSERT(false);  // LCOV_EXCL_LINE
+            }
+        }
+    }
+
+    /*!
+    @brief count digits
+
+    Count the number of decimal (base 10) digits for an input unsigned integer.
+
+    @param[in] x  unsigned integer number to count its digits
+    @return    number of decimal digits
+    */
+    inline unsigned int count_digits(number_unsigned_t x) noexcept
+    {
+        unsigned int n_digits = 1;
+        for (;;)
+        {
+            if (x < 10)
+            {
+                return n_digits;
+            }
+            if (x < 100)
+            {
+                return n_digits + 1;
+            }
+            if (x < 1000)
+            {
+                return n_digits + 2;
+            }
+            if (x < 10000)
+            {
+                return n_digits + 3;
+            }
+            x = x / 10000u;
+            n_digits += 4;
+        }
+    }
+
+    /*!
+    @brief dump an integer
+
+    Dump a given integer to output stream @a o. Works internally with
+    @a number_buffer.
+
+    @param[in] x  integer number (signed or unsigned) to dump
+    @tparam NumberType either @a number_integer_t or @a number_unsigned_t
+    */
+    template < typename NumberType, detail::enable_if_t <
+                   std::is_same<NumberType, number_unsigned_t>::value ||
+                   std::is_same<NumberType, number_integer_t>::value ||
+                   std::is_same<NumberType, binary_char_t>::value,
+                   int > = 0 >
+    void dump_integer(NumberType x)
+    {
+        static constexpr std::array<std::array<char, 2>, 100> digits_to_99
+        {
+            {
+                {{'0', '0'}}, {{'0', '1'}}, {{'0', '2'}}, {{'0', '3'}}, {{'0', '4'}}, {{'0', '5'}}, {{'0', '6'}}, {{'0', '7'}}, {{'0', '8'}}, {{'0', '9'}},
+                {{'1', '0'}}, {{'1', '1'}}, {{'1', '2'}}, {{'1', '3'}}, {{'1', '4'}}, {{'1', '5'}}, {{'1', '6'}}, {{'1', '7'}}, {{'1', '8'}}, {{'1', '9'}},
+                {{'2', '0'}}, {{'2', '1'}}, {{'2', '2'}}, {{'2', '3'}}, {{'2', '4'}}, {{'2', '5'}}, {{'2', '6'}}, {{'2', '7'}}, {{'2', '8'}}, {{'2', '9'}},
+                {{'3', '0'}}, {{'3', '1'}}, {{'3', '2'}}, {{'3', '3'}}, {{'3', '4'}}, {{'3', '5'}}, {{'3', '6'}}, {{'3', '7'}}, {{'3', '8'}}, {{'3', '9'}},
+                {{'4', '0'}}, {{'4', '1'}}, {{'4', '2'}}, {{'4', '3'}}, {{'4', '4'}}, {{'4', '5'}}, {{'4', '6'}}, {{'4', '7'}}, {{'4', '8'}}, {{'4', '9'}},
+                {{'5', '0'}}, {{'5', '1'}}, {{'5', '2'}}, {{'5', '3'}}, {{'5', '4'}}, {{'5', '5'}}, {{'5', '6'}}, {{'5', '7'}}, {{'5', '8'}}, {{'5', '9'}},
+                {{'6', '0'}}, {{'6', '1'}}, {{'6', '2'}}, {{'6', '3'}}, {{'6', '4'}}, {{'6', '5'}}, {{'6', '6'}}, {{'6', '7'}}, {{'6', '8'}}, {{'6', '9'}},
+                {{'7', '0'}}, {{'7', '1'}}, {{'7', '2'}}, {{'7', '3'}}, {{'7', '4'}}, {{'7', '5'}}, {{'7', '6'}}, {{'7', '7'}}, {{'7', '8'}}, {{'7', '9'}},
+                {{'8', '0'}}, {{'8', '1'}}, {{'8', '2'}}, {{'8', '3'}}, {{'8', '4'}}, {{'8', '5'}}, {{'8', '6'}}, {{'8', '7'}}, {{'8', '8'}}, {{'8', '9'}},
+                {{'9', '0'}}, {{'9', '1'}}, {{'9', '2'}}, {{'9', '3'}}, {{'9', '4'}}, {{'9', '5'}}, {{'9', '6'}}, {{'9', '7'}}, {{'9', '8'}}, {{'9', '9'}},
+            }
+        };
+
+        // special case for "0"
+        if (x == 0)
+        {
+            o->write_character('0');
+            return;
+        }
+
+        // use a pointer to fill the buffer
+        auto buffer_ptr = number_buffer.begin();
+
+        const bool is_negative = std::is_same<NumberType, number_integer_t>::value && !(x >= 0); // see issue #755
+        number_unsigned_t abs_value;
+
+        unsigned int n_chars;
+
+        if (is_negative)
+        {
+            *buffer_ptr = '-';
+            abs_value = remove_sign(static_cast<number_integer_t>(x));
+
+            // account one more byte for the minus sign
+            n_chars = 1 + count_digits(abs_value);
+        }
+        else
+        {
+            abs_value = static_cast<number_unsigned_t>(x);
+            n_chars = count_digits(abs_value);
+        }
+
+        // spare 1 byte for '\0'
+        JSON_ASSERT(n_chars < number_buffer.size() - 1);
+
+        // jump to the end to generate the string from backward
+        // so we later avoid reversing the result
+        buffer_ptr += n_chars;
+
+        // Fast int2ascii implementation inspired by "Fastware" talk by Andrei Alexandrescu
+        // See: https://www.youtube.com/watch?v=o4-CwDo2zpg
+        while (abs_value >= 100)
+        {
+            const auto digits_index = static_cast<unsigned>((abs_value % 100));
+            abs_value /= 100;
+            *(--buffer_ptr) = digits_to_99[digits_index][1];
+            *(--buffer_ptr) = digits_to_99[digits_index][0];
+        }
+
+        if (abs_value >= 10)
+        {
+            const auto digits_index = static_cast<unsigned>(abs_value);
+            *(--buffer_ptr) = digits_to_99[digits_index][1];
+            *(--buffer_ptr) = digits_to_99[digits_index][0];
+        }
+        else
+        {
+            *(--buffer_ptr) = static_cast<char>('0' + abs_value);
+        }
+
+        o->write_characters(number_buffer.data(), n_chars);
+    }
+
+    /*!
+    @brief dump a floating-point number
+
+    Dump a given floating-point number to output stream @a o. Works internally
+    with @a number_buffer.
+
+    @param[in] x  floating-point number to dump
+    */
+    void dump_float(number_float_t x)
+    {
+        // NaN / inf
+        if (!std::isfinite(x))
+        {
+            o->write_characters("null", 4);
+            return;
+        }
+
+        // If number_float_t is an IEEE-754 single or double precision number,
+        // use the Grisu2 algorithm to produce short numbers which are
+        // guaranteed to round-trip, using strtof and strtod, resp.
+        //
+        // NB: The test below works if <long double> == <double>.
+        static constexpr bool is_ieee_single_or_double
+            = (std::numeric_limits<number_float_t>::is_iec559 && std::numeric_limits<number_float_t>::digits == 24 && std::numeric_limits<number_float_t>::max_exponent == 128) ||
+              (std::numeric_limits<number_float_t>::is_iec559 && std::numeric_limits<number_float_t>::digits == 53 && std::numeric_limits<number_float_t>::max_exponent == 1024);
+
+        dump_float(x, std::integral_constant<bool, is_ieee_single_or_double>());
+    }
+
+    void dump_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/)
+    {
+        char* begin = number_buffer.data();
+        char* end = ::nlohmann::detail::to_chars(begin, begin + number_buffer.size(), x);
+
+        o->write_characters(begin, static_cast<size_t>(end - begin));
+    }
+
+    void dump_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/)
+    {
+        // get number of digits for a float -> text -> float round-trip
+        static constexpr auto d = std::numeric_limits<number_float_t>::max_digits10;
+
+        // the actual conversion
+        std::ptrdiff_t len = (std::snprintf)(number_buffer.data(), number_buffer.size(), "%.*g", d, x);
+
+        // negative value indicates an error
+        JSON_ASSERT(len > 0);
+        // check if buffer was large enough
+        JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size());
+
+        // erase thousands separator
+        if (thousands_sep != '\0')
+        {
+            const auto end = std::remove(number_buffer.begin(),
+                                         number_buffer.begin() + len, thousands_sep);
+            std::fill(end, number_buffer.end(), '\0');
+            JSON_ASSERT((end - number_buffer.begin()) <= len);
+            len = (end - number_buffer.begin());
+        }
+
+        // convert decimal point to '.'
+        if (decimal_point != '\0' && decimal_point != '.')
+        {
+            const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point);
+            if (dec_pos != number_buffer.end())
+            {
+                *dec_pos = '.';
+            }
+        }
+
+        o->write_characters(number_buffer.data(), static_cast<std::size_t>(len));
+
+        // determine if need to append ".0"
+        const bool value_is_int_like =
+            std::none_of(number_buffer.begin(), number_buffer.begin() + len + 1,
+                         [](char c)
+        {
+            return c == '.' || c == 'e';
+        });
+
+        if (value_is_int_like)
+        {
+            o->write_characters(".0", 2);
+        }
+    }
+
+    /*!
+    @brief check whether a string is UTF-8 encoded
+
+    The function checks each byte of a string whether it is UTF-8 encoded. The
+    result of the check is stored in the @a state parameter. The function must
+    be called initially with state 0 (accept). State 1 means the string must
+    be rejected, because the current byte is not allowed. If the string is
+    completely processed, but the state is non-zero, the string ended
+    prematurely; that is, the last byte indicated more bytes should have
+    followed.
+
+    @param[in,out] state  the state of the decoding
+    @param[in,out] codep  codepoint (valid only if resulting state is UTF8_ACCEPT)
+    @param[in] byte       next byte to decode
+    @return               new state
+
+    @note The function has been edited: a std::array is used.
+
+    @copyright Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+    @sa http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+    */
+    static std::uint8_t decode(std::uint8_t& state, std::uint32_t& codep, const std::uint8_t byte) noexcept
+    {
+        static const std::array<std::uint8_t, 400> utf8d =
+        {
+            {
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F
+                1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F
+                7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF
+                8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF
+                0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF
+                0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF
+                0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
+                1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
+                1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
+                1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
+                1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8
+            }
+        };
+
+        const std::uint8_t type = utf8d[byte];
+
+        codep = (state != UTF8_ACCEPT)
+                ? (byte & 0x3fu) | (codep << 6u)
+                : (0xFFu >> type) & (byte);
+
+        std::size_t index = 256u + static_cast<size_t>(state) * 16u + static_cast<size_t>(type);
+        JSON_ASSERT(index < 400);
+        state = utf8d[index];
+        return state;
+    }
+
+    /*
+     * Overload to make the compiler happy while it is instantiating
+     * dump_integer for number_unsigned_t.
+     * Must never be called.
+     */
+    number_unsigned_t remove_sign(number_unsigned_t x)
+    {
+        JSON_ASSERT(false); // LCOV_EXCL_LINE
+        return x; // LCOV_EXCL_LINE
+    }
+
+    /*
+     * Helper function for dump_integer
+     *
+     * This function takes a negative signed integer and returns its absolute
+     * value as unsigned integer. The plus/minus shuffling is necessary as we can
+     * not directly remove the sign of an arbitrary signed integer as the
+     * absolute values of INT_MIN and INT_MAX are usually not the same. See
+     * #1708 for details.
+     */
+    inline number_unsigned_t remove_sign(number_integer_t x) noexcept
+    {
+        JSON_ASSERT(x < 0 && x < (std::numeric_limits<number_integer_t>::max)());
+        return static_cast<number_unsigned_t>(-(x + 1)) + 1;
+    }
+
+  private:
+    /// the output of the serializer
+    output_adapter_t<char> o = nullptr;
+
+    /// a (hopefully) large enough character buffer
+    std::array<char, 64> number_buffer{{}};
+
+    /// the locale
+    const std::lconv* loc = nullptr;
+    /// the locale's thousand separator character
+    const char thousands_sep = '\0';
+    /// the locale's decimal point character
+    const char decimal_point = '\0';
+
+    /// string buffer
+    std::array<char, 512> string_buffer{{}};
+
+    /// the indentation character
+    const char indent_char;
+    /// the indentation string
+    string_t indent_string;
+
+    /// error_handler how to react on decoding errors
+    const error_handler_t error_handler;
+};
+}  // namespace detail
+}  // namespace nlohmann
+
+// #include <nlohmann/detail/value_t.hpp>
+
+// #include <nlohmann/json_fwd.hpp>
+
+// #include <nlohmann/ordered_map.hpp>
+
+
+#include <functional> // less
+#include <memory> // allocator
+#include <utility> // pair
+#include <vector> // vector
+
+namespace nlohmann
+{
+
+/// ordered_map: a minimal map-like container that preserves insertion order
+/// for use within nlohmann::basic_json<ordered_map>
+template <class Key, class T, class IgnoredLess = std::less<Key>,
+          class Allocator = std::allocator<std::pair<const Key, T>>>
+                  struct ordered_map : std::vector<std::pair<const Key, T>, Allocator>
+{
+    using key_type = Key;
+    using mapped_type = T;
+    using Container = std::vector<std::pair<const Key, T>, Allocator>;
+    using typename Container::iterator;
+    using typename Container::const_iterator;
+    using typename Container::size_type;
+    using typename Container::value_type;
+
+    // Explicit constructors instead of `using Container::Container`
+    // otherwise older compilers choke on it (GCC <= 5.5, xcode <= 9.4)
+    ordered_map(const Allocator& alloc = Allocator()) : Container{alloc} {}
+    template <class It>
+    ordered_map(It first, It last, const Allocator& alloc = Allocator())
+        : Container{first, last, alloc} {}
+    ordered_map(std::initializer_list<T> init, const Allocator& alloc = Allocator() )
+        : Container{init, alloc} {}
+
+    std::pair<iterator, bool> emplace(const key_type& key, T&& t)
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return {it, false};
+            }
+        }
+        Container::emplace_back(key, t);
+        return {--this->end(), true};
+    }
+
+    T& operator[](const Key& key)
+    {
+        return emplace(key, T{}).first->second;
+    }
+
+    const T& operator[](const Key& key) const
+    {
+        return at(key);
+    }
+
+    T& at(const Key& key)
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return it->second;
+            }
+        }
+
+        throw std::out_of_range("key not found");
+    }
+
+    const T& at(const Key& key) const
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return it->second;
+            }
+        }
+
+        throw std::out_of_range("key not found");
+    }
+
+    size_type erase(const Key& key)
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                // Since we cannot move const Keys, re-construct them in place
+                for (auto next = it; ++next != this->end(); ++it)
+                {
+                    it->~value_type(); // Destroy but keep allocation
+                    new (&*it) value_type{std::move(*next)};
+                }
+                Container::pop_back();
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+    iterator erase(iterator pos)
+    {
+        auto it = pos;
+
+        // Since we cannot move const Keys, re-construct them in place
+        for (auto next = it; ++next != this->end(); ++it)
+        {
+            it->~value_type(); // Destroy but keep allocation
+            new (&*it) value_type{std::move(*next)};
+        }
+        Container::pop_back();
+        return pos;
+    }
+
+    size_type count(const Key& key) const
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+    iterator find(const Key& key)
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return it;
+            }
+        }
+        return Container::end();
+    }
+
+    const_iterator find(const Key& key) const
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == key)
+            {
+                return it;
+            }
+        }
+        return Container::end();
+    }
+
+    std::pair<iterator, bool> insert( value_type&& value )
+    {
+        return emplace(value.first, std::move(value.second));
+    }
+
+    std::pair<iterator, bool> insert( const value_type& value )
+    {
+        for (auto it = this->begin(); it != this->end(); ++it)
+        {
+            if (it->first == value.first)
+            {
+                return {it, false};
+            }
+        }
+        Container::push_back(value);
+        return {--this->end(), true};
+    }
+};
+
+}  // namespace nlohmann
+
+
+/*!
+@brief namespace for Niels Lohmann
+@see https://github.com/nlohmann
+@since version 1.0.0
+*/
+namespace nlohmann
+{
+
+/*!
+@brief a class to store JSON values
+
+@tparam ObjectType type for JSON objects (`std::map` by default; will be used
+in @ref object_t)
+@tparam ArrayType type for JSON arrays (`std::vector` by default; will be used
+in @ref array_t)
+@tparam StringType type for JSON strings and object keys (`std::string` by
+default; will be used in @ref string_t)
+@tparam BooleanType type for JSON booleans (`bool` by default; will be used
+in @ref boolean_t)
+@tparam NumberIntegerType type for JSON integer numbers (`int64_t` by
+default; will be used in @ref number_integer_t)
+@tparam NumberUnsignedType type for JSON unsigned integer numbers (@c
+`uint64_t` by default; will be used in @ref number_unsigned_t)
+@tparam NumberFloatType type for JSON floating-point numbers (`double` by
+default; will be used in @ref number_float_t)
+@tparam BinaryType type for packed binary data for compatibility with binary
+serialization formats (`std::vector<std::uint8_t>` by default; will be used in
+@ref binary_t)
+@tparam AllocatorType type of the allocator to use (`std::allocator` by
+default)
+@tparam JSONSerializer the serializer to resolve internal calls to `to_json()`
+and `from_json()` (@ref adl_serializer by default)
+
+@requirement The class satisfies the following concept requirements:
+- Basic
+ - [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible):
+   JSON values can be default constructed. The result will be a JSON null
+   value.
+ - [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible):
+   A JSON value can be constructed from an rvalue argument.
+ - [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible):
+   A JSON value can be copy-constructed from an lvalue expression.
+ - [MoveAssignable](https://en.cppreference.com/w/cpp/named_req/MoveAssignable):
+   A JSON value van be assigned from an rvalue argument.
+ - [CopyAssignable](https://en.cppreference.com/w/cpp/named_req/CopyAssignable):
+   A JSON value can be copy-assigned from an lvalue expression.
+ - [Destructible](https://en.cppreference.com/w/cpp/named_req/Destructible):
+   JSON values can be destructed.
+- Layout
+ - [StandardLayoutType](https://en.cppreference.com/w/cpp/named_req/StandardLayoutType):
+   JSON values have
+   [standard layout](https://en.cppreference.com/w/cpp/language/data_members#Standard_layout):
+   All non-static data members are private and standard layout types, the
+   class has no virtual functions or (virtual) base classes.
+- Library-wide
+ - [EqualityComparable](https://en.cppreference.com/w/cpp/named_req/EqualityComparable):
+   JSON values can be compared with `==`, see @ref
+   operator==(const_reference,const_reference).
+ - [LessThanComparable](https://en.cppreference.com/w/cpp/named_req/LessThanComparable):
+   JSON values can be compared with `<`, see @ref
+   operator<(const_reference,const_reference).
+ - [Swappable](https://en.cppreference.com/w/cpp/named_req/Swappable):
+   Any JSON lvalue or rvalue of can be swapped with any lvalue or rvalue of
+   other compatible types, using unqualified function call @ref swap().
+ - [NullablePointer](https://en.cppreference.com/w/cpp/named_req/NullablePointer):
+   JSON values can be compared against `std::nullptr_t` objects which are used
+   to model the `null` value.
+- Container
+ - [Container](https://en.cppreference.com/w/cpp/named_req/Container):
+   JSON values can be used like STL containers and provide iterator access.
+ - [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer);
+   JSON values can be used like STL containers and provide reverse iterator
+   access.
+
+@invariant The member variables @a m_value and @a m_type have the following
+relationship:
+- If `m_type == value_t::object`, then `m_value.object != nullptr`.
+- If `m_type == value_t::array`, then `m_value.array != nullptr`.
+- If `m_type == value_t::string`, then `m_value.string != nullptr`.
+The invariants are checked by member function assert_invariant().
+
+@internal
+@note ObjectType trick from https://stackoverflow.com/a/9860911
+@endinternal
+
+@see [RFC 7159: The JavaScript Object Notation (JSON) Data Interchange
+Format](http://rfc7159.net/rfc7159)
+
+@since version 1.0.0
+
+@nosubgrouping
+*/
+NLOHMANN_BASIC_JSON_TPL_DECLARATION
+class basic_json
+{
+  private:
+    template<detail::value_t> friend struct detail::external_constructor;
+    friend ::nlohmann::json_pointer<basic_json>;
+
+    template<typename BasicJsonType, typename InputType>
+    friend class ::nlohmann::detail::parser;
+    friend ::nlohmann::detail::serializer<basic_json>;
+    template<typename BasicJsonType>
+    friend class ::nlohmann::detail::iter_impl;
+    template<typename BasicJsonType, typename CharType>
+    friend class ::nlohmann::detail::binary_writer;
+    template<typename BasicJsonType, typename InputType, typename SAX>
+    friend class ::nlohmann::detail::binary_reader;
+    template<typename BasicJsonType>
+    friend class ::nlohmann::detail::json_sax_dom_parser;
+    template<typename BasicJsonType>
+    friend class ::nlohmann::detail::json_sax_dom_callback_parser;
+
+    /// workaround type for MSVC
+    using basic_json_t = NLOHMANN_BASIC_JSON_TPL;
+
+    // convenience aliases for types residing in namespace detail;
+    using lexer = ::nlohmann::detail::lexer_base<basic_json>;
+
+    template<typename InputAdapterType>
+    static ::nlohmann::detail::parser<basic_json, InputAdapterType> parser(
+        InputAdapterType adapter,
+        detail::parser_callback_t<basic_json>cb = nullptr,
+        const bool allow_exceptions = true,
+        const bool ignore_comments = false
+                                 )
+    {
+        return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter),
+                std::move(cb), allow_exceptions, ignore_comments);
+    }
+
+    using primitive_iterator_t = ::nlohmann::detail::primitive_iterator_t;
+    template<typename BasicJsonType>
+    using internal_iterator = ::nlohmann::detail::internal_iterator<BasicJsonType>;
+    template<typename BasicJsonType>
+    using iter_impl = ::nlohmann::detail::iter_impl<BasicJsonType>;
+    template<typename Iterator>
+    using iteration_proxy = ::nlohmann::detail::iteration_proxy<Iterator>;
+    template<typename Base> using json_reverse_iterator = ::nlohmann::detail::json_reverse_iterator<Base>;
+
+    template<typename CharType>
+    using output_adapter_t = ::nlohmann::detail::output_adapter_t<CharType>;
+
+    template<typename InputType>
+    using binary_reader = ::nlohmann::detail::binary_reader<basic_json, InputType>;
+    template<typename CharType> using binary_writer = ::nlohmann::detail::binary_writer<basic_json, CharType>;
+
+    using serializer = ::nlohmann::detail::serializer<basic_json>;
+
+  public:
+    using value_t = detail::value_t;
+    /// JSON Pointer, see @ref nlohmann::json_pointer
+    using json_pointer = ::nlohmann::json_pointer<basic_json>;
+    template<typename T, typename SFINAE>
+    using json_serializer = JSONSerializer<T, SFINAE>;
+    /// how to treat decoding errors
+    using error_handler_t = detail::error_handler_t;
+    /// how to treat CBOR tags
+    using cbor_tag_handler_t = detail::cbor_tag_handler_t;
+    /// helper type for initializer lists of basic_json values
+    using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
+
+    using input_format_t = detail::input_format_t;
+    /// SAX interface type, see @ref nlohmann::json_sax
+    using json_sax_t = json_sax<basic_json>;
+
+    ////////////////
+    // exceptions //
+    ////////////////
+
+    /// @name exceptions
+    /// Classes to implement user-defined exceptions.
+    /// @{
+
+    /// @copydoc detail::exception
+    using exception = detail::exception;
+    /// @copydoc detail::parse_error
+    using parse_error = detail::parse_error;
+    /// @copydoc detail::invalid_iterator
+    using invalid_iterator = detail::invalid_iterator;
+    /// @copydoc detail::type_error
+    using type_error = detail::type_error;
+    /// @copydoc detail::out_of_range
+    using out_of_range = detail::out_of_range;
+    /// @copydoc detail::other_error
+    using other_error = detail::other_error;
+
+    /// @}
+
+
+    /////////////////////
+    // container types //
+    /////////////////////
+
+    /// @name container types
+    /// The canonic container types to use @ref basic_json like any other STL
+    /// container.
+    /// @{
+
+    /// the type of elements in a basic_json container
+    using value_type = basic_json;
+
+    /// the type of an element reference
+    using reference = value_type&;
+    /// the type of an element const reference
+    using const_reference = const value_type&;
+
+    /// a type to represent differences between iterators
+    using difference_type = std::ptrdiff_t;
+    /// a type to represent container sizes
+    using size_type = std::size_t;
+
+    /// the allocator type
+    using allocator_type = AllocatorType<basic_json>;
+
+    /// the type of an element pointer
+    using pointer = typename std::allocator_traits<allocator_type>::pointer;
+    /// the type of an element const pointer
+    using const_pointer = typename std::allocator_traits<allocator_type>::const_pointer;
+
+    /// an iterator for a basic_json container
+    using iterator = iter_impl<basic_json>;
+    /// a const iterator for a basic_json container
+    using const_iterator = iter_impl<const basic_json>;
+    /// a reverse iterator for a basic_json container
+    using reverse_iterator = json_reverse_iterator<typename basic_json::iterator>;
+    /// a const reverse iterator for a basic_json container
+    using const_reverse_iterator = json_reverse_iterator<typename basic_json::const_iterator>;
+
+    /// @}
+
+
+    /*!
+    @brief returns the allocator associated with the container
+    */
+    static allocator_type get_allocator()
+    {
+        return allocator_type();
+    }
+
+    /*!
+    @brief returns version information on the library
+
+    This function returns a JSON object with information about the library,
+    including the version number and information on the platform and compiler.
+
+    @return JSON object holding version information
+    key         | description
+    ----------- | ---------------
+    `compiler`  | Information on the used compiler. It is an object with the following keys: `c++` (the used C++ standard), `family` (the compiler family; possible values are `clang`, `icc`, `gcc`, `ilecpp`, `msvc`, `pgcpp`, `sunpro`, and `unknown`), and `version` (the compiler version).
+    `copyright` | The copyright line for the library as string.
+    `name`      | The name of the library as string.
+    `platform`  | The used platform as string. Possible values are `win32`, `linux`, `apple`, `unix`, and `unknown`.
+    `url`       | The URL of the project as string.
+    `version`   | The version of the library. It is an object with the following keys: `major`, `minor`, and `patch` as defined by [Semantic Versioning](http://semver.org), and `string` (the version string).
+
+    @liveexample{The following code shows an example output of the `meta()`
+    function.,meta}
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @complexity Constant.
+
+    @since 2.1.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json meta()
+    {
+        basic_json result;
+
+        result["copyright"] = "(C) 2013-2020 Niels Lohmann";
+        result["name"] = "JSON for Modern C++";
+        result["url"] = "https://github.com/nlohmann/json";
+        result["version"]["string"] =
+            std::to_string(NLOHMANN_JSON_VERSION_MAJOR) + "." +
+            std::to_string(NLOHMANN_JSON_VERSION_MINOR) + "." +
+            std::to_string(NLOHMANN_JSON_VERSION_PATCH);
+        result["version"]["major"] = NLOHMANN_JSON_VERSION_MAJOR;
+        result["version"]["minor"] = NLOHMANN_JSON_VERSION_MINOR;
+        result["version"]["patch"] = NLOHMANN_JSON_VERSION_PATCH;
+
+#ifdef _WIN32
+        result["platform"] = "win32";
+#elif defined __linux__
+        result["platform"] = "linux";
+#elif defined __APPLE__
+        result["platform"] = "apple";
+#elif defined __unix__
+        result["platform"] = "unix";
+#else
+        result["platform"] = "unknown";
+#endif
+
+#if defined(__ICC) || defined(__INTEL_COMPILER)
+        result["compiler"] = {{"family", "icc"}, {"version", __INTEL_COMPILER}};
+#elif defined(__clang__)
+        result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}};
+#elif defined(__GNUC__) || defined(__GNUG__)
+        result["compiler"] = {{"family", "gcc"}, {"version", std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__)}};
+#elif defined(__HP_cc) || defined(__HP_aCC)
+        result["compiler"] = "hp"
+#elif defined(__IBMCPP__)
+        result["compiler"] = {{"family", "ilecpp"}, {"version", __IBMCPP__}};
+#elif defined(_MSC_VER)
+        result["compiler"] = {{"family", "msvc"}, {"version", _MSC_VER}};
+#elif defined(__PGI)
+        result["compiler"] = {{"family", "pgcpp"}, {"version", __PGI}};
+#elif defined(__SUNPRO_CC)
+        result["compiler"] = {{"family", "sunpro"}, {"version", __SUNPRO_CC}};
+#else
+        result["compiler"] = {{"family", "unknown"}, {"version", "unknown"}};
+#endif
+
+#ifdef __cplusplus
+        result["compiler"]["c++"] = std::to_string(__cplusplus);
+#else
+        result["compiler"]["c++"] = "unknown";
+#endif
+        return result;
+    }
+
+
+    ///////////////////////////
+    // JSON value data types //
+    ///////////////////////////
+
+    /// @name JSON value data types
+    /// The data types to store a JSON value. These types are derived from
+    /// the template arguments passed to class @ref basic_json.
+    /// @{
+
+#if defined(JSON_HAS_CPP_14)
+    // Use transparent comparator if possible, combined with perfect forwarding
+    // on find() and count() calls prevents unnecessary string construction.
+    using object_comparator_t = std::less<>;
+#else
+    using object_comparator_t = std::less<StringType>;
+#endif
+
+    /*!
+    @brief a type for an object
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes JSON objects as follows:
+    > An object is an unordered collection of zero or more name/value pairs,
+    > where a name is a string and a value is a string, number, boolean, null,
+    > object, or array.
+
+    To store objects in C++, a type is defined by the template parameters
+    described below.
+
+    @tparam ObjectType  the container to store objects (e.g., `std::map` or
+    `std::unordered_map`)
+    @tparam StringType the type of the keys or names (e.g., `std::string`).
+    The comparison function `std::less<StringType>` is used to order elements
+    inside the container.
+    @tparam AllocatorType the allocator to use for objects (e.g.,
+    `std::allocator`)
+
+    #### Default type
+
+    With the default values for @a ObjectType (`std::map`), @a StringType
+    (`std::string`), and @a AllocatorType (`std::allocator`), the default
+    value for @a object_t is:
+
+    @code {.cpp}
+    std::map<
+      std::string, // key_type
+      basic_json, // value_type
+      std::less<std::string>, // key_compare
+      std::allocator<std::pair<const std::string, basic_json>> // allocator_type
+    >
+    @endcode
+
+    #### Behavior
+
+    The choice of @a object_t influences the behavior of the JSON class. With
+    the default type, objects have the following behavior:
+
+    - When all names are unique, objects will be interoperable in the sense
+      that all software implementations receiving that object will agree on
+      the name-value mappings.
+    - When the names within an object are not unique, it is unspecified which
+      one of the values for a given key will be chosen. For instance,
+      `{"key": 2, "key": 1}` could be equal to either `{"key": 1}` or
+      `{"key": 2}`.
+    - Internally, name/value pairs are stored in lexicographical order of the
+      names. Objects will also be serialized (see @ref dump) in this order.
+      For instance, `{"b": 1, "a": 2}` and `{"a": 2, "b": 1}` will be stored
+      and serialized as `{"a": 2, "b": 1}`.
+    - When comparing objects, the order of the name/value pairs is irrelevant.
+      This makes objects interoperable in the sense that they will not be
+      affected by these differences. For instance, `{"b": 1, "a": 2}` and
+      `{"a": 2, "b": 1}` will be treated as equal.
+
+    #### Limits
+
+    [RFC 7159](http://rfc7159.net/rfc7159) specifies:
+    > An implementation may set limits on the maximum depth of nesting.
+
+    In this class, the object's limit of nesting is not explicitly constrained.
+    However, a maximum depth of nesting may be introduced by the compiler or
+    runtime environment. A theoretical limit can be queried by calling the
+    @ref max_size function of a JSON object.
+
+    #### Storage
+
+    Objects are stored as pointers in a @ref basic_json type. That is, for any
+    access to object values, a pointer of type `object_t*` must be
+    dereferenced.
+
+    @sa @ref array_t -- type for an array value
+
+    @since version 1.0.0
+
+    @note The order name/value pairs are added to the object is *not*
+    preserved by the library. Therefore, iterating an object may return
+    name/value pairs in a different order than they were originally stored. In
+    fact, keys will be traversed in alphabetical order as `std::map` with
+    `std::less` is used by default. Please note this behavior conforms to [RFC
+    7159](http://rfc7159.net/rfc7159), because any order implements the
+    specified "unordered" nature of JSON objects.
+    */
+    using object_t = ObjectType<StringType,
+          basic_json,
+          object_comparator_t,
+          AllocatorType<std::pair<const StringType,
+          basic_json>>>;
+
+    /*!
+    @brief a type for an array
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes JSON arrays as follows:
+    > An array is an ordered sequence of zero or more values.
+
+    To store objects in C++, a type is defined by the template parameters
+    explained below.
+
+    @tparam ArrayType  container type to store arrays (e.g., `std::vector` or
+    `std::list`)
+    @tparam AllocatorType allocator to use for arrays (e.g., `std::allocator`)
+
+    #### Default type
+
+    With the default values for @a ArrayType (`std::vector`) and @a
+    AllocatorType (`std::allocator`), the default value for @a array_t is:
+
+    @code {.cpp}
+    std::vector<
+      basic_json, // value_type
+      std::allocator<basic_json> // allocator_type
+    >
+    @endcode
+
+    #### Limits
+
+    [RFC 7159](http://rfc7159.net/rfc7159) specifies:
+    > An implementation may set limits on the maximum depth of nesting.
+
+    In this class, the array's limit of nesting is not explicitly constrained.
+    However, a maximum depth of nesting may be introduced by the compiler or
+    runtime environment. A theoretical limit can be queried by calling the
+    @ref max_size function of a JSON array.
+
+    #### Storage
+
+    Arrays are stored as pointers in a @ref basic_json type. That is, for any
+    access to array values, a pointer of type `array_t*` must be dereferenced.
+
+    @sa @ref object_t -- type for an object value
+
+    @since version 1.0.0
+    */
+    using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
+
+    /*!
+    @brief a type for a string
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes JSON strings as follows:
+    > A string is a sequence of zero or more Unicode characters.
+
+    To store objects in C++, a type is defined by the template parameter
+    described below. Unicode values are split by the JSON class into
+    byte-sized characters during deserialization.
+
+    @tparam StringType  the container to store strings (e.g., `std::string`).
+    Note this container is used for keys/names in objects, see @ref object_t.
+
+    #### Default type
+
+    With the default values for @a StringType (`std::string`), the default
+    value for @a string_t is:
+
+    @code {.cpp}
+    std::string
+    @endcode
+
+    #### Encoding
+
+    Strings are stored in UTF-8 encoding. Therefore, functions like
+    `std::string::size()` or `std::string::length()` return the number of
+    bytes in the string rather than the number of characters or glyphs.
+
+    #### String comparison
+
+    [RFC 7159](http://rfc7159.net/rfc7159) states:
+    > Software implementations are typically required to test names of object
+    > members for equality. Implementations that transform the textual
+    > representation into sequences of Unicode code units and then perform the
+    > comparison numerically, code unit by code unit, are interoperable in the
+    > sense that implementations will agree in all cases on equality or
+    > inequality of two strings. For example, implementations that compare
+    > strings with escaped characters unconverted may incorrectly find that
+    > `"a\\b"` and `"a\u005Cb"` are not equal.
+
+    This implementation is interoperable as it does compare strings code unit
+    by code unit.
+
+    #### Storage
+
+    String values are stored as pointers in a @ref basic_json type. That is,
+    for any access to string values, a pointer of type `string_t*` must be
+    dereferenced.
+
+    @since version 1.0.0
+    */
+    using string_t = StringType;
+
+    /*!
+    @brief a type for a boolean
+
+    [RFC 7159](http://rfc7159.net/rfc7159) implicitly describes a boolean as a
+    type which differentiates the two literals `true` and `false`.
+
+    To store objects in C++, a type is defined by the template parameter @a
+    BooleanType which chooses the type to use.
+
+    #### Default type
+
+    With the default values for @a BooleanType (`bool`), the default value for
+    @a boolean_t is:
+
+    @code {.cpp}
+    bool
+    @endcode
+
+    #### Storage
+
+    Boolean values are stored directly inside a @ref basic_json type.
+
+    @since version 1.0.0
+    */
+    using boolean_t = BooleanType;
+
+    /*!
+    @brief a type for a number (integer)
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes numbers as follows:
+    > The representation of numbers is similar to that used in most
+    > programming languages. A number is represented in base 10 using decimal
+    > digits. It contains an integer component that may be prefixed with an
+    > optional minus sign, which may be followed by a fraction part and/or an
+    > exponent part. Leading zeros are not allowed. (...) Numeric values that
+    > cannot be represented in the grammar below (such as Infinity and NaN)
+    > are not permitted.
+
+    This description includes both integer and floating-point numbers.
+    However, C++ allows more precise storage if it is known whether the number
+    is a signed integer, an unsigned integer or a floating-point number.
+    Therefore, three different types, @ref number_integer_t, @ref
+    number_unsigned_t and @ref number_float_t are used.
+
+    To store integer numbers in C++, a type is defined by the template
+    parameter @a NumberIntegerType which chooses the type to use.
+
+    #### Default type
+
+    With the default values for @a NumberIntegerType (`int64_t`), the default
+    value for @a number_integer_t is:
+
+    @code {.cpp}
+    int64_t
+    @endcode
+
+    #### Default behavior
+
+    - The restrictions about leading zeros is not enforced in C++. Instead,
+      leading zeros in integer literals lead to an interpretation as octal
+      number. Internally, the value will be stored as decimal number. For
+      instance, the C++ integer literal `010` will be serialized to `8`.
+      During deserialization, leading zeros yield an error.
+    - Not-a-number (NaN) values will be serialized to `null`.
+
+    #### Limits
+
+    [RFC 7159](http://rfc7159.net/rfc7159) specifies:
+    > An implementation may set limits on the range and precision of numbers.
+
+    When the default type is used, the maximal integer number that can be
+    stored is `9223372036854775807` (INT64_MAX) and the minimal integer number
+    that can be stored is `-9223372036854775808` (INT64_MIN). Integer numbers
+    that are out of range will yield over/underflow when used in a
+    constructor. During deserialization, too large or small integer numbers
+    will be automatically be stored as @ref number_unsigned_t or @ref
+    number_float_t.
+
+    [RFC 7159](http://rfc7159.net/rfc7159) further states:
+    > Note that when such software is used, numbers that are integers and are
+    > in the range \f$[-2^{53}+1, 2^{53}-1]\f$ are interoperable in the sense
+    > that implementations will agree exactly on their numeric values.
+
+    As this range is a subrange of the exactly supported range [INT64_MIN,
+    INT64_MAX], this class's integer type is interoperable.
+
+    #### Storage
+
+    Integer number values are stored directly inside a @ref basic_json type.
+
+    @sa @ref number_float_t -- type for number values (floating-point)
+
+    @sa @ref number_unsigned_t -- type for number values (unsigned integer)
+
+    @since version 1.0.0
+    */
+    using number_integer_t = NumberIntegerType;
+
+    /*!
+    @brief a type for a number (unsigned)
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes numbers as follows:
+    > The representation of numbers is similar to that used in most
+    > programming languages. A number is represented in base 10 using decimal
+    > digits. It contains an integer component that may be prefixed with an
+    > optional minus sign, which may be followed by a fraction part and/or an
+    > exponent part. Leading zeros are not allowed. (...) Numeric values that
+    > cannot be represented in the grammar below (such as Infinity and NaN)
+    > are not permitted.
+
+    This description includes both integer and floating-point numbers.
+    However, C++ allows more precise storage if it is known whether the number
+    is a signed integer, an unsigned integer or a floating-point number.
+    Therefore, three different types, @ref number_integer_t, @ref
+    number_unsigned_t and @ref number_float_t are used.
+
+    To store unsigned integer numbers in C++, a type is defined by the
+    template parameter @a NumberUnsignedType which chooses the type to use.
+
+    #### Default type
+
+    With the default values for @a NumberUnsignedType (`uint64_t`), the
+    default value for @a number_unsigned_t is:
+
+    @code {.cpp}
+    uint64_t
+    @endcode
+
+    #### Default behavior
+
+    - The restrictions about leading zeros is not enforced in C++. Instead,
+      leading zeros in integer literals lead to an interpretation as octal
+      number. Internally, the value will be stored as decimal number. For
+      instance, the C++ integer literal `010` will be serialized to `8`.
+      During deserialization, leading zeros yield an error.
+    - Not-a-number (NaN) values will be serialized to `null`.
+
+    #### Limits
+
+    [RFC 7159](http://rfc7159.net/rfc7159) specifies:
+    > An implementation may set limits on the range and precision of numbers.
+
+    When the default type is used, the maximal integer number that can be
+    stored is `18446744073709551615` (UINT64_MAX) and the minimal integer
+    number that can be stored is `0`. Integer numbers that are out of range
+    will yield over/underflow when used in a constructor. During
+    deserialization, too large or small integer numbers will be automatically
+    be stored as @ref number_integer_t or @ref number_float_t.
+
+    [RFC 7159](http://rfc7159.net/rfc7159) further states:
+    > Note that when such software is used, numbers that are integers and are
+    > in the range \f$[-2^{53}+1, 2^{53}-1]\f$ are interoperable in the sense
+    > that implementations will agree exactly on their numeric values.
+
+    As this range is a subrange (when considered in conjunction with the
+    number_integer_t type) of the exactly supported range [0, UINT64_MAX],
+    this class's integer type is interoperable.
+
+    #### Storage
+
+    Integer number values are stored directly inside a @ref basic_json type.
+
+    @sa @ref number_float_t -- type for number values (floating-point)
+    @sa @ref number_integer_t -- type for number values (integer)
+
+    @since version 2.0.0
+    */
+    using number_unsigned_t = NumberUnsignedType;
+
+    /*!
+    @brief a type for a number (floating-point)
+
+    [RFC 7159](http://rfc7159.net/rfc7159) describes numbers as follows:
+    > The representation of numbers is similar to that used in most
+    > programming languages. A number is represented in base 10 using decimal
+    > digits. It contains an integer component that may be prefixed with an
+    > optional minus sign, which may be followed by a fraction part and/or an
+    > exponent part. Leading zeros are not allowed. (...) Numeric values that
+    > cannot be represented in the grammar below (such as Infinity and NaN)
+    > are not permitted.
+
+    This description includes both integer and floating-point numbers.
+    However, C++ allows more precise storage if it is known whether the number
+    is a signed integer, an unsigned integer or a floating-point number.
+    Therefore, three different types, @ref number_integer_t, @ref
+    number_unsigned_t and @ref number_float_t are used.
+
+    To store floating-point numbers in C++, a type is defined by the template
+    parameter @a NumberFloatType which chooses the type to use.
+
+    #### Default type
+
+    With the default values for @a NumberFloatType (`double`), the default
+    value for @a number_float_t is:
+
+    @code {.cpp}
+    double
+    @endcode
+
+    #### Default behavior
+
+    - The restrictions about leading zeros is not enforced in C++. Instead,
+      leading zeros in floating-point literals will be ignored. Internally,
+      the value will be stored as decimal number. For instance, the C++
+      floating-point literal `01.2` will be serialized to `1.2`. During
+      deserialization, leading zeros yield an error.
+    - Not-a-number (NaN) values will be serialized to `null`.
+
+    #### Limits
+
+    [RFC 7159](http://rfc7159.net/rfc7159) states:
+    > This specification allows implementations to set limits on the range and
+    > precision of numbers accepted. Since software that implements IEEE
+    > 754-2008 binary64 (double precision) numbers is generally available and
+    > widely used, good interoperability can be achieved by implementations
+    > that expect no more precision or range than these provide, in the sense
+    > that implementations will approximate JSON numbers within the expected
+    > precision.
+
+    This implementation does exactly follow this approach, as it uses double
+    precision floating-point numbers. Note values smaller than
+    `-1.79769313486232e+308` and values greater than `1.79769313486232e+308`
+    will be stored as NaN internally and be serialized to `null`.
+
+    #### Storage
+
+    Floating-point number values are stored directly inside a @ref basic_json
+    type.
+
+    @sa @ref number_integer_t -- type for number values (integer)
+
+    @sa @ref number_unsigned_t -- type for number values (unsigned integer)
+
+    @since version 1.0.0
+    */
+    using number_float_t = NumberFloatType;
+
+    /*!
+    @brief a type for a packed binary type
+
+    This type is a type designed to carry binary data that appears in various
+    serialized formats, such as CBOR's Major Type 2, MessagePack's bin, and
+    BSON's generic binary subtype. This type is NOT a part of standard JSON and
+    exists solely for compatibility with these binary types. As such, it is
+    simply defined as an ordered sequence of zero or more byte values.
+
+    Additionally, as an implementation detail, the subtype of the binary data is
+    carried around as a `std::uint8_t`, which is compatible with both of the
+    binary data formats that use binary subtyping, (though the specific
+    numbering is incompatible with each other, and it is up to the user to
+    translate between them).
+
+    [CBOR's RFC 7049](https://tools.ietf.org/html/rfc7049) describes this type
+    as:
+    > Major type 2: a byte string. The string's length in bytes is represented
+    > following the rules for positive integers (major type 0).
+
+    [MessagePack's documentation on the bin type
+    family](https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family)
+    describes this type as:
+    > Bin format family stores an byte array in 2, 3, or 5 bytes of extra bytes
+    > in addition to the size of the byte array.
+
+    [BSON's specifications](http://bsonspec.org/spec.html) describe several
+    binary types; however, this type is intended to represent the generic binary
+    type which has the description:
+    > Generic binary subtype - This is the most commonly used binary subtype and
+    > should be the 'default' for drivers and tools.
+
+    None of these impose any limitations on the internal representation other
+    than the basic unit of storage be some type of array whose parts are
+    decomposable into bytes.
+
+    The default representation of this binary format is a
+    `std::vector<std::uint8_t>`, which is a very common way to represent a byte
+    array in modern C++.
+
+    #### Default type
+
+    The default values for @a BinaryType is `std::vector<std::uint8_t>`
+
+    #### Storage
+
+    Binary Arrays are stored as pointers in a @ref basic_json type. That is,
+    for any access to array values, a pointer of the type `binary_t*` must be
+    dereferenced.
+
+    #### Notes on subtypes
+
+    - CBOR
+       - Binary values are represented as byte strings. No subtypes are
+         supported and will be ignored when CBOR is written.
+    - MessagePack
+       - If a subtype is given and the binary array contains exactly 1, 2, 4, 8,
+         or 16 elements, the fixext family (fixext1, fixext2, fixext4, fixext8)
+         is used. For other sizes, the ext family (ext8, ext16, ext32) is used.
+         The subtype is then added as singed 8-bit integer.
+       - If no subtype is given, the bin family (bin8, bin16, bin32) is used.
+    - BSON
+       - If a subtype is given, it is used and added as unsigned 8-bit integer.
+       - If no subtype is given, the generic binary subtype 0x00 is used.
+
+    @sa @ref binary -- create a binary array
+
+    @since version 3.8.0
+    */
+    using binary_t = nlohmann::byte_container_with_subtype<BinaryType>;
+    /// @}
+
+  private:
+
+    /// helper for exception-safe object creation
+    template<typename T, typename... Args>
+    JSON_HEDLEY_RETURNS_NON_NULL
+    static T* create(Args&& ... args)
+    {
+        AllocatorType<T> alloc;
+        using AllocatorTraits = std::allocator_traits<AllocatorType<T>>;
+
+        auto deleter = [&](T * object)
+        {
+            AllocatorTraits::deallocate(alloc, object, 1);
+        };
+        std::unique_ptr<T, decltype(deleter)> object(AllocatorTraits::allocate(alloc, 1), deleter);
+        AllocatorTraits::construct(alloc, object.get(), std::forward<Args>(args)...);
+        JSON_ASSERT(object != nullptr);
+        return object.release();
+    }
+
+    ////////////////////////
+    // JSON value storage //
+    ////////////////////////
+
+    /*!
+    @brief a JSON value
+
+    The actual storage for a JSON value of the @ref basic_json class. This
+    union combines the different storage types for the JSON value types
+    defined in @ref value_t.
+
+    JSON type | value_t type    | used type
+    --------- | --------------- | ------------------------
+    object    | object          | pointer to @ref object_t
+    array     | array           | pointer to @ref array_t
+    string    | string          | pointer to @ref string_t
+    boolean   | boolean         | @ref boolean_t
+    number    | number_integer  | @ref number_integer_t
+    number    | number_unsigned | @ref number_unsigned_t
+    number    | number_float    | @ref number_float_t
+    binary    | binary          | pointer to @ref binary_t
+    null      | null            | *no value is stored*
+
+    @note Variable-length types (objects, arrays, and strings) are stored as
+    pointers. The size of the union should not exceed 64 bits if the default
+    value types are used.
+
+    @since version 1.0.0
+    */
+    union json_value
+    {
+        /// object (stored with pointer to save storage)
+        object_t* object;
+        /// array (stored with pointer to save storage)
+        array_t* array;
+        /// string (stored with pointer to save storage)
+        string_t* string;
+        /// binary (stored with pointer to save storage)
+        binary_t* binary;
+        /// boolean
+        boolean_t boolean;
+        /// number (integer)
+        number_integer_t number_integer;
+        /// number (unsigned integer)
+        number_unsigned_t number_unsigned;
+        /// number (floating-point)
+        number_float_t number_float;
+
+        /// default constructor (for null values)
+        json_value() = default;
+        /// constructor for booleans
+        json_value(boolean_t v) noexcept : boolean(v) {}
+        /// constructor for numbers (integer)
+        json_value(number_integer_t v) noexcept : number_integer(v) {}
+        /// constructor for numbers (unsigned)
+        json_value(number_unsigned_t v) noexcept : number_unsigned(v) {}
+        /// constructor for numbers (floating-point)
+        json_value(number_float_t v) noexcept : number_float(v) {}
+        /// constructor for empty values of a given type
+        json_value(value_t t)
+        {
+            switch (t)
+            {
+                case value_t::object:
+                {
+                    object = create<object_t>();
+                    break;
+                }
+
+                case value_t::array:
+                {
+                    array = create<array_t>();
+                    break;
+                }
+
+                case value_t::string:
+                {
+                    string = create<string_t>("");
+                    break;
+                }
+
+                case value_t::binary:
+                {
+                    binary = create<binary_t>();
+                    break;
+                }
+
+                case value_t::boolean:
+                {
+                    boolean = boolean_t(false);
+                    break;
+                }
+
+                case value_t::number_integer:
+                {
+                    number_integer = number_integer_t(0);
+                    break;
+                }
+
+                case value_t::number_unsigned:
+                {
+                    number_unsigned = number_unsigned_t(0);
+                    break;
+                }
+
+                case value_t::number_float:
+                {
+                    number_float = number_float_t(0.0);
+                    break;
+                }
+
+                case value_t::null:
+                {
+                    object = nullptr;  // silence warning, see #821
+                    break;
+                }
+
+                default:
+                {
+                    object = nullptr;  // silence warning, see #821
+                    if (JSON_HEDLEY_UNLIKELY(t == value_t::null))
+                    {
+                        JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.9.1")); // LCOV_EXCL_LINE
+                    }
+                    break;
+                }
+            }
+        }
+
+        /// constructor for strings
+        json_value(const string_t& value)
+        {
+            string = create<string_t>(value);
+        }
+
+        /// constructor for rvalue strings
+        json_value(string_t&& value)
+        {
+            string = create<string_t>(std::move(value));
+        }
+
+        /// constructor for objects
+        json_value(const object_t& value)
+        {
+            object = create<object_t>(value);
+        }
+
+        /// constructor for rvalue objects
+        json_value(object_t&& value)
+        {
+            object = create<object_t>(std::move(value));
+        }
+
+        /// constructor for arrays
+        json_value(const array_t& value)
+        {
+            array = create<array_t>(value);
+        }
+
+        /// constructor for rvalue arrays
+        json_value(array_t&& value)
+        {
+            array = create<array_t>(std::move(value));
+        }
+
+        /// constructor for binary arrays
+        json_value(const typename binary_t::container_type& value)
+        {
+            binary = create<binary_t>(value);
+        }
+
+        /// constructor for rvalue binary arrays
+        json_value(typename binary_t::container_type&& value)
+        {
+            binary = create<binary_t>(std::move(value));
+        }
+
+        /// constructor for binary arrays (internal type)
+        json_value(const binary_t& value)
+        {
+            binary = create<binary_t>(value);
+        }
+
+        /// constructor for rvalue binary arrays (internal type)
+        json_value(binary_t&& value)
+        {
+            binary = create<binary_t>(std::move(value));
+        }
+
+        void destroy(value_t t) noexcept
+        {
+            // flatten the current json_value to a heap-allocated stack
+            std::vector<basic_json> stack;
+
+            // move the top-level items to stack
+            if (t == value_t::array)
+            {
+                stack.reserve(array->size());
+                std::move(array->begin(), array->end(), std::back_inserter(stack));
+            }
+            else if (t == value_t::object)
+            {
+                stack.reserve(object->size());
+                for (auto&& it : *object)
+                {
+                    stack.push_back(std::move(it.second));
+                }
+            }
+
+            while (!stack.empty())
+            {
+                // move the last item to local variable to be processed
+                basic_json current_item(std::move(stack.back()));
+                stack.pop_back();
+
+                // if current_item is array/object, move
+                // its children to the stack to be processed later
+                if (current_item.is_array())
+                {
+                    std::move(current_item.m_value.array->begin(), current_item.m_value.array->end(),
+                              std::back_inserter(stack));
+
+                    current_item.m_value.array->clear();
+                }
+                else if (current_item.is_object())
+                {
+                    for (auto&& it : *current_item.m_value.object)
+                    {
+                        stack.push_back(std::move(it.second));
+                    }
+
+                    current_item.m_value.object->clear();
+                }
+
+                // it's now safe that current_item get destructed
+                // since it doesn't have any children
+            }
+
+            switch (t)
+            {
+                case value_t::object:
+                {
+                    AllocatorType<object_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, object);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, object, 1);
+                    break;
+                }
+
+                case value_t::array:
+                {
+                    AllocatorType<array_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, array);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, array, 1);
+                    break;
+                }
+
+                case value_t::string:
+                {
+                    AllocatorType<string_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, string);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, string, 1);
+                    break;
+                }
+
+                case value_t::binary:
+                {
+                    AllocatorType<binary_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, binary);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, binary, 1);
+                    break;
+                }
+
+                default:
+                {
+                    break;
+                }
+            }
+        }
+    };
+
+    /*!
+    @brief checks the class invariants
+
+    This function asserts the class invariants. It needs to be called at the
+    end of every constructor to make sure that created objects respect the
+    invariant. Furthermore, it has to be called each time the type of a JSON
+    value is changed, because the invariant expresses a relationship between
+    @a m_type and @a m_value.
+    */
+    void assert_invariant() const noexcept
+    {
+        JSON_ASSERT(m_type != value_t::object || m_value.object != nullptr);
+        JSON_ASSERT(m_type != value_t::array || m_value.array != nullptr);
+        JSON_ASSERT(m_type != value_t::string || m_value.string != nullptr);
+        JSON_ASSERT(m_type != value_t::binary || m_value.binary != nullptr);
+    }
+
+  public:
+    //////////////////////////
+    // JSON parser callback //
+    //////////////////////////
+
+    /*!
+    @brief parser event types
+
+    The parser callback distinguishes the following events:
+    - `object_start`: the parser read `{` and started to process a JSON object
+    - `key`: the parser read a key of a value in an object
+    - `object_end`: the parser read `}` and finished processing a JSON object
+    - `array_start`: the parser read `[` and started to process a JSON array
+    - `array_end`: the parser read `]` and finished processing a JSON array
+    - `value`: the parser finished reading a JSON value
+
+    @image html callback_events.png "Example when certain parse events are triggered"
+
+    @sa @ref parser_callback_t for more information and examples
+    */
+    using parse_event_t = detail::parse_event_t;
+
+    /*!
+    @brief per-element parser callback type
+
+    With a parser callback function, the result of parsing a JSON text can be
+    influenced. When passed to @ref parse, it is called on certain events
+    (passed as @ref parse_event_t via parameter @a event) with a set recursion
+    depth @a depth and context JSON value @a parsed. The return value of the
+    callback function is a boolean indicating whether the element that emitted
+    the callback shall be kept or not.
+
+    We distinguish six scenarios (determined by the event type) in which the
+    callback function can be called. The following table describes the values
+    of the parameters @a depth, @a event, and @a parsed.
+
+    parameter @a event | description | parameter @a depth | parameter @a parsed
+    ------------------ | ----------- | ------------------ | -------------------
+    parse_event_t::object_start | the parser read `{` and started to process a JSON object | depth of the parent of the JSON object | a JSON value with type discarded
+    parse_event_t::key | the parser read a key of a value in an object | depth of the currently parsed JSON object | a JSON string containing the key
+    parse_event_t::object_end | the parser read `}` and finished processing a JSON object | depth of the parent of the JSON object | the parsed JSON object
+    parse_event_t::array_start | the parser read `[` and started to process a JSON array | depth of the parent of the JSON array | a JSON value with type discarded
+    parse_event_t::array_end | the parser read `]` and finished processing a JSON array | depth of the parent of the JSON array | the parsed JSON array
+    parse_event_t::value | the parser finished reading a JSON value | depth of the value | the parsed JSON value
+
+    @image html callback_events.png "Example when certain parse events are triggered"
+
+    Discarding a value (i.e., returning `false`) has different effects
+    depending on the context in which function was called:
+
+    - Discarded values in structured types are skipped. That is, the parser
+      will behave as if the discarded value was never read.
+    - In case a value outside a structured type is skipped, it is replaced
+      with `null`. This case happens if the top-level element is skipped.
+
+    @param[in] depth  the depth of the recursion during parsing
+
+    @param[in] event  an event of type parse_event_t indicating the context in
+    the callback function has been called
+
+    @param[in,out] parsed  the current intermediate parse result; note that
+    writing to this value has no effect for parse_event_t::key events
+
+    @return Whether the JSON value which called the function during parsing
+    should be kept (`true`) or not (`false`). In the latter case, it is either
+    skipped completely or replaced by an empty discarded object.
+
+    @sa @ref parse for examples
+
+    @since version 1.0.0
+    */
+    using parser_callback_t = detail::parser_callback_t<basic_json>;
+
+    //////////////////
+    // constructors //
+    //////////////////
+
+    /// @name constructors and destructors
+    /// Constructors of class @ref basic_json, copy/move constructor, copy
+    /// assignment, static functions creating objects, and the destructor.
+    /// @{
+
+    /*!
+    @brief create an empty value with a given type
+
+    Create an empty JSON value with a given type. The value will be default
+    initialized with an empty value which depends on the type:
+
+    Value type  | initial value
+    ----------- | -------------
+    null        | `null`
+    boolean     | `false`
+    string      | `""`
+    number      | `0`
+    object      | `{}`
+    array       | `[]`
+    binary      | empty array
+
+    @param[in] v  the type of the value to create
+
+    @complexity Constant.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The following code shows the constructor for different @ref
+    value_t values,basic_json__value_t}
+
+    @sa @ref clear() -- restores the postcondition of this constructor
+
+    @since version 1.0.0
+    */
+    basic_json(const value_t v)
+        : m_type(v), m_value(v)
+    {
+        assert_invariant();
+    }
+
+    /*!
+    @brief create a null object
+
+    Create a `null` JSON value. It either takes a null pointer as parameter
+    (explicitly creating `null`) or no parameter (implicitly creating `null`).
+    The passed null pointer itself is not read -- it is only used to choose
+    the right constructor.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this constructor never throws
+    exceptions.
+
+    @liveexample{The following code shows the constructor with and without a
+    null pointer parameter.,basic_json__nullptr_t}
+
+    @since version 1.0.0
+    */
+    basic_json(std::nullptr_t = nullptr) noexcept
+        : basic_json(value_t::null)
+    {
+        assert_invariant();
+    }
+
+    /*!
+    @brief create a JSON value
+
+    This is a "catch all" constructor for all compatible JSON types; that is,
+    types for which a `to_json()` method exists. The constructor forwards the
+    parameter @a val to that method (to `json_serializer<U>::to_json` method
+    with `U = uncvref_t<CompatibleType>`, to be exact).
+
+    Template type @a CompatibleType includes, but is not limited to, the
+    following types:
+    - **arrays**: @ref array_t and all kinds of compatible containers such as
+      `std::vector`, `std::deque`, `std::list`, `std::forward_list`,
+      `std::array`, `std::valarray`, `std::set`, `std::unordered_set`,
+      `std::multiset`, and `std::unordered_multiset` with a `value_type` from
+      which a @ref basic_json value can be constructed.
+    - **objects**: @ref object_t and all kinds of compatible associative
+      containers such as `std::map`, `std::unordered_map`, `std::multimap`,
+      and `std::unordered_multimap` with a `key_type` compatible to
+      @ref string_t and a `value_type` from which a @ref basic_json value can
+      be constructed.
+    - **strings**: @ref string_t, string literals, and all compatible string
+      containers can be used.
+    - **numbers**: @ref number_integer_t, @ref number_unsigned_t,
+      @ref number_float_t, and all convertible number types such as `int`,
+      `size_t`, `int64_t`, `float` or `double` can be used.
+    - **boolean**: @ref boolean_t / `bool` can be used.
+    - **binary**: @ref binary_t / `std::vector<uint8_t>` may be used,
+      unfortunately because string literals cannot be distinguished from binary
+      character arrays by the C++ type system, all types compatible with `const
+      char*` will be directed to the string constructor instead.  This is both
+      for backwards compatibility, and due to the fact that a binary type is not
+      a standard JSON type.
+
+    See the examples below.
+
+    @tparam CompatibleType a type such that:
+    - @a CompatibleType is not derived from `std::istream`,
+    - @a CompatibleType is not @ref basic_json (to avoid hijacking copy/move
+         constructors),
+    - @a CompatibleType is not a different @ref basic_json type (i.e. with different template arguments)
+    - @a CompatibleType is not a @ref basic_json nested type (e.g.,
+         @ref json_pointer, @ref iterator, etc ...)
+    - @ref @ref json_serializer<U> has a
+         `to_json(basic_json_t&, CompatibleType&&)` method
+
+    @tparam U = `uncvref_t<CompatibleType>`
+
+    @param[in] val the value to be forwarded to the respective constructor
+
+    @complexity Usually linear in the size of the passed @a val, also
+                depending on the implementation of the called `to_json()`
+                method.
+
+    @exceptionsafety Depends on the called constructor. For types directly
+    supported by the library (i.e., all types for which no `to_json()` function
+    was provided), strong guarantee holds: if an exception is thrown, there are
+    no changes to any JSON value.
+
+    @liveexample{The following code shows the constructor with several
+    compatible types.,basic_json__CompatibleType}
+
+    @since version 2.1.0
+    */
+    template < typename CompatibleType,
+               typename U = detail::uncvref_t<CompatibleType>,
+               detail::enable_if_t <
+                   !detail::is_basic_json<U>::value && detail::is_compatible_type<basic_json_t, U>::value, int > = 0 >
+    basic_json(CompatibleType && val) noexcept(noexcept(
+                JSONSerializer<U>::to_json(std::declval<basic_json_t&>(),
+                                           std::forward<CompatibleType>(val))))
+    {
+        JSONSerializer<U>::to_json(*this, std::forward<CompatibleType>(val));
+        assert_invariant();
+    }
+
+    /*!
+    @brief create a JSON value from an existing one
+
+    This is a constructor for existing @ref basic_json types.
+    It does not hijack copy/move constructors, since the parameter has different
+    template arguments than the current ones.
+
+    The constructor tries to convert the internal @ref m_value of the parameter.
+
+    @tparam BasicJsonType a type such that:
+    - @a BasicJsonType is a @ref basic_json type.
+    - @a BasicJsonType has different template arguments than @ref basic_json_t.
+
+    @param[in] val the @ref basic_json value to be converted.
+
+    @complexity Usually linear in the size of the passed @a val, also
+                depending on the implementation of the called `to_json()`
+                method.
+
+    @exceptionsafety Depends on the called constructor. For types directly
+    supported by the library (i.e., all types for which no `to_json()` function
+    was provided), strong guarantee holds: if an exception is thrown, there are
+    no changes to any JSON value.
+
+    @since version 3.2.0
+    */
+    template < typename BasicJsonType,
+               detail::enable_if_t <
+                   detail::is_basic_json<BasicJsonType>::value&& !std::is_same<basic_json, BasicJsonType>::value, int > = 0 >
+    basic_json(const BasicJsonType& val)
+    {
+        using other_boolean_t = typename BasicJsonType::boolean_t;
+        using other_number_float_t = typename BasicJsonType::number_float_t;
+        using other_number_integer_t = typename BasicJsonType::number_integer_t;
+        using other_number_unsigned_t = typename BasicJsonType::number_unsigned_t;
+        using other_string_t = typename BasicJsonType::string_t;
+        using other_object_t = typename BasicJsonType::object_t;
+        using other_array_t = typename BasicJsonType::array_t;
+        using other_binary_t = typename BasicJsonType::binary_t;
+
+        switch (val.type())
+        {
+            case value_t::boolean:
+                JSONSerializer<other_boolean_t>::to_json(*this, val.template get<other_boolean_t>());
+                break;
+            case value_t::number_float:
+                JSONSerializer<other_number_float_t>::to_json(*this, val.template get<other_number_float_t>());
+                break;
+            case value_t::number_integer:
+                JSONSerializer<other_number_integer_t>::to_json(*this, val.template get<other_number_integer_t>());
+                break;
+            case value_t::number_unsigned:
+                JSONSerializer<other_number_unsigned_t>::to_json(*this, val.template get<other_number_unsigned_t>());
+                break;
+            case value_t::string:
+                JSONSerializer<other_string_t>::to_json(*this, val.template get_ref<const other_string_t&>());
+                break;
+            case value_t::object:
+                JSONSerializer<other_object_t>::to_json(*this, val.template get_ref<const other_object_t&>());
+                break;
+            case value_t::array:
+                JSONSerializer<other_array_t>::to_json(*this, val.template get_ref<const other_array_t&>());
+                break;
+            case value_t::binary:
+                JSONSerializer<other_binary_t>::to_json(*this, val.template get_ref<const other_binary_t&>());
+                break;
+            case value_t::null:
+                *this = nullptr;
+                break;
+            case value_t::discarded:
+                m_type = value_t::discarded;
+                break;
+            default:            // LCOV_EXCL_LINE
+                JSON_ASSERT(false);  // LCOV_EXCL_LINE
+        }
+        assert_invariant();
+    }
+
+    /*!
+    @brief create a container (array or object) from an initializer list
+
+    Creates a JSON value of type array or object from the passed initializer
+    list @a init. In case @a type_deduction is `true` (default), the type of
+    the JSON value to be created is deducted from the initializer list @a init
+    according to the following rules:
+
+    1. If the list is empty, an empty JSON object value `{}` is created.
+    2. If the list consists of pairs whose first element is a string, a JSON
+       object value is created where the first elements of the pairs are
+       treated as keys and the second elements are as values.
+    3. In all other cases, an array is created.
+
+    The rules aim to create the best fit between a C++ initializer list and
+    JSON values. The rationale is as follows:
+
+    1. The empty initializer list is written as `{}` which is exactly an empty
+       JSON object.
+    2. C++ has no way of describing mapped types other than to list a list of
+       pairs. As JSON requires that keys must be of type string, rule 2 is the
+       weakest constraint one can pose on initializer lists to interpret them
+       as an object.
+    3. In all other cases, the initializer list could not be interpreted as
+       JSON object type, so interpreting it as JSON array type is safe.
+
+    With the rules described above, the following JSON values cannot be
+    expressed by an initializer list:
+
+    - the empty array (`[]`): use @ref array(initializer_list_t)
+      with an empty initializer list in this case
+    - arrays whose elements satisfy rule 2: use @ref
+      array(initializer_list_t) with the same initializer list
+      in this case
+
+    @note When used without parentheses around an empty initializer list, @ref
+    basic_json() is called instead of this function, yielding the JSON null
+    value.
+
+    @param[in] init  initializer list with JSON values
+
+    @param[in] type_deduction internal parameter; when set to `true`, the type
+    of the JSON value is deducted from the initializer list @a init; when set
+    to `false`, the type provided via @a manual_type is forced. This mode is
+    used by the functions @ref array(initializer_list_t) and
+    @ref object(initializer_list_t).
+
+    @param[in] manual_type internal parameter; when @a type_deduction is set
+    to `false`, the created JSON value will use the provided type (only @ref
+    value_t::array and @ref value_t::object are valid); when @a type_deduction
+    is set to `true`, this parameter has no effect
+
+    @throw type_error.301 if @a type_deduction is `false`, @a manual_type is
+    `value_t::object`, but @a init contains an element which is not a pair
+    whose first element is a string. In this case, the constructor could not
+    create an object. If @a type_deduction would have be `true`, an array
+    would have been created. See @ref object(initializer_list_t)
+    for an example.
+
+    @complexity Linear in the size of the initializer list @a init.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The example below shows how JSON values are created from
+    initializer lists.,basic_json__list_init_t}
+
+    @sa @ref array(initializer_list_t) -- create a JSON array
+    value from an initializer list
+    @sa @ref object(initializer_list_t) -- create a JSON object
+    value from an initializer list
+
+    @since version 1.0.0
+    */
+    basic_json(initializer_list_t init,
+               bool type_deduction = true,
+               value_t manual_type = value_t::array)
+    {
+        // check if each element is an array with two elements whose first
+        // element is a string
+        bool is_an_object = std::all_of(init.begin(), init.end(),
+                                        [](const detail::json_ref<basic_json>& element_ref)
+        {
+            return element_ref->is_array() && element_ref->size() == 2 && (*element_ref)[0].is_string();
+        });
+
+        // adjust type if type deduction is not wanted
+        if (!type_deduction)
+        {
+            // if array is wanted, do not create an object though possible
+            if (manual_type == value_t::array)
+            {
+                is_an_object = false;
+            }
+
+            // if object is wanted but impossible, throw an exception
+            if (JSON_HEDLEY_UNLIKELY(manual_type == value_t::object && !is_an_object))
+            {
+                JSON_THROW(type_error::create(301, "cannot create object from initializer list"));
+            }
+        }
+
+        if (is_an_object)
+        {
+            // the initializer list is a list of pairs -> create object
+            m_type = value_t::object;
+            m_value = value_t::object;
+
+            std::for_each(init.begin(), init.end(), [this](const detail::json_ref<basic_json>& element_ref)
+            {
+                auto element = element_ref.moved_or_copied();
+                m_value.object->emplace(
+                    std::move(*((*element.m_value.array)[0].m_value.string)),
+                    std::move((*element.m_value.array)[1]));
+            });
+        }
+        else
+        {
+            // the initializer list describes an array -> create array
+            m_type = value_t::array;
+            m_value.array = create<array_t>(init.begin(), init.end());
+        }
+
+        assert_invariant();
+    }
+
+    /*!
+    @brief explicitly create a binary array (without subtype)
+
+    Creates a JSON binary array value from a given binary container. Binary
+    values are part of various binary formats, such as CBOR, MessagePack, and
+    BSON. This constructor is used to create a value for serialization to those
+    formats.
+
+    @note Note, this function exists because of the difficulty in correctly
+    specifying the correct template overload in the standard value ctor, as both
+    JSON arrays and JSON binary arrays are backed with some form of a
+    `std::vector`. Because JSON binary arrays are a non-standard extension it
+    was decided that it would be best to prevent automatic initialization of a
+    binary array type, for backwards compatibility and so it does not happen on
+    accident.
+
+    @param[in] init container containing bytes to use as binary type
+
+    @return JSON binary array value
+
+    @complexity Linear in the size of @a init.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @since version 3.8.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json binary(const typename binary_t::container_type& init)
+    {
+        auto res = basic_json();
+        res.m_type = value_t::binary;
+        res.m_value = init;
+        return res;
+    }
+
+    /*!
+    @brief explicitly create a binary array (with subtype)
+
+    Creates a JSON binary array value from a given binary container. Binary
+    values are part of various binary formats, such as CBOR, MessagePack, and
+    BSON. This constructor is used to create a value for serialization to those
+    formats.
+
+    @note Note, this function exists because of the difficulty in correctly
+    specifying the correct template overload in the standard value ctor, as both
+    JSON arrays and JSON binary arrays are backed with some form of a
+    `std::vector`. Because JSON binary arrays are a non-standard extension it
+    was decided that it would be best to prevent automatic initialization of a
+    binary array type, for backwards compatibility and so it does not happen on
+    accident.
+
+    @param[in] init container containing bytes to use as binary type
+    @param[in] subtype subtype to use in MessagePack and BSON
+
+    @return JSON binary array value
+
+    @complexity Linear in the size of @a init.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @since version 3.8.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json binary(const typename binary_t::container_type& init, std::uint8_t subtype)
+    {
+        auto res = basic_json();
+        res.m_type = value_t::binary;
+        res.m_value = binary_t(init, subtype);
+        return res;
+    }
+
+    /// @copydoc binary(const typename binary_t::container_type&)
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json binary(typename binary_t::container_type&& init)
+    {
+        auto res = basic_json();
+        res.m_type = value_t::binary;
+        res.m_value = std::move(init);
+        return res;
+    }
+
+    /// @copydoc binary(const typename binary_t::container_type&, std::uint8_t)
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json binary(typename binary_t::container_type&& init, std::uint8_t subtype)
+    {
+        auto res = basic_json();
+        res.m_type = value_t::binary;
+        res.m_value = binary_t(std::move(init), subtype);
+        return res;
+    }
+
+    /*!
+    @brief explicitly create an array from an initializer list
+
+    Creates a JSON array value from a given initializer list. That is, given a
+    list of values `a, b, c`, creates the JSON value `[a, b, c]`. If the
+    initializer list is empty, the empty array `[]` is created.
+
+    @note This function is only needed to express two edge cases that cannot
+    be realized with the initializer list constructor (@ref
+    basic_json(initializer_list_t, bool, value_t)). These cases
+    are:
+    1. creating an array whose elements are all pairs whose first element is a
+    string -- in this case, the initializer list constructor would create an
+    object, taking the first elements as keys
+    2. creating an empty array -- passing the empty initializer list to the
+    initializer list constructor yields an empty object
+
+    @param[in] init  initializer list with JSON values to create an array from
+    (optional)
+
+    @return JSON array value
+
+    @complexity Linear in the size of @a init.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The following code shows an example for the `array`
+    function.,array}
+
+    @sa @ref basic_json(initializer_list_t, bool, value_t) --
+    create a JSON value from an initializer list
+    @sa @ref object(initializer_list_t) -- create a JSON object
+    value from an initializer list
+
+    @since version 1.0.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json array(initializer_list_t init = {})
+    {
+        return basic_json(init, false, value_t::array);
+    }
+
+    /*!
+    @brief explicitly create an object from an initializer list
+
+    Creates a JSON object value from a given initializer list. The initializer
+    lists elements must be pairs, and their first elements must be strings. If
+    the initializer list is empty, the empty object `{}` is created.
+
+    @note This function is only added for symmetry reasons. In contrast to the
+    related function @ref array(initializer_list_t), there are
+    no cases which can only be expressed by this function. That is, any
+    initializer list @a init can also be passed to the initializer list
+    constructor @ref basic_json(initializer_list_t, bool, value_t).
+
+    @param[in] init  initializer list to create an object from (optional)
+
+    @return JSON object value
+
+    @throw type_error.301 if @a init is not a list of pairs whose first
+    elements are strings. In this case, no object can be created. When such a
+    value is passed to @ref basic_json(initializer_list_t, bool, value_t),
+    an array would have been created from the passed initializer list @a init.
+    See example below.
+
+    @complexity Linear in the size of @a init.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The following code shows an example for the `object`
+    function.,object}
+
+    @sa @ref basic_json(initializer_list_t, bool, value_t) --
+    create a JSON value from an initializer list
+    @sa @ref array(initializer_list_t) -- create a JSON array
+    value from an initializer list
+
+    @since version 1.0.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json object(initializer_list_t init = {})
+    {
+        return basic_json(init, false, value_t::object);
+    }
+
+    /*!
+    @brief construct an array with count copies of given value
+
+    Constructs a JSON array value by creating @a cnt copies of a passed value.
+    In case @a cnt is `0`, an empty array is created.
+
+    @param[in] cnt  the number of JSON copies of @a val to create
+    @param[in] val  the JSON value to copy
+
+    @post `std::distance(begin(),end()) == cnt` holds.
+
+    @complexity Linear in @a cnt.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The following code shows examples for the @ref
+    basic_json(size_type\, const basic_json&)
+    constructor.,basic_json__size_type_basic_json}
+
+    @since version 1.0.0
+    */
+    basic_json(size_type cnt, const basic_json& val)
+        : m_type(value_t::array)
+    {
+        m_value.array = create<array_t>(cnt, val);
+        assert_invariant();
+    }
+
+    /*!
+    @brief construct a JSON container given an iterator range
+
+    Constructs the JSON value with the contents of the range `[first, last)`.
+    The semantics depends on the different types a JSON value can have:
+    - In case of a null type, invalid_iterator.206 is thrown.
+    - In case of other primitive types (number, boolean, or string), @a first
+      must be `begin()` and @a last must be `end()`. In this case, the value is
+      copied. Otherwise, invalid_iterator.204 is thrown.
+    - In case of structured types (array, object), the constructor behaves as
+      similar versions for `std::vector` or `std::map`; that is, a JSON array
+      or object is constructed from the values in the range.
+
+    @tparam InputIT an input iterator type (@ref iterator or @ref
+    const_iterator)
+
+    @param[in] first begin of the range to copy from (included)
+    @param[in] last end of the range to copy from (excluded)
+
+    @pre Iterators @a first and @a last must be initialized. **This
+         precondition is enforced with an assertion (see warning).** If
+         assertions are switched off, a violation of this precondition yields
+         undefined behavior.
+
+    @pre Range `[first, last)` is valid. Usually, this precondition cannot be
+         checked efficiently. Only certain edge cases are detected; see the
+         description of the exceptions below. A violation of this precondition
+         yields undefined behavior.
+
+    @warning A precondition is enforced with a runtime assertion that will
+             result in calling `std::abort` if this precondition is not met.
+             Assertions can be disabled by defining `NDEBUG` at compile time.
+             See https://en.cppreference.com/w/cpp/error/assert for more
+             information.
+
+    @throw invalid_iterator.201 if iterators @a first and @a last are not
+    compatible (i.e., do not belong to the same JSON value). In this case,
+    the range `[first, last)` is undefined.
+    @throw invalid_iterator.204 if iterators @a first and @a last belong to a
+    primitive type (number, boolean, or string), but @a first does not point
+    to the first element any more. In this case, the range `[first, last)` is
+    undefined. See example code below.
+    @throw invalid_iterator.206 if iterators @a first and @a last belong to a
+    null value. In this case, the range `[first, last)` is undefined.
+
+    @complexity Linear in distance between @a first and @a last.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @liveexample{The example below shows several ways to create JSON values by
+    specifying a subrange with iterators.,basic_json__InputIt_InputIt}
+
+    @since version 1.0.0
+    */
+    template < class InputIT, typename std::enable_if <
+                   std::is_same<InputIT, typename basic_json_t::iterator>::value ||
+                   std::is_same<InputIT, typename basic_json_t::const_iterator>::value, int >::type = 0 >
+    basic_json(InputIT first, InputIT last)
+    {
+        JSON_ASSERT(first.m_object != nullptr);
+        JSON_ASSERT(last.m_object != nullptr);
+
+        // make sure iterator fits the current value
+        if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(201, "iterators are not compatible"));
+        }
+
+        // copy type from first iterator
+        m_type = first.m_object->m_type;
+
+        // check if iterator range is complete for primitive values
+        switch (m_type)
+        {
+            case value_t::boolean:
+            case value_t::number_float:
+            case value_t::number_integer:
+            case value_t::number_unsigned:
+            case value_t::string:
+            {
+                if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin()
+                                         || !last.m_it.primitive_iterator.is_end()))
+                {
+                    JSON_THROW(invalid_iterator::create(204, "iterators out of range"));
+                }
+                break;
+            }
+
+            default:
+                break;
+        }
+
+        switch (m_type)
+        {
+            case value_t::number_integer:
+            {
+                m_value.number_integer = first.m_object->m_value.number_integer;
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                m_value.number_unsigned = first.m_object->m_value.number_unsigned;
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                m_value.number_float = first.m_object->m_value.number_float;
+                break;
+            }
+
+            case value_t::boolean:
+            {
+                m_value.boolean = first.m_object->m_value.boolean;
+                break;
+            }
+
+            case value_t::string:
+            {
+                m_value = *first.m_object->m_value.string;
+                break;
+            }
+
+            case value_t::object:
+            {
+                m_value.object = create<object_t>(first.m_it.object_iterator,
+                                                  last.m_it.object_iterator);
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_value.array = create<array_t>(first.m_it.array_iterator,
+                                                last.m_it.array_iterator);
+                break;
+            }
+
+            case value_t::binary:
+            {
+                m_value = *first.m_object->m_value.binary;
+                break;
+            }
+
+            default:
+                JSON_THROW(invalid_iterator::create(206, "cannot construct with iterators from " +
+                                                    std::string(first.m_object->type_name())));
+        }
+
+        assert_invariant();
+    }
+
+
+    ///////////////////////////////////////
+    // other constructors and destructor //
+    ///////////////////////////////////////
+
+    template<typename JsonRef,
+             detail::enable_if_t<detail::conjunction<detail::is_json_ref<JsonRef>,
+                                 std::is_same<typename JsonRef::value_type, basic_json>>::value, int> = 0 >
+    basic_json(const JsonRef& ref) : basic_json(ref.moved_or_copied()) {}
+
+    /*!
+    @brief copy constructor
+
+    Creates a copy of a given JSON value.
+
+    @param[in] other  the JSON value to copy
+
+    @post `*this == other`
+
+    @complexity Linear in the size of @a other.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes to any JSON value.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is linear.
+    - As postcondition, it holds: `other == basic_json(other)`.
+
+    @liveexample{The following code shows an example for the copy
+    constructor.,basic_json__basic_json}
+
+    @since version 1.0.0
+    */
+    basic_json(const basic_json& other)
+        : m_type(other.m_type)
+    {
+        // check of passed value is valid
+        other.assert_invariant();
+
+        switch (m_type)
+        {
+            case value_t::object:
+            {
+                m_value = *other.m_value.object;
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_value = *other.m_value.array;
+                break;
+            }
+
+            case value_t::string:
+            {
+                m_value = *other.m_value.string;
+                break;
+            }
+
+            case value_t::boolean:
+            {
+                m_value = other.m_value.boolean;
+                break;
+            }
+
+            case value_t::number_integer:
+            {
+                m_value = other.m_value.number_integer;
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                m_value = other.m_value.number_unsigned;
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                m_value = other.m_value.number_float;
+                break;
+            }
+
+            case value_t::binary:
+            {
+                m_value = *other.m_value.binary;
+                break;
+            }
+
+            default:
+                break;
+        }
+
+        assert_invariant();
+    }
+
+    /*!
+    @brief move constructor
+
+    Move constructor. Constructs a JSON value with the contents of the given
+    value @a other using move semantics. It "steals" the resources from @a
+    other and leaves it as JSON null value.
+
+    @param[in,out] other  value to move to this object
+
+    @post `*this` has the same value as @a other before the call.
+    @post @a other is a JSON null value.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this constructor never throws
+    exceptions.
+
+    @requirement This function helps `basic_json` satisfying the
+    [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible)
+    requirements.
+
+    @liveexample{The code below shows the move constructor explicitly called
+    via std::move.,basic_json__moveconstructor}
+
+    @since version 1.0.0
+    */
+    basic_json(basic_json&& other) noexcept
+        : m_type(std::move(other.m_type)),
+          m_value(std::move(other.m_value))
+    {
+        // check that passed value is valid
+        other.assert_invariant();
+
+        // invalidate payload
+        other.m_type = value_t::null;
+        other.m_value = {};
+
+        assert_invariant();
+    }
+
+    /*!
+    @brief copy assignment
+
+    Copy assignment operator. Copies a JSON value via the "copy and swap"
+    strategy: It is expressed in terms of the copy constructor, destructor,
+    and the `swap()` member function.
+
+    @param[in] other  value to copy from
+
+    @complexity Linear.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is linear.
+
+    @liveexample{The code below shows and example for the copy assignment. It
+    creates a copy of value `a` which is then swapped with `b`. Finally\, the
+    copy of `a` (which is the null value after the swap) is
+    destroyed.,basic_json__copyassignment}
+
+    @since version 1.0.0
+    */
+    basic_json& operator=(basic_json other) noexcept (
+        std::is_nothrow_move_constructible<value_t>::value&&
+        std::is_nothrow_move_assignable<value_t>::value&&
+        std::is_nothrow_move_constructible<json_value>::value&&
+        std::is_nothrow_move_assignable<json_value>::value
+    )
+    {
+        // check that passed value is valid
+        other.assert_invariant();
+
+        using std::swap;
+        swap(m_type, other.m_type);
+        swap(m_value, other.m_value);
+
+        assert_invariant();
+        return *this;
+    }
+
+    /*!
+    @brief destructor
+
+    Destroys the JSON value and frees all allocated memory.
+
+    @complexity Linear.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is linear.
+    - All stored elements are destroyed and all memory is freed.
+
+    @since version 1.0.0
+    */
+    ~basic_json() noexcept
+    {
+        assert_invariant();
+        m_value.destroy(m_type);
+    }
+
+    /// @}
+
+  public:
+    ///////////////////////
+    // object inspection //
+    ///////////////////////
+
+    /// @name object inspection
+    /// Functions to inspect the type of a JSON value.
+    /// @{
+
+    /*!
+    @brief serialization
+
+    Serialization function for JSON values. The function tries to mimic
+    Python's `json.dumps()` function, and currently supports its @a indent
+    and @a ensure_ascii parameters.
+
+    @param[in] indent If indent is nonnegative, then array elements and object
+    members will be pretty-printed with that indent level. An indent level of
+    `0` will only insert newlines. `-1` (the default) selects the most compact
+    representation.
+    @param[in] indent_char The character to use for indentation if @a indent is
+    greater than `0`. The default is ` ` (space).
+    @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters
+    in the output are escaped with `\uXXXX` sequences, and the result consists
+    of ASCII characters only.
+    @param[in] error_handler  how to react on decoding errors; there are three
+    possible values: `strict` (throws and exception in case a decoding error
+    occurs; default), `replace` (replace invalid UTF-8 sequences with U+FFFD),
+    and `ignore` (ignore invalid UTF-8 sequences during serialization; all
+    bytes are copied to the output unchanged).
+
+    @return string containing the serialization of the JSON value
+
+    @throw type_error.316 if a string stored inside the JSON value is not
+                          UTF-8 encoded and @a error_handler is set to strict
+
+    @note Binary values are serialized as object containing two keys:
+      - "bytes": an array of bytes as integers
+      - "subtype": the subtype as integer or "null" if the binary has no subtype
+
+    @complexity Linear.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @liveexample{The following example shows the effect of different @a indent\,
+    @a indent_char\, and @a ensure_ascii parameters to the result of the
+    serialization.,dump}
+
+    @see https://docs.python.org/2/library/json.html#json.dump
+
+    @since version 1.0.0; indentation character @a indent_char, option
+           @a ensure_ascii and exceptions added in version 3.0.0; error
+           handlers added in version 3.4.0; serialization of binary values added
+           in version 3.8.0.
+    */
+    string_t dump(const int indent = -1,
+                  const char indent_char = ' ',
+                  const bool ensure_ascii = false,
+                  const error_handler_t error_handler = error_handler_t::strict) const
+    {
+        string_t result;
+        serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);
+
+        if (indent >= 0)
+        {
+            s.dump(*this, true, ensure_ascii, static_cast<unsigned int>(indent));
+        }
+        else
+        {
+            s.dump(*this, false, ensure_ascii, 0);
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief return the type of the JSON value (explicit)
+
+    Return the type of the JSON value as a value from the @ref value_t
+    enumeration.
+
+    @return the type of the JSON value
+            Value type                | return value
+            ------------------------- | -------------------------
+            null                      | value_t::null
+            boolean                   | value_t::boolean
+            string                    | value_t::string
+            number (integer)          | value_t::number_integer
+            number (unsigned integer) | value_t::number_unsigned
+            number (floating-point)   | value_t::number_float
+            object                    | value_t::object
+            array                     | value_t::array
+            binary                    | value_t::binary
+            discarded                 | value_t::discarded
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `type()` for all JSON
+    types.,type}
+
+    @sa @ref operator value_t() -- return the type of the JSON value (implicit)
+    @sa @ref type_name() -- return the type as string
+
+    @since version 1.0.0
+    */
+    constexpr value_t type() const noexcept
+    {
+        return m_type;
+    }
+
+    /*!
+    @brief return whether type is primitive
+
+    This function returns true if and only if the JSON type is primitive
+    (string, number, boolean, or null).
+
+    @return `true` if type is primitive (string, number, boolean, or null),
+    `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_primitive()` for all JSON
+    types.,is_primitive}
+
+    @sa @ref is_structured() -- returns whether JSON value is structured
+    @sa @ref is_null() -- returns whether JSON value is `null`
+    @sa @ref is_string() -- returns whether JSON value is a string
+    @sa @ref is_boolean() -- returns whether JSON value is a boolean
+    @sa @ref is_number() -- returns whether JSON value is a number
+    @sa @ref is_binary() -- returns whether JSON value is a binary array
+
+    @since version 1.0.0
+    */
+    constexpr bool is_primitive() const noexcept
+    {
+        return is_null() || is_string() || is_boolean() || is_number() || is_binary();
+    }
+
+    /*!
+    @brief return whether type is structured
+
+    This function returns true if and only if the JSON type is structured
+    (array or object).
+
+    @return `true` if type is structured (array or object), `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_structured()` for all JSON
+    types.,is_structured}
+
+    @sa @ref is_primitive() -- returns whether value is primitive
+    @sa @ref is_array() -- returns whether value is an array
+    @sa @ref is_object() -- returns whether value is an object
+
+    @since version 1.0.0
+    */
+    constexpr bool is_structured() const noexcept
+    {
+        return is_array() || is_object();
+    }
+
+    /*!
+    @brief return whether value is null
+
+    This function returns true if and only if the JSON value is null.
+
+    @return `true` if type is null, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_null()` for all JSON
+    types.,is_null}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_null() const noexcept
+    {
+        return m_type == value_t::null;
+    }
+
+    /*!
+    @brief return whether value is a boolean
+
+    This function returns true if and only if the JSON value is a boolean.
+
+    @return `true` if type is boolean, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_boolean()` for all JSON
+    types.,is_boolean}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_boolean() const noexcept
+    {
+        return m_type == value_t::boolean;
+    }
+
+    /*!
+    @brief return whether value is a number
+
+    This function returns true if and only if the JSON value is a number. This
+    includes both integer (signed and unsigned) and floating-point values.
+
+    @return `true` if type is number (regardless whether integer, unsigned
+    integer or floating-type), `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_number()` for all JSON
+    types.,is_number}
+
+    @sa @ref is_number_integer() -- check if value is an integer or unsigned
+    integer number
+    @sa @ref is_number_unsigned() -- check if value is an unsigned integer
+    number
+    @sa @ref is_number_float() -- check if value is a floating-point number
+
+    @since version 1.0.0
+    */
+    constexpr bool is_number() const noexcept
+    {
+        return is_number_integer() || is_number_float();
+    }
+
+    /*!
+    @brief return whether value is an integer number
+
+    This function returns true if and only if the JSON value is a signed or
+    unsigned integer number. This excludes floating-point values.
+
+    @return `true` if type is an integer or unsigned integer number, `false`
+    otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_number_integer()` for all
+    JSON types.,is_number_integer}
+
+    @sa @ref is_number() -- check if value is a number
+    @sa @ref is_number_unsigned() -- check if value is an unsigned integer
+    number
+    @sa @ref is_number_float() -- check if value is a floating-point number
+
+    @since version 1.0.0
+    */
+    constexpr bool is_number_integer() const noexcept
+    {
+        return m_type == value_t::number_integer || m_type == value_t::number_unsigned;
+    }
+
+    /*!
+    @brief return whether value is an unsigned integer number
+
+    This function returns true if and only if the JSON value is an unsigned
+    integer number. This excludes floating-point and signed integer values.
+
+    @return `true` if type is an unsigned integer number, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_number_unsigned()` for all
+    JSON types.,is_number_unsigned}
+
+    @sa @ref is_number() -- check if value is a number
+    @sa @ref is_number_integer() -- check if value is an integer or unsigned
+    integer number
+    @sa @ref is_number_float() -- check if value is a floating-point number
+
+    @since version 2.0.0
+    */
+    constexpr bool is_number_unsigned() const noexcept
+    {
+        return m_type == value_t::number_unsigned;
+    }
+
+    /*!
+    @brief return whether value is a floating-point number
+
+    This function returns true if and only if the JSON value is a
+    floating-point number. This excludes signed and unsigned integer values.
+
+    @return `true` if type is a floating-point number, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_number_float()` for all
+    JSON types.,is_number_float}
+
+    @sa @ref is_number() -- check if value is number
+    @sa @ref is_number_integer() -- check if value is an integer number
+    @sa @ref is_number_unsigned() -- check if value is an unsigned integer
+    number
+
+    @since version 1.0.0
+    */
+    constexpr bool is_number_float() const noexcept
+    {
+        return m_type == value_t::number_float;
+    }
+
+    /*!
+    @brief return whether value is an object
+
+    This function returns true if and only if the JSON value is an object.
+
+    @return `true` if type is object, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_object()` for all JSON
+    types.,is_object}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_object() const noexcept
+    {
+        return m_type == value_t::object;
+    }
+
+    /*!
+    @brief return whether value is an array
+
+    This function returns true if and only if the JSON value is an array.
+
+    @return `true` if type is array, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_array()` for all JSON
+    types.,is_array}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_array() const noexcept
+    {
+        return m_type == value_t::array;
+    }
+
+    /*!
+    @brief return whether value is a string
+
+    This function returns true if and only if the JSON value is a string.
+
+    @return `true` if type is string, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_string()` for all JSON
+    types.,is_string}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_string() const noexcept
+    {
+        return m_type == value_t::string;
+    }
+
+    /*!
+    @brief return whether value is a binary array
+
+    This function returns true if and only if the JSON value is a binary array.
+
+    @return `true` if type is binary array, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_binary()` for all JSON
+    types.,is_binary}
+
+    @since version 3.8.0
+    */
+    constexpr bool is_binary() const noexcept
+    {
+        return m_type == value_t::binary;
+    }
+
+    /*!
+    @brief return whether value is discarded
+
+    This function returns true if and only if the JSON value was discarded
+    during parsing with a callback function (see @ref parser_callback_t).
+
+    @note This function will always be `false` for JSON values after parsing.
+    That is, discarded values can only occur during parsing, but will be
+    removed when inside a structured value or replaced by null in other cases.
+
+    @return `true` if type is discarded, `false` otherwise.
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies `is_discarded()` for all JSON
+    types.,is_discarded}
+
+    @since version 1.0.0
+    */
+    constexpr bool is_discarded() const noexcept
+    {
+        return m_type == value_t::discarded;
+    }
+
+    /*!
+    @brief return the type of the JSON value (implicit)
+
+    Implicitly return the type of the JSON value as a value from the @ref
+    value_t enumeration.
+
+    @return the type of the JSON value
+
+    @complexity Constant.
+
+    @exceptionsafety No-throw guarantee: this member function never throws
+    exceptions.
+
+    @liveexample{The following code exemplifies the @ref value_t operator for
+    all JSON types.,operator__value_t}
+
+    @sa @ref type() -- return the type of the JSON value (explicit)
+    @sa @ref type_name() -- return the type as string
+
+    @since version 1.0.0
+    */
+    constexpr operator value_t() const noexcept
+    {
+        return m_type;
+    }
+
+    /// @}
+
+  private:
+    //////////////////
+    // value access //
+    //////////////////
+
+    /// get a boolean (explicit)
+    boolean_t get_impl(boolean_t* /*unused*/) const
+    {
+        if (JSON_HEDLEY_LIKELY(is_boolean()))
+        {
+            return m_value.boolean;
+        }
+
+        JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(type_name())));
+    }
+
+    /// get a pointer to the value (object)
+    object_t* get_impl_ptr(object_t* /*unused*/) noexcept
+    {
+        return is_object() ? m_value.object : nullptr;
+    }
+
+    /// get a pointer to the value (object)
+    constexpr const object_t* get_impl_ptr(const object_t* /*unused*/) const noexcept
+    {
+        return is_object() ? m_value.object : nullptr;
+    }
+
+    /// get a pointer to the value (array)
+    array_t* get_impl_ptr(array_t* /*unused*/) noexcept
+    {
+        return is_array() ? m_value.array : nullptr;
+    }
+
+    /// get a pointer to the value (array)
+    constexpr const array_t* get_impl_ptr(const array_t* /*unused*/) const noexcept
+    {
+        return is_array() ? m_value.array : nullptr;
+    }
+
+    /// get a pointer to the value (string)
+    string_t* get_impl_ptr(string_t* /*unused*/) noexcept
+    {
+        return is_string() ? m_value.string : nullptr;
+    }
+
+    /// get a pointer to the value (string)
+    constexpr const string_t* get_impl_ptr(const string_t* /*unused*/) const noexcept
+    {
+        return is_string() ? m_value.string : nullptr;
+    }
+
+    /// get a pointer to the value (boolean)
+    boolean_t* get_impl_ptr(boolean_t* /*unused*/) noexcept
+    {
+        return is_boolean() ? &m_value.boolean : nullptr;
+    }
+
+    /// get a pointer to the value (boolean)
+    constexpr const boolean_t* get_impl_ptr(const boolean_t* /*unused*/) const noexcept
+    {
+        return is_boolean() ? &m_value.boolean : nullptr;
+    }
+
+    /// get a pointer to the value (integer number)
+    number_integer_t* get_impl_ptr(number_integer_t* /*unused*/) noexcept
+    {
+        return is_number_integer() ? &m_value.number_integer : nullptr;
+    }
+
+    /// get a pointer to the value (integer number)
+    constexpr const number_integer_t* get_impl_ptr(const number_integer_t* /*unused*/) const noexcept
+    {
+        return is_number_integer() ? &m_value.number_integer : nullptr;
+    }
+
+    /// get a pointer to the value (unsigned number)
+    number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept
+    {
+        return is_number_unsigned() ? &m_value.number_unsigned : nullptr;
+    }
+
+    /// get a pointer to the value (unsigned number)
+    constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept
+    {
+        return is_number_unsigned() ? &m_value.number_unsigned : nullptr;
+    }
+
+    /// get a pointer to the value (floating-point number)
+    number_float_t* get_impl_ptr(number_float_t* /*unused*/) noexcept
+    {
+        return is_number_float() ? &m_value.number_float : nullptr;
+    }
+
+    /// get a pointer to the value (floating-point number)
+    constexpr const number_float_t* get_impl_ptr(const number_float_t* /*unused*/) const noexcept
+    {
+        return is_number_float() ? &m_value.number_float : nullptr;
+    }
+
+    /// get a pointer to the value (binary)
+    binary_t* get_impl_ptr(binary_t* /*unused*/) noexcept
+    {
+        return is_binary() ? m_value.binary : nullptr;
+    }
+
+    /// get a pointer to the value (binary)
+    constexpr const binary_t* get_impl_ptr(const binary_t* /*unused*/) const noexcept
+    {
+        return is_binary() ? m_value.binary : nullptr;
+    }
+
+    /*!
+    @brief helper function to implement get_ref()
+
+    This function helps to implement get_ref() without code duplication for
+    const and non-const overloads
+
+    @tparam ThisType will be deduced as `basic_json` or `const basic_json`
+
+    @throw type_error.303 if ReferenceType does not match underlying value
+    type of the current JSON
+    */
+    template<typename ReferenceType, typename ThisType>
+    static ReferenceType get_ref_impl(ThisType& obj)
+    {
+        // delegate the call to get_ptr<>()
+        auto ptr = obj.template get_ptr<typename std::add_pointer<ReferenceType>::type>();
+
+        if (JSON_HEDLEY_LIKELY(ptr != nullptr))
+        {
+            return *ptr;
+        }
+
+        JSON_THROW(type_error::create(303, "incompatible ReferenceType for get_ref, actual type is " + std::string(obj.type_name())));
+    }
+
+  public:
+    /// @name value access
+    /// Direct access to the stored value of a JSON value.
+    /// @{
+
+    /*!
+    @brief get special-case overload
+
+    This overloads avoids a lot of template boilerplate, it can be seen as the
+    identity method
+
+    @tparam BasicJsonType == @ref basic_json
+
+    @return a copy of *this
+
+    @complexity Constant.
+
+    @since version 2.1.0
+    */
+    template<typename BasicJsonType, detail::enable_if_t<
+                 std::is_same<typename std::remove_const<BasicJsonType>::type, basic_json_t>::value,
+                 int> = 0>
+    basic_json get() const
+    {
+        return *this;
+    }
+
+    /*!
+    @brief get special-case overload
+
+    This overloads converts the current @ref basic_json in a different
+    @ref basic_json type
+
+    @tparam BasicJsonType == @ref basic_json
+
+    @return a copy of *this, converted into @tparam BasicJsonType
+
+    @complexity Depending on the implementation of the called `from_json()`
+                method.
+
+    @since version 3.2.0
+    */
+    template < typename BasicJsonType, detail::enable_if_t <
+                   !std::is_same<BasicJsonType, basic_json>::value&&
+                   detail::is_basic_json<BasicJsonType>::value, int > = 0 >
+    BasicJsonType get() const
+    {
+        return *this;
+    }
+
+    /*!
+    @brief get a value (explicit)
+
+    Explicit type conversion between the JSON value and a compatible value
+    which is [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible)
+    and [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible).
+    The value is converted by calling the @ref json_serializer<ValueType>
+    `from_json()` method.
+
+    The function is equivalent to executing
+    @code {.cpp}
+    ValueType ret;
+    JSONSerializer<ValueType>::from_json(*this, ret);
+    return ret;
+    @endcode
+
+    This overloads is chosen if:
+    - @a ValueType is not @ref basic_json,
+    - @ref json_serializer<ValueType> has a `from_json()` method of the form
+      `void from_json(const basic_json&, ValueType&)`, and
+    - @ref json_serializer<ValueType> does not have a `from_json()` method of
+      the form `ValueType from_json(const basic_json&)`
+
+    @tparam ValueTypeCV the provided value type
+    @tparam ValueType the returned value type
+
+    @return copy of the JSON value, converted to @a ValueType
+
+    @throw what @ref json_serializer<ValueType> `from_json()` method throws
+
+    @liveexample{The example below shows several conversions from JSON values
+    to other types. There a few things to note: (1) Floating-point numbers can
+    be converted to integers\, (2) A JSON array can be converted to a standard
+    `std::vector<short>`\, (3) A JSON object can be converted to C++
+    associative containers such as `std::unordered_map<std::string\,
+    json>`.,get__ValueType_const}
+
+    @since version 2.1.0
+    */
+    template < typename ValueTypeCV, typename ValueType = detail::uncvref_t<ValueTypeCV>,
+               detail::enable_if_t <
+                   !detail::is_basic_json<ValueType>::value &&
+                   detail::has_from_json<basic_json_t, ValueType>::value &&
+                   !detail::has_non_default_from_json<basic_json_t, ValueType>::value,
+                   int > = 0 >
+    ValueType get() const noexcept(noexcept(
+                                       JSONSerializer<ValueType>::from_json(std::declval<const basic_json_t&>(), std::declval<ValueType&>())))
+    {
+        // we cannot static_assert on ValueTypeCV being non-const, because
+        // there is support for get<const basic_json_t>(), which is why we
+        // still need the uncvref
+        static_assert(!std::is_reference<ValueTypeCV>::value,
+                      "get() cannot be used with reference types, you might want to use get_ref()");
+        static_assert(std::is_default_constructible<ValueType>::value,
+                      "types must be DefaultConstructible when used with get()");
+
+        ValueType ret;
+        JSONSerializer<ValueType>::from_json(*this, ret);
+        return ret;
+    }
+
+    /*!
+    @brief get a value (explicit); special case
+
+    Explicit type conversion between the JSON value and a compatible value
+    which is **not** [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible)
+    and **not** [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible).
+    The value is converted by calling the @ref json_serializer<ValueType>
+    `from_json()` method.
+
+    The function is equivalent to executing
+    @code {.cpp}
+    return JSONSerializer<ValueTypeCV>::from_json(*this);
+    @endcode
+
+    This overloads is chosen if:
+    - @a ValueType is not @ref basic_json and
+    - @ref json_serializer<ValueType> has a `from_json()` method of the form
+      `ValueType from_json(const basic_json&)`
+
+    @note If @ref json_serializer<ValueType> has both overloads of
+    `from_json()`, this one is chosen.
+
+    @tparam ValueTypeCV the provided value type
+    @tparam ValueType the returned value type
+
+    @return copy of the JSON value, converted to @a ValueType
+
+    @throw what @ref json_serializer<ValueType> `from_json()` method throws
+
+    @since version 2.1.0
+    */
+    template < typename ValueTypeCV, typename ValueType = detail::uncvref_t<ValueTypeCV>,
+               detail::enable_if_t < !std::is_same<basic_json_t, ValueType>::value &&
+                                     detail::has_non_default_from_json<basic_json_t, ValueType>::value,
+                                     int > = 0 >
+    ValueType get() const noexcept(noexcept(
+                                       JSONSerializer<ValueType>::from_json(std::declval<const basic_json_t&>())))
+    {
+        static_assert(!std::is_reference<ValueTypeCV>::value,
+                      "get() cannot be used with reference types, you might want to use get_ref()");
+        return JSONSerializer<ValueType>::from_json(*this);
+    }
+
+    /*!
+    @brief get a value (explicit)
+
+    Explicit type conversion between the JSON value and a compatible value.
+    The value is filled into the input parameter by calling the @ref json_serializer<ValueType>
+    `from_json()` method.
+
+    The function is equivalent to executing
+    @code {.cpp}
+    ValueType v;
+    JSONSerializer<ValueType>::from_json(*this, v);
+    @endcode
+
+    This overloads is chosen if:
+    - @a ValueType is not @ref basic_json,
+    - @ref json_serializer<ValueType> has a `from_json()` method of the form
+      `void from_json(const basic_json&, ValueType&)`, and
+
+    @tparam ValueType the input parameter type.
+
+    @return the input parameter, allowing chaining calls.
+
+    @throw what @ref json_serializer<ValueType> `from_json()` method throws
+
+    @liveexample{The example below shows several conversions from JSON values
+    to other types. There a few things to note: (1) Floating-point numbers can
+    be converted to integers\, (2) A JSON array can be converted to a standard
+    `std::vector<short>`\, (3) A JSON object can be converted to C++
+    associative containers such as `std::unordered_map<std::string\,
+    json>`.,get_to}
+
+    @since version 3.3.0
+    */
+    template < typename ValueType,
+               detail::enable_if_t <
+                   !detail::is_basic_json<ValueType>::value&&
+                   detail::has_from_json<basic_json_t, ValueType>::value,
+                   int > = 0 >
+    ValueType & get_to(ValueType& v) const noexcept(noexcept(
+                JSONSerializer<ValueType>::from_json(std::declval<const basic_json_t&>(), v)))
+    {
+        JSONSerializer<ValueType>::from_json(*this, v);
+        return v;
+    }
+
+    // specialization to allow to call get_to with a basic_json value
+    // see https://github.com/nlohmann/json/issues/2175
+    template<typename ValueType,
+             detail::enable_if_t <
+                 detail::is_basic_json<ValueType>::value,
+                 int> = 0>
+    ValueType & get_to(ValueType& v) const
+    {
+        v = *this;
+        return v;
+    }
+
+    template <
+        typename T, std::size_t N,
+        typename Array = T (&)[N],
+        detail::enable_if_t <
+            detail::has_from_json<basic_json_t, Array>::value, int > = 0 >
+    Array get_to(T (&v)[N]) const
+    noexcept(noexcept(JSONSerializer<Array>::from_json(
+                          std::declval<const basic_json_t&>(), v)))
+    {
+        JSONSerializer<Array>::from_json(*this, v);
+        return v;
+    }
+
+
+    /*!
+    @brief get a pointer value (implicit)
+
+    Implicit pointer access to the internally stored JSON value. No copies are
+    made.
+
+    @warning Writing data to the pointee of the result yields an undefined
+    state.
+
+    @tparam PointerType pointer type; must be a pointer to @ref array_t, @ref
+    object_t, @ref string_t, @ref boolean_t, @ref number_integer_t,
+    @ref number_unsigned_t, or @ref number_float_t. Enforced by a static
+    assertion.
+
+    @return pointer to the internally stored JSON value if the requested
+    pointer type @a PointerType fits to the JSON value; `nullptr` otherwise
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how pointers to internal values of a
+    JSON value can be requested. Note that no type conversions are made and a
+    `nullptr` is returned if the value and the requested pointer type does not
+    match.,get_ptr}
+
+    @since version 1.0.0
+    */
+    template<typename PointerType, typename std::enable_if<
+                 std::is_pointer<PointerType>::value, int>::type = 0>
+    auto get_ptr() noexcept -> decltype(std::declval<basic_json_t&>().get_impl_ptr(std::declval<PointerType>()))
+    {
+        // delegate the call to get_impl_ptr<>()
+        return get_impl_ptr(static_cast<PointerType>(nullptr));
+    }
+
+    /*!
+    @brief get a pointer value (implicit)
+    @copydoc get_ptr()
+    */
+    template < typename PointerType, typename std::enable_if <
+                   std::is_pointer<PointerType>::value&&
+                   std::is_const<typename std::remove_pointer<PointerType>::type>::value, int >::type = 0 >
+    constexpr auto get_ptr() const noexcept -> decltype(std::declval<const basic_json_t&>().get_impl_ptr(std::declval<PointerType>()))
+    {
+        // delegate the call to get_impl_ptr<>() const
+        return get_impl_ptr(static_cast<PointerType>(nullptr));
+    }
+
+    /*!
+    @brief get a pointer value (explicit)
+
+    Explicit pointer access to the internally stored JSON value. No copies are
+    made.
+
+    @warning The pointer becomes invalid if the underlying JSON object
+    changes.
+
+    @tparam PointerType pointer type; must be a pointer to @ref array_t, @ref
+    object_t, @ref string_t, @ref boolean_t, @ref number_integer_t,
+    @ref number_unsigned_t, or @ref number_float_t.
+
+    @return pointer to the internally stored JSON value if the requested
+    pointer type @a PointerType fits to the JSON value; `nullptr` otherwise
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how pointers to internal values of a
+    JSON value can be requested. Note that no type conversions are made and a
+    `nullptr` is returned if the value and the requested pointer type does not
+    match.,get__PointerType}
+
+    @sa @ref get_ptr() for explicit pointer-member access
+
+    @since version 1.0.0
+    */
+    template<typename PointerType, typename std::enable_if<
+                 std::is_pointer<PointerType>::value, int>::type = 0>
+    auto get() noexcept -> decltype(std::declval<basic_json_t&>().template get_ptr<PointerType>())
+    {
+        // delegate the call to get_ptr
+        return get_ptr<PointerType>();
+    }
+
+    /*!
+    @brief get a pointer value (explicit)
+    @copydoc get()
+    */
+    template<typename PointerType, typename std::enable_if<
+                 std::is_pointer<PointerType>::value, int>::type = 0>
+    constexpr auto get() const noexcept -> decltype(std::declval<const basic_json_t&>().template get_ptr<PointerType>())
+    {
+        // delegate the call to get_ptr
+        return get_ptr<PointerType>();
+    }
+
+    /*!
+    @brief get a reference value (implicit)
+
+    Implicit reference access to the internally stored JSON value. No copies
+    are made.
+
+    @warning Writing data to the referee of the result yields an undefined
+    state.
+
+    @tparam ReferenceType reference type; must be a reference to @ref array_t,
+    @ref object_t, @ref string_t, @ref boolean_t, @ref number_integer_t, or
+    @ref number_float_t. Enforced by static assertion.
+
+    @return reference to the internally stored JSON value if the requested
+    reference type @a ReferenceType fits to the JSON value; throws
+    type_error.303 otherwise
+
+    @throw type_error.303 in case passed type @a ReferenceType is incompatible
+    with the stored JSON value; see example below
+
+    @complexity Constant.
+
+    @liveexample{The example shows several calls to `get_ref()`.,get_ref}
+
+    @since version 1.1.0
+    */
+    template<typename ReferenceType, typename std::enable_if<
+                 std::is_reference<ReferenceType>::value, int>::type = 0>
+    ReferenceType get_ref()
+    {
+        // delegate call to get_ref_impl
+        return get_ref_impl<ReferenceType>(*this);
+    }
+
+    /*!
+    @brief get a reference value (implicit)
+    @copydoc get_ref()
+    */
+    template < typename ReferenceType, typename std::enable_if <
+                   std::is_reference<ReferenceType>::value&&
+                   std::is_const<typename std::remove_reference<ReferenceType>::type>::value, int >::type = 0 >
+    ReferenceType get_ref() const
+    {
+        // delegate call to get_ref_impl
+        return get_ref_impl<ReferenceType>(*this);
+    }
+
+    /*!
+    @brief get a value (implicit)
+
+    Implicit type conversion between the JSON value and a compatible value.
+    The call is realized by calling @ref get() const.
+
+    @tparam ValueType non-pointer type compatible to the JSON value, for
+    instance `int` for JSON integer numbers, `bool` for JSON booleans, or
+    `std::vector` types for JSON arrays. The character type of @ref string_t
+    as well as an initializer list of this type is excluded to avoid
+    ambiguities as these types implicitly convert to `std::string`.
+
+    @return copy of the JSON value, converted to type @a ValueType
+
+    @throw type_error.302 in case passed type @a ValueType is incompatible
+    to the JSON value type (e.g., the JSON value is of type boolean, but a
+    string is requested); see example below
+
+    @complexity Linear in the size of the JSON value.
+
+    @liveexample{The example below shows several conversions from JSON values
+    to other types. There a few things to note: (1) Floating-point numbers can
+    be converted to integers\, (2) A JSON array can be converted to a standard
+    `std::vector<short>`\, (3) A JSON object can be converted to C++
+    associative containers such as `std::unordered_map<std::string\,
+    json>`.,operator__ValueType}
+
+    @since version 1.0.0
+    */
+    template < typename ValueType, typename std::enable_if <
+                   !std::is_pointer<ValueType>::value&&
+                   !std::is_same<ValueType, detail::json_ref<basic_json>>::value&&
+                   !std::is_same<ValueType, typename string_t::value_type>::value&&
+                   !detail::is_basic_json<ValueType>::value
+                   && !std::is_same<ValueType, std::initializer_list<typename string_t::value_type>>::value
+#if defined(JSON_HAS_CPP_17) && (defined(__GNUC__) || (defined(_MSC_VER) && _MSC_VER >= 1910 && _MSC_VER <= 1914))
+                   && !std::is_same<ValueType, typename std::string_view>::value
+#endif
+                   && detail::is_detected<detail::get_template_function, const basic_json_t&, ValueType>::value
+                   , int >::type = 0 >
+    JSON_EXPLICIT operator ValueType() const
+    {
+        // delegate the call to get<>() const
+        return get<ValueType>();
+    }
+
+    /*!
+    @return reference to the binary value
+
+    @throw type_error.302 if the value is not binary
+
+    @sa @ref is_binary() to check if the value is binary
+
+    @since version 3.8.0
+    */
+    binary_t& get_binary()
+    {
+        if (!is_binary())
+        {
+            JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(type_name())));
+        }
+
+        return *get_ptr<binary_t*>();
+    }
+
+    /// @copydoc get_binary()
+    const binary_t& get_binary() const
+    {
+        if (!is_binary())
+        {
+            JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(type_name())));
+        }
+
+        return *get_ptr<const binary_t*>();
+    }
+
+    /// @}
+
+
+    ////////////////////
+    // element access //
+    ////////////////////
+
+    /// @name element access
+    /// Access to the JSON value.
+    /// @{
+
+    /*!
+    @brief access specified array element with bounds checking
+
+    Returns a reference to the element at specified location @a idx, with
+    bounds checking.
+
+    @param[in] idx  index of the element to access
+
+    @return reference to the element at index @a idx
+
+    @throw type_error.304 if the JSON value is not an array; in this case,
+    calling `at` with an index makes no sense. See example below.
+    @throw out_of_range.401 if the index @a idx is out of range of the array;
+    that is, `idx >= size()`. See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @since version 1.0.0
+
+    @liveexample{The example below shows how array elements can be read and
+    written using `at()`. It also demonstrates the different exceptions that
+    can be thrown.,at__size_type}
+    */
+    reference at(size_type idx)
+    {
+        // at only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            JSON_TRY
+            {
+                return m_value.array->at(idx);
+            }
+            JSON_CATCH (std::out_of_range&)
+            {
+                // create better exception explanation
+                JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range"));
+            }
+        }
+        else
+        {
+            JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief access specified array element with bounds checking
+
+    Returns a const reference to the element at specified location @a idx,
+    with bounds checking.
+
+    @param[in] idx  index of the element to access
+
+    @return const reference to the element at index @a idx
+
+    @throw type_error.304 if the JSON value is not an array; in this case,
+    calling `at` with an index makes no sense. See example below.
+    @throw out_of_range.401 if the index @a idx is out of range of the array;
+    that is, `idx >= size()`. See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @since version 1.0.0
+
+    @liveexample{The example below shows how array elements can be read using
+    `at()`. It also demonstrates the different exceptions that can be thrown.,
+    at__size_type_const}
+    */
+    const_reference at(size_type idx) const
+    {
+        // at only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            JSON_TRY
+            {
+                return m_value.array->at(idx);
+            }
+            JSON_CATCH (std::out_of_range&)
+            {
+                // create better exception explanation
+                JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range"));
+            }
+        }
+        else
+        {
+            JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief access specified object element with bounds checking
+
+    Returns a reference to the element at with specified key @a key, with
+    bounds checking.
+
+    @param[in] key  key of the element to access
+
+    @return reference to the element at key @a key
+
+    @throw type_error.304 if the JSON value is not an object; in this case,
+    calling `at` with a key makes no sense. See example below.
+    @throw out_of_range.403 if the key @a key is is not stored in the object;
+    that is, `find(key) == end()`. See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Logarithmic in the size of the container.
+
+    @sa @ref operator[](const typename object_t::key_type&) for unchecked
+    access by reference
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.0.0
+
+    @liveexample{The example below shows how object elements can be read and
+    written using `at()`. It also demonstrates the different exceptions that
+    can be thrown.,at__object_t_key_type}
+    */
+    reference at(const typename object_t::key_type& key)
+    {
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            JSON_TRY
+            {
+                return m_value.object->at(key);
+            }
+            JSON_CATCH (std::out_of_range&)
+            {
+                // create better exception explanation
+                JSON_THROW(out_of_range::create(403, "key '" + key + "' not found"));
+            }
+        }
+        else
+        {
+            JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief access specified object element with bounds checking
+
+    Returns a const reference to the element at with specified key @a key,
+    with bounds checking.
+
+    @param[in] key  key of the element to access
+
+    @return const reference to the element at key @a key
+
+    @throw type_error.304 if the JSON value is not an object; in this case,
+    calling `at` with a key makes no sense. See example below.
+    @throw out_of_range.403 if the key @a key is is not stored in the object;
+    that is, `find(key) == end()`. See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Logarithmic in the size of the container.
+
+    @sa @ref operator[](const typename object_t::key_type&) for unchecked
+    access by reference
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.0.0
+
+    @liveexample{The example below shows how object elements can be read using
+    `at()`. It also demonstrates the different exceptions that can be thrown.,
+    at__object_t_key_type_const}
+    */
+    const_reference at(const typename object_t::key_type& key) const
+    {
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            JSON_TRY
+            {
+                return m_value.object->at(key);
+            }
+            JSON_CATCH (std::out_of_range&)
+            {
+                // create better exception explanation
+                JSON_THROW(out_of_range::create(403, "key '" + key + "' not found"));
+            }
+        }
+        else
+        {
+            JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief access specified array element
+
+    Returns a reference to the element at specified location @a idx.
+
+    @note If @a idx is beyond the range of the array (i.e., `idx >= size()`),
+    then the array is silently filled up with `null` values to make `idx` a
+    valid reference to the last stored element.
+
+    @param[in] idx  index of the element to access
+
+    @return reference to the element at index @a idx
+
+    @throw type_error.305 if the JSON value is not an array or null; in that
+    cases, using the [] operator with an index makes no sense.
+
+    @complexity Constant if @a idx is in the range of the array. Otherwise
+    linear in `idx - size()`.
+
+    @liveexample{The example below shows how array elements can be read and
+    written using `[]` operator. Note the addition of `null`
+    values.,operatorarray__size_type}
+
+    @since version 1.0.0
+    */
+    reference operator[](size_type idx)
+    {
+        // implicitly convert null value to an empty array
+        if (is_null())
+        {
+            m_type = value_t::array;
+            m_value.array = create<array_t>();
+            assert_invariant();
+        }
+
+        // operator[] only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            // fill up array with null values if given idx is outside range
+            if (idx >= m_value.array->size())
+            {
+                m_value.array->insert(m_value.array->end(),
+                                      idx - m_value.array->size() + 1,
+                                      basic_json());
+            }
+
+            return m_value.array->operator[](idx);
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a numeric argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief access specified array element
+
+    Returns a const reference to the element at specified location @a idx.
+
+    @param[in] idx  index of the element to access
+
+    @return const reference to the element at index @a idx
+
+    @throw type_error.305 if the JSON value is not an array; in that case,
+    using the [] operator with an index makes no sense.
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how array elements can be read using
+    the `[]` operator.,operatorarray__size_type_const}
+
+    @since version 1.0.0
+    */
+    const_reference operator[](size_type idx) const
+    {
+        // const operator[] only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            return m_value.array->operator[](idx);
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a numeric argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief access specified object element
+
+    Returns a reference to the element at with specified key @a key.
+
+    @note If @a key is not found in the object, then it is silently added to
+    the object and filled with a `null` value to make `key` a valid reference.
+    In case the value was `null` before, it is converted to an object.
+
+    @param[in] key  key of the element to access
+
+    @return reference to the element at key @a key
+
+    @throw type_error.305 if the JSON value is not an object or null; in that
+    cases, using the [] operator with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be read and
+    written using the `[]` operator.,operatorarray__key_type}
+
+    @sa @ref at(const typename object_t::key_type&) for access by reference
+    with range checking
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.0.0
+    */
+    reference operator[](const typename object_t::key_type& key)
+    {
+        // implicitly convert null value to an empty object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value.object = create<object_t>();
+            assert_invariant();
+        }
+
+        // operator[] only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            return m_value.object->operator[](key);
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief read-only access specified object element
+
+    Returns a const reference to the element at with specified key @a key. No
+    bounds checking is performed.
+
+    @warning If the element with key @a key does not exist, the behavior is
+    undefined.
+
+    @param[in] key  key of the element to access
+
+    @return const reference to the element at key @a key
+
+    @pre The element with key @a key must exist. **This precondition is
+         enforced with an assertion.**
+
+    @throw type_error.305 if the JSON value is not an object; in that case,
+    using the [] operator with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be read using
+    the `[]` operator.,operatorarray__key_type_const}
+
+    @sa @ref at(const typename object_t::key_type&) for access by reference
+    with range checking
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.0.0
+    */
+    const_reference operator[](const typename object_t::key_type& key) const
+    {
+        // const operator[] only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            JSON_ASSERT(m_value.object->find(key) != m_value.object->end());
+            return m_value.object->find(key)->second;
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief access specified object element
+
+    Returns a reference to the element at with specified key @a key.
+
+    @note If @a key is not found in the object, then it is silently added to
+    the object and filled with a `null` value to make `key` a valid reference.
+    In case the value was `null` before, it is converted to an object.
+
+    @param[in] key  key of the element to access
+
+    @return reference to the element at key @a key
+
+    @throw type_error.305 if the JSON value is not an object or null; in that
+    cases, using the [] operator with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be read and
+    written using the `[]` operator.,operatorarray__key_type}
+
+    @sa @ref at(const typename object_t::key_type&) for access by reference
+    with range checking
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.1.0
+    */
+    template<typename T>
+    JSON_HEDLEY_NON_NULL(2)
+    reference operator[](T* key)
+    {
+        // implicitly convert null to object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value = value_t::object;
+            assert_invariant();
+        }
+
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            return m_value.object->operator[](key);
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief read-only access specified object element
+
+    Returns a const reference to the element at with specified key @a key. No
+    bounds checking is performed.
+
+    @warning If the element with key @a key does not exist, the behavior is
+    undefined.
+
+    @param[in] key  key of the element to access
+
+    @return const reference to the element at key @a key
+
+    @pre The element with key @a key must exist. **This precondition is
+         enforced with an assertion.**
+
+    @throw type_error.305 if the JSON value is not an object; in that case,
+    using the [] operator with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be read using
+    the `[]` operator.,operatorarray__key_type_const}
+
+    @sa @ref at(const typename object_t::key_type&) for access by reference
+    with range checking
+    @sa @ref value() for access by value with a default value
+
+    @since version 1.1.0
+    */
+    template<typename T>
+    JSON_HEDLEY_NON_NULL(2)
+    const_reference operator[](T* key) const
+    {
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            JSON_ASSERT(m_value.object->find(key) != m_value.object->end());
+            return m_value.object->find(key)->second;
+        }
+
+        JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief access specified object element with default value
+
+    Returns either a copy of an object's element at the specified key @a key
+    or a given default value if no element with key @a key exists.
+
+    The function is basically equivalent to executing
+    @code {.cpp}
+    try {
+        return at(key);
+    } catch(out_of_range) {
+        return default_value;
+    }
+    @endcode
+
+    @note Unlike @ref at(const typename object_t::key_type&), this function
+    does not throw if the given key @a key was not found.
+
+    @note Unlike @ref operator[](const typename object_t::key_type& key), this
+    function does not implicitly add an element to the position defined by @a
+    key. This function is furthermore also applicable to const objects.
+
+    @param[in] key  key of the element to access
+    @param[in] default_value  the value to return if @a key is not found
+
+    @tparam ValueType type compatible to JSON values, for instance `int` for
+    JSON integer numbers, `bool` for JSON booleans, or `std::vector` types for
+    JSON arrays. Note the type of the expected value at @a key and the default
+    value @a default_value must be compatible.
+
+    @return copy of the element at key @a key or @a default_value if @a key
+    is not found
+
+    @throw type_error.302 if @a default_value does not match the type of the
+    value at @a key
+    @throw type_error.306 if the JSON value is not an object; in that case,
+    using `value()` with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be queried
+    with a default value.,basic_json__value}
+
+    @sa @ref at(const typename object_t::key_type&) for access by reference
+    with range checking
+    @sa @ref operator[](const typename object_t::key_type&) for unchecked
+    access by reference
+
+    @since version 1.0.0
+    */
+    // using std::is_convertible in a std::enable_if will fail when using explicit conversions
+    template < class ValueType, typename std::enable_if <
+                   detail::is_getable<basic_json_t, ValueType>::value
+                   && !std::is_same<value_t, ValueType>::value, int >::type = 0 >
+    ValueType value(const typename object_t::key_type& key, const ValueType& default_value) const
+    {
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            // if key is found, return value and given default value otherwise
+            const auto it = find(key);
+            if (it != end())
+            {
+                return it->template get<ValueType>();
+            }
+
+            return default_value;
+        }
+
+        JSON_THROW(type_error::create(306, "cannot use value() with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief overload for a default value of type const char*
+    @copydoc basic_json::value(const typename object_t::key_type&, const ValueType&) const
+    */
+    string_t value(const typename object_t::key_type& key, const char* default_value) const
+    {
+        return value(key, string_t(default_value));
+    }
+
+    /*!
+    @brief access specified object element via JSON Pointer with default value
+
+    Returns either a copy of an object's element at the specified key @a key
+    or a given default value if no element with key @a key exists.
+
+    The function is basically equivalent to executing
+    @code {.cpp}
+    try {
+        return at(ptr);
+    } catch(out_of_range) {
+        return default_value;
+    }
+    @endcode
+
+    @note Unlike @ref at(const json_pointer&), this function does not throw
+    if the given key @a key was not found.
+
+    @param[in] ptr  a JSON pointer to the element to access
+    @param[in] default_value  the value to return if @a ptr found no value
+
+    @tparam ValueType type compatible to JSON values, for instance `int` for
+    JSON integer numbers, `bool` for JSON booleans, or `std::vector` types for
+    JSON arrays. Note the type of the expected value at @a key and the default
+    value @a default_value must be compatible.
+
+    @return copy of the element at key @a key or @a default_value if @a key
+    is not found
+
+    @throw type_error.302 if @a default_value does not match the type of the
+    value at @a ptr
+    @throw type_error.306 if the JSON value is not an object; in that case,
+    using `value()` with a key makes no sense.
+
+    @complexity Logarithmic in the size of the container.
+
+    @liveexample{The example below shows how object elements can be queried
+    with a default value.,basic_json__value_ptr}
+
+    @sa @ref operator[](const json_pointer&) for unchecked access by reference
+
+    @since version 2.0.2
+    */
+    template<class ValueType, typename std::enable_if<
+                 detail::is_getable<basic_json_t, ValueType>::value, int>::type = 0>
+    ValueType value(const json_pointer& ptr, const ValueType& default_value) const
+    {
+        // at only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            // if pointer resolves a value, return it or use default value
+            JSON_TRY
+            {
+                return ptr.get_checked(this).template get<ValueType>();
+            }
+            JSON_INTERNAL_CATCH (out_of_range&)
+            {
+                return default_value;
+            }
+        }
+
+        JSON_THROW(type_error::create(306, "cannot use value() with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief overload for a default value of type const char*
+    @copydoc basic_json::value(const json_pointer&, ValueType) const
+    */
+    JSON_HEDLEY_NON_NULL(3)
+    string_t value(const json_pointer& ptr, const char* default_value) const
+    {
+        return value(ptr, string_t(default_value));
+    }
+
+    /*!
+    @brief access the first element
+
+    Returns a reference to the first element in the container. For a JSON
+    container `c`, the expression `c.front()` is equivalent to `*c.begin()`.
+
+    @return In case of a structured type (array or object), a reference to the
+    first element is returned. In case of number, string, boolean, or binary
+    values, a reference to the value is returned.
+
+    @complexity Constant.
+
+    @pre The JSON value must not be `null` (would throw `std::out_of_range`)
+    or an empty array or object (undefined behavior, **guarded by
+    assertions**).
+    @post The JSON value remains unchanged.
+
+    @throw invalid_iterator.214 when called on `null` value
+
+    @liveexample{The following code shows an example for `front()`.,front}
+
+    @sa @ref back() -- access the last element
+
+    @since version 1.0.0
+    */
+    reference front()
+    {
+        return *begin();
+    }
+
+    /*!
+    @copydoc basic_json::front()
+    */
+    const_reference front() const
+    {
+        return *cbegin();
+    }
+
+    /*!
+    @brief access the last element
+
+    Returns a reference to the last element in the container. For a JSON
+    container `c`, the expression `c.back()` is equivalent to
+    @code {.cpp}
+    auto tmp = c.end();
+    --tmp;
+    return *tmp;
+    @endcode
+
+    @return In case of a structured type (array or object), a reference to the
+    last element is returned. In case of number, string, boolean, or binary
+    values, a reference to the value is returned.
+
+    @complexity Constant.
+
+    @pre The JSON value must not be `null` (would throw `std::out_of_range`)
+    or an empty array or object (undefined behavior, **guarded by
+    assertions**).
+    @post The JSON value remains unchanged.
+
+    @throw invalid_iterator.214 when called on a `null` value. See example
+    below.
+
+    @liveexample{The following code shows an example for `back()`.,back}
+
+    @sa @ref front() -- access the first element
+
+    @since version 1.0.0
+    */
+    reference back()
+    {
+        auto tmp = end();
+        --tmp;
+        return *tmp;
+    }
+
+    /*!
+    @copydoc basic_json::back()
+    */
+    const_reference back() const
+    {
+        auto tmp = cend();
+        --tmp;
+        return *tmp;
+    }
+
+    /*!
+    @brief remove element given an iterator
+
+    Removes the element specified by iterator @a pos. The iterator @a pos must
+    be valid and dereferenceable. Thus the `end()` iterator (which is valid,
+    but is not dereferenceable) cannot be used as a value for @a pos.
+
+    If called on a primitive type other than `null`, the resulting JSON value
+    will be `null`.
+
+    @param[in] pos iterator to the element to remove
+    @return Iterator following the last removed element. If the iterator @a
+    pos refers to the last element, the `end()` iterator is returned.
+
+    @tparam IteratorType an @ref iterator or @ref const_iterator
+
+    @post Invalidates iterators and references at or after the point of the
+    erase, including the `end()` iterator.
+
+    @throw type_error.307 if called on a `null` value; example: `"cannot use
+    erase() with null"`
+    @throw invalid_iterator.202 if called on an iterator which does not belong
+    to the current JSON value; example: `"iterator does not fit current
+    value"`
+    @throw invalid_iterator.205 if called on a primitive type with invalid
+    iterator (i.e., any iterator which is not `begin()`); example: `"iterator
+    out of range"`
+
+    @complexity The complexity depends on the type:
+    - objects: amortized constant
+    - arrays: linear in distance between @a pos and the end of the container
+    - strings and binary: linear in the length of the member
+    - other types: constant
+
+    @liveexample{The example shows the result of `erase()` for different JSON
+    types.,erase__IteratorType}
+
+    @sa @ref erase(IteratorType, IteratorType) -- removes the elements in
+    the given range
+    @sa @ref erase(const typename object_t::key_type&) -- removes the element
+    from an object at the given key
+    @sa @ref erase(const size_type) -- removes the element from an array at
+    the given index
+
+    @since version 1.0.0
+    */
+    template < class IteratorType, typename std::enable_if <
+                   std::is_same<IteratorType, typename basic_json_t::iterator>::value ||
+                   std::is_same<IteratorType, typename basic_json_t::const_iterator>::value, int >::type
+               = 0 >
+    IteratorType erase(IteratorType pos)
+    {
+        // make sure iterator fits the current value
+        if (JSON_HEDLEY_UNLIKELY(this != pos.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
+        }
+
+        IteratorType result = end();
+
+        switch (m_type)
+        {
+            case value_t::boolean:
+            case value_t::number_float:
+            case value_t::number_integer:
+            case value_t::number_unsigned:
+            case value_t::string:
+            case value_t::binary:
+            {
+                if (JSON_HEDLEY_UNLIKELY(!pos.m_it.primitive_iterator.is_begin()))
+                {
+                    JSON_THROW(invalid_iterator::create(205, "iterator out of range"));
+                }
+
+                if (is_string())
+                {
+                    AllocatorType<string_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, m_value.string);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, m_value.string, 1);
+                    m_value.string = nullptr;
+                }
+                else if (is_binary())
+                {
+                    AllocatorType<binary_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, m_value.binary);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, m_value.binary, 1);
+                    m_value.binary = nullptr;
+                }
+
+                m_type = value_t::null;
+                assert_invariant();
+                break;
+            }
+
+            case value_t::object:
+            {
+                result.m_it.object_iterator = m_value.object->erase(pos.m_it.object_iterator);
+                break;
+            }
+
+            case value_t::array:
+            {
+                result.m_it.array_iterator = m_value.array->erase(pos.m_it.array_iterator);
+                break;
+            }
+
+            default:
+                JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name())));
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief remove elements given an iterator range
+
+    Removes the element specified by the range `[first; last)`. The iterator
+    @a first does not need to be dereferenceable if `first == last`: erasing
+    an empty range is a no-op.
+
+    If called on a primitive type other than `null`, the resulting JSON value
+    will be `null`.
+
+    @param[in] first iterator to the beginning of the range to remove
+    @param[in] last iterator past the end of the range to remove
+    @return Iterator following the last removed element. If the iterator @a
+    second refers to the last element, the `end()` iterator is returned.
+
+    @tparam IteratorType an @ref iterator or @ref const_iterator
+
+    @post Invalidates iterators and references at or after the point of the
+    erase, including the `end()` iterator.
+
+    @throw type_error.307 if called on a `null` value; example: `"cannot use
+    erase() with null"`
+    @throw invalid_iterator.203 if called on iterators which does not belong
+    to the current JSON value; example: `"iterators do not fit current value"`
+    @throw invalid_iterator.204 if called on a primitive type with invalid
+    iterators (i.e., if `first != begin()` and `last != end()`); example:
+    `"iterators out of range"`
+
+    @complexity The complexity depends on the type:
+    - objects: `log(size()) + std::distance(first, last)`
+    - arrays: linear in the distance between @a first and @a last, plus linear
+      in the distance between @a last and end of the container
+    - strings and binary: linear in the length of the member
+    - other types: constant
+
+    @liveexample{The example shows the result of `erase()` for different JSON
+    types.,erase__IteratorType_IteratorType}
+
+    @sa @ref erase(IteratorType) -- removes the element at a given position
+    @sa @ref erase(const typename object_t::key_type&) -- removes the element
+    from an object at the given key
+    @sa @ref erase(const size_type) -- removes the element from an array at
+    the given index
+
+    @since version 1.0.0
+    */
+    template < class IteratorType, typename std::enable_if <
+                   std::is_same<IteratorType, typename basic_json_t::iterator>::value ||
+                   std::is_same<IteratorType, typename basic_json_t::const_iterator>::value, int >::type
+               = 0 >
+    IteratorType erase(IteratorType first, IteratorType last)
+    {
+        // make sure iterator fits the current value
+        if (JSON_HEDLEY_UNLIKELY(this != first.m_object || this != last.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(203, "iterators do not fit current value"));
+        }
+
+        IteratorType result = end();
+
+        switch (m_type)
+        {
+            case value_t::boolean:
+            case value_t::number_float:
+            case value_t::number_integer:
+            case value_t::number_unsigned:
+            case value_t::string:
+            case value_t::binary:
+            {
+                if (JSON_HEDLEY_LIKELY(!first.m_it.primitive_iterator.is_begin()
+                                       || !last.m_it.primitive_iterator.is_end()))
+                {
+                    JSON_THROW(invalid_iterator::create(204, "iterators out of range"));
+                }
+
+                if (is_string())
+                {
+                    AllocatorType<string_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, m_value.string);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, m_value.string, 1);
+                    m_value.string = nullptr;
+                }
+                else if (is_binary())
+                {
+                    AllocatorType<binary_t> alloc;
+                    std::allocator_traits<decltype(alloc)>::destroy(alloc, m_value.binary);
+                    std::allocator_traits<decltype(alloc)>::deallocate(alloc, m_value.binary, 1);
+                    m_value.binary = nullptr;
+                }
+
+                m_type = value_t::null;
+                assert_invariant();
+                break;
+            }
+
+            case value_t::object:
+            {
+                result.m_it.object_iterator = m_value.object->erase(first.m_it.object_iterator,
+                                              last.m_it.object_iterator);
+                break;
+            }
+
+            case value_t::array:
+            {
+                result.m_it.array_iterator = m_value.array->erase(first.m_it.array_iterator,
+                                             last.m_it.array_iterator);
+                break;
+            }
+
+            default:
+                JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name())));
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief remove element from a JSON object given a key
+
+    Removes elements from a JSON object with the key value @a key.
+
+    @param[in] key value of the elements to remove
+
+    @return Number of elements removed. If @a ObjectType is the default
+    `std::map` type, the return value will always be `0` (@a key was not
+    found) or `1` (@a key was found).
+
+    @post References and iterators to the erased elements are invalidated.
+    Other references and iterators are not affected.
+
+    @throw type_error.307 when called on a type other than JSON object;
+    example: `"cannot use erase() with null"`
+
+    @complexity `log(size()) + count(key)`
+
+    @liveexample{The example shows the effect of `erase()`.,erase__key_type}
+
+    @sa @ref erase(IteratorType) -- removes the element at a given position
+    @sa @ref erase(IteratorType, IteratorType) -- removes the elements in
+    the given range
+    @sa @ref erase(const size_type) -- removes the element from an array at
+    the given index
+
+    @since version 1.0.0
+    */
+    size_type erase(const typename object_t::key_type& key)
+    {
+        // this erase only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            return m_value.object->erase(key);
+        }
+
+        JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief remove element from a JSON array given an index
+
+    Removes element from a JSON array at the index @a idx.
+
+    @param[in] idx index of the element to remove
+
+    @throw type_error.307 when called on a type other than JSON object;
+    example: `"cannot use erase() with null"`
+    @throw out_of_range.401 when `idx >= size()`; example: `"array index 17
+    is out of range"`
+
+    @complexity Linear in distance between @a idx and the end of the container.
+
+    @liveexample{The example shows the effect of `erase()`.,erase__size_type}
+
+    @sa @ref erase(IteratorType) -- removes the element at a given position
+    @sa @ref erase(IteratorType, IteratorType) -- removes the elements in
+    the given range
+    @sa @ref erase(const typename object_t::key_type&) -- removes the element
+    from an object at the given key
+
+    @since version 1.0.0
+    */
+    void erase(const size_type idx)
+    {
+        // this erase only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            if (JSON_HEDLEY_UNLIKELY(idx >= size()))
+            {
+                JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range"));
+            }
+
+            m_value.array->erase(m_value.array->begin() + static_cast<difference_type>(idx));
+        }
+        else
+        {
+            JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name())));
+        }
+    }
+
+    /// @}
+
+
+    ////////////
+    // lookup //
+    ////////////
+
+    /// @name lookup
+    /// @{
+
+    /*!
+    @brief find an element in a JSON object
+
+    Finds an element in a JSON object with key equivalent to @a key. If the
+    element is not found or the JSON value is not an object, end() is
+    returned.
+
+    @note This method always returns @ref end() when executed on a JSON type
+          that is not an object.
+
+    @param[in] key key value of the element to search for.
+
+    @return Iterator to an element with key equivalent to @a key. If no such
+    element is found or the JSON value is not an object, past-the-end (see
+    @ref end()) iterator is returned.
+
+    @complexity Logarithmic in the size of the JSON object.
+
+    @liveexample{The example shows how `find()` is used.,find__key_type}
+
+    @sa @ref contains(KeyT&&) const -- checks whether a key exists
+
+    @since version 1.0.0
+    */
+    template<typename KeyT>
+    iterator find(KeyT&& key)
+    {
+        auto result = end();
+
+        if (is_object())
+        {
+            result.m_it.object_iterator = m_value.object->find(std::forward<KeyT>(key));
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief find an element in a JSON object
+    @copydoc find(KeyT&&)
+    */
+    template<typename KeyT>
+    const_iterator find(KeyT&& key) const
+    {
+        auto result = cend();
+
+        if (is_object())
+        {
+            result.m_it.object_iterator = m_value.object->find(std::forward<KeyT>(key));
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief returns the number of occurrences of a key in a JSON object
+
+    Returns the number of elements with key @a key. If ObjectType is the
+    default `std::map` type, the return value will always be `0` (@a key was
+    not found) or `1` (@a key was found).
+
+    @note This method always returns `0` when executed on a JSON type that is
+          not an object.
+
+    @param[in] key key value of the element to count
+
+    @return Number of elements with key @a key. If the JSON value is not an
+    object, the return value will be `0`.
+
+    @complexity Logarithmic in the size of the JSON object.
+
+    @liveexample{The example shows how `count()` is used.,count}
+
+    @since version 1.0.0
+    */
+    template<typename KeyT>
+    size_type count(KeyT&& key) const
+    {
+        // return 0 for all nonobject types
+        return is_object() ? m_value.object->count(std::forward<KeyT>(key)) : 0;
+    }
+
+    /*!
+    @brief check the existence of an element in a JSON object
+
+    Check whether an element exists in a JSON object with key equivalent to
+    @a key. If the element is not found or the JSON value is not an object,
+    false is returned.
+
+    @note This method always returns false when executed on a JSON type
+          that is not an object.
+
+    @param[in] key key value to check its existence.
+
+    @return true if an element with specified @a key exists. If no such
+    element with such key is found or the JSON value is not an object,
+    false is returned.
+
+    @complexity Logarithmic in the size of the JSON object.
+
+    @liveexample{The following code shows an example for `contains()`.,contains}
+
+    @sa @ref find(KeyT&&) -- returns an iterator to an object element
+    @sa @ref contains(const json_pointer&) const -- checks the existence for a JSON pointer
+
+    @since version 3.6.0
+    */
+    template < typename KeyT, typename std::enable_if <
+                   !std::is_same<typename std::decay<KeyT>::type, json_pointer>::value, int >::type = 0 >
+    bool contains(KeyT && key) const
+    {
+        return is_object() && m_value.object->find(std::forward<KeyT>(key)) != m_value.object->end();
+    }
+
+    /*!
+    @brief check the existence of an element in a JSON object given a JSON pointer
+
+    Check whether the given JSON pointer @a ptr can be resolved in the current
+    JSON value.
+
+    @note This method can be executed on any JSON value type.
+
+    @param[in] ptr JSON pointer to check its existence.
+
+    @return true if the JSON pointer can be resolved to a stored value, false
+    otherwise.
+
+    @post If `j.contains(ptr)` returns true, it is safe to call `j[ptr]`.
+
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+
+    @complexity Logarithmic in the size of the JSON object.
+
+    @liveexample{The following code shows an example for `contains()`.,contains_json_pointer}
+
+    @sa @ref contains(KeyT &&) const -- checks the existence of a key
+
+    @since version 3.7.0
+    */
+    bool contains(const json_pointer& ptr) const
+    {
+        return ptr.contains(this);
+    }
+
+    /// @}
+
+
+    ///////////////
+    // iterators //
+    ///////////////
+
+    /// @name iterators
+    /// @{
+
+    /*!
+    @brief returns an iterator to the first element
+
+    Returns an iterator to the first element.
+
+    @image html range-begin-end.svg "Illustration from cppreference.com"
+
+    @return iterator to the first element
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+
+    @liveexample{The following code shows an example for `begin()`.,begin}
+
+    @sa @ref cbegin() -- returns a const iterator to the beginning
+    @sa @ref end() -- returns an iterator to the end
+    @sa @ref cend() -- returns a const iterator to the end
+
+    @since version 1.0.0
+    */
+    iterator begin() noexcept
+    {
+        iterator result(this);
+        result.set_begin();
+        return result;
+    }
+
+    /*!
+    @copydoc basic_json::cbegin()
+    */
+    const_iterator begin() const noexcept
+    {
+        return cbegin();
+    }
+
+    /*!
+    @brief returns a const iterator to the first element
+
+    Returns a const iterator to the first element.
+
+    @image html range-begin-end.svg "Illustration from cppreference.com"
+
+    @return const iterator to the first element
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `const_cast<const basic_json&>(*this).begin()`.
+
+    @liveexample{The following code shows an example for `cbegin()`.,cbegin}
+
+    @sa @ref begin() -- returns an iterator to the beginning
+    @sa @ref end() -- returns an iterator to the end
+    @sa @ref cend() -- returns a const iterator to the end
+
+    @since version 1.0.0
+    */
+    const_iterator cbegin() const noexcept
+    {
+        const_iterator result(this);
+        result.set_begin();
+        return result;
+    }
+
+    /*!
+    @brief returns an iterator to one past the last element
+
+    Returns an iterator to one past the last element.
+
+    @image html range-begin-end.svg "Illustration from cppreference.com"
+
+    @return iterator one past the last element
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+
+    @liveexample{The following code shows an example for `end()`.,end}
+
+    @sa @ref cend() -- returns a const iterator to the end
+    @sa @ref begin() -- returns an iterator to the beginning
+    @sa @ref cbegin() -- returns a const iterator to the beginning
+
+    @since version 1.0.0
+    */
+    iterator end() noexcept
+    {
+        iterator result(this);
+        result.set_end();
+        return result;
+    }
+
+    /*!
+    @copydoc basic_json::cend()
+    */
+    const_iterator end() const noexcept
+    {
+        return cend();
+    }
+
+    /*!
+    @brief returns a const iterator to one past the last element
+
+    Returns a const iterator to one past the last element.
+
+    @image html range-begin-end.svg "Illustration from cppreference.com"
+
+    @return const iterator one past the last element
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `const_cast<const basic_json&>(*this).end()`.
+
+    @liveexample{The following code shows an example for `cend()`.,cend}
+
+    @sa @ref end() -- returns an iterator to the end
+    @sa @ref begin() -- returns an iterator to the beginning
+    @sa @ref cbegin() -- returns a const iterator to the beginning
+
+    @since version 1.0.0
+    */
+    const_iterator cend() const noexcept
+    {
+        const_iterator result(this);
+        result.set_end();
+        return result;
+    }
+
+    /*!
+    @brief returns an iterator to the reverse-beginning
+
+    Returns an iterator to the reverse-beginning; that is, the last element.
+
+    @image html range-rbegin-rend.svg "Illustration from cppreference.com"
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `reverse_iterator(end())`.
+
+    @liveexample{The following code shows an example for `rbegin()`.,rbegin}
+
+    @sa @ref crbegin() -- returns a const reverse iterator to the beginning
+    @sa @ref rend() -- returns a reverse iterator to the end
+    @sa @ref crend() -- returns a const reverse iterator to the end
+
+    @since version 1.0.0
+    */
+    reverse_iterator rbegin() noexcept
+    {
+        return reverse_iterator(end());
+    }
+
+    /*!
+    @copydoc basic_json::crbegin()
+    */
+    const_reverse_iterator rbegin() const noexcept
+    {
+        return crbegin();
+    }
+
+    /*!
+    @brief returns an iterator to the reverse-end
+
+    Returns an iterator to the reverse-end; that is, one before the first
+    element.
+
+    @image html range-rbegin-rend.svg "Illustration from cppreference.com"
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `reverse_iterator(begin())`.
+
+    @liveexample{The following code shows an example for `rend()`.,rend}
+
+    @sa @ref crend() -- returns a const reverse iterator to the end
+    @sa @ref rbegin() -- returns a reverse iterator to the beginning
+    @sa @ref crbegin() -- returns a const reverse iterator to the beginning
+
+    @since version 1.0.0
+    */
+    reverse_iterator rend() noexcept
+    {
+        return reverse_iterator(begin());
+    }
+
+    /*!
+    @copydoc basic_json::crend()
+    */
+    const_reverse_iterator rend() const noexcept
+    {
+        return crend();
+    }
+
+    /*!
+    @brief returns a const reverse iterator to the last element
+
+    Returns a const iterator to the reverse-beginning; that is, the last
+    element.
+
+    @image html range-rbegin-rend.svg "Illustration from cppreference.com"
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `const_cast<const basic_json&>(*this).rbegin()`.
+
+    @liveexample{The following code shows an example for `crbegin()`.,crbegin}
+
+    @sa @ref rbegin() -- returns a reverse iterator to the beginning
+    @sa @ref rend() -- returns a reverse iterator to the end
+    @sa @ref crend() -- returns a const reverse iterator to the end
+
+    @since version 1.0.0
+    */
+    const_reverse_iterator crbegin() const noexcept
+    {
+        return const_reverse_iterator(cend());
+    }
+
+    /*!
+    @brief returns a const reverse iterator to one before the first
+
+    Returns a const reverse iterator to the reverse-end; that is, one before
+    the first element.
+
+    @image html range-rbegin-rend.svg "Illustration from cppreference.com"
+
+    @complexity Constant.
+
+    @requirement This function helps `basic_json` satisfying the
+    [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `const_cast<const basic_json&>(*this).rend()`.
+
+    @liveexample{The following code shows an example for `crend()`.,crend}
+
+    @sa @ref rend() -- returns a reverse iterator to the end
+    @sa @ref rbegin() -- returns a reverse iterator to the beginning
+    @sa @ref crbegin() -- returns a const reverse iterator to the beginning
+
+    @since version 1.0.0
+    */
+    const_reverse_iterator crend() const noexcept
+    {
+        return const_reverse_iterator(cbegin());
+    }
+
+  public:
+    /*!
+    @brief wrapper to access iterator member functions in range-based for
+
+    This function allows to access @ref iterator::key() and @ref
+    iterator::value() during range-based for loops. In these loops, a
+    reference to the JSON values is returned, so there is no access to the
+    underlying iterator.
+
+    For loop without iterator_wrapper:
+
+    @code{cpp}
+    for (auto it = j_object.begin(); it != j_object.end(); ++it)
+    {
+        std::cout << "key: " << it.key() << ", value:" << it.value() << '\n';
+    }
+    @endcode
+
+    Range-based for loop without iterator proxy:
+
+    @code{cpp}
+    for (auto it : j_object)
+    {
+        // "it" is of type json::reference and has no key() member
+        std::cout << "value: " << it << '\n';
+    }
+    @endcode
+
+    Range-based for loop with iterator proxy:
+
+    @code{cpp}
+    for (auto it : json::iterator_wrapper(j_object))
+    {
+        std::cout << "key: " << it.key() << ", value:" << it.value() << '\n';
+    }
+    @endcode
+
+    @note When iterating over an array, `key()` will return the index of the
+          element as string (see example).
+
+    @param[in] ref  reference to a JSON value
+    @return iteration proxy object wrapping @a ref with an interface to use in
+            range-based for loops
+
+    @liveexample{The following code shows how the wrapper is used,iterator_wrapper}
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @note The name of this function is not yet final and may change in the
+    future.
+
+    @deprecated This stream operator is deprecated and will be removed in
+                future 4.0.0 of the library. Please use @ref items() instead;
+                that is, replace `json::iterator_wrapper(j)` with `j.items()`.
+    */
+    JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items())
+    static iteration_proxy<iterator> iterator_wrapper(reference ref) noexcept
+    {
+        return ref.items();
+    }
+
+    /*!
+    @copydoc iterator_wrapper(reference)
+    */
+    JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items())
+    static iteration_proxy<const_iterator> iterator_wrapper(const_reference ref) noexcept
+    {
+        return ref.items();
+    }
+
+    /*!
+    @brief helper to access iterator member functions in range-based for
+
+    This function allows to access @ref iterator::key() and @ref
+    iterator::value() during range-based for loops. In these loops, a
+    reference to the JSON values is returned, so there is no access to the
+    underlying iterator.
+
+    For loop without `items()` function:
+
+    @code{cpp}
+    for (auto it = j_object.begin(); it != j_object.end(); ++it)
+    {
+        std::cout << "key: " << it.key() << ", value:" << it.value() << '\n';
+    }
+    @endcode
+
+    Range-based for loop without `items()` function:
+
+    @code{cpp}
+    for (auto it : j_object)
+    {
+        // "it" is of type json::reference and has no key() member
+        std::cout << "value: " << it << '\n';
+    }
+    @endcode
+
+    Range-based for loop with `items()` function:
+
+    @code{cpp}
+    for (auto& el : j_object.items())
+    {
+        std::cout << "key: " << el.key() << ", value:" << el.value() << '\n';
+    }
+    @endcode
+
+    The `items()` function also allows to use
+    [structured bindings](https://en.cppreference.com/w/cpp/language/structured_binding)
+    (C++17):
+
+    @code{cpp}
+    for (auto& [key, val] : j_object.items())
+    {
+        std::cout << "key: " << key << ", value:" << val << '\n';
+    }
+    @endcode
+
+    @note When iterating over an array, `key()` will return the index of the
+          element as string (see example). For primitive types (e.g., numbers),
+          `key()` returns an empty string.
+
+    @warning Using `items()` on temporary objects is dangerous. Make sure the
+             object's lifetime exeeds the iteration. See
+             <https://github.com/nlohmann/json/issues/2040> for more
+             information.
+
+    @return iteration proxy object wrapping @a ref with an interface to use in
+            range-based for loops
+
+    @liveexample{The following code shows how the function is used.,items}
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @since version 3.1.0, structured bindings support since 3.5.0.
+    */
+    iteration_proxy<iterator> items() noexcept
+    {
+        return iteration_proxy<iterator>(*this);
+    }
+
+    /*!
+    @copydoc items()
+    */
+    iteration_proxy<const_iterator> items() const noexcept
+    {
+        return iteration_proxy<const_iterator>(*this);
+    }
+
+    /// @}
+
+
+    //////////////
+    // capacity //
+    //////////////
+
+    /// @name capacity
+    /// @{
+
+    /*!
+    @brief checks whether the container is empty.
+
+    Checks if a JSON value has no elements (i.e. whether its @ref size is `0`).
+
+    @return The return value depends on the different types and is
+            defined as follows:
+            Value type  | return value
+            ----------- | -------------
+            null        | `true`
+            boolean     | `false`
+            string      | `false`
+            number      | `false`
+            binary      | `false`
+            object      | result of function `object_t::empty()`
+            array       | result of function `array_t::empty()`
+
+    @liveexample{The following code uses `empty()` to check if a JSON
+    object contains any elements.,empty}
+
+    @complexity Constant, as long as @ref array_t and @ref object_t satisfy
+    the Container concept; that is, their `empty()` functions have constant
+    complexity.
+
+    @iterators No changes.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @note This function does not return whether a string stored as JSON value
+    is empty - it returns whether the JSON container itself is empty which is
+    false in the case of a string.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `begin() == end()`.
+
+    @sa @ref size() -- returns the number of elements
+
+    @since version 1.0.0
+    */
+    bool empty() const noexcept
+    {
+        switch (m_type)
+        {
+            case value_t::null:
+            {
+                // null values are empty
+                return true;
+            }
+
+            case value_t::array:
+            {
+                // delegate call to array_t::empty()
+                return m_value.array->empty();
+            }
+
+            case value_t::object:
+            {
+                // delegate call to object_t::empty()
+                return m_value.object->empty();
+            }
+
+            default:
+            {
+                // all other types are nonempty
+                return false;
+            }
+        }
+    }
+
+    /*!
+    @brief returns the number of elements
+
+    Returns the number of elements in a JSON value.
+
+    @return The return value depends on the different types and is
+            defined as follows:
+            Value type  | return value
+            ----------- | -------------
+            null        | `0`
+            boolean     | `1`
+            string      | `1`
+            number      | `1`
+            binary      | `1`
+            object      | result of function object_t::size()
+            array       | result of function array_t::size()
+
+    @liveexample{The following code calls `size()` on the different value
+    types.,size}
+
+    @complexity Constant, as long as @ref array_t and @ref object_t satisfy
+    the Container concept; that is, their size() functions have constant
+    complexity.
+
+    @iterators No changes.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @note This function does not return the length of a string stored as JSON
+    value - it returns the number of elements in the JSON value which is 1 in
+    the case of a string.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of `std::distance(begin(), end())`.
+
+    @sa @ref empty() -- checks whether the container is empty
+    @sa @ref max_size() -- returns the maximal number of elements
+
+    @since version 1.0.0
+    */
+    size_type size() const noexcept
+    {
+        switch (m_type)
+        {
+            case value_t::null:
+            {
+                // null values are empty
+                return 0;
+            }
+
+            case value_t::array:
+            {
+                // delegate call to array_t::size()
+                return m_value.array->size();
+            }
+
+            case value_t::object:
+            {
+                // delegate call to object_t::size()
+                return m_value.object->size();
+            }
+
+            default:
+            {
+                // all other types have size 1
+                return 1;
+            }
+        }
+    }
+
+    /*!
+    @brief returns the maximum possible number of elements
+
+    Returns the maximum number of elements a JSON value is able to hold due to
+    system or library implementation limitations, i.e. `std::distance(begin(),
+    end())` for the JSON value.
+
+    @return The return value depends on the different types and is
+            defined as follows:
+            Value type  | return value
+            ----------- | -------------
+            null        | `0` (same as `size()`)
+            boolean     | `1` (same as `size()`)
+            string      | `1` (same as `size()`)
+            number      | `1` (same as `size()`)
+            binary      | `1` (same as `size()`)
+            object      | result of function `object_t::max_size()`
+            array       | result of function `array_t::max_size()`
+
+    @liveexample{The following code calls `max_size()` on the different value
+    types. Note the output is implementation specific.,max_size}
+
+    @complexity Constant, as long as @ref array_t and @ref object_t satisfy
+    the Container concept; that is, their `max_size()` functions have constant
+    complexity.
+
+    @iterators No changes.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @requirement This function helps `basic_json` satisfying the
+    [Container](https://en.cppreference.com/w/cpp/named_req/Container)
+    requirements:
+    - The complexity is constant.
+    - Has the semantics of returning `b.size()` where `b` is the largest
+      possible JSON value.
+
+    @sa @ref size() -- returns the number of elements
+
+    @since version 1.0.0
+    */
+    size_type max_size() const noexcept
+    {
+        switch (m_type)
+        {
+            case value_t::array:
+            {
+                // delegate call to array_t::max_size()
+                return m_value.array->max_size();
+            }
+
+            case value_t::object:
+            {
+                // delegate call to object_t::max_size()
+                return m_value.object->max_size();
+            }
+
+            default:
+            {
+                // all other types have max_size() == size()
+                return size();
+            }
+        }
+    }
+
+    /// @}
+
+
+    ///////////////
+    // modifiers //
+    ///////////////
+
+    /// @name modifiers
+    /// @{
+
+    /*!
+    @brief clears the contents
+
+    Clears the content of a JSON value and resets it to the default value as
+    if @ref basic_json(value_t) would have been called with the current value
+    type from @ref type():
+
+    Value type  | initial value
+    ----------- | -------------
+    null        | `null`
+    boolean     | `false`
+    string      | `""`
+    number      | `0`
+    binary      | An empty byte vector
+    object      | `{}`
+    array       | `[]`
+
+    @post Has the same effect as calling
+    @code {.cpp}
+    *this = basic_json(type());
+    @endcode
+
+    @liveexample{The example below shows the effect of `clear()` to different
+    JSON types.,clear}
+
+    @complexity Linear in the size of the JSON value.
+
+    @iterators All iterators, pointers and references related to this container
+               are invalidated.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @sa @ref basic_json(value_t) -- constructor that creates an object with the
+        same value than calling `clear()`
+
+    @since version 1.0.0
+    */
+    void clear() noexcept
+    {
+        switch (m_type)
+        {
+            case value_t::number_integer:
+            {
+                m_value.number_integer = 0;
+                break;
+            }
+
+            case value_t::number_unsigned:
+            {
+                m_value.number_unsigned = 0;
+                break;
+            }
+
+            case value_t::number_float:
+            {
+                m_value.number_float = 0.0;
+                break;
+            }
+
+            case value_t::boolean:
+            {
+                m_value.boolean = false;
+                break;
+            }
+
+            case value_t::string:
+            {
+                m_value.string->clear();
+                break;
+            }
+
+            case value_t::binary:
+            {
+                m_value.binary->clear();
+                break;
+            }
+
+            case value_t::array:
+            {
+                m_value.array->clear();
+                break;
+            }
+
+            case value_t::object:
+            {
+                m_value.object->clear();
+                break;
+            }
+
+            default:
+                break;
+        }
+    }
+
+    /*!
+    @brief add an object to an array
+
+    Appends the given element @a val to the end of the JSON value. If the
+    function is called on a JSON null value, an empty array is created before
+    appending @a val.
+
+    @param[in] val the value to add to the JSON array
+
+    @throw type_error.308 when called on a type other than JSON array or
+    null; example: `"cannot use push_back() with number"`
+
+    @complexity Amortized constant.
+
+    @liveexample{The example shows how `push_back()` and `+=` can be used to
+    add elements to a JSON array. Note how the `null` value was silently
+    converted to a JSON array.,push_back}
+
+    @since version 1.0.0
+    */
+    void push_back(basic_json&& val)
+    {
+        // push_back only works for null objects or arrays
+        if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array())))
+        {
+            JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name())));
+        }
+
+        // transform null object into an array
+        if (is_null())
+        {
+            m_type = value_t::array;
+            m_value = value_t::array;
+            assert_invariant();
+        }
+
+        // add element to array (move semantics)
+        m_value.array->push_back(std::move(val));
+        // if val is moved from, basic_json move constructor marks it null so we do not call the destructor
+    }
+
+    /*!
+    @brief add an object to an array
+    @copydoc push_back(basic_json&&)
+    */
+    reference operator+=(basic_json&& val)
+    {
+        push_back(std::move(val));
+        return *this;
+    }
+
+    /*!
+    @brief add an object to an array
+    @copydoc push_back(basic_json&&)
+    */
+    void push_back(const basic_json& val)
+    {
+        // push_back only works for null objects or arrays
+        if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array())))
+        {
+            JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name())));
+        }
+
+        // transform null object into an array
+        if (is_null())
+        {
+            m_type = value_t::array;
+            m_value = value_t::array;
+            assert_invariant();
+        }
+
+        // add element to array
+        m_value.array->push_back(val);
+    }
+
+    /*!
+    @brief add an object to an array
+    @copydoc push_back(basic_json&&)
+    */
+    reference operator+=(const basic_json& val)
+    {
+        push_back(val);
+        return *this;
+    }
+
+    /*!
+    @brief add an object to an object
+
+    Inserts the given element @a val to the JSON object. If the function is
+    called on a JSON null value, an empty object is created before inserting
+    @a val.
+
+    @param[in] val the value to add to the JSON object
+
+    @throw type_error.308 when called on a type other than JSON object or
+    null; example: `"cannot use push_back() with number"`
+
+    @complexity Logarithmic in the size of the container, O(log(`size()`)).
+
+    @liveexample{The example shows how `push_back()` and `+=` can be used to
+    add elements to a JSON object. Note how the `null` value was silently
+    converted to a JSON object.,push_back__object_t__value}
+
+    @since version 1.0.0
+    */
+    void push_back(const typename object_t::value_type& val)
+    {
+        // push_back only works for null objects or objects
+        if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object())))
+        {
+            JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name())));
+        }
+
+        // transform null object into an object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value = value_t::object;
+            assert_invariant();
+        }
+
+        // add element to array
+        m_value.object->insert(val);
+    }
+
+    /*!
+    @brief add an object to an object
+    @copydoc push_back(const typename object_t::value_type&)
+    */
+    reference operator+=(const typename object_t::value_type& val)
+    {
+        push_back(val);
+        return *this;
+    }
+
+    /*!
+    @brief add an object to an object
+
+    This function allows to use `push_back` with an initializer list. In case
+
+    1. the current value is an object,
+    2. the initializer list @a init contains only two elements, and
+    3. the first element of @a init is a string,
+
+    @a init is converted into an object element and added using
+    @ref push_back(const typename object_t::value_type&). Otherwise, @a init
+    is converted to a JSON value and added using @ref push_back(basic_json&&).
+
+    @param[in] init  an initializer list
+
+    @complexity Linear in the size of the initializer list @a init.
+
+    @note This function is required to resolve an ambiguous overload error,
+          because pairs like `{"key", "value"}` can be both interpreted as
+          `object_t::value_type` or `std::initializer_list<basic_json>`, see
+          https://github.com/nlohmann/json/issues/235 for more information.
+
+    @liveexample{The example shows how initializer lists are treated as
+    objects when possible.,push_back__initializer_list}
+    */
+    void push_back(initializer_list_t init)
+    {
+        if (is_object() && init.size() == 2 && (*init.begin())->is_string())
+        {
+            basic_json&& key = init.begin()->moved_or_copied();
+            push_back(typename object_t::value_type(
+                          std::move(key.get_ref<string_t&>()), (init.begin() + 1)->moved_or_copied()));
+        }
+        else
+        {
+            push_back(basic_json(init));
+        }
+    }
+
+    /*!
+    @brief add an object to an object
+    @copydoc push_back(initializer_list_t)
+    */
+    reference operator+=(initializer_list_t init)
+    {
+        push_back(init);
+        return *this;
+    }
+
+    /*!
+    @brief add an object to an array
+
+    Creates a JSON value from the passed parameters @a args to the end of the
+    JSON value. If the function is called on a JSON null value, an empty array
+    is created before appending the value created from @a args.
+
+    @param[in] args arguments to forward to a constructor of @ref basic_json
+    @tparam Args compatible types to create a @ref basic_json object
+
+    @return reference to the inserted element
+
+    @throw type_error.311 when called on a type other than JSON array or
+    null; example: `"cannot use emplace_back() with number"`
+
+    @complexity Amortized constant.
+
+    @liveexample{The example shows how `push_back()` can be used to add
+    elements to a JSON array. Note how the `null` value was silently converted
+    to a JSON array.,emplace_back}
+
+    @since version 2.0.8, returns reference since 3.7.0
+    */
+    template<class... Args>
+    reference emplace_back(Args&& ... args)
+    {
+        // emplace_back only works for null objects or arrays
+        if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array())))
+        {
+            JSON_THROW(type_error::create(311, "cannot use emplace_back() with " + std::string(type_name())));
+        }
+
+        // transform null object into an array
+        if (is_null())
+        {
+            m_type = value_t::array;
+            m_value = value_t::array;
+            assert_invariant();
+        }
+
+        // add element to array (perfect forwarding)
+#ifdef JSON_HAS_CPP_17
+        return m_value.array->emplace_back(std::forward<Args>(args)...);
+#else
+        m_value.array->emplace_back(std::forward<Args>(args)...);
+        return m_value.array->back();
+#endif
+    }
+
+    /*!
+    @brief add an object to an object if key does not exist
+
+    Inserts a new element into a JSON object constructed in-place with the
+    given @a args if there is no element with the key in the container. If the
+    function is called on a JSON null value, an empty object is created before
+    appending the value created from @a args.
+
+    @param[in] args arguments to forward to a constructor of @ref basic_json
+    @tparam Args compatible types to create a @ref basic_json object
+
+    @return a pair consisting of an iterator to the inserted element, or the
+            already-existing element if no insertion happened, and a bool
+            denoting whether the insertion took place.
+
+    @throw type_error.311 when called on a type other than JSON object or
+    null; example: `"cannot use emplace() with number"`
+
+    @complexity Logarithmic in the size of the container, O(log(`size()`)).
+
+    @liveexample{The example shows how `emplace()` can be used to add elements
+    to a JSON object. Note how the `null` value was silently converted to a
+    JSON object. Further note how no value is added if there was already one
+    value stored with the same key.,emplace}
+
+    @since version 2.0.8
+    */
+    template<class... Args>
+    std::pair<iterator, bool> emplace(Args&& ... args)
+    {
+        // emplace only works for null objects or arrays
+        if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object())))
+        {
+            JSON_THROW(type_error::create(311, "cannot use emplace() with " + std::string(type_name())));
+        }
+
+        // transform null object into an object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value = value_t::object;
+            assert_invariant();
+        }
+
+        // add element to array (perfect forwarding)
+        auto res = m_value.object->emplace(std::forward<Args>(args)...);
+        // create result iterator and set iterator to the result of emplace
+        auto it = begin();
+        it.m_it.object_iterator = res.first;
+
+        // return pair of iterator and boolean
+        return {it, res.second};
+    }
+
+    /// Helper for insertion of an iterator
+    /// @note: This uses std::distance to support GCC 4.8,
+    ///        see https://github.com/nlohmann/json/pull/1257
+    template<typename... Args>
+    iterator insert_iterator(const_iterator pos, Args&& ... args)
+    {
+        iterator result(this);
+        JSON_ASSERT(m_value.array != nullptr);
+
+        auto insert_pos = std::distance(m_value.array->begin(), pos.m_it.array_iterator);
+        m_value.array->insert(pos.m_it.array_iterator, std::forward<Args>(args)...);
+        result.m_it.array_iterator = m_value.array->begin() + insert_pos;
+
+        // This could have been written as:
+        // result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, cnt, val);
+        // but the return value of insert is missing in GCC 4.8, so it is written this way instead.
+
+        return result;
+    }
+
+    /*!
+    @brief inserts element
+
+    Inserts element @a val before iterator @a pos.
+
+    @param[in] pos iterator before which the content will be inserted; may be
+    the end() iterator
+    @param[in] val element to insert
+    @return iterator pointing to the inserted @a val.
+
+    @throw type_error.309 if called on JSON values other than arrays;
+    example: `"cannot use insert() with string"`
+    @throw invalid_iterator.202 if @a pos is not an iterator of *this;
+    example: `"iterator does not fit current value"`
+
+    @complexity Constant plus linear in the distance between @a pos and end of
+    the container.
+
+    @liveexample{The example shows how `insert()` is used.,insert}
+
+    @since version 1.0.0
+    */
+    iterator insert(const_iterator pos, const basic_json& val)
+    {
+        // insert only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            // check if iterator pos fits to this JSON value
+            if (JSON_HEDLEY_UNLIKELY(pos.m_object != this))
+            {
+                JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
+            }
+
+            // insert to array and return iterator
+            return insert_iterator(pos, val);
+        }
+
+        JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief inserts element
+    @copydoc insert(const_iterator, const basic_json&)
+    */
+    iterator insert(const_iterator pos, basic_json&& val)
+    {
+        return insert(pos, val);
+    }
+
+    /*!
+    @brief inserts elements
+
+    Inserts @a cnt copies of @a val before iterator @a pos.
+
+    @param[in] pos iterator before which the content will be inserted; may be
+    the end() iterator
+    @param[in] cnt number of copies of @a val to insert
+    @param[in] val element to insert
+    @return iterator pointing to the first element inserted, or @a pos if
+    `cnt==0`
+
+    @throw type_error.309 if called on JSON values other than arrays; example:
+    `"cannot use insert() with string"`
+    @throw invalid_iterator.202 if @a pos is not an iterator of *this;
+    example: `"iterator does not fit current value"`
+
+    @complexity Linear in @a cnt plus linear in the distance between @a pos
+    and end of the container.
+
+    @liveexample{The example shows how `insert()` is used.,insert__count}
+
+    @since version 1.0.0
+    */
+    iterator insert(const_iterator pos, size_type cnt, const basic_json& val)
+    {
+        // insert only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            // check if iterator pos fits to this JSON value
+            if (JSON_HEDLEY_UNLIKELY(pos.m_object != this))
+            {
+                JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
+            }
+
+            // insert to array and return iterator
+            return insert_iterator(pos, cnt, val);
+        }
+
+        JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name())));
+    }
+
+    /*!
+    @brief inserts elements
+
+    Inserts elements from range `[first, last)` before iterator @a pos.
+
+    @param[in] pos iterator before which the content will be inserted; may be
+    the end() iterator
+    @param[in] first begin of the range of elements to insert
+    @param[in] last end of the range of elements to insert
+
+    @throw type_error.309 if called on JSON values other than arrays; example:
+    `"cannot use insert() with string"`
+    @throw invalid_iterator.202 if @a pos is not an iterator of *this;
+    example: `"iterator does not fit current value"`
+    @throw invalid_iterator.210 if @a first and @a last do not belong to the
+    same JSON value; example: `"iterators do not fit"`
+    @throw invalid_iterator.211 if @a first or @a last are iterators into
+    container for which insert is called; example: `"passed iterators may not
+    belong to container"`
+
+    @return iterator pointing to the first element inserted, or @a pos if
+    `first==last`
+
+    @complexity Linear in `std::distance(first, last)` plus linear in the
+    distance between @a pos and end of the container.
+
+    @liveexample{The example shows how `insert()` is used.,insert__range}
+
+    @since version 1.0.0
+    */
+    iterator insert(const_iterator pos, const_iterator first, const_iterator last)
+    {
+        // insert only works for arrays
+        if (JSON_HEDLEY_UNLIKELY(!is_array()))
+        {
+            JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name())));
+        }
+
+        // check if iterator pos fits to this JSON value
+        if (JSON_HEDLEY_UNLIKELY(pos.m_object != this))
+        {
+            JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
+        }
+
+        // check if range iterators belong to the same JSON object
+        if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(first.m_object == this))
+        {
+            JSON_THROW(invalid_iterator::create(211, "passed iterators may not belong to container"));
+        }
+
+        // insert to array and return iterator
+        return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator);
+    }
+
+    /*!
+    @brief inserts elements
+
+    Inserts elements from initializer list @a ilist before iterator @a pos.
+
+    @param[in] pos iterator before which the content will be inserted; may be
+    the end() iterator
+    @param[in] ilist initializer list to insert the values from
+
+    @throw type_error.309 if called on JSON values other than arrays; example:
+    `"cannot use insert() with string"`
+    @throw invalid_iterator.202 if @a pos is not an iterator of *this;
+    example: `"iterator does not fit current value"`
+
+    @return iterator pointing to the first element inserted, or @a pos if
+    `ilist` is empty
+
+    @complexity Linear in `ilist.size()` plus linear in the distance between
+    @a pos and end of the container.
+
+    @liveexample{The example shows how `insert()` is used.,insert__ilist}
+
+    @since version 1.0.0
+    */
+    iterator insert(const_iterator pos, initializer_list_t ilist)
+    {
+        // insert only works for arrays
+        if (JSON_HEDLEY_UNLIKELY(!is_array()))
+        {
+            JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name())));
+        }
+
+        // check if iterator pos fits to this JSON value
+        if (JSON_HEDLEY_UNLIKELY(pos.m_object != this))
+        {
+            JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
+        }
+
+        // insert to array and return iterator
+        return insert_iterator(pos, ilist.begin(), ilist.end());
+    }
+
+    /*!
+    @brief inserts elements
+
+    Inserts elements from range `[first, last)`.
+
+    @param[in] first begin of the range of elements to insert
+    @param[in] last end of the range of elements to insert
+
+    @throw type_error.309 if called on JSON values other than objects; example:
+    `"cannot use insert() with string"`
+    @throw invalid_iterator.202 if iterator @a first or @a last does does not
+    point to an object; example: `"iterators first and last must point to
+    objects"`
+    @throw invalid_iterator.210 if @a first and @a last do not belong to the
+    same JSON value; example: `"iterators do not fit"`
+
+    @complexity Logarithmic: `O(N*log(size() + N))`, where `N` is the number
+    of elements to insert.
+
+    @liveexample{The example shows how `insert()` is used.,insert__range_object}
+
+    @since version 3.0.0
+    */
+    void insert(const_iterator first, const_iterator last)
+    {
+        // insert only works for objects
+        if (JSON_HEDLEY_UNLIKELY(!is_object()))
+        {
+            JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name())));
+        }
+
+        // check if range iterators belong to the same JSON object
+        if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
+        }
+
+        // passed iterators must belong to objects
+        if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object()))
+        {
+            JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects"));
+        }
+
+        m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator);
+    }
+
+    /*!
+    @brief updates a JSON object from another object, overwriting existing keys
+
+    Inserts all values from JSON object @a j and overwrites existing keys.
+
+    @param[in] j  JSON object to read values from
+
+    @throw type_error.312 if called on JSON values other than objects; example:
+    `"cannot use update() with string"`
+
+    @complexity O(N*log(size() + N)), where N is the number of elements to
+                insert.
+
+    @liveexample{The example shows how `update()` is used.,update}
+
+    @sa https://docs.python.org/3.6/library/stdtypes.html#dict.update
+
+    @since version 3.0.0
+    */
+    void update(const_reference j)
+    {
+        // implicitly convert null value to an empty object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value.object = create<object_t>();
+            assert_invariant();
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(!is_object()))
+        {
+            JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(type_name())));
+        }
+        if (JSON_HEDLEY_UNLIKELY(!j.is_object()))
+        {
+            JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(j.type_name())));
+        }
+
+        for (auto it = j.cbegin(); it != j.cend(); ++it)
+        {
+            m_value.object->operator[](it.key()) = it.value();
+        }
+    }
+
+    /*!
+    @brief updates a JSON object from another object, overwriting existing keys
+
+    Inserts all values from from range `[first, last)` and overwrites existing
+    keys.
+
+    @param[in] first begin of the range of elements to insert
+    @param[in] last end of the range of elements to insert
+
+    @throw type_error.312 if called on JSON values other than objects; example:
+    `"cannot use update() with string"`
+    @throw invalid_iterator.202 if iterator @a first or @a last does does not
+    point to an object; example: `"iterators first and last must point to
+    objects"`
+    @throw invalid_iterator.210 if @a first and @a last do not belong to the
+    same JSON value; example: `"iterators do not fit"`
+
+    @complexity O(N*log(size() + N)), where N is the number of elements to
+                insert.
+
+    @liveexample{The example shows how `update()` is used__range.,update}
+
+    @sa https://docs.python.org/3.6/library/stdtypes.html#dict.update
+
+    @since version 3.0.0
+    */
+    void update(const_iterator first, const_iterator last)
+    {
+        // implicitly convert null value to an empty object
+        if (is_null())
+        {
+            m_type = value_t::object;
+            m_value.object = create<object_t>();
+            assert_invariant();
+        }
+
+        if (JSON_HEDLEY_UNLIKELY(!is_object()))
+        {
+            JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(type_name())));
+        }
+
+        // check if range iterators belong to the same JSON object
+        if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
+        {
+            JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
+        }
+
+        // passed iterators must belong to objects
+        if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object()
+                                 || !last.m_object->is_object()))
+        {
+            JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects"));
+        }
+
+        for (auto it = first; it != last; ++it)
+        {
+            m_value.object->operator[](it.key()) = it.value();
+        }
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of the JSON value with those of @a other. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated.
+
+    @param[in,out] other JSON value to exchange the contents with
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how JSON values can be swapped with
+    `swap()`.,swap__reference}
+
+    @since version 1.0.0
+    */
+    void swap(reference other) noexcept (
+        std::is_nothrow_move_constructible<value_t>::value&&
+        std::is_nothrow_move_assignable<value_t>::value&&
+        std::is_nothrow_move_constructible<json_value>::value&&
+        std::is_nothrow_move_assignable<json_value>::value
+    )
+    {
+        std::swap(m_type, other.m_type);
+        std::swap(m_value, other.m_value);
+        assert_invariant();
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of the JSON value from @a left with those of @a right. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated. implemented as a friend function callable via ADL.
+
+    @param[in,out] left JSON value to exchange the contents with
+    @param[in,out] right JSON value to exchange the contents with
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how JSON values can be swapped with
+    `swap()`.,swap__reference}
+
+    @since version 1.0.0
+    */
+    friend void swap(reference left, reference right) noexcept (
+        std::is_nothrow_move_constructible<value_t>::value&&
+        std::is_nothrow_move_assignable<value_t>::value&&
+        std::is_nothrow_move_constructible<json_value>::value&&
+        std::is_nothrow_move_assignable<json_value>::value
+    )
+    {
+        left.swap(right);
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of a JSON array with those of @a other. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated.
+
+    @param[in,out] other array to exchange the contents with
+
+    @throw type_error.310 when JSON value is not an array; example: `"cannot
+    use swap() with string"`
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how arrays can be swapped with
+    `swap()`.,swap__array_t}
+
+    @since version 1.0.0
+    */
+    void swap(array_t& other)
+    {
+        // swap only works for arrays
+        if (JSON_HEDLEY_LIKELY(is_array()))
+        {
+            std::swap(*(m_value.array), other);
+        }
+        else
+        {
+            JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of a JSON object with those of @a other. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated.
+
+    @param[in,out] other object to exchange the contents with
+
+    @throw type_error.310 when JSON value is not an object; example:
+    `"cannot use swap() with string"`
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how objects can be swapped with
+    `swap()`.,swap__object_t}
+
+    @since version 1.0.0
+    */
+    void swap(object_t& other)
+    {
+        // swap only works for objects
+        if (JSON_HEDLEY_LIKELY(is_object()))
+        {
+            std::swap(*(m_value.object), other);
+        }
+        else
+        {
+            JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of a JSON string with those of @a other. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated.
+
+    @param[in,out] other string to exchange the contents with
+
+    @throw type_error.310 when JSON value is not a string; example: `"cannot
+    use swap() with boolean"`
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how strings can be swapped with
+    `swap()`.,swap__string_t}
+
+    @since version 1.0.0
+    */
+    void swap(string_t& other)
+    {
+        // swap only works for strings
+        if (JSON_HEDLEY_LIKELY(is_string()))
+        {
+            std::swap(*(m_value.string), other);
+        }
+        else
+        {
+            JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name())));
+        }
+    }
+
+    /*!
+    @brief exchanges the values
+
+    Exchanges the contents of a JSON string with those of @a other. Does not
+    invoke any move, copy, or swap operations on individual elements. All
+    iterators and references remain valid. The past-the-end iterator is
+    invalidated.
+
+    @param[in,out] other binary to exchange the contents with
+
+    @throw type_error.310 when JSON value is not a string; example: `"cannot
+    use swap() with boolean"`
+
+    @complexity Constant.
+
+    @liveexample{The example below shows how strings can be swapped with
+    `swap()`.,swap__binary_t}
+
+    @since version 3.8.0
+    */
+    void swap(binary_t& other)
+    {
+        // swap only works for strings
+        if (JSON_HEDLEY_LIKELY(is_binary()))
+        {
+            std::swap(*(m_value.binary), other);
+        }
+        else
+        {
+            JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name())));
+        }
+    }
+
+    /// @copydoc swap(binary_t)
+    void swap(typename binary_t::container_type& other)
+    {
+        // swap only works for strings
+        if (JSON_HEDLEY_LIKELY(is_binary()))
+        {
+            std::swap(*(m_value.binary), other);
+        }
+        else
+        {
+            JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name())));
+        }
+    }
+
+    /// @}
+
+  public:
+    //////////////////////////////////////////
+    // lexicographical comparison operators //
+    //////////////////////////////////////////
+
+    /// @name lexicographical comparison operators
+    /// @{
+
+    /*!
+    @brief comparison: equal
+
+    Compares two JSON values for equality according to the following rules:
+    - Two JSON values are equal if (1) they are from the same type and (2)
+      their stored values are the same according to their respective
+      `operator==`.
+    - Integer and floating-point numbers are automatically converted before
+      comparison. Note that two NaN values are always treated as unequal.
+    - Two JSON null values are equal.
+
+    @note Floating-point inside JSON values numbers are compared with
+    `json::number_float_t::operator==` which is `double::operator==` by
+    default. To compare floating-point while respecting an epsilon, an alternative
+    [comparison function](https://github.com/mariokonrad/marnav/blob/master/include/marnav/math/floatingpoint.hpp#L34-#L39)
+    could be used, for instance
+    @code {.cpp}
+    template<typename T, typename = typename std::enable_if<std::is_floating_point<T>::value, T>::type>
+    inline bool is_same(T a, T b, T epsilon = std::numeric_limits<T>::epsilon()) noexcept
+    {
+        return std::abs(a - b) <= epsilon;
+    }
+    @endcode
+    Or you can self-defined operator equal function like this:
+    @code {.cpp}
+    bool my_equal(const_reference lhs, const_reference rhs) {
+    const auto lhs_type lhs.type();
+    const auto rhs_type rhs.type();
+    if (lhs_type == rhs_type) {
+        switch(lhs_type)
+            // self_defined case
+            case value_t::number_float:
+                return std::abs(lhs - rhs) <= std::numeric_limits<float>::epsilon();
+            // other cases remain the same with the original
+            ...
+    }
+    ...
+    }
+    @endcode
+
+    @note NaN values never compare equal to themselves or to other NaN values.
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether the values @a lhs and @a rhs are equal
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @complexity Linear.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__equal}
+
+    @since version 1.0.0
+    */
+    friend bool operator==(const_reference lhs, const_reference rhs) noexcept
+    {
+        const auto lhs_type = lhs.type();
+        const auto rhs_type = rhs.type();
+
+        if (lhs_type == rhs_type)
+        {
+            switch (lhs_type)
+            {
+                case value_t::array:
+                    return *lhs.m_value.array == *rhs.m_value.array;
+
+                case value_t::object:
+                    return *lhs.m_value.object == *rhs.m_value.object;
+
+                case value_t::null:
+                    return true;
+
+                case value_t::string:
+                    return *lhs.m_value.string == *rhs.m_value.string;
+
+                case value_t::boolean:
+                    return lhs.m_value.boolean == rhs.m_value.boolean;
+
+                case value_t::number_integer:
+                    return lhs.m_value.number_integer == rhs.m_value.number_integer;
+
+                case value_t::number_unsigned:
+                    return lhs.m_value.number_unsigned == rhs.m_value.number_unsigned;
+
+                case value_t::number_float:
+                    return lhs.m_value.number_float == rhs.m_value.number_float;
+
+                case value_t::binary:
+                    return *lhs.m_value.binary == *rhs.m_value.binary;
+
+                default:
+                    return false;
+            }
+        }
+        else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float)
+        {
+            return static_cast<number_float_t>(lhs.m_value.number_integer) == rhs.m_value.number_float;
+        }
+        else if (lhs_type == value_t::number_float && rhs_type == value_t::number_integer)
+        {
+            return lhs.m_value.number_float == static_cast<number_float_t>(rhs.m_value.number_integer);
+        }
+        else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_float)
+        {
+            return static_cast<number_float_t>(lhs.m_value.number_unsigned) == rhs.m_value.number_float;
+        }
+        else if (lhs_type == value_t::number_float && rhs_type == value_t::number_unsigned)
+        {
+            return lhs.m_value.number_float == static_cast<number_float_t>(rhs.m_value.number_unsigned);
+        }
+        else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer)
+        {
+            return static_cast<number_integer_t>(lhs.m_value.number_unsigned) == rhs.m_value.number_integer;
+        }
+        else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned)
+        {
+            return lhs.m_value.number_integer == static_cast<number_integer_t>(rhs.m_value.number_unsigned);
+        }
+
+        return false;
+    }
+
+    /*!
+    @brief comparison: equal
+    @copydoc operator==(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator==(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs == basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: equal
+    @copydoc operator==(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator==(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) == rhs;
+    }
+
+    /*!
+    @brief comparison: not equal
+
+    Compares two JSON values for inequality by calculating `not (lhs == rhs)`.
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether the values @a lhs and @a rhs are not equal
+
+    @complexity Linear.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__notequal}
+
+    @since version 1.0.0
+    */
+    friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
+    {
+        return !(lhs == rhs);
+    }
+
+    /*!
+    @brief comparison: not equal
+    @copydoc operator!=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator!=(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs != basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: not equal
+    @copydoc operator!=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator!=(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) != rhs;
+    }
+
+    /*!
+    @brief comparison: less than
+
+    Compares whether one JSON value @a lhs is less than another JSON value @a
+    rhs according to the following rules:
+    - If @a lhs and @a rhs have the same type, the values are compared using
+      the default `<` operator.
+    - Integer and floating-point numbers are automatically converted before
+      comparison
+    - In case @a lhs and @a rhs have different types, the values are ignored
+      and the order of the types is considered, see
+      @ref operator<(const value_t, const value_t).
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether @a lhs is less than @a rhs
+
+    @complexity Linear.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__less}
+
+    @since version 1.0.0
+    */
+    friend bool operator<(const_reference lhs, const_reference rhs) noexcept
+    {
+        const auto lhs_type = lhs.type();
+        const auto rhs_type = rhs.type();
+
+        if (lhs_type == rhs_type)
+        {
+            switch (lhs_type)
+            {
+                case value_t::array:
+                    // note parentheses are necessary, see
+                    // https://github.com/nlohmann/json/issues/1530
+                    return (*lhs.m_value.array) < (*rhs.m_value.array);
+
+                case value_t::object:
+                    return (*lhs.m_value.object) < (*rhs.m_value.object);
+
+                case value_t::null:
+                    return false;
+
+                case value_t::string:
+                    return (*lhs.m_value.string) < (*rhs.m_value.string);
+
+                case value_t::boolean:
+                    return (lhs.m_value.boolean) < (rhs.m_value.boolean);
+
+                case value_t::number_integer:
+                    return (lhs.m_value.number_integer) < (rhs.m_value.number_integer);
+
+                case value_t::number_unsigned:
+                    return (lhs.m_value.number_unsigned) < (rhs.m_value.number_unsigned);
+
+                case value_t::number_float:
+                    return (lhs.m_value.number_float) < (rhs.m_value.number_float);
+
+                case value_t::binary:
+                    return (*lhs.m_value.binary) < (*rhs.m_value.binary);
+
+                default:
+                    return false;
+            }
+        }
+        else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float)
+        {
+            return static_cast<number_float_t>(lhs.m_value.number_integer) < rhs.m_value.number_float;
+        }
+        else if (lhs_type == value_t::number_float && rhs_type == value_t::number_integer)
+        {
+            return lhs.m_value.number_float < static_cast<number_float_t>(rhs.m_value.number_integer);
+        }
+        else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_float)
+        {
+            return static_cast<number_float_t>(lhs.m_value.number_unsigned) < rhs.m_value.number_float;
+        }
+        else if (lhs_type == value_t::number_float && rhs_type == value_t::number_unsigned)
+        {
+            return lhs.m_value.number_float < static_cast<number_float_t>(rhs.m_value.number_unsigned);
+        }
+        else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned)
+        {
+            return lhs.m_value.number_integer < static_cast<number_integer_t>(rhs.m_value.number_unsigned);
+        }
+        else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer)
+        {
+            return static_cast<number_integer_t>(lhs.m_value.number_unsigned) < rhs.m_value.number_integer;
+        }
+
+        // We only reach this line if we cannot compare values. In that case,
+        // we compare types. Note we have to call the operator explicitly,
+        // because MSVC has problems otherwise.
+        return operator<(lhs_type, rhs_type);
+    }
+
+    /*!
+    @brief comparison: less than
+    @copydoc operator<(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator<(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs < basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: less than
+    @copydoc operator<(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator<(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) < rhs;
+    }
+
+    /*!
+    @brief comparison: less than or equal
+
+    Compares whether one JSON value @a lhs is less than or equal to another
+    JSON value by calculating `not (rhs < lhs)`.
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether @a lhs is less than or equal to @a rhs
+
+    @complexity Linear.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__greater}
+
+    @since version 1.0.0
+    */
+    friend bool operator<=(const_reference lhs, const_reference rhs) noexcept
+    {
+        return !(rhs < lhs);
+    }
+
+    /*!
+    @brief comparison: less than or equal
+    @copydoc operator<=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator<=(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs <= basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: less than or equal
+    @copydoc operator<=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator<=(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) <= rhs;
+    }
+
+    /*!
+    @brief comparison: greater than
+
+    Compares whether one JSON value @a lhs is greater than another
+    JSON value by calculating `not (lhs <= rhs)`.
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether @a lhs is greater than to @a rhs
+
+    @complexity Linear.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__lessequal}
+
+    @since version 1.0.0
+    */
+    friend bool operator>(const_reference lhs, const_reference rhs) noexcept
+    {
+        return !(lhs <= rhs);
+    }
+
+    /*!
+    @brief comparison: greater than
+    @copydoc operator>(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator>(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs > basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: greater than
+    @copydoc operator>(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator>(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) > rhs;
+    }
+
+    /*!
+    @brief comparison: greater than or equal
+
+    Compares whether one JSON value @a lhs is greater than or equal to another
+    JSON value by calculating `not (lhs < rhs)`.
+
+    @param[in] lhs  first JSON value to consider
+    @param[in] rhs  second JSON value to consider
+    @return whether @a lhs is greater than or equal to @a rhs
+
+    @complexity Linear.
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @liveexample{The example demonstrates comparing several JSON
+    types.,operator__greaterequal}
+
+    @since version 1.0.0
+    */
+    friend bool operator>=(const_reference lhs, const_reference rhs) noexcept
+    {
+        return !(lhs < rhs);
+    }
+
+    /*!
+    @brief comparison: greater than or equal
+    @copydoc operator>=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator>=(const_reference lhs, const ScalarType rhs) noexcept
+    {
+        return lhs >= basic_json(rhs);
+    }
+
+    /*!
+    @brief comparison: greater than or equal
+    @copydoc operator>=(const_reference, const_reference)
+    */
+    template<typename ScalarType, typename std::enable_if<
+                 std::is_scalar<ScalarType>::value, int>::type = 0>
+    friend bool operator>=(const ScalarType lhs, const_reference rhs) noexcept
+    {
+        return basic_json(lhs) >= rhs;
+    }
+
+    /// @}
+
+    ///////////////////
+    // serialization //
+    ///////////////////
+
+    /// @name serialization
+    /// @{
+
+    /*!
+    @brief serialize to stream
+
+    Serialize the given JSON value @a j to the output stream @a o. The JSON
+    value will be serialized using the @ref dump member function.
+
+    - The indentation of the output can be controlled with the member variable
+      `width` of the output stream @a o. For instance, using the manipulator
+      `std::setw(4)` on @a o sets the indentation level to `4` and the
+      serialization result is the same as calling `dump(4)`.
+
+    - The indentation character can be controlled with the member variable
+      `fill` of the output stream @a o. For instance, the manipulator
+      `std::setfill('\\t')` sets indentation to use a tab character rather than
+      the default space character.
+
+    @param[in,out] o  stream to serialize to
+    @param[in] j  JSON value to serialize
+
+    @return the stream @a o
+
+    @throw type_error.316 if a string stored inside the JSON value is not
+                          UTF-8 encoded
+
+    @complexity Linear.
+
+    @liveexample{The example below shows the serialization with different
+    parameters to `width` to adjust the indentation level.,operator_serialize}
+
+    @since version 1.0.0; indentation character added in version 3.0.0
+    */
+    friend std::ostream& operator<<(std::ostream& o, const basic_json& j)
+    {
+        // read width member and use it as indentation parameter if nonzero
+        const bool pretty_print = o.width() > 0;
+        const auto indentation = pretty_print ? o.width() : 0;
+
+        // reset width to 0 for subsequent calls to this stream
+        o.width(0);
+
+        // do the actual serialization
+        serializer s(detail::output_adapter<char>(o), o.fill());
+        s.dump(j, pretty_print, false, static_cast<unsigned int>(indentation));
+        return o;
+    }
+
+    /*!
+    @brief serialize to stream
+    @deprecated This stream operator is deprecated and will be removed in
+                future 4.0.0 of the library. Please use
+                @ref operator<<(std::ostream&, const basic_json&)
+                instead; that is, replace calls like `j >> o;` with `o << j;`.
+    @since version 1.0.0; deprecated since version 3.0.0
+    */
+    JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&))
+    friend std::ostream& operator>>(const basic_json& j, std::ostream& o)
+    {
+        return o << j;
+    }
+
+    /// @}
+
+
+    /////////////////////
+    // deserialization //
+    /////////////////////
+
+    /// @name deserialization
+    /// @{
+
+    /*!
+    @brief deserialize from a compatible input
+
+    @tparam InputType A compatible input, for instance
+    - an std::istream object
+    - a FILE pointer
+    - a C-style array of characters
+    - a pointer to a null-terminated string of single byte characters
+    - an object obj for which begin(obj) and end(obj) produces a valid pair of
+      iterators.
+
+    @param[in] i  input to read from
+    @param[in] cb  a parser callback function of type @ref parser_callback_t
+    which is used to control the deserialization by filtering unwanted values
+    (optional)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+    @param[in] ignore_comments  whether comments should be ignored and treated
+    like whitespace (true) or yield a parse error (true); (optional, false by
+    default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.101 if a parse error occurs; example: `""unexpected end
+    of input; expected string literal""`
+    @throw parse_error.102 if to_unicode fails or surrogate error
+    @throw parse_error.103 if to_unicode fails
+
+    @complexity Linear in the length of the input. The parser is a predictive
+    LL(1) parser. The complexity can be higher if the parser callback function
+    @a cb or reading from the input @a i has a super-linear complexity.
+
+    @note A UTF-8 byte order mark is silently ignored.
+
+    @liveexample{The example below demonstrates the `parse()` function reading
+    from an array.,parse__array__parser_callback_t}
+
+    @liveexample{The example below demonstrates the `parse()` function with
+    and without callback function.,parse__string__parser_callback_t}
+
+    @liveexample{The example below demonstrates the `parse()` function with
+    and without callback function.,parse__istream__parser_callback_t}
+
+    @liveexample{The example below demonstrates the `parse()` function reading
+    from a contiguous container.,parse__contiguouscontainer__parser_callback_t}
+
+    @since version 2.0.3 (contiguous containers); version 3.9.0 allowed to
+    ignore comments.
+    */
+    template<typename InputType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json parse(InputType&& i,
+                            const parser_callback_t cb = nullptr,
+                            const bool allow_exceptions = true,
+                            const bool ignore_comments = false)
+    {
+        basic_json result;
+        parser(detail::input_adapter(std::forward<InputType>(i)), cb, allow_exceptions, ignore_comments).parse(true, result);
+        return result;
+    }
+
+    /*!
+    @brief deserialize from a pair of character iterators
+
+    The value_type of the iterator must be a integral type with size of 1, 2 or
+    4 bytes, which will be interpreted respectively as UTF-8, UTF-16 and UTF-32.
+
+    @param[in] first iterator to start of character range
+    @param[in] last  iterator to end of character range
+    @param[in] cb  a parser callback function of type @ref parser_callback_t
+    which is used to control the deserialization by filtering unwanted values
+    (optional)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+    @param[in] ignore_comments  whether comments should be ignored and treated
+    like whitespace (true) or yield a parse error (true); (optional, false by
+    default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.101 if a parse error occurs; example: `""unexpected end
+    of input; expected string literal""`
+    @throw parse_error.102 if to_unicode fails or surrogate error
+    @throw parse_error.103 if to_unicode fails
+    */
+    template<typename IteratorType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json parse(IteratorType first,
+                            IteratorType last,
+                            const parser_callback_t cb = nullptr,
+                            const bool allow_exceptions = true,
+                            const bool ignore_comments = false)
+    {
+        basic_json result;
+        parser(detail::input_adapter(std::move(first), std::move(last)), cb, allow_exceptions, ignore_comments).parse(true, result);
+        return result;
+    }
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, parse(ptr, ptr + len))
+    static basic_json parse(detail::span_input_adapter&& i,
+                            const parser_callback_t cb = nullptr,
+                            const bool allow_exceptions = true,
+                            const bool ignore_comments = false)
+    {
+        basic_json result;
+        parser(i.get(), cb, allow_exceptions, ignore_comments).parse(true, result);
+        return result;
+    }
+
+    /*!
+    @brief check if the input is valid JSON
+
+    Unlike the @ref parse(InputType&&, const parser_callback_t,const bool)
+    function, this function neither throws an exception in case of invalid JSON
+    input (i.e., a parse error) nor creates diagnostic information.
+
+    @tparam InputType A compatible input, for instance
+    - an std::istream object
+    - a FILE pointer
+    - a C-style array of characters
+    - a pointer to a null-terminated string of single byte characters
+    - an object obj for which begin(obj) and end(obj) produces a valid pair of
+      iterators.
+
+    @param[in] i input to read from
+    @param[in] ignore_comments  whether comments should be ignored and treated
+    like whitespace (true) or yield a parse error (true); (optional, false by
+    default)
+
+    @return Whether the input read from @a i is valid JSON.
+
+    @complexity Linear in the length of the input. The parser is a predictive
+    LL(1) parser.
+
+    @note A UTF-8 byte order mark is silently ignored.
+
+    @liveexample{The example below demonstrates the `accept()` function reading
+    from a string.,accept__string}
+    */
+    template<typename InputType>
+    static bool accept(InputType&& i,
+                       const bool ignore_comments = false)
+    {
+        return parser(detail::input_adapter(std::forward<InputType>(i)), nullptr, false, ignore_comments).accept(true);
+    }
+
+    template<typename IteratorType>
+    static bool accept(IteratorType first, IteratorType last,
+                       const bool ignore_comments = false)
+    {
+        return parser(detail::input_adapter(std::move(first), std::move(last)), nullptr, false, ignore_comments).accept(true);
+    }
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, accept(ptr, ptr + len))
+    static bool accept(detail::span_input_adapter&& i,
+                       const bool ignore_comments = false)
+    {
+        return parser(i.get(), nullptr, false, ignore_comments).accept(true);
+    }
+
+    /*!
+    @brief generate SAX events
+
+    The SAX event lister must follow the interface of @ref json_sax.
+
+    This function reads from a compatible input. Examples are:
+    - an std::istream object
+    - a FILE pointer
+    - a C-style array of characters
+    - a pointer to a null-terminated string of single byte characters
+    - an object obj for which begin(obj) and end(obj) produces a valid pair of
+      iterators.
+
+    @param[in] i  input to read from
+    @param[in,out] sax  SAX event listener
+    @param[in] format  the format to parse (JSON, CBOR, MessagePack, or UBJSON)
+    @param[in] strict  whether the input has to be consumed completely
+    @param[in] ignore_comments  whether comments should be ignored and treated
+    like whitespace (true) or yield a parse error (true); (optional, false by
+    default); only applies to the JSON file format.
+
+    @return return value of the last processed SAX event
+
+    @throw parse_error.101 if a parse error occurs; example: `""unexpected end
+    of input; expected string literal""`
+    @throw parse_error.102 if to_unicode fails or surrogate error
+    @throw parse_error.103 if to_unicode fails
+
+    @complexity Linear in the length of the input. The parser is a predictive
+    LL(1) parser. The complexity can be higher if the SAX consumer @a sax has
+    a super-linear complexity.
+
+    @note A UTF-8 byte order mark is silently ignored.
+
+    @liveexample{The example below demonstrates the `sax_parse()` function
+    reading from string and processing the events with a user-defined SAX
+    event consumer.,sax_parse}
+
+    @since version 3.2.0
+    */
+    template <typename InputType, typename SAX>
+    JSON_HEDLEY_NON_NULL(2)
+    static bool sax_parse(InputType&& i, SAX* sax,
+                          input_format_t format = input_format_t::json,
+                          const bool strict = true,
+                          const bool ignore_comments = false)
+    {
+        auto ia = detail::input_adapter(std::forward<InputType>(i));
+        return format == input_format_t::json
+               ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict)
+               : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia)).sax_parse(format, sax, strict);
+    }
+
+    template<class IteratorType, class SAX>
+    JSON_HEDLEY_NON_NULL(3)
+    static bool sax_parse(IteratorType first, IteratorType last, SAX* sax,
+                          input_format_t format = input_format_t::json,
+                          const bool strict = true,
+                          const bool ignore_comments = false)
+    {
+        auto ia = detail::input_adapter(std::move(first), std::move(last));
+        return format == input_format_t::json
+               ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict)
+               : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia)).sax_parse(format, sax, strict);
+    }
+
+    template <typename SAX>
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, sax_parse(ptr, ptr + len, ...))
+    JSON_HEDLEY_NON_NULL(2)
+    static bool sax_parse(detail::span_input_adapter&& i, SAX* sax,
+                          input_format_t format = input_format_t::json,
+                          const bool strict = true,
+                          const bool ignore_comments = false)
+    {
+        auto ia = i.get();
+        return format == input_format_t::json
+               ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict)
+               : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia)).sax_parse(format, sax, strict);
+    }
+
+    /*!
+    @brief deserialize from stream
+    @deprecated This stream operator is deprecated and will be removed in
+                version 4.0.0 of the library. Please use
+                @ref operator>>(std::istream&, basic_json&)
+                instead; that is, replace calls like `j << i;` with `i >> j;`.
+    @since version 1.0.0; deprecated since version 3.0.0
+    */
+    JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&))
+    friend std::istream& operator<<(basic_json& j, std::istream& i)
+    {
+        return operator>>(i, j);
+    }
+
+    /*!
+    @brief deserialize from stream
+
+    Deserializes an input stream to a JSON value.
+
+    @param[in,out] i  input stream to read a serialized JSON value from
+    @param[in,out] j  JSON value to write the deserialized input to
+
+    @throw parse_error.101 in case of an unexpected token
+    @throw parse_error.102 if to_unicode fails or surrogate error
+    @throw parse_error.103 if to_unicode fails
+
+    @complexity Linear in the length of the input. The parser is a predictive
+    LL(1) parser.
+
+    @note A UTF-8 byte order mark is silently ignored.
+
+    @liveexample{The example below shows how a JSON value is constructed by
+    reading a serialization from a stream.,operator_deserialize}
+
+    @sa parse(std::istream&, const parser_callback_t) for a variant with a
+    parser callback function to filter values while parsing
+
+    @since version 1.0.0
+    */
+    friend std::istream& operator>>(std::istream& i, basic_json& j)
+    {
+        parser(detail::input_adapter(i)).parse(false, j);
+        return i;
+    }
+
+    /// @}
+
+    ///////////////////////////
+    // convenience functions //
+    ///////////////////////////
+
+    /*!
+    @brief return the type as string
+
+    Returns the type name as string to be used in error messages - usually to
+    indicate that a function was called on a wrong JSON type.
+
+    @return a string representation of a the @a m_type member:
+            Value type  | return value
+            ----------- | -------------
+            null        | `"null"`
+            boolean     | `"boolean"`
+            string      | `"string"`
+            number      | `"number"` (for all number types)
+            object      | `"object"`
+            array       | `"array"`
+            binary      | `"binary"`
+            discarded   | `"discarded"`
+
+    @exceptionsafety No-throw guarantee: this function never throws exceptions.
+
+    @complexity Constant.
+
+    @liveexample{The following code exemplifies `type_name()` for all JSON
+    types.,type_name}
+
+    @sa @ref type() -- return the type of the JSON value
+    @sa @ref operator value_t() -- return the type of the JSON value (implicit)
+
+    @since version 1.0.0, public since 2.1.0, `const char*` and `noexcept`
+    since 3.0.0
+    */
+    JSON_HEDLEY_RETURNS_NON_NULL
+    const char* type_name() const noexcept
+    {
+        {
+            switch (m_type)
+            {
+                case value_t::null:
+                    return "null";
+                case value_t::object:
+                    return "object";
+                case value_t::array:
+                    return "array";
+                case value_t::string:
+                    return "string";
+                case value_t::boolean:
+                    return "boolean";
+                case value_t::binary:
+                    return "binary";
+                case value_t::discarded:
+                    return "discarded";
+                default:
+                    return "number";
+            }
+        }
+    }
+
+
+  private:
+    //////////////////////
+    // member variables //
+    //////////////////////
+
+    /// the type of the current element
+    value_t m_type = value_t::null;
+
+    /// the value of the current element
+    json_value m_value = {};
+
+    //////////////////////////////////////////
+    // binary serialization/deserialization //
+    //////////////////////////////////////////
+
+    /// @name binary serialization/deserialization support
+    /// @{
+
+  public:
+    /*!
+    @brief create a CBOR serialization of a given JSON value
+
+    Serializes a given JSON value @a j to a byte vector using the CBOR (Concise
+    Binary Object Representation) serialization format. CBOR is a binary
+    serialization format which aims to be more compact than JSON itself, yet
+    more efficient to parse.
+
+    The library uses the following mapping from JSON values types to
+    CBOR types according to the CBOR specification (RFC 7049):
+
+    JSON value type | value/range                                | CBOR type                          | first byte
+    --------------- | ------------------------------------------ | ---------------------------------- | ---------------
+    null            | `null`                                     | Null                               | 0xF6
+    boolean         | `true`                                     | True                               | 0xF5
+    boolean         | `false`                                    | False                              | 0xF4
+    number_integer  | -9223372036854775808..-2147483649          | Negative integer (8 bytes follow)  | 0x3B
+    number_integer  | -2147483648..-32769                        | Negative integer (4 bytes follow)  | 0x3A
+    number_integer  | -32768..-129                               | Negative integer (2 bytes follow)  | 0x39
+    number_integer  | -128..-25                                  | Negative integer (1 byte follow)   | 0x38
+    number_integer  | -24..-1                                    | Negative integer                   | 0x20..0x37
+    number_integer  | 0..23                                      | Integer                            | 0x00..0x17
+    number_integer  | 24..255                                    | Unsigned integer (1 byte follow)   | 0x18
+    number_integer  | 256..65535                                 | Unsigned integer (2 bytes follow)  | 0x19
+    number_integer  | 65536..4294967295                          | Unsigned integer (4 bytes follow)  | 0x1A
+    number_integer  | 4294967296..18446744073709551615           | Unsigned integer (8 bytes follow)  | 0x1B
+    number_unsigned | 0..23                                      | Integer                            | 0x00..0x17
+    number_unsigned | 24..255                                    | Unsigned integer (1 byte follow)   | 0x18
+    number_unsigned | 256..65535                                 | Unsigned integer (2 bytes follow)  | 0x19
+    number_unsigned | 65536..4294967295                          | Unsigned integer (4 bytes follow)  | 0x1A
+    number_unsigned | 4294967296..18446744073709551615           | Unsigned integer (8 bytes follow)  | 0x1B
+    number_float    | *any value representable by a float*       | Single-Precision Float             | 0xFA
+    number_float    | *any value NOT representable by a float*   | Double-Precision Float             | 0xFB
+    string          | *length*: 0..23                            | UTF-8 string                       | 0x60..0x77
+    string          | *length*: 23..255                          | UTF-8 string (1 byte follow)       | 0x78
+    string          | *length*: 256..65535                       | UTF-8 string (2 bytes follow)      | 0x79
+    string          | *length*: 65536..4294967295                | UTF-8 string (4 bytes follow)      | 0x7A
+    string          | *length*: 4294967296..18446744073709551615 | UTF-8 string (8 bytes follow)      | 0x7B
+    array           | *size*: 0..23                              | array                              | 0x80..0x97
+    array           | *size*: 23..255                            | array (1 byte follow)              | 0x98
+    array           | *size*: 256..65535                         | array (2 bytes follow)             | 0x99
+    array           | *size*: 65536..4294967295                  | array (4 bytes follow)             | 0x9A
+    array           | *size*: 4294967296..18446744073709551615   | array (8 bytes follow)             | 0x9B
+    object          | *size*: 0..23                              | map                                | 0xA0..0xB7
+    object          | *size*: 23..255                            | map (1 byte follow)                | 0xB8
+    object          | *size*: 256..65535                         | map (2 bytes follow)               | 0xB9
+    object          | *size*: 65536..4294967295                  | map (4 bytes follow)               | 0xBA
+    object          | *size*: 4294967296..18446744073709551615   | map (8 bytes follow)               | 0xBB
+    binary          | *size*: 0..23                              | byte string                        | 0x40..0x57
+    binary          | *size*: 23..255                            | byte string (1 byte follow)        | 0x58
+    binary          | *size*: 256..65535                         | byte string (2 bytes follow)       | 0x59
+    binary          | *size*: 65536..4294967295                  | byte string (4 bytes follow)       | 0x5A
+    binary          | *size*: 4294967296..18446744073709551615   | byte string (8 bytes follow)       | 0x5B
+
+    @note The mapping is **complete** in the sense that any JSON value type
+          can be converted to a CBOR value.
+
+    @note If NaN or Infinity are stored inside a JSON number, they are
+          serialized properly. This behavior differs from the @ref dump()
+          function which serializes NaN or Infinity to `null`.
+
+    @note The following CBOR types are not used in the conversion:
+          - UTF-8 strings terminated by "break" (0x7F)
+          - arrays terminated by "break" (0x9F)
+          - maps terminated by "break" (0xBF)
+          - byte strings terminated by "break" (0x5F)
+          - date/time (0xC0..0xC1)
+          - bignum (0xC2..0xC3)
+          - decimal fraction (0xC4)
+          - bigfloat (0xC5)
+          - expected conversions (0xD5..0xD7)
+          - simple values (0xE0..0xF3, 0xF8)
+          - undefined (0xF7)
+          - half-precision floats (0xF9)
+          - break (0xFF)
+
+    @param[in] j  JSON value to serialize
+    @return CBOR serialization as byte vector
+
+    @complexity Linear in the size of the JSON value @a j.
+
+    @liveexample{The example shows the serialization of a JSON value to a byte
+    vector in CBOR format.,to_cbor}
+
+    @sa http://cbor.io
+    @sa @ref from_cbor(detail::input_adapter&&, const bool, const bool, const cbor_tag_handler_t) for the
+        analogous deserialization
+    @sa @ref to_msgpack(const basic_json&) for the related MessagePack format
+    @sa @ref to_ubjson(const basic_json&, const bool, const bool) for the
+             related UBJSON format
+
+    @since version 2.0.9; compact representation of floating-point numbers
+           since version 3.8.0
+    */
+    static std::vector<uint8_t> to_cbor(const basic_json& j)
+    {
+        std::vector<uint8_t> result;
+        to_cbor(j, result);
+        return result;
+    }
+
+    static void to_cbor(const basic_json& j, detail::output_adapter<uint8_t> o)
+    {
+        binary_writer<uint8_t>(o).write_cbor(j);
+    }
+
+    static void to_cbor(const basic_json& j, detail::output_adapter<char> o)
+    {
+        binary_writer<char>(o).write_cbor(j);
+    }
+
+    /*!
+    @brief create a MessagePack serialization of a given JSON value
+
+    Serializes a given JSON value @a j to a byte vector using the MessagePack
+    serialization format. MessagePack is a binary serialization format which
+    aims to be more compact than JSON itself, yet more efficient to parse.
+
+    The library uses the following mapping from JSON values types to
+    MessagePack types according to the MessagePack specification:
+
+    JSON value type | value/range                       | MessagePack type | first byte
+    --------------- | --------------------------------- | ---------------- | ----------
+    null            | `null`                            | nil              | 0xC0
+    boolean         | `true`                            | true             | 0xC3
+    boolean         | `false`                           | false            | 0xC2
+    number_integer  | -9223372036854775808..-2147483649 | int64            | 0xD3
+    number_integer  | -2147483648..-32769               | int32            | 0xD2
+    number_integer  | -32768..-129                      | int16            | 0xD1
+    number_integer  | -128..-33                         | int8             | 0xD0
+    number_integer  | -32..-1                           | negative fixint  | 0xE0..0xFF
+    number_integer  | 0..127                            | positive fixint  | 0x00..0x7F
+    number_integer  | 128..255                          | uint 8           | 0xCC
+    number_integer  | 256..65535                        | uint 16          | 0xCD
+    number_integer  | 65536..4294967295                 | uint 32          | 0xCE
+    number_integer  | 4294967296..18446744073709551615  | uint 64          | 0xCF
+    number_unsigned | 0..127                            | positive fixint  | 0x00..0x7F
+    number_unsigned | 128..255                          | uint 8           | 0xCC
+    number_unsigned | 256..65535                        | uint 16          | 0xCD
+    number_unsigned | 65536..4294967295                 | uint 32          | 0xCE
+    number_unsigned | 4294967296..18446744073709551615  | uint 64          | 0xCF
+    number_float    | *any value representable by a float*     | float 32 | 0xCA
+    number_float    | *any value NOT representable by a float* | float 64 | 0xCB
+    string          | *length*: 0..31                   | fixstr           | 0xA0..0xBF
+    string          | *length*: 32..255                 | str 8            | 0xD9
+    string          | *length*: 256..65535              | str 16           | 0xDA
+    string          | *length*: 65536..4294967295       | str 32           | 0xDB
+    array           | *size*: 0..15                     | fixarray         | 0x90..0x9F
+    array           | *size*: 16..65535                 | array 16         | 0xDC
+    array           | *size*: 65536..4294967295         | array 32         | 0xDD
+    object          | *size*: 0..15                     | fix map          | 0x80..0x8F
+    object          | *size*: 16..65535                 | map 16           | 0xDE
+    object          | *size*: 65536..4294967295         | map 32           | 0xDF
+    binary          | *size*: 0..255                    | bin 8            | 0xC4
+    binary          | *size*: 256..65535                | bin 16           | 0xC5
+    binary          | *size*: 65536..4294967295         | bin 32           | 0xC6
+
+    @note The mapping is **complete** in the sense that any JSON value type
+          can be converted to a MessagePack value.
+
+    @note The following values can **not** be converted to a MessagePack value:
+          - strings with more than 4294967295 bytes
+          - byte strings with more than 4294967295 bytes
+          - arrays with more than 4294967295 elements
+          - objects with more than 4294967295 elements
+
+    @note Any MessagePack output created @ref to_msgpack can be successfully
+          parsed by @ref from_msgpack.
+
+    @note If NaN or Infinity are stored inside a JSON number, they are
+          serialized properly. This behavior differs from the @ref dump()
+          function which serializes NaN or Infinity to `null`.
+
+    @param[in] j  JSON value to serialize
+    @return MessagePack serialization as byte vector
+
+    @complexity Linear in the size of the JSON value @a j.
+
+    @liveexample{The example shows the serialization of a JSON value to a byte
+    vector in MessagePack format.,to_msgpack}
+
+    @sa http://msgpack.org
+    @sa @ref from_msgpack for the analogous deserialization
+    @sa @ref to_cbor(const basic_json& for the related CBOR format
+    @sa @ref to_ubjson(const basic_json&, const bool, const bool) for the
+             related UBJSON format
+
+    @since version 2.0.9
+    */
+    static std::vector<uint8_t> to_msgpack(const basic_json& j)
+    {
+        std::vector<uint8_t> result;
+        to_msgpack(j, result);
+        return result;
+    }
+
+    static void to_msgpack(const basic_json& j, detail::output_adapter<uint8_t> o)
+    {
+        binary_writer<uint8_t>(o).write_msgpack(j);
+    }
+
+    static void to_msgpack(const basic_json& j, detail::output_adapter<char> o)
+    {
+        binary_writer<char>(o).write_msgpack(j);
+    }
+
+    /*!
+    @brief create a UBJSON serialization of a given JSON value
+
+    Serializes a given JSON value @a j to a byte vector using the UBJSON
+    (Universal Binary JSON) serialization format. UBJSON aims to be more compact
+    than JSON itself, yet more efficient to parse.
+
+    The library uses the following mapping from JSON values types to
+    UBJSON types according to the UBJSON specification:
+
+    JSON value type | value/range                       | UBJSON type | marker
+    --------------- | --------------------------------- | ----------- | ------
+    null            | `null`                            | null        | `Z`
+    boolean         | `true`                            | true        | `T`
+    boolean         | `false`                           | false       | `F`
+    number_integer  | -9223372036854775808..-2147483649 | int64       | `L`
+    number_integer  | -2147483648..-32769               | int32       | `l`
+    number_integer  | -32768..-129                      | int16       | `I`
+    number_integer  | -128..127                         | int8        | `i`
+    number_integer  | 128..255                          | uint8       | `U`
+    number_integer  | 256..32767                        | int16       | `I`
+    number_integer  | 32768..2147483647                 | int32       | `l`
+    number_integer  | 2147483648..9223372036854775807   | int64       | `L`
+    number_unsigned | 0..127                            | int8        | `i`
+    number_unsigned | 128..255                          | uint8       | `U`
+    number_unsigned | 256..32767                        | int16       | `I`
+    number_unsigned | 32768..2147483647                 | int32       | `l`
+    number_unsigned | 2147483648..9223372036854775807   | int64       | `L`
+    number_unsigned | 2147483649..18446744073709551615  | high-precision | `H`
+    number_float    | *any value*                       | float64     | `D`
+    string          | *with shortest length indicator*  | string      | `S`
+    array           | *see notes on optimized format*   | array       | `[`
+    object          | *see notes on optimized format*   | map         | `{`
+
+    @note The mapping is **complete** in the sense that any JSON value type
+          can be converted to a UBJSON value.
+
+    @note The following values can **not** be converted to a UBJSON value:
+          - strings with more than 9223372036854775807 bytes (theoretical)
+
+    @note The following markers are not used in the conversion:
+          - `Z`: no-op values are not created.
+          - `C`: single-byte strings are serialized with `S` markers.
+
+    @note Any UBJSON output created @ref to_ubjson can be successfully parsed
+          by @ref from_ubjson.
+
+    @note If NaN or Infinity are stored inside a JSON number, they are
+          serialized properly. This behavior differs from the @ref dump()
+          function which serializes NaN or Infinity to `null`.
+
+    @note The optimized formats for containers are supported: Parameter
+          @a use_size adds size information to the beginning of a container and
+          removes the closing marker. Parameter @a use_type further checks
+          whether all elements of a container have the same type and adds the
+          type marker to the beginning of the container. The @a use_type
+          parameter must only be used together with @a use_size = true. Note
+          that @a use_size = true alone may result in larger representations -
+          the benefit of this parameter is that the receiving side is
+          immediately informed on the number of elements of the container.
+
+    @note If the JSON data contains the binary type, the value stored is a list
+          of integers, as suggested by the UBJSON documentation.  In particular,
+          this means that serialization and the deserialization of a JSON
+          containing binary values into UBJSON and back will result in a
+          different JSON object.
+
+    @param[in] j  JSON value to serialize
+    @param[in] use_size  whether to add size annotations to container types
+    @param[in] use_type  whether to add type annotations to container types
+                         (must be combined with @a use_size = true)
+    @return UBJSON serialization as byte vector
+
+    @complexity Linear in the size of the JSON value @a j.
+
+    @liveexample{The example shows the serialization of a JSON value to a byte
+    vector in UBJSON format.,to_ubjson}
+
+    @sa http://ubjson.org
+    @sa @ref from_ubjson(detail::input_adapter&&, const bool, const bool) for the
+        analogous deserialization
+    @sa @ref to_cbor(const basic_json& for the related CBOR format
+    @sa @ref to_msgpack(const basic_json&) for the related MessagePack format
+
+    @since version 3.1.0
+    */
+    static std::vector<uint8_t> to_ubjson(const basic_json& j,
+                                          const bool use_size = false,
+                                          const bool use_type = false)
+    {
+        std::vector<uint8_t> result;
+        to_ubjson(j, result, use_size, use_type);
+        return result;
+    }
+
+    static void to_ubjson(const basic_json& j, detail::output_adapter<uint8_t> o,
+                          const bool use_size = false, const bool use_type = false)
+    {
+        binary_writer<uint8_t>(o).write_ubjson(j, use_size, use_type);
+    }
+
+    static void to_ubjson(const basic_json& j, detail::output_adapter<char> o,
+                          const bool use_size = false, const bool use_type = false)
+    {
+        binary_writer<char>(o).write_ubjson(j, use_size, use_type);
+    }
+
+
+    /*!
+    @brief Serializes the given JSON object `j` to BSON and returns a vector
+           containing the corresponding BSON-representation.
+
+    BSON (Binary JSON) is a binary format in which zero or more ordered key/value pairs are
+    stored as a single entity (a so-called document).
+
+    The library uses the following mapping from JSON values types to BSON types:
+
+    JSON value type | value/range                       | BSON type   | marker
+    --------------- | --------------------------------- | ----------- | ------
+    null            | `null`                            | null        | 0x0A
+    boolean         | `true`, `false`                   | boolean     | 0x08
+    number_integer  | -9223372036854775808..-2147483649 | int64       | 0x12
+    number_integer  | -2147483648..2147483647           | int32       | 0x10
+    number_integer  | 2147483648..9223372036854775807   | int64       | 0x12
+    number_unsigned | 0..2147483647                     | int32       | 0x10
+    number_unsigned | 2147483648..9223372036854775807   | int64       | 0x12
+    number_unsigned | 9223372036854775808..18446744073709551615| --   | --
+    number_float    | *any value*                       | double      | 0x01
+    string          | *any value*                       | string      | 0x02
+    array           | *any value*                       | document    | 0x04
+    object          | *any value*                       | document    | 0x03
+    binary          | *any value*                       | binary      | 0x05
+
+    @warning The mapping is **incomplete**, since only JSON-objects (and things
+    contained therein) can be serialized to BSON.
+    Also, integers larger than 9223372036854775807 cannot be serialized to BSON,
+    and the keys may not contain U+0000, since they are serialized a
+    zero-terminated c-strings.
+
+    @throw out_of_range.407  if `j.is_number_unsigned() && j.get<std::uint64_t>() > 9223372036854775807`
+    @throw out_of_range.409  if a key in `j` contains a NULL (U+0000)
+    @throw type_error.317    if `!j.is_object()`
+
+    @pre The input `j` is required to be an object: `j.is_object() == true`.
+
+    @note Any BSON output created via @ref to_bson can be successfully parsed
+          by @ref from_bson.
+
+    @param[in] j  JSON value to serialize
+    @return BSON serialization as byte vector
+
+    @complexity Linear in the size of the JSON value @a j.
+
+    @liveexample{The example shows the serialization of a JSON value to a byte
+    vector in BSON format.,to_bson}
+
+    @sa http://bsonspec.org/spec.html
+    @sa @ref from_bson(detail::input_adapter&&, const bool strict) for the
+        analogous deserialization
+    @sa @ref to_ubjson(const basic_json&, const bool, const bool) for the
+             related UBJSON format
+    @sa @ref to_cbor(const basic_json&) for the related CBOR format
+    @sa @ref to_msgpack(const basic_json&) for the related MessagePack format
+    */
+    static std::vector<uint8_t> to_bson(const basic_json& j)
+    {
+        std::vector<uint8_t> result;
+        to_bson(j, result);
+        return result;
+    }
+
+    /*!
+    @brief Serializes the given JSON object `j` to BSON and forwards the
+           corresponding BSON-representation to the given output_adapter `o`.
+    @param j The JSON object to convert to BSON.
+    @param o The output adapter that receives the binary BSON representation.
+    @pre The input `j` shall be an object: `j.is_object() == true`
+    @sa @ref to_bson(const basic_json&)
+    */
+    static void to_bson(const basic_json& j, detail::output_adapter<uint8_t> o)
+    {
+        binary_writer<uint8_t>(o).write_bson(j);
+    }
+
+    /*!
+    @copydoc to_bson(const basic_json&, detail::output_adapter<uint8_t>)
+    */
+    static void to_bson(const basic_json& j, detail::output_adapter<char> o)
+    {
+        binary_writer<char>(o).write_bson(j);
+    }
+
+
+    /*!
+    @brief create a JSON value from an input in CBOR format
+
+    Deserializes a given input @a i to a JSON value using the CBOR (Concise
+    Binary Object Representation) serialization format.
+
+    The library maps CBOR types to JSON value types as follows:
+
+    CBOR type              | JSON value type | first byte
+    ---------------------- | --------------- | ----------
+    Integer                | number_unsigned | 0x00..0x17
+    Unsigned integer       | number_unsigned | 0x18
+    Unsigned integer       | number_unsigned | 0x19
+    Unsigned integer       | number_unsigned | 0x1A
+    Unsigned integer       | number_unsigned | 0x1B
+    Negative integer       | number_integer  | 0x20..0x37
+    Negative integer       | number_integer  | 0x38
+    Negative integer       | number_integer  | 0x39
+    Negative integer       | number_integer  | 0x3A
+    Negative integer       | number_integer  | 0x3B
+    Byte string            | binary          | 0x40..0x57
+    Byte string            | binary          | 0x58
+    Byte string            | binary          | 0x59
+    Byte string            | binary          | 0x5A
+    Byte string            | binary          | 0x5B
+    UTF-8 string           | string          | 0x60..0x77
+    UTF-8 string           | string          | 0x78
+    UTF-8 string           | string          | 0x79
+    UTF-8 string           | string          | 0x7A
+    UTF-8 string           | string          | 0x7B
+    UTF-8 string           | string          | 0x7F
+    array                  | array           | 0x80..0x97
+    array                  | array           | 0x98
+    array                  | array           | 0x99
+    array                  | array           | 0x9A
+    array                  | array           | 0x9B
+    array                  | array           | 0x9F
+    map                    | object          | 0xA0..0xB7
+    map                    | object          | 0xB8
+    map                    | object          | 0xB9
+    map                    | object          | 0xBA
+    map                    | object          | 0xBB
+    map                    | object          | 0xBF
+    False                  | `false`         | 0xF4
+    True                   | `true`          | 0xF5
+    Null                   | `null`          | 0xF6
+    Half-Precision Float   | number_float    | 0xF9
+    Single-Precision Float | number_float    | 0xFA
+    Double-Precision Float | number_float    | 0xFB
+
+    @warning The mapping is **incomplete** in the sense that not all CBOR
+             types can be converted to a JSON value. The following CBOR types
+             are not supported and will yield parse errors (parse_error.112):
+             - date/time (0xC0..0xC1)
+             - bignum (0xC2..0xC3)
+             - decimal fraction (0xC4)
+             - bigfloat (0xC5)
+             - expected conversions (0xD5..0xD7)
+             - simple values (0xE0..0xF3, 0xF8)
+             - undefined (0xF7)
+
+    @warning CBOR allows map keys of any type, whereas JSON only allows
+             strings as keys in object values. Therefore, CBOR maps with keys
+             other than UTF-8 strings are rejected (parse_error.113).
+
+    @note Any CBOR output created @ref to_cbor can be successfully parsed by
+          @ref from_cbor.
+
+    @param[in] i  an input in CBOR format convertible to an input adapter
+    @param[in] strict  whether to expect the input to be consumed until EOF
+                       (true by default)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+    @param[in] tag_handler how to treat CBOR tags (optional, error by default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.110 if the given input ends prematurely or the end of
+    file was not reached when @a strict was set to true
+    @throw parse_error.112 if unsupported features from CBOR were
+    used in the given input @a v or if the input is not valid CBOR
+    @throw parse_error.113 if a string was expected as map key, but not found
+
+    @complexity Linear in the size of the input @a i.
+
+    @liveexample{The example shows the deserialization of a byte vector in CBOR
+    format to a JSON value.,from_cbor}
+
+    @sa http://cbor.io
+    @sa @ref to_cbor(const basic_json&) for the analogous serialization
+    @sa @ref from_msgpack(detail::input_adapter&&, const bool, const bool) for the
+        related MessagePack format
+    @sa @ref from_ubjson(detail::input_adapter&&, const bool, const bool) for the
+        related UBJSON format
+
+    @since version 2.0.9; parameter @a start_index since 2.1.1; changed to
+           consume input adapters, removed start_index parameter, and added
+           @a strict parameter since 3.0.0; added @a allow_exceptions parameter
+           since 3.2.0; added @a tag_handler parameter since 3.9.0.
+    */
+    template<typename InputType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_cbor(InputType&& i,
+                                const bool strict = true,
+                                const bool allow_exceptions = true,
+                                const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::forward<InputType>(i));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    /*!
+    @copydoc from_cbor(detail::input_adapter&&, const bool, const bool, const cbor_tag_handler_t)
+    */
+    template<typename IteratorType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_cbor(IteratorType first, IteratorType last,
+                                const bool strict = true,
+                                const bool allow_exceptions = true,
+                                const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::move(first), std::move(last));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    template<typename T>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len))
+    static basic_json from_cbor(const T* ptr, std::size_t len,
+                                const bool strict = true,
+                                const bool allow_exceptions = true,
+                                const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
+    {
+        return from_cbor(ptr, ptr + len, strict, allow_exceptions, tag_handler);
+    }
+
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len))
+    static basic_json from_cbor(detail::span_input_adapter&& i,
+                                const bool strict = true,
+                                const bool allow_exceptions = true,
+                                const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = i.get();
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    /*!
+    @brief create a JSON value from an input in MessagePack format
+
+    Deserializes a given input @a i to a JSON value using the MessagePack
+    serialization format.
+
+    The library maps MessagePack types to JSON value types as follows:
+
+    MessagePack type | JSON value type | first byte
+    ---------------- | --------------- | ----------
+    positive fixint  | number_unsigned | 0x00..0x7F
+    fixmap           | object          | 0x80..0x8F
+    fixarray         | array           | 0x90..0x9F
+    fixstr           | string          | 0xA0..0xBF
+    nil              | `null`          | 0xC0
+    false            | `false`         | 0xC2
+    true             | `true`          | 0xC3
+    float 32         | number_float    | 0xCA
+    float 64         | number_float    | 0xCB
+    uint 8           | number_unsigned | 0xCC
+    uint 16          | number_unsigned | 0xCD
+    uint 32          | number_unsigned | 0xCE
+    uint 64          | number_unsigned | 0xCF
+    int 8            | number_integer  | 0xD0
+    int 16           | number_integer  | 0xD1
+    int 32           | number_integer  | 0xD2
+    int 64           | number_integer  | 0xD3
+    str 8            | string          | 0xD9
+    str 16           | string          | 0xDA
+    str 32           | string          | 0xDB
+    array 16         | array           | 0xDC
+    array 32         | array           | 0xDD
+    map 16           | object          | 0xDE
+    map 32           | object          | 0xDF
+    bin 8            | binary          | 0xC4
+    bin 16           | binary          | 0xC5
+    bin 32           | binary          | 0xC6
+    ext 8            | binary          | 0xC7
+    ext 16           | binary          | 0xC8
+    ext 32           | binary          | 0xC9
+    fixext 1         | binary          | 0xD4
+    fixext 2         | binary          | 0xD5
+    fixext 4         | binary          | 0xD6
+    fixext 8         | binary          | 0xD7
+    fixext 16        | binary          | 0xD8
+    negative fixint  | number_integer  | 0xE0-0xFF
+
+    @note Any MessagePack output created @ref to_msgpack can be successfully
+          parsed by @ref from_msgpack.
+
+    @param[in] i  an input in MessagePack format convertible to an input
+                  adapter
+    @param[in] strict  whether to expect the input to be consumed until EOF
+                       (true by default)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.110 if the given input ends prematurely or the end of
+    file was not reached when @a strict was set to true
+    @throw parse_error.112 if unsupported features from MessagePack were
+    used in the given input @a i or if the input is not valid MessagePack
+    @throw parse_error.113 if a string was expected as map key, but not found
+
+    @complexity Linear in the size of the input @a i.
+
+    @liveexample{The example shows the deserialization of a byte vector in
+    MessagePack format to a JSON value.,from_msgpack}
+
+    @sa http://msgpack.org
+    @sa @ref to_msgpack(const basic_json&) for the analogous serialization
+    @sa @ref from_cbor(detail::input_adapter&&, const bool, const bool, const cbor_tag_handler_t) for the
+        related CBOR format
+    @sa @ref from_ubjson(detail::input_adapter&&, const bool, const bool) for
+        the related UBJSON format
+    @sa @ref from_bson(detail::input_adapter&&, const bool, const bool) for
+        the related BSON format
+
+    @since version 2.0.9; parameter @a start_index since 2.1.1; changed to
+           consume input adapters, removed start_index parameter, and added
+           @a strict parameter since 3.0.0; added @a allow_exceptions parameter
+           since 3.2.0
+    */
+    template<typename InputType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_msgpack(InputType&& i,
+                                   const bool strict = true,
+                                   const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::forward<InputType>(i));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    /*!
+    @copydoc from_msgpack(detail::input_adapter&&, const bool, const bool)
+    */
+    template<typename IteratorType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_msgpack(IteratorType first, IteratorType last,
+                                   const bool strict = true,
+                                   const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::move(first), std::move(last));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+
+    template<typename T>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len))
+    static basic_json from_msgpack(const T* ptr, std::size_t len,
+                                   const bool strict = true,
+                                   const bool allow_exceptions = true)
+    {
+        return from_msgpack(ptr, ptr + len, strict, allow_exceptions);
+    }
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len))
+    static basic_json from_msgpack(detail::span_input_adapter&& i,
+                                   const bool strict = true,
+                                   const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = i.get();
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+
+    /*!
+    @brief create a JSON value from an input in UBJSON format
+
+    Deserializes a given input @a i to a JSON value using the UBJSON (Universal
+    Binary JSON) serialization format.
+
+    The library maps UBJSON types to JSON value types as follows:
+
+    UBJSON type | JSON value type                         | marker
+    ----------- | --------------------------------------- | ------
+    no-op       | *no value, next value is read*          | `N`
+    null        | `null`                                  | `Z`
+    false       | `false`                                 | `F`
+    true        | `true`                                  | `T`
+    float32     | number_float                            | `d`
+    float64     | number_float                            | `D`
+    uint8       | number_unsigned                         | `U`
+    int8        | number_integer                          | `i`
+    int16       | number_integer                          | `I`
+    int32       | number_integer                          | `l`
+    int64       | number_integer                          | `L`
+    high-precision number | number_integer, number_unsigned, or number_float - depends on number string | 'H'
+    string      | string                                  | `S`
+    char        | string                                  | `C`
+    array       | array (optimized values are supported)  | `[`
+    object      | object (optimized values are supported) | `{`
+
+    @note The mapping is **complete** in the sense that any UBJSON value can
+          be converted to a JSON value.
+
+    @param[in] i  an input in UBJSON format convertible to an input adapter
+    @param[in] strict  whether to expect the input to be consumed until EOF
+                       (true by default)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.110 if the given input ends prematurely or the end of
+    file was not reached when @a strict was set to true
+    @throw parse_error.112 if a parse error occurs
+    @throw parse_error.113 if a string could not be parsed successfully
+
+    @complexity Linear in the size of the input @a i.
+
+    @liveexample{The example shows the deserialization of a byte vector in
+    UBJSON format to a JSON value.,from_ubjson}
+
+    @sa http://ubjson.org
+    @sa @ref to_ubjson(const basic_json&, const bool, const bool) for the
+             analogous serialization
+    @sa @ref from_cbor(detail::input_adapter&&, const bool, const bool, const cbor_tag_handler_t) for the
+        related CBOR format
+    @sa @ref from_msgpack(detail::input_adapter&&, const bool, const bool) for
+        the related MessagePack format
+    @sa @ref from_bson(detail::input_adapter&&, const bool, const bool) for
+        the related BSON format
+
+    @since version 3.1.0; added @a allow_exceptions parameter since 3.2.0
+    */
+    template<typename InputType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_ubjson(InputType&& i,
+                                  const bool strict = true,
+                                  const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::forward<InputType>(i));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    /*!
+    @copydoc from_ubjson(detail::input_adapter&&, const bool, const bool)
+    */
+    template<typename IteratorType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_ubjson(IteratorType first, IteratorType last,
+                                  const bool strict = true,
+                                  const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::move(first), std::move(last));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    template<typename T>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len))
+    static basic_json from_ubjson(const T* ptr, std::size_t len,
+                                  const bool strict = true,
+                                  const bool allow_exceptions = true)
+    {
+        return from_ubjson(ptr, ptr + len, strict, allow_exceptions);
+    }
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len))
+    static basic_json from_ubjson(detail::span_input_adapter&& i,
+                                  const bool strict = true,
+                                  const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = i.get();
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+
+    /*!
+    @brief Create a JSON value from an input in BSON format
+
+    Deserializes a given input @a i to a JSON value using the BSON (Binary JSON)
+    serialization format.
+
+    The library maps BSON record types to JSON value types as follows:
+
+    BSON type       | BSON marker byte | JSON value type
+    --------------- | ---------------- | ---------------------------
+    double          | 0x01             | number_float
+    string          | 0x02             | string
+    document        | 0x03             | object
+    array           | 0x04             | array
+    binary          | 0x05             | still unsupported
+    undefined       | 0x06             | still unsupported
+    ObjectId        | 0x07             | still unsupported
+    boolean         | 0x08             | boolean
+    UTC Date-Time   | 0x09             | still unsupported
+    null            | 0x0A             | null
+    Regular Expr.   | 0x0B             | still unsupported
+    DB Pointer      | 0x0C             | still unsupported
+    JavaScript Code | 0x0D             | still unsupported
+    Symbol          | 0x0E             | still unsupported
+    JavaScript Code | 0x0F             | still unsupported
+    int32           | 0x10             | number_integer
+    Timestamp       | 0x11             | still unsupported
+    128-bit decimal float | 0x13       | still unsupported
+    Max Key         | 0x7F             | still unsupported
+    Min Key         | 0xFF             | still unsupported
+
+    @warning The mapping is **incomplete**. The unsupported mappings
+             are indicated in the table above.
+
+    @param[in] i  an input in BSON format convertible to an input adapter
+    @param[in] strict  whether to expect the input to be consumed until EOF
+                       (true by default)
+    @param[in] allow_exceptions  whether to throw exceptions in case of a
+    parse error (optional, true by default)
+
+    @return deserialized JSON value; in case of a parse error and
+            @a allow_exceptions set to `false`, the return value will be
+            value_t::discarded.
+
+    @throw parse_error.114 if an unsupported BSON record type is encountered
+
+    @complexity Linear in the size of the input @a i.
+
+    @liveexample{The example shows the deserialization of a byte vector in
+    BSON format to a JSON value.,from_bson}
+
+    @sa http://bsonspec.org/spec.html
+    @sa @ref to_bson(const basic_json&) for the analogous serialization
+    @sa @ref from_cbor(detail::input_adapter&&, const bool, const bool, const cbor_tag_handler_t) for the
+        related CBOR format
+    @sa @ref from_msgpack(detail::input_adapter&&, const bool, const bool) for
+        the related MessagePack format
+    @sa @ref from_ubjson(detail::input_adapter&&, const bool, const bool) for the
+        related UBJSON format
+    */
+    template<typename InputType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_bson(InputType&& i,
+                                const bool strict = true,
+                                const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::forward<InputType>(i));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    /*!
+    @copydoc from_bson(detail::input_adapter&&, const bool, const bool)
+    */
+    template<typename IteratorType>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json from_bson(IteratorType first, IteratorType last,
+                                const bool strict = true,
+                                const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = detail::input_adapter(std::move(first), std::move(last));
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+
+    template<typename T>
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len))
+    static basic_json from_bson(const T* ptr, std::size_t len,
+                                const bool strict = true,
+                                const bool allow_exceptions = true)
+    {
+        return from_bson(ptr, ptr + len, strict, allow_exceptions);
+    }
+
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len))
+    static basic_json from_bson(detail::span_input_adapter&& i,
+                                const bool strict = true,
+                                const bool allow_exceptions = true)
+    {
+        basic_json result;
+        detail::json_sax_dom_parser<basic_json> sdp(result, allow_exceptions);
+        auto ia = i.get();
+        const bool res = binary_reader<decltype(ia)>(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict);
+        return res ? result : basic_json(value_t::discarded);
+    }
+    /// @}
+
+    //////////////////////////
+    // JSON Pointer support //
+    //////////////////////////
+
+    /// @name JSON Pointer functions
+    /// @{
+
+    /*!
+    @brief access specified element via JSON Pointer
+
+    Uses a JSON pointer to retrieve a reference to the respective JSON value.
+    No bound checking is performed. Similar to @ref operator[](const typename
+    object_t::key_type&), `null` values are created in arrays and objects if
+    necessary.
+
+    In particular:
+    - If the JSON pointer points to an object key that does not exist, it
+      is created an filled with a `null` value before a reference to it
+      is returned.
+    - If the JSON pointer points to an array index that does not exist, it
+      is created an filled with a `null` value before a reference to it
+      is returned. All indices between the current maximum and the given
+      index are also filled with `null`.
+    - The special value `-` is treated as a synonym for the index past the
+      end.
+
+    @param[in] ptr  a JSON pointer
+
+    @return reference to the element pointed to by @a ptr
+
+    @complexity Constant.
+
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+
+    @liveexample{The behavior is shown in the example.,operatorjson_pointer}
+
+    @since version 2.0.0
+    */
+    reference operator[](const json_pointer& ptr)
+    {
+        return ptr.get_unchecked(this);
+    }
+
+    /*!
+    @brief access specified element via JSON Pointer
+
+    Uses a JSON pointer to retrieve a reference to the respective JSON value.
+    No bound checking is performed. The function does not change the JSON
+    value; no `null` values are created. In particular, the special value
+    `-` yields an exception.
+
+    @param[in] ptr  JSON pointer to the desired element
+
+    @return const reference to the element pointed to by @a ptr
+
+    @complexity Constant.
+
+    @throw parse_error.106   if an array index begins with '0'
+    @throw parse_error.109   if an array index was not a number
+    @throw out_of_range.402  if the array index '-' is used
+    @throw out_of_range.404  if the JSON pointer can not be resolved
+
+    @liveexample{The behavior is shown in the example.,operatorjson_pointer_const}
+
+    @since version 2.0.0
+    */
+    const_reference operator[](const json_pointer& ptr) const
+    {
+        return ptr.get_unchecked(this);
+    }
+
+    /*!
+    @brief access specified element via JSON Pointer
+
+    Returns a reference to the element at with specified JSON pointer @a ptr,
+    with bounds checking.
+
+    @param[in] ptr  JSON pointer to the desired element
+
+    @return reference to the element pointed to by @a ptr
+
+    @throw parse_error.106 if an array index in the passed JSON pointer @a ptr
+    begins with '0'. See example below.
+
+    @throw parse_error.109 if an array index in the passed JSON pointer @a ptr
+    is not a number. See example below.
+
+    @throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
+    is out of range. See example below.
+
+    @throw out_of_range.402 if the array index '-' is used in the passed JSON
+    pointer @a ptr. As `at` provides checked access (and no elements are
+    implicitly inserted), the index '-' is always invalid. See example below.
+
+    @throw out_of_range.403 if the JSON pointer describes a key of an object
+    which cannot be found. See example below.
+
+    @throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
+    See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @since version 2.0.0
+
+    @liveexample{The behavior is shown in the example.,at_json_pointer}
+    */
+    reference at(const json_pointer& ptr)
+    {
+        return ptr.get_checked(this);
+    }
+
+    /*!
+    @brief access specified element via JSON Pointer
+
+    Returns a const reference to the element at with specified JSON pointer @a
+    ptr, with bounds checking.
+
+    @param[in] ptr  JSON pointer to the desired element
+
+    @return reference to the element pointed to by @a ptr
+
+    @throw parse_error.106 if an array index in the passed JSON pointer @a ptr
+    begins with '0'. See example below.
+
+    @throw parse_error.109 if an array index in the passed JSON pointer @a ptr
+    is not a number. See example below.
+
+    @throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
+    is out of range. See example below.
+
+    @throw out_of_range.402 if the array index '-' is used in the passed JSON
+    pointer @a ptr. As `at` provides checked access (and no elements are
+    implicitly inserted), the index '-' is always invalid. See example below.
+
+    @throw out_of_range.403 if the JSON pointer describes a key of an object
+    which cannot be found. See example below.
+
+    @throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
+    See example below.
+
+    @exceptionsafety Strong guarantee: if an exception is thrown, there are no
+    changes in the JSON value.
+
+    @complexity Constant.
+
+    @since version 2.0.0
+
+    @liveexample{The behavior is shown in the example.,at_json_pointer_const}
+    */
+    const_reference at(const json_pointer& ptr) const
+    {
+        return ptr.get_checked(this);
+    }
+
+    /*!
+    @brief return flattened JSON value
+
+    The function creates a JSON object whose keys are JSON pointers (see [RFC
+    6901](https://tools.ietf.org/html/rfc6901)) and whose values are all
+    primitive. The original JSON value can be restored using the @ref
+    unflatten() function.
+
+    @return an object that maps JSON pointers to primitive values
+
+    @note Empty objects and arrays are flattened to `null` and will not be
+          reconstructed correctly by the @ref unflatten() function.
+
+    @complexity Linear in the size the JSON value.
+
+    @liveexample{The following code shows how a JSON object is flattened to an
+    object whose keys consist of JSON pointers.,flatten}
+
+    @sa @ref unflatten() for the reverse function
+
+    @since version 2.0.0
+    */
+    basic_json flatten() const
+    {
+        basic_json result(value_t::object);
+        json_pointer::flatten("", *this, result);
+        return result;
+    }
+
+    /*!
+    @brief unflatten a previously flattened JSON value
+
+    The function restores the arbitrary nesting of a JSON value that has been
+    flattened before using the @ref flatten() function. The JSON value must
+    meet certain constraints:
+    1. The value must be an object.
+    2. The keys must be JSON pointers (see
+       [RFC 6901](https://tools.ietf.org/html/rfc6901))
+    3. The mapped values must be primitive JSON types.
+
+    @return the original JSON from a flattened version
+
+    @note Empty objects and arrays are flattened by @ref flatten() to `null`
+          values and can not unflattened to their original type. Apart from
+          this example, for a JSON value `j`, the following is always true:
+          `j == j.flatten().unflatten()`.
+
+    @complexity Linear in the size the JSON value.
+
+    @throw type_error.314  if value is not an object
+    @throw type_error.315  if object values are not primitive
+
+    @liveexample{The following code shows how a flattened JSON object is
+    unflattened into the original nested JSON object.,unflatten}
+
+    @sa @ref flatten() for the reverse function
+
+    @since version 2.0.0
+    */
+    basic_json unflatten() const
+    {
+        return json_pointer::unflatten(*this);
+    }
+
+    /// @}
+
+    //////////////////////////
+    // JSON Patch functions //
+    //////////////////////////
+
+    /// @name JSON Patch functions
+    /// @{
+
+    /*!
+    @brief applies a JSON patch
+
+    [JSON Patch](http://jsonpatch.com) defines a JSON document structure for
+    expressing a sequence of operations to apply to a JSON) document. With
+    this function, a JSON Patch is applied to the current JSON value by
+    executing all operations from the patch.
+
+    @param[in] json_patch  JSON patch document
+    @return patched document
+
+    @note The application of a patch is atomic: Either all operations succeed
+          and the patched document is returned or an exception is thrown. In
+          any case, the original value is not changed: the patch is applied
+          to a copy of the value.
+
+    @throw parse_error.104 if the JSON patch does not consist of an array of
+    objects
+
+    @throw parse_error.105 if the JSON patch is malformed (e.g., mandatory
+    attributes are missing); example: `"operation add must have member path"`
+
+    @throw out_of_range.401 if an array index is out of range.
+
+    @throw out_of_range.403 if a JSON pointer inside the patch could not be
+    resolved successfully in the current JSON value; example: `"key baz not
+    found"`
+
+    @throw out_of_range.405 if JSON pointer has no parent ("add", "remove",
+    "move")
+
+    @throw other_error.501 if "test" operation was unsuccessful
+
+    @complexity Linear in the size of the JSON value and the length of the
+    JSON patch. As usually only a fraction of the JSON value is affected by
+    the patch, the complexity can usually be neglected.
+
+    @liveexample{The following code shows how a JSON patch is applied to a
+    value.,patch}
+
+    @sa @ref diff -- create a JSON patch by comparing two JSON values
+
+    @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902)
+    @sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901)
+
+    @since version 2.0.0
+    */
+    basic_json patch(const basic_json& json_patch) const
+    {
+        // make a working copy to apply the patch to
+        basic_json result = *this;
+
+        // the valid JSON Patch operations
+        enum class patch_operations {add, remove, replace, move, copy, test, invalid};
+
+        const auto get_op = [](const std::string & op)
+        {
+            if (op == "add")
+            {
+                return patch_operations::add;
+            }
+            if (op == "remove")
+            {
+                return patch_operations::remove;
+            }
+            if (op == "replace")
+            {
+                return patch_operations::replace;
+            }
+            if (op == "move")
+            {
+                return patch_operations::move;
+            }
+            if (op == "copy")
+            {
+                return patch_operations::copy;
+            }
+            if (op == "test")
+            {
+                return patch_operations::test;
+            }
+
+            return patch_operations::invalid;
+        };
+
+        // wrapper for "add" operation; add value at ptr
+        const auto operation_add = [&result](json_pointer & ptr, basic_json val)
+        {
+            // adding to the root of the target document means replacing it
+            if (ptr.empty())
+            {
+                result = val;
+                return;
+            }
+
+            // make sure the top element of the pointer exists
+            json_pointer top_pointer = ptr.top();
+            if (top_pointer != ptr)
+            {
+                result.at(top_pointer);
+            }
+
+            // get reference to parent of JSON pointer ptr
+            const auto last_path = ptr.back();
+            ptr.pop_back();
+            basic_json& parent = result[ptr];
+
+            switch (parent.m_type)
+            {
+                case value_t::null:
+                case value_t::object:
+                {
+                    // use operator[] to add value
+                    parent[last_path] = val;
+                    break;
+                }
+
+                case value_t::array:
+                {
+                    if (last_path == "-")
+                    {
+                        // special case: append to back
+                        parent.push_back(val);
+                    }
+                    else
+                    {
+                        const auto idx = json_pointer::array_index(last_path);
+                        if (JSON_HEDLEY_UNLIKELY(idx > parent.size()))
+                        {
+                            // avoid undefined behavior
+                            JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range"));
+                        }
+
+                        // default case: insert add offset
+                        parent.insert(parent.begin() + static_cast<difference_type>(idx), val);
+                    }
+                    break;
+                }
+
+                // if there exists a parent it cannot be primitive
+                default:            // LCOV_EXCL_LINE
+                    JSON_ASSERT(false);  // LCOV_EXCL_LINE
+            }
+        };
+
+        // wrapper for "remove" operation; remove value at ptr
+        const auto operation_remove = [&result](json_pointer & ptr)
+        {
+            // get reference to parent of JSON pointer ptr
+            const auto last_path = ptr.back();
+            ptr.pop_back();
+            basic_json& parent = result.at(ptr);
+
+            // remove child
+            if (parent.is_object())
+            {
+                // perform range check
+                auto it = parent.find(last_path);
+                if (JSON_HEDLEY_LIKELY(it != parent.end()))
+                {
+                    parent.erase(it);
+                }
+                else
+                {
+                    JSON_THROW(out_of_range::create(403, "key '" + last_path + "' not found"));
+                }
+            }
+            else if (parent.is_array())
+            {
+                // note erase performs range check
+                parent.erase(json_pointer::array_index(last_path));
+            }
+        };
+
+        // type check: top level value must be an array
+        if (JSON_HEDLEY_UNLIKELY(!json_patch.is_array()))
+        {
+            JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
+        }
+
+        // iterate and apply the operations
+        for (const auto& val : json_patch)
+        {
+            // wrapper to get a value for an operation
+            const auto get_value = [&val](const std::string & op,
+                                          const std::string & member,
+                                          bool string_type) -> basic_json &
+            {
+                // find value
+                auto it = val.m_value.object->find(member);
+
+                // context-sensitive error message
+                const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";
+
+                // check if desired value is present
+                if (JSON_HEDLEY_UNLIKELY(it == val.m_value.object->end()))
+                {
+                    JSON_THROW(parse_error::create(105, 0, error_msg + " must have member '" + member + "'"));
+                }
+
+                // check if result is of type string
+                if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string()))
+                {
+                    JSON_THROW(parse_error::create(105, 0, error_msg + " must have string member '" + member + "'"));
+                }
+
+                // no error: return value
+                return it->second;
+            };
+
+            // type check: every element of the array must be an object
+            if (JSON_HEDLEY_UNLIKELY(!val.is_object()))
+            {
+                JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
+            }
+
+            // collect mandatory members
+            const auto op = get_value("op", "op", true).template get<std::string>();
+            const auto path = get_value(op, "path", true).template get<std::string>();
+            json_pointer ptr(path);
+
+            switch (get_op(op))
+            {
+                case patch_operations::add:
+                {
+                    operation_add(ptr, get_value("add", "value", false));
+                    break;
+                }
+
+                case patch_operations::remove:
+                {
+                    operation_remove(ptr);
+                    break;
+                }
+
+                case patch_operations::replace:
+                {
+                    // the "path" location must exist - use at()
+                    result.at(ptr) = get_value("replace", "value", false);
+                    break;
+                }
+
+                case patch_operations::move:
+                {
+                    const auto from_path = get_value("move", "from", true).template get<std::string>();
+                    json_pointer from_ptr(from_path);
+
+                    // the "from" location must exist - use at()
+                    basic_json v = result.at(from_ptr);
+
+                    // The move operation is functionally identical to a
+                    // "remove" operation on the "from" location, followed
+                    // immediately by an "add" operation at the target
+                    // location with the value that was just removed.
+                    operation_remove(from_ptr);
+                    operation_add(ptr, v);
+                    break;
+                }
+
+                case patch_operations::copy:
+                {
+                    const auto from_path = get_value("copy", "from", true).template get<std::string>();
+                    const json_pointer from_ptr(from_path);
+
+                    // the "from" location must exist - use at()
+                    basic_json v = result.at(from_ptr);
+
+                    // The copy is functionally identical to an "add"
+                    // operation at the target location using the value
+                    // specified in the "from" member.
+                    operation_add(ptr, v);
+                    break;
+                }
+
+                case patch_operations::test:
+                {
+                    bool success = false;
+                    JSON_TRY
+                    {
+                        // check if "value" matches the one at "path"
+                        // the "path" location must exist - use at()
+                        success = (result.at(ptr) == get_value("test", "value", false));
+                    }
+                    JSON_INTERNAL_CATCH (out_of_range&)
+                    {
+                        // ignore out of range errors: success remains false
+                    }
+
+                    // throw an exception if test fails
+                    if (JSON_HEDLEY_UNLIKELY(!success))
+                    {
+                        JSON_THROW(other_error::create(501, "unsuccessful: " + val.dump()));
+                    }
+
+                    break;
+                }
+
+                default:
+                {
+                    // op must be "add", "remove", "replace", "move", "copy", or
+                    // "test"
+                    JSON_THROW(parse_error::create(105, 0, "operation value '" + op + "' is invalid"));
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /*!
+    @brief creates a diff as a JSON patch
+
+    Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can
+    be changed into the value @a target by calling @ref patch function.
+
+    @invariant For two JSON values @a source and @a target, the following code
+    yields always `true`:
+    @code {.cpp}
+    source.patch(diff(source, target)) == target;
+    @endcode
+
+    @note Currently, only `remove`, `add`, and `replace` operations are
+          generated.
+
+    @param[in] source  JSON value to compare from
+    @param[in] target  JSON value to compare against
+    @param[in] path    helper value to create JSON pointers
+
+    @return a JSON patch to convert the @a source to @a target
+
+    @complexity Linear in the lengths of @a source and @a target.
+
+    @liveexample{The following code shows how a JSON patch is created as a
+    diff for two JSON values.,diff}
+
+    @sa @ref patch -- apply a JSON patch
+    @sa @ref merge_patch -- apply a JSON Merge Patch
+
+    @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902)
+
+    @since version 2.0.0
+    */
+    JSON_HEDLEY_WARN_UNUSED_RESULT
+    static basic_json diff(const basic_json& source, const basic_json& target,
+                           const std::string& path = "")
+    {
+        // the patch
+        basic_json result(value_t::array);
+
+        // if the values are the same, return empty patch
+        if (source == target)
+        {
+            return result;
+        }
+
+        if (source.type() != target.type())
+        {
+            // different types: replace value
+            result.push_back(
+            {
+                {"op", "replace"}, {"path", path}, {"value", target}
+            });
+            return result;
+        }
+
+        switch (source.type())
+        {
+            case value_t::array:
+            {
+                // first pass: traverse common elements
+                std::size_t i = 0;
+                while (i < source.size() && i < target.size())
+                {
+                    // recursive call to compare array values at index i
+                    auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i));
+                    result.insert(result.end(), temp_diff.begin(), temp_diff.end());
+                    ++i;
+                }
+
+                // i now reached the end of at least one array
+                // in a second pass, traverse the remaining elements
+
+                // remove my remaining elements
+                const auto end_index = static_cast<difference_type>(result.size());
+                while (i < source.size())
+                {
+                    // add operations in reverse order to avoid invalid
+                    // indices
+                    result.insert(result.begin() + end_index, object(
+                    {
+                        {"op", "remove"},
+                        {"path", path + "/" + std::to_string(i)}
+                    }));
+                    ++i;
+                }
+
+                // add other remaining elements
+                while (i < target.size())
+                {
+                    result.push_back(
+                    {
+                        {"op", "add"},
+                        {"path", path + "/-"},
+                        {"value", target[i]}
+                    });
+                    ++i;
+                }
+
+                break;
+            }
+
+            case value_t::object:
+            {
+                // first pass: traverse this object's elements
+                for (auto it = source.cbegin(); it != source.cend(); ++it)
+                {
+                    // escape the key name to be used in a JSON patch
+                    const auto key = json_pointer::escape(it.key());
+
+                    if (target.find(it.key()) != target.end())
+                    {
+                        // recursive call to compare object values at key it
+                        auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key);
+                        result.insert(result.end(), temp_diff.begin(), temp_diff.end());
+                    }
+                    else
+                    {
+                        // found a key that is not in o -> remove it
+                        result.push_back(object(
+                        {
+                            {"op", "remove"}, {"path", path + "/" + key}
+                        }));
+                    }
+                }
+
+                // second pass: traverse other object's elements
+                for (auto it = target.cbegin(); it != target.cend(); ++it)
+                {
+                    if (source.find(it.key()) == source.end())
+                    {
+                        // found a key that is not in this -> add it
+                        const auto key = json_pointer::escape(it.key());
+                        result.push_back(
+                        {
+                            {"op", "add"}, {"path", path + "/" + key},
+                            {"value", it.value()}
+                        });
+                    }
+                }
+
+                break;
+            }
+
+            default:
+            {
+                // both primitive type: replace value
+                result.push_back(
+                {
+                    {"op", "replace"}, {"path", path}, {"value", target}
+                });
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    /// @}
+
+    ////////////////////////////////
+    // JSON Merge Patch functions //
+    ////////////////////////////////
+
+    /// @name JSON Merge Patch functions
+    /// @{
+
+    /*!
+    @brief applies a JSON Merge Patch
+
+    The merge patch format is primarily intended for use with the HTTP PATCH
+    method as a means of describing a set of modifications to a target
+    resource's content. This function applies a merge patch to the current
+    JSON value.
+
+    The function implements the following algorithm from Section 2 of
+    [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396):
+
+    ```
+    define MergePatch(Target, Patch):
+      if Patch is an Object:
+        if Target is not an Object:
+          Target = {} // Ignore the contents and set it to an empty Object
+        for each Name/Value pair in Patch:
+          if Value is null:
+            if Name exists in Target:
+              remove the Name/Value pair from Target
+          else:
+            Target[Name] = MergePatch(Target[Name], Value)
+        return Target
+      else:
+        return Patch
+    ```
+
+    Thereby, `Target` is the current object; that is, the patch is applied to
+    the current value.
+
+    @param[in] apply_patch  the patch to apply
+
+    @complexity Linear in the lengths of @a patch.
+
+    @liveexample{The following code shows how a JSON Merge Patch is applied to
+    a JSON document.,merge_patch}
+
+    @sa @ref patch -- apply a JSON patch
+    @sa [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
+
+    @since version 3.0.0
+    */
+    void merge_patch(const basic_json& apply_patch)
+    {
+        if (apply_patch.is_object())
+        {
+            if (!is_object())
+            {
+                *this = object();
+            }
+            for (auto it = apply_patch.begin(); it != apply_patch.end(); ++it)
+            {
+                if (it.value().is_null())
+                {
+                    erase(it.key());
+                }
+                else
+                {
+                    operator[](it.key()).merge_patch(it.value());
+                }
+            }
+        }
+        else
+        {
+            *this = apply_patch;
+        }
+    }
+
+    /// @}
+};
+
+/*!
+@brief user-defined to_string function for JSON values
+
+This function implements a user-defined to_string  for JSON objects.
+
+@param[in] j  a JSON object
+@return a std::string object
+*/
+
+NLOHMANN_BASIC_JSON_TPL_DECLARATION
+std::string to_string(const NLOHMANN_BASIC_JSON_TPL& j)
+{
+    return j.dump();
+}
+} // namespace nlohmann
+
+///////////////////////
+// nonmember support //
+///////////////////////
+
+// specialization of std::swap, and std::hash
+namespace std
+{
+
+/// hash value for JSON objects
+template<>
+struct hash<nlohmann::json>
+{
+    /*!
+    @brief return a hash value for a JSON object
+
+    @since version 1.0.0
+    */
+    std::size_t operator()(const nlohmann::json& j) const
+    {
+        return nlohmann::detail::hash(j);
+    }
+};
+
+/// specialization for std::less<value_t>
+/// @note: do not remove the space after '<',
+///        see https://github.com/nlohmann/json/pull/679
+template<>
+struct less<::nlohmann::detail::value_t>
+{
+    /*!
+    @brief compare two value_t enum values
+    @since version 3.0.0
+    */
+    bool operator()(nlohmann::detail::value_t lhs,
+                    nlohmann::detail::value_t rhs) const noexcept
+    {
+        return nlohmann::detail::operator<(lhs, rhs);
+    }
+};
+
+// C++20 prohibit function specialization in the std namespace.
+#ifndef JSON_HAS_CPP_20
+
+/*!
+@brief exchanges the values of two JSON objects
+
+@since version 1.0.0
+*/
+template<>
+inline void swap<nlohmann::json>(nlohmann::json& j1, nlohmann::json& j2) noexcept(
+    is_nothrow_move_constructible<nlohmann::json>::value&&
+    is_nothrow_move_assignable<nlohmann::json>::value
+                              )
+{
+    j1.swap(j2);
+}
+
+#endif
+
+} // namespace std
+
+/*!
+@brief user-defined string literal for JSON values
+
+This operator implements a user-defined string literal for JSON objects. It
+can be used by adding `"_json"` to a string literal and returns a JSON object
+if no parse error occurred.
+
+@param[in] s  a string representation of a JSON object
+@param[in] n  the length of string @a s
+@return a JSON object
+
+@since version 1.0.0
+*/
+JSON_HEDLEY_NON_NULL(1)
+inline nlohmann::json operator "" _json(const char* s, std::size_t n)
+{
+    return nlohmann::json::parse(s, s + n);
+}
+
+/*!
+@brief user-defined string literal for JSON pointer
+
+This operator implements a user-defined string literal for JSON Pointers. It
+can be used by adding `"_json_pointer"` to a string literal and returns a JSON pointer
+object if no parse error occurred.
+
+@param[in] s  a string representation of a JSON Pointer
+@param[in] n  the length of string @a s
+@return a JSON pointer object
+
+@since version 2.0.0
+*/
+JSON_HEDLEY_NON_NULL(1)
+inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t n)
+{
+    return nlohmann::json::json_pointer(std::string(s, n));
+}
+
+// #include <nlohmann/detail/macro_unscope.hpp>
+
+
+// restore GCC/clang diagnostic settings
+#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
+    #pragma GCC diagnostic pop
+#endif
+#if defined(__clang__)
+    #pragma GCC diagnostic pop
+#endif
+
+// clean up
+#undef JSON_ASSERT
+#undef JSON_INTERNAL_CATCH
+#undef JSON_CATCH
+#undef JSON_THROW
+#undef JSON_TRY
+#undef JSON_HAS_CPP_14
+#undef JSON_HAS_CPP_17
+#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION
+#undef NLOHMANN_BASIC_JSON_TPL
+#undef JSON_EXPLICIT
+
+// #include <nlohmann/thirdparty/hedley/hedley_undef.hpp>
+#undef JSON_HEDLEY_ALWAYS_INLINE
+#undef JSON_HEDLEY_ARM_VERSION
+#undef JSON_HEDLEY_ARM_VERSION_CHECK
+#undef JSON_HEDLEY_ARRAY_PARAM
+#undef JSON_HEDLEY_ASSUME
+#undef JSON_HEDLEY_BEGIN_C_DECLS
+#undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE
+#undef JSON_HEDLEY_CLANG_HAS_BUILTIN
+#undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE
+#undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE
+#undef JSON_HEDLEY_CLANG_HAS_EXTENSION
+#undef JSON_HEDLEY_CLANG_HAS_FEATURE
+#undef JSON_HEDLEY_CLANG_HAS_WARNING
+#undef JSON_HEDLEY_COMPCERT_VERSION
+#undef JSON_HEDLEY_COMPCERT_VERSION_CHECK
+#undef JSON_HEDLEY_CONCAT
+#undef JSON_HEDLEY_CONCAT3
+#undef JSON_HEDLEY_CONCAT3_EX
+#undef JSON_HEDLEY_CONCAT_EX
+#undef JSON_HEDLEY_CONST
+#undef JSON_HEDLEY_CONSTEXPR
+#undef JSON_HEDLEY_CONST_CAST
+#undef JSON_HEDLEY_CPP_CAST
+#undef JSON_HEDLEY_CRAY_VERSION
+#undef JSON_HEDLEY_CRAY_VERSION_CHECK
+#undef JSON_HEDLEY_C_DECL
+#undef JSON_HEDLEY_DEPRECATED
+#undef JSON_HEDLEY_DEPRECATED_FOR
+#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL
+#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_
+#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
+#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES
+#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS
+#undef JSON_HEDLEY_DIAGNOSTIC_POP
+#undef JSON_HEDLEY_DIAGNOSTIC_PUSH
+#undef JSON_HEDLEY_DMC_VERSION
+#undef JSON_HEDLEY_DMC_VERSION_CHECK
+#undef JSON_HEDLEY_EMPTY_BASES
+#undef JSON_HEDLEY_EMSCRIPTEN_VERSION
+#undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK
+#undef JSON_HEDLEY_END_C_DECLS
+#undef JSON_HEDLEY_FLAGS
+#undef JSON_HEDLEY_FLAGS_CAST
+#undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE
+#undef JSON_HEDLEY_GCC_HAS_BUILTIN
+#undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE
+#undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE
+#undef JSON_HEDLEY_GCC_HAS_EXTENSION
+#undef JSON_HEDLEY_GCC_HAS_FEATURE
+#undef JSON_HEDLEY_GCC_HAS_WARNING
+#undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK
+#undef JSON_HEDLEY_GCC_VERSION
+#undef JSON_HEDLEY_GCC_VERSION_CHECK
+#undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE
+#undef JSON_HEDLEY_GNUC_HAS_BUILTIN
+#undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE
+#undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE
+#undef JSON_HEDLEY_GNUC_HAS_EXTENSION
+#undef JSON_HEDLEY_GNUC_HAS_FEATURE
+#undef JSON_HEDLEY_GNUC_HAS_WARNING
+#undef JSON_HEDLEY_GNUC_VERSION
+#undef JSON_HEDLEY_GNUC_VERSION_CHECK
+#undef JSON_HEDLEY_HAS_ATTRIBUTE
+#undef JSON_HEDLEY_HAS_BUILTIN
+#undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE
+#undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS
+#undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE
+#undef JSON_HEDLEY_HAS_EXTENSION
+#undef JSON_HEDLEY_HAS_FEATURE
+#undef JSON_HEDLEY_HAS_WARNING
+#undef JSON_HEDLEY_IAR_VERSION
+#undef JSON_HEDLEY_IAR_VERSION_CHECK
+#undef JSON_HEDLEY_IBM_VERSION
+#undef JSON_HEDLEY_IBM_VERSION_CHECK
+#undef JSON_HEDLEY_IMPORT
+#undef JSON_HEDLEY_INLINE
+#undef JSON_HEDLEY_INTEL_VERSION
+#undef JSON_HEDLEY_INTEL_VERSION_CHECK
+#undef JSON_HEDLEY_IS_CONSTANT
+#undef JSON_HEDLEY_IS_CONSTEXPR_
+#undef JSON_HEDLEY_LIKELY
+#undef JSON_HEDLEY_MALLOC
+#undef JSON_HEDLEY_MESSAGE
+#undef JSON_HEDLEY_MSVC_VERSION
+#undef JSON_HEDLEY_MSVC_VERSION_CHECK
+#undef JSON_HEDLEY_NEVER_INLINE
+#undef JSON_HEDLEY_NON_NULL
+#undef JSON_HEDLEY_NO_ESCAPE
+#undef JSON_HEDLEY_NO_RETURN
+#undef JSON_HEDLEY_NO_THROW
+#undef JSON_HEDLEY_NULL
+#undef JSON_HEDLEY_PELLES_VERSION
+#undef JSON_HEDLEY_PELLES_VERSION_CHECK
+#undef JSON_HEDLEY_PGI_VERSION
+#undef JSON_HEDLEY_PGI_VERSION_CHECK
+#undef JSON_HEDLEY_PREDICT
+#undef JSON_HEDLEY_PRINTF_FORMAT
+#undef JSON_HEDLEY_PRIVATE
+#undef JSON_HEDLEY_PUBLIC
+#undef JSON_HEDLEY_PURE
+#undef JSON_HEDLEY_REINTERPRET_CAST
+#undef JSON_HEDLEY_REQUIRE
+#undef JSON_HEDLEY_REQUIRE_CONSTEXPR
+#undef JSON_HEDLEY_REQUIRE_MSG
+#undef JSON_HEDLEY_RESTRICT
+#undef JSON_HEDLEY_RETURNS_NON_NULL
+#undef JSON_HEDLEY_SENTINEL
+#undef JSON_HEDLEY_STATIC_ASSERT
+#undef JSON_HEDLEY_STATIC_CAST
+#undef JSON_HEDLEY_STRINGIFY
+#undef JSON_HEDLEY_STRINGIFY_EX
+#undef JSON_HEDLEY_SUNPRO_VERSION
+#undef JSON_HEDLEY_SUNPRO_VERSION_CHECK
+#undef JSON_HEDLEY_TINYC_VERSION
+#undef JSON_HEDLEY_TINYC_VERSION_CHECK
+#undef JSON_HEDLEY_TI_ARMCL_VERSION
+#undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK
+#undef JSON_HEDLEY_TI_CL2000_VERSION
+#undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK
+#undef JSON_HEDLEY_TI_CL430_VERSION
+#undef JSON_HEDLEY_TI_CL430_VERSION_CHECK
+#undef JSON_HEDLEY_TI_CL6X_VERSION
+#undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK
+#undef JSON_HEDLEY_TI_CL7X_VERSION
+#undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK
+#undef JSON_HEDLEY_TI_CLPRU_VERSION
+#undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK
+#undef JSON_HEDLEY_TI_VERSION
+#undef JSON_HEDLEY_TI_VERSION_CHECK
+#undef JSON_HEDLEY_UNAVAILABLE
+#undef JSON_HEDLEY_UNLIKELY
+#undef JSON_HEDLEY_UNPREDICTABLE
+#undef JSON_HEDLEY_UNREACHABLE
+#undef JSON_HEDLEY_UNREACHABLE_RETURN
+#undef JSON_HEDLEY_VERSION
+#undef JSON_HEDLEY_VERSION_DECODE_MAJOR
+#undef JSON_HEDLEY_VERSION_DECODE_MINOR
+#undef JSON_HEDLEY_VERSION_DECODE_REVISION
+#undef JSON_HEDLEY_VERSION_ENCODE
+#undef JSON_HEDLEY_WARNING
+#undef JSON_HEDLEY_WARN_UNUSED_RESULT
+#undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG
+#undef JSON_HEDLEY_FALL_THROUGH
+
+
+
+#endif  // INCLUDE_NLOHMANN_JSON_HPP_
diff --git a/espanso-ui/src/win32/mod.rs b/espanso-ui/src/win32/mod.rs
new file mode 100644
index 00000000..41fb2bd0
--- /dev/null
+++ b/espanso-ui/src/win32/mod.rs
@@ -0,0 +1,377 @@
+/*
+ * 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::{
+  cmp::min,
+  collections::HashMap,
+  ffi::{c_void, CString},
+  os::raw::c_char,
+  path::PathBuf,
+  sync::{
+    atomic::{AtomicPtr, Ordering},
+    Arc,
+  },
+  thread::ThreadId,
+};
+
+mod notification;
+
+use anyhow::Result;
+use lazycell::LazyCell;
+use log::{error, trace};
+use thiserror::Error;
+use widestring::WideCString;
+
+use crate::{event::UIEvent, icons::TrayIcon, menu::Menu, UIEventCallback, UIEventLoop, UIRemote};
+
+// IMPORTANT: if you change these, also edit the native.h file.
+const MAX_FILE_PATH: usize = 260;
+const MAX_ICON_COUNT: usize = 3;
+
+const UI_EVENT_TYPE_ICON_CLICK: i32 = 1;
+const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2;
+const UI_EVENT_TYPE_HEARTBEAT: i32 = 3;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIOptions {
+  pub show_icon: i32,
+
+  pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT],
+  pub icon_paths_count: i32,
+}
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIEvent {
+  pub event_type: i32,
+  pub context_menu_id: u32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansoui", kind = "static")]
+extern "C" {
+  pub fn ui_initialize(
+    _self: *const Win32EventLoop,
+    options: RawUIOptions,
+    error_code: *mut i32,
+  ) -> *mut c_void;
+  pub fn ui_eventloop(
+    window_handle: *const c_void,
+    event_callback: extern "C" fn(_self: *mut Win32EventLoop, event: RawUIEvent),
+  ) -> i32;
+  pub fn ui_destroy(window_handle: *const c_void) -> i32;
+  pub fn ui_exit(window_handle: *const c_void) -> i32;
+  pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32);
+  pub fn ui_show_context_menu(window_handle: *const c_void, payload: *const c_char);
+}
+
+pub struct Win32UIOptions<'a> {
+  pub show_icon: bool,
+  pub icon_paths: &'a Vec<(TrayIcon, String)>,
+  pub notification_icon_path: String,
+}
+
+pub fn create(options: Win32UIOptions) -> Result<(Win32Remote, Win32EventLoop)> {
+  let handle: Arc<AtomicPtr<c_void>> = Arc::new(AtomicPtr::new(std::ptr::null_mut()));
+
+  // Validate icons
+  if options.icon_paths.len() > MAX_ICON_COUNT {
+    panic!("Win32 UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more");
+  }
+
+  // Convert the icon paths to the internal representation
+  let mut icon_indexes: HashMap<TrayIcon, usize> = HashMap::new();
+  let mut icons = Vec::new();
+  for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() {
+    icon_indexes.insert(tray_icon.clone(), index);
+    icons.push(path.clone());
+  }
+
+  let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon);
+  let remote = Win32Remote::new(
+    handle,
+    icon_indexes,
+    PathBuf::from(options.notification_icon_path),
+  );
+
+  Ok((remote, eventloop))
+}
+
+pub struct Win32EventLoop {
+  handle: Arc<AtomicPtr<c_void>>,
+
+  show_icon: bool,
+  icons: Vec<String>,
+
+  // Internal
+  _event_callback: LazyCell<UIEventCallback>,
+  _init_thread_id: LazyCell<ThreadId>,
+}
+
+impl Win32EventLoop {
+  pub(crate) fn new(handle: Arc<AtomicPtr<c_void>>, icons: Vec<String>, show_icon: bool) -> Self {
+    Self {
+      handle,
+      icons,
+      show_icon,
+      _event_callback: LazyCell::new(),
+      _init_thread_id: LazyCell::new(),
+    }
+  }
+}
+
+impl UIEventLoop for Win32EventLoop {
+  fn initialize(&mut self) -> Result<()> {
+    let window_handle = self.handle.load(Ordering::Acquire);
+    if !window_handle.is_null() {
+      error!("Attempt to initialize Win32EventLoop on non-null window handle");
+      return Err(Win32UIError::InvalidHandle().into());
+    }
+
+    // Convert the icon paths to the raw representation
+    let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] =
+      [[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
+    for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
+      let wide_path = WideCString::from_str(&self.icons[i])?;
+      let len = min(wide_path.len(), MAX_FILE_PATH - 1);
+      icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
+    }
+
+    let options = RawUIOptions {
+      show_icon: if self.show_icon { 1 } else { 0 },
+      icon_paths,
+      icon_paths_count: self.icons.len() as i32,
+    };
+
+    let mut error_code = 0;
+    let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options, &mut error_code) };
+
+    if handle.is_null() {
+      return match error_code {
+        -1 => Err(
+          Win32UIError::EventLoopInitError(
+            "Unable to initialize Win32EventLoop, error registering window class".to_string(),
+          )
+          .into(),
+        ),
+        -2 => Err(
+          Win32UIError::EventLoopInitError(
+            "Unable to initialize Win32EventLoop, error creating window".to_string(),
+          )
+          .into(),
+        ),
+        _ => Err(
+          Win32UIError::EventLoopInitError(
+            "Unable to initialize Win32EventLoop, unknown error".to_string(),
+          )
+          .into(),
+        ),
+      };
+    }
+
+    self.handle.store(handle, Ordering::Release);
+
+    // Make sure the run() method is called in the same thread as initialize()
+    self
+      ._init_thread_id
+      .fill(std::thread::current().id())
+      .expect("Unable to set initialization thread id");
+
+    Ok(())
+  }
+
+  fn run(&self, event_callback: UIEventCallback) -> Result<()> {
+    // Make sure the run() method is called in the same thread as initialize()
+    if let Some(init_id) = self._init_thread_id.borrow() {
+      if init_id != &std::thread::current().id() {
+        panic!("Win32EventLoop run() and initialize() methods should be called in the same thread");
+      }
+    }
+
+    let window_handle = self.handle.load(Ordering::Acquire);
+    if window_handle.is_null() {
+      error!("Attempt to run Win32EventLoop on a null window handle");
+      return Err(Win32UIError::InvalidHandle().into());
+    }
+
+    if self._event_callback.fill(event_callback).is_err() {
+      error!("Unable to set Win32EventLoop callback");
+      return Err(Win32UIError::InternalError().into());
+    }
+
+    extern "C" fn callback(_self: *mut Win32EventLoop, event: RawUIEvent) {
+      if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } {
+        let event: Option<UIEvent> = event.into();
+        if let Some(event) = event {
+          callback(event)
+        } else {
+          trace!("Unable to convert raw event to input event");
+        }
+      }
+    }
+
+    let error_code = unsafe { ui_eventloop(window_handle, callback) };
+
+    if error_code <= 0 {
+      error!("Win32EventLoop exited with <= 0 code");
+      return Err(Win32UIError::InternalError().into());
+    }
+
+    Ok(())
+  }
+}
+
+impl Drop for Win32EventLoop {
+  fn drop(&mut self) {
+    let handle = self.handle.swap(std::ptr::null_mut(), Ordering::Acquire);
+    if handle.is_null() {
+      error!("Win32EventLoop destruction cannot be performed, handle is null");
+      return;
+    }
+
+    let result = unsafe { ui_destroy(handle) };
+
+    if result != 0 {
+      error!("Win32EventLoop destruction returned non-zero code");
+    }
+  }
+}
+
+pub struct Win32Remote {
+  handle: Arc<AtomicPtr<c_void>>,
+
+  // Maps icon name to their index
+  icon_indexes: HashMap<TrayIcon, usize>,
+}
+
+impl Win32Remote {
+  pub(crate) fn new(
+    handle: Arc<AtomicPtr<c_void>>,
+    icon_indexes: HashMap<TrayIcon, usize>,
+    notification_icon_path: PathBuf,
+  ) -> Self {
+    if let Err(err) = notification::initialize_notification_thread(notification_icon_path) {
+      error!("unable to initialize notification thread: {}", err);
+    }
+
+    Self {
+      handle,
+      icon_indexes,
+    }
+  }
+}
+
+impl UIRemote for Win32Remote {
+  fn update_tray_icon(&self, icon: TrayIcon) {
+    let handle = self.handle.load(Ordering::Acquire);
+    if handle.is_null() {
+      error!("Unable to update tray icon, pointer is null");
+      return;
+    }
+
+    if let Some(index) = self.icon_indexes.get(&icon) {
+      unsafe { ui_update_tray_icon(handle, (*index) as i32) }
+    } else {
+      error!("Unable to update tray icon, invalid icon id");
+    }
+  }
+
+  fn show_notification(&self, message: &str) {
+    if let Err(err) = notification::show_notification(message) {
+      error!("Unable to show notification: {}", err);
+    }
+  }
+
+  fn show_context_menu(&self, menu: &Menu) {
+    let handle = self.handle.load(Ordering::Acquire);
+    if handle.is_null() {
+      error!("Unable to show context menu, pointer is null");
+      return;
+    }
+
+    match menu.to_json() {
+      Ok(payload) => {
+        let c_string = CString::new(payload);
+        match c_string {
+          Ok(c_string) => unsafe { ui_show_context_menu(handle, c_string.as_ptr()) },
+          Err(error) => error!(
+            "Unable to show context menu, impossible to convert payload to c_string: {}",
+            error
+          ),
+        }
+      }
+      Err(error) => {
+        error!("Unable to show context menu, {}", error);
+      }
+    }
+  }
+
+  fn exit(&self) {
+    let handle = self.handle.load(Ordering::Acquire);
+    if handle.is_null() {
+      error!("Unable to exit eventloop, pointer is null");
+      return;
+    }
+
+    unsafe { ui_exit(handle) };
+  }
+}
+
+#[allow(clippy::single_match)] // TODO: remove after another match is used
+impl From<RawUIEvent> for Option<UIEvent> {
+  fn from(raw: RawUIEvent) -> Option<UIEvent> {
+    match raw.event_type {
+      UI_EVENT_TYPE_ICON_CLICK => {
+        return Some(UIEvent::TrayIconClick);
+      }
+      UI_EVENT_TYPE_CONTEXT_MENU_CLICK => {
+        return Some(UIEvent::ContextMenuClick(raw.context_menu_id));
+      }
+      UI_EVENT_TYPE_HEARTBEAT => {
+        return Some(UIEvent::Heartbeat);
+      }
+      _ => {}
+    }
+
+    None
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum Win32UIError {
+  #[error("invalid handle")]
+  InvalidHandle(),
+
+  #[error("event loop initialization failed: `{0}`")]
+  EventLoopInitError(String),
+
+  #[error("internal error")]
+  InternalError(),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn constants_are_not_changed_by_mistake() {
+    assert_eq!(MAX_FILE_PATH, 260);
+    assert_eq!(MAX_ICON_COUNT, 3);
+  }
+}
diff --git a/espanso-ui/src/win32/native.cpp b/espanso-ui/src/win32/native.cpp
new file mode 100644
index 00000000..7455d82e
--- /dev/null
+++ b/espanso-ui/src/win32/native.cpp
@@ -0,0 +1,381 @@
+/*
+ * 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/>.
+ */
+
+#include "native.h"
+#include <stdio.h>
+#include <iostream>
+#include <string>
+#include <vector>
+#include <memory>
+#include <array>
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include <windows.h>
+#include <winuser.h>
+#include <string.h>
+#include <strsafe.h>
+#pragma comment(lib, "Shell32.lib")
+#include <shellapi.h>
+
+#include <Windows.h>
+
+#include "json/json.hpp"
+using json = nlohmann::json;
+
+#define APPWM_ICON_CLICK (WM_APP + 1)
+#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2)
+#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3)
+
+#define HEARTBEAT_TIMER_ID 10001
+
+const wchar_t *const ui_winclass = L"EspansoUI";
+
+typedef struct
+{
+  UIOptions options;
+  NOTIFYICONDATA nid;
+  HICON g_icons[MAX_ICON_COUNT];
+
+  // Rust interop
+  void *rust_instance;
+  EventCallback event_callback;
+} UIVariables;
+
+// Needed to detect when Explorer crashes
+UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
+
+/*
+ * Message handler procedure for the window
+ */
+LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
+{
+  UIEvent event = {};
+  UIVariables *variables = reinterpret_cast<UIVariables *>(GetWindowLongPtrW(window, GWLP_USERDATA));
+
+  switch (msg)
+  {
+  case WM_DESTROY:
+    PostQuitMessage(0);
+
+    // Remove tray icon
+    if (variables->options.show_icon)
+    {
+      Shell_NotifyIcon(NIM_DELETE, &variables->nid);
+    }
+
+    // Free the tray icons
+    for (int i = 0; i < variables->options.icon_paths_count; i++)
+    {
+      DeleteObject(variables->g_icons[i]);
+    }
+
+    // Free the window variables
+    delete variables;
+    SetWindowLongPtrW(window, GWLP_USERDATA, NULL);
+
+    return 0L;
+  case WM_COMMAND: // Click on the tray icon context menu
+  {
+    UINT idItem = (UINT)LOWORD(wp);
+    UINT flags = (UINT)HIWORD(wp);
+
+    if (flags == 0)
+    {
+      event.event_type = UI_EVENT_TYPE_CONTEXT_MENU_CLICK;
+      event.context_menu_id = (uint32_t)idItem;
+      if (variables->event_callback && variables->rust_instance)
+      {
+        variables->event_callback(variables->rust_instance, event);
+      }
+    }
+
+    break;
+  }
+  case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu
+  {
+    HMENU menu = (HMENU)lp;
+    POINT pt;
+    GetCursorPos(&pt);
+    SetForegroundWindow(window);
+    TrackPopupMenu(menu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, window, NULL);
+
+    break;
+  }
+  case APPWM_UPDATE_TRAY_ICON: // Request to update the tray icon
+  {
+    int32_t index = (int32_t)lp;
+    if (index >= variables->options.icon_paths_count)
+    {
+      break;
+    }
+
+    variables->nid.hIcon = variables->g_icons[index];
+    if (variables->options.show_icon)
+    {
+      Shell_NotifyIcon(NIM_MODIFY, &variables->nid);
+    }
+
+    break;
+  }
+  case APPWM_ICON_CLICK: // Click on the tray icon
+  {
+    switch (lp)
+    {
+    case WM_LBUTTONUP:
+    case WM_RBUTTONUP:
+      event.event_type = UI_EVENT_TYPE_ICON_CLICK;
+      if (variables->event_callback && variables->rust_instance)
+      {
+        variables->event_callback(variables->rust_instance, event);
+      }
+      break;
+    }
+  }
+  case WM_TIMER: // Regular timer check event
+  {
+    if (wp == HEARTBEAT_TIMER_ID)
+    {
+      event.event_type = UI_EVENT_TYPE_HEARTBEAT;
+      if (variables->event_callback && variables->rust_instance)
+      {
+        variables->event_callback(variables->rust_instance, event);
+      }
+      break;
+    }
+  }
+  default:
+    if (msg == WM_TASKBARCREATED)
+    { // Explorer crashed, recreate the icon
+      if (variables->options.show_icon)
+      {
+        Shell_NotifyIcon(NIM_ADD, &variables->nid);
+      }
+    }
+    return DefWindowProc(window, msg, wp, lp);
+  }
+}
+
+void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
+{
+  HWND window = NULL;
+
+  SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
+
+  // Service Window
+
+  // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
+  WNDCLASSEX uiwndclass = {
+      sizeof(WNDCLASSEX),       // cbSize: Size of this structure
+      0,                        // style: Class styles
+      ui_window_procedure,      // lpfnWndProc: Pointer to the window procedure
+      0,                        // cbClsExtra: Number of extra bytes to allocate following the window-class structure
+      0,                        // cbWndExtra: The number of extra bytes to allocate following the window instance.
+      GetModuleHandle(0),       // hInstance: A handle to the instance that contains the window procedure for the class.
+      NULL,                     // hIcon: A handle to the class icon.
+      LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor.
+      NULL,                     // hbrBackground: A handle to the class background brush.
+      NULL,                     // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
+      ui_winclass,              // lpszClassName: A pointer to a null-terminated string or is an atom.
+      NULL                      // hIconSm: A handle to a small icon that is associated with the window class.
+  };
+
+  if (RegisterClassEx(&uiwndclass))
+  {
+    // Initialize the service window
+    window = CreateWindowEx(
+        0,                    // dwExStyle: The extended window style of the window being created.
+        ui_winclass,          // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
+        L"Espanso UI Window", // lpWindowName: The window name.
+        WS_OVERLAPPEDWINDOW,  // dwStyle: The style of the window being created.
+        CW_USEDEFAULT,        // X: The initial horizontal position of the window.
+        CW_USEDEFAULT,        // Y: The initial vertical position of the window.
+        100,                  // nWidth: The width, in device units, of the window.
+        100,                  // nHeight: The height, in device units, of the window.
+        NULL,                 // hWndParent:  handle to the parent or owner window of the window being created.
+        NULL,                 // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
+        GetModuleHandle(0),   // hInstance: A handle to the instance of the module to be associated with the window.
+        NULL                  // lpParam: Pointer to a value to be passed to the window
+    );
+
+    if (window)
+    {
+      UIVariables *variables = new UIVariables();
+      variables->options = _options;
+      variables->rust_instance = _self;
+      SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
+
+      // Load the tray icons
+      for (int i = 0; i < variables->options.icon_paths_count; i++)
+      {
+        variables->g_icons[i] = (HICON)LoadImage(NULL, variables->options.icon_paths[i], IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
+      }
+
+      // Hide the window
+      ShowWindow(window, SW_HIDE);
+
+      // Setup the icon in the tray space
+      SendMessage(window, WM_SETICON, ICON_BIG, (LPARAM)variables->g_icons[0]);
+      SendMessage(window, WM_SETICON, ICON_SMALL, (LPARAM)variables->g_icons[0]);
+
+      // Tray icon
+      variables->nid.cbSize = sizeof(variables->nid);
+      variables->nid.hWnd = window;
+      variables->nid.uID = 1;
+      variables->nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
+      variables->nid.uCallbackMessage = APPWM_ICON_CLICK;
+      variables->nid.hIcon = variables->g_icons[0];
+      StringCchCopyW(variables->nid.szTip, ARRAYSIZE(variables->nid.szTip), L"espanso");
+
+      // Show the tray icon
+      if (variables->options.show_icon)
+      {
+        Shell_NotifyIcon(NIM_ADD, &variables->nid);
+      }
+
+      // Setup heartbeat timer
+      SetTimer(window, HEARTBEAT_TIMER_ID, 1000, (TIMERPROC)NULL);
+    }
+    else
+    {
+      *error_code = -2;
+      return nullptr;
+    }
+  }
+  else
+  {
+    *error_code = -1;
+    return nullptr;
+  }
+
+  return window;
+}
+
+int32_t ui_eventloop(void *window, EventCallback _callback)
+{
+  if (window)
+  {
+    UIVariables *variables = reinterpret_cast<UIVariables *>(GetWindowLongPtrW((HWND)window, GWLP_USERDATA));
+    variables->event_callback = _callback;
+
+    // Enter the Event loop
+    MSG msg;
+    while (GetMessage(&msg, 0, 0, 0))
+      DispatchMessage(&msg);
+  }
+
+  return 1;
+}
+
+int32_t ui_destroy(void *window)
+{
+  if (window)
+  {
+    return DestroyWindow((HWND)window);
+  }
+  return -1;
+}
+
+void ui_exit(void *window)
+{
+  if (window)
+  {
+    PostMessage((HWND)window, WM_CLOSE, 0, 0);
+  }
+}
+
+void ui_update_tray_icon(void *window, int32_t index)
+{
+  if (window)
+  {
+    PostMessage((HWND)window, APPWM_UPDATE_TRAY_ICON, 0, index);
+  }
+}
+
+// Menu related methods
+
+void _insert_separator_menu(HMENU parent)
+{
+  InsertMenu(parent, -1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
+}
+
+void _insert_single_menu(HMENU parent, json item)
+{
+  if (!item["label"].is_string() || !item["id"].is_number())
+  {
+    return;
+  }
+  std::string label = item["label"];
+  uint32_t raw_id = item["id"];
+
+  // Convert to wide chars
+  std::wstring wide_label(label.length(), L'#');
+  mbstowcs(&wide_label[0], label.c_str(), label.length());
+
+  InsertMenu(parent, -1, MF_BYPOSITION | MF_STRING, raw_id, wide_label.c_str());
+}
+
+void _insert_sub_menu(HMENU parent, json items)
+{
+  for (auto &item : items)
+  {
+    if (item["type"] == "simple")
+    {
+      _insert_single_menu(parent, item);
+    }
+    else if (item["type"] == "separator")
+    {
+      _insert_separator_menu(parent);
+    }
+    else if (item["type"] == "sub")
+    {
+      HMENU subMenu = CreatePopupMenu();
+      std::string label = item["label"];
+
+      // Convert to wide chars
+      std::wstring wide_label(label.length(), L'#');
+      mbstowcs(&wide_label[0], label.c_str(), label.length());
+
+      InsertMenu(parent, -1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)subMenu, wide_label.c_str());
+      _insert_sub_menu(subMenu, item["items"]);
+    }
+  }
+}
+
+int32_t ui_show_context_menu(void *window, char *payload)
+{
+  if (window)
+  {
+    auto j_menu = json::parse(payload);
+    // Generate the menu from the JSON payload
+    HMENU parentMenu = CreatePopupMenu();
+    _insert_sub_menu(parentMenu, j_menu);
+
+    PostMessage((HWND)window, APPWM_SHOW_CONTEXT_MENU, 0, (LPARAM)parentMenu);
+    return 0;
+  }
+  return -1;
+}
\ No newline at end of file
diff --git a/espanso-ui/src/win32/native.h b/espanso-ui/src/win32/native.h
new file mode 100644
index 00000000..c403603b
--- /dev/null
+++ b/espanso-ui/src/win32/native.h
@@ -0,0 +1,69 @@
+/*
+ * 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/>.
+ */
+
+#ifndef ESPANSO_UI_H
+#define ESPANSO_UI_H
+
+#include <stdint.h>
+
+// Explicitly define this constant as we need to use it from the Rust side
+// https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
+#define MAX_FILE_PATH 260
+#define MAX_ICON_COUNT 3
+
+#define UI_EVENT_TYPE_ICON_CLICK 1
+#define UI_EVENT_TYPE_CONTEXT_MENU_CLICK 2
+#define UI_EVENT_TYPE_HEARTBEAT 3
+
+typedef struct {
+  int32_t show_icon;
+
+  wchar_t icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
+  int32_t icon_paths_count;
+} UIOptions;
+
+typedef struct {
+  int32_t event_type;
+  uint32_t context_menu_id;
+} UIEvent;
+
+typedef void (*EventCallback)(void * self, UIEvent data);
+
+// Initialize the hidden UI window, the tray icon and returns the window handle.
+extern "C" void * ui_initialize(void * self, UIOptions options, int32_t * error_code);
+
+// Run the event loop. Blocking call.
+extern "C" int32_t ui_eventloop(void * window, EventCallback callback);
+
+// Destroy the given window.
+extern "C" int32_t ui_destroy(void * window);
+
+// Send a termination event that exits the event loop
+extern "C" void ui_exit(void * window);
+
+// Updates the tray icon to the given one. The method accepts an index that refers to
+// the icon within the UIOptions.icon_paths array.
+extern "C" void ui_update_tray_icon(void * window, int32_t index);
+
+// Display the context menu on the tray icon.
+// Payload is passed as JSON as given the complex structure, parsing
+// this manually would have been complex.
+extern "C" int32_t ui_show_context_menu(void * window, char * payload);
+
+#endif //ESPANSO_UI_H
\ No newline at end of file
diff --git a/espanso-ui/src/win32/notification.rs b/espanso-ui/src/win32/notification.rs
new file mode 100644
index 00000000..40d2cf37
--- /dev/null
+++ b/espanso-ui/src/win32/notification.rs
@@ -0,0 +1,112 @@
+/*
+ * 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::{
+  path::PathBuf,
+  sync::{
+    mpsc::{channel, Sender},
+    Arc, Mutex,
+  },
+};
+
+use anyhow::{anyhow, bail, Result};
+use lazy_static::lazy_static;
+use log::{error, warn};
+use std::os::windows::process::CommandExt;
+use std::process::Command;
+use winrt_notification::{IconCrop, Toast};
+
+const ESPANSO_APP_USER_MODEL_ID: &str = "{5E3B6C0F-1A4D-45C4-8872-D8174702101A}";
+
+lazy_static! {
+  static ref SEND_CHANNEL: Arc<Mutex<Option<Sender<String>>>> = Arc::new(Mutex::new(None));
+}
+
+pub fn initialize_notification_thread(notification_icon_path: PathBuf) -> Result<()> {
+  let (sender, receiver) = channel::<String>();
+
+  {
+    let mut lock = SEND_CHANNEL
+      .lock()
+      .map_err(|e| anyhow!("failed to define shared notification sender: {}", e))?;
+    *lock = Some(sender);
+  }
+
+  std::thread::Builder::new().name("notification-thread".to_string()).spawn(move || {
+    // First determine which AppUserModelID we can use
+    lazy_static! {
+      static ref APP_USER_MODEL_ID: &'static str = if is_espanso_app_user_model_id_set() {
+        ESPANSO_APP_USER_MODEL_ID
+      } else {
+        warn!("unable to find espanso AppUserModelID in the list of registered ones, falling back to Powershell");
+        Toast::POWERSHELL_APP_ID
+      };
+    }
+
+    while let Ok(message) = receiver.recv() {
+      if let Err(err) = Toast::new(&APP_USER_MODEL_ID)
+        .icon(&notification_icon_path, IconCrop::Square, "Espanso")
+        .title("Espanso")
+        .text1(&message)
+        .sound(None)
+        .show()
+        .map_err(|e| anyhow!("failed to show notification: {}", e)) {    
+          error!("unable to show notification: {}", err); 
+      }
+    }
+  })?;
+
+  Ok(())
+}
+
+pub fn show_notification(msg: &str) -> Result<()> {
+  let mut lock = SEND_CHANNEL
+    .lock()
+    .map_err(|e| anyhow!("unable to acquire notification send channel: {}", e))?;
+  match &mut *lock {
+    Some(sender) => {
+      sender.send(msg.to_string())?;
+      Ok(())
+    }
+    None => bail!("notification sender not available"),
+  }
+}
+
+fn is_espanso_app_user_model_id_set() -> bool {
+  match Command::new("powershell")
+    .args(&["-c", "get-startapps"])
+    .creation_flags(0x08000000)
+    .output()
+  {
+    Ok(output) => {
+      let output_str = String::from_utf8_lossy(&output.stdout);
+      // Check if espanso is present
+      output_str
+        .lines()
+        .any(|line| line.contains(ESPANSO_APP_USER_MODEL_ID))
+    }
+    Err(err) => {
+      error!(
+        "unable to determine if AppUserModelID was registered: {}",
+        err
+      );
+      false
+    }
+  }
+}
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
new file mode 100644
index 00000000..0a5e80cf
--- /dev/null
+++ b/espanso/Cargo.toml
@@ -0,0 +1,75 @@
+[package]
+name = "espanso"
+version = "2.0.1"
+authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
+license = "GPL-3.0"
+description = "Cross-platform Text Expander written in Rust"
+readme = "README.md"
+homepage = "https://github.com/federico-terzi/espanso"
+edition = "2018"
+
+[features]
+default = ["modulo"]
+
+# If the wayland feature is enabled, all X11 dependencies will be dropped
+# and only methods suitable for Wayland will be used
+wayland = ["espanso-detect/wayland", "espanso-inject/wayland", "espanso-clipboard/wayland", "espanso-info/wayland"]
+
+# Compile modulo and all its dependencies (including wxWidgets). If you don't
+# enable it, features like Forms and Search might not be available.
+modulo = ["espanso-modulo", "espanso-clipboard/avoid-gdi", "espanso-ui/avoid-gdi"]
+
+[dependencies]
+espanso-detect = { path = "../espanso-detect" } 
+espanso-ui = { path = "../espanso-ui" } 
+espanso-inject = { path = "../espanso-inject" } 
+espanso-config = { path = "../espanso-config" } 
+espanso-match = { path = "../espanso-match" } 
+espanso-clipboard = { path = "../espanso-clipboard" } 
+espanso-info = { path = "../espanso-info" } 
+espanso-render = { path = "../espanso-render" } 
+espanso-path = { path = "../espanso-path" } 
+espanso-ipc = { path = "../espanso-ipc" } 
+espanso-modulo = { path = "../espanso-modulo", optional = true }
+espanso-migrate = { path = "../espanso-migrate" } 
+espanso-kvs = { path = "../espanso-kvs" } 
+espanso-engine = { path = "../espanso-engine" } 
+espanso-package = { path = "../espanso-package"}
+maplit = "1.0.2"
+simplelog = "0.9.0"
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+clap = "2.33.3"
+lazy_static = "1.4.0"
+crossbeam = "0.8.0"
+enum-as-inner = "0.3.3"
+dirs = "3.0.1"
+serde = { version = "1.0.123", features = ["derive"] }
+serde_json = "1.0.62"
+log-panics = "2.0.0"
+fs2 = "0.4.3"
+serde_yaml = "0.8.17"
+fs_extra = "1.2.0"
+dialoguer = "0.8.0"
+colored = "2.0.0"
+tempdir = "0.3.7"
+notify = "4.0.17"
+opener = "0.5.0"
+
+[target.'cfg(windows)'.dependencies]
+named_pipe = "0.4.1"
+winapi = { version = "0.3.9", features = ["wincon"] }
+winreg = "0.9.0"
+widestring = "0.4.3"
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2.98"
+
+[target.'cfg(target_os="macos")'.dependencies]
+espanso-mac-utils = { path = "../espanso-mac-utils" } 
+
+[target.'cfg(target_os="linux")'.dependencies]
+caps = "0.5.2"
+const_format = "0.2.14"
+regex = "1.4.3"
\ No newline at end of file
diff --git a/espanso/src/capabilities/fallback.rs b/espanso/src/capabilities/fallback.rs
new file mode 100644
index 00000000..10b08c98
--- /dev/null
+++ b/espanso/src/capabilities/fallback.rs
@@ -0,0 +1,32 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+
+pub fn can_use_capabilities() -> bool {
+  false
+}
+
+pub fn grant_capabilities() -> Result<()> {
+  Ok(())
+}
+
+pub fn clear_capabilities() -> Result<()> {
+  Ok(())
+}
diff --git a/espanso/src/capabilities/linux.rs b/espanso/src/capabilities/linux.rs
new file mode 100644
index 00000000..5a4970e4
--- /dev/null
+++ b/espanso/src/capabilities/linux.rs
@@ -0,0 +1,42 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+use anyhow::Result;
+use caps::{CapSet, Capability};
+use log::error;
+
+pub fn can_use_capabilities() -> bool {
+  match caps::has_cap(None, CapSet::Permitted, Capability::CAP_DAC_OVERRIDE) {
+    Ok(has_cap) => has_cap,
+    Err(err) => {
+      error!("error while checking if capabilities are enabled: {}", err);
+      false
+    }
+  }
+}
+
+pub fn grant_capabilities() -> Result<()> {
+  caps::raise(None, CapSet::Effective, Capability::CAP_DAC_OVERRIDE)?;
+  Ok(())
+}
+
+pub fn clear_capabilities() -> Result<()> {
+  caps::clear(None, CapSet::Effective)?;
+  caps::clear(None, CapSet::Permitted)?;
+  Ok(())
+}
diff --git a/espanso/src/capabilities/mod.rs b/espanso/src/capabilities/mod.rs
new file mode 100644
index 00000000..f407bb6a
--- /dev/null
+++ b/espanso/src/capabilities/mod.rs
@@ -0,0 +1,28 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "linux")]
+pub use linux::*;
+
+#[cfg(not(target_os = "linux"))]
+mod fallback;
+#[cfg(not(target_os = "linux"))]
+pub use fallback::*;
diff --git a/espanso/src/cli/daemon/ipc.rs b/espanso/src/cli/daemon/ipc.rs
new file mode 100644
index 00000000..32d41f54
--- /dev/null
+++ b/espanso/src/cli/daemon/ipc.rs
@@ -0,0 +1,60 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+use espanso_ipc::{EventHandlerResponse, IPCServer};
+use log::{error, warn};
+
+use crate::{exit_code::DAEMON_SUCCESS, ipc::IPCEvent};
+
+pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<i32>) -> Result<()> {
+  let server = crate::ipc::create_daemon_ipc_server(runtime_dir)?;
+
+  std::thread::Builder::new()
+    .name("daemon-ipc-handler".to_string())
+    .spawn(move || {
+      server
+        .run(Box::new(move |event| match event {
+          IPCEvent::Exit => {
+            if let Err(err) = exit_notify.send(DAEMON_SUCCESS) {
+              error!(
+                "experienced error while sending exit signal from daemon ipc handler: {}",
+                err
+              );
+            }
+
+            EventHandlerResponse::NoResponse
+          }
+          unexpected_event => {
+            warn!(
+              "received unexpected event in daemon ipc handler: {:?}",
+              unexpected_event
+            );
+
+            EventHandlerResponse::NoResponse
+          }
+        }))
+        .expect("unable to start IPC handler");
+    })?;
+
+  Ok(())
+}
diff --git a/espanso/src/cli/daemon/keyboard_layout_watcher.rs b/espanso/src/cli/daemon/keyboard_layout_watcher.rs
new file mode 100644
index 00000000..8f778928
--- /dev/null
+++ b/espanso/src/cli/daemon/keyboard_layout_watcher.rs
@@ -0,0 +1,72 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+use log::{debug, error};
+
+const WATCHER_INTERVAL: u64 = 1000;
+
+pub fn initialize_and_spawn(watcher_notify: Sender<()>) -> Result<()> {
+  // On Windows and macOS we don't need to restart espanso when the layout changes
+  if !cfg!(target_os = "linux") {
+    return Ok(());
+  }
+
+  std::thread::Builder::new()
+    .name("keyboard_layout_watcher".to_string())
+    .spawn(move || {
+      watcher_main(&watcher_notify);
+    })?;
+
+  Ok(())
+}
+
+fn watcher_main(watcher_notify: &Sender<()>) {
+  let layout = espanso_detect::get_active_layout();
+
+  if layout.is_none() {
+    error!("unable to start keyboard layout watcher, as espanso couldn't determine active layout.");
+    return;
+  }
+
+  let mut layout = layout.expect("missing active layout");
+
+  loop {
+    std::thread::sleep(std::time::Duration::from_millis(WATCHER_INTERVAL));
+
+    if let Some(current_layout) = espanso_detect::get_active_layout() {
+      if current_layout != layout {
+        debug!(
+          "detected keyboard layout change: from {} to {}",
+          layout, current_layout
+        );
+
+        if let Err(error) = watcher_notify.send(()) {
+          error!("unable to send keyboard layout changed event: {}", error);
+        }
+
+        layout = current_layout;
+      }
+    } else {
+      error!("keyboard layout watcher couldn't determine active layout");
+      break;
+    }
+  }
+}
diff --git a/espanso/src/cli/daemon/mod.rs b/espanso/src/cli/daemon/mod.rs
new file mode 100644
index 00000000..4543f172
--- /dev/null
+++ b/espanso/src/cli/daemon/mod.rs
@@ -0,0 +1,318 @@
+/*
+ * 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::{path::Path, process::Command, time::Instant};
+
+use crossbeam::{
+  channel::{unbounded, Sender},
+  select,
+};
+use espanso_ipc::IPCClient;
+use espanso_path::Paths;
+use log::{error, info, warn};
+
+use crate::{
+  cli::util::CommandExt,
+  common_flags::*,
+  exit_code::{
+    DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR,
+    DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART,
+    WORKER_SUCCESS,
+  },
+  ipc::{create_ipc_client_to_worker, IPCEvent},
+  lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock},
+  VERSION,
+};
+
+use super::{CliModule, CliModuleArgs, PathsOverrides};
+
+mod ipc;
+mod keyboard_layout_watcher;
+mod troubleshoot;
+mod watcher;
+
+pub fn new() -> CliModule {
+  #[allow(clippy::needless_update)]
+  CliModule {
+    requires_paths: true,
+    enable_logs: true,
+    log_mode: super::LogMode::CleanAndAppend,
+    subcommand: "daemon".to_string(),
+    entry: daemon_main,
+    ..Default::default()
+  }
+}
+
+fn daemon_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths in daemon main");
+  let paths_overrides = args
+    .paths_overrides
+    .expect("missing paths_overrides in daemon main");
+
+  // Make sure only one instance of the daemon is running
+  let lock_file = acquire_daemon_lock(&paths.runtime);
+  if lock_file.is_none() {
+    error!("daemon is already running!");
+    return DAEMON_ALREADY_RUNNING;
+  }
+
+  let legacy_lock_file = acquire_legacy_lock(&paths.runtime);
+  if legacy_lock_file.is_none() {
+    // TODO: show a (blocking) alert message using modulo
+
+    error!("an instance of legacy espanso is running, please terminate it, otherwise the new version cannot start");
+    return DAEMON_LEGACY_ALREADY_RUNNING;
+  }
+  drop(legacy_lock_file);
+
+  // TODO: we might need to check preconditions: accessibility on macOS, presence of binaries on Linux, etc
+
+  // This variable holds the current troubleshooter guard.
+  // When a guard is dropped, the troubleshooting GUI is killed
+  // so this ensures that there is only one troubleshooter running
+  // at a given time.
+  let mut _current_troubleshoot_guard = None;
+
+  let (watcher_notify, watcher_signal) = unbounded::<()>();
+
+  watcher::initialize_and_spawn(&paths.config, watcher_notify)
+    .expect("unable to initialize config watcher thread");
+
+  let (_keyboard_layout_watcher_notify, keyboard_layout_watcher_signal) = unbounded::<()>();
+
+  #[allow(clippy::redundant_clone)]
+  // IMPORTANT: Here we clone the channel instead of simply passing it to avoid
+  // dropping the channel immediately on those platforms that don't support the
+  // layout watcher (currently Windows and macOS).
+  // Otherwise, the select below would always return an error because the channel is closed.
+  keyboard_layout_watcher::initialize_and_spawn(_keyboard_layout_watcher_notify.clone()) // DON'T REMOVE THE CLONE!
+    .expect("unable to initialize keyboard layout watcher thread");
+
+  let config_store =
+    match troubleshoot::load_config_or_troubleshoot_until_config_is_correct_or_abort(
+      &paths,
+      &paths_overrides,
+      watcher_signal.clone(),
+    ) {
+      Ok((result, guard)) => {
+        _current_troubleshoot_guard = guard;
+        result.config_store
+      }
+      Err(err) => {
+        error!("critical error while loading config: {}", err);
+        return DAEMON_FATAL_CONFIG_ERROR;
+      }
+    };
+
+  info!("espanso version: {}", VERSION);
+  // TODO: print os system and version? (with os_info crate)
+
+  terminate_worker_if_already_running(&paths.runtime);
+
+  let (exit_notify, exit_signal) = unbounded::<i32>();
+
+  // TODO: register signals to terminate the worker if the daemon terminates
+
+  spawn_worker(&paths_overrides, exit_notify.clone(), None);
+
+  ipc::initialize_and_spawn(&paths.runtime, exit_notify.clone())
+    .expect("unable to initialize ipc server for daemon");
+
+  loop {
+    select! {
+      recv(watcher_signal) -> _ => {
+        if !config_store.default().auto_restart() {
+          continue;
+        }
+
+        info!("configuration change detected, restarting worker process...");
+
+        // Before killing the previous worker, we make sure there is no fatal error
+        // in the configs.
+        let should_restart_worker = match troubleshoot::load_config_or_troubleshoot(&paths, &paths_overrides) {
+          troubleshoot::LoadResult::Correct(_) => {
+            _current_troubleshoot_guard = None;
+            true
+          },
+          troubleshoot::LoadResult::Warning(_, guard) => {
+            _current_troubleshoot_guard = guard;
+            true
+          }
+          troubleshoot::LoadResult::Fatal(guard) => {
+            _current_troubleshoot_guard = Some(guard);
+            error!("critical error while loading config, could not restart worker");
+            false
+          }
+        };
+
+        if should_restart_worker {
+          restart_worker(&paths, &paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_CONFIG_CHANGED.to_string()));
+        }
+      }
+      recv(keyboard_layout_watcher_signal) -> _ => {
+        info!("keyboard layout change detected, restarting worker...");
+        restart_worker(&paths, &paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED.to_string()));
+      }
+      recv(exit_signal) -> code => {
+        match code {
+          Ok(code) => {
+            match code {
+              WORKER_EXIT_ALL_PROCESSES => {
+                info!("worker requested a general exit, quitting the daemon");
+                break;
+              }
+              WORKER_RESTART => {
+                info!("worker requested a restart, spawning a new one...");
+                spawn_worker(&paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_MANUAL.to_string()));
+              }
+              _ => {
+                error!("received unexpected exit code from worker {}, exiting", code);
+                return code;
+              }
+            }
+          },
+          Err(err) => {
+            error!("received error when unwrapping exit_code: {}", err);
+            return DAEMON_GENERAL_ERROR;
+          },
+        }
+      },
+    }
+  }
+
+  DAEMON_SUCCESS
+}
+
+fn terminate_worker_if_already_running(runtime_dir: &Path) {
+  let lock_file = acquire_worker_lock(runtime_dir);
+  if lock_file.is_some() {
+    return;
+  }
+
+  warn!("a worker process is already running, sending termination signal...");
+
+  match create_ipc_client_to_worker(runtime_dir) {
+    Ok(mut worker_ipc) => {
+      if let Err(err) = worker_ipc.send_async(IPCEvent::Exit) {
+        error!(
+          "unable to send termination signal to worker process: {}",
+          err
+        );
+      }
+    }
+    Err(err) => {
+      error!("could not establish IPC connection with worker: {}", err);
+    }
+  }
+
+  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;
+    }
+
+    std::thread::sleep(std::time::Duration::from_millis(200));
+  }
+
+  panic!(
+    "could not terminate worker process, please kill it manually, otherwise espanso won't start"
+  )
+}
+
+fn spawn_worker(
+  paths_overrides: &PathsOverrides,
+  exit_notify: Sender<i32>,
+  start_reason: Option<String>,
+) {
+  info!("spawning the worker process...");
+
+  let espanso_exe_path =
+    std::env::current_exe().expect("unable to obtain espanso executable location");
+
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+
+  let mut args = vec!["worker", "--monitor-daemon"];
+  if let Some(start_reason) = &start_reason {
+    args.push("--start-reason");
+    args.push(start_reason);
+  }
+  command.args(&args);
+  command.with_paths_overrides(paths_overrides);
+
+  let mut child = command.spawn().expect("unable to spawn worker process");
+
+  // Create a monitor thread that will exit with the same non-zero code if
+  // the worker thread exits
+  std::thread::Builder::new()
+    .name("worker-status-monitor".to_string())
+    .spawn(move || {
+      let result = child.wait();
+      if let Ok(status) = result {
+        if let Some(code) = status.code() {
+          if code != WORKER_SUCCESS {
+            exit_notify
+              .send(code)
+              .expect("unable to forward worker exit code");
+          }
+        }
+      }
+    })
+    .expect("Unable to spawn worker monitor thread");
+}
+
+fn restart_worker(
+  paths: &Paths,
+  paths_overrides: &PathsOverrides,
+  exit_notify: Sender<i32>,
+  start_reason: Option<String>,
+) {
+  match create_ipc_client_to_worker(&paths.runtime) {
+    Ok(mut worker_ipc) => {
+      if let Err(err) = worker_ipc.send_async(IPCEvent::Exit) {
+        error!(
+          "unable to send termination signal to worker process: {}",
+          err
+        );
+      }
+    }
+    Err(err) => {
+      error!("could not establish IPC connection with worker: {}", err);
+    }
+  }
+
+  // Wait until the worker process has terminated
+  let start = Instant::now();
+  let mut has_timed_out = true;
+  while start.elapsed() < std::time::Duration::from_secs(30) {
+    let lock_file = acquire_worker_lock(&paths.runtime);
+    if lock_file.is_some() {
+      has_timed_out = false;
+      break;
+    }
+
+    std::thread::sleep(std::time::Duration::from_millis(100));
+  }
+
+  if !has_timed_out {
+    spawn_worker(paths_overrides, exit_notify, start_reason);
+  } else {
+    error!("could not restart worker, as the exit process has timed out");
+  }
+}
diff --git a/espanso/src/cli/daemon/troubleshoot.rs b/espanso/src/cli/daemon/troubleshoot.rs
new file mode 100644
index 00000000..d33a4e30
--- /dev/null
+++ b/espanso/src/cli/daemon/troubleshoot.rs
@@ -0,0 +1,147 @@
+/*
+ * 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::process::{Child, Command};
+use std::time::Duration;
+
+use anyhow::{bail, Result};
+use crossbeam::channel::Receiver;
+use crossbeam::select;
+use espanso_path::Paths;
+use log::info;
+
+use crate::cli::util::CommandExt;
+use crate::cli::PathsOverrides;
+use crate::config::ConfigLoadResult;
+use crate::error_eprintln;
+use crate::preferences::Preferences;
+
+pub fn launch_troubleshoot(paths_overrides: &PathsOverrides) -> Result<TroubleshootGuard> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["modulo", "troubleshoot"]);
+  command.with_paths_overrides(paths_overrides);
+
+  let child = command.spawn()?;
+
+  Ok(TroubleshootGuard::new(child))
+}
+
+pub struct TroubleshootGuard {
+  child: Child,
+}
+
+impl TroubleshootGuard {
+  pub fn new(child: Child) -> Self {
+    Self { child }
+  }
+  #[allow(dead_code)]
+  pub fn wait(&mut self) -> Result<()> {
+    self.child.wait()?;
+    Ok(())
+  }
+  pub fn try_wait(&mut self) -> Result<bool> {
+    let result = self.child.try_wait()?;
+    Ok(result.is_some())
+  }
+}
+
+impl Drop for TroubleshootGuard {
+  fn drop(&mut self) {
+    let _ = self.child.kill();
+  }
+}
+
+pub enum LoadResult {
+  Correct(ConfigLoadResult),
+  Warning(ConfigLoadResult, Option<TroubleshootGuard>),
+  Fatal(TroubleshootGuard),
+}
+
+pub fn load_config_or_troubleshoot(paths: &Paths, paths_overrides: &PathsOverrides) -> LoadResult {
+  match crate::load_config(&paths.config, &paths.packages) {
+    Ok(load_result) => {
+      if load_result.non_fatal_errors.is_empty() {
+        LoadResult::Correct(load_result)
+      } else {
+        let mut troubleshoot_handle = None;
+
+        let preferences =
+          crate::preferences::get_default(&paths.runtime).expect("unable to get preferences");
+
+        if preferences.should_display_troubleshoot_for_non_fatal_errors() {
+          match launch_troubleshoot(paths_overrides) {
+            Ok(handle) => {
+              troubleshoot_handle = Some(handle);
+            }
+            Err(err) => {
+              error_eprintln!("unable to launch troubleshoot GUI: {}", err);
+            }
+          }
+        }
+
+        LoadResult::Warning(load_result, troubleshoot_handle)
+      }
+    }
+    Err(_) => LoadResult::Fatal(
+      launch_troubleshoot(paths_overrides).expect("unable to launch troubleshoot GUI"),
+    ),
+  }
+}
+
+pub fn load_config_or_troubleshoot_until_config_is_correct_or_abort(
+  paths: &Paths,
+  paths_overrides: &PathsOverrides,
+  watcher_receiver: Receiver<()>,
+) -> Result<(ConfigLoadResult, Option<TroubleshootGuard>)> {
+  let mut _troubleshoot_guard = None;
+
+  loop {
+    // If the loading process is fatal, we keep showing the troubleshooter until
+    // either the config is correct or the user aborts by closing the troubleshooter
+    _troubleshoot_guard = match load_config_or_troubleshoot(paths, paths_overrides) {
+      LoadResult::Correct(result) => return Ok((result, None)),
+      LoadResult::Warning(result, guard) => return Ok((result, guard)),
+      LoadResult::Fatal(guard) => Some(guard),
+    };
+
+    loop {
+      select! {
+        recv(watcher_receiver) -> _ => {
+          info!("config change detected, reloading configs...");
+
+          break
+        },
+        default(Duration::from_millis(500)) => {
+          if let Some(guard) = &mut _troubleshoot_guard {
+            if let Ok(ended) = guard.try_wait() {
+              if ended {
+                bail!("user aborted troubleshooter");
+              }
+            } else {
+              bail!("unable to wait for troubleshooter");
+            }
+          } else {
+            bail!("no troubleshoot guard found");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/espanso/src/cli/daemon/watcher.rs b/espanso/src/cli/daemon/watcher.rs
new file mode 100644
index 00000000..9bf817a2
--- /dev/null
+++ b/espanso/src/cli/daemon/watcher.rs
@@ -0,0 +1,133 @@
+/*
+ * 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::{
+  path::Path,
+  time::{Duration, Instant},
+};
+
+use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+use log::{error, info, warn};
+
+const WATCHER_DEBOUNCE_DURATION: u64 = 1;
+
+pub fn initialize_and_spawn(config_dir: &Path, watcher_notify: Sender<()>) -> Result<()> {
+  let config_dir = config_dir.to_path_buf();
+
+  std::thread::Builder::new()
+    .name("watcher".to_string())
+    .spawn(move || {
+      watcher_main(&config_dir, &watcher_notify);
+    })?;
+
+  Ok(())
+}
+
+fn watcher_main(config_dir: &Path, watcher_notify: &Sender<()>) {
+  let (tx, rx) = std::sync::mpsc::channel();
+
+  let mut watcher: RecommendedWatcher =
+    Watcher::new(tx, Duration::from_secs(WATCHER_DEBOUNCE_DURATION))
+      .expect("unable to create file watcher");
+
+  watcher
+    .watch(&config_dir, RecursiveMode::Recursive)
+    .expect("unable to start file watcher");
+
+  info!("watching for changes in path: {:?}", config_dir);
+
+  let mut last_event_arrival = Instant::now();
+
+  loop {
+    let should_reload = match rx.recv() {
+      Ok(event) => {
+        let path = match event {
+          DebouncedEvent::Create(path) => Some(path),
+          DebouncedEvent::Write(path) => Some(path),
+          DebouncedEvent::Remove(path) => Some(path),
+          DebouncedEvent::Rename(_, path) => Some(path),
+          _ => None,
+        };
+
+        if let Some(path) = path {
+          let extension = path
+            .extension()
+            .unwrap_or_default()
+            .to_string_lossy()
+            .to_ascii_lowercase();
+
+          if ["yml", "yaml"].iter().any(|ext| ext == &extension) {
+            // Only load non-hidden yml files
+            !is_file_hidden(&path)
+          } else {
+            // If there is no extension, it's probably a folder
+            extension.is_empty()
+          }
+        } else {
+          false
+        }
+      }
+      Err(e) => {
+        warn!("error while watching files: {:?}", e);
+        false
+      }
+    };
+
+    // Send only one event, otherwise we could run the risk of useless reloads or even race conditions.
+    if should_reload
+      && last_event_arrival.elapsed() > std::time::Duration::from_secs(WATCHER_DEBOUNCE_DURATION)
+    {
+      if let Err(error) = watcher_notify.send(()) {
+        error!("unable to send watcher file changed event: {}", error);
+      }
+    }
+
+    last_event_arrival = Instant::now();
+  }
+}
+
+fn is_file_hidden(path: &Path) -> bool {
+  let starts_with_dot = path
+    .file_name()
+    .unwrap_or_default()
+    .to_string_lossy()
+    .starts_with('.');
+
+  starts_with_dot || has_hidden_attribute(path)
+}
+
+#[cfg(windows)]
+fn has_hidden_attribute(path: &Path) -> bool {
+  use std::os::windows::prelude::*;
+  let metadata = std::fs::metadata(path);
+  if metadata.is_err() {
+    return false;
+  }
+  let attributes = metadata.unwrap().file_attributes();
+
+  (attributes & 0x2) > 0
+}
+
+#[cfg(not(windows))]
+fn has_hidden_attribute(_: &Path) -> bool {
+  false
+}
diff --git a/espanso/src/cli/edit.rs b/espanso/src/cli/edit.rs
new file mode 100644
index 00000000..3050508e
--- /dev/null
+++ b/espanso/src/cli/edit.rs
@@ -0,0 +1,230 @@
+/*
+ * 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::path::{Path, PathBuf};
+use std::process::Command;
+
+use super::{CliModule, CliModuleArgs};
+
+#[cfg(target_os = "linux")]
+fn default_editor() -> String {
+  "/bin/nano".to_owned()
+}
+#[cfg(target_os = "macos")]
+fn default_editor() -> String {
+  "/usr/bin/nano".to_owned()
+}
+#[cfg(target_os = "windows")]
+fn default_editor() -> String {
+  "C:\\Windows\\System32\\notepad.exe".to_owned()
+}
+
+pub fn new() -> CliModule {
+  CliModule {
+    requires_paths: true,
+    subcommand: "edit".to_string(),
+    entry: edit_main,
+    ..Default::default()
+  }
+}
+
+fn edit_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args");
+
+  if !paths.config.is_dir() {
+    panic!(
+      "config directory does not exist in path: {:?}",
+      paths.config
+    );
+  }
+
+  // Determine which is the file to edit
+  let target_file = cli_args.value_of("target_file");
+  let target_path = determine_target_path(&paths.config, target_file);
+
+  println!(
+    "Editing file: {}",
+    &target_path.to_string_lossy().to_string()
+  );
+
+  open_editor(&target_path);
+
+  // The previous version automatically reloaded the config after saving.
+  // Given that the new version automatically reloads config changes, this could be avoided.
+  // Nevertheless, this assumption might be wrong, so I'm keeping the necessary code.
+  // TODO: evaluate if reload is needed after v2 becomes stable
+
+  // // Based on the fact that the file already exists or not, we should detect in different
+  // // ways if a reload is needed
+  // let should_reload = if target_path.exists() {
+  //   // Get the last modified date, so that we can detect if the user actually edits the file
+  //   // before reloading
+  //   let metadata = std::fs::metadata(&target_path).expect("cannot gather file metadata");
+  //   let last_modified = metadata
+  //     .modified()
+  //     .expect("cannot read file last modified date");
+
+  //   let result = open_editor(&target_path);
+  //   if result {
+  //     let new_metadata = std::fs::metadata(&target_path).expect("cannot gather file metadata");
+  //     let new_last_modified = new_metadata
+  //       .modified()
+  //       .expect("cannot read file last modified date");
+
+  //     if last_modified != new_last_modified {
+  //       println!("File has been modified, reloading configuration");
+  //       true
+  //     } else {
+  //       println!("File has not been modified, avoiding reload");
+  //       false
+  //     }
+  //   } else {
+  //     false
+  //   }
+  // } else {
+  //   let result = open_editor(&target_path);
+  //   if result {
+  //     // If the file has been created, we should reload the espanso config
+  //     if target_path.exists() {
+  //       println!("A new file has been created, reloading configuration");
+  //       true
+  //     } else {
+  //       println!("No file has been created, avoiding reload");
+  //       false
+  //     }
+  //   } else {
+  //     false
+  //   }
+  // };
+
+  // let no_restart: bool = if cli_args.is_present("norestart") {
+  //   println!("Skipping automatic restart");
+  //   true
+  // } else {
+  //   false
+  // };
+
+  // if should_reload && !no_restart {
+  //   // Check if the new configuration is valid
+
+  //   if let Err(err) = crate::config::load_config(&paths.config, &paths.packages) {
+  //     eprintln!("Unable to reload espanso due to a configuration error:");
+  //     eprintln!("{:?}", err);
+  //     return 1;
+  //   };
+
+  //   restart_espanso(&paths_overrides).expect("unable to restart espanso");
+  // }
+
+  0
+}
+
+fn determine_target_path(config_path: &Path, target_file: Option<&str>) -> PathBuf {
+  if let Some(target_file) = target_file {
+    match target_file {
+      "default" => {
+        if espanso_config::is_legacy_config(config_path) {
+          config_path.join("default.yml")
+        } else {
+          config_path.join("config").join("default.yml")
+        }
+      }
+      "base" => {
+        if espanso_config::is_legacy_config(config_path) {
+          panic!("'base' alias cannot be used in compatibility mode, please migrate your configuration by running 'espanso migrate'")
+        } else {
+          config_path.join("match").join("base.yml")
+        }
+      }
+      custom => {
+        if !custom.ends_with(".yml") && !custom.ends_with(".yaml") {
+          if espanso_config::is_legacy_config(config_path) {
+            config_path.join("user").join(format!("{}.yml", custom))
+          } else {
+            config_path.join("match").join(format!("{}.yml", custom))
+          }
+        } else {
+          config_path.join(custom)
+        }
+      }
+    }
+  } else if espanso_config::is_legacy_config(config_path) {
+    config_path.join("default.yml")
+  } else {
+    config_path.join("match").join("base.yml")
+  }
+}
+
+pub fn open_editor(file_path: &Path) -> bool {
+  // Check if another editor is defined in the environment variables
+  let editor_var = std::env::var_os("EDITOR");
+  let visual_var = std::env::var_os("VISUAL");
+
+  // Prioritize the editors specified by the environment variable, use the default one
+  let editor: String = if let Some(editor_var) = editor_var {
+    editor_var.to_string_lossy().to_string()
+  } else if let Some(visual_var) = visual_var {
+    visual_var.to_string_lossy().to_string()
+  } else {
+    default_editor()
+  };
+
+  // Start the editor and wait for its termination
+  let status = if cfg!(target_os = "windows") {
+    Command::new(&editor).arg(file_path).spawn()
+  } else {
+    // On Unix, spawn the editor using the shell so that it can
+    // accept parameters. See issue #245
+    Command::new("/bin/bash")
+      .arg("-c")
+      .arg(format!("{} '{}'", editor, file_path.to_string_lossy()))
+      .spawn()
+  };
+
+  if let Ok(mut child) = status {
+    // Wait for the user to edit the configuration
+    let result = child.wait();
+
+    if let Ok(exit_status) = result {
+      exit_status.success()
+    } else {
+      false
+    }
+  } else {
+    println!("Error: could not start editor at: {}", &editor);
+    false
+  }
+}
+
+// fn restart_espanso(paths_overrides: &PathsOverrides) -> Result<()> {
+//   let espanso_exe_path = std::env::current_exe()?;
+
+//   let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+//   command.args(&["restart"]);
+//   command.with_paths_overrides(paths_overrides);
+
+//   let output = command.output()?;
+
+//   if output.status.success() {
+//     Ok(())
+//   } else {
+//     bail!("restart command returned a non-zero exit code");
+//   }
+// }
diff --git a/espanso/src/cli/env_path.rs b/espanso/src/cli/env_path.rs
new file mode 100644
index 00000000..ea18299c
--- /dev/null
+++ b/espanso/src/cli/env_path.rs
@@ -0,0 +1,68 @@
+/*
+ * 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 crate::exit_code::{ADD_TO_PATH_FAILURE, ADD_TO_PATH_SUCCESS};
+
+use super::{CliModule, CliModuleArgs};
+use log::error;
+
+pub fn new() -> CliModule {
+  CliModule {
+    enable_logs: true,
+    disable_logs_terminal_output: true,
+    log_mode: super::LogMode::AppendOnly,
+    subcommand: "env-path".to_string(),
+    entry: env_path_main,
+    ..Default::default()
+  }
+}
+
+fn env_path_main(args: CliModuleArgs) -> i32 {
+  let cli_args = args.cli_args.expect("missing cli_args");
+
+  let elevated_priviledge_prompt = cli_args.is_present("prompt");
+
+  if cli_args.subcommand_matches("register").is_some() {
+    if let Err(error) = crate::path::add_espanso_to_path(elevated_priviledge_prompt) {
+      error_print_and_log(&format!(
+        "Unable to add 'espanso' command to PATH: {:?}",
+        error
+      ));
+      return ADD_TO_PATH_FAILURE;
+    }
+  } else if cli_args.subcommand_matches("unregister").is_some() {
+    if let Err(error) = crate::path::remove_espanso_from_path(elevated_priviledge_prompt) {
+      error_print_and_log(&format!(
+        "Unable to remove 'espanso' command from PATH: {:?}",
+        error
+      ));
+      return ADD_TO_PATH_FAILURE;
+    }
+  } else {
+    eprintln!("Please specify a subcommand, either `espanso env-path register` to add the 'espanso' command or `espanso env-path unregister` to remove it");
+    return ADD_TO_PATH_FAILURE;
+  }
+
+  ADD_TO_PATH_SUCCESS
+}
+
+fn error_print_and_log(msg: &str) {
+  error!("{}", msg);
+  eprintln!("{}", msg);
+}
diff --git a/espanso/src/cli/launcher/accessibility.rs b/espanso/src/cli/launcher/accessibility.rs
new file mode 100644
index 00000000..a5c078f8
--- /dev/null
+++ b/espanso/src/cli/launcher/accessibility.rs
@@ -0,0 +1,38 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(not(target_os = "macos"))]
+pub fn is_accessibility_enabled() -> bool {
+  true
+}
+
+#[cfg(not(target_os = "macos"))]
+pub fn prompt_enable_accessibility() -> bool {
+  true
+}
+
+#[cfg(target_os = "macos")]
+pub fn is_accessibility_enabled() -> bool {
+  espanso_mac_utils::check_accessibility()
+}
+
+#[cfg(target_os = "macos")]
+pub fn prompt_enable_accessibility() -> bool {
+  espanso_mac_utils::prompt_accessibility()
+}
diff --git a/espanso/src/cli/launcher/daemon.rs b/espanso/src/cli/launcher/daemon.rs
new file mode 100644
index 00000000..ee109dac
--- /dev/null
+++ b/espanso/src/cli/launcher/daemon.rs
@@ -0,0 +1,48 @@
+/*
+ * 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::process::Command;
+
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::cli::util::CommandExt;
+use crate::cli::PathsOverrides;
+
+pub fn launch_daemon(paths_overrides: &PathsOverrides) -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["daemon"]);
+  command.with_paths_overrides(paths_overrides);
+
+  let mut child = command.spawn()?;
+  let result = child.wait()?;
+
+  if result.success() {
+    Ok(())
+  } else {
+    Err(DaemonError::NonZeroExitCode.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum DaemonError {
+  #[error("unexpected error, 'espanso daemon' returned a non-zero exit code.")]
+  NonZeroExitCode,
+}
diff --git a/espanso/src/cli/launcher/edition_check.rs b/espanso/src/cli/launcher/edition_check.rs
new file mode 100644
index 00000000..1638c524
--- /dev/null
+++ b/espanso/src/cli/launcher/edition_check.rs
@@ -0,0 +1,59 @@
+/*
+ * 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 crate::warn_eprintln;
+use espanso_modulo::wizard::DetectedOS;
+
+pub fn is_wrong_edition() -> (bool, DetectedOS) {
+  if !cfg!(target_os = "linux") {
+    return (false, DetectedOS::Unknown);
+  }
+
+  match get_session_type().as_deref() {
+    Some("x11") if cfg!(feature = "wayland") => return (true, DetectedOS::X11),
+    Some("wayland") if !cfg!(feature = "wayland") => return (true, DetectedOS::Wayland),
+    None => {
+      warn_eprintln!("could not automatically determine the session type (X11/Wayland), so make sure you have the correct espanso version!");
+    }
+    _ => {}
+  }
+
+  (false, DetectedOS::Unknown)
+}
+
+fn get_session_type() -> Option<String> {
+  let output = std::process::Command::new("sh")
+    .arg("-c")
+    .arg("loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type")
+    .output()
+    .ok()?;
+
+  if !output.status.success() {
+    return None;
+  }
+
+  let raw_session_type = String::from_utf8_lossy(&output.stdout);
+  let raw_session_type = raw_session_type.trim();
+  if !raw_session_type.contains("Type=") {
+    return None;
+  }
+
+  let session_type: Option<&str> = raw_session_type.split('=').into_iter().last();
+  session_type.map(String::from)
+}
diff --git a/espanso/src/cli/launcher/mod.rs b/espanso/src/cli/launcher/mod.rs
new file mode 100644
index 00000000..9be664ab
--- /dev/null
+++ b/espanso/src/cli/launcher/mod.rs
@@ -0,0 +1,222 @@
+/*
+ * 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 self::util::MigrationError;
+use crate::preferences::Preferences;
+use crate::{
+  exit_code::{LAUNCHER_ALREADY_RUNNING, LAUNCHER_CONFIG_DIR_POPULATION_FAILURE, LAUNCHER_SUCCESS},
+  lock::acquire_daemon_lock,
+};
+use log::error;
+
+use super::{CliModule, CliModuleArgs};
+
+mod accessibility;
+mod daemon;
+#[cfg(feature = "modulo")]
+mod edition_check;
+mod util;
+
+// TODO: test also with modulo feature disabled
+
+pub fn new() -> CliModule {
+  #[allow(clippy::needless_update)]
+  CliModule {
+    requires_paths: true,
+    enable_logs: false,
+    subcommand: "launcher".to_string(),
+    show_in_dock: true,
+    entry: launcher_main,
+    ..Default::default()
+  }
+}
+
+#[cfg(feature = "modulo")]
+fn launcher_main(args: CliModuleArgs) -> i32 {
+  use espanso_modulo::wizard::{MigrationResult, WizardHandlers, WizardOptions};
+  let paths = args.paths.expect("missing paths in launcher main");
+
+  // TODO: should we create a non-gui wizard? We can also use it for the non-modulo versions of espanso
+
+  // If espanso is already running, show a warning
+  let lock_file = acquire_daemon_lock(&paths.runtime);
+  if lock_file.is_none() {
+    util::show_already_running_warning().expect("unable to show already running warning");
+    return LAUNCHER_ALREADY_RUNNING;
+  }
+  drop(lock_file);
+
+  let paths_overrides = args
+    .paths_overrides
+    .expect("missing paths overrides in launcher main");
+  let icon_paths = crate::icon::load_icon_paths(&paths.runtime).expect("unable to load icon paths");
+
+  let preferences =
+    crate::preferences::get_default(&paths.runtime).expect("unable to initialize preferences");
+
+  let is_welcome_page_enabled = !preferences.has_completed_wizard();
+
+  let is_move_bundle_page_enabled = false; // TODO
+
+  let is_legacy_version_page_enabled = util::is_legacy_version_running(&paths.runtime);
+  let runtime_dir_clone = paths.runtime.clone();
+  let is_legacy_version_running_handler =
+    Box::new(move || util::is_legacy_version_running(&runtime_dir_clone));
+
+  let (is_wrong_edition_page_enabled, wrong_edition_detected_os) =
+    edition_check::is_wrong_edition();
+
+  let is_migrate_page_enabled = espanso_config::is_legacy_config(&paths.config);
+  let paths_clone = paths.clone();
+  let backup_and_migrate_handler =
+    Box::new(move || match util::migrate_configuration(&paths_clone) {
+      Ok(_) => MigrationResult::Success,
+      Err(error) => match error.downcast_ref::<MigrationError>() {
+        Some(MigrationError::Dirty) => MigrationResult::DirtyFailure,
+        Some(MigrationError::Clean) => MigrationResult::CleanFailure,
+        _ => MigrationResult::UnknownFailure,
+      },
+    });
+
+  let is_auto_start_page_enabled = !preferences.has_selected_auto_start_option();
+  let preferences_clone = preferences.clone();
+  let auto_start_handler = Box::new(move |auto_start| {
+    preferences_clone.set_has_selected_auto_start_option(true);
+
+    if auto_start {
+      match util::configure_auto_start(true) {
+        Ok(_) => true,
+        Err(error) => {
+          eprintln!("Service register returned error: {}", error);
+          false
+        }
+      }
+    } else {
+      true
+    }
+  });
+
+  let is_add_path_page_enabled =
+    if cfg!(not(target_os = "linux")) && !preferences.has_completed_wizard() {
+      if cfg!(target_os = "macos") {
+        !crate::path::is_espanso_in_path()
+      } else if paths.is_portable_mode {
+        false
+      } else {
+        !crate::path::is_espanso_in_path()
+      }
+    } else {
+      false
+    };
+  let add_to_path_handler = Box::new(move || match util::add_espanso_to_path() {
+    Ok(_) => true,
+    Err(error) => {
+      eprintln!("Add to path returned error: {}", error);
+      false
+    }
+  });
+
+  let is_accessibility_page_enabled = if cfg!(target_os = "macos") {
+    !accessibility::is_accessibility_enabled()
+  } else {
+    false
+  };
+  let is_accessibility_enabled_handler = Box::new(accessibility::is_accessibility_enabled);
+  let enable_accessibility_handler = Box::new(move || {
+    accessibility::prompt_enable_accessibility();
+  });
+
+  let on_completed_handler = Box::new(move || {
+    preferences.set_completed_wizard(true);
+  });
+
+  // Only show the wizard if a panel should be displayed
+  let should_launch_daemon = if is_welcome_page_enabled
+    || is_move_bundle_page_enabled
+    || is_legacy_version_page_enabled
+    || is_migrate_page_enabled
+    || is_auto_start_page_enabled
+    || is_add_path_page_enabled
+    || is_accessibility_page_enabled
+    || is_wrong_edition_page_enabled
+  {
+    espanso_modulo::wizard::show(WizardOptions {
+      version: crate::VERSION.to_string(),
+      is_welcome_page_enabled,
+      is_move_bundle_page_enabled,
+      is_legacy_version_page_enabled,
+      is_wrong_edition_page_enabled,
+      is_migrate_page_enabled,
+      is_auto_start_page_enabled,
+      is_add_path_page_enabled,
+      is_accessibility_page_enabled,
+      window_icon_path: icon_paths
+        .wizard_icon
+        .map(|path| path.to_string_lossy().to_string()),
+      welcome_image_path: icon_paths
+        .logo_no_background
+        .map(|path| path.to_string_lossy().to_string()),
+      accessibility_image_1_path: icon_paths
+        .accessibility_image_1
+        .map(|path| path.to_string_lossy().to_string()),
+      accessibility_image_2_path: icon_paths
+        .accessibility_image_2
+        .map(|path| path.to_string_lossy().to_string()),
+      detected_os: wrong_edition_detected_os,
+      handlers: WizardHandlers {
+        is_legacy_version_running: Some(is_legacy_version_running_handler),
+        backup_and_migrate: Some(backup_and_migrate_handler),
+        auto_start: Some(auto_start_handler),
+        add_to_path: Some(add_to_path_handler),
+        enable_accessibility: Some(enable_accessibility_handler),
+        is_accessibility_enabled: Some(is_accessibility_enabled_handler),
+        on_completed: Some(on_completed_handler),
+      },
+    })
+  } else {
+    true
+  };
+
+  if !espanso_config::is_legacy_config(&paths.config) {
+    if let Err(err) = crate::config::populate_default_config(&paths.config) {
+      error!("Error populating the config directory: {:?}", err);
+
+      // TODO: show an error message with GUI
+      return LAUNCHER_CONFIG_DIR_POPULATION_FAILURE;
+    }
+  }
+
+  if should_launch_daemon {
+    // We hide the dock icon on macOS to avoid having it around when the daemon is running
+    #[cfg(target_os = "macos")]
+    {
+      espanso_mac_utils::convert_to_background_app();
+    }
+
+    daemon::launch_daemon(&paths_overrides).expect("failed to launch daemon");
+  }
+
+  LAUNCHER_SUCCESS
+}
+
+#[cfg(not(feature = "modulo"))]
+fn launcher_main(_: CliModuleArgs) -> i32 {
+  // TODO: handle what happens here
+  unimplemented!();
+}
diff --git a/espanso/src/cli/launcher/util.rs b/espanso/src/cli/launcher/util.rs
new file mode 100644
index 00000000..e4fd3b07
--- /dev/null
+++ b/espanso/src/cli/launcher/util.rs
@@ -0,0 +1,132 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::{path::Path, process::Command};
+
+use anyhow::{bail, Result};
+use espanso_path::Paths;
+use thiserror::Error;
+
+use crate::{
+  exit_code::{MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE},
+  lock::acquire_legacy_lock,
+  util::set_command_flags,
+};
+
+pub fn is_legacy_version_running(runtime_path: &Path) -> bool {
+  let legacy_lock_file = acquire_legacy_lock(runtime_path);
+  legacy_lock_file.is_none()
+}
+
+pub fn migrate_configuration(paths: &Paths) -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["migrate", "--noconfirm"]);
+  command.env(
+    "ESPANSO_CONFIG_DIR",
+    paths.config.to_string_lossy().to_string(),
+  );
+  command.env(
+    "ESPANSO_PACKAGE_DIR",
+    paths.packages.to_string_lossy().to_string(),
+  );
+  command.env(
+    "ESPANSO_RUNTIME_DIR",
+    paths.runtime.to_string_lossy().to_string(),
+  );
+
+  let mut child = command.spawn()?;
+  let result = child.wait()?;
+
+  if result.success() {
+    Ok(())
+  } else {
+    match result.code() {
+      Some(code) if code == MIGRATE_CLEAN_FAILURE => Err(MigrationError::Clean.into()),
+      Some(code) if code == MIGRATE_DIRTY_FAILURE => Err(MigrationError::Dirty.into()),
+      _ => Err(MigrationError::Unexpected.into()),
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum MigrationError {
+  #[error("clean error")]
+  Clean,
+
+  #[error("dirty error")]
+  Dirty,
+
+  #[error("unexpected error")]
+  Unexpected,
+}
+
+pub fn add_espanso_to_path() -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["env-path", "--prompt", "register"]);
+
+  let mut child = command.spawn()?;
+  let result = child.wait()?;
+
+  if result.success() {
+    Ok(())
+  } else {
+    Err(AddToPathError::NonZeroExitCode.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum AddToPathError {
+  #[error("unexpected error, 'espanso env-path register' returned a non-zero exit code.")]
+  NonZeroExitCode,
+}
+
+pub fn show_already_running_warning() -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["modulo", "welcome", "--already-running"]);
+
+  let mut child = command.spawn()?;
+  child.wait()?;
+  Ok(())
+}
+
+pub fn configure_auto_start(auto_start: bool) -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  let mut args = vec!["service"];
+  if auto_start {
+    args.push("register");
+  } else {
+    args.push("unregister");
+  }
+
+  command.args(&args);
+  set_command_flags(&mut command);
+
+  let mut child = command.spawn()?;
+  let result = child.wait()?;
+
+  if result.success() {
+    Ok(())
+  } else {
+    bail!("service registration returned non-zero exit code");
+  }
+}
diff --git a/espanso/src/cli/log.rs b/espanso/src/cli/log.rs
new file mode 100644
index 00000000..93b6bcee
--- /dev/null
+++ b/espanso/src/cli/log.rs
@@ -0,0 +1,55 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::io::BufRead;
+use std::{fs::File, io::BufReader};
+
+use super::{CliModule, CliModuleArgs};
+
+pub fn new() -> CliModule {
+  CliModule {
+    requires_paths: true,
+    subcommand: "log".to_string(),
+    entry: log_main,
+    ..Default::default()
+  }
+}
+
+fn log_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths argument");
+  let log_file = paths.runtime.join(crate::LOG_FILE_NAME);
+
+  if !log_file.exists() {
+    eprintln!("No log file found.");
+    return 2;
+  }
+
+  let log_file = File::open(log_file);
+  if let Ok(log_file) = log_file {
+    let reader = BufReader::new(log_file);
+    for line in reader.lines().flatten() {
+      println!("{}", line);
+    }
+  } else {
+    eprintln!("Error reading log file");
+    return 1;
+  }
+
+  0
+}
diff --git a/espanso/src/cli/migrate.rs b/espanso/src/cli/migrate.rs
new file mode 100644
index 00000000..22ddb8ff
--- /dev/null
+++ b/espanso/src/cli/migrate.rs
@@ -0,0 +1,195 @@
+/*
+ * 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::path::PathBuf;
+
+use crate::{
+  exit_code::{
+    configure_custom_panic_hook, update_panic_exit_code, MIGRATE_ALREADY_NEW_FORMAT,
+    MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE, MIGRATE_LEGACY_INSTANCE_RUNNING, MIGRATE_SUCCESS,
+    MIGRATE_USER_ABORTED,
+  },
+  lock::acquire_legacy_lock,
+};
+
+use super::{CliModule, CliModuleArgs};
+use colored::*;
+use dialoguer::Confirm;
+use fs_extra::dir::CopyOptions;
+use log::{error, info};
+use tempdir::TempDir;
+
+pub fn new() -> CliModule {
+  CliModule {
+    enable_logs: true,
+    disable_logs_terminal_output: true,
+    requires_paths: true,
+    requires_config: true,
+    log_mode: super::LogMode::AppendOnly,
+    subcommand: "migrate".to_string(),
+    entry: migrate_main,
+    ..Default::default()
+  }
+}
+
+fn migrate_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args");
+
+  info!("--- MIGRATION STARTED ---");
+
+  configure_custom_panic_hook();
+
+  if !args.is_legacy_config {
+    error_print_and_log("Can't migrate configurations, as the default directory [1] is already encoded with the new format");
+    error_print_and_log(&format!("[1]: {:?}", paths.config));
+    error_print_and_log("The migration tool is only meant to convert the espanso's legacy configuration format (prior to");
+    error_print_and_log("version 0.7.3) to the new one (since version 2.0)");
+    return MIGRATE_ALREADY_NEW_FORMAT;
+  }
+
+  let legacy_lock_file = acquire_legacy_lock(&paths.runtime);
+  if legacy_lock_file.is_none() {
+    error_print_and_log("An instance of legacy espanso is running, please terminate it, otherwise the migration can't be performed");
+    return MIGRATE_LEGACY_INSTANCE_RUNNING;
+  }
+
+  let target_backup_dir = find_available_backup_dir();
+
+  println!("\n{}\n", "Welcome to espanso v2!".bold());
+  println!("This migration tool will help you to smoothly transition to the new espanso v2 configuration format.");
+  println!();
+  println!(
+    "1. Firstly, espanso will {} your current configuration, located in:\n",
+    "backup".green().bold()
+  );
+  println!("   {}\n", paths.config.to_string_lossy().italic());
+  println!("   into this folder:\n");
+  println!("   {}\n", target_backup_dir.to_string_lossy().italic());
+  println!(
+    "2. Then, it will {} your configuration to the new format, replacing",
+    "convert".bold().green()
+  );
+  println!("   the current content of the config directory.");
+  println!();
+
+  info!(
+    "backing up the configuration directory: '{}'",
+    paths.config.to_string_lossy()
+  );
+  info!(
+    " -> into this folder: '{}'",
+    target_backup_dir.to_string_lossy()
+  );
+
+  if !cli_args.is_present("noconfirm")
+    && !Confirm::new()
+      .with_prompt("Do you want to proceed?")
+      .default(true)
+      .interact()
+      .expect("unable to read choice")
+  {
+    return MIGRATE_USER_ABORTED;
+  }
+
+  println!("Backing up your configuration...");
+  update_panic_exit_code(MIGRATE_CLEAN_FAILURE);
+
+  fs_extra::dir::copy(
+    &paths.config,
+    &target_backup_dir,
+    &CopyOptions {
+      copy_inside: true,
+      ..Default::default()
+    },
+  )
+  .expect("unable to backup the current config");
+  println!("{}", "Backup completed!".green());
+  info!("backup completed!");
+
+  println!("Converting the configuration...");
+  info!("converting the configuration...");
+  let temp_dir = TempDir::new("espanso-migrate-out").expect("unable to create temporary directory");
+  let temp_out_dir = temp_dir.path().join("out");
+  espanso_migrate::migrate(&paths.config, &paths.packages, &temp_out_dir)
+    .expect("an error occurred while converting the configuration");
+  println!("{}", "Conversion completed!".green());
+  info!("conversion completed!");
+
+  println!("Replacing old configuration with the new one...");
+  info!("replacing old configuration with the new one...");
+  update_panic_exit_code(MIGRATE_DIRTY_FAILURE);
+
+  let mut to_be_removed = Vec::new();
+  let legacy_dir_content =
+    fs_extra::dir::get_dir_content(&paths.config).expect("unable to list legacy dir files");
+  to_be_removed.extend(legacy_dir_content.files);
+  to_be_removed.extend(legacy_dir_content.directories);
+
+  // Skip the config directory itself to preserve the symbolic link (if present)
+  let config_dir_as_str = paths.config.to_string_lossy().to_string();
+  to_be_removed.retain(|path| path != &config_dir_as_str);
+
+  fs_extra::remove_items(&to_be_removed).expect("unable to remove previous configuration");
+  fs_extra::dir::copy(
+    &temp_out_dir,
+    &paths.config,
+    &CopyOptions {
+      copy_inside: true,
+      content_only: true,
+      ..Default::default()
+    },
+  )
+  .expect("unable to copy new configuration into target location");
+
+  let target_packages_dir = &paths.config.join("match").join("packages");
+  if !target_packages_dir.is_dir() {
+    std::fs::create_dir_all(target_packages_dir).expect("unable to create new packages directory");
+  }
+
+  println!("{}", "Configuration successfully migrated!".green());
+  info!("configuration migrated!");
+
+  MIGRATE_SUCCESS
+}
+
+fn find_available_backup_dir() -> PathBuf {
+  for i in 1..20 {
+    let num = if i > 1 {
+      format!("-{}", i)
+    } else {
+      "".to_string()
+    };
+
+    let target_backup_dir = dirs::document_dir()
+      .expect("unable to generate backup directory")
+      .join(format!("espanso-migrate-backup{}", num));
+
+    if !target_backup_dir.is_dir() {
+      return target_backup_dir;
+    }
+  }
+
+  panic!("could not generate valid backup directory");
+}
+
+fn error_print_and_log(msg: &str) {
+  error!("{}", msg);
+  eprintln!("{}", msg);
+}
diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs
new file mode 100644
index 00000000..2cdba944
--- /dev/null
+++ b/espanso/src/cli/mod.rs
@@ -0,0 +1,108 @@
+/*
+ * 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::path::PathBuf;
+
+use clap::ArgMatches;
+use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::store::MatchStore};
+use espanso_path::Paths;
+
+pub mod daemon;
+pub mod edit;
+pub mod env_path;
+pub mod launcher;
+pub mod log;
+pub mod migrate;
+pub mod modulo;
+pub mod package;
+pub mod path;
+pub mod service;
+pub mod util;
+pub mod workaround;
+pub mod worker;
+
+pub struct CliModule {
+  pub enable_logs: bool,
+  pub disable_logs_terminal_output: bool,
+  pub log_mode: LogMode,
+  pub requires_paths: bool,
+  pub requires_config: bool,
+  pub subcommand: String,
+  pub show_in_dock: bool,
+  pub requires_linux_capabilities: bool,
+  pub entry: fn(CliModuleArgs) -> i32,
+}
+
+impl Default for CliModule {
+  fn default() -> Self {
+    Self {
+      enable_logs: false,
+      log_mode: LogMode::Read,
+      disable_logs_terminal_output: false,
+      requires_paths: false,
+      requires_config: false,
+      subcommand: "".to_string(),
+      show_in_dock: false,
+      requires_linux_capabilities: false,
+      entry: |_| 0,
+    }
+  }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum LogMode {
+  Read,
+  AppendOnly,
+  CleanAndAppend,
+}
+
+pub struct CliModuleArgs {
+  pub config_store: Option<Box<dyn ConfigStore>>,
+  pub match_store: Option<Box<dyn MatchStore>>,
+  pub is_legacy_config: bool,
+  pub non_fatal_errors: Vec<NonFatalErrorSet>,
+  pub paths: Option<Paths>,
+  pub paths_overrides: Option<PathsOverrides>,
+  pub cli_args: Option<ArgMatches<'static>>,
+}
+
+impl Default for CliModuleArgs {
+  fn default() -> Self {
+    Self {
+      config_store: None,
+      match_store: None,
+      is_legacy_config: false,
+      non_fatal_errors: Vec::new(),
+      paths: None,
+      paths_overrides: None,
+      cli_args: None,
+    }
+  }
+}
+
+pub struct PathsOverrides {
+  pub config: Option<PathBuf>,
+  pub runtime: Option<PathBuf>,
+  pub packages: Option<PathBuf>,
+}
+
+pub struct CliAlias {
+  pub subcommand: String,
+  pub forward_into: String,
+}
diff --git a/espanso/src/cli/modulo/form.rs b/espanso/src/cli/modulo/form.rs
new file mode 100644
index 00000000..73e6a4ff
--- /dev/null
+++ b/espanso/src/cli/modulo/form.rs
@@ -0,0 +1,60 @@
+/*
+ * 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 crate::icon::IconPaths;
+use clap::ArgMatches;
+use espanso_modulo::form::*;
+
+pub fn form_main(matches: &ArgMatches, _icon_paths: &IconPaths) -> i32 {
+  let as_json: bool = matches.is_present("json");
+
+  let input_file = matches
+    .value_of("input_file")
+    .expect("missing input, please specify the -i option");
+  let data = if input_file == "-" {
+    use std::io::Read;
+    let mut buffer = String::new();
+    std::io::stdin()
+      .read_to_string(&mut buffer)
+      .expect("unable to obtain input from stdin");
+    buffer
+  } else {
+    std::fs::read_to_string(input_file).expect("unable to read input file")
+  };
+
+  let mut config: config::FormConfig = if !as_json {
+    serde_yaml::from_str(&data).expect("unable to parse form configuration")
+  } else {
+    serde_json::from_str(&data).expect("unable to parse form configuration")
+  };
+
+  // Overwrite the icon
+  config.icon = _icon_paths
+    .form_icon
+    .as_deref()
+    .map(|path| path.to_string_lossy().to_string());
+
+  let form = generator::generate(config);
+  let values = show(form);
+
+  let output = serde_json::to_string(&values).expect("unable to encode values as JSON");
+  println!("{}", output);
+
+  0
+}
diff --git a/espanso/src/cli/modulo/mod.rs b/espanso/src/cli/modulo/mod.rs
new file mode 100644
index 00000000..7954d50c
--- /dev/null
+++ b/espanso/src/cli/modulo/mod.rs
@@ -0,0 +1,70 @@
+/*
+ * 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 super::{CliModule, CliModuleArgs};
+
+#[cfg(feature = "modulo")]
+mod form;
+#[cfg(feature = "modulo")]
+mod search;
+#[cfg(feature = "modulo")]
+mod troubleshoot;
+#[cfg(feature = "modulo")]
+mod welcome;
+
+pub fn new() -> CliModule {
+  #[allow(clippy::needless_update)]
+  CliModule {
+    requires_paths: true,
+    enable_logs: false,
+    subcommand: "modulo".to_string(),
+    entry: modulo_main,
+    ..Default::default()
+  }
+}
+
+#[cfg(feature = "modulo")]
+fn modulo_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths in modulo main");
+  let cli_args = args.cli_args.expect("missing cli_args in modulo main");
+  let icon_paths = crate::icon::load_icon_paths(&paths.runtime).expect("unable to load icon paths");
+
+  if let Some(matches) = cli_args.subcommand_matches("form") {
+    return form::form_main(matches, &icon_paths);
+  }
+
+  if let Some(matches) = cli_args.subcommand_matches("search") {
+    return search::search_main(matches, &icon_paths);
+  }
+
+  if let Some(matches) = cli_args.subcommand_matches("welcome") {
+    return welcome::welcome_main(matches, &paths, &icon_paths);
+  }
+
+  if cli_args.subcommand_matches("troubleshoot").is_some() {
+    return troubleshoot::troubleshoot_main(&paths, &icon_paths);
+  }
+
+  0
+}
+
+#[cfg(not(feature = "modulo"))]
+fn modulo_main(_: CliModuleArgs) -> i32 {
+  panic!("this version of espanso was not compiled with 'modulo' support, please obtain a version that does or recompile it with the 'modulo' feature flag");
+}
diff --git a/espanso/src/cli/modulo/search.rs b/espanso/src/cli/modulo/search.rs
new file mode 100644
index 00000000..a8d7f519
--- /dev/null
+++ b/espanso/src/cli/modulo/search.rs
@@ -0,0 +1,65 @@
+/*
+ * 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 crate::icon::IconPaths;
+use clap::ArgMatches;
+use espanso_modulo::search::*;
+use std::collections::HashMap;
+
+pub fn search_main(matches: &ArgMatches, icon_paths: &IconPaths) -> i32 {
+  let as_json: bool = matches.is_present("json");
+
+  let input_file = matches
+    .value_of("input_file")
+    .expect("missing input, please specify the -i option");
+  let data = if input_file == "-" {
+    use std::io::Read;
+    let mut buffer = String::new();
+    std::io::stdin()
+      .read_to_string(&mut buffer)
+      .expect("unable to obtain input from stdin");
+    buffer
+  } else {
+    std::fs::read_to_string(input_file).expect("unable to read input file")
+  };
+
+  let mut config: config::SearchConfig = if !as_json {
+    serde_yaml::from_str(&data).expect("unable to parse search configuration")
+  } else {
+    serde_json::from_str(&data).expect("unable to parse search configuration")
+  };
+
+  // Overwrite the icon
+  config.icon = icon_paths
+    .logo
+    .as_deref()
+    .map(|path| path.to_string_lossy().to_string());
+
+  let algorithm = algorithm::get_algorithm(&config.algorithm, true);
+
+  let search = generator::generate(config);
+  let result = show(search, algorithm);
+  let mut result_map = HashMap::new();
+  result_map.insert("selected", result);
+
+  let output = serde_json::to_string(&result_map).expect("unable to encode values as JSON");
+  println!("{}", output);
+
+  0
+}
diff --git a/espanso/src/cli/modulo/troubleshoot.rs b/espanso/src/cli/modulo/troubleshoot.rs
new file mode 100644
index 00000000..5a6f2626
--- /dev/null
+++ b/espanso/src/cli/modulo/troubleshoot.rs
@@ -0,0 +1,106 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2 file: (), errors: ()  file: (), errors: () 021 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::path::Path;
+
+use crate::icon::IconPaths;
+use crate::preferences::Preferences;
+use espanso_modulo::troubleshooting::{TroubleshootingHandlers, TroubleshootingOptions};
+use espanso_path::Paths;
+
+pub fn troubleshoot_main(paths: &Paths, icon_paths: &IconPaths) -> i32 {
+  let preferences =
+    crate::preferences::get_default(&paths.runtime).expect("unable to initialize preferences");
+
+  let dont_show_again_handler = Box::new(move |dont_show: bool| {
+    preferences.set_should_display_troubleshoot_for_non_fatal_errors(!dont_show);
+  });
+
+  let open_file_handler = Box::new(move |file_path: &Path| {
+    if let Err(err) = opener::open(file_path) {
+      eprintln!("error opening file: {}", err);
+    }
+  });
+
+  let (is_fatal_error, error_sets) =
+    match crate::config::load_config(&paths.config, &paths.packages) {
+      Ok(config_result) => {
+        let error_sets = config_result
+          .non_fatal_errors
+          .into_iter()
+          .map(|error_set| espanso_modulo::troubleshooting::ErrorSet {
+            file: Some(error_set.file),
+            errors: error_set
+              .errors
+              .into_iter()
+              .map(|error| espanso_modulo::troubleshooting::ErrorRecord {
+                level: match error.level {
+                  espanso_config::error::ErrorLevel::Error => {
+                    espanso_modulo::troubleshooting::ErrorLevel::Error
+                  }
+                  espanso_config::error::ErrorLevel::Warning => {
+                    espanso_modulo::troubleshooting::ErrorLevel::Warning
+                  }
+                },
+                message: format!("{:?}", error.error),
+              })
+              .collect(),
+          })
+          .collect();
+
+        (false, error_sets)
+      }
+      Err(err) => {
+        let message = format!("{:?}", err);
+        let file_path = if message.contains("default.yml") {
+          let default_file_path = paths.config.join("config").join("default.yml");
+          Some(default_file_path)
+        } else {
+          None
+        };
+
+        (
+          true,
+          vec![espanso_modulo::troubleshooting::ErrorSet {
+            file: file_path,
+            errors: vec![espanso_modulo::troubleshooting::ErrorRecord {
+              level: espanso_modulo::troubleshooting::ErrorLevel::Error,
+              message: format!("{:?}", err),
+            }],
+          }],
+        )
+      }
+    };
+
+  espanso_modulo::troubleshooting::show(TroubleshootingOptions {
+    window_icon_path: icon_paths
+      .wizard_icon
+      .as_ref()
+      .map(|path| path.to_string_lossy().to_string()),
+    error_sets,
+    is_fatal_error,
+    handlers: TroubleshootingHandlers {
+      dont_show_again_changed: Some(dont_show_again_handler),
+      open_file: Some(open_file_handler),
+    },
+  })
+  .expect("troubleshoot GUI returned error");
+
+  0
+}
diff --git a/espanso/src/cli/modulo/welcome.rs b/espanso/src/cli/modulo/welcome.rs
new file mode 100644
index 00000000..1a429224
--- /dev/null
+++ b/espanso/src/cli/modulo/welcome.rs
@@ -0,0 +1,49 @@
+/*
+ * 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 crate::icon::IconPaths;
+use clap::ArgMatches;
+use espanso_modulo::welcome::*;
+use espanso_path::Paths;
+
+pub fn welcome_main(matches: &ArgMatches, _: &Paths, icon_paths: &IconPaths) -> i32 {
+  let dont_show_again_handler = Box::new(move |_dont_show: bool| {
+    //preferences.set_should_display_welcome(!dont_show);
+    // TODO: this should probably be deleted if not used?
+  });
+
+  let is_already_running = matches.is_present("already-running");
+
+  espanso_modulo::welcome::show(WelcomeOptions {
+    window_icon_path: icon_paths
+      .wizard_icon
+      .as_ref()
+      .map(|path| path.to_string_lossy().to_string()),
+    tray_image_path: icon_paths
+      .tray_explain_image
+      .as_ref()
+      .map(|path| path.to_string_lossy().to_string()),
+    is_already_running,
+    handlers: WelcomeHandlers {
+      dont_show_again_changed: Some(dont_show_again_handler),
+    },
+  });
+
+  0
+}
diff --git a/espanso/src/cli/package/install.rs b/espanso/src/cli/package/install.rs
new file mode 100644
index 00000000..a2f71204
--- /dev/null
+++ b/espanso/src/cli/package/install.rs
@@ -0,0 +1,121 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{anyhow, bail, Context, Result};
+use clap::ArgMatches;
+use espanso_package::{PackageSpecifier, ProviderOptions, SaveOptions};
+use espanso_path::Paths;
+
+use crate::{error_eprintln, info_println};
+
+pub fn install_package(paths: &Paths, matches: &ArgMatches) -> Result<()> {
+  let package_name = matches
+    .value_of("package_name")
+    .ok_or_else(|| anyhow!("missing package name"))?;
+  let version = matches.value_of("version");
+  let force = matches.is_present("force");
+  let refresh_index = matches.is_present("refresh-index");
+  let external = matches.is_present("external");
+
+  info_println!(
+    "installing package: {} - version: {}",
+    package_name,
+    version.unwrap_or("latest")
+  );
+
+  let (package_specifier, requires_external) = if let Some(git_repo) = matches.value_of("git") {
+    let git_branch = matches.value_of("git-branch");
+    let use_native_git = matches.is_present("use-native-git");
+
+    (
+      PackageSpecifier {
+        name: package_name.to_string(),
+        version: version.map(String::from),
+        git_repo_url: Some(git_repo.to_string()),
+        git_branch: git_branch.map(String::from),
+        use_native_git,
+      },
+      true,
+    )
+  } else {
+    // Install from the hub
+
+    (
+      PackageSpecifier {
+        name: package_name.to_string(),
+        version: version.map(String::from),
+        ..Default::default()
+      },
+      false,
+    )
+  };
+
+  if requires_external && !external {
+    error_eprintln!("Error: the requested package is hosted on an external repository");
+    error_eprintln!("and its contents may not have been verified by the espanso team.");
+    error_eprintln!("");
+    error_eprintln!(
+      "For security reasons, espanso blocks packages that are not verified by default."
+    );
+    error_eprintln!(
+      "If you want to install the package anyway, you can proceed with the installation"
+    );
+    error_eprintln!("by passing the '--external' flag, but please do it only if you trust the");
+    error_eprintln!("source or you verified the contents of the package yourself.");
+    error_eprintln!("");
+
+    bail!("installing from external repository without --external flag");
+  }
+
+  let package_provider = espanso_package::get_provider(
+    &package_specifier,
+    &paths.runtime,
+    &ProviderOptions {
+      force_index_update: refresh_index,
+    },
+  )
+  .context("unable to obtain compatible package provider")?;
+
+  info_println!("using package provider: {}", package_provider.name());
+
+  let package = package_provider.download(&package_specifier)?;
+
+  info_println!(
+    "found package: {} - version: {}",
+    package.name(),
+    package.version()
+  );
+
+  let archiver =
+    espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?;
+
+  archiver
+    .save(
+      &*package,
+      &package_specifier,
+      &SaveOptions {
+        overwrite_existing: force,
+      },
+    )
+    .context("unable to save package")?;
+
+  info_println!("package installed!");
+
+  Ok(())
+}
diff --git a/espanso/src/cli/package/list.rs b/espanso/src/cli/package/list.rs
new file mode 100644
index 00000000..9c1fe387
--- /dev/null
+++ b/espanso/src/cli/package/list.rs
@@ -0,0 +1,58 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{Context, Result};
+use clap::ArgMatches;
+use espanso_package::StoredPackage;
+use espanso_path::Paths;
+
+use crate::info_println;
+
+pub fn list_packages(paths: &Paths, _: &ArgMatches) -> Result<()> {
+  let archiver =
+    espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?;
+
+  let packages = archiver.list().context("unable to list packages")?;
+
+  if packages.is_empty() {
+    info_println!("No packages found!");
+    return Ok(());
+  }
+
+  info_println!("Installed packages:");
+  info_println!("");
+
+  for package in packages {
+    match package {
+      StoredPackage::Legacy(legacy) => {
+        info_println!("- {} (legacy)", legacy.name);
+      }
+      StoredPackage::Modern(package) => {
+        info_println!(
+          "- {} - version: {} ({})",
+          package.manifest.name,
+          package.manifest.version,
+          package.source
+        );
+      }
+    }
+  }
+
+  Ok(())
+}
diff --git a/espanso/src/cli/package/mod.rs b/espanso/src/cli/package/mod.rs
new file mode 100644
index 00000000..67d3dfc7
--- /dev/null
+++ b/espanso/src/cli/package/mod.rs
@@ -0,0 +1,83 @@
+/*
+ * 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 crate::{
+  error_eprintln,
+  exit_code::{
+    configure_custom_panic_hook, PACKAGE_INSTALL_FAILED, PACKAGE_LIST_FAILED, PACKAGE_SUCCESS,
+    PACKAGE_UNINSTALL_FAILED, PACKAGE_UPDATE_FAILED, PACKAGE_UPDATE_PARTIAL_FAILURE,
+  },
+};
+
+use super::{CliModule, CliModuleArgs};
+
+mod install;
+mod list;
+mod uninstall;
+mod update;
+
+pub fn new() -> CliModule {
+  CliModule {
+    enable_logs: true,
+    disable_logs_terminal_output: true,
+    requires_paths: true,
+    subcommand: "package".to_string(),
+    log_mode: super::LogMode::AppendOnly,
+    entry: package_main,
+    ..Default::default()
+  }
+}
+
+fn package_main(args: CliModuleArgs) -> i32 {
+  configure_custom_panic_hook();
+
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args");
+
+  if let Some(sub_matches) = cli_args.subcommand_matches("install") {
+    if let Err(err) = install::install_package(&paths, sub_matches) {
+      error_eprintln!("unable to install package: {:?}", err);
+      return PACKAGE_INSTALL_FAILED;
+    }
+  } else if let Some(sub_matches) = cli_args.subcommand_matches("uninstall") {
+    if let Err(err) = uninstall::uninstall_package(&paths, sub_matches) {
+      error_eprintln!("unable to uninstall package: {:?}", err);
+      return PACKAGE_UNINSTALL_FAILED;
+    }
+  } else if let Some(sub_matches) = cli_args.subcommand_matches("list") {
+    if let Err(err) = list::list_packages(&paths, sub_matches) {
+      error_eprintln!("unable to list packages: {:?}", err);
+      return PACKAGE_LIST_FAILED;
+    }
+  } else if let Some(sub_matches) = cli_args.subcommand_matches("update") {
+    match update::update_package(&paths, sub_matches) {
+      Ok(update::UpdateResults::PartialFailure) => {
+        error_eprintln!("some packages were updated, but not all of them. Check the previous log for more information");
+        return PACKAGE_UPDATE_PARTIAL_FAILURE;
+      }
+      Err(err) => {
+        error_eprintln!("unable to update package: {:?}", err);
+        return PACKAGE_UPDATE_FAILED;
+      }
+      _ => {}
+    }
+  }
+
+  PACKAGE_SUCCESS
+}
diff --git a/espanso/src/cli/package/uninstall.rs b/espanso/src/cli/package/uninstall.rs
new file mode 100644
index 00000000..07519916
--- /dev/null
+++ b/espanso/src/cli/package/uninstall.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{anyhow, Context, Result};
+use clap::ArgMatches;
+use espanso_path::Paths;
+
+use crate::info_println;
+
+pub fn uninstall_package(paths: &Paths, matches: &ArgMatches) -> Result<()> {
+  let package_name = matches
+    .value_of("package_name")
+    .ok_or_else(|| anyhow!("missing package name"))?;
+
+  let archiver =
+    espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?;
+
+  archiver
+    .delete(package_name)
+    .context("unable to delete package")?;
+
+  info_println!("package '{}' uninstalled!", package_name);
+
+  Ok(())
+}
diff --git a/espanso/src/cli/package/update.rs b/espanso/src/cli/package/update.rs
new file mode 100644
index 00000000..7646dac1
--- /dev/null
+++ b/espanso/src/cli/package/update.rs
@@ -0,0 +1,132 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{anyhow, Context, Result};
+use clap::ArgMatches;
+use espanso_package::{Archiver, PackageSpecifier, ProviderOptions, SaveOptions, StoredPackage};
+use espanso_path::Paths;
+
+use crate::{error_eprintln, info_println, warn_eprintln};
+
+pub enum UpdateResults {
+  Success,
+  PartialFailure,
+}
+
+pub fn update_package(paths: &Paths, matches: &ArgMatches) -> Result<UpdateResults> {
+  let package_name = matches
+    .value_of("package_name")
+    .ok_or_else(|| anyhow!("missing package name"))?;
+
+  let archiver =
+    espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?;
+
+  let packages_to_update = if package_name == "all" {
+    let packages = archiver.list()?;
+    info_println!("updating {} packages", packages.len());
+
+    packages
+      .into_iter()
+      .map(|package| match package {
+        StoredPackage::Legacy(legacy) => legacy.name,
+        StoredPackage::Modern(modern) => modern.manifest.name,
+      })
+      .collect()
+  } else {
+    vec![package_name.to_owned()]
+  };
+
+  let mut update_errors = Vec::new();
+
+  for package_name in &packages_to_update {
+    if let Err(err) = perform_package_update(paths, &*archiver, package_name) {
+      error_eprintln!("error updating package '{}': {:?}", package_name, err);
+      update_errors.push(err);
+    }
+  }
+
+  if update_errors.is_empty() {
+    Ok(UpdateResults::Success)
+  } else if packages_to_update.len() == update_errors.len() {
+    Err(update_errors.pop().expect("unable to extract error"))
+  } else {
+    Ok(UpdateResults::PartialFailure)
+  }
+}
+
+fn perform_package_update(
+  paths: &Paths,
+  archiver: &dyn Archiver,
+  package_name: &str,
+) -> Result<()> {
+  info_println!("updating package: {}", package_name);
+
+  let package = archiver.get(package_name)?;
+
+  let (package_specifier, old_version) = match package {
+    StoredPackage::Legacy(legacy) => {
+      warn_eprintln!(
+        "detected legacy package '{}' without source information, pulling from espanso hub.",
+        legacy.name
+      );
+      (
+        PackageSpecifier {
+          name: legacy.name,
+          ..Default::default()
+        },
+        None,
+      )
+    }
+    StoredPackage::Modern(modern) => ((&modern).into(), Some(modern.manifest.version)),
+  };
+
+  let package_provider = espanso_package::get_provider(
+    &package_specifier,
+    &paths.runtime,
+    &ProviderOptions::default(),
+  )
+  .context("unable to obtain compatible package provider")?;
+
+  info_println!("using package provider: {}", package_provider.name());
+
+  let new_package = package_provider.download(&package_specifier)?;
+
+  if new_package.version() == old_version.unwrap_or_default() {
+    info_println!("already up to date!");
+    return Ok(());
+  }
+
+  archiver
+    .save(
+      &*new_package,
+      &package_specifier,
+      &SaveOptions {
+        overwrite_existing: true,
+      },
+    )
+    .context("unable to save package")?;
+
+  info_println!(
+    "updated package '{}' to version: {}",
+    new_package.name(),
+    new_package.version()
+  );
+
+  Ok(())
+}
diff --git a/espanso/src/cli/path.rs b/espanso/src/cli/path.rs
new file mode 100644
index 00000000..d4c31407
--- /dev/null
+++ b/espanso/src/cli/path.rs
@@ -0,0 +1,77 @@
+/*
+ * 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 super::{CliModule, CliModuleArgs};
+
+pub fn new() -> CliModule {
+  CliModule {
+    requires_paths: true,
+    requires_config: true,
+    subcommand: "path".to_string(),
+    entry: path_main,
+    ..Default::default()
+  }
+}
+
+fn path_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args argument");
+
+  if cli_args.subcommand_matches("config").is_some() {
+    println!("{}", paths.config.to_string_lossy());
+  } else if cli_args.subcommand_matches("packages").is_some() {
+    println!("{}", paths.packages.to_string_lossy());
+  } else if cli_args.subcommand_matches("data").is_some()
+    || cli_args.subcommand_matches("runtime").is_some()
+  {
+    println!("{}", paths.runtime.to_string_lossy());
+  } else if cli_args.subcommand_matches("default").is_some() {
+    if args.is_legacy_config {
+      println!("{}", paths.config.join("default.yml").to_string_lossy());
+    } else {
+      println!(
+        "{}",
+        paths
+          .config
+          .join("config")
+          .join("default.yml")
+          .to_string_lossy()
+      );
+    }
+  } else if cli_args.subcommand_matches("base").is_some() {
+    if args.is_legacy_config {
+      eprintln!("base config not available when using legacy configuration format");
+    } else {
+      println!(
+        "{}",
+        paths
+          .config
+          .join("match")
+          .join("base.yml")
+          .to_string_lossy()
+      );
+    }
+  } else {
+    println!("Config: {}", paths.config.to_string_lossy());
+    println!("Packages: {}", paths.packages.to_string_lossy());
+    println!("Runtime: {}", paths.runtime.to_string_lossy());
+  }
+
+  0
+}
diff --git a/espanso/src/cli/service/linux.rs b/espanso/src/cli/service/linux.rs
new file mode 100644
index 00000000..55ce07e1
--- /dev/null
+++ b/espanso/src/cli/service/linux.rs
@@ -0,0 +1,284 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use const_format::formatcp;
+use regex::Regex;
+use std::fs::create_dir_all;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use thiserror::Error;
+
+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");
+const LINUX_SERVICE_FILENAME: &str = formatcp!("{}.service", LINUX_SERVICE_NAME);
+
+pub fn register() -> Result<()> {
+  let service_file = get_service_file_path()?;
+
+  if service_file.exists() {
+    warn_eprintln!("service file already exists, this operation will overwrite it");
+  }
+
+  info_println!("creating service file in {:?}", service_file);
+  let espanso_path = get_binary_path().expect("unable to get espanso executable path");
+
+  let service_content = String::from(LINUX_SERVICE_CONTENT).replace(
+    "{{{espanso_path}}}",
+    &espanso_path.to_string_lossy().to_string(),
+  );
+
+  std::fs::write(service_file, service_content)?;
+
+  info_println!("enabling systemd service");
+
+  match Command::new("systemctl")
+    .args(&["--user", "enable", LINUX_SERVICE_NAME])
+    .status()
+  {
+    Ok(status) => {
+      if !status.success() {
+        return Err(RegisterError::SystemdEnableFailed.into());
+      }
+    }
+    Err(err) => {
+      error_eprintln!("unable to call systemctl: {}", err);
+      return Err(RegisterError::SystemdCallFailed(err.into()).into());
+    }
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum RegisterError {
+  #[error("systemctl command failed `{0}`")]
+  SystemdCallFailed(anyhow::Error),
+
+  #[error("systemctl enable failed")]
+  SystemdEnableFailed,
+}
+
+pub fn unregister() -> Result<()> {
+  let service_file = get_service_file_path()?;
+
+  if !service_file.exists() {
+    return Err(UnregisterError::ServiceFileNotFound.into());
+  }
+
+  info_println!("disabling espanso systemd service");
+
+  match Command::new("systemctl")
+    .args(&["--user", "disable", LINUX_SERVICE_NAME])
+    .status()
+  {
+    Ok(status) => {
+      if !status.success() {
+        return Err(UnregisterError::SystemdDisableFailed.into());
+      }
+    }
+    Err(err) => {
+      error_eprintln!("unable to call systemctl: {}", err);
+      return Err(UnregisterError::SystemdCallFailed(err.into()).into());
+    }
+  }
+
+  info_println!("deleting espanso systemd entry");
+  std::fs::remove_file(service_file)?;
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum UnregisterError {
+  #[error("service file not found")]
+  ServiceFileNotFound,
+
+  #[error("failed to disable systemd service")]
+  SystemdDisableFailed,
+
+  #[error("systemctl command failed `{0}`")]
+  SystemdCallFailed(anyhow::Error),
+}
+
+pub fn is_registered() -> bool {
+  let res = Command::new("systemctl")
+    .args(&["--user", "is-enabled", LINUX_SERVICE_NAME])
+    .output();
+  if let Ok(output) = res {
+    if !output.status.success() {
+      return false;
+    }
+
+    // Make sure the systemd service points to the right binary
+    lazy_static! {
+      static ref EXEC_PATH_REGEX: Regex = Regex::new("ExecStart=(?P<path>.*?)\\s").unwrap();
+    }
+
+    match Command::new("systemctl")
+      .args(&["--user", "cat", LINUX_SERVICE_NAME])
+      .output()
+    {
+      Ok(cmd_output) => {
+        let output = String::from_utf8_lossy(cmd_output.stdout.as_slice());
+        let output = output.trim();
+        if cmd_output.status.success() {
+          let caps = EXEC_PATH_REGEX.captures(output).unwrap();
+          let path = caps.get(1).map_or("", |m| m.as_str());
+          let espanso_path = get_binary_path().expect("unable to get espanso executable path");
+
+          if espanso_path.to_string_lossy() != path {
+            error_eprintln!("Espanso is registered as a systemd service, but it points to another binary location:");
+            error_eprintln!("");
+            error_eprintln!("  {}", path);
+            error_eprintln!("");
+            error_eprintln!("This could have been caused by an update that changed its location.");
+            error_eprintln!("To solve the problem, please unregister and register espanso again with these commands:");
+            error_eprintln!("");
+            error_eprintln!("  espanso service unregister && espanso service register");
+            error_eprintln!("");
+
+            false
+          } else {
+            true
+          }
+        } else {
+          error_eprintln!("systemctl command returned non-zero exit code");
+          false
+        }
+      }
+      Err(err) => {
+        error_eprintln!("failed to execute systemctl: {}", err);
+        false
+      }
+    }
+  } else {
+    false
+  }
+}
+
+pub fn start_service() -> Result<()> {
+  // Check if systemd is available in the system
+  match Command::new("systemctl")
+    .args(&["--version"])
+    .stdin(Stdio::null())
+    .stdout(Stdio::null())
+    .status()
+  {
+    Ok(status) => {
+      if !status.success() {
+        return Err(StartError::SystemctlNonZeroExitCode.into());
+      }
+    }
+    Err(_) => {
+      error_eprintln!(
+        "Systemd was not found in this system, which means espanso can't run in managed mode"
+      );
+      error_eprintln!("You can run it in unmanaged mode with `espanso service start --unmanaged`");
+      error_eprintln!("");
+      error_eprintln!(
+        "NOTE: unmanaged mode means espanso does not rely on the system service manager"
+      );
+      error_eprintln!(
+        "      to run, but as a result, you are in charge of starting/stopping espanso"
+      );
+      error_eprintln!("      when needed.");
+      return Err(StartError::SystemdNotFound.into());
+    }
+  }
+
+  if !is_registered() {
+    error_eprintln!("Unable to start espanso as a service as it's not been registered.");
+    error_eprintln!("You can either register it first with `espanso service register` or");
+    error_eprintln!("you can run it in unmanaged mode with `espanso service start --unmanaged`");
+    error_eprintln!("");
+    error_eprintln!(
+      "NOTE: unmanaged mode means espanso does not rely on the system service manager"
+    );
+    error_eprintln!(
+      "      to run, but as a result, you are in charge of starting/stopping espanso"
+    );
+    error_eprintln!("      when needed.");
+    return Err(StartError::NotRegistered.into());
+  }
+
+  match Command::new("systemctl")
+    .args(&["--user", "start", LINUX_SERVICE_NAME])
+    .status()
+  {
+    Ok(status) => {
+      if !status.success() {
+        return Err(StartError::SystemctlStartFailed.into());
+      }
+    }
+    Err(err) => {
+      return Err(StartError::SystemctlFailed(err.into()).into());
+    }
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum StartError {
+  #[error("not registered as a service")]
+  NotRegistered,
+
+  #[error("systemd not found")]
+  SystemdNotFound,
+
+  #[error("failed to start systemctl: `{0}`")]
+  SystemctlFailed(anyhow::Error),
+
+  #[error("systemctl non-zero exit code")]
+  SystemctlNonZeroExitCode,
+
+  #[error("failed to launch espanso service through systemctl")]
+  SystemctlStartFailed,
+}
+
+fn get_service_file_dir() -> Result<PathBuf> {
+  // User level systemd services should be placed in this directory:
+  // $XDG_CONFIG_HOME/systemd/user/, usually: ~/.config/systemd/user/
+  let config_dir = dirs::config_dir().expect("Could not get configuration directory");
+  let systemd_dir = config_dir.join("systemd");
+  let user_dir = systemd_dir.join("user");
+
+  if !user_dir.is_dir() {
+    create_dir_all(&user_dir)?;
+  }
+
+  Ok(user_dir)
+}
+
+fn get_service_file_path() -> Result<PathBuf> {
+  Ok(get_service_file_dir()?.join(LINUX_SERVICE_FILENAME))
+}
+
+fn get_binary_path() -> Result<PathBuf> {
+  // If executed as part of an AppImage, get the app image path instead of
+  // the binary itself (which was extracted in a temp directory).
+  if let Some(app_image_path) = std::env::var_os("APPIMAGE") {
+    return Ok(PathBuf::from(app_image_path));
+  }
+
+  Ok(std::env::current_exe()?)
+}
diff --git a/espanso/src/cli/service/macos.rs b/espanso/src/cli/service/macos.rs
new file mode 100644
index 00000000..f6914f5a
--- /dev/null
+++ b/espanso/src/cli/service/macos.rs
@@ -0,0 +1,166 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::{info, warn};
+use std::process::Command;
+use std::{fs::create_dir_all, process::ExitStatus};
+use thiserror::Error;
+
+#[cfg(target_os = "macos")]
+const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist");
+#[cfg(target_os = "macos")]
+const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist";
+
+pub fn register() -> Result<()> {
+  let home_dir = dirs::home_dir().expect("could not get user home directory");
+  let library_dir = home_dir.join("Library");
+  let agents_dir = library_dir.join("LaunchAgents");
+
+  // Make sure agents directory exists
+  if !agents_dir.exists() {
+    create_dir_all(agents_dir.clone())?;
+  }
+
+  let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME);
+  if !plist_file.exists() {
+    info!(
+      "creating LaunchAgents entry: {}",
+      plist_file.to_str().unwrap_or_default()
+    );
+
+    let espanso_path = std::env::current_exe()?;
+    info!(
+      "entry will point to: {}",
+      espanso_path.to_str().unwrap_or_default()
+    );
+
+    let plist_content = String::from(SERVICE_PLIST_CONTENT).replace(
+      "{{{espanso_path}}}",
+      espanso_path.to_str().unwrap_or_default(),
+    );
+
+    // Copy the user PATH variable and inject it in the Plist file so that
+    // it gets loaded by Launchd.
+    // To see why this is necessary: https://github.com/federico-terzi/espanso/issues/233
+    let user_path = std::env::var("PATH").unwrap_or_else(|_| "".to_owned());
+    let plist_content = plist_content.replace("{{{PATH}}}", &user_path);
+
+    std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file");
+  }
+
+  info!("reloading espanso launchctl entry");
+
+  if let Err(err) = Command::new("launchctl")
+    .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()])
+    .output()
+  {
+    warn!("unload command failed: {}", err);
+  }
+
+  let res = Command::new("launchctl")
+    .args(&["load", "-w", plist_file.to_str().unwrap_or_default()])
+    .status();
+
+  if let Ok(status) = res {
+    if status.success() {
+      return Ok(());
+    }
+  }
+
+  Err(RegisterError::LaunchCtlLoadFailed.into())
+}
+
+#[derive(Error, Debug)]
+pub enum RegisterError {
+  #[error("launchctl load failed")]
+  LaunchCtlLoadFailed,
+}
+
+pub fn unregister() -> Result<()> {
+  let home_dir = dirs::home_dir().expect("could not get user home directory");
+  let library_dir = home_dir.join("Library");
+  let agents_dir = library_dir.join("LaunchAgents");
+
+  let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME);
+  if plist_file.exists() {
+    let _res = Command::new("launchctl")
+      .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()])
+      .output();
+
+    std::fs::remove_file(&plist_file)?;
+
+    Ok(())
+  } else {
+    Err(UnregisterError::PlistNotFound.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum UnregisterError {
+  #[error("plist entry not found")]
+  PlistNotFound,
+}
+
+pub fn is_registered() -> bool {
+  let home_dir = dirs::home_dir().expect("could not get user home directory");
+  let library_dir = home_dir.join("Library");
+  let agents_dir = library_dir.join("LaunchAgents");
+  let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME);
+  plist_file.is_file()
+}
+
+pub fn start_service() -> Result<()> {
+  if !is_registered() {
+    eprintln!("Unable to start espanso as a service as it's not been registered.");
+    eprintln!("You can either register it first with `espanso service register` or");
+    eprintln!("you can run it in unmanaged mode with `espanso service start --unmanaged`");
+    eprintln!();
+    eprintln!("NOTE: unmanaged mode means espanso does not rely on the system service manager");
+    eprintln!("      to run, but as a result, you are in charge of starting/stopping espanso");
+    eprintln!("      when needed.");
+    return Err(StartError::NotRegistered.into());
+  }
+
+  let res = Command::new("launchctl")
+    .args(&["start", "com.federicoterzi.espanso"])
+    .status();
+
+  if let Ok(status) = res {
+    if status.success() {
+      Ok(())
+    } else {
+      Err(StartError::LaunchCtlNonZeroExit(status).into())
+    }
+  } else {
+    Err(StartError::LaunchCtlFailure.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum StartError {
+  #[error("not registered as a service")]
+  NotRegistered,
+
+  #[error("launchctl failed to run")]
+  LaunchCtlFailure,
+
+  #[error("launchctl exited with non-zero code `{0}`")]
+  LaunchCtlNonZeroExit(ExitStatus),
+}
diff --git a/espanso/src/cli/service/mod.rs b/espanso/src/cli/service/mod.rs
new file mode 100644
index 00000000..0e710cb1
--- /dev/null
+++ b/espanso/src/cli/service/mod.rs
@@ -0,0 +1,154 @@
+/*
+ * 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 super::{CliModule, CliModuleArgs, PathsOverrides};
+use crate::{
+  error_eprintln,
+  exit_code::{
+    SERVICE_ALREADY_RUNNING, SERVICE_FAILURE, SERVICE_NOT_REGISTERED, SERVICE_NOT_RUNNING,
+    SERVICE_SUCCESS,
+  },
+  info_println,
+  lock::acquire_worker_lock,
+};
+
+#[cfg(target_os = "macos")]
+mod macos;
+use clap::ArgMatches;
+use espanso_path::Paths;
+#[cfg(target_os = "macos")]
+use macos::*;
+
+#[cfg(not(target_os = "windows"))]
+mod unix;
+#[cfg(not(target_os = "windows"))]
+use unix::*;
+
+#[cfg(target_os = "windows")]
+mod win;
+#[cfg(target_os = "windows")]
+use win::*;
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "linux")]
+use linux::*;
+
+mod stop;
+
+pub fn new() -> CliModule {
+  CliModule {
+    enable_logs: true,
+    disable_logs_terminal_output: true,
+    requires_paths: true,
+    subcommand: "service".to_string(),
+    log_mode: super::LogMode::AppendOnly,
+    entry: service_main,
+    ..Default::default()
+  }
+}
+
+fn service_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args");
+  #[allow(unused_variables)]
+  let paths_overrides = args.paths_overrides.expect("missing paths_overrides");
+
+  if cli_args.subcommand_matches("register").is_some() {
+    if let Err(err) = register() {
+      error_eprintln!("unable to register service: {}", err);
+      return SERVICE_FAILURE;
+    } else {
+      info_println!("service registered correctly!");
+    }
+  } else if cli_args.subcommand_matches("unregister").is_some() {
+    if let Err(err) = unregister() {
+      error_eprintln!("unable to unregister service: {}", err);
+      return SERVICE_FAILURE;
+    } else {
+      info_println!("service unregistered correctly!");
+    }
+  } else if cli_args.subcommand_matches("check").is_some() {
+    if is_registered() {
+      info_println!("registered as a service");
+    } else {
+      error_eprintln!("not registered as a service");
+      return SERVICE_NOT_REGISTERED;
+    }
+  } else if let Some(sub_args) = cli_args.subcommand_matches("start") {
+    return start_main(&paths, &paths_overrides, sub_args);
+  } else if cli_args.subcommand_matches("stop").is_some() {
+    return stop_main(&paths);
+  } else if let Some(sub_args) = cli_args.subcommand_matches("restart") {
+    stop_main(&paths);
+    return start_main(&paths, &paths_overrides, sub_args);
+  }
+
+  SERVICE_SUCCESS
+}
+
+fn start_main(paths: &Paths, _paths_overrides: &PathsOverrides, args: &ArgMatches) -> i32 {
+  let lock_file = acquire_worker_lock(&paths.runtime);
+  if lock_file.is_none() {
+    error_eprintln!("espanso is already running!");
+    return SERVICE_ALREADY_RUNNING;
+  }
+  drop(lock_file);
+
+  if args.is_present("unmanaged") && !cfg!(target_os = "windows") {
+    // Unmanaged service
+    #[cfg(unix)]
+    {
+      if let Err(err) = fork_daemon(_paths_overrides) {
+        error_eprintln!("unable to start service (unmanaged): {}", err);
+        return SERVICE_FAILURE;
+      }
+    }
+    #[cfg(windows)]
+    {
+      unreachable!();
+    }
+  } else {
+    // Managed service
+    if let Err(err) = start_service() {
+      error_eprintln!("unable to start service: {}", err);
+      return SERVICE_FAILURE;
+    } else {
+      info_println!("espanso started correctly!");
+    }
+  }
+
+  SERVICE_SUCCESS
+}
+
+fn stop_main(paths: &Paths) -> i32 {
+  let lock_file = acquire_worker_lock(&paths.runtime);
+  if lock_file.is_some() {
+    error_eprintln!("espanso is not running!");
+    return SERVICE_NOT_RUNNING;
+  }
+  drop(lock_file);
+
+  if let Err(err) = stop::terminate_worker(&paths.runtime) {
+    error_eprintln!("unable to stop espanso: {}", err);
+    return SERVICE_FAILURE;
+  }
+
+  SERVICE_SUCCESS
+}
diff --git a/espanso/src/cli/service/stop.rs b/espanso/src/cli/service/stop.rs
new file mode 100644
index 00000000..4a39efa3
--- /dev/null
+++ b/espanso/src/cli/service/stop.rs
@@ -0,0 +1,69 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{Error, Result};
+use log::error;
+use std::{path::Path, time::Instant};
+use thiserror::Error;
+
+use espanso_ipc::IPCClient;
+
+use crate::{
+  ipc::{create_ipc_client_to_worker, IPCEvent},
+  lock::acquire_worker_lock,
+};
+
+pub fn terminate_worker(runtime_dir: &Path) -> Result<()> {
+  match create_ipc_client_to_worker(runtime_dir) {
+    Ok(mut worker_ipc) => {
+      if let Err(err) = worker_ipc.send_async(IPCEvent::ExitAllProcesses) {
+        error!(
+          "unable to send termination signal to worker process: {}",
+          err
+        );
+        return Err(StopError::IPCError(err).into());
+      }
+    }
+    Err(err) => {
+      error!("could not establish IPC connection with worker: {}", err);
+      return Err(StopError::IPCError(err).into());
+    }
+  }
+
+  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(());
+    }
+
+    std::thread::sleep(std::time::Duration::from_millis(200));
+  }
+
+  Err(StopError::WorkerTimedOut.into())
+}
+
+#[derive(Error, Debug)]
+pub enum StopError {
+  #[error("worker timed out")]
+  WorkerTimedOut,
+
+  #[error("ipc error: `{0}`")]
+  IPCError(Error),
+}
diff --git a/espanso/src/cli/service/unix.rs b/espanso/src/cli/service/unix.rs
new file mode 100644
index 00000000..327001a9
--- /dev/null
+++ b/espanso/src/cli/service/unix.rs
@@ -0,0 +1,85 @@
+/*
+ * 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 crate::cli::util::CommandExt;
+use anyhow::Result;
+use thiserror::Error;
+
+use crate::cli::PathsOverrides;
+
+pub fn fork_daemon(paths_overrides: &PathsOverrides) -> Result<()> {
+  let pid = unsafe { libc::fork() };
+  if pid < 0 {
+    return Err(ForkError::ForkFailed.into());
+  }
+
+  if pid > 0 {
+    // Parent process
+    return Ok(());
+  }
+
+  // Spawned process
+
+  // Create a new SID for the child process
+  let sid = unsafe { libc::setsid() };
+  if sid < 0 {
+    return Err(ForkError::SetSidFailed.into());
+  }
+
+  // Detach stdout and stderr
+  let null_path = std::ffi::CString::new("/dev/null").expect("CString unwrap failed");
+  unsafe {
+    let fd = libc::open(null_path.as_ptr(), libc::O_RDWR, 0);
+    if fd != -1 {
+      libc::dup2(fd, libc::STDIN_FILENO);
+      libc::dup2(fd, libc::STDOUT_FILENO);
+      libc::dup2(fd, libc::STDERR_FILENO);
+    }
+  };
+
+  spawn_launcher(paths_overrides)
+}
+
+pub fn spawn_launcher(paths_overrides: &PathsOverrides) -> Result<()> {
+  let espanso_exe_path = std::env::current_exe()?;
+  let mut command = std::process::Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["launcher"]);
+  command.with_paths_overrides(paths_overrides);
+
+  let mut child = command.spawn()?;
+  let result = child.wait()?;
+
+  if result.success() {
+    Ok(())
+  } else {
+    Err(ForkError::LauncherSpawnFailure.into())
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ForkError {
+  #[error("unable to fork")]
+  ForkFailed,
+
+  #[error("setsid failed")]
+  SetSidFailed,
+
+  #[error("launcher spawn failure")]
+  LauncherSpawnFailure,
+}
diff --git a/espanso/src/cli/service/win.rs b/espanso/src/cli/service/win.rs
new file mode 100644
index 00000000..f41b3832
--- /dev/null
+++ b/espanso/src/cli/service/win.rs
@@ -0,0 +1,170 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::fs::create_dir_all;
+use std::os::windows::process::CommandExt;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use thiserror::Error;
+
+use crate::util::set_command_flags;
+use crate::{error_eprintln, warn_eprintln};
+
+pub fn register() -> Result<()> {
+  let current_path = std::env::current_exe().expect("unable to get exec path");
+
+  let shortcut_path = get_startup_shortcut_file()?;
+
+  create_shortcut_target_file(&shortcut_path, &current_path, "launcher")
+}
+
+pub fn unregister() -> Result<()> {
+  let shortcut_path = get_startup_shortcut_file()?;
+  if !shortcut_path.is_file() {
+    error_eprintln!("could not unregister espanso, as it's not registered");
+    return Err(UnregisterError::EntryNotFound.into());
+  }
+
+  std::fs::remove_file(shortcut_path)?;
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum UnregisterError {
+  #[error("entry not found")]
+  EntryNotFound,
+}
+
+pub fn is_registered() -> bool {
+  match get_startup_shortcut_file() {
+    Ok(shortcut_path) => {
+      if !shortcut_path.is_file() {
+        return false;
+      }
+
+      match get_shortcut_target_file(&shortcut_path) {
+        Ok(target_path) => {
+          // Check if the target file is the same as the current binary
+          let current_path = std::env::current_exe().expect("unable to get exec path");
+
+          if current_path != target_path {
+            warn_eprintln!("WARNING: Espanso is already registered as a service, but it points to another executable,");
+            warn_eprintln!("which can create some inconsistencies.");
+            warn_eprintln!(
+              "To fix the problem, unregister and register espanso again with these commands:"
+            );
+            warn_eprintln!("");
+            warn_eprintln!("   espanso service unregister");
+            warn_eprintln!("   espanso service register");
+            warn_eprintln!("");
+          }
+
+          true
+        }
+        Err(err) => {
+          error_eprintln!("unable to determine shortcut target path: {}", err);
+          false
+        }
+      }
+    }
+    Err(err) => {
+      error_eprintln!("could not locate shortcut file: {}", err);
+      false
+    }
+  }
+}
+
+pub fn start_service() -> Result<()> {
+  let current_path = std::env::current_exe().expect("unable to get exec path");
+
+  Command::new(current_path)
+    .args(&["launcher"])
+    .creation_flags(0x08000008) // CREATE_NO_WINDOW + DETACHED_PROCESS
+    .spawn()?;
+
+  Ok(())
+}
+
+fn get_startup_dir() -> Result<PathBuf> {
+  let home_dir = dirs::home_dir().expect("unable to obtain user's home folder");
+  let app_data = home_dir.join("AppData");
+  let roaming = app_data.join("Roaming");
+  let microsoft = roaming.join("Microsoft");
+  let windows = microsoft.join("Windows");
+  let start_menu = windows.join("Start Menu");
+  let programs = start_menu.join("Programs");
+  let startup = programs.join("Startup");
+
+  if !startup.is_dir() {
+    create_dir_all(&startup)?;
+  }
+
+  Ok(startup)
+}
+
+fn get_startup_shortcut_file() -> Result<PathBuf> {
+  let parent = get_startup_dir()?;
+  Ok(parent.join("espanso.lnk"))
+}
+
+fn get_shortcut_target_file(shortcut_path: &Path) -> Result<PathBuf> {
+  let output = Command::new("powershell")
+            .arg("-c")
+            .arg("$sh = New-Object -ComObject WScript.Shell; $target = $sh.CreateShortcut($env:TARGET_FILE_PATH).TargetPath; echo $target")
+            .env("TARGET_FILE_PATH", shortcut_path.to_string_lossy().to_string())
+            .output()?;
+
+  if !output.status.success() {
+    return Err(ShortcutError::PowershellNonZeroExitCode.into());
+  }
+
+  let raw_path = String::from_utf8_lossy(&output.stdout);
+  let path = PathBuf::from(raw_path.trim().to_string());
+  Ok(path)
+}
+
+fn create_shortcut_target_file(
+  shortcut_path: &Path,
+  target_path: &Path,
+  arguments: &str,
+) -> Result<()> {
+  let mut command = Command::new("powershell");
+  command.arg("-c");
+  command.arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()");
+  command.env("SHORTCUT_PATH", shortcut_path.to_string_lossy().to_string());
+  command.env("TARGET_PATH", target_path.to_string_lossy().to_string());
+  command.env("TARGET_ARGS", arguments);
+
+  set_command_flags(&mut command);
+  let output = command.output()?;
+
+  if !output.status.success() {
+    return Err(ShortcutError::PowershellNonZeroExitCode.into());
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum ShortcutError {
+  #[error("powershell exit with non-zero code")]
+  PowershellNonZeroExitCode,
+}
diff --git a/espanso/src/cli/util.rs b/espanso/src/cli/util.rs
new file mode 100644
index 00000000..e029a745
--- /dev/null
+++ b/espanso/src/cli/util.rs
@@ -0,0 +1,55 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use super::PathsOverrides;
+use std::process::Command;
+
+pub trait CommandExt {
+  fn with_paths_overrides(&mut self, paths_overrides: &PathsOverrides) -> &mut Self;
+}
+
+impl CommandExt for Command {
+  fn with_paths_overrides(&mut self, paths_overrides: &PathsOverrides) -> &mut Self {
+    // We only inject the paths that were explicitly overrided because otherwise
+    // the migration process might create some incompatibilities.
+    // For example, after the migration the "packages" dir could have been
+    // moved to a different one, and that might cause the daemon to crash
+    // if we inject the old packages dir that was automatically resolved.
+    if let Some(config_override) = &paths_overrides.config {
+      self.env(
+        "ESPANSO_CONFIG_DIR",
+        config_override.to_string_lossy().to_string(),
+      );
+    }
+    if let Some(packages_override) = &paths_overrides.packages {
+      self.env(
+        "ESPANSO_PACKAGE_DIR",
+        packages_override.to_string_lossy().to_string(),
+      );
+    }
+    if let Some(runtime_override) = &paths_overrides.runtime {
+      self.env(
+        "ESPANSO_RUNTIME_DIR",
+        runtime_override.to_string_lossy().to_string(),
+      );
+    }
+
+    self
+  }
+}
diff --git a/espanso/src/cli/workaround/mod.rs b/espanso/src/cli/workaround/mod.rs
new file mode 100644
index 00000000..857c61f9
--- /dev/null
+++ b/espanso/src/cli/workaround/mod.rs
@@ -0,0 +1,54 @@
+/*
+ * 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 super::{CliModule, CliModuleArgs};
+use crate::{error_eprintln, exit_code::WORKAROUND_SUCCESS};
+
+#[cfg(target_os = "macos")]
+mod secure_input;
+
+pub fn new() -> CliModule {
+  CliModule {
+    subcommand: "workaround".to_string(),
+    entry: workaround_main,
+    ..Default::default()
+  }
+}
+
+fn workaround_main(args: CliModuleArgs) -> i32 {
+  let cli_args = args.cli_args.expect("missing cli_args");
+
+  if cli_args.subcommand_matches("secure-input").is_some() {
+    #[cfg(target_os = "macos")]
+    {
+      use crate::exit_code::WORKAROUND_FAILURE;
+      if let Err(err) = secure_input::run_secure_input_workaround() {
+        error_eprintln!("secure-input workaround reported error: {}", err);
+        return WORKAROUND_FAILURE;
+      }
+    }
+    #[cfg(not(target_os = "macos"))]
+    {
+      error_eprintln!("secure-input workaround is only available on macOS");
+      return crate::exit_code::WORKAROUND_NOT_AVAILABLE;
+    }
+  }
+
+  WORKAROUND_SUCCESS
+}
diff --git a/espanso/src/cli/workaround/secure_input.rs b/espanso/src/cli/workaround/secure_input.rs
new file mode 100644
index 00000000..dd00429a
--- /dev/null
+++ b/espanso/src/cli/workaround/secure_input.rs
@@ -0,0 +1,137 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{bail, Result};
+use std::io::Write;
+use std::{
+  collections::HashSet,
+  process::{Command, Stdio},
+};
+
+const BLUR_CHROME_WINDOWS_SCRIPT: &str =
+  include_str!("../../res/macos/scripts/blur_chrome_windows.scpt");
+
+const GET_RUNNING_APPS_SCRIPT: &str = include_str!("../../res/macos/scripts/get_running_apps.scpt");
+
+const FOCUS_BITWARDEN_SCRIPT: &str = include_str!("../../res/macos/scripts/focus_bitwarden.scpt");
+
+const SECURE_INPUT_ASK_LOCK_SCREEN_SCRIPT: &str =
+  include_str!("../../res/macos/scripts/secure_input_ask_lock_screen.scpt");
+
+const SUCCESS_DIALOG_SCRIPT: &str =
+  include_str!("../../res/macos/scripts/secure_input_disabled_dialog.scpt");
+
+pub fn run_secure_input_workaround() -> Result<()> {
+  if espanso_mac_utils::get_secure_input_pid().is_none() {
+    println!("secure input is not active, no workaround needed");
+    return Ok(());
+  }
+
+  execute_secure_input_workaround()?;
+  let _ = run_apple_script(SUCCESS_DIALOG_SCRIPT);
+  Ok(())
+}
+
+fn execute_secure_input_workaround() -> Result<()> {
+  println!(
+    "Secure input is enabled. Our guess is that it was activated by '{}',",
+    espanso_mac_utils::get_secure_input_application()
+      .map(|entry| entry.0)
+      .unwrap_or_default()
+  );
+  println!("so restarting that application could solve the problem.");
+  println!();
+  println!("Unfortunately, that guess might be wrong if SecureInput was activated by");
+  println!("the application while in the background.");
+  println!();
+  println!("This workaround will attempt to execute a series of known actions that often");
+  println!("help in disabling secure input.");
+
+  let running_apps = get_running_apps()?;
+
+  if running_apps.contains("com.google.Chrome") {
+    println!("-> Running chrome defocusing workaround");
+    if let Err(err) = run_apple_script(BLUR_CHROME_WINDOWS_SCRIPT) {
+      eprintln!("unable to run chrome defocusing workaround: {}", err);
+    }
+
+    if espanso_mac_utils::get_secure_input_pid().is_none() {
+      return Ok(());
+    }
+  }
+
+  if running_apps.contains("com.bitwarden.desktop") {
+    println!("-> Focusing/Defocusing on Bitwarden");
+    if let Err(err) = run_apple_script(FOCUS_BITWARDEN_SCRIPT) {
+      eprintln!("unable to run bitwarden defocusing workaround: {}", err);
+    }
+
+    if espanso_mac_utils::get_secure_input_pid().is_none() {
+      return Ok(());
+    }
+  }
+
+  // Ask the user if he wants to try locking the screen
+  if run_apple_script(SECURE_INPUT_ASK_LOCK_SCREEN_SCRIPT)?.trim() == "yes" {
+    if let Err(err) = lock_screen() {
+      eprintln!("failed to lock the screen: {}", err);
+    }
+  }
+
+  if espanso_mac_utils::get_secure_input_pid().is_some() {
+    bail!("failed to release secure input");
+  }
+
+  Ok(())
+}
+
+fn run_apple_script(script: &str) -> Result<String> {
+  let mut child = Command::new("osascript")
+    .arg("-")
+    .stdin(Stdio::piped())
+    .stdout(Stdio::piped())
+    .spawn()?;
+
+  let child_stdin = child.stdin.as_mut().unwrap();
+  child_stdin.write_all(script.as_bytes())?;
+
+  let output = child.wait_with_output()?;
+  let stdout = String::from_utf8_lossy(&output.stdout);
+  Ok(stdout.to_string())
+}
+
+fn lock_screen() -> Result<()> {
+  let mut child = Command::new("osascript")
+    .arg("-e")
+    .arg(r#"tell application "System Events" to keystroke "q" using {command down,control down}"#)
+    .spawn()?;
+
+  child.wait()?;
+  Ok(())
+}
+
+fn get_running_apps() -> Result<HashSet<String>> {
+  let apps_raw = run_apple_script(GET_RUNNING_APPS_SCRIPT)?;
+  let mut apps = HashSet::new();
+  for app in apps_raw.trim().split(", ") {
+    apps.insert(app.to_string());
+  }
+
+  Ok(apps)
+}
diff --git a/espanso/src/cli/worker/builtin/debug.rs b/espanso/src/cli/worker/builtin/debug.rs
new file mode 100644
index 00000000..6ab41df2
--- /dev/null
+++ b/espanso/src/cli/worker/builtin/debug.rs
@@ -0,0 +1,67 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::event::{effect::TextInjectRequest, EventType};
+
+use crate::cli::worker::builtin::generate_next_builtin_id;
+
+use super::BuiltInMatch;
+
+// TODO: create task that opens up a GUI with this content
+
+pub fn create_match_paste_active_config_info() -> BuiltInMatch {
+  BuiltInMatch {
+    id: generate_next_builtin_id(),
+    label: "Paste active config information",
+    triggers: vec!["#acfg#".to_string()],
+    action: |context| {
+      let dump = context.get_active_config().pretty_dump();
+
+      EventType::TextInject(TextInjectRequest {
+        text: dump,
+        force_mode: None,
+      })
+    },
+    ..Default::default()
+  }
+}
+
+pub fn create_match_paste_active_app_info() -> BuiltInMatch {
+  BuiltInMatch {
+    id: generate_next_builtin_id(),
+    label: "Paste active application information (detect)",
+    triggers: vec!["#detect#".to_string()],
+    action: |context| {
+      let info = context.get_active_app_info();
+
+      let dump = format!(
+        "title: '{}'\nexec: '{}'\nclass: '{}'",
+        info.title.unwrap_or_default(),
+        info.exec.unwrap_or_default(),
+        info.class.unwrap_or_default()
+      );
+
+      EventType::TextInject(TextInjectRequest {
+        text: dump,
+        force_mode: None,
+      })
+    },
+    ..Default::default()
+  }
+}
diff --git a/espanso/src/cli/worker/builtin/mod.rs b/espanso/src/cli/worker/builtin/mod.rs
new file mode 100644
index 00000000..1189f1ba
--- /dev/null
+++ b/espanso/src/cli/worker/builtin/mod.rs
@@ -0,0 +1,86 @@
+/*
+ * 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::cell::Cell;
+
+use espanso_config::config::Config;
+
+use espanso_engine::event::EventType;
+
+use super::context::Context;
+
+mod debug;
+mod process;
+mod search;
+
+const MIN_BUILTIN_MATCH_ID: i32 = 1_000_000_000;
+
+pub struct BuiltInMatch {
+  pub id: i32,
+  pub label: &'static str,
+  pub triggers: Vec<String>,
+  pub hotkey: Option<String>,
+  pub action: fn(context: &dyn Context) -> EventType,
+}
+
+impl Default for BuiltInMatch {
+  fn default() -> Self {
+    Self {
+      id: 0,
+      label: "",
+      triggers: Vec::new(),
+      hotkey: None,
+      action: |_| EventType::NOOP,
+    }
+  }
+}
+
+pub fn get_builtin_matches(config: &dyn Config) -> Vec<BuiltInMatch> {
+  let mut matches = vec![
+    debug::create_match_paste_active_config_info(),
+    debug::create_match_paste_active_app_info(),
+    process::create_match_exit(),
+    process::create_match_restart(),
+  ];
+
+  if config.search_trigger().is_some() || config.search_shortcut().is_some() {
+    matches.push(search::create_match_trigger_search_bar(
+      config.search_trigger(),
+      config.search_shortcut(),
+    ));
+  }
+
+  matches
+}
+
+pub fn is_builtin_match(id: i32) -> bool {
+  id >= MIN_BUILTIN_MATCH_ID
+}
+
+thread_local! {
+  static CURRENT_BUILTIN_MATCH_ID: Cell<i32> = Cell::new(MIN_BUILTIN_MATCH_ID);
+}
+
+fn generate_next_builtin_id() -> i32 {
+  CURRENT_BUILTIN_MATCH_ID.with(|value| {
+    let current = value.get();
+    value.set(current + 1);
+    current
+  })
+}
diff --git a/espanso/src/cli/worker/builtin/process.rs b/espanso/src/cli/worker/builtin/process.rs
new file mode 100644
index 00000000..b09b752b
--- /dev/null
+++ b/espanso/src/cli/worker/builtin/process.rs
@@ -0,0 +1,44 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::event::{EventType, ExitMode};
+
+use crate::cli::worker::builtin::generate_next_builtin_id;
+
+use super::BuiltInMatch;
+
+pub fn create_match_exit() -> BuiltInMatch {
+  BuiltInMatch {
+    id: generate_next_builtin_id(),
+    label: "Exit espanso",
+    triggers: Vec::new(),
+    action: |_| EventType::ExitRequested(ExitMode::ExitAllProcesses),
+    ..Default::default()
+  }
+}
+
+pub fn create_match_restart() -> BuiltInMatch {
+  BuiltInMatch {
+    id: generate_next_builtin_id(),
+    label: "Restart espanso",
+    triggers: Vec::new(),
+    action: |_| EventType::ExitRequested(ExitMode::RestartWorker),
+    ..Default::default()
+  }
+}
diff --git a/espanso/src/cli/worker/builtin/search.rs b/espanso/src/cli/worker/builtin/search.rs
new file mode 100644
index 00000000..1bf07564
--- /dev/null
+++ b/espanso/src/cli/worker/builtin/search.rs
@@ -0,0 +1,37 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::event::EventType;
+
+use crate::cli::worker::builtin::generate_next_builtin_id;
+
+use super::BuiltInMatch;
+
+pub fn create_match_trigger_search_bar(
+  trigger: Option<String>,
+  hotkey: Option<String>,
+) -> BuiltInMatch {
+  BuiltInMatch {
+    id: generate_next_builtin_id(),
+    label: "Open search bar",
+    triggers: trigger.map(|trigger| vec![trigger]).unwrap_or_default(),
+    hotkey,
+    action: |_| EventType::ShowSearchBar,
+  }
+}
diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs
new file mode 100644
index 00000000..c4808bba
--- /dev/null
+++ b/espanso/src/cli/worker/config.rs
@@ -0,0 +1,179 @@
+/*
+ * 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::HashSet, sync::Arc};
+
+use espanso_config::{
+  config::{AppProperties, Config, ConfigStore},
+  matches::store::{MatchSet, MatchStore},
+};
+use espanso_info::{AppInfo, AppInfoProvider};
+
+use super::builtin::is_builtin_match;
+
+pub struct ConfigManager<'a> {
+  config_store: &'a dyn ConfigStore,
+  match_store: &'a dyn MatchStore,
+  app_info_provider: &'a dyn AppInfoProvider,
+}
+
+impl<'a> ConfigManager<'a> {
+  pub fn new(
+    config_store: &'a dyn ConfigStore,
+    match_store: &'a dyn MatchStore,
+    app_info_provider: &'a dyn AppInfoProvider,
+  ) -> Self {
+    Self {
+      config_store,
+      match_store,
+      app_info_provider,
+    }
+  }
+
+  pub fn active(&self) -> Arc<dyn Config> {
+    let current_app = self.app_info_provider.get_info();
+    let info = to_app_properties(&current_app);
+    self.config_store.active(&info)
+  }
+
+  pub fn active_context(&self) -> (Arc<dyn Config>, MatchSet) {
+    let config = self.active();
+    let match_paths = config.match_paths();
+    (config.clone(), self.match_store.query(match_paths))
+  }
+
+  pub fn default(&self) -> Arc<dyn Config> {
+    self.config_store.default()
+  }
+}
+
+// TODO: test
+fn to_app_properties(info: &AppInfo) -> AppProperties {
+  AppProperties {
+    title: info.title.as_deref(),
+    class: info.class.as_deref(),
+    exec: info.exec.as_deref(),
+  }
+}
+
+impl<'a> espanso_engine::process::MatchFilter for ConfigManager<'a> {
+  fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32> {
+    let ids_set: HashSet<i32> = matches_ids.iter().copied().collect::<HashSet<_>>();
+    let (_, match_set) = self.active_context();
+
+    let active_user_defined_matches: Vec<i32> = match_set
+      .matches
+      .iter()
+      .filter(|m| ids_set.contains(&m.id))
+      .map(|m| m.id)
+      .collect();
+
+    let builtin_matches: Vec<i32> = matches_ids
+      .iter()
+      .filter(|id| is_builtin_match(**id))
+      .copied()
+      .collect();
+
+    let mut output = active_user_defined_matches;
+    output.extend(builtin_matches);
+    output
+  }
+}
+
+impl<'a> super::engine::process::middleware::render::ConfigProvider<'a> for ConfigManager<'a> {
+  fn configs(&self) -> Vec<(Arc<dyn Config>, MatchSet)> {
+    self
+      .config_store
+      .configs()
+      .into_iter()
+      .map(|config| {
+        let match_set = self.match_store.query(config.match_paths());
+        (config, match_set)
+      })
+      .collect()
+  }
+
+  fn active(&self) -> (Arc<dyn Config>, MatchSet) {
+    self.active_context()
+  }
+}
+
+impl<'a> espanso_engine::dispatch::ModeProvider for ConfigManager<'a> {
+  fn active_mode(&self) -> espanso_engine::dispatch::Mode {
+    let config = self.active();
+    match config.backend() {
+      espanso_config::config::Backend::Inject => espanso_engine::dispatch::Mode::Event,
+      espanso_config::config::Backend::Clipboard => espanso_engine::dispatch::Mode::Clipboard,
+      espanso_config::config::Backend::Auto => espanso_engine::dispatch::Mode::Auto {
+        clipboard_threshold: config.clipboard_threshold(),
+      },
+    }
+  }
+}
+
+impl<'a> super::engine::dispatch::executor::clipboard_injector::ClipboardParamsProvider
+  for ConfigManager<'a>
+{
+  fn get(&self) -> super::engine::dispatch::executor::clipboard_injector::ClipboardParams {
+    let active = self.active();
+    super::engine::dispatch::executor::clipboard_injector::ClipboardParams {
+      pre_paste_delay: active.pre_paste_delay(),
+      paste_shortcut_event_delay: active.paste_shortcut_event_delay(),
+      paste_shortcut: active.paste_shortcut(),
+      disable_x11_fast_inject: active.disable_x11_fast_inject(),
+      restore_clipboard: active.preserve_clipboard(),
+      restore_clipboard_delay: active.restore_clipboard_delay(),
+    }
+  }
+}
+
+impl<'a> super::engine::dispatch::executor::InjectParamsProvider for ConfigManager<'a> {
+  fn get(&self) -> super::engine::dispatch::executor::InjectParams {
+    let active = self.active();
+    super::engine::dispatch::executor::InjectParams {
+      disable_x11_fast_inject: active.disable_x11_fast_inject(),
+      inject_delay: active.inject_delay(),
+      key_delay: active.key_delay(),
+      evdev_modifier_delay: active.evdev_modifier_delay(),
+    }
+  }
+}
+
+impl<'a> espanso_engine::process::MatcherMiddlewareConfigProvider for ConfigManager<'a> {
+  fn max_history_size(&self) -> usize {
+    self.default().backspace_limit()
+  }
+}
+
+impl<'a> espanso_engine::process::UndoEnabledProvider for ConfigManager<'a> {
+  fn is_undo_enabled(&self) -> bool {
+    // Disable undo_backspace on Wayland for now as it's not stable
+    if cfg!(feature = "wayland") {
+      return false;
+    }
+
+    self.active().undo_backspace()
+  }
+}
+
+impl<'a> espanso_engine::process::EnabledStatusProvider for ConfigManager<'a> {
+  fn is_config_enabled(&self) -> bool {
+    self.active().enable()
+  }
+}
diff --git a/espanso/src/cli/worker/context/default.rs b/espanso/src/cli/worker/context/default.rs
new file mode 100644
index 00000000..e5f323e4
--- /dev/null
+++ b/espanso/src/cli/worker/context/default.rs
@@ -0,0 +1,58 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_info::AppInfoProvider;
+
+use super::*;
+use crate::cli::worker::config::ConfigManager;
+
+pub struct DefaultContext<'a> {
+  config_manager: &'a ConfigManager<'a>,
+  app_info_provider: &'a dyn AppInfoProvider,
+}
+
+impl<'a> DefaultContext<'a> {
+  pub fn new(
+    config_manager: &'a ConfigManager<'a>,
+    app_info_provider: &'a dyn AppInfoProvider,
+  ) -> Self {
+    Self {
+      config_manager,
+      app_info_provider,
+    }
+  }
+}
+
+impl<'a> Context for DefaultContext<'a> {}
+
+impl<'a> ConfigContext for DefaultContext<'a> {
+  fn get_default_config(&self) -> Arc<dyn Config> {
+    self.config_manager.default()
+  }
+
+  fn get_active_config(&self) -> Arc<dyn Config> {
+    self.config_manager.active()
+  }
+}
+
+impl<'a> AppInfoContext for DefaultContext<'a> {
+  fn get_active_app_info(&self) -> AppInfo {
+    self.app_info_provider.get_info()
+  }
+}
diff --git a/espanso/src/cli/worker/context/mod.rs b/espanso/src/cli/worker/context/mod.rs
new file mode 100644
index 00000000..d3af481c
--- /dev/null
+++ b/espanso/src/cli/worker/context/mod.rs
@@ -0,0 +1,37 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use espanso_config::config::Config;
+
+mod default;
+pub use default::DefaultContext;
+use espanso_info::AppInfo;
+
+pub trait Context: ConfigContext + AppInfoContext {}
+
+pub trait ConfigContext {
+  fn get_default_config(&self) -> Arc<dyn Config>;
+  fn get_active_config(&self) -> Arc<dyn Config>;
+}
+
+pub trait AppInfoContext {
+  fn get_active_app_info(&self) -> AppInfo;
+}
diff --git a/espanso/src/cli/worker/daemon_monitor.rs b/espanso/src/cli/worker/daemon_monitor.rs
new file mode 100644
index 00000000..83028742
--- /dev/null
+++ b/espanso/src/cli/worker/daemon_monitor.rs
@@ -0,0 +1,63 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+use espanso_engine::event::ExitMode;
+use log::{error, info, warn};
+
+use crate::lock::acquire_daemon_lock;
+
+const DAEMON_STATUS_CHECK_INTERVAL: u64 = 1000;
+
+pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -> Result<()> {
+  let runtime_dir_clone = runtime_dir.to_path_buf();
+  std::thread::Builder::new()
+    .name("daemon-monitor".to_string())
+    .spawn(move || {
+      daemon_monitor_main(&runtime_dir_clone, exit_notify.clone());
+    })?;
+
+  Ok(())
+}
+
+fn daemon_monitor_main(runtime_dir: &Path, exit_notify: Sender<ExitMode>) {
+  info!("monitoring the status of the daemon process");
+
+  loop {
+    let is_daemon_lock_free = {
+      let lock = acquire_daemon_lock(runtime_dir);
+      lock.is_some()
+    };
+
+    if is_daemon_lock_free {
+      warn!("detected unexpected daemon termination, sending exit signal to the engine");
+      if let Err(error) = exit_notify.send(ExitMode::Exit) {
+        error!("unable to send daemon exit signal: {}", error);
+      }
+      break;
+    }
+
+    std::thread::sleep(std::time::Duration::from_millis(
+      DAEMON_STATUS_CHECK_INTERVAL,
+    ));
+  }
+}
diff --git a/espanso/src/cli/worker/engine/caches/app_info_provider.rs b/espanso/src/cli/worker/engine/caches/app_info_provider.rs
new file mode 100644
index 00000000..a0184ff3
--- /dev/null
+++ b/espanso/src/cli/worker/engine/caches/app_info_provider.rs
@@ -0,0 +1,59 @@
+/*
+ * 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::{
+  cell::RefCell,
+  time::{Duration, Instant},
+};
+
+use espanso_info::{AppInfo, AppInfoProvider};
+
+pub struct CachedAppInfoProvider<'a> {
+  app_info_provider: &'a dyn AppInfoProvider,
+  caching_interval: Duration,
+
+  _cached_info: RefCell<Option<(Instant, AppInfo)>>,
+}
+
+impl<'a> CachedAppInfoProvider<'a> {
+  pub fn from(app_info_provider: &'a dyn AppInfoProvider, caching_interval: Duration) -> Self {
+    Self {
+      app_info_provider,
+      caching_interval,
+      _cached_info: RefCell::new(None),
+    }
+  }
+}
+
+impl<'a> AppInfoProvider for CachedAppInfoProvider<'a> {
+  fn get_info(&self) -> espanso_info::AppInfo {
+    let mut cached_info = self._cached_info.borrow_mut();
+    if let Some((instant, cached_value)) = &*cached_info {
+      if instant.elapsed() < self.caching_interval {
+        // Return cached config
+        return cached_value.clone();
+      }
+    }
+
+    let info = self.app_info_provider.get_info();
+    *cached_info = Some((Instant::now(), info.clone()));
+
+    info
+  }
+}
diff --git a/espanso/src/cli/worker/engine/caches/mod.rs b/espanso/src/cli/worker/engine/caches/mod.rs
new file mode 100644
index 00000000..46e020a0
--- /dev/null
+++ b/espanso/src/cli/worker/engine/caches/mod.rs
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+pub mod app_info_provider;
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
new file mode 100644
index 00000000..8ebfd54c
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs
@@ -0,0 +1,207 @@
+/*
+ * 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::{convert::TryInto, path::PathBuf};
+
+use espanso_clipboard::Clipboard;
+use espanso_inject::{keys::Key, InjectionOptions, Injector};
+use log::error;
+
+use espanso_engine::{
+  dispatch::HtmlInjector,
+  dispatch::{ImageInjector, TextInjector},
+};
+
+pub trait ClipboardParamsProvider {
+  fn get(&self) -> ClipboardParams;
+}
+
+pub struct ClipboardParams {
+  pub pre_paste_delay: usize,
+  pub paste_shortcut_event_delay: usize,
+  pub paste_shortcut: Option<String>,
+  pub disable_x11_fast_inject: bool,
+  pub restore_clipboard: bool,
+  pub restore_clipboard_delay: usize,
+}
+
+pub struct ClipboardInjectorAdapter<'a> {
+  injector: &'a dyn Injector,
+  clipboard: &'a dyn Clipboard,
+  params_provider: &'a dyn ClipboardParamsProvider,
+}
+
+impl<'a> ClipboardInjectorAdapter<'a> {
+  pub fn new(
+    injector: &'a dyn Injector,
+    clipboard: &'a dyn Clipboard,
+    params_provider: &'a dyn ClipboardParamsProvider,
+  ) -> Self {
+    Self {
+      injector,
+      clipboard,
+      params_provider,
+    }
+  }
+
+  fn send_paste_combination(&self) -> anyhow::Result<()> {
+    let params = self.params_provider.get();
+
+    std::thread::sleep(std::time::Duration::from_millis(
+      params.pre_paste_delay.try_into().unwrap(),
+    ));
+
+    let mut custom_combination = None;
+    if let Some(custom_shortcut) = params.paste_shortcut {
+      if let Some(combination) = parse_combination(&custom_shortcut) {
+        custom_combination = Some(combination);
+      } else {
+        error!("'{}' is not a valid paste shortcut", custom_shortcut);
+      }
+    }
+
+    let combination = if let Some(custom_combination) = custom_combination {
+      custom_combination
+    } else if cfg!(target_os = "macos") {
+      vec![Key::Meta, Key::V]
+    } else {
+      vec![Key::Control, Key::V]
+    };
+
+    self.injector.send_key_combination(
+      &combination,
+      InjectionOptions {
+        delay: params.paste_shortcut_event_delay as i32,
+        disable_fast_inject: params.disable_x11_fast_inject,
+        ..Default::default()
+      },
+    )?;
+
+    Ok(())
+  }
+
+  fn restore_clipboard_guard(&self) -> Option<ClipboardRestoreGuard<'a>> {
+    let params = self.params_provider.get();
+
+    if params.restore_clipboard {
+      Some(ClipboardRestoreGuard::lock(
+        self.clipboard,
+        params.restore_clipboard_delay.try_into().unwrap(),
+      ))
+    } else {
+      None
+    }
+  }
+}
+
+impl<'a> TextInjector for ClipboardInjectorAdapter<'a> {
+  fn name(&self) -> &'static str {
+    "clipboard"
+  }
+
+  fn inject_text(&self, text: &str) -> anyhow::Result<()> {
+    let _guard = self.restore_clipboard_guard();
+
+    self.clipboard.set_text(text)?;
+
+    self.send_paste_combination()?;
+
+    Ok(())
+  }
+}
+
+impl<'a> HtmlInjector for ClipboardInjectorAdapter<'a> {
+  fn inject_html(&self, html: &str, fallback_text: &str) -> anyhow::Result<()> {
+    let _guard = self.restore_clipboard_guard();
+
+    self.clipboard.set_html(html, Some(fallback_text))?;
+
+    self.send_paste_combination()?;
+
+    Ok(())
+  }
+}
+
+impl<'a> ImageInjector for ClipboardInjectorAdapter<'a> {
+  fn inject_image(&self, image_path: &str) -> anyhow::Result<()> {
+    let path = PathBuf::from(image_path);
+    if !path.is_file() {
+      return Err(
+        std::io::Error::new(
+          std::io::ErrorKind::NotFound,
+          "image can't be found in the given path",
+        )
+        .into(),
+      );
+    }
+
+    let _guard = self.restore_clipboard_guard();
+
+    self.clipboard.set_image(&path)?;
+
+    self.send_paste_combination()?;
+
+    Ok(())
+  }
+}
+
+struct ClipboardRestoreGuard<'a> {
+  clipboard: &'a dyn Clipboard,
+  content: Option<String>,
+  restore_delay: u64,
+}
+
+impl<'a> ClipboardRestoreGuard<'a> {
+  pub fn lock(clipboard: &'a dyn Clipboard, restore_delay: u64) -> Self {
+    let clipboard_content = clipboard.get_text();
+
+    Self {
+      clipboard,
+      content: clipboard_content,
+      restore_delay,
+    }
+  }
+}
+
+impl<'a> Drop for ClipboardRestoreGuard<'a> {
+  fn drop(&mut self) {
+    if let Some(content) = self.content.take() {
+      // Sometimes an expansion gets overwritten before pasting by the previous content
+      // A delay is needed to mitigate the problem
+      std::thread::sleep(std::time::Duration::from_millis(self.restore_delay));
+
+      if let Err(error) = self.clipboard.set_text(&content) {
+        error!(
+          "unable to restore clipboard content after expansion: {}",
+          error
+        );
+      }
+    }
+  }
+}
+
+fn parse_combination(combination: &str) -> Option<Vec<Key>> {
+  let tokens = combination.split('+');
+  let mut keys: Vec<Key> = Vec::new();
+  for token in tokens {
+    keys.push(Key::parse(token)?);
+  }
+
+  Some(keys)
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs
new file mode 100644
index 00000000..01b87111
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs
@@ -0,0 +1,70 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_ui::UIRemote;
+
+use espanso_engine::dispatch::ContextMenuHandler;
+
+pub struct ContextMenuHandlerAdapter<'a> {
+  remote: &'a dyn UIRemote,
+}
+
+impl<'a> ContextMenuHandlerAdapter<'a> {
+  pub fn new(remote: &'a dyn UIRemote) -> Self {
+    Self { remote }
+  }
+}
+
+impl<'a> ContextMenuHandler for ContextMenuHandlerAdapter<'a> {
+  fn show_context_menu(&self, items: &[espanso_engine::event::ui::MenuItem]) -> anyhow::Result<()> {
+    let ui_menu_items: Vec<espanso_ui::menu::MenuItem> =
+      items.iter().map(convert_to_ui_menu_item).collect();
+    let ui_menu = espanso_ui::menu::Menu {
+      items: ui_menu_items,
+    };
+
+    self.remote.show_context_menu(&ui_menu);
+
+    Ok(())
+  }
+}
+
+fn convert_to_ui_menu_item(
+  item: &espanso_engine::event::ui::MenuItem,
+) -> espanso_ui::menu::MenuItem {
+  match item {
+    espanso_engine::event::ui::MenuItem::Simple(simple) => {
+      espanso_ui::menu::MenuItem::Simple(espanso_ui::menu::SimpleMenuItem {
+        id: simple.id,
+        label: simple.label.clone(),
+      })
+    }
+    espanso_engine::event::ui::MenuItem::Sub(sub) => {
+      espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem {
+        label: sub.label.clone(),
+        items: sub
+          .items
+          .iter()
+          .map(|item| convert_to_ui_menu_item(item))
+          .collect(),
+      })
+    }
+    espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator,
+  }
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs
new file mode 100644
index 00000000..66b70084
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs
@@ -0,0 +1,86 @@
+/*
+ * 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::convert::TryInto;
+
+use espanso_inject::{InjectionOptions, Injector};
+
+use espanso_engine::dispatch::TextInjector;
+
+use super::InjectParamsProvider;
+
+pub struct EventInjectorAdapter<'a> {
+  injector: &'a dyn Injector,
+  params_provider: &'a dyn InjectParamsProvider,
+}
+
+impl<'a> EventInjectorAdapter<'a> {
+  pub fn new(injector: &'a dyn Injector, params_provider: &'a dyn InjectParamsProvider) -> Self {
+    Self {
+      injector,
+      params_provider,
+    }
+  }
+}
+
+impl<'a> TextInjector for EventInjectorAdapter<'a> {
+  fn name(&self) -> &'static str {
+    "event"
+  }
+
+  fn inject_text(&self, text: &str) -> anyhow::Result<()> {
+    let params = self.params_provider.get();
+
+    // Handle CRLF or LF line endings correctly
+    let split_sequence = if text.contains("\r\n") { "\r\n" } else { "\n" };
+
+    let injection_options = InjectionOptions {
+      delay: params
+        .inject_delay
+        .unwrap_or_else(|| InjectionOptions::default().delay.try_into().unwrap())
+        .try_into()
+        .unwrap(),
+      disable_fast_inject: params.disable_x11_fast_inject,
+      evdev_modifier_delay: params
+        .evdev_modifier_delay
+        .unwrap_or_else(|| {
+          InjectionOptions::default()
+            .evdev_modifier_delay
+            .try_into()
+            .unwrap()
+        })
+        .try_into()
+        .unwrap(),
+    };
+
+    // We don't use the lines() method because it skips emtpy lines, which is not what we want.
+    for (i, line) in text.split(split_sequence).enumerate() {
+      // We simulate an Return press between lines
+      if i > 0 {
+        self
+          .injector
+          .send_keys(&[espanso_inject::keys::Key::Enter], injection_options)?
+      }
+
+      self.injector.send_string(line, injection_options)?;
+    }
+
+    Ok(())
+  }
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/icon.rs b/espanso/src/cli/worker/engine/dispatch/executor/icon.rs
new file mode 100644
index 00000000..0956cd29
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/icon.rs
@@ -0,0 +1,46 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_ui::{icons::TrayIcon, UIRemote};
+
+use espanso_engine::{dispatch::IconHandler, event::ui::IconStatus};
+
+pub struct IconHandlerAdapter<'a> {
+  remote: &'a dyn UIRemote,
+}
+
+impl<'a> IconHandlerAdapter<'a> {
+  pub fn new(remote: &'a dyn UIRemote) -> Self {
+    Self { remote }
+  }
+}
+
+impl<'a> IconHandler for IconHandlerAdapter<'a> {
+  fn update_icon(&self, status: &IconStatus) -> anyhow::Result<()> {
+    let icon = match status {
+      IconStatus::Enabled => TrayIcon::Normal,
+      IconStatus::Disabled => TrayIcon::Disabled,
+      IconStatus::SecureInputDisabled => TrayIcon::SystemDisabled,
+    };
+
+    self.remote.update_tray_icon(icon);
+
+    Ok(())
+  }
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs
new file mode 100644
index 00000000..38f19a54
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs
@@ -0,0 +1,112 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_inject::{InjectionOptions, Injector};
+use std::convert::TryInto;
+
+use espanso_engine::dispatch::KeyInjector;
+
+use super::InjectParamsProvider;
+
+pub struct KeyInjectorAdapter<'a> {
+  injector: &'a dyn Injector,
+  params_provider: &'a dyn InjectParamsProvider,
+}
+
+impl<'a> KeyInjectorAdapter<'a> {
+  pub fn new(injector: &'a dyn Injector, params_provider: &'a dyn InjectParamsProvider) -> Self {
+    Self {
+      injector,
+      params_provider,
+    }
+  }
+}
+
+impl<'a> KeyInjector for KeyInjectorAdapter<'a> {
+  fn inject_sequence(&self, keys: &[espanso_engine::event::input::Key]) -> anyhow::Result<()> {
+    let params = self.params_provider.get();
+
+    let injection_options = InjectionOptions {
+      delay: params
+        .key_delay
+        .unwrap_or_else(|| InjectionOptions::default().delay.try_into().unwrap())
+        .try_into()
+        .unwrap(),
+      disable_fast_inject: params.disable_x11_fast_inject,
+      evdev_modifier_delay: params
+        .evdev_modifier_delay
+        .unwrap_or_else(|| {
+          InjectionOptions::default()
+            .evdev_modifier_delay
+            .try_into()
+            .unwrap()
+        })
+        .try_into()
+        .unwrap(),
+    };
+
+    let converted_keys: Vec<_> = keys.iter().map(convert_to_inject_key).collect();
+    self.injector.send_keys(&converted_keys, injection_options)
+  }
+}
+
+fn convert_to_inject_key(key: &espanso_engine::event::input::Key) -> espanso_inject::keys::Key {
+  match key {
+    espanso_engine::event::input::Key::Alt => espanso_inject::keys::Key::Alt,
+    espanso_engine::event::input::Key::CapsLock => espanso_inject::keys::Key::CapsLock,
+    espanso_engine::event::input::Key::Control => espanso_inject::keys::Key::Control,
+    espanso_engine::event::input::Key::Meta => espanso_inject::keys::Key::Meta,
+    espanso_engine::event::input::Key::NumLock => espanso_inject::keys::Key::NumLock,
+    espanso_engine::event::input::Key::Shift => espanso_inject::keys::Key::Shift,
+    espanso_engine::event::input::Key::Enter => espanso_inject::keys::Key::Enter,
+    espanso_engine::event::input::Key::Tab => espanso_inject::keys::Key::Tab,
+    espanso_engine::event::input::Key::Space => espanso_inject::keys::Key::Space,
+    espanso_engine::event::input::Key::ArrowDown => espanso_inject::keys::Key::ArrowDown,
+    espanso_engine::event::input::Key::ArrowLeft => espanso_inject::keys::Key::ArrowLeft,
+    espanso_engine::event::input::Key::ArrowRight => espanso_inject::keys::Key::ArrowRight,
+    espanso_engine::event::input::Key::ArrowUp => espanso_inject::keys::Key::ArrowUp,
+    espanso_engine::event::input::Key::End => espanso_inject::keys::Key::End,
+    espanso_engine::event::input::Key::Home => espanso_inject::keys::Key::Home,
+    espanso_engine::event::input::Key::PageDown => espanso_inject::keys::Key::PageDown,
+    espanso_engine::event::input::Key::PageUp => espanso_inject::keys::Key::PageUp,
+    espanso_engine::event::input::Key::Escape => espanso_inject::keys::Key::Escape,
+    espanso_engine::event::input::Key::Backspace => espanso_inject::keys::Key::Backspace,
+    espanso_engine::event::input::Key::F1 => espanso_inject::keys::Key::F1,
+    espanso_engine::event::input::Key::F2 => espanso_inject::keys::Key::F2,
+    espanso_engine::event::input::Key::F3 => espanso_inject::keys::Key::F3,
+    espanso_engine::event::input::Key::F4 => espanso_inject::keys::Key::F4,
+    espanso_engine::event::input::Key::F5 => espanso_inject::keys::Key::F5,
+    espanso_engine::event::input::Key::F6 => espanso_inject::keys::Key::F6,
+    espanso_engine::event::input::Key::F7 => espanso_inject::keys::Key::F7,
+    espanso_engine::event::input::Key::F8 => espanso_inject::keys::Key::F8,
+    espanso_engine::event::input::Key::F9 => espanso_inject::keys::Key::F9,
+    espanso_engine::event::input::Key::F10 => espanso_inject::keys::Key::F10,
+    espanso_engine::event::input::Key::F11 => espanso_inject::keys::Key::F11,
+    espanso_engine::event::input::Key::F12 => espanso_inject::keys::Key::F12,
+    espanso_engine::event::input::Key::F13 => espanso_inject::keys::Key::F13,
+    espanso_engine::event::input::Key::F14 => espanso_inject::keys::Key::F14,
+    espanso_engine::event::input::Key::F15 => espanso_inject::keys::Key::F15,
+    espanso_engine::event::input::Key::F16 => espanso_inject::keys::Key::F16,
+    espanso_engine::event::input::Key::F17 => espanso_inject::keys::Key::F17,
+    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::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
new file mode 100644
index 00000000..7fee77a6
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs
@@ -0,0 +1,36 @@
+/*
+ * 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/>.
+ */
+
+pub mod clipboard_injector;
+pub mod context_menu;
+pub mod event_injector;
+pub mod icon;
+pub mod key_injector;
+pub mod secure_input;
+
+pub trait InjectParamsProvider {
+  fn get(&self) -> InjectParams;
+}
+
+pub struct InjectParams {
+  pub inject_delay: Option<usize>,
+  pub key_delay: Option<usize>,
+  pub disable_x11_fast_inject: bool,
+  pub evdev_modifier_delay: Option<usize>,
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/executor/secure_input.rs b/espanso/src/cli/worker/engine/dispatch/executor/secure_input.rs
new file mode 100644
index 00000000..40ae47cf
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/executor/secure_input.rs
@@ -0,0 +1,64 @@
+/*
+ * 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::process::Stdio;
+
+use anyhow::{bail, Context};
+use log::{error, info};
+
+use espanso_engine::dispatch::SecureInputManager;
+
+pub struct SecureInputManagerAdapter {}
+
+impl SecureInputManagerAdapter {
+  pub fn new() -> Self {
+    Self {}
+  }
+}
+
+impl SecureInputManager for SecureInputManagerAdapter {
+  fn display_secure_input_troubleshoot(&self) -> anyhow::Result<()> {
+    // TODO: replace with actual URL
+    // TODO: in the future, this might be a self-contained WebView window
+    opener::open_browser("https://espanso.org/docs")?;
+    Ok(())
+  }
+
+  fn launch_secure_input_autofix(&self) -> anyhow::Result<()> {
+    let espanso_path = std::env::current_exe()?;
+    let child = std::process::Command::new(espanso_path)
+      .args(&["workaround", "secure-input"])
+      .stdout(Stdio::piped())
+      .spawn()
+      .context("unable to spawn workaround process")?;
+    let output = child.wait_with_output()?;
+    let output_str = String::from_utf8_lossy(&output.stdout);
+    let error_str = String::from_utf8_lossy(&output.stderr);
+
+    if output.status.success() {
+      info!(
+        "Secure input workaround executed successfully: {}",
+        output_str
+      );
+      Ok(())
+    } else {
+      error!("Secure input autofix reported error: {}", error_str);
+      bail!("non-successful autofix status code");
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/engine/dispatch/mod.rs b/espanso/src/cli/worker/engine/dispatch/mod.rs
new file mode 100644
index 00000000..a38d5ca2
--- /dev/null
+++ b/espanso/src/cli/worker/engine/dispatch/mod.rs
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+pub mod executor;
diff --git a/espanso/src/cli/worker/engine/funnel/detect.rs b/espanso/src/cli/worker/engine/funnel/detect.rs
new file mode 100644
index 00000000..f0a074f5
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/detect.rs
@@ -0,0 +1,141 @@
+/*
+ * 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 crossbeam::channel::{Receiver, Select, SelectedOperation};
+use espanso_detect::event::InputEvent;
+
+use espanso_engine::{
+  event::{
+    input::{HotKeyEvent, Key, KeyboardEvent, MouseButton, MouseEvent, Status, Variant},
+    Event, EventType, SourceId,
+  },
+  funnel,
+};
+
+pub struct DetectSource {
+  pub receiver: Receiver<(InputEvent, SourceId)>,
+}
+
+impl<'a> funnel::Source<'a> for DetectSource {
+  fn register(&'a self, select: &mut Select<'a>) -> usize {
+    select.recv(&self.receiver)
+  }
+
+  fn receive(&self, op: SelectedOperation) -> Event {
+    let (input_event, source_id) = op
+      .recv(&self.receiver)
+      .expect("unable to select data from DetectSource receiver");
+    match input_event {
+      InputEvent::Keyboard(keyboard_event) => Event {
+        source_id,
+        etype: EventType::Keyboard(KeyboardEvent {
+          key: convert_to_engine_key(keyboard_event.key),
+          value: keyboard_event.value,
+          status: convert_to_engine_status(keyboard_event.status),
+          variant: keyboard_event.variant.map(convert_to_engine_variant),
+        }),
+      },
+      InputEvent::Mouse(mouse_event) => Event {
+        source_id,
+        etype: EventType::Mouse(MouseEvent {
+          status: convert_to_engine_status(mouse_event.status),
+          button: convert_to_engine_mouse_button(mouse_event.button),
+        }),
+      },
+      InputEvent::HotKey(hotkey_event) => Event {
+        source_id,
+        etype: EventType::HotKey(HotKeyEvent {
+          hotkey_id: hotkey_event.hotkey_id,
+        }),
+      },
+    }
+  }
+}
+
+pub fn convert_to_engine_key(key: espanso_detect::event::Key) -> Key {
+  match key {
+    espanso_detect::event::Key::Alt => Key::Alt,
+    espanso_detect::event::Key::CapsLock => Key::CapsLock,
+    espanso_detect::event::Key::Control => Key::Control,
+    espanso_detect::event::Key::Meta => Key::Meta,
+    espanso_detect::event::Key::NumLock => Key::NumLock,
+    espanso_detect::event::Key::Shift => Key::Shift,
+    espanso_detect::event::Key::Enter => Key::Enter,
+    espanso_detect::event::Key::Tab => Key::Tab,
+    espanso_detect::event::Key::Space => Key::Space,
+    espanso_detect::event::Key::ArrowDown => Key::ArrowDown,
+    espanso_detect::event::Key::ArrowLeft => Key::ArrowLeft,
+    espanso_detect::event::Key::ArrowRight => Key::ArrowRight,
+    espanso_detect::event::Key::ArrowUp => Key::ArrowUp,
+    espanso_detect::event::Key::End => Key::End,
+    espanso_detect::event::Key::Home => Key::Home,
+    espanso_detect::event::Key::PageDown => Key::PageDown,
+    espanso_detect::event::Key::PageUp => Key::PageUp,
+    espanso_detect::event::Key::Escape => Key::Escape,
+    espanso_detect::event::Key::Backspace => Key::Backspace,
+    espanso_detect::event::Key::F1 => Key::F1,
+    espanso_detect::event::Key::F2 => Key::F2,
+    espanso_detect::event::Key::F3 => Key::F3,
+    espanso_detect::event::Key::F4 => Key::F4,
+    espanso_detect::event::Key::F5 => Key::F5,
+    espanso_detect::event::Key::F6 => Key::F6,
+    espanso_detect::event::Key::F7 => Key::F7,
+    espanso_detect::event::Key::F8 => Key::F8,
+    espanso_detect::event::Key::F9 => Key::F9,
+    espanso_detect::event::Key::F10 => Key::F10,
+    espanso_detect::event::Key::F11 => Key::F11,
+    espanso_detect::event::Key::F12 => Key::F12,
+    espanso_detect::event::Key::F13 => Key::F13,
+    espanso_detect::event::Key::F14 => Key::F14,
+    espanso_detect::event::Key::F15 => Key::F15,
+    espanso_detect::event::Key::F16 => Key::F16,
+    espanso_detect::event::Key::F17 => Key::F17,
+    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::Other(code) => Key::Other(code),
+  }
+}
+
+pub fn convert_to_engine_variant(variant: espanso_detect::event::Variant) -> Variant {
+  match variant {
+    espanso_detect::event::Variant::Left => Variant::Left,
+    espanso_detect::event::Variant::Right => Variant::Right,
+  }
+}
+
+pub fn convert_to_engine_status(status: espanso_detect::event::Status) -> Status {
+  match status {
+    espanso_detect::event::Status::Pressed => Status::Pressed,
+    espanso_detect::event::Status::Released => Status::Released,
+  }
+}
+
+pub fn convert_to_engine_mouse_button(button: espanso_detect::event::MouseButton) -> MouseButton {
+  match button {
+    espanso_detect::event::MouseButton::Left => MouseButton::Left,
+    espanso_detect::event::MouseButton::Right => MouseButton::Right,
+    espanso_detect::event::MouseButton::Middle => MouseButton::Middle,
+    espanso_detect::event::MouseButton::Button1 => MouseButton::Button1,
+    espanso_detect::event::MouseButton::Button2 => MouseButton::Button2,
+    espanso_detect::event::MouseButton::Button3 => MouseButton::Button3,
+    espanso_detect::event::MouseButton::Button4 => MouseButton::Button4,
+    espanso_detect::event::MouseButton::Button5 => MouseButton::Button5,
+  }
+}
diff --git a/espanso/src/cli/worker/engine/funnel/exit.rs b/espanso/src/cli/worker/engine/funnel/exit.rs
new file mode 100644
index 00000000..ad1f5405
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/exit.rs
@@ -0,0 +1,57 @@
+/*
+ * 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 crossbeam::channel::{Receiver, Select, SelectedOperation};
+
+use espanso_engine::{
+  event::{Event, EventType, ExitMode},
+  funnel,
+};
+
+use super::sequencer::Sequencer;
+
+pub struct ExitSource<'a> {
+  pub exit_signal: Receiver<ExitMode>,
+  pub sequencer: &'a Sequencer,
+}
+
+impl<'a> ExitSource<'a> {
+  pub fn new(exit_signal: Receiver<ExitMode>, sequencer: &'a Sequencer) -> Self {
+    ExitSource {
+      exit_signal,
+      sequencer,
+    }
+  }
+}
+
+impl<'a> funnel::Source<'a> for ExitSource<'a> {
+  fn register(&'a self, select: &mut Select<'a>) -> usize {
+    select.recv(&self.exit_signal)
+  }
+
+  fn receive(&self, op: SelectedOperation) -> Event {
+    let mode = op
+      .recv(&self.exit_signal)
+      .expect("unable to select data from ExitSource receiver");
+    Event {
+      source_id: self.sequencer.next_id(),
+      etype: EventType::ExitRequested(mode),
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/engine/funnel/key_state.rs b/espanso/src/cli/worker/engine/funnel/key_state.rs
new file mode 100644
index 00000000..f3522199
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/key_state.rs
@@ -0,0 +1,132 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::{
+  collections::HashMap,
+  sync::{Arc, Mutex},
+  time::{Duration, Instant},
+};
+
+use espanso_inject::KeyboardStateProvider;
+use log::warn;
+
+/// This duration represents the maximum length for which a pressed key
+/// event is considered valid. This is useful when the "release" event is
+/// lost for whatever reason, so that espanso becomes eventually consistent
+/// after a while.
+const KEY_PRESS_EVENT_INVALIDATION_TIMEOUT: Duration = Duration::from_secs(3);
+
+#[derive(Clone)]
+pub struct KeyStateStore {
+  state: Arc<Mutex<KeyState>>,
+}
+
+impl KeyStateStore {
+  pub fn new() -> Self {
+    Self {
+      state: Arc::new(Mutex::new(KeyState::default())),
+    }
+  }
+
+  pub fn is_key_pressed(&self, key_code: u32) -> bool {
+    let mut state = self.state.lock().expect("unable to obtain modifier state");
+
+    if let Some(status) = state.keys.get_mut(&key_code) {
+      if status.is_outdated() {
+        warn!(
+          "detected outdated key records for {:?}, releasing the state",
+          key_code
+        );
+        status.release();
+      }
+
+      status.is_pressed()
+    } else {
+      false
+    }
+  }
+
+  pub fn update_state(&self, key_code: u32, is_pressed: bool) {
+    let mut state = self.state.lock().expect("unable to obtain key state");
+    if let Some(status) = state.keys.get_mut(&key_code) {
+      if is_pressed {
+        status.press();
+      } else {
+        status.release();
+      }
+    } else {
+      state.keys.insert(key_code, KeyStatus::new(is_pressed));
+    }
+  }
+}
+
+struct KeyState {
+  keys: HashMap<u32, KeyStatus>,
+}
+
+impl Default for KeyState {
+  fn default() -> Self {
+    Self {
+      keys: HashMap::new(),
+    }
+  }
+}
+
+struct KeyStatus {
+  pressed_at: Option<Instant>,
+}
+
+impl KeyStatus {
+  fn new(is_pressed: bool) -> Self {
+    Self {
+      pressed_at: if is_pressed {
+        Some(Instant::now())
+      } else {
+        None
+      },
+    }
+  }
+
+  fn is_pressed(&self) -> bool {
+    self.pressed_at.is_some()
+  }
+
+  fn is_outdated(&self) -> bool {
+    let now = Instant::now();
+    if let Some(pressed_at) = self.pressed_at {
+      now.duration_since(pressed_at) > KEY_PRESS_EVENT_INVALIDATION_TIMEOUT
+    } else {
+      false
+    }
+  }
+
+  fn release(&mut self) {
+    self.pressed_at = None
+  }
+
+  fn press(&mut self) {
+    self.pressed_at = Some(Instant::now());
+  }
+}
+
+impl KeyboardStateProvider for KeyStateStore {
+  fn is_key_pressed(&self, code: u32) -> bool {
+    self.is_key_pressed(code)
+  }
+}
diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs
new file mode 100644
index 00000000..78bd38b3
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/mod.rs
@@ -0,0 +1,163 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use espanso_detect::{
+  event::{InputEvent, KeyboardEvent, Status},
+  SourceCreationOptions,
+};
+use log::error;
+use thiserror::Error;
+
+use detect::DetectSource;
+
+use self::{
+  key_state::KeyStateStore,
+  modifier::{Modifier, ModifierStateStore},
+  sequencer::Sequencer,
+};
+
+pub mod detect;
+pub mod exit;
+pub mod key_state;
+pub mod modifier;
+pub mod secure_input;
+pub mod sequencer;
+pub mod ui;
+
+pub fn init_and_spawn(
+  source_options: SourceCreationOptions,
+) -> Result<(
+  DetectSource,
+  ModifierStateStore,
+  Sequencer,
+  Option<KeyStateStore>,
+)> {
+  let (sender, receiver) = crossbeam::channel::unbounded();
+  let (init_tx, init_rx) = crossbeam::channel::unbounded();
+
+  let modifier_state_store = ModifierStateStore::new();
+  let key_state_store = if source_options.use_evdev {
+    Some(KeyStateStore::new())
+  } else {
+    None
+  };
+  let sequencer = Sequencer::new();
+
+  let modifier_state_store_clone = modifier_state_store.clone();
+  let sequencer_clone = sequencer.clone();
+  let key_state_store_clone = key_state_store.clone();
+  if let Err(error) = std::thread::Builder::new()
+    .name("detect thread".to_string())
+    .spawn(move || match espanso_detect::get_source(source_options) {
+      Ok(mut source) => {
+        if source.initialize().is_err() {
+          init_tx
+            .send(false)
+            .expect("unable to send to the init_tx channel");
+        } else {
+          init_tx
+            .send(true)
+            .expect("unable to send to the init_tx channel");
+
+          source
+            .eventloop(Box::new(move |event| {
+              // Update the modifiers state
+              if let Some((modifier, is_pressed)) = get_modifier_status(&event) {
+                modifier_state_store_clone.update_state(modifier, is_pressed);
+              }
+
+              // Update the key state (if needed)
+              if let Some(key_state_store) = &key_state_store_clone {
+                if let InputEvent::Keyboard(keyboard_event) = &event {
+                  key_state_store.update_state(
+                    keyboard_event.code,
+                    keyboard_event.status == Status::Pressed,
+                  );
+                }
+              }
+
+              // Generate a monotonically increasing id for the current event
+              let source_id = sequencer_clone.next_id();
+
+              sender
+                .send((event, source_id))
+                .expect("unable to send to the source channel");
+            }))
+            .expect("detect eventloop crashed");
+        }
+      }
+      Err(error) => {
+        error!("cannot initialize event source: {:?}", error);
+        init_tx
+          .send(false)
+          .expect("unable to send to the init_tx channel");
+      }
+    })
+  {
+    error!("detection thread initialization failed: {:?}", error);
+    return Err(DetectSourceError::ThreadInitFailed.into());
+  }
+
+  // Wait for the initialization status
+  let has_initialized = init_rx
+    .recv()
+    .expect("unable to receive from the init_rx channel");
+  if !has_initialized {
+    return Err(DetectSourceError::InitFailed.into());
+  }
+
+  Ok((
+    DetectSource { receiver },
+    modifier_state_store,
+    sequencer,
+    key_state_store,
+  ))
+}
+
+#[derive(Error, Debug)]
+pub enum DetectSourceError {
+  #[error("detection thread initialization failed")]
+  ThreadInitFailed,
+
+  #[error("detection source initialization failed")]
+  InitFailed,
+}
+
+fn get_modifier_status(event: &InputEvent) -> Option<(Modifier, bool)> {
+  match event {
+    InputEvent::Keyboard(KeyboardEvent {
+      key,
+      status,
+      value: _,
+      variant: _,
+      code: _,
+    }) => {
+      let is_pressed = *status == Status::Pressed;
+      match key {
+        espanso_detect::event::Key::Alt => Some((Modifier::Alt, is_pressed)),
+        espanso_detect::event::Key::Control => Some((Modifier::Ctrl, is_pressed)),
+        espanso_detect::event::Key::Meta => Some((Modifier::Meta, is_pressed)),
+        espanso_detect::event::Key::Shift => Some((Modifier::Shift, is_pressed)),
+        _ => None,
+      }
+    }
+    _ => None,
+  }
+}
diff --git a/espanso/src/cli/worker/engine/funnel/modifier.rs b/espanso/src/cli/worker/engine/funnel/modifier.rs
new file mode 100644
index 00000000..ba3229f5
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/modifier.rs
@@ -0,0 +1,144 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::{
+  sync::{Arc, Mutex},
+  time::{Duration, Instant},
+};
+
+use log::warn;
+
+use espanso_engine::process::ModifierStatusProvider;
+
+/// This duration represents the maximum length for which a pressed modifier
+/// event is considered valid. This is useful when the "release" event is
+/// lost for whatever reason, so that espanso becomes eventually consistent
+/// after a while.
+const MAXIMUM_MODIFIERS_PRESS_TIME_RECORD: Duration = Duration::from_secs(30);
+
+const CONFLICTING_MODIFIERS: &[Modifier] = &[
+  Modifier::Ctrl,
+  Modifier::Alt,
+  Modifier::Meta,
+  Modifier::Shift,
+];
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub enum Modifier {
+  Ctrl,
+  Shift,
+  Alt,
+  Meta,
+}
+
+#[derive(Clone)]
+pub struct ModifierStateStore {
+  state: Arc<Mutex<ModifiersState>>,
+}
+
+impl ModifierStateStore {
+  pub fn new() -> Self {
+    Self {
+      state: Arc::new(Mutex::new(ModifiersState::default())),
+    }
+  }
+
+  pub fn is_any_conflicting_modifier_pressed(&self) -> bool {
+    let mut state = self.state.lock().expect("unable to obtain modifier state");
+    let mut is_any_modifier_pressed = false;
+    for (modifier, status) in &mut state.modifiers {
+      if status.is_outdated() {
+        warn!(
+          "detected outdated modifier records for {:?}, releasing the state",
+          modifier
+        );
+        status.release();
+      }
+
+      if status.is_pressed() && CONFLICTING_MODIFIERS.contains(modifier) {
+        is_any_modifier_pressed = true;
+      }
+    }
+    is_any_modifier_pressed
+  }
+
+  pub fn update_state(&self, modifier: Modifier, is_pressed: bool) {
+    let mut state = self.state.lock().expect("unable to obtain modifier state");
+    for (curr_modifier, status) in &mut state.modifiers {
+      if curr_modifier == &modifier {
+        if is_pressed {
+          status.press();
+        } else {
+          status.release();
+        }
+        break;
+      }
+    }
+  }
+}
+
+struct ModifiersState {
+  modifiers: Vec<(Modifier, ModifierStatus)>,
+}
+
+impl Default for ModifiersState {
+  fn default() -> Self {
+    Self {
+      modifiers: vec![
+        (Modifier::Ctrl, ModifierStatus { pressed_at: None }),
+        (Modifier::Alt, ModifierStatus { pressed_at: None }),
+        (Modifier::Shift, ModifierStatus { pressed_at: None }),
+        (Modifier::Meta, ModifierStatus { pressed_at: None }),
+      ],
+    }
+  }
+}
+
+struct ModifierStatus {
+  pressed_at: Option<Instant>,
+}
+
+impl ModifierStatus {
+  fn is_pressed(&self) -> bool {
+    self.pressed_at.is_some()
+  }
+
+  fn is_outdated(&self) -> bool {
+    let now = Instant::now();
+    if let Some(pressed_at) = self.pressed_at {
+      now.duration_since(pressed_at) > MAXIMUM_MODIFIERS_PRESS_TIME_RECORD
+    } else {
+      false
+    }
+  }
+
+  fn release(&mut self) {
+    self.pressed_at = None
+  }
+
+  fn press(&mut self) {
+    self.pressed_at = Some(Instant::now());
+  }
+}
+
+impl ModifierStatusProvider for ModifierStateStore {
+  fn is_any_conflicting_modifier_pressed(&self) -> bool {
+    self.is_any_conflicting_modifier_pressed()
+  }
+}
diff --git a/espanso/src/cli/worker/engine/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs
new file mode 100644
index 00000000..527c80b7
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/secure_input.rs
@@ -0,0 +1,76 @@
+/*
+ * 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 crossbeam::channel::{Receiver, Select, SelectedOperation};
+
+use crate::cli::worker::secure_input::SecureInputEvent;
+use espanso_engine::{
+  event::{internal::SecureInputEnabledEvent, Event, EventType},
+  funnel,
+};
+
+use super::sequencer::Sequencer;
+
+pub struct SecureInputSource<'a> {
+  pub receiver: Receiver<SecureInputEvent>,
+  pub sequencer: &'a Sequencer,
+}
+
+impl<'a> SecureInputSource<'a> {
+  pub fn new(receiver: Receiver<SecureInputEvent>, sequencer: &'a Sequencer) -> Self {
+    SecureInputSource {
+      receiver,
+      sequencer,
+    }
+  }
+}
+
+impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
+  fn register(&'a self, select: &mut Select<'a>) -> usize {
+    if cfg!(target_os = "macos") {
+      select.recv(&self.receiver)
+    } else {
+      999999
+    }
+  }
+
+  fn receive(&self, op: SelectedOperation) -> Event {
+    if cfg!(target_os = "macos") {
+      let si_event = op
+        .recv(&self.receiver)
+        .expect("unable to select data from SecureInputSource receiver");
+
+      Event {
+        source_id: self.sequencer.next_id(),
+        etype: match si_event {
+          SecureInputEvent::Disabled => EventType::SecureInputDisabled,
+          SecureInputEvent::Enabled { app_name, app_path } => {
+            EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
+          }
+        },
+      }
+    } else {
+      println!("noop");
+      Event {
+        source_id: self.sequencer.next_id(),
+        etype: EventType::NOOP,
+      }
+    }
+  }
+}
diff --git a/src/render/mod.rs b/espanso/src/cli/worker/engine/funnel/sequencer.rs
similarity index 55%
rename from src/render/mod.rs
rename to espanso/src/cli/worker/engine/funnel/sequencer.rs
index 264fc060..001d26c1 100644
--- a/src/render/mod.rs
+++ b/espanso/src/cli/worker/engine/funnel/sequencer.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -17,29 +17,32 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use crate::config::Configs;
-use crate::matcher::Match;
-use std::path::PathBuf;
+use std::sync::{
+  atomic::{AtomicU32, Ordering},
+  Arc,
+};
 
-pub(crate) mod default;
-pub(crate) mod utils;
+use espanso_engine::{event::SourceId, process::EventSequenceProvider};
 
-pub trait Renderer {
-    // Render a match output
-    fn render_match(
-        &self,
-        m: &Match,
-        trigger_offset: usize,
-        config: &Configs,
-        args: Vec<String>,
-    ) -> RenderResult;
+#[derive(Clone)]
+pub struct Sequencer {
+  current_id: Arc<AtomicU32>,
+}
+
+impl Sequencer {
+  pub fn new() -> Self {
+    Self {
+      current_id: Arc::new(AtomicU32::new(0)),
+    }
+  }
 
-    // Render a passive expansion text
-    fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
+  pub fn next_id(&self) -> SourceId {
+    self.current_id.fetch_add(1, Ordering::SeqCst)
+  }
 }
 
-pub enum RenderResult {
-    Text(String),
-    Image(PathBuf),
-    Error,
+impl EventSequenceProvider for Sequencer {
+  fn get_next_id(&self) -> SourceId {
+    self.next_id()
+  }
 }
diff --git a/espanso/src/cli/worker/engine/funnel/ui.rs b/espanso/src/cli/worker/engine/funnel/ui.rs
new file mode 100644
index 00000000..ded79c5c
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/ui.rs
@@ -0,0 +1,65 @@
+/*
+ * 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 crossbeam::channel::{Receiver, Select, SelectedOperation};
+use espanso_ui::event::UIEvent;
+
+use espanso_engine::{
+  event::{input::ContextMenuClickedEvent, Event, EventType},
+  funnel,
+};
+
+use super::sequencer::Sequencer;
+
+pub struct UISource<'a> {
+  pub ui_receiver: Receiver<UIEvent>,
+  pub sequencer: &'a Sequencer,
+}
+
+impl<'a> UISource<'a> {
+  pub fn new(ui_receiver: Receiver<UIEvent>, sequencer: &'a Sequencer) -> Self {
+    UISource {
+      ui_receiver,
+      sequencer,
+    }
+  }
+}
+
+impl<'a> funnel::Source<'a> for UISource<'a> {
+  fn register(&'a self, select: &mut Select<'a>) -> usize {
+    select.recv(&self.ui_receiver)
+  }
+
+  fn receive(&self, op: SelectedOperation) -> Event {
+    let ui_event = op
+      .recv(&self.ui_receiver)
+      .expect("unable to select data from UISource receiver");
+
+    Event {
+      source_id: self.sequencer.next_id(),
+      etype: match ui_event {
+        UIEvent::TrayIconClick => EventType::TrayIconClicked,
+        UIEvent::ContextMenuClick(context_item_id) => {
+          EventType::ContextMenuClicked(ContextMenuClickedEvent { context_item_id })
+        }
+        UIEvent::Heartbeat => EventType::Heartbeat,
+      },
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/engine/keyboard_layout_util.rs b/espanso/src/cli/worker/engine/keyboard_layout_util.rs
new file mode 100644
index 00000000..4db36480
--- /dev/null
+++ b/espanso/src/cli/worker/engine/keyboard_layout_util.rs
@@ -0,0 +1,75 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_config::config::{Config, RMLVOConfig};
+use log::{info, warn};
+
+fn generate_rmlvo_config(config: &dyn Config) -> Option<RMLVOConfig> {
+  // Not needed on Windows and macOS
+  if !cfg!(target_os = "linux") {
+    return None;
+  }
+
+  // Not needed on X11
+  if !cfg!(feature = "wayland") {
+    return None;
+  }
+
+  if let Some(keyboard_config) = config.keyboard_layout() {
+    Some(keyboard_config)
+  } else if let Some(active_layout) = espanso_detect::get_active_layout() {
+    Some(RMLVOConfig {
+      layout: Some(active_layout),
+      ..Default::default()
+    })
+  } else {
+    warn!("unable to determine keyboard layout automatically, please explicitly specify it in the configuration.");
+    None
+  }
+}
+
+pub fn generate_detect_rmlvo(config: &dyn Config) -> Option<espanso_detect::KeyboardConfig> {
+  generate_rmlvo_config(config)
+    .map(|config| {
+      info!("detection module will use this keyboard layout: {}", config);
+      config
+    })
+    .map(|config| espanso_detect::KeyboardConfig {
+      rules: config.rules,
+      model: config.model,
+      layout: config.layout,
+      variant: config.variant,
+      options: config.options,
+    })
+}
+
+pub fn generate_inject_rmlvo(config: &dyn Config) -> Option<espanso_inject::KeyboardConfig> {
+  generate_rmlvo_config(config)
+    .map(|config| {
+      info!("inject module will use this keyboard layout: {}", config);
+      config
+    })
+    .map(|config| espanso_inject::KeyboardConfig {
+      rules: config.rules,
+      model: config.model,
+      layout: config.layout,
+      variant: config.variant,
+      options: config.options,
+    })
+}
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
new file mode 100644
index 00000000..53e21b5b
--- /dev/null
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -0,0 +1,313 @@
+/*
+ * 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::thread::JoinHandle;
+
+use anyhow::Result;
+use crossbeam::channel::Receiver;
+use espanso_config::{config::ConfigStore, matches::store::MatchStore};
+use espanso_detect::SourceCreationOptions;
+use espanso_engine::event::ExitMode;
+use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider};
+use espanso_path::Paths;
+use espanso_ui::{event::UIEvent, UIRemote};
+use log::{debug, error, info, warn};
+
+use crate::{
+  cli::worker::{
+    context::Context,
+    engine::{
+      dispatch::executor::{
+        clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter,
+        event_injector::EventInjectorAdapter, icon::IconHandlerAdapter,
+        key_injector::KeyInjectorAdapter, secure_input::SecureInputManagerAdapter,
+      },
+      process::middleware::{
+        image_resolve::PathProviderAdapter,
+        match_select::MatchSelectorAdapter,
+        matcher::{
+          convert::MatchConverter,
+          regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions},
+          rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions},
+        },
+        multiplex::MultiplexAdapter,
+        render::{
+          extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter},
+          RendererAdapter,
+        },
+      },
+    },
+    match_cache::{CombinedMatchCache, MatchCache},
+    ui::notification::NotificationManager,
+  },
+  common_flags::{
+    WORKER_START_REASON_CONFIG_CHANGED, WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED,
+    WORKER_START_REASON_MANUAL,
+  },
+  preferences::Preferences,
+};
+
+use super::secure_input::SecureInputEvent;
+
+mod caches;
+pub mod dispatch;
+pub mod funnel;
+mod keyboard_layout_util;
+pub mod process;
+
+#[allow(clippy::too_many_arguments)]
+pub fn initialize_and_spawn(
+  paths: Paths,
+  config_store: Box<dyn ConfigStore>,
+  match_store: Box<dyn MatchStore>,
+  ui_remote: Box<dyn UIRemote>,
+  exit_signal: Receiver<ExitMode>,
+  ui_event_receiver: Receiver<UIEvent>,
+  secure_input_receiver: Receiver<SecureInputEvent>,
+  use_evdev_backend: bool,
+  start_reason: Option<String>,
+) -> Result<JoinHandle<ExitMode>> {
+  let handle = std::thread::Builder::new()
+    .name("engine thread".to_string())
+    .spawn(move || {
+      // TODO: properly order the initializations if necessary
+      let preferences =
+        crate::preferences::get_default(&paths.runtime).expect("unable to load preferences");
+
+      let app_info_provider =
+        espanso_info::get_provider().expect("unable to initialize app info provider");
+      // TODO: read interval from configs?
+      let cached_app_info_provider = caches::app_info_provider::CachedAppInfoProvider::from(
+        &*app_info_provider,
+        std::time::Duration::from_millis(400),
+      );
+      let config_manager =
+        super::config::ConfigManager::new(&*config_store, &*match_store, &cached_app_info_provider);
+      let match_cache = MatchCache::load(&*config_store, &*match_store);
+      let default_config = &*config_manager.default();
+
+      let modulo_manager = crate::gui::modulo::manager::ModuloManager::new();
+      let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager);
+      let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager);
+
+      let context: Box<dyn Context> = Box::new(super::context::DefaultContext::new(
+        &config_manager,
+        &cached_app_info_provider,
+      ));
+      let builtin_matches = super::builtin::get_builtin_matches(&*config_manager.default());
+      let combined_match_cache = CombinedMatchCache::load(&match_cache, &builtin_matches);
+
+      let match_converter = MatchConverter::new(&*config_store, &*match_store, &builtin_matches);
+
+      let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend);
+
+      // TODO: pass all the options
+      let (detect_source, modifier_state_store, sequencer, key_state_store) =
+        super::engine::funnel::init_and_spawn(SourceCreationOptions {
+          use_evdev: use_evdev_backend,
+          evdev_keyboard_rmlvo: keyboard_layout_util::generate_detect_rmlvo(
+            &*config_manager.default(),
+          ),
+          hotkeys: match_converter.get_hotkeys(),
+          win32_exclude_orphan_events: default_config.win32_exclude_orphan_events(),
+        })
+        .expect("failed to initialize detector module");
+      let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
+      let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
+      let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
+        secure_input_receiver,
+        &sequencer,
+      );
+      let sources: Vec<&dyn espanso_engine::funnel::Source> = vec![
+        &detect_source,
+        &exit_source,
+        &ui_source,
+        &secure_input_source,
+      ];
+      let funnel = espanso_engine::funnel::default(&sources);
+
+      let rolling_matcher = RollingMatcherAdapter::new(
+        &match_converter.get_rolling_matches(),
+        RollingMatcherAdapterOptions {
+          char_word_separators: config_manager.default().word_separators(),
+        },
+      );
+      let regex_matcher = RegexMatcherAdapter::new(
+        &match_converter.get_regex_matches(),
+        &RegexMatcherAdapterOptions {
+          max_buffer_size: 30, // TODO: load from configs
+        },
+      );
+      let matchers: Vec<
+        &dyn espanso_engine::process::Matcher<
+          super::engine::process::middleware::matcher::MatcherState,
+        >,
+      > = vec![&rolling_matcher, &regex_matcher];
+      let selector = MatchSelectorAdapter::new(&modulo_search_ui, &combined_match_cache);
+      let multiplexer = MultiplexAdapter::new(&combined_match_cache, &*context);
+
+      let injector = espanso_inject::get_injector(InjectorCreationOptions {
+        use_evdev: use_evdev_backend,
+        keyboard_state_provider: key_state_store
+          .map(|store| Box::new(store) as Box<dyn KeyboardStateProvider>),
+        evdev_keyboard_rmlvo: keyboard_layout_util::generate_inject_rmlvo(
+          &*config_manager.default(),
+        ),
+        ..Default::default()
+      })
+      .expect("failed to initialize injector module"); // TODO: handle the options
+      let clipboard = espanso_clipboard::get_clipboard(Default::default())
+        .expect("failed to initialize clipboard module"); // TODO: handle options
+
+      let clipboard_adapter = ClipboardAdapter::new(&*clipboard);
+      let clipboard_extension =
+        espanso_render::extension::clipboard::ClipboardExtension::new(&clipboard_adapter);
+      let date_extension = espanso_render::extension::date::DateExtension::new();
+      let echo_extension = espanso_render::extension::echo::EchoExtension::new();
+      // For backwards compatiblity purposes, the echo extension can also be called with "dummy" type
+      let dummy_extension = espanso_render::extension::echo::EchoExtension::new_with_alias("dummy");
+      let random_extension = espanso_render::extension::random::RandomExtension::new();
+      let home_path = dirs::home_dir().expect("unable to obtain home dir path");
+      let script_extension = espanso_render::extension::script::ScriptExtension::new(
+        &paths.config,
+        &home_path,
+        &paths.packages,
+      );
+      let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config);
+      let form_adapter = FormProviderAdapter::new(&modulo_form_ui);
+      let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter);
+      let renderer = espanso_render::create(vec![
+        &clipboard_extension,
+        &date_extension,
+        &echo_extension,
+        &dummy_extension,
+        &random_extension,
+        &script_extension,
+        &shell_extension,
+        &form_extension,
+      ]);
+      let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer);
+      let path_provider = PathProviderAdapter::new(&paths);
+
+      let disable_options =
+        process::middleware::disable::extract_disable_options(&*config_manager.default());
+
+      let mut processor = espanso_engine::process::default(
+        &matchers,
+        &config_manager,
+        &selector,
+        &multiplexer,
+        &renderer_adapter,
+        &match_cache,
+        &modifier_state_store,
+        &sequencer,
+        &path_provider,
+        disable_options,
+        &config_manager,
+        &combined_match_cache,
+        &config_manager,
+        &config_manager,
+      );
+
+      let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);
+      let clipboard_injector =
+        ClipboardInjectorAdapter::new(&*injector, &*clipboard, &config_manager);
+      let key_injector = KeyInjectorAdapter::new(&*injector, &config_manager);
+      let context_menu_adapter = ContextMenuHandlerAdapter::new(&*ui_remote);
+      let icon_adapter = IconHandlerAdapter::new(&*ui_remote);
+      let secure_input_adapter = SecureInputManagerAdapter::new();
+      let dispatcher = espanso_engine::dispatch::default(
+        &event_injector,
+        &clipboard_injector,
+        &config_manager,
+        &key_injector,
+        &clipboard_injector,
+        &clipboard_injector,
+        &context_menu_adapter,
+        &icon_adapter,
+        &secure_input_adapter,
+      );
+
+      // Disable previously granted linux capabilities if not needed anymore
+      if has_granted_capabilities {
+        if let Err(err) = crate::capabilities::clear_capabilities() {
+          error!("unable to revoke linux capabilities: {}", err);
+        }
+      }
+
+      let notification_manager = NotificationManager::new(&*ui_remote, default_config);
+
+      match start_reason.as_deref() {
+        Some(flag) if flag == WORKER_START_REASON_CONFIG_CHANGED => {
+          notification_manager.notify_config_reloaded(false);
+        }
+        Some(flag) if flag == WORKER_START_REASON_MANUAL => {
+          notification_manager.notify_config_reloaded(true);
+        }
+        Some(flag) if flag == WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED => {
+          notification_manager.notify_keyboard_layout_reloaded()
+        }
+        _ => {
+          notification_manager.notify_start();
+
+          if !preferences.has_displayed_welcome() {
+            super::ui::welcome::show_welcome_screen();
+            preferences.set_has_displayed_welcome(true);
+          }
+        }
+      }
+
+      let mut engine = espanso_engine::Engine::new(&funnel, &mut processor, &dispatcher);
+      let exit_mode = engine.run();
+
+      info!("engine eventloop has terminated, propagating exit event...");
+      ui_remote.exit();
+
+      exit_mode
+    })?;
+
+  Ok(handle)
+}
+
+fn grant_linux_capabilities(use_evdev_backend: bool) -> bool {
+  if use_evdev_backend {
+    if crate::capabilities::can_use_capabilities() {
+      debug!("using linux capabilities to grant permissions needed by EVDEV backend");
+      if let Err(err) = crate::capabilities::grant_capabilities() {
+        error!("unable to grant CAP_DAC_OVERRIDE capability: {}", err);
+        false
+      } else {
+        debug!("successfully granted permissions using capabilities");
+        true
+      }
+    } else {
+      warn!("EVDEV backend is being used, but without enabling linux capabilities.");
+      warn!("  Although you CAN run espanso EVDEV backend as root, it's not recommended due");
+      warn!(
+        "  to security reasons. Espanso supports linux capabilities to limit the attack surface"
+      );
+      warn!("  area by only leveraging on the CAP_DAC_OVERRIDE capability (needed to work with");
+      warn!("  /dev/input/* devices to detect and inject text) and disabling it as soon as the");
+      warn!("  initial setup is completed.");
+      false
+    }
+  } else {
+    false
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/disable.rs b/espanso/src/cli/worker/engine/process/middleware/disable.rs
new file mode 100644
index 00000000..b78e28cd
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/disable.rs
@@ -0,0 +1,52 @@
+/*
+ * 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::time::Duration;
+
+use espanso_config::config::Config;
+use espanso_engine::{
+  event::input::{Key, Variant},
+  process::DisableOptions,
+};
+
+pub fn extract_disable_options(config: &dyn Config) -> DisableOptions {
+  let (toggle_key, variant) = match config.toggle_key() {
+    Some(key) => match key {
+      espanso_config::config::ToggleKey::Ctrl => (Some(Key::Control), None),
+      espanso_config::config::ToggleKey::Meta => (Some(Key::Meta), None),
+      espanso_config::config::ToggleKey::Alt => (Some(Key::Alt), None),
+      espanso_config::config::ToggleKey::Shift => (Some(Key::Shift), None),
+      espanso_config::config::ToggleKey::RightCtrl => (Some(Key::Control), Some(Variant::Right)),
+      espanso_config::config::ToggleKey::RightAlt => (Some(Key::Alt), Some(Variant::Right)),
+      espanso_config::config::ToggleKey::RightShift => (Some(Key::Shift), Some(Variant::Right)),
+      espanso_config::config::ToggleKey::RightMeta => (Some(Key::Meta), Some(Variant::Right)),
+      espanso_config::config::ToggleKey::LeftCtrl => (Some(Key::Control), Some(Variant::Left)),
+      espanso_config::config::ToggleKey::LeftAlt => (Some(Key::Alt), Some(Variant::Left)),
+      espanso_config::config::ToggleKey::LeftShift => (Some(Key::Shift), Some(Variant::Left)),
+      espanso_config::config::ToggleKey::LeftMeta => (Some(Key::Meta), Some(Variant::Left)),
+    },
+    None => (None, None),
+  };
+
+  DisableOptions {
+    toggle_key,
+    toggle_key_variant: variant,
+    toggle_key_maximum_window: Duration::from_millis(1000),
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/image_resolve.rs b/espanso/src/cli/worker/engine/process/middleware/image_resolve.rs
new file mode 100644
index 00000000..2b1bf884
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/image_resolve.rs
@@ -0,0 +1,38 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_path::Paths;
+
+use espanso_engine::process::PathProvider;
+
+pub struct PathProviderAdapter<'a> {
+  paths: &'a Paths,
+}
+
+impl<'a> PathProviderAdapter<'a> {
+  pub fn new(paths: &'a Paths) -> Self {
+    Self { paths }
+  }
+}
+
+impl<'a> PathProvider for PathProviderAdapter<'a> {
+  fn get_config_path(&self) -> &std::path::Path {
+    &self.paths.config
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/match_select.rs b/espanso/src/cli/worker/engine/process/middleware/match_select.rs
new file mode 100644
index 00000000..b318bc2b
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/match_select.rs
@@ -0,0 +1,95 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::process::MatchSelector;
+use log::error;
+
+use crate::gui::{SearchItem, SearchUI};
+
+const MAX_LABEL_LEN: usize = 100;
+
+pub trait MatchProvider<'a> {
+  fn get_matches(&self, ids: &[i32]) -> Vec<MatchSummary<'a>>;
+}
+
+pub struct MatchSummary<'a> {
+  pub id: i32,
+  pub label: &'a str,
+  pub tag: Option<&'a str>,
+  pub is_builtin: bool,
+}
+
+pub struct MatchSelectorAdapter<'a> {
+  search_ui: &'a dyn SearchUI,
+  match_provider: &'a dyn MatchProvider<'a>,
+}
+
+impl<'a> MatchSelectorAdapter<'a> {
+  pub fn new(search_ui: &'a dyn SearchUI, match_provider: &'a dyn MatchProvider<'a>) -> Self {
+    Self {
+      search_ui,
+      match_provider,
+    }
+  }
+}
+
+impl<'a> MatchSelector for MatchSelectorAdapter<'a> {
+  fn select(&self, matches_ids: &[i32], is_search: bool) -> Option<i32> {
+    let matches = self.match_provider.get_matches(matches_ids);
+    let search_items: Vec<SearchItem> = matches
+      .into_iter()
+      .map(|m| {
+        let clipped_label = &m.label[..std::cmp::min(m.label.len(), MAX_LABEL_LEN)];
+
+        SearchItem {
+          id: m.id.to_string(),
+          label: clipped_label.to_string(),
+          tag: m.tag.map(String::from),
+          is_builtin: m.is_builtin,
+        }
+      })
+      .collect();
+
+    let hint = if is_search {
+      Some("Search matches by content or trigger (or type > to see commands)")
+    } else {
+      None
+    };
+
+    match self.search_ui.show(&search_items, hint) {
+      Ok(Some(selected_id)) => match selected_id.parse::<i32>() {
+        Ok(id) => Some(id),
+        Err(err) => {
+          error!(
+            "match selector received an invalid id from SearchUI: {}",
+            err
+          );
+          None
+        }
+      },
+      Ok(None) => None,
+      Err(err) => {
+        error!("SearchUI reported an error: {}", err);
+        None
+      }
+    }
+  }
+}
+
+// TODO: test
diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs
new file mode 100644
index 00000000..71cfa706
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs
@@ -0,0 +1,131 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_config::{
+  config::ConfigStore,
+  matches::{
+    store::{MatchSet, MatchStore},
+    MatchCause,
+  },
+};
+use espanso_detect::hotkey::HotKey;
+use espanso_match::{
+  regex::RegexMatch,
+  rolling::{RollingMatch, StringMatchOptions},
+};
+use log::error;
+
+use crate::cli::worker::builtin::BuiltInMatch;
+
+pub struct MatchConverter<'a> {
+  config_store: &'a dyn ConfigStore,
+  match_store: &'a dyn MatchStore,
+  builtin_matches: &'a [BuiltInMatch],
+}
+
+impl<'a> MatchConverter<'a> {
+  pub fn new(
+    config_store: &'a dyn ConfigStore,
+    match_store: &'a dyn MatchStore,
+    builtin_matches: &'a [BuiltInMatch],
+  ) -> Self {
+    Self {
+      config_store,
+      match_store,
+      builtin_matches,
+    }
+  }
+
+  // TODO: test (might need to move the conversion logic into a separate function)
+  pub fn get_rolling_matches(&self) -> Vec<RollingMatch<i32>> {
+    let match_set = self.global_match_set();
+    let mut matches = Vec::new();
+
+    // First convert configuration (user-defined) matches
+    for m in match_set.matches {
+      if let MatchCause::Trigger(cause) = &m.cause {
+        for trigger in cause.triggers.iter() {
+          matches.push(RollingMatch::from_string(
+            m.id,
+            trigger,
+            &StringMatchOptions {
+              case_insensitive: cause.propagate_case,
+              left_word: cause.left_word,
+              right_word: cause.right_word,
+            },
+          ))
+        }
+      }
+    }
+
+    // Then convert built-in ones
+    for m in self.builtin_matches {
+      for trigger in m.triggers.iter() {
+        matches.push(RollingMatch::from_string(
+          m.id,
+          trigger,
+          &StringMatchOptions::default(),
+        ))
+      }
+    }
+
+    matches
+  }
+
+  // TODO: test (might need to move the conversion logic into a separate function)
+  pub fn get_regex_matches(&self) -> Vec<RegexMatch<i32>> {
+    let match_set = self.global_match_set();
+    let mut matches = Vec::new();
+
+    for m in match_set.matches {
+      if let MatchCause::Regex(cause) = &m.cause {
+        matches.push(RegexMatch::new(m.id, &cause.regex))
+      }
+    }
+
+    matches
+  }
+
+  pub fn get_hotkeys(&self) -> Vec<HotKey> {
+    let mut hotkeys = Vec::new();
+
+    // TODO: read user-defined matches
+
+    // Then convert built-in ones
+    for m in self.builtin_matches {
+      if let Some(hotkey) = &m.hotkey {
+        match HotKey::new(m.id, hotkey) {
+          Ok(hotkey) => hotkeys.push(hotkey),
+          Err(err) => {
+            error!("unable to register hotkey: {}, with error: {}", hotkey, err);
+          }
+        }
+      }
+    }
+
+    hotkeys
+  }
+
+  fn global_match_set(&self) -> MatchSet {
+    let paths = self.config_store.get_all_match_paths();
+    self
+      .match_store
+      .query(&paths.into_iter().collect::<Vec<_>>())
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs
new file mode 100644
index 00000000..9eda6385
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/matcher/mod.rs
@@ -0,0 +1,102 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::{
+  event::input::Key,
+  process::{MatchResult, MatcherEvent},
+};
+use espanso_match::regex::RegexMatcherState;
+use espanso_match::rolling::matcher::RollingMatcherState;
+
+use enum_as_inner::EnumAsInner;
+
+pub mod convert;
+pub mod regex;
+pub mod rolling;
+
+#[derive(Clone, EnumAsInner)]
+pub enum MatcherState<'a> {
+  Rolling(RollingMatcherState<'a, i32>),
+  Regex(RegexMatcherState),
+}
+
+pub fn convert_to_match_event(event: &MatcherEvent) -> espanso_match::event::Event {
+  match event {
+    MatcherEvent::Key { key, chars } => espanso_match::event::Event::Key {
+      key: convert_to_match_key(key.clone()),
+      chars: chars.to_owned(),
+    },
+    MatcherEvent::VirtualSeparator => espanso_match::event::Event::VirtualSeparator,
+  }
+}
+
+pub fn convert_to_engine_result(result: espanso_match::MatchResult<i32>) -> MatchResult {
+  MatchResult {
+    id: result.id,
+    trigger: result.trigger,
+    left_separator: result.left_separator,
+    right_separator: result.right_separator,
+    args: result.vars,
+  }
+}
+
+pub fn convert_to_match_key(key: Key) -> espanso_match::event::Key {
+  match key {
+    Key::Alt => espanso_match::event::Key::Alt,
+    Key::CapsLock => espanso_match::event::Key::CapsLock,
+    Key::Control => espanso_match::event::Key::Control,
+    Key::Meta => espanso_match::event::Key::Meta,
+    Key::NumLock => espanso_match::event::Key::NumLock,
+    Key::Shift => espanso_match::event::Key::Shift,
+    Key::Enter => espanso_match::event::Key::Enter,
+    Key::Tab => espanso_match::event::Key::Tab,
+    Key::Space => espanso_match::event::Key::Space,
+    Key::ArrowDown => espanso_match::event::Key::ArrowDown,
+    Key::ArrowLeft => espanso_match::event::Key::ArrowLeft,
+    Key::ArrowRight => espanso_match::event::Key::ArrowRight,
+    Key::ArrowUp => espanso_match::event::Key::ArrowUp,
+    Key::End => espanso_match::event::Key::End,
+    Key::Home => espanso_match::event::Key::Home,
+    Key::PageDown => espanso_match::event::Key::PageDown,
+    Key::PageUp => espanso_match::event::Key::PageUp,
+    Key::Escape => espanso_match::event::Key::Escape,
+    Key::Backspace => espanso_match::event::Key::Backspace,
+    Key::F1 => espanso_match::event::Key::F1,
+    Key::F2 => espanso_match::event::Key::F2,
+    Key::F3 => espanso_match::event::Key::F3,
+    Key::F4 => espanso_match::event::Key::F4,
+    Key::F5 => espanso_match::event::Key::F5,
+    Key::F6 => espanso_match::event::Key::F6,
+    Key::F7 => espanso_match::event::Key::F7,
+    Key::F8 => espanso_match::event::Key::F8,
+    Key::F9 => espanso_match::event::Key::F9,
+    Key::F10 => espanso_match::event::Key::F10,
+    Key::F11 => espanso_match::event::Key::F11,
+    Key::F12 => espanso_match::event::Key::F12,
+    Key::F13 => espanso_match::event::Key::F13,
+    Key::F14 => espanso_match::event::Key::F14,
+    Key::F15 => espanso_match::event::Key::F15,
+    Key::F16 => espanso_match::event::Key::F16,
+    Key::F17 => espanso_match::event::Key::F17,
+    Key::F18 => espanso_match::event::Key::F18,
+    Key::F19 => espanso_match::event::Key::F19,
+    Key::F20 => espanso_match::event::Key::F20,
+    Key::Other(_) => espanso_match::event::Key::Other,
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/regex.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/regex.rs
new file mode 100644
index 00000000..313ca02a
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/matcher/regex.rs
@@ -0,0 +1,70 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_engine::process::{MatchResult, Matcher, MatcherEvent};
+use espanso_match::regex::{RegexMatch, RegexMatcher, RegexMatcherOptions};
+
+use super::{convert_to_engine_result, convert_to_match_event, MatcherState};
+
+pub struct RegexMatcherAdapterOptions {
+  pub max_buffer_size: usize,
+}
+
+pub struct RegexMatcherAdapter {
+  matcher: RegexMatcher<i32>,
+}
+
+impl RegexMatcherAdapter {
+  pub fn new(matches: &[RegexMatch<i32>], options: &RegexMatcherAdapterOptions) -> Self {
+    let matcher = RegexMatcher::new(
+      matches,
+      RegexMatcherOptions {
+        max_buffer_size: options.max_buffer_size,
+      },
+    );
+
+    Self { matcher }
+  }
+}
+
+impl<'a> Matcher<'a, MatcherState<'a>> for RegexMatcherAdapter {
+  fn process(
+    &'a self,
+    prev_state: Option<&MatcherState<'a>>,
+    event: &MatcherEvent,
+  ) -> (MatcherState<'a>, Vec<MatchResult>) {
+    use espanso_match::Matcher;
+
+    let prev_state = prev_state.map(|state| {
+      if let Some(state) = state.as_regex() {
+        state
+      } else {
+        panic!("invalid state type received in RegexMatcherAdapter")
+      }
+    });
+    let event = convert_to_match_event(event);
+
+    let (state, results) = self.matcher.process(prev_state, event);
+
+    let enum_state = MatcherState::Regex(state);
+    let results: Vec<MatchResult> = results.into_iter().map(convert_to_engine_result).collect();
+
+    (enum_state, results)
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/rolling.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/rolling.rs
new file mode 100644
index 00000000..f5d96b00
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/matcher/rolling.rs
@@ -0,0 +1,75 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_match::rolling::{
+  matcher::{RollingMatcher, RollingMatcherOptions},
+  RollingMatch,
+};
+
+use espanso_engine::process::{MatchResult, Matcher, MatcherEvent};
+
+use super::{convert_to_engine_result, convert_to_match_event, MatcherState};
+
+pub struct RollingMatcherAdapterOptions {
+  pub char_word_separators: Vec<String>,
+}
+
+pub struct RollingMatcherAdapter {
+  matcher: RollingMatcher<i32>,
+}
+
+impl RollingMatcherAdapter {
+  pub fn new(matches: &[RollingMatch<i32>], options: RollingMatcherAdapterOptions) -> Self {
+    let matcher = RollingMatcher::new(
+      matches,
+      RollingMatcherOptions {
+        char_word_separators: options.char_word_separators,
+        key_word_separators: vec![], // TODO?
+      },
+    );
+
+    Self { matcher }
+  }
+}
+
+impl<'a> Matcher<'a, MatcherState<'a>> for RollingMatcherAdapter {
+  fn process(
+    &'a self,
+    prev_state: Option<&MatcherState<'a>>,
+    event: &MatcherEvent,
+  ) -> (MatcherState<'a>, Vec<MatchResult>) {
+    use espanso_match::Matcher;
+
+    let prev_state = prev_state.map(|state| {
+      if let Some(state) = state.as_rolling() {
+        state
+      } else {
+        panic!("invalid state type received in RollingMatcherAdapter")
+      }
+    });
+    let event = convert_to_match_event(event);
+
+    let (state, results) = self.matcher.process(prev_state, event);
+
+    let enum_state = MatcherState::Rolling(state);
+    let results: Vec<MatchResult> = results.into_iter().map(convert_to_engine_result).collect();
+
+    (enum_state, results)
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/mod.rs b/espanso/src/cli/worker/engine/process/middleware/mod.rs
new file mode 100644
index 00000000..d3998bf3
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/mod.rs
@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+ */
+
+pub mod disable;
+pub mod image_resolve;
+pub mod match_select;
+pub mod matcher;
+pub mod multiplex;
+pub mod render;
diff --git a/espanso/src/cli/worker/engine/process/middleware/multiplex.rs b/espanso/src/cli/worker/engine/process/middleware/multiplex.rs
new file mode 100644
index 00000000..be197ba5
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/multiplex.rs
@@ -0,0 +1,81 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_config::matches::{Match, MatchEffect};
+
+use crate::cli::worker::{builtin::BuiltInMatch, context::Context};
+use espanso_engine::{
+  event::{
+    internal::DetectedMatch,
+    internal::{ImageRequestedEvent, RenderingRequestedEvent, TextFormat},
+    EventType,
+  },
+  process::Multiplexer,
+};
+
+pub trait MatchProvider<'a> {
+  fn get(&self, match_id: i32) -> Option<MatchResult<'a>>;
+}
+
+pub enum MatchResult<'a> {
+  User(&'a Match),
+  Builtin(&'a BuiltInMatch),
+}
+
+pub struct MultiplexAdapter<'a> {
+  provider: &'a dyn MatchProvider<'a>,
+  context: &'a dyn Context,
+}
+
+impl<'a> MultiplexAdapter<'a> {
+  pub fn new(provider: &'a dyn MatchProvider<'a>, context: &'a dyn Context) -> Self {
+    Self { provider, context }
+  }
+}
+
+impl<'a> Multiplexer for MultiplexAdapter<'a> {
+  fn convert(&self, detected_match: DetectedMatch) -> Option<EventType> {
+    match self.provider.get(detected_match.id)? {
+      MatchResult::User(m) => match &m.effect {
+        MatchEffect::Text(effect) => Some(EventType::RenderingRequested(RenderingRequestedEvent {
+          match_id: detected_match.id,
+          trigger: detected_match.trigger,
+          left_separator: detected_match.left_separator,
+          right_separator: detected_match.right_separator,
+          trigger_args: detected_match.args,
+          format: convert_format(&effect.format),
+        })),
+        MatchEffect::Image(effect) => Some(EventType::ImageRequested(ImageRequestedEvent {
+          match_id: detected_match.id,
+          image_path: effect.path.clone(),
+        })),
+        MatchEffect::None => None,
+      },
+      MatchResult::Builtin(m) => Some((m.action)(self.context)),
+    }
+  }
+}
+
+fn convert_format(format: &espanso_config::matches::TextFormat) -> TextFormat {
+  match format {
+    espanso_config::matches::TextFormat::Plain => TextFormat::Plain,
+    espanso_config::matches::TextFormat::Markdown => TextFormat::Markdown,
+    espanso_config::matches::TextFormat::Html => TextFormat::Html,
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs
new file mode 100644
index 00000000..57b2c913
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs
@@ -0,0 +1,37 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_clipboard::Clipboard;
+use espanso_render::extension::clipboard::ClipboardProvider;
+
+pub struct ClipboardAdapter<'a> {
+  clipboard: &'a dyn Clipboard,
+}
+
+impl<'a> ClipboardAdapter<'a> {
+  pub fn new(clipboard: &'a dyn Clipboard) -> Self {
+    Self { clipboard }
+  }
+}
+
+impl<'a> ClipboardProvider for ClipboardAdapter<'a> {
+  fn get_text(&self) -> Option<String> {
+    self.clipboard.get_text()
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs
new file mode 100644
index 00000000..dd9fbe76
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/form.rs
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+use espanso_render::{
+  extension::form::{FormProvider, FormProviderResult},
+  Params, Value,
+};
+use log::error;
+
+use crate::gui::{FormField, FormUI};
+
+pub struct FormProviderAdapter<'a> {
+  form_ui: &'a dyn FormUI,
+}
+
+impl<'a> FormProviderAdapter<'a> {
+  pub fn new(form_ui: &'a dyn FormUI) -> Self {
+    Self { form_ui }
+  }
+}
+
+impl<'a> FormProvider for FormProviderAdapter<'a> {
+  fn show(&self, layout: &str, fields: &Params, _: &Params) -> FormProviderResult {
+    let fields = convert_fields(fields);
+    match self.form_ui.show(layout, &fields) {
+      Ok(Some(results)) => FormProviderResult::Success(results),
+      Ok(None) => FormProviderResult::Aborted,
+      Err(err) => FormProviderResult::Error(err),
+    }
+  }
+}
+
+// TODO: test
+fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
+  let mut out = HashMap::new();
+  for (name, field) in fields {
+    let mut form_field = None;
+
+    if let Value::Object(params) = field {
+      if let Some(Value::String(field_type)) = params.get("type") {
+        form_field = match field_type.as_str() {
+          "text" => Some(FormField::Text {
+            default: params
+              .get("default")
+              .and_then(|val| val.as_string())
+              .cloned(),
+            multiline: params
+              .get("multiline")
+              .and_then(|val| val.as_bool())
+              .cloned()
+              .unwrap_or(false),
+          }),
+          "choice" => Some(FormField::Choice {
+            default: params
+              .get("default")
+              .and_then(|val| val.as_string())
+              .cloned(),
+            values: params
+              .get("values")
+              .and_then(|val| val.as_array())
+              .map(|arr| {
+                arr
+                  .iter()
+                  .flat_map(|choice| choice.as_string())
+                  .cloned()
+                  .collect()
+              })
+              .unwrap_or_default(),
+          }),
+          "list" => Some(FormField::List {
+            default: params
+              .get("default")
+              .and_then(|val| val.as_string())
+              .cloned(),
+            values: params
+              .get("values")
+              .and_then(|val| val.as_array())
+              .map(|arr| {
+                arr
+                  .iter()
+                  .flat_map(|choice| choice.as_string())
+                  .cloned()
+                  .collect()
+              })
+              .unwrap_or_default(),
+          }),
+          _ => None,
+        }
+      }
+    }
+
+    if let Some(form_field) = form_field {
+      out.insert(name.clone(), form_field);
+    } else {
+      error!("malformed form field format for '{}'", name);
+    }
+  }
+  out
+}
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs
new file mode 100644
index 00000000..d7cafacc
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs
@@ -0,0 +1,21 @@
+/*
+ * 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/>.
+ */
+
+pub mod clipboard;
+pub mod form;
diff --git a/espanso/src/cli/worker/engine/process/middleware/render/mod.rs b/espanso/src/cli/worker/engine/process/middleware/render/mod.rs
new file mode 100644
index 00000000..e2f73ea6
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/middleware/render/mod.rs
@@ -0,0 +1,315 @@
+/*
+ * 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::{cell::RefCell, collections::HashMap, sync::Arc};
+
+pub mod extension;
+
+use espanso_config::{
+  config::Config,
+  matches::{store::MatchSet, Match, MatchCause, MatchEffect, UpperCasingStyle},
+};
+use espanso_render::{CasingStyle, Context, RenderOptions, Template, Value, Variable};
+
+use espanso_engine::process::{Renderer, RendererError};
+
+pub trait MatchProvider<'a> {
+  fn matches(&self) -> Vec<&'a Match>;
+  fn get(&self, id: i32) -> Option<&'a Match>;
+}
+
+pub trait ConfigProvider<'a> {
+  fn configs(&self) -> Vec<(Arc<dyn Config>, MatchSet)>;
+  fn active(&self) -> (Arc<dyn Config>, MatchSet);
+}
+
+pub struct RendererAdapter<'a> {
+  renderer: &'a dyn espanso_render::Renderer,
+  match_provider: &'a dyn MatchProvider<'a>,
+  config_provider: &'a dyn ConfigProvider<'a>,
+
+  template_map: HashMap<i32, Option<Template>>,
+  global_vars_map: HashMap<i32, Variable>,
+
+  context_cache: RefCell<HashMap<i32, Context<'a>>>,
+}
+
+impl<'a> RendererAdapter<'a> {
+  pub fn new(
+    match_provider: &'a dyn MatchProvider<'a>,
+    config_provider: &'a dyn ConfigProvider<'a>,
+    renderer: &'a dyn espanso_render::Renderer,
+  ) -> Self {
+    let template_map = generate_template_map(match_provider);
+    let global_vars_map = generate_global_vars_map(config_provider);
+
+    Self {
+      renderer,
+      config_provider,
+      match_provider,
+      template_map,
+      global_vars_map,
+      context_cache: RefCell::new(HashMap::new()),
+    }
+  }
+}
+
+// TODO: test
+fn generate_template_map(match_provider: &dyn MatchProvider) -> HashMap<i32, Option<Template>> {
+  let mut template_map = HashMap::new();
+  for m in match_provider.matches() {
+    let entry = convert_to_template(m);
+    template_map.insert(m.id, entry);
+  }
+  template_map
+}
+
+// TODO: test
+fn generate_global_vars_map(config_provider: &dyn ConfigProvider) -> HashMap<i32, Variable> {
+  let mut global_vars_map = HashMap::new();
+
+  for (_, match_set) in config_provider.configs() {
+    for var in match_set.global_vars.iter() {
+      global_vars_map
+        .entry(var.id)
+        .or_insert_with(|| convert_var((*var).clone()));
+    }
+  }
+
+  global_vars_map
+}
+
+// TODO: test
+fn generate_context<'a>(
+  match_set: &MatchSet,
+  template_map: &'a HashMap<i32, Option<Template>>,
+  global_vars_map: &'a HashMap<i32, Variable>,
+) -> Context<'a> {
+  let mut templates = Vec::new();
+  let mut global_vars = Vec::new();
+
+  for m in match_set.matches.iter() {
+    if let Some(Some(template)) = template_map.get(&m.id) {
+      templates.push(template);
+    }
+  }
+
+  for var in match_set.global_vars.iter() {
+    if let Some(var) = global_vars_map.get(&var.id) {
+      global_vars.push(var);
+    }
+  }
+
+  Context {
+    global_vars,
+    templates,
+  }
+}
+
+// TODO: move conversion methods to new file?
+
+fn convert_to_template(m: &Match) -> Option<Template> {
+  if let MatchEffect::Text(text_effect) = &m.effect {
+    let ids = if let MatchCause::Trigger(cause) = &m.cause {
+      cause.triggers.clone()
+    } else {
+      Vec::new()
+    };
+
+    Some(Template {
+      ids,
+      body: text_effect.replace.clone(),
+      vars: convert_vars(text_effect.vars.clone()),
+    })
+  } else {
+    None
+  }
+}
+
+fn convert_vars(vars: Vec<espanso_config::matches::Variable>) -> Vec<espanso_render::Variable> {
+  vars.into_iter().map(convert_var).collect()
+}
+
+fn convert_var(var: espanso_config::matches::Variable) -> espanso_render::Variable {
+  Variable {
+    name: var.name,
+    var_type: var.var_type,
+    params: convert_params(var.params),
+  }
+}
+
+fn convert_params(params: espanso_config::matches::Params) -> espanso_render::Params {
+  let mut new_params = espanso_render::Params::new();
+  for (key, value) in params {
+    new_params.insert(key, convert_value(value));
+  }
+  new_params
+}
+
+fn convert_value(value: espanso_config::matches::Value) -> espanso_render::Value {
+  match value {
+    espanso_config::matches::Value::Null => espanso_render::Value::Null,
+    espanso_config::matches::Value::Bool(v) => espanso_render::Value::Bool(v),
+    espanso_config::matches::Value::Number(n) => match n {
+      espanso_config::matches::Number::Integer(i) => {
+        espanso_render::Value::Number(espanso_render::Number::Integer(i))
+      }
+      espanso_config::matches::Number::Float(f) => {
+        espanso_render::Value::Number(espanso_render::Number::Float(f.into_inner()))
+      }
+    },
+    espanso_config::matches::Value::String(s) => espanso_render::Value::String(s),
+    espanso_config::matches::Value::Array(v) => {
+      espanso_render::Value::Array(v.into_iter().map(convert_value).collect())
+    }
+    espanso_config::matches::Value::Object(params) => {
+      espanso_render::Value::Object(convert_params(params))
+    }
+  }
+}
+
+impl<'a> Renderer<'a> for RendererAdapter<'a> {
+  fn render(
+    &'a self,
+    match_id: i32,
+    trigger: Option<&str>,
+    trigger_vars: HashMap<String, String>,
+  ) -> anyhow::Result<String> {
+    if let Some(Some(template)) = self.template_map.get(&match_id) {
+      let (config, match_set) = self.config_provider.active();
+
+      let mut context_cache = self.context_cache.borrow_mut();
+      let context = context_cache
+        .entry(config.id())
+        .or_insert_with(|| generate_context(&match_set, &self.template_map, &self.global_vars_map));
+
+      let raw_match = self.match_provider.get(match_id);
+      let propagate_case = raw_match.map(is_propagate_case).unwrap_or(false);
+      let preferred_uppercasing_style = raw_match.and_then(extract_uppercasing_style);
+
+      let options = RenderOptions {
+        casing_style: if !propagate_case {
+          CasingStyle::None
+        } else if let Some(trigger) = trigger {
+          calculate_casing_style(trigger, preferred_uppercasing_style)
+        } else {
+          CasingStyle::None
+        },
+      };
+
+      // If some trigger vars are specified, augment the template with them
+      let augmented_template = if !trigger_vars.is_empty() {
+        let mut augmented = template.clone();
+        for (name, value) in trigger_vars {
+          let mut params = espanso_render::Params::new();
+          params.insert("echo".to_string(), Value::String(value));
+          augmented.vars.insert(
+            0,
+            Variable {
+              name,
+              var_type: "echo".to_string(),
+              params,
+            },
+          )
+        }
+        Some(augmented)
+      } else {
+        None
+      };
+
+      let template = if let Some(augmented) = augmented_template.as_ref() {
+        augmented
+      } else {
+        template
+      };
+
+      match self.renderer.render(template, context, &options) {
+        espanso_render::RenderResult::Success(body) => Ok(body),
+        espanso_render::RenderResult::Aborted => Err(RendererError::Aborted.into()),
+        espanso_render::RenderResult::Error(err) => Err(RendererError::RenderingError(err).into()),
+      }
+    } else {
+      Err(RendererError::NotFound.into())
+    }
+  }
+}
+
+fn extract_uppercasing_style(m: &Match) -> Option<UpperCasingStyle> {
+  if let MatchCause::Trigger(cause) = &m.cause {
+    Some(cause.uppercase_style.clone())
+  } else {
+    None
+  }
+}
+
+fn is_propagate_case(m: &Match) -> bool {
+  if let MatchCause::Trigger(cause) = &m.cause {
+    cause.propagate_case
+  } else {
+    false
+  }
+}
+
+// TODO: test
+fn calculate_casing_style(
+  trigger: &str,
+  uppercasing_style: Option<UpperCasingStyle>,
+) -> CasingStyle {
+  let mut first_alphabetic = None;
+  let mut second_alphabetic = None;
+
+  for c in trigger.chars() {
+    if c.is_alphabetic() {
+      if first_alphabetic.is_none() {
+        first_alphabetic = Some(c);
+      } else if second_alphabetic.is_none() {
+        second_alphabetic = Some(c);
+      } else {
+        break;
+      }
+    }
+  }
+
+  if let Some(first) = first_alphabetic {
+    if let Some(second) = second_alphabetic {
+      if first.is_uppercase() {
+        if second.is_uppercase() {
+          CasingStyle::Uppercase
+        } else {
+          match uppercasing_style {
+            Some(UpperCasingStyle::CapitalizeWords) => CasingStyle::CapitalizeWords,
+            _ => CasingStyle::Capitalize,
+          }
+        }
+      } else {
+        CasingStyle::None
+      }
+    } else if first.is_uppercase() {
+      match uppercasing_style {
+        Some(UpperCasingStyle::Capitalize) => CasingStyle::Capitalize,
+        Some(UpperCasingStyle::CapitalizeWords) => CasingStyle::CapitalizeWords,
+        _ => CasingStyle::Uppercase,
+      }
+    } else {
+      CasingStyle::None
+    }
+  } else {
+    CasingStyle::None
+  }
+}
diff --git a/espanso/src/cli/worker/engine/process/mod.rs b/espanso/src/cli/worker/engine/process/mod.rs
new file mode 100644
index 00000000..f5441b92
--- /dev/null
+++ b/espanso/src/cli/worker/engine/process/mod.rs
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+pub mod middleware;
diff --git a/espanso/src/cli/worker/ipc.rs b/espanso/src/cli/worker/ipc.rs
new file mode 100644
index 00000000..07f61774
--- /dev/null
+++ b/espanso/src/cli/worker/ipc.rs
@@ -0,0 +1,72 @@
+/*
+ * 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::path::Path;
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+use espanso_engine::event::ExitMode;
+use espanso_ipc::{EventHandlerResponse, IPCServer};
+use log::{error, warn};
+
+use crate::ipc::IPCEvent;
+
+pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -> Result<()> {
+  let server = crate::ipc::create_worker_ipc_server(runtime_dir)?;
+
+  std::thread::Builder::new()
+    .name("worker-ipc-handler".to_string())
+    .spawn(move || {
+      server
+        .run(Box::new(move |event| match event {
+          IPCEvent::Exit => {
+            if let Err(err) = exit_notify.send(ExitMode::Exit) {
+              error!(
+                "experienced error while sending exit signal from worker ipc handler: {}",
+                err
+              );
+            }
+
+            EventHandlerResponse::NoResponse
+          }
+          IPCEvent::ExitAllProcesses => {
+            if let Err(err) = exit_notify.send(ExitMode::ExitAllProcesses) {
+              error!(
+                "experienced error while sending exit signal from worker ipc handler: {}",
+                err
+              );
+            }
+
+            EventHandlerResponse::NoResponse
+          }
+          #[allow(unreachable_patterns)]
+          unexpected_event => {
+            warn!(
+              "received unexpected event in worker ipc handler: {:?}",
+              unexpected_event
+            );
+
+            EventHandlerResponse::NoResponse
+          }
+        }))
+        .expect("unable to spawn IPC server");
+    })?;
+
+  Ok(())
+}
diff --git a/espanso/src/cli/worker/match_cache.rs b/espanso/src/cli/worker/match_cache.rs
new file mode 100644
index 00000000..fe23b7d1
--- /dev/null
+++ b/espanso/src/cli/worker/match_cache.rs
@@ -0,0 +1,166 @@
+/*
+ * 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;
+
+use espanso_config::{
+  config::ConfigStore,
+  matches::{store::MatchStore, Match, MatchEffect},
+};
+
+use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary};
+
+pub struct MatchCache<'a> {
+  cache: HashMap<i32, &'a Match>,
+}
+
+impl<'a> MatchCache<'a> {
+  pub fn load(config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore) -> Self {
+    let mut cache = HashMap::new();
+
+    let paths = config_store.get_all_match_paths();
+    let global_set = match_store.query(&paths.into_iter().collect::<Vec<_>>());
+
+    for m in global_set.matches {
+      cache.insert(m.id, m);
+    }
+
+    Self { cache }
+  }
+
+  fn ids(&self) -> Vec<i32> {
+    self.cache.keys().copied().collect()
+  }
+}
+
+impl<'a> super::engine::process::middleware::render::MatchProvider<'a> for MatchCache<'a> {
+  fn matches(&self) -> Vec<&'a Match> {
+    self.cache.iter().map(|(_, m)| *m).collect()
+  }
+
+  fn get(&self, id: i32) -> Option<&'a Match> {
+    self.cache.get(&id).copied()
+  }
+}
+
+impl<'a> espanso_engine::process::MatchInfoProvider for MatchCache<'a> {
+  fn get_force_mode(&self, match_id: i32) -> Option<espanso_engine::event::effect::TextInjectMode> {
+    let m = self.cache.get(&match_id)?;
+    if let MatchEffect::Text(text_effect) = &m.effect {
+      if let Some(force_mode) = &text_effect.force_mode {
+        match force_mode {
+          espanso_config::matches::TextInjectMode::Keys => {
+            return Some(espanso_engine::event::effect::TextInjectMode::Keys)
+          }
+          espanso_config::matches::TextInjectMode::Clipboard => {
+            return Some(espanso_engine::event::effect::TextInjectMode::Clipboard)
+          }
+        }
+      }
+    }
+
+    None
+  }
+}
+
+pub struct CombinedMatchCache<'a> {
+  user_match_cache: &'a MatchCache<'a>,
+  builtin_match_cache: HashMap<i32, &'a BuiltInMatch>,
+}
+
+pub enum MatchVariant<'a> {
+  User(&'a Match),
+  Builtin(&'a BuiltInMatch),
+}
+
+impl<'a> CombinedMatchCache<'a> {
+  pub fn load(user_match_cache: &'a MatchCache<'a>, builtin_matches: &'a [BuiltInMatch]) -> Self {
+    let mut builtin_match_cache = HashMap::new();
+
+    for m in builtin_matches {
+      builtin_match_cache.insert(m.id, m);
+    }
+
+    Self {
+      user_match_cache,
+      builtin_match_cache,
+    }
+  }
+
+  pub fn get(&self, match_id: i32) -> Option<MatchVariant<'a>> {
+    if let Some(user_match) = self.user_match_cache.cache.get(&match_id).copied() {
+      return Some(MatchVariant::User(user_match));
+    }
+
+    if let Some(builtin_match) = self.builtin_match_cache.get(&match_id).copied() {
+      return Some(MatchVariant::Builtin(builtin_match));
+    }
+
+    None
+  }
+}
+
+impl<'a> super::engine::process::middleware::match_select::MatchProvider<'a>
+  for CombinedMatchCache<'a>
+{
+  fn get_matches(&self, ids: &[i32]) -> Vec<MatchSummary<'a>> {
+    ids
+      .iter()
+      .filter_map(|id| self.get(*id))
+      .map(|m| match m {
+        MatchVariant::User(m) => MatchSummary {
+          id: m.id,
+          label: m.description(),
+          tag: m.cause_description(),
+          is_builtin: false,
+        },
+        MatchVariant::Builtin(m) => MatchSummary {
+          id: m.id,
+          label: m.label,
+          tag: m.triggers.first().map(String::as_ref),
+          is_builtin: true,
+        },
+      })
+      .collect()
+  }
+}
+
+impl<'a> super::engine::process::middleware::multiplex::MatchProvider<'a>
+  for CombinedMatchCache<'a>
+{
+  fn get(
+    &self,
+    match_id: i32,
+  ) -> Option<super::engine::process::middleware::multiplex::MatchResult<'a>> {
+    Some(match self.get(match_id)? {
+      MatchVariant::User(m) => super::engine::process::middleware::multiplex::MatchResult::User(m),
+      MatchVariant::Builtin(m) => {
+        super::engine::process::middleware::multiplex::MatchResult::Builtin(m)
+      }
+    })
+  }
+}
+
+impl<'a> espanso_engine::process::MatchProvider for CombinedMatchCache<'a> {
+  fn get_all_matches_ids(&self) -> Vec<i32> {
+    let mut ids: Vec<i32> = self.builtin_match_cache.keys().copied().collect();
+    ids.extend(self.user_match_cache.ids());
+    ids
+  }
+}
diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs
new file mode 100644
index 00000000..473b5f87
--- /dev/null
+++ b/espanso/src/cli/worker/mod.rs
@@ -0,0 +1,178 @@
+/*
+ * 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 crossbeam::channel::unbounded;
+use espanso_engine::event::ExitMode;
+use log::{debug, error, info};
+
+use crate::{
+  exit_code::{
+    WORKER_ALREADY_RUNNING, WORKER_EXIT_ALL_PROCESSES, WORKER_GENERAL_ERROR,
+    WORKER_LEGACY_ALREADY_RUNNING, WORKER_RESTART, WORKER_SUCCESS,
+  },
+  lock::{acquire_legacy_lock, acquire_worker_lock},
+};
+
+use self::ui::util::convert_icon_paths_to_tray_vec;
+
+use super::{CliModule, CliModuleArgs};
+
+mod builtin;
+mod config;
+mod context;
+mod daemon_monitor;
+mod engine;
+mod ipc;
+mod match_cache;
+mod secure_input;
+mod ui;
+
+pub fn new() -> CliModule {
+  #[allow(clippy::needless_update)]
+  CliModule {
+    requires_paths: true,
+    requires_config: true,
+    requires_linux_capabilities: true,
+    enable_logs: true,
+    log_mode: super::LogMode::AppendOnly,
+    subcommand: "worker".to_string(),
+    entry: worker_main,
+    ..Default::default()
+  }
+}
+
+fn worker_main(args: CliModuleArgs) -> i32 {
+  let paths = args.paths.expect("missing paths in worker main");
+  let cli_args = args.cli_args.expect("missing cli_args in worker main");
+
+  // When restarted, the daemon passes the reason why the worker was restarted (config_change, etc)
+  let start_reason = cli_args.value_of("start-reason").map(String::from);
+  debug!("starting with start-reason = {:?}", start_reason);
+
+  // Avoid running multiple worker instances
+  let lock_file = acquire_worker_lock(&paths.runtime);
+  if lock_file.is_none() {
+    error!("worker is already running!");
+    return WORKER_ALREADY_RUNNING;
+  }
+
+  let legacy_lock_file = acquire_legacy_lock(&paths.runtime);
+  if legacy_lock_file.is_none() {
+    error!("an instance of legacy espanso is running, please terminate it, otherwise the new version cannot start");
+    return WORKER_LEGACY_ALREADY_RUNNING;
+  }
+  drop(legacy_lock_file);
+
+  let config_store = args
+    .config_store
+    .expect("missing config store in worker main");
+  let match_store = args
+    .match_store
+    .expect("missing match store in worker main");
+
+  // TODO: show config loading errors in a GUI, if any
+
+  let use_evdev_backend = if cfg!(feature = "wayland") {
+    true
+  } else {
+    std::env::var("USE_EVDEV").unwrap_or_else(|_| "false".to_string()) == "true"
+  };
+
+  let icon_paths =
+    crate::icon::load_icon_paths(&paths.runtime).expect("unable to initialize icons");
+
+  let (remote, mut eventloop) = espanso_ui::create_ui(espanso_ui::UIOptions {
+    show_icon: config_store.default().show_icon(),
+    icon_paths: convert_icon_paths_to_tray_vec(&icon_paths),
+    notification_icon_path: icon_paths
+      .logo
+      .as_ref()
+      .map(|path| path.to_string_lossy().to_string()),
+  })
+  .expect("unable to create tray icon UI module");
+
+  eventloop
+    .initialize()
+    .expect("unable to initialize UI module");
+
+  let (engine_exit_notify, engine_exit_receiver) = unbounded();
+  let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded();
+  let (engine_secure_input_sender, engine_secure_input_receiver) = unbounded();
+
+  // Initialize the engine on another thread and start it
+  let engine_handle = engine::initialize_and_spawn(
+    paths.clone(),
+    config_store,
+    match_store,
+    remote,
+    engine_exit_receiver,
+    engine_ui_event_receiver,
+    engine_secure_input_receiver,
+    use_evdev_backend,
+    start_reason,
+  )
+  .expect("unable to initialize engine");
+
+  // Setup the IPC server
+  ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone())
+    .expect("unable to initialize IPC server");
+
+  // If specified, automatically monitor the daemon status and
+  // terminate the worker if the daemon terminates
+  // This is needed to avoid "dangling" worker processes
+  // if the daemon crashes or is forcefully terminated.
+  if cli_args.is_present("monitor-daemon") {
+    daemon_monitor::initialize_and_spawn(&paths.runtime, engine_exit_notify)
+      .expect("unable to initialize daemon monitor thread");
+  }
+
+  secure_input::initialize_and_spawn(engine_secure_input_sender)
+    .expect("unable to initialize secure input watcher");
+
+  eventloop
+    .run(Box::new(move |event| {
+      if let Err(error) = engine_ui_event_sender.send(event) {
+        error!("unable to send UIEvent to engine: {}", error);
+        panic!("broken UI->Engine channel");
+      }
+    }))
+    .expect("unable to run main eventloop");
+
+  info!("waiting for engine exit mode...");
+  match engine_handle.join() {
+    Ok(mode) => match mode {
+      ExitMode::Exit => {
+        info!("exiting worker process...");
+        WORKER_SUCCESS
+      }
+      ExitMode::ExitAllProcesses => {
+        info!("exiting worker process and daemon...");
+        WORKER_EXIT_ALL_PROCESSES
+      }
+      ExitMode::RestartWorker => {
+        info!("exiting worker (to be restarted)");
+        WORKER_RESTART
+      }
+    },
+    Err(err) => {
+      error!("unable to read engine exit mode: {:?}", err);
+      WORKER_GENERAL_ERROR
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/secure_input.rs b/espanso/src/cli/worker/secure_input.rs
new file mode 100644
index 00000000..417582ff
--- /dev/null
+++ b/espanso/src/cli/worker/secure_input.rs
@@ -0,0 +1,121 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use crossbeam::channel::Sender;
+
+#[allow(dead_code)]
+pub enum SecureInputEvent {
+  Disabled,
+  Enabled { app_name: String, app_path: String },
+}
+
+#[cfg(not(target_os = "macos"))]
+pub fn initialize_and_spawn(_secure_input_send: Sender<SecureInputEvent>) -> Result<()> {
+  // NOOP on Windows and Linux
+  Ok(())
+}
+
+#[cfg(target_os = "macos")]
+pub fn initialize_and_spawn(secure_input_sender: Sender<SecureInputEvent>) -> Result<()> {
+  std::thread::Builder::new()
+    .name("secure-input-monitor".to_string())
+    .spawn(move || {
+      // TODO: pass interval from config parameter
+      secure_input_main(
+        secure_input_sender,
+        std::time::Duration::from_secs(3),
+        std::time::Duration::from_secs(1),
+      );
+    })?;
+
+  Ok(())
+}
+
+#[cfg(target_os = "macos")]
+fn secure_input_main(
+  secure_input_sender: Sender<SecureInputEvent>,
+  min_watch_interval: std::time::Duration,
+  max_watch_interval: std::time::Duration,
+) {
+  use log::{error, info};
+
+  info!("monitoring the status of secure input");
+
+  let mut last_secure_input_pid: Option<i64> = None;
+  loop {
+    let pid = espanso_mac_utils::get_secure_input_pid();
+
+    if let Some(pid) = pid {
+      // Some application is currently on SecureInput
+      let should_notify = if let Some(old_pid) = last_secure_input_pid {
+        // We already detected a SecureInput app
+        #[allow(clippy::needless_bool)]
+        if old_pid != pid {
+          // The old app is different from the current one, we should take action
+          true
+        } else {
+          // We already notified this application before
+          false
+        }
+      } else {
+        // First time we see this SecureInput app, we should take action
+        true
+      };
+
+      if should_notify {
+        let secure_input_app = espanso_mac_utils::get_secure_input_application();
+
+        if let Some((app_name, app_path)) = secure_input_app {
+          info!("secure input has been acquired, preventing espanso from working correctly. Our guess is that this is caused by '{}', but there are cases in which the detection is unreliable. Full path: {}", app_name, app_path);
+
+          if let Err(error) =
+            secure_input_sender.send(SecureInputEvent::Enabled { app_name, app_path })
+          {
+            error!("unable to send secure input disabled event: {}", error);
+          }
+        } else {
+          error!("detected secure input, but could not figure out which application triggered it");
+        }
+      }
+
+      last_secure_input_pid = Some(pid);
+    } else {
+      // No app is currently keeping SecureInput
+
+      // If there was an app with SecureInput, notify that is now free
+      if last_secure_input_pid.is_some() {
+        info!("secure input has been disabled");
+
+        if let Err(error) = secure_input_sender.send(SecureInputEvent::Disabled) {
+          error!("unable to send secure input disabled event: {}", error);
+        }
+      }
+
+      last_secure_input_pid = None
+    }
+
+    // If an application is currently keeping secure input, refresh the status more often
+    if pid.is_some() {
+      std::thread::sleep(max_watch_interval);
+    } else {
+      std::thread::sleep(min_watch_interval);
+    }
+  }
+}
diff --git a/espanso/src/cli/worker/ui/mod.rs b/espanso/src/cli/worker/ui/mod.rs
new file mode 100644
index 00000000..bcb5b109
--- /dev/null
+++ b/espanso/src/cli/worker/ui/mod.rs
@@ -0,0 +1,22 @@
+/*
+ * 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/>.
+ */
+
+pub mod notification;
+pub mod util;
+pub mod welcome;
diff --git a/espanso/src/cli/worker/ui/notification.rs b/espanso/src/cli/worker/ui/notification.rs
new file mode 100644
index 00000000..c08f139f
--- /dev/null
+++ b/espanso/src/cli/worker/ui/notification.rs
@@ -0,0 +1,56 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_config::config::Config;
+use espanso_ui::UIRemote;
+
+pub struct NotificationManager<'a> {
+  ui_remote: &'a dyn UIRemote,
+  config: &'a dyn Config,
+}
+
+impl<'a> NotificationManager<'a> {
+  pub fn new(ui_remote: &'a dyn UIRemote, config: &'a dyn Config) -> Self {
+    NotificationManager { ui_remote, config }
+  }
+
+  fn notify(&self, text: &str) {
+    if self.config.show_notifications() {
+      self.ui_remote.show_notification(text);
+    }
+  }
+
+  pub fn notify_start(&self) {
+    self.notify("Espanso is running!");
+  }
+
+  pub fn notify_config_reloaded(&self, is_manual_restart: bool) {
+    if is_manual_restart {
+      self.notify("Configuration reloaded!");
+    } else {
+      self.notify(
+        "Configuration reloaded! Espanso automatically loads new changes as soon as you save them.",
+      );
+    }
+  }
+
+  pub fn notify_keyboard_layout_reloaded(&self) {
+    self.notify("Updated keyboard layout!");
+  }
+}
diff --git a/espanso/src/cli/worker/ui/util.rs b/espanso/src/cli/worker/ui/util.rs
new file mode 100644
index 00000000..f9b8e7e4
--- /dev/null
+++ b/espanso/src/cli/worker/ui/util.rs
@@ -0,0 +1,44 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_ui::icons::TrayIcon;
+
+use crate::icon::IconPaths;
+
+// TODO: test
+pub fn convert_icon_paths_to_tray_vec(icon_paths: &IconPaths) -> Vec<(TrayIcon, String)> {
+  let mut paths = Vec::new();
+
+  if let Some(normal) = &icon_paths.tray_icon_normal {
+    paths.push((TrayIcon::Normal, normal.to_string_lossy().to_string()));
+  }
+
+  if let Some(disabled) = &icon_paths.tray_icon_disabled {
+    paths.push((TrayIcon::Disabled, disabled.to_string_lossy().to_string()));
+  }
+
+  if let Some(system_disabled) = &icon_paths.tray_icon_system_disabled {
+    paths.push((
+      TrayIcon::SystemDisabled,
+      system_disabled.to_string_lossy().to_string(),
+    ));
+  }
+
+  paths
+}
diff --git a/espanso/src/cli/worker/ui/welcome.rs b/espanso/src/cli/worker/ui/welcome.rs
new file mode 100644
index 00000000..d37c46ea
--- /dev/null
+++ b/espanso/src/cli/worker/ui/welcome.rs
@@ -0,0 +1,32 @@
+/*
+ * 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/>.
+ */
+
+#[cfg(feature = "modulo")]
+pub fn show_welcome_screen() {
+  let espanso_exe_path = std::env::current_exe().expect("unable to determine executable path");
+  let mut command = std::process::Command::new(&espanso_exe_path.to_string_lossy().to_string());
+  command.args(&["modulo", "welcome"]);
+
+  command.spawn().expect("unable to show welcome screen");
+}
+
+#[cfg(not(feature = "modulo"))]
+pub fn show_welcome_screen() {
+  // NOOP
+}
diff --git a/espanso/src/common_flags.rs b/espanso/src/common_flags.rs
new file mode 100644
index 00000000..bf54654b
--- /dev/null
+++ b/espanso/src/common_flags.rs
@@ -0,0 +1,22 @@
+/*
+ * 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/>.
+ */
+
+pub const WORKER_START_REASON_MANUAL: &str = "manual_restart";
+pub const WORKER_START_REASON_CONFIG_CHANGED: &str = "config_changed";
+pub const WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED: &str = "keyboard_layout_changed";
diff --git a/espanso/src/config.rs b/espanso/src/config.rs
new file mode 100644
index 00000000..d091b424
--- /dev/null
+++ b/espanso/src/config.rs
@@ -0,0 +1,124 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::{Context, Result};
+use espanso_config::{
+  config::ConfigStore,
+  error::{ErrorLevel, NonFatalErrorSet},
+  matches::store::MatchStore,
+};
+use log::{error, info, warn};
+use std::path::Path;
+
+const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("./res/config/default.yml");
+const DEFAULT_MATCH_FILE_CONTENT: &str = include_str!("./res/config/base.yml");
+
+pub fn populate_default_config(config_dir: &Path) -> Result<()> {
+  if !config_dir.is_dir() {
+    info!(
+      "generating base configuration directory in: {:?}",
+      config_dir
+    );
+    std::fs::create_dir_all(config_dir)?;
+  }
+
+  let sub_config_dir = config_dir.join("config");
+  let sub_match_dir = config_dir.join("match");
+
+  if !sub_config_dir.is_dir() {
+    info!("generating config directory in: {:?}", sub_config_dir);
+    std::fs::create_dir_all(&sub_config_dir)?;
+  }
+  if !sub_match_dir.is_dir() {
+    info!("generating match directory in: {:?}", sub_match_dir);
+    std::fs::create_dir_all(&sub_match_dir)?;
+  }
+
+  let default_file = sub_config_dir.join("default.yml");
+  let match_file = sub_match_dir.join("base.yml");
+
+  if !default_file.is_file() {
+    info!(
+      "populating default.yml file with initial content: {:?}",
+      default_file
+    );
+    std::fs::write(default_file, DEFAULT_CONFIG_FILE_CONTENT)?;
+  }
+  if !match_file.is_file() {
+    info!(
+      "populating base.yml file with initial content: {:?}",
+      match_file
+    );
+    std::fs::write(match_file, DEFAULT_MATCH_FILE_CONTENT)?;
+  }
+
+  Ok(())
+}
+
+pub struct ConfigLoadResult {
+  pub config_store: Box<dyn ConfigStore>,
+  pub match_store: Box<dyn MatchStore>,
+  pub is_legacy_config: bool,
+  pub non_fatal_errors: Vec<NonFatalErrorSet>,
+}
+
+pub fn load_config(config_path: &Path, packages_path: &Path) -> Result<ConfigLoadResult> {
+  if espanso_config::is_legacy_config(config_path) {
+    let (config_store, match_store) = espanso_config::load_legacy(config_path, packages_path)
+      .context("unable to load legacy config")?;
+
+    Ok(ConfigLoadResult {
+      // Apply the built-in patches
+      config_store: crate::patch::patch_store(config_store),
+      match_store,
+      is_legacy_config: true,
+      non_fatal_errors: Vec::new(),
+    })
+  } else {
+    let (config_store, match_store, non_fatal_errors) =
+      espanso_config::load(config_path).context("unable to load config")?;
+
+    // TODO: add an option to avoid dumping the errors in the logs
+    if !non_fatal_errors.is_empty() {
+      warn!("------- detected some errors in the configuration: -------");
+      for non_fatal_error_set in &non_fatal_errors {
+        warn!(
+          ">>> {}",
+          non_fatal_error_set.file.to_string_lossy().to_string()
+        );
+        for record in &non_fatal_error_set.errors {
+          if record.level == ErrorLevel::Error {
+            error!("{:?}", record.error);
+          } else {
+            warn!("{:?}", record.error);
+          }
+        }
+      }
+      warn!("-----------------------------------------------------------");
+    }
+
+    Ok(ConfigLoadResult {
+      // Apply the built-in patches
+      config_store: crate::patch::patch_store(config_store),
+      match_store,
+      is_legacy_config: false,
+      non_fatal_errors,
+    })
+  }
+}
diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs
new file mode 100644
index 00000000..81eda519
--- /dev/null
+++ b/espanso/src/exit_code.rs
@@ -0,0 +1,120 @@
+/*
+ * 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/>.
+ */
+
+pub const WORKER_SUCCESS: i32 = 0;
+pub const WORKER_ALREADY_RUNNING: i32 = 1;
+pub const WORKER_GENERAL_ERROR: i32 = 2;
+pub const WORKER_LEGACY_ALREADY_RUNNING: i32 = 3;
+pub const WORKER_EXIT_ALL_PROCESSES: i32 = 101;
+pub const WORKER_RESTART: i32 = 102;
+
+pub const DAEMON_SUCCESS: i32 = 0;
+pub const DAEMON_ALREADY_RUNNING: i32 = 1;
+pub const DAEMON_GENERAL_ERROR: i32 = 2;
+pub const DAEMON_LEGACY_ALREADY_RUNNING: i32 = 3;
+pub const DAEMON_FATAL_CONFIG_ERROR: i32 = 4;
+
+pub const MIGRATE_SUCCESS: i32 = 0;
+pub const MIGRATE_ALREADY_NEW_FORMAT: i32 = 1;
+pub const MIGRATE_LEGACY_INSTANCE_RUNNING: i32 = 2;
+pub const MIGRATE_USER_ABORTED: i32 = 3;
+pub const MIGRATE_CLEAN_FAILURE: i32 = 50;
+pub const MIGRATE_DIRTY_FAILURE: i32 = 51;
+pub const MIGRATE_UNEXPECTED_FAILURE: i32 = 101;
+
+pub const ADD_TO_PATH_SUCCESS: i32 = 0;
+pub const ADD_TO_PATH_FAILURE: i32 = 1;
+
+pub const LAUNCHER_SUCCESS: i32 = 0;
+pub const LAUNCHER_CONFIG_DIR_POPULATION_FAILURE: i32 = 1;
+pub const LAUNCHER_ALREADY_RUNNING: i32 = 2;
+
+pub const SERVICE_SUCCESS: i32 = 0;
+pub const SERVICE_FAILURE: i32 = 1;
+pub const SERVICE_NOT_REGISTERED: i32 = 2;
+pub const SERVICE_ALREADY_RUNNING: i32 = 3;
+pub const SERVICE_NOT_RUNNING: i32 = 4;
+
+pub const WORKAROUND_SUCCESS: i32 = 0;
+#[allow(dead_code)]
+pub const WORKAROUND_FAILURE: i32 = 1;
+#[allow(dead_code)]
+pub const WORKAROUND_NOT_AVAILABLE: i32 = 2;
+
+#[allow(dead_code)]
+pub const PACKAGE_SUCCESS: i32 = 0;
+#[allow(dead_code)]
+pub const PACKAGE_UNEXPECTED_FAILURE: i32 = 1;
+pub const PACKAGE_INSTALL_FAILED: i32 = 2;
+pub const PACKAGE_UNINSTALL_FAILED: i32 = 3;
+pub const PACKAGE_LIST_FAILED: i32 = 4;
+pub const PACKAGE_UPDATE_FAILED: i32 = 5;
+pub const PACKAGE_UPDATE_PARTIAL_FAILURE: i32 = 6;
+
+use std::sync::Mutex;
+
+use crate::error_eprintln;
+
+lazy_static! {
+  static ref CURRENT_PANIC_EXIT_CODE: Mutex<i32> = Mutex::new(MIGRATE_UNEXPECTED_FAILURE);
+}
+
+pub fn configure_custom_panic_hook() {
+  let previous_hook = std::panic::take_hook();
+  std::panic::set_hook(Box::new(move |info| {
+    (*previous_hook)(info);
+
+    // Part of this code is taken from the "rust-log-panics" crate
+    let thread = std::thread::current();
+    let thread = thread.name().unwrap_or("<unnamed>");
+
+    let msg = match info.payload().downcast_ref::<&'static str>() {
+      Some(s) => *s,
+      None => match info.payload().downcast_ref::<String>() {
+        Some(s) => &**s,
+        None => "Box<Any>",
+      },
+    };
+
+    match info.location() {
+      Some(location) => {
+        error_eprintln!(
+          "ERROR: '{}' panicked at '{}': {}:{}",
+          thread,
+          msg,
+          location.file(),
+          location.line(),
+        );
+      }
+      None => {
+        error_eprintln!("ERROR: '{}' panicked at '{}'", thread, msg);
+      }
+    }
+
+    let exit_code = CURRENT_PANIC_EXIT_CODE.lock().unwrap();
+    std::process::exit(*exit_code);
+  }));
+}
+
+pub fn update_panic_exit_code(exit_code: i32) {
+  let mut lock = CURRENT_PANIC_EXIT_CODE
+    .lock()
+    .expect("unable to update panic exit code");
+  *lock = exit_code;
+}
diff --git a/espanso/src/gui/mod.rs b/espanso/src/gui/mod.rs
new file mode 100644
index 00000000..bcd26e7c
--- /dev/null
+++ b/espanso/src/gui/mod.rs
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+use anyhow::Result;
+
+pub mod modulo;
+
+pub trait SearchUI {
+  fn show(&self, items: &[SearchItem], hint: Option<&str>) -> Result<Option<String>>;
+}
+
+#[derive(Debug)]
+pub struct SearchItem {
+  pub id: String,
+  pub label: String,
+  pub tag: Option<String>,
+  pub is_builtin: bool,
+}
+
+pub trait FormUI {
+  fn show(
+    &self,
+    layout: &str,
+    fields: &HashMap<String, FormField>,
+  ) -> Result<Option<HashMap<String, String>>>;
+}
+
+#[derive(Debug)]
+pub enum FormField {
+  Text {
+    default: Option<String>,
+    multiline: bool,
+  },
+  Choice {
+    default: Option<String>,
+    values: Vec<String>,
+  },
+  List {
+    default: Option<String>,
+    values: Vec<String>,
+  },
+}
diff --git a/espanso/src/gui/modulo/form.rs b/espanso/src/gui/modulo/form.rs
new file mode 100644
index 00000000..6c0f778d
--- /dev/null
+++ b/espanso/src/gui/modulo/form.rs
@@ -0,0 +1,99 @@
+/*
+ * 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 serde::Serialize;
+use serde_json::{json, Map, Value};
+use std::collections::HashMap;
+
+use crate::gui::{FormField, FormUI};
+
+use super::manager::ModuloManager;
+
+pub struct ModuloFormUI<'a> {
+  manager: &'a ModuloManager,
+}
+
+impl<'a> ModuloFormUI<'a> {
+  pub fn new(manager: &'a ModuloManager) -> Self {
+    Self { manager }
+  }
+}
+
+impl<'a> FormUI for ModuloFormUI<'a> {
+  fn show(
+    &self,
+    layout: &str,
+    fields: &HashMap<String, FormField>,
+  ) -> anyhow::Result<Option<HashMap<String, String>>> {
+    let modulo_form_config = ModuloFormConfig {
+      title: "espanso",
+      layout,
+      fields: convert_fields_into_object(fields),
+    };
+
+    let json_config = serde_json::to_string(&modulo_form_config)?;
+    let output = self
+      .manager
+      .invoke(&["form", "-j", "-i", "-"], &json_config)?;
+    let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
+    match json {
+      Ok(json) => {
+        if json.is_empty() {
+          Ok(None)
+        } else {
+          Ok(Some(json))
+        }
+      }
+      Err(error) => Err(error.into()),
+    }
+  }
+}
+
+#[derive(Debug, Serialize)]
+struct ModuloFormConfig<'a> {
+  title: &'a str,
+  layout: &'a str,
+  fields: Map<String, Value>,
+}
+
+// TODO: test
+fn convert_fields_into_object(fields: &HashMap<String, FormField>) -> Map<String, Value> {
+  let mut obj = Map::new();
+  for (name, field) in fields {
+    let value = match field {
+      FormField::Text { default, multiline } => json!({
+        "type": "text",
+        "default": default,
+        "multiline": multiline,
+      }),
+      FormField::Choice { default, values } => json!({
+        "type": "choice",
+        "default": default,
+        "values": values,
+      }),
+      FormField::List { default, values } => json!({
+        "type": "list",
+        "default": default,
+        "values": values,
+      }),
+    };
+    obj.insert(name.clone(), value);
+  }
+  obj
+}
diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs
new file mode 100644
index 00000000..0cc31845
--- /dev/null
+++ b/espanso/src/gui/modulo/manager.rs
@@ -0,0 +1,112 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::{error, warn};
+use std::io::Write;
+use std::process::Command;
+use thiserror::Error;
+
+pub struct ModuloManager {
+  is_support_enabled: bool,
+}
+
+impl ModuloManager {
+  pub fn new() -> Self {
+    let is_support_enabled = if cfg!(feature = "modulo") {
+      true
+    } else {
+      warn!("this version of espanso doesn't come with modulo support, so graphical features (such as Forms and Search) might not be available");
+      false
+    };
+
+    Self { is_support_enabled }
+  }
+
+  pub fn invoke(&self, args: &[&str], body: &str) -> Result<String> {
+    if self.is_support_enabled {
+      let exec_path = std::env::current_exe().expect("unable to obtain current exec path");
+      let mut command = Command::new(exec_path);
+      let mut full_args = vec!["modulo"];
+      full_args.extend(args);
+      command
+        .args(full_args)
+        .stdin(std::process::Stdio::piped())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped());
+
+      crate::util::set_command_flags(&mut command);
+
+      let child = command.spawn();
+
+      match child {
+        Ok(mut child) => {
+          if let Some(stdin) = child.stdin.as_mut() {
+            match stdin.write_all(body.as_bytes()) {
+              Ok(_) => {
+                // Get the output
+                match child.wait_with_output() {
+                  Ok(child_output) => {
+                    let output = String::from_utf8_lossy(&child_output.stdout);
+
+                    // Check also if the program reports an error
+                    let error = String::from_utf8_lossy(&child_output.stderr);
+                    if !error.is_empty() {
+                      error!("modulo reported an error: {}", error);
+                    }
+
+                    if !output.trim().is_empty() {
+                      Ok(output.to_string())
+                    } else {
+                      Err(ModuloError::EmptyOutput.into())
+                    }
+                  }
+                  Err(error) => Err(ModuloError::Error(error).into()),
+                }
+              }
+              Err(error) => Err(ModuloError::Error(error).into()),
+            }
+          } else {
+            Err(ModuloError::StdinError.into())
+          }
+        }
+        Err(error) => Err(ModuloError::Error(error).into()),
+      }
+    } else {
+      Err(ModuloError::MissingModulo.into())
+    }
+  }
+}
+
+#[derive(Error, Debug)]
+pub enum ModuloError {
+  #[error(
+    "attempt to invoke modulo, but this version of espanso is not compiled with support for it"
+  )]
+  MissingModulo,
+
+  #[error("modulo returned an empty output")]
+  EmptyOutput,
+
+  #[error("could not connect to modulo stdin")]
+  StdinError,
+
+  #[error("error occurred during modulo invocation")]
+  Error(#[from] std::io::Error),
+}
diff --git a/espanso/src/gui/modulo/mod.rs b/espanso/src/gui/modulo/mod.rs
new file mode 100644
index 00000000..9e27396d
--- /dev/null
+++ b/espanso/src/gui/modulo/mod.rs
@@ -0,0 +1,22 @@
+/*
+ * 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/>.
+ */
+
+pub mod form;
+pub mod manager;
+pub mod search;
diff --git a/espanso/src/gui/modulo/search.rs b/espanso/src/gui/modulo/search.rs
new file mode 100644
index 00000000..163ef943
--- /dev/null
+++ b/espanso/src/gui/modulo/search.rs
@@ -0,0 +1,90 @@
+/*
+ * 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 serde::Serialize;
+use serde_json::Value;
+use std::collections::HashMap;
+
+use crate::gui::{SearchItem, SearchUI};
+
+use super::manager::ModuloManager;
+
+pub struct ModuloSearchUI<'a> {
+  manager: &'a ModuloManager,
+}
+
+impl<'a> ModuloSearchUI<'a> {
+  pub fn new(manager: &'a ModuloManager) -> Self {
+    Self { manager }
+  }
+}
+
+impl<'a> SearchUI for ModuloSearchUI<'a> {
+  fn show(&self, items: &[SearchItem], hint: Option<&str>) -> anyhow::Result<Option<String>> {
+    let modulo_config = ModuloSearchConfig {
+      title: "espanso",
+      hint,
+      items: convert_items(items),
+    };
+
+    let json_config = serde_json::to_string(&modulo_config)?;
+    let output = self
+      .manager
+      .invoke(&["search", "-j", "-i", "-"], &json_config)?;
+    let json: Result<HashMap<String, Value>, _> = serde_json::from_str(&output);
+    match json {
+      Ok(json) => {
+        if let Some(Value::String(selected_id)) = json.get("selected") {
+          Ok(Some(selected_id.clone()))
+        } else {
+          Ok(None)
+        }
+      }
+      Err(error) => Err(error.into()),
+    }
+  }
+}
+
+#[derive(Debug, Serialize)]
+struct ModuloSearchConfig<'a> {
+  title: &'a str,
+  hint: Option<&'a str>,
+  items: Vec<ModuloSearchItemConfig<'a>>,
+}
+
+#[derive(Debug, Serialize)]
+struct ModuloSearchItemConfig<'a> {
+  id: &'a str,
+  label: &'a str,
+  trigger: Option<&'a str>,
+  is_builtin: bool,
+}
+
+// TODO: test
+fn convert_items(items: &[SearchItem]) -> Vec<ModuloSearchItemConfig> {
+  items
+    .iter()
+    .map(|item| ModuloSearchItemConfig {
+      id: &item.id,
+      label: &item.label,
+      trigger: item.tag.as_deref(),
+      is_builtin: item.is_builtin,
+    })
+    .collect()
+}
diff --git a/espanso/src/icon.rs b/espanso/src/icon.rs
new file mode 100644
index 00000000..b0d6e03b
--- /dev/null
+++ b/espanso/src/icon.rs
@@ -0,0 +1,160 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use log::{debug, info};
+use std::path::{Path, PathBuf};
+
+const ICON_BINARY: &[u8] = include_bytes!("res/icon.png");
+const LOGO_NO_BACKGROUND_BINARY: &[u8] = include_bytes!("res/logo_no_background.png");
+
+#[cfg(target_os = "windows")]
+const WINDOWS_NORMAL_DARK_ICO_BINARY: &[u8] = include_bytes!("res/windows/normal_dark.ico");
+#[cfg(target_os = "windows")]
+const WINDOWS_DISABLED_DARK_ICO_BINARY: &[u8] = include_bytes!("res/windows/disabled_dark.ico");
+#[cfg(target_os = "windows")]
+const WINDOWS_LOGO_ICO_BINARY: &[u8] = include_bytes!("res/windows/logo.ico");
+#[cfg(target_os = "windows")]
+const WINDOWS_TRAY_EXPLAIN_IMAGE: &[u8] = include_bytes!("res/windows/tray_explain_image.png");
+
+#[cfg(target_os = "macos")]
+const MAC_BINARY: &[u8] = include_bytes!("res/macos/icon.png");
+#[cfg(target_os = "macos")]
+const MAC_DISABLED_BINARY: &[u8] = include_bytes!("res/macos/icondisabled.png");
+#[cfg(target_os = "macos")]
+const MAC_SYSTEM_DISABLED_BINARY: &[u8] = include_bytes!("res/macos/iconsystemdisabled.png");
+#[cfg(target_os = "macos")]
+const MAC_ACCESSIBILITY_1_BINARY: &[u8] = include_bytes!("res/accessibility_1.png");
+#[cfg(target_os = "macos")]
+const MAC_ACCESSIBILITY_2_BINARY: &[u8] = include_bytes!("res/accessibility_2.png");
+#[cfg(target_os = "macos")]
+const MAC_STATUS_ICON_EXPLAIN_IMAGE: &[u8] = include_bytes!("res/macos/icon_explain_image.png");
+
+#[derive(Debug, Default)]
+pub struct IconPaths {
+  pub form_icon: Option<PathBuf>,
+  pub search_icon: Option<PathBuf>,
+  pub wizard_icon: Option<PathBuf>,
+
+  pub tray_icon_normal: Option<PathBuf>,
+  pub tray_icon_disabled: Option<PathBuf>,
+  pub tray_icon_system_disabled: Option<PathBuf>,
+
+  pub accessibility_image_1: Option<PathBuf>,
+  pub accessibility_image_2: Option<PathBuf>,
+  pub tray_explain_image: Option<PathBuf>,
+
+  pub logo: Option<PathBuf>,
+  pub logo_no_background: Option<PathBuf>,
+}
+
+#[cfg(target_os = "windows")]
+pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
+  Ok(IconPaths {
+    form_icon: Some(extract_icon(
+      WINDOWS_LOGO_ICO_BINARY,
+      &runtime_dir.join("form.ico"),
+    )?),
+    search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
+    wizard_icon: Some(extract_icon(
+      WINDOWS_LOGO_ICO_BINARY,
+      &runtime_dir.join("wizard.ico"),
+    )?),
+    tray_icon_normal: Some(extract_icon(
+      WINDOWS_NORMAL_DARK_ICO_BINARY,
+      &runtime_dir.join("normal.ico"),
+    )?),
+    tray_icon_disabled: Some(extract_icon(
+      WINDOWS_DISABLED_DARK_ICO_BINARY,
+      &runtime_dir.join("disabled.ico"),
+    )?),
+    logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?),
+    logo_no_background: Some(extract_icon(
+      LOGO_NO_BACKGROUND_BINARY,
+      &runtime_dir.join("icon_no_background.png"),
+    )?),
+    tray_explain_image: Some(extract_icon(
+      WINDOWS_TRAY_EXPLAIN_IMAGE,
+      &runtime_dir.join("tray_explain_image.png"),
+    )?),
+    ..Default::default()
+  })
+}
+
+#[cfg(target_os = "macos")]
+pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
+  Ok(IconPaths {
+    search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
+    tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normal.png"))?),
+    tray_icon_disabled: Some(extract_icon(
+      MAC_DISABLED_BINARY,
+      &runtime_dir.join("disabled.png"),
+    )?),
+    tray_icon_system_disabled: Some(extract_icon(
+      MAC_SYSTEM_DISABLED_BINARY,
+      &runtime_dir.join("systemdisabled.png"),
+    )?),
+    logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?),
+    logo_no_background: Some(extract_icon(
+      LOGO_NO_BACKGROUND_BINARY,
+      &runtime_dir.join("icon_no_background.png"),
+    )?),
+    accessibility_image_1: Some(extract_icon(
+      MAC_ACCESSIBILITY_1_BINARY,
+      &runtime_dir.join("accessibility_1.png"),
+    )?),
+    accessibility_image_2: Some(extract_icon(
+      MAC_ACCESSIBILITY_2_BINARY,
+      &runtime_dir.join("accessibility_2.png"),
+    )?),
+    tray_explain_image: Some(extract_icon(
+      MAC_STATUS_ICON_EXPLAIN_IMAGE,
+      &runtime_dir.join("icon_explain_image.png"),
+    )?),
+    ..Default::default()
+  })
+}
+
+#[cfg(target_os = "linux")]
+pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
+  Ok(IconPaths {
+    logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("iconv2.png"))?),
+    search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
+    logo_no_background: Some(extract_icon(
+      LOGO_NO_BACKGROUND_BINARY,
+      &runtime_dir.join("icon_no_background.png"),
+    )?),
+    ..Default::default()
+  })
+}
+
+// TODO: test
+fn extract_icon(data: &[u8], target_file: &Path) -> Result<PathBuf> {
+  if target_file.exists() {
+    debug!(
+      "skipping extraction for '{:?}', as it's already present",
+      target_file
+    );
+  } else {
+    std::fs::write(target_file, data)?;
+    info!("extracted icon to: {:?}", target_file);
+  }
+
+  Ok(target_file.to_owned())
+}
diff --git a/espanso/src/ipc.rs b/espanso/src/ipc.rs
new file mode 100644
index 00000000..4d157d4c
--- /dev/null
+++ b/espanso/src/ipc.rs
@@ -0,0 +1,50 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use espanso_ipc::{IPCClient, IPCServer};
+use serde::{Deserialize, Serialize};
+use std::path::Path;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum IPCEvent {
+  Exit,
+  ExitAllProcesses,
+}
+
+pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result<impl IPCServer<IPCEvent>> {
+  create_ipc_server(runtime_dir, "daemonv2")
+}
+
+pub fn create_worker_ipc_server(runtime_dir: &Path) -> Result<impl IPCServer<IPCEvent>> {
+  create_ipc_server(runtime_dir, "workerv2")
+}
+
+pub fn create_ipc_client_to_worker(runtime_dir: &Path) -> Result<impl IPCClient<IPCEvent>> {
+  create_ipc_client(runtime_dir, "workerv2")
+}
+
+fn create_ipc_server(runtime_dir: &Path, name: &str) -> Result<impl IPCServer<IPCEvent>> {
+  espanso_ipc::server(&format!("espanso{}", name), runtime_dir)
+}
+
+fn create_ipc_client(runtime_dir: &Path, target_process: &str) -> Result<impl IPCClient<IPCEvent>> {
+  let client = espanso_ipc::client(&format!("espanso{}", target_process), runtime_dir)?;
+  Ok(client)
+}
diff --git a/espanso/src/lock.rs b/espanso/src/lock.rs
new file mode 100644
index 00000000..ee2dc2e1
--- /dev/null
+++ b/espanso/src/lock.rs
@@ -0,0 +1,79 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use fs2::FileExt;
+use std::{
+  fs::{File, OpenOptions},
+  path::Path,
+};
+
+pub struct Lock {
+  lock_file: File,
+}
+
+impl Lock {
+  #[allow(dead_code)]
+  pub fn release(self) -> Result<()> {
+    self.lock_file.unlock()?;
+    Ok(())
+  }
+
+  fn acquire(runtime_dir: &Path, name: &str) -> Option<Lock> {
+    let lock_file_path = runtime_dir.join(format!("{}.lock", name));
+    let lock_file = OpenOptions::new()
+      .read(true)
+      .write(true)
+      .create(true)
+      .open(&lock_file_path)
+      .unwrap_or_else(|_| {
+        panic!(
+          "unable to create reference to lock file: {:?}",
+          lock_file_path
+        )
+      });
+
+    if lock_file.try_lock_exclusive().is_ok() {
+      Some(Lock { lock_file })
+    } else {
+      None
+    }
+  }
+}
+
+impl Drop for Lock {
+  fn drop(&mut self) {
+    self
+      .lock_file
+      .unlock()
+      .unwrap_or_else(|_| panic!("unable to unlock lock_file: {:?}", self.lock_file));
+  }
+}
+
+pub fn acquire_daemon_lock(runtime_dir: &Path) -> Option<Lock> {
+  Lock::acquire(runtime_dir, "espanso-daemon")
+}
+
+pub fn acquire_worker_lock(runtime_dir: &Path) -> Option<Lock> {
+  Lock::acquire(runtime_dir, "espanso-worker")
+}
+
+pub fn acquire_legacy_lock(runtime_dir: &Path) -> Option<Lock> {
+  Lock::acquire(runtime_dir, "espanso")
+}
diff --git a/espanso/src/logging/mod.rs b/espanso/src/logging/mod.rs
new file mode 100644
index 00000000..8f070679
--- /dev/null
+++ b/espanso/src/logging/mod.rs
@@ -0,0 +1,126 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::{
+  fs::{File, OpenOptions},
+  path::Path,
+};
+use std::{
+  io::Write,
+  sync::{Arc, Mutex},
+};
+
+/// This struct can be passed as an output to the logger to "defer" the
+/// decision of the output file
+#[derive(Clone)]
+pub(crate) struct FileProxy {
+  output: Arc<Mutex<Output>>,
+}
+
+enum Output {
+  Memory(Vec<u8>),
+  File(File),
+}
+
+impl FileProxy {
+  pub fn new() -> Self {
+    Self {
+      output: Arc::new(Mutex::new(Output::Memory(Vec::new()))),
+    }
+  }
+
+  pub fn set_output_file(&self, path: &Path, read_only: bool, create_new: bool) -> Result<()> {
+    // Remove previous log, if present
+    if create_new && !read_only && path.is_file() {
+      std::fs::remove_file(path)?;
+    }
+
+    let mut log_file = OpenOptions::new()
+      .read(true)
+      .write(!read_only)
+      .create(true)
+      .append(true)
+      .open(path)?;
+    let mut lock = self.output.lock().expect("unable to obtain FileProxy lock");
+
+    // Transfer the log content that has been buffered into the file
+    if let Output::Memory(buffered) = &mut (*lock) {
+      log_file.write_all(buffered)?;
+      buffered.clear();
+    }
+    *lock = Output::File(log_file);
+    Ok(())
+  }
+}
+
+impl Write for FileProxy {
+  fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+    match self.output.lock() {
+      Ok(mut lock) => {
+        match &mut (*lock) {
+          // Write to the memory buffer until a file is ready
+          Output::Memory(buffer) => buffer.write(buf),
+          Output::File(output) => output.write(buf),
+        }
+      }
+      Err(_) => Err(std::io::Error::new(
+        std::io::ErrorKind::Other,
+        "lock poison error",
+      )),
+    }
+  }
+
+  fn flush(&mut self) -> std::io::Result<()> {
+    match self.output.lock() {
+      Ok(mut lock) => match &mut (*lock) {
+        Output::Memory(buffer) => buffer.flush(),
+        Output::File(output) => output.flush(),
+      },
+      Err(_) => Err(std::io::Error::new(
+        std::io::ErrorKind::Other,
+        "lock poison error",
+      )),
+    }
+  }
+}
+
+#[macro_export]
+macro_rules! info_println {
+  ($($tts:tt)*) => {
+    println!($($tts)*);
+    log::info!($($tts)*);
+  }
+}
+
+#[macro_export]
+macro_rules! warn_eprintln {
+  ($($tts:tt)*) => {
+    eprintln!($($tts)*);
+    log::warn!($($tts)*);
+  }
+}
+
+#[macro_export]
+macro_rules! error_eprintln {
+  ($($tts:tt)*) => {
+    eprintln!($($tts)*);
+    log::error!($($tts)*);
+  }
+}
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
new file mode 100644
index 00000000..02b56711
--- /dev/null
+++ b/espanso/src/main.rs
@@ -0,0 +1,610 @@
+/*
+ * 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/>.
+ */
+
+// This is needed to avoid showing a console window when starting espanso on Windows
+#![windows_subsystem = "windows"]
+
+#[macro_use]
+extern crate lazy_static;
+
+use std::path::PathBuf;
+
+use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
+use cli::{CliAlias, CliModule, CliModuleArgs};
+use log::{error, info, warn};
+use logging::FileProxy;
+use simplelog::{
+  CombinedLogger, ConfigBuilder, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger,
+};
+
+use crate::{
+  cli::{LogMode, PathsOverrides},
+  config::load_config,
+};
+
+mod capabilities;
+mod cli;
+mod common_flags;
+mod config;
+mod exit_code;
+mod gui;
+mod icon;
+mod ipc;
+mod lock;
+#[macro_use]
+mod logging;
+mod patch;
+mod path;
+mod preferences;
+mod util;
+
+const VERSION: &str = env!("CARGO_PKG_VERSION");
+const LOG_FILE_NAME: &str = "espanso.log";
+
+lazy_static! {
+  static ref CLI_HANDLERS: Vec<CliModule> = vec![
+    cli::path::new(),
+    cli::edit::new(),
+    cli::launcher::new(),
+    cli::log::new(),
+    cli::worker::new(),
+    cli::daemon::new(),
+    cli::modulo::new(),
+    cli::migrate::new(),
+    cli::env_path::new(),
+    cli::service::new(),
+    cli::workaround::new(),
+    cli::package::new(),
+  ];
+  static ref ALIASES: Vec<CliAlias> = vec![
+    CliAlias {
+      subcommand: "start".to_owned(),
+      forward_into: "service".to_owned(),
+    },
+    CliAlias {
+      subcommand: "restart".to_owned(),
+      forward_into: "service".to_owned(),
+    },
+    CliAlias {
+      subcommand: "stop".to_owned(),
+      forward_into: "service".to_owned(),
+    },
+    CliAlias {
+      subcommand: "install".to_owned(),
+      forward_into: "package".to_owned(),
+    },
+    CliAlias {
+      subcommand: "uninstall".to_owned(),
+      forward_into: "package".to_owned(),
+    },
+  ];
+}
+
+fn main() {
+  util::attach_console();
+
+  let install_subcommand = SubCommand::with_name("install")
+    .about("Install a package")
+    .arg(
+      Arg::with_name("external")
+        .short("e")
+        .long("external")
+        .required(false)
+        .takes_value(false)
+        .help("Allow installing packages from non-verified repositories."),
+    )
+    .arg(Arg::with_name("package_name").help("Package name"))
+    .arg(
+      Arg::with_name("version")
+        .long("version")
+        .required(false)
+        .takes_value(true)
+        .help("Force a particular version to be installed instead of the latest available."),
+    )
+    .arg(
+      Arg::with_name("git")
+        .long("git")
+        .required(false)
+        .takes_value(true)
+        .help("Git repository from which espanso should install the package."),
+    )
+    .arg(
+      Arg::with_name("git-branch")
+        .long("git-branch")
+        .required(false)
+        .takes_value(true)
+        .help("Force espanso to search for the package on a specific git branch"),
+    )
+    .arg(
+      Arg::with_name("force")
+        .long("force")
+        .required(false)
+        .takes_value(false)
+        .help("Overwrite the package if already installed"),
+    )
+    .arg(
+      Arg::with_name("refresh-index")
+        .long("refresh-index")
+        .required(false)
+        .takes_value(false)
+        .help("Request a fresh copy of the Espanso Hub package index instead of using the cached version.")
+    )
+    .arg(
+      Arg::with_name("use-native-git")
+        .long("use-native-git")
+        .required(false)
+        .takes_value(false)
+        .help("If specified, espanso will use the 'git' command instead of trying direct methods."),
+    );
+
+  let uninstall_subcommand = SubCommand::with_name("uninstall")
+    .about("Remove a package")
+    .arg(Arg::with_name("package_name").help("Package name"));
+
+  let start_subcommand = SubCommand::with_name("start")
+    .about("Start espanso as a service")
+    .arg(
+      Arg::with_name("unmanaged")
+        .long("unmanaged")
+        .required(false)
+        .takes_value(false)
+        .help("Run espanso as an unmanaged service (avoid system manager)"),
+    );
+  let restart_subcommand = start_subcommand
+    .clone()
+    .about("Restart the espanso service")
+    .name("restart");
+  let stop_subcommand = SubCommand::with_name("stop").about("Stop espanso service");
+
+  let mut clap_instance = App::new("espanso")
+    .version(VERSION)
+    .author("Federico Terzi")
+    .about("A Privacy-first, Cross-platform Text Expander")
+    .arg(
+      Arg::with_name("v")
+        .short("v")
+        .multiple(true)
+        .help("Sets the level of verbosity"),
+    )
+    .arg(
+      Arg::with_name("config_dir")
+        .long("config_dir")
+        .takes_value(true)
+        .help("Specify a custom path from which espanso should read the configuration"),
+    )
+    .arg(
+      Arg::with_name("package_dir")
+        .long("package_dir")
+        .takes_value(true)
+        .help("Specify a custom path for the espanso package directory"),
+    )
+    .arg(
+      Arg::with_name("runtime_dir")
+        .long("runtime_dir")
+        .takes_value(true)
+        .help("Specify a custom path for the espanso runtime directory"),
+    )
+    .subcommand(
+      SubCommand::with_name("env-path")
+        .arg(
+          Arg::with_name("prompt")
+            .long("prompt")
+            .required(false)
+            .takes_value(false)
+            .help("macOS only:Prompt for permissions if the operation requires elevated privileges."),
+        )
+        .subcommand(SubCommand::with_name("register").about("Add 'espanso' command to PATH"))
+        .subcommand(SubCommand::with_name("unregister").about("Remove 'espanso' command from PATH"))
+        .about("Add or remove the 'espanso' command from the PATH"),
+    )
+    // .subcommand(SubCommand::with_name("cmd")
+    //     .about("Send a command to the espanso daemon.")
+    //     .subcommand(SubCommand::with_name("exit")
+    //         .about("Terminate the daemon."))
+    //     .subcommand(SubCommand::with_name("enable")
+    //         .about("Enable the espanso replacement engine."))
+    //     .subcommand(SubCommand::with_name("disable")
+    //         .about("Disable the espanso replacement engine."))
+    //     .subcommand(SubCommand::with_name("toggle")
+    //         .about("Toggle the status of the espanso replacement engine."))
+    // )
+    .subcommand(SubCommand::with_name("edit")
+        .about("Shortcut to open the default text editor to edit config files")
+        .arg(Arg::with_name("target_file")
+            .help(r#"Defaults to "match/base.yml", it contains the relative path of the file you want to edit, 
+such as 'config/default.yml' or 'match/base.yml'. 
+For convenience, you can also specify the name directly and Espanso will figure out the path. 
+For example, specifying 'email' is equivalent to 'match/email.yml'."#))
+        // .arg(Arg::with_name("norestart")
+        //     .short("n")
+        //     .long("norestart")
+        //     .required(false)
+        //     .takes_value(false)
+        //     .help("Avoid restarting espanso after editing the file"))
+    )
+    // .subcommand(SubCommand::with_name("detect")
+    //     .about("Tool to detect current window properties, to simplify filters creation."))
+    .subcommand(
+      SubCommand::with_name("daemon")
+        .setting(AppSettings::Hidden)
+        .about("Start the daemon without spawning a new process."),
+    )
+    // .subcommand(SubCommand::with_name("register")
+    //     .about("MacOS and Linux only. Register espanso in the system daemon manager."))
+    // .subcommand(SubCommand::with_name("unregister")
+    //     .about("MacOS and Linux only. Unregister espanso from the system daemon manager."))
+    .subcommand(SubCommand::with_name("launcher").setting(AppSettings::Hidden))
+    .subcommand(SubCommand::with_name("log").about("Print the daemon logs."))
+    .subcommand(
+      SubCommand::with_name("modulo")
+        .setting(AppSettings::Hidden)
+        .subcommand(
+          SubCommand::with_name("form")
+            .about("Display a customizable form")
+            .arg(
+              Arg::with_name("input_file")
+                .short("i")
+                .takes_value(true)
+                .help("Input file or - for stdin"),
+            )
+            .arg(
+              Arg::with_name("json")
+                .short("j")
+                .required(false)
+                .takes_value(false)
+                .help("Interpret the input data as JSON"),
+            ),
+        )
+        .subcommand(
+          SubCommand::with_name("search")
+            .about("Display a search box")
+            .arg(
+              Arg::with_name("input_file")
+                .short("i")
+                .takes_value(true)
+                .help("Input file or - for stdin"),
+            )
+            .arg(
+              Arg::with_name("json")
+                .short("j")
+                .required(false)
+                .takes_value(false)
+                .help("Interpret the input data as JSON"),
+            ),
+        )
+        .subcommand(SubCommand::with_name("troubleshoot").about("Display the troubleshooting GUI"))
+        .subcommand(
+          SubCommand::with_name("welcome")
+            .about("Display the welcome screen")
+            .arg(
+              Arg::with_name("already-running")
+                .long("already-running")
+                .takes_value(false),
+            ),
+        ),
+    )
+    // .subcommand(SubCommand::with_name("start")
+    //     .about("Start the daemon spawning a new process in the background."))
+    // .subcommand(SubCommand::with_name("stop")
+    //     .about("Stop the espanso daemon."))
+    // .subcommand(SubCommand::with_name("restart")
+    //     .about("Restart the espanso daemon."))
+    // .subcommand(SubCommand::with_name("status")
+    //     .about("Check if the espanso daemon is running or not."))
+    .subcommand(
+      SubCommand::with_name("path")
+        .about("Prints all the espanso directory paths to easily locate configuration and matches.")
+        .subcommand(SubCommand::with_name("config").about("Print the current config folder path."))
+        .subcommand(
+          SubCommand::with_name("packages").about("Print the current packages folder path."),
+        )
+        .subcommand(
+          SubCommand::with_name("data")
+            .about("Print the current data folder path.")
+            .setting(AppSettings::Hidden),
+        ) // Legacy path
+        .subcommand(
+          SubCommand::with_name("runtime").about("Print the current runtime folder path."),
+        )
+        .subcommand(
+          SubCommand::with_name("default").about("Print the default configuration file path."),
+        )
+        .subcommand(SubCommand::with_name("base").about("Print the default match file path.")),
+    )
+    .subcommand(
+      SubCommand::with_name("migrate")
+        .about("Automatically migrate legacy config files to the new v2 format.")
+        .arg(Arg::with_name("noconfirm").long("noconfirm"))
+        .help("Migrate the configuration without asking for confirmation"),
+    )
+    .subcommand(
+      SubCommand::with_name("service")
+        .subcommand(SubCommand::with_name("register").about("Register espanso as a system service"))
+        .subcommand(
+          SubCommand::with_name("unregister").about("Unregister espanso from system services"),
+        )
+        .subcommand(
+          SubCommand::with_name("check")
+            .about("Check if espanso is registered as a system service"),
+        )
+        .subcommand(start_subcommand.clone())
+        .subcommand(restart_subcommand.clone())
+        .subcommand(stop_subcommand.clone())
+        .about("Register and manage 'espanso' as a system service."),
+    )
+    .subcommand(start_subcommand)
+    .subcommand(restart_subcommand)
+    .subcommand(stop_subcommand)
+    // .subcommand(SubCommand::with_name("match")
+    //     .about("List and execute matches from the CLI")
+    //     .subcommand(SubCommand::with_name("list")
+    //         .about("Print all matches to standard output")
+    //         .arg(Arg::with_name("json")
+    //             .short("j")
+    //             .long("json")
+    //             .help("Return the matches as json")
+    //             .required(false)
+    //             .takes_value(false)
+    //         )
+    //         .arg(Arg::with_name("onlytriggers")
+    //             .short("t")
+    //             .long("onlytriggers")
+    //             .help("Print only triggers without replacement")
+    //             .required(false)
+    //             .takes_value(false)
+    //         )
+    //         .arg(Arg::with_name("preservenewlines")
+    //             .short("n")
+    //             .long("preservenewlines")
+    //             .help("Preserve newlines when printing replacements")
+    //             .required(false)
+    //             .takes_value(false)
+    //         )
+    //     )
+    //     .subcommand(SubCommand::with_name("exec")
+    //         .about("Triggers the expansion of the given match")
+    //         .arg(Arg::with_name("trigger")
+    //             .help("The trigger of the match to be expanded")
+    //         )
+    //     )
+    // )
+    .subcommand(
+      SubCommand::with_name("package")
+        .about("package-management commands")
+        .subcommand(install_subcommand.clone())
+        .subcommand(uninstall_subcommand.clone())
+        .subcommand(SubCommand::with_name("update").about(
+          "Update a package. If 'all' is passed as package name, attempts to update all packages.",
+        ).arg(Arg::with_name("package_name").help("Package name")))
+        .subcommand(
+          SubCommand::with_name("list").about("List all installed packages"), // TODO: update <Package> and update all
+        ),
+    )
+    .subcommand(
+      SubCommand::with_name("workaround")
+        .subcommand(
+          SubCommand::with_name("secure-input")
+            .about("Attempt to disable secure input by automating the common steps."),
+        )
+        .about("A collection of workarounds to solve some common problems."),
+    )
+    .subcommand(
+      SubCommand::with_name("worker")
+        .setting(AppSettings::Hidden)
+        .arg(
+          Arg::with_name("start-reason")
+            .long("start-reason")
+            .required(false)
+            .takes_value(true),
+        )
+        .arg(
+          Arg::with_name("monitor-daemon")
+            .long("monitor-daemon")
+            .required(false)
+            .takes_value(false),
+        ),
+    )
+    .subcommand(install_subcommand)
+    .subcommand(uninstall_subcommand);
+
+  // TODO: explain that the register and unregister commands are only meaningful
+  // when using the system daemon manager on macOS and Linux
+
+  // TODO: set the LSEnvironment variable as described here: https://stackoverflow.com/questions/12203377/combined-gui-and-command-line-os-x-app?rq=1
+  // to detect if the executable was launched inside an AppBundle, and if so, launch the "launcher" handler
+  // This should only apply when on macOS.
+
+  let matches = clap_instance.clone().get_matches();
+  let log_level = match matches.occurrences_of("v") {
+    0 | 1 => LevelFilter::Info,
+
+    // Trace mode is only available in debug mode for security reasons
+    #[cfg(debug_assertions)]
+    3 => LevelFilter::Trace,
+
+    _ => LevelFilter::Debug,
+  };
+
+  let alias = ALIASES
+    .iter()
+    .find(|cli| matches.subcommand_matches(&cli.subcommand).is_some());
+
+  let mut handler = if let Some(alias) = alias {
+    CLI_HANDLERS
+      .iter()
+      .find(|cli| cli.subcommand == alias.forward_into)
+  } else {
+    CLI_HANDLERS
+      .iter()
+      .find(|cli| matches.subcommand_matches(&cli.subcommand).is_some())
+  };
+
+  if handler.is_none() {
+    // When started from the macOS App Bundle, override the default
+    // handler with "launcher" if not present, otherwise the GUI could not be started.
+    if let Some(context) = std::env::var_os("MAC_LAUNCH_CONTEXT") {
+      if context == "bundle" {
+        handler = CLI_HANDLERS.iter().find(|cli| cli.subcommand == "launcher");
+      }
+    }
+
+    // When started from a Linux app image, override the default handler with the launcher
+    // to start espanso when launching it directly
+    if std::env::var_os("APPIMAGE").is_some() {
+      handler = CLI_HANDLERS.iter().find(|cli| cli.subcommand == "launcher");
+    }
+  }
+
+  if let Some(handler) = handler {
+    let log_proxy = FileProxy::new();
+    if handler.enable_logs {
+      let config = ConfigBuilder::new()
+        .set_time_to_local(true)
+        .set_time_format(format!(
+          "%H:%M:%S [{}({})]",
+          handler.subcommand,
+          std::process::id()
+        ))
+        .set_location_level(LevelFilter::Off)
+        .add_filter_ignore_str("html5ever")
+        .build();
+
+      let mut outputs: Vec<Box<dyn SharedLogger>> = vec![WriteLogger::new(
+        LevelFilter::Info,
+        config.clone(),
+        log_proxy.clone(),
+      )];
+
+      if !handler.disable_logs_terminal_output {
+        outputs.insert(0, TermLogger::new(log_level, config, TerminalMode::Mixed));
+      }
+
+      CombinedLogger::init(outputs).expect("unable to initialize logs");
+
+      // Activate logging for panics
+      log_panics::init();
+    }
+
+    // If the process doesn't require linux capabilities, disable them
+    if !handler.requires_linux_capabilities {
+      if let Err(err) = crate::capabilities::clear_capabilities() {
+        error!("unable to clear linux capabilities: {}", err);
+      }
+    }
+
+    // If explicitly requested, we show the Dock icon on macOS
+    // We need to enable this selectively, otherwise we would end up with multiple
+    // dock icons due to the multi-process nature of espanso.
+    #[cfg(target_os = "macos")]
+    if handler.show_in_dock {
+      espanso_mac_utils::convert_to_foreground_app();
+    }
+
+    let mut cli_args: CliModuleArgs = CliModuleArgs::default();
+
+    if handler.requires_paths || handler.requires_config {
+      let force_config_path = get_path_override(&matches, "config_dir", "ESPANSO_CONFIG_DIR");
+      let force_package_path = get_path_override(&matches, "package_dir", "ESPANSO_PACKAGE_DIR");
+      let force_runtime_path = get_path_override(&matches, "runtime_dir", "ESPANSO_RUNTIME_DIR");
+
+      let paths = espanso_path::resolve_paths(
+        force_config_path.as_deref(),
+        force_package_path.as_deref(),
+        force_runtime_path.as_deref(),
+      );
+
+      cli_args.paths_overrides = Some(PathsOverrides {
+        config: force_config_path,
+        packages: force_package_path,
+        runtime: force_runtime_path,
+      });
+
+      info!("reading configs from: {:?}", paths.config);
+      info!("reading packages from: {:?}", paths.packages);
+      info!("using runtime dir: {:?}", paths.runtime);
+
+      if handler.requires_config {
+        let config_result =
+          load_config(&paths.config, &paths.packages).expect("unable to load config");
+
+        cli_args.is_legacy_config = config_result.is_legacy_config;
+        cli_args.config_store = Some(config_result.config_store);
+        cli_args.match_store = Some(config_result.match_store);
+        cli_args.non_fatal_errors = config_result.non_fatal_errors;
+
+        if config_result.is_legacy_config {
+          warn!("espanso is reading the configuration using compatibility mode, thus some features might not be available");
+          warn!("you can migrate to the new configuration format by running 'espanso migrate' in a terminal");
+        }
+      }
+
+      if handler.enable_logs {
+        log_proxy
+          .set_output_file(
+            &paths.runtime.join(LOG_FILE_NAME),
+            handler.log_mode == LogMode::Read,
+            handler.log_mode == LogMode::CleanAndAppend,
+          )
+          .expect("unable to set up log output file");
+      }
+
+      cli_args.paths = Some(paths);
+    }
+
+    // If the current handler is an alias, rather than sending the sub-arguments
+    // we simply forward the current ones
+    // For example, the args for "espanso start" are forwarded to "espanso service start"
+    if alias.is_some() {
+      cli_args.cli_args = Some(matches);
+    } else if let Some(args) = matches.subcommand_matches(&handler.subcommand) {
+      cli_args.cli_args = Some(args.clone());
+    }
+
+    let exit_code = (handler.entry)(cli_args);
+
+    std::process::exit(exit_code);
+  } else {
+    clap_instance
+      .print_long_help()
+      .expect("unable to print help");
+    println!();
+  }
+}
+
+fn get_path_override(matches: &ArgMatches, argument: &str, env_var: &str) -> Option<PathBuf> {
+  if let Some(path) = matches.value_of(argument) {
+    let path = PathBuf::from(path.trim());
+    if path.is_dir() {
+      Some(path)
+    } else {
+      error_eprintln!("{} argument was specified, but it doesn't point to a valid directory. Make sure to create it first.", argument);
+      std::process::exit(1);
+    }
+  } else if let Ok(path) = std::env::var(env_var) {
+    let path = PathBuf::from(path.trim());
+    if path.is_dir() {
+      Some(path)
+    } else {
+      error_eprintln!("{} env variable was specified, but it doesn't point to a valid directory. Make sure to create it first.", env_var);
+      std::process::exit(1);
+    }
+  } else {
+    None
+  }
+}
diff --git a/espanso/src/patch/config_store.rs b/espanso/src/patch/config_store.rs
new file mode 100644
index 00000000..169c4aaa
--- /dev/null
+++ b/espanso/src/patch/config_store.rs
@@ -0,0 +1,93 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use espanso_config::config::{Config, ConfigStore};
+use log::debug;
+
+use super::PatchDefinition;
+
+pub struct PatchedConfigStore {
+  config_store: Box<dyn ConfigStore>,
+  patches: Vec<PatchDefinition>,
+}
+
+impl PatchedConfigStore {
+  pub fn from_store(config_store: Box<dyn ConfigStore>) -> Self {
+    Self::from_store_with_patches(config_store, super::get_builtin_patches())
+  }
+
+  pub fn from_store_with_patches(
+    config_store: Box<dyn ConfigStore>,
+    patches: Vec<PatchDefinition>,
+  ) -> Self {
+    // Only keep the patches that should be active in the current system
+    let active_patches = patches
+      .into_iter()
+      .filter(|patch| {
+        let is_enabled = (patch.is_enabled)();
+
+        if is_enabled {
+          debug!("enabled '{}' patch", patch.name);
+        } else {
+          debug!("skipping '{}' patch", patch.name);
+        }
+
+        is_enabled
+      })
+      .collect();
+
+    Self {
+      config_store,
+      patches: active_patches,
+    }
+  }
+}
+
+impl ConfigStore for PatchedConfigStore {
+  fn default(&self) -> Arc<dyn Config> {
+    self.config_store.default()
+  }
+
+  fn active<'f>(&'f self, app: &espanso_config::config::AppProperties) -> Arc<dyn Config> {
+    let active_config = self.config_store.active(app);
+
+    if !active_config.apply_patch() {
+      return active_config;
+    }
+
+    // Check if a patch should be applied
+    if let Some(patch) = self.patches.iter().find(|patch| (patch.should_patch)(app)) {
+      (patch.apply)(active_config, patch.name)
+    } else {
+      active_config
+    }
+  }
+
+  fn configs(&self) -> Vec<Arc<dyn Config>> {
+    self.config_store.configs()
+  }
+
+  fn get_all_match_paths(&self) -> std::collections::HashSet<String> {
+    self.config_store.get_all_match_paths()
+  }
+}
+
+// TODO: test
diff --git a/espanso/src/patch/mod.rs b/espanso/src/patch/mod.rs
new file mode 100644
index 00000000..3582aaea
--- /dev/null
+++ b/espanso/src/patch/mod.rs
@@ -0,0 +1,65 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use espanso_config::config::{AppProperties, Config, ConfigStore};
+
+mod config_store;
+mod patches;
+
+pub fn patch_store(store: Box<dyn ConfigStore>) -> Box<dyn ConfigStore> {
+  Box::new(config_store::PatchedConfigStore::from_store(store))
+}
+
+fn get_builtin_patches() -> Vec<PatchDefinition> {
+  #[cfg(target_os = "windows")]
+  return vec![
+    patches::win::onenote_for_windows_10::patch(),
+    patches::win::vscode_win::patch(),
+  ];
+
+  #[cfg(target_os = "macos")]
+  return vec![];
+
+  #[cfg(target_os = "linux")]
+  return vec![
+    patches::linux::alacritty_terminal_x11::patch(),
+    patches::linux::emacs_x11::patch(),
+    patches::linux::generic_terminal_x11::patch(),
+    patches::linux::kitty_terminal_x11::patch(),
+    patches::linux::konsole_terminal_x11::patch(),
+    patches::linux::libreoffice_writer_x11::patch(),
+    patches::linux::simple_terminal_x11::patch(),
+    patches::linux::simple_terminal_2_x11::patch(),
+    patches::linux::terminator_terminal_x11::patch(),
+    patches::linux::termite_terminal_x11::patch(),
+    patches::linux::tilix_terminal_x11::patch(),
+    patches::linux::urxvt_terminal_x11::patch(),
+    patches::linux::xterm_terminal_x11::patch(),
+    patches::linux::yakuake_terminal_x11::patch(),
+  ];
+}
+
+pub struct PatchDefinition {
+  pub name: &'static str,
+  pub is_enabled: fn() -> bool,
+  pub should_patch: fn(app: &AppProperties) -> bool,
+  pub apply: fn(config: Arc<dyn Config>, name: &str) -> Arc<dyn Config>,
+}
diff --git a/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs b/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs
new file mode 100644
index 00000000..f02ea347
--- /dev/null
+++ b/espanso/src/patch/patches/linux/alacritty_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("Alacritty"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/emacs_x11.rs b/espanso/src/patch/patches/linux/emacs_x11.rs
new file mode 100644
index 00000000..c78bdae2
--- /dev/null
+++ b/espanso/src/patch/patches/linux/emacs_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("Emacs"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("SHIFT+INSERT".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/generic_terminal_x11.rs b/espanso/src/patch/patches/linux/generic_terminal_x11.rs
new file mode 100644
index 00000000..f0a60cb1
--- /dev/null
+++ b/espanso/src/patch/patches/linux/generic_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("terminal"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/kitty_terminal_x11.rs b/espanso/src/patch/patches/linux/kitty_terminal_x11.rs
new file mode 100644
index 00000000..f57f041e
--- /dev/null
+++ b/espanso/src/patch/patches/linux/kitty_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("kitty"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/konsole_terminal_x11.rs b/espanso/src/patch/patches/linux/konsole_terminal_x11.rs
new file mode 100644
index 00000000..5922eeba
--- /dev/null
+++ b/espanso/src/patch/patches/linux/konsole_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("konsole"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/libreoffice_writer_x11.rs b/espanso/src/patch/patches/linux/libreoffice_writer_x11.rs
new file mode 100644
index 00000000..4307b157
--- /dev/null
+++ b/espanso/src/patch/patches/linux/libreoffice_writer_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("libreoffice-writer"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          key_delay: Some(Some(2)),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/mod.rs b/espanso/src/patch/patches/linux/mod.rs
new file mode 100644
index 00000000..6dd311b2
--- /dev/null
+++ b/espanso/src/patch/patches/linux/mod.rs
@@ -0,0 +1,35 @@
+/*
+ * 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/>.
+ */
+
+pub mod alacritty_terminal_x11;
+pub mod emacs_x11;
+pub mod generic_terminal_x11;
+pub mod kitty_terminal_x11;
+pub mod konsole_terminal_x11;
+pub mod libreoffice_writer_x11;
+pub mod simple_terminal_2_x11;
+pub mod simple_terminal_x11;
+pub mod terminator_terminal_x11;
+pub mod termite_terminal_x11;
+pub mod tilix_terminal_x11;
+pub mod urxvt_terminal_x11;
+pub mod xterm_terminal_x11;
+pub mod yakuake_terminal_x11;
+
+mod util;
diff --git a/espanso/src/patch/patches/linux/simple_terminal_2_x11.rs b/espanso/src/patch/patches/linux/simple_terminal_2_x11.rs
new file mode 100644
index 00000000..2327add5
--- /dev/null
+++ b/espanso/src/patch/patches/linux/simple_terminal_2_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().to_lowercase().contains("st"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/simple_terminal_x11.rs b/espanso/src/patch/patches/linux/simple_terminal_x11.rs
new file mode 100644
index 00000000..87c5fe7c
--- /dev/null
+++ b/espanso/src/patch/patches/linux/simple_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("stterm"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("SHIFT+ALT+INSERT".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/terminator_terminal_x11.rs b/espanso/src/patch/patches/linux/terminator_terminal_x11.rs
new file mode 100644
index 00000000..e6ab86ac
--- /dev/null
+++ b/espanso/src/patch/patches/linux/terminator_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("Terminator"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/termite_terminal_x11.rs b/espanso/src/patch/patches/linux/termite_terminal_x11.rs
new file mode 100644
index 00000000..6e412c5c
--- /dev/null
+++ b/espanso/src/patch/patches/linux/termite_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("Termite"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/tilix_terminal_x11.rs b/espanso/src/patch/patches/linux/tilix_terminal_x11.rs
new file mode 100644
index 00000000..3b205f3a
--- /dev/null
+++ b/espanso/src/patch/patches/linux/tilix_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("Tilix"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/urxvt_terminal_x11.rs b/espanso/src/patch/patches/linux/urxvt_terminal_x11.rs
new file mode 100644
index 00000000..f09e51aa
--- /dev/null
+++ b/espanso/src/patch/patches/linux/urxvt_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("URxvt"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+ALT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/util.rs b/espanso/src/patch/patches/linux/util.rs
new file mode 100644
index 00000000..0a6b28f1
--- /dev/null
+++ b/espanso/src/patch/patches/linux/util.rs
@@ -0,0 +1,22 @@
+/*
+ * 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/>.
+ */
+
+pub fn is_wayland() -> bool {
+  cfg!(feature = "wayland")
+}
diff --git a/espanso/src/patch/patches/linux/xterm_terminal_x11.rs b/espanso/src/patch/patches/linux/xterm_terminal_x11.rs
new file mode 100644
index 00000000..2bc205ff
--- /dev/null
+++ b/espanso/src/patch/patches/linux/xterm_terminal_x11.rs
@@ -0,0 +1,44 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use espanso_config::config::Backend;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("XTerm"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          disable_x11_fast_inject: Some(true),
+          backend: Some(Backend::Inject),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/linux/yakuake_terminal_x11.rs b/espanso/src/patch/patches/linux/yakuake_terminal_x11.rs
new file mode 100644
index 00000000..31c6a9ea
--- /dev/null
+++ b/espanso/src/patch/patches/linux/yakuake_terminal_x11.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "linux") && !super::util::is_wayland(),
+    should_patch: |app| app.class.unwrap_or_default().contains("yakuake"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          paste_shortcut: Some(Some("CTRL+SHIFT+V".to_string())),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/macros.rs b/espanso/src/patch/patches/macros.rs
new file mode 100644
index 00000000..c89c4ac8
--- /dev/null
+++ b/espanso/src/patch/patches/macros.rs
@@ -0,0 +1,99 @@
+/*
+ * 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/>.
+ */
+
+#[macro_export]
+macro_rules! generate_patchable_config {
+  ( $struct_name:ident, $( $config_field:ident ->$config_type:ty ),* ) => {
+      use std::sync::Arc;
+      use espanso_config::config::{AppProperties, Config};
+
+      pub struct $struct_name {
+        base: Arc<dyn Config>,
+        patch: Patches,
+        patched_label: String,
+      }
+
+      impl $struct_name {
+        #[allow(dead_code)]
+        pub fn patch(base: Arc<dyn Config>, name: &str, patch: Patches) -> Self {
+          let patched_label = format!("{} (PATCHED: {})", base.label(), name);
+          Self {
+            base,
+            patch,
+            patched_label,
+          }
+        }
+      }
+
+      #[derive(Default)]
+      pub struct Patches {
+        $(
+          pub $config_field: Option<$config_type>,
+        )*
+      }
+
+      impl Config for $struct_name {
+        $(
+          fn $config_field(&self) -> $config_type {
+            if let Some(patched) = self.patch.$config_field.clone() {
+              return patched;
+            }
+
+            self.base.$config_field()
+          }
+        )*
+
+        fn id(&self) -> i32 {
+          self.base.id()
+        }
+
+        fn label(&self) -> &str {
+          &self.patched_label
+        }
+
+        fn match_paths(&self) -> &[String] {
+          self.base.match_paths()
+        }
+
+        fn is_match<'b>(&self, app: &AppProperties<'b>) -> bool {
+          self.base.is_match(app)
+        }
+
+        fn search_trigger(&self) -> Option<String> {
+          self.base.search_trigger()
+        }
+
+        fn search_shortcut(&self) -> Option<String> {
+          self.base.search_shortcut()
+        }
+
+        fn show_icon(&self) -> bool {
+          self.base.show_icon()
+        }
+
+        fn show_notifications(&self) -> bool {
+          self.base.show_notifications()
+        }
+
+        fn secure_input_notification(&self) -> bool {
+          self.base.secure_input_notification()
+        }
+      }
+  };
+}
diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs
new file mode 100644
index 00000000..5fddc89f
--- /dev/null
+++ b/espanso/src/patch/patches/mod.rs
@@ -0,0 +1,53 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_config::config::{Backend, RMLVOConfig, ToggleKey};
+
+#[cfg(target_os = "windows")]
+pub mod win;
+
+#[cfg(target_os = "linux")]
+pub mod linux;
+
+#[macro_use]
+mod macros;
+
+generate_patchable_config!(
+  PatchedConfig,
+  backend -> Backend,
+  enable -> bool,
+  clipboard_threshold -> usize,
+  pre_paste_delay -> usize,
+  paste_shortcut_event_delay -> usize,
+  paste_shortcut -> Option<String>,
+  disable_x11_fast_inject -> bool,
+  toggle_key -> Option<ToggleKey>,
+  auto_restart -> bool,
+  preserve_clipboard -> bool,
+  restore_clipboard_delay -> usize,
+  inject_delay -> Option<usize>,
+  key_delay -> Option<usize>,
+  evdev_modifier_delay -> Option<usize>,
+  word_separators -> Vec<String>,
+  backspace_limit -> usize,
+  apply_patch -> bool,
+  undo_backspace -> bool,
+  win32_exclude_orphan_events -> bool,
+  keyboard_layout -> Option<RMLVOConfig>
+);
diff --git a/espanso/src/patch/patches/win/mod.rs b/espanso/src/patch/patches/win/mod.rs
new file mode 100644
index 00000000..63fb8629
--- /dev/null
+++ b/espanso/src/patch/patches/win/mod.rs
@@ -0,0 +1,21 @@
+/*
+ * 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/>.
+ */
+
+pub mod onenote_for_windows_10;
+pub mod vscode_win;
diff --git a/espanso/src/patch/patches/win/onenote_for_windows_10.rs b/espanso/src/patch/patches/win/onenote_for_windows_10.rs
new file mode 100644
index 00000000..f1a5db74
--- /dev/null
+++ b/espanso/src/patch/patches/win/onenote_for_windows_10.rs
@@ -0,0 +1,41 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "windows"),
+    should_patch: |app| app.title.unwrap_or_default().contains("OneNote"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          key_delay: Some(Some(10)),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/patch/patches/win/vscode_win.rs b/espanso/src/patch/patches/win/vscode_win.rs
new file mode 100644
index 00000000..c080dee7
--- /dev/null
+++ b/espanso/src/patch/patches/win/vscode_win.rs
@@ -0,0 +1,44 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::Arc;
+
+use espanso_config::config::Backend;
+
+use crate::patch::patches::{PatchedConfig, Patches};
+use crate::patch::PatchDefinition;
+
+pub fn patch() -> PatchDefinition {
+  PatchDefinition {
+    name: module_path!().split(':').last().unwrap_or("unknown"),
+    is_enabled: || cfg!(target_os = "windows"),
+    should_patch: |app| app.exec.unwrap_or_default().contains("Code.exe"),
+    apply: |base, name| {
+      Arc::new(PatchedConfig::patch(
+        base,
+        name,
+        Patches {
+          key_delay: Some(Some(10)),
+          backend: Some(Backend::Clipboard),
+          ..Default::default()
+        },
+      ))
+    },
+  }
+}
diff --git a/espanso/src/path/linux.rs b/espanso/src/path/linux.rs
new file mode 100644
index 00000000..29a27c27
--- /dev/null
+++ b/espanso/src/path/linux.rs
@@ -0,0 +1,79 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::path::PathBuf;
+use thiserror::Error;
+
+pub fn is_espanso_in_path() -> bool {
+  PathBuf::from("/usr/local/bin/espanso").is_file()
+}
+
+pub fn add_espanso_to_path(_: bool) -> Result<()> {
+  let target_link_dir = PathBuf::from("/usr/local/bin");
+  let exec_path = get_binary_path()?;
+
+  if !target_link_dir.is_dir() {
+    return Err(PathError::UsrLocalBinDirDoesNotExist.into());
+  }
+
+  let target_link_path = target_link_dir.join("espanso");
+
+  if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) {
+    return Err(PathError::SymlinkError(error).into());
+  }
+
+  Ok(())
+}
+
+pub fn remove_espanso_from_path(_: bool) -> Result<()> {
+  let target_link_path = PathBuf::from("/usr/local/bin/espanso");
+
+  if std::fs::symlink_metadata(&target_link_path).is_err() {
+    return Err(PathError::SymlinkNotFound.into());
+  }
+
+  if let Err(error) = std::fs::remove_file(&target_link_path) {
+    return Err(PathError::SymlinkError(error).into());
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum PathError {
+  #[error("/usr/local/bin directory doesn't exist")]
+  UsrLocalBinDirDoesNotExist,
+
+  #[error("symlink error: `{0}`")]
+  SymlinkError(std::io::Error),
+
+  #[error("symlink does not exist, so there is nothing to remove.")]
+  SymlinkNotFound,
+}
+
+fn get_binary_path() -> Result<PathBuf> {
+  // If executed as part of an AppImage, get the app image path instead of
+  // the binary itself (which was extracted in a temp directory).
+  if let Some(app_image_path) = std::env::var_os("APPIMAGE") {
+    return Ok(PathBuf::from(app_image_path));
+  }
+
+  Ok(std::env::current_exe()?)
+}
diff --git a/espanso/src/path/macos.rs b/espanso/src/path/macos.rs
new file mode 100644
index 00000000..58f020ae
--- /dev/null
+++ b/espanso/src/path/macos.rs
@@ -0,0 +1,126 @@
+/*
+ * 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::io::ErrorKind;
+use std::path::PathBuf;
+use thiserror::Error;
+
+use anyhow::Result;
+use log::{error, warn};
+
+pub fn is_espanso_in_path() -> bool {
+  PathBuf::from("/usr/local/bin/espanso").is_file()
+}
+
+pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> {
+  let target_link_dir = PathBuf::from("/usr/local/bin");
+  let exec_path = std::env::current_exe()?;
+
+  if !target_link_dir.is_dir() {
+    return Err(PathError::UsrLocalBinDirDoesNotExist.into());
+  }
+
+  let target_link_path = target_link_dir.join("espanso");
+
+  if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) {
+    match error.kind() {
+      ErrorKind::PermissionDenied => {
+        if prompt_when_necessary {
+          warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
+
+          let params = format!(
+            r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##,
+            exec_path.to_string_lossy(),
+            target_link_path.to_string_lossy(),
+          );
+
+          let mut child = std::process::Command::new("osascript")
+            .args(&["-e", &params])
+            .spawn()?;
+
+          let result = child.wait()?;
+          if !result.success() {
+            return Err(PathError::ElevationRequestFailure.into());
+          }
+        } else {
+          return Err(PathError::SymlinkError(error).into());
+        }
+      }
+      _other_error => {
+        return Err(PathError::SymlinkError(error).into());
+      }
+    }
+  }
+
+  Ok(())
+}
+
+pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> {
+  let target_link_path = PathBuf::from("/usr/local/bin/espanso");
+
+  if std::fs::symlink_metadata(&target_link_path).is_err() {
+    return Err(PathError::SymlinkNotFound.into());
+  }
+
+  if let Err(error) = std::fs::remove_file(&target_link_path) {
+    match error.kind() {
+      ErrorKind::PermissionDenied => {
+        if prompt_when_necessary {
+          warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
+
+          let params = format!(
+            r##"do shell script "rm '{}'" with administrator privileges"##,
+            target_link_path.to_string_lossy(),
+          );
+
+          let mut child = std::process::Command::new("osascript")
+            .args(&["-e", &params])
+            .spawn()?;
+
+          let result = child.wait()?;
+          if !result.success() {
+            return Err(PathError::ElevationRequestFailure.into());
+          }
+        } else {
+          return Err(PathError::SymlinkError(error).into());
+        }
+      }
+      _other_error => {
+        return Err(PathError::SymlinkError(error).into());
+      }
+    }
+  }
+
+  Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum PathError {
+  #[error("/usr/local/bin directory doesn't exist")]
+  UsrLocalBinDirDoesNotExist,
+
+  #[error("symlink error: `{0}`")]
+  SymlinkError(std::io::Error),
+
+  #[error("elevation request failed")]
+  ElevationRequestFailure,
+
+  #[error("symlink does not exist, so there is nothing to remove.")]
+  SymlinkNotFound,
+}
diff --git a/src/system/mod.rs b/espanso/src/path/mod.rs
similarity index 58%
rename from src/system/mod.rs
rename to espanso/src/path/mod.rs
index 479ef86d..4278155b 100644
--- a/src/system/mod.rs
+++ b/espanso/src/path/mod.rs
@@ -1,7 +1,7 @@
 /*
  * This file is part of espanso.
  *
- * Copyright (C) 2019 Federico Terzi
+ * 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
@@ -17,35 +17,29 @@
  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+#[cfg(target_os = "macos")]
+mod macos;
+#[cfg(target_os = "macos")]
+pub use macos::add_espanso_to_path;
+#[cfg(target_os = "macos")]
+pub use macos::is_espanso_in_path;
+#[cfg(target_os = "macos")]
+pub use macos::remove_espanso_from_path;
+
 #[cfg(target_os = "windows")]
-mod windows;
+mod win;
+#[cfg(target_os = "windows")]
+pub use win::add_espanso_to_path;
+#[cfg(target_os = "windows")]
+pub use win::is_espanso_in_path;
+#[cfg(target_os = "windows")]
+pub use win::remove_espanso_from_path;
 
 #[cfg(target_os = "linux")]
 mod linux;
-
-#[cfg(target_os = "macos")]
-pub mod macos;
-
-pub trait SystemManager {
-    fn get_current_window_title(&self) -> Option<String>;
-    fn get_current_window_class(&self) -> Option<String>;
-    fn get_current_window_executable(&self) -> Option<String>;
-}
-
-// LINUX IMPLEMENTATION
 #[cfg(target_os = "linux")]
-pub fn get_manager() -> impl SystemManager {
-    linux::LinuxSystemManager::new()
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn get_manager() -> impl SystemManager {
-    windows::WindowsSystemManager::new()
-}
-
-// MAC IMPLEMENTATION
-#[cfg(target_os = "macos")]
-pub fn get_manager() -> impl SystemManager {
-    macos::MacSystemManager::new()
-}
+pub use linux::add_espanso_to_path;
+#[cfg(target_os = "linux")]
+pub use linux::is_espanso_in_path;
+#[cfg(target_os = "linux")]
+pub use linux::remove_espanso_from_path;
diff --git a/espanso/src/path/win.rs b/espanso/src/path/win.rs
new file mode 100644
index 00000000..c042e79c
--- /dev/null
+++ b/espanso/src/path/win.rs
@@ -0,0 +1,92 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use winreg::enums::*;
+use winreg::RegKey;
+
+pub fn is_espanso_in_path() -> bool {
+  read_paths().iter().any(|path| path.contains("espanso"))
+}
+
+pub fn add_espanso_to_path(_: bool) -> Result<()> {
+  let mut paths = read_paths();
+  let exe_path = std::env::current_exe().expect("unable to obtain exec path");
+  let parent_path = exe_path.parent().expect("unable to obtain parent path");
+
+  // Add espanso to path list
+  paths.push(parent_path.to_string_lossy().to_string());
+
+  let path = paths.join(";");
+  write_user_path_value(path)?;
+
+  // Send broadcast to let other applications know that the env variables have changed
+  send_change_broadcast();
+
+  Ok(())
+}
+
+pub fn remove_espanso_from_path(_: bool) -> Result<()> {
+  let paths = read_paths();
+  let paths_without_espanso: Vec<String> = paths
+    .into_iter()
+    .filter(|path| !path.contains("espanso"))
+    .collect();
+  let path = paths_without_espanso.join(";");
+  write_user_path_value(path)
+}
+
+fn read_user_path_value() -> Result<String> {
+  let hkcu = RegKey::predef(HKEY_CURRENT_USER);
+  let env = hkcu.open_subkey("Environment")?;
+  let path: String = env.get_value("Path")?;
+  Ok(path)
+}
+
+fn read_paths() -> Vec<String> {
+  let path_value = read_user_path_value().unwrap_or_default();
+  let paths = path_value.split(';');
+  paths.map(String::from).collect()
+}
+
+fn write_user_path_value(value: String) -> Result<()> {
+  let hkcu = RegKey::predef(HKEY_CURRENT_USER);
+  let env = hkcu.open_subkey_with_flags("Environment", KEY_ALL_ACCESS)?;
+  env.set_value("Path", &value)?;
+  Ok(())
+}
+
+fn send_change_broadcast() {
+  use winapi::um::winuser::{SendMessageTimeoutW, HWND_BROADCAST, WM_SETTINGCHANGE};
+
+  let wide_string = widestring::WideString::from("Environment".to_string());
+
+  unsafe {
+    let mut res: usize = 0;
+    SendMessageTimeoutW(
+      HWND_BROADCAST,
+      WM_SETTINGCHANGE,
+      0,
+      wide_string.as_ptr() as isize,
+      2,
+      50,
+      &mut res,
+    );
+  }
+}
diff --git a/espanso/src/preferences/default.rs b/espanso/src/preferences/default.rs
new file mode 100644
index 00000000..d04cccab
--- /dev/null
+++ b/espanso/src/preferences/default.rs
@@ -0,0 +1,92 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use espanso_kvs::KVS;
+use serde::{de::DeserializeOwned, Serialize};
+
+use anyhow::Result;
+
+use super::Preferences;
+
+const HAS_COMPLETED_WIZARD_KEY: &str = "has_completed_wizard";
+const HAS_DISPLAYED_WELCOME_KEY: &str = "has_displayed_welcome";
+const SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS: &str =
+  "should_display_troubleshoot_for_non_fatal_errors";
+const HAS_SELECTED_AUTO_START_OPTION: &str = "has_selected_auto_start_option";
+
+#[derive(Clone)]
+pub struct DefaultPreferences<KVSType: KVS> {
+  kvs: KVSType,
+}
+
+impl<KVSType: KVS> DefaultPreferences<KVSType> {
+  pub fn new(kvs: KVSType) -> Result<Self> {
+    Ok(Self { kvs })
+  }
+
+  fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
+    self
+      .kvs
+      .get(key)
+      .unwrap_or_else(|_| panic!("unable to read preference for key {}", key))
+  }
+
+  fn set<T: Serialize>(&self, key: &str, value: T) {
+    self
+      .kvs
+      .set(key, value)
+      .unwrap_or_else(|_| panic!("unable to write preference for key {}", key))
+  }
+}
+
+impl<KVSType: KVS> Preferences for DefaultPreferences<KVSType> {
+  fn has_completed_wizard(&self) -> bool {
+    self.get(HAS_COMPLETED_WIZARD_KEY).unwrap_or(false)
+  }
+
+  fn set_completed_wizard(&self, value: bool) {
+    self.set(HAS_COMPLETED_WIZARD_KEY, value);
+  }
+
+  fn has_displayed_welcome(&self) -> bool {
+    self.get(HAS_DISPLAYED_WELCOME_KEY).unwrap_or(false)
+  }
+
+  fn set_has_displayed_welcome(&self, value: bool) {
+    self.set(HAS_DISPLAYED_WELCOME_KEY, value);
+  }
+
+  fn should_display_troubleshoot_for_non_fatal_errors(&self) -> bool {
+    self
+      .get(SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS)
+      .unwrap_or(true)
+  }
+
+  fn set_should_display_troubleshoot_for_non_fatal_errors(&self, value: bool) {
+    self.set(SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS, value);
+  }
+
+  fn has_selected_auto_start_option(&self) -> bool {
+    self.get(HAS_SELECTED_AUTO_START_OPTION).unwrap_or(false)
+  }
+
+  fn set_has_selected_auto_start_option(&self, value: bool) {
+    self.set(HAS_SELECTED_AUTO_START_OPTION, value);
+  }
+}
diff --git a/espanso/src/preferences/mod.rs b/espanso/src/preferences/mod.rs
new file mode 100644
index 00000000..57392175
--- /dev/null
+++ b/espanso/src/preferences/mod.rs
@@ -0,0 +1,42 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use anyhow::Result;
+use std::path::Path;
+
+mod default;
+
+pub trait Preferences: Send + Sync + Clone {
+  fn has_completed_wizard(&self) -> bool;
+  fn set_completed_wizard(&self, value: bool);
+
+  fn has_displayed_welcome(&self) -> bool;
+  fn set_has_displayed_welcome(&self, value: bool);
+
+  fn should_display_troubleshoot_for_non_fatal_errors(&self) -> bool;
+  fn set_should_display_troubleshoot_for_non_fatal_errors(&self, value: bool);
+
+  fn has_selected_auto_start_option(&self) -> bool;
+  fn set_has_selected_auto_start_option(&self, value: bool);
+}
+
+pub fn get_default(runtime_dir: &Path) -> Result<impl Preferences> {
+  let kvs = espanso_kvs::get_persistent(runtime_dir)?;
+  default::DefaultPreferences::new(kvs)
+}
diff --git a/espanso/src/res/accessibility_1.png b/espanso/src/res/accessibility_1.png
new file mode 100644
index 00000000..ef280a51
Binary files /dev/null and b/espanso/src/res/accessibility_1.png differ
diff --git a/espanso/src/res/accessibility_2.png b/espanso/src/res/accessibility_2.png
new file mode 100644
index 00000000..6ee3adfc
Binary files /dev/null and b/espanso/src/res/accessibility_2.png differ
diff --git a/espanso/src/res/config/base.yml b/espanso/src/res/config/base.yml
new file mode 100644
index 00000000..43a1bb87
--- /dev/null
+++ b/espanso/src/res/config/base.yml
@@ -0,0 +1,37 @@
+# espanso match file
+
+# For a complete introduction, visit the official docs at: https://espanso.org/docs/
+
+# You can use this file to define the base matches (aka snippets)
+# that will be available in every application when using espanso.
+
+# Matches are substitution rules: when you type the "trigger" string
+# it gets replaced by the "replace" string.
+matches:
+  # Simple text replacement
+  - trigger: ":espanso"
+    replace: "Hi there!"
+
+  # NOTE: espanso uses YAML to define matches, so pay attention to the indentation!
+
+  # But matches can also be dynamic:
+
+  # Print the current date
+  - trigger: ":date"
+    replace: "{{mydate}}"
+    vars:
+      - name: mydate
+        type: date
+        params:
+          format: "%m/%d/%Y"
+
+  # Print the output of a shell command
+  - trigger: ":shell"
+    replace: "{{output}}"
+    vars:
+      - name: output
+        type: shell
+        params:
+          cmd: "echo 'Hello from your shell'"
+
+  # And much more! For more information, visit the docs: https://espanso.org/docs/
\ No newline at end of file
diff --git a/espanso/src/res/config/default.yml b/espanso/src/res/config/default.yml
new file mode 100644
index 00000000..4f52b020
--- /dev/null
+++ b/espanso/src/res/config/default.yml
@@ -0,0 +1,40 @@
+# espanso configuration file
+
+# For a complete introduction, visit the official docs at: https://espanso.org/docs/
+
+# You can use this file to define the global configuration options for espanso.
+# These are the parameters that will be used by default on every application,
+# but you can also override them on a per-application basis.
+
+# To make customization easier, this file contains some of the commonly used
+# parameters. Feel free to uncomment and tune them to fit your needs!
+
+# --- Toggle key
+
+# Customize the key used to disable and enable espanso (when double tapped)
+# Available options: CTRL, SHIFT, ALT, CMD, OFF
+# You can also specify the key variant, such as LEFT_CTRL, RIGHT_SHIFT, etc...
+# toggle_key: ALT
+# You can also disable the toggle key completely with
+# toggle_key: OFF
+
+# --- Injection Backend
+
+# Espanso supports multiple ways of injecting text into applications. Each of
+# them has its quirks, therefore you may want to change it if you are having problems.
+# By default, espanso uses the "Auto" backend which should work well in most cases,
+# but you may want to try the "Clipboard" or "Inject" backend in case of issues.
+# backend: Clipboard
+
+# --- Auto-restart
+
+# Enable/disable the config auto-reload after a file change is detected.
+# auto_restart: false
+
+# --- Clipboard threshold
+
+# Because injecting long texts char-by-char is a slow operation, espanso automatically
+# uses the clipboard if the text is longer than 'clipboard_threshold' characters.
+# clipboard_threshold: 100
+
+# For a list of all the available options, visit the official docs at: https://espanso.org/docs/
\ No newline at end of file
diff --git a/espanso/src/res/icon.png b/espanso/src/res/icon.png
new file mode 100644
index 00000000..380e2f39
Binary files /dev/null and b/espanso/src/res/icon.png differ
diff --git a/espanso/src/res/linux/espanso.desktop b/espanso/src/res/linux/espanso.desktop
new file mode 100644
index 00000000..bd30bb99
--- /dev/null
+++ b/espanso/src/res/linux/espanso.desktop
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Name=Espanso
+Exec=espanso launcher
+Icon=icon
+Type=Application
+Categories=Utility;
\ No newline at end of file
diff --git a/espanso/src/res/linux/icon.png b/espanso/src/res/linux/icon.png
new file mode 100644
index 00000000..380e2f39
Binary files /dev/null and b/espanso/src/res/linux/icon.png differ
diff --git a/src/res/linux/systemd.service b/espanso/src/res/linux/systemd.service
similarity index 57%
rename from src/res/linux/systemd.service
rename to espanso/src/res/linux/systemd.service
index 4fdd26e0..79e90787 100644
--- a/src/res/linux/systemd.service
+++ b/espanso/src/res/linux/systemd.service
@@ -1,8 +1,8 @@
 [Unit]
-Description=espanso daemon
+Description=espanso
 
 [Service]
-ExecStart={{{espanso_path}}} daemon
+ExecStart={{{espanso_path}}} launcher 
 Restart=on-failure
 RestartSec=3
 
diff --git a/espanso/src/res/logo_no_background.png b/espanso/src/res/logo_no_background.png
new file mode 100644
index 00000000..e6e9e568
Binary files /dev/null and b/espanso/src/res/logo_no_background.png differ
diff --git a/espanso/src/res/macos/Info.plist b/espanso/src/res/macos/Info.plist
new file mode 100644
index 00000000..340a1645
--- /dev/null
+++ b/espanso/src/res/macos/Info.plist
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<plist version="0.9">
+<dict>
+	<key>LSEnvironment</key>
+	<dict>
+    <key>MAC_LAUNCH_CONTEXT</key>
+    <string>bundle</string>
+	</dict>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.federicoterzi.espanso</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>espanso</string>
+	<key>CFBundleIconFile</key>
+	<string>icon.icns</string>
+	<key>CFBundleName</key>
+	<string>Espanso</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>VERSION</string>
+	<key>CFBundleShortVersionString</key>
+	<string>VERSION</string>
+	<key>CFBundleGetInfoString</key>
+	<string>Espanso version VERSION (C) 2019-2021 Federico Terzi</string>
+	<key>CFBundleLongVersionString</key>
+	<string>VERSION (C) 2019-2021 Federico Terzi</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>Copyright 2019-2021 Federico Terzi</string>
+	<key>LSRequiresCarbon</key>
+	<true/>
+	<key>LSUIElement</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/src/res/mac/com.federicoterzi.espanso.plist b/espanso/src/res/macos/com.federicoterzi.espanso.plist
similarity index 95%
rename from src/res/mac/com.federicoterzi.espanso.plist
rename to espanso/src/res/macos/com.federicoterzi.espanso.plist
index e5373679..cacf47b1 100644
--- a/src/res/mac/com.federicoterzi.espanso.plist
+++ b/espanso/src/res/macos/com.federicoterzi.espanso.plist
@@ -12,7 +12,7 @@
     <key>ProgramArguments</key>
     <array>
         <string>{{{espanso_path}}}</string>
-        <string>daemon</string>
+        <string>launcher</string>
     </array>
     <key>RunAtLoad</key>
     <true/>
diff --git a/espanso/src/res/macos/icon.icns b/espanso/src/res/macos/icon.icns
new file mode 100644
index 00000000..b68e5485
Binary files /dev/null and b/espanso/src/res/macos/icon.icns differ
diff --git a/espanso/src/res/macos/icon.png b/espanso/src/res/macos/icon.png
new file mode 100644
index 00000000..9452d31d
Binary files /dev/null and b/espanso/src/res/macos/icon.png differ
diff --git a/espanso/src/res/macos/icon_explain_image.png b/espanso/src/res/macos/icon_explain_image.png
new file mode 100644
index 00000000..6cfb9994
Binary files /dev/null and b/espanso/src/res/macos/icon_explain_image.png differ
diff --git a/espanso/src/res/macos/icondisabled.png b/espanso/src/res/macos/icondisabled.png
new file mode 100644
index 00000000..f6c4546e
Binary files /dev/null and b/espanso/src/res/macos/icondisabled.png differ
diff --git a/espanso/src/res/macos/iconsystemdisabled.png b/espanso/src/res/macos/iconsystemdisabled.png
new file mode 100644
index 00000000..4210fe0e
Binary files /dev/null and b/espanso/src/res/macos/iconsystemdisabled.png differ
diff --git a/espanso/src/res/macos/scripts/blur_chrome_windows.scpt b/espanso/src/res/macos/scripts/blur_chrome_windows.scpt
new file mode 100644
index 00000000..f45cb549
--- /dev/null
+++ b/espanso/src/res/macos/scripts/blur_chrome_windows.scpt
@@ -0,0 +1,15 @@
+-- Activate each Window of Google Chrome and press CMD+L to focus the address bar
+-- This makes it possible to "blur" any focused password field that might be keeping
+-- SecureInput enabled
+tell application "Google Chrome"
+	activate
+	-- For each window
+	repeat with win in windows
+		-- Bring to front
+		set index of item 1 of win to 1
+		delay 0.5
+		-- And press CMD+L
+		tell application "System Events" to keystroke "l" using command down
+		delay 0.5
+	end repeat
+end tell
\ No newline at end of file
diff --git a/espanso/src/res/macos/scripts/focus_bitwarden.scpt b/espanso/src/res/macos/scripts/focus_bitwarden.scpt
new file mode 100644
index 00000000..37723cde
--- /dev/null
+++ b/espanso/src/res/macos/scripts/focus_bitwarden.scpt
@@ -0,0 +1,4 @@
+tell application "Bitwarden" to activate
+delay 1 
+tell application "Finder" to activate
+delay 1
\ No newline at end of file
diff --git a/espanso/src/res/macos/scripts/get_running_apps.scpt b/espanso/src/res/macos/scripts/get_running_apps.scpt
new file mode 100644
index 00000000..8a1185fc
--- /dev/null
+++ b/espanso/src/res/macos/scripts/get_running_apps.scpt
@@ -0,0 +1,5 @@
+tell application "System Events"
+	set listOfProcesses to (bundle identifier of every process where background only is false)
+end tell
+
+return listOfProcesses
\ No newline at end of file
diff --git a/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt b/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt
new file mode 100644
index 00000000..b33681f7
--- /dev/null
+++ b/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt
@@ -0,0 +1,8 @@
+display alert "Espanso wasn't able to automatically disable secure input. Sometimes locking and unlocking the screen helps, do you want to try?" buttons {"No", "Yes"} default button "Yes"
+if button returned of result = "No" then
+  return "no"
+else
+  if button returned of result = "Yes" then
+    return "yes"
+  end if
+end if
\ No newline at end of file
diff --git a/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt b/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt
new file mode 100644
index 00000000..adc40c6e
--- /dev/null
+++ b/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt
@@ -0,0 +1,1 @@
+display alert "Secure input successfully disabled!" buttons {"Great!"}
\ No newline at end of file
diff --git a/espanso/src/res/windows/disabled_dark.ico b/espanso/src/res/windows/disabled_dark.ico
new file mode 100644
index 00000000..78a845cf
Binary files /dev/null and b/espanso/src/res/windows/disabled_dark.ico differ
diff --git a/espanso/src/res/windows/disabled_light.ico b/espanso/src/res/windows/disabled_light.ico
new file mode 100644
index 00000000..75ca9e04
Binary files /dev/null and b/espanso/src/res/windows/disabled_light.ico differ
diff --git a/espanso/src/res/windows/logo.ico b/espanso/src/res/windows/logo.ico
new file mode 100644
index 00000000..42580669
Binary files /dev/null and b/espanso/src/res/windows/logo.ico differ
diff --git a/espanso/src/res/windows/normal_dark.ico b/espanso/src/res/windows/normal_dark.ico
new file mode 100644
index 00000000..53ef64bd
Binary files /dev/null and b/espanso/src/res/windows/normal_dark.ico differ
diff --git a/espanso/src/res/windows/normal_light.ico b/espanso/src/res/windows/normal_light.ico
new file mode 100644
index 00000000..b23bcc0d
Binary files /dev/null and b/espanso/src/res/windows/normal_light.ico differ
diff --git a/espanso/src/res/windows/tray_explain_image.png b/espanso/src/res/windows/tray_explain_image.png
new file mode 100644
index 00000000..1750b999
Binary files /dev/null and b/espanso/src/res/windows/tray_explain_image.png differ
diff --git a/espanso/src/util.rs b/espanso/src/util.rs
new file mode 100644
index 00000000..1e4a22f2
--- /dev/null
+++ b/espanso/src/util.rs
@@ -0,0 +1,45 @@
+/*
+ * 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::process::Command;
+
+#[cfg(target_os = "windows")]
+pub fn set_command_flags(command: &mut Command) {
+  use std::os::windows::process::CommandExt;
+  // Avoid showing the shell window
+  // See: https://github.com/federico-terzi/espanso/issues/249
+  command.creation_flags(0x08000000);
+}
+
+#[cfg(not(target_os = "windows"))]
+pub fn set_command_flags(_: &mut Command) {
+  // NOOP on Linux and macOS
+}
+
+#[cfg(target_os = "windows")]
+pub fn attach_console() {
+  // When using the windows subsystem we loose the terminal output.
+  // Therefore we try to attach to the current console if available.
+  unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) };
+}
+
+#[cfg(not(target_os = "windows"))]
+pub fn attach_console() {
+  // Not necessary on Linux and macOS
+}
diff --git a/images/logo_extended.png b/images/logo_extended.png
new file mode 100644
index 00000000..03849238
Binary files /dev/null and b/images/logo_extended.png differ
diff --git a/images/titlebar.png b/images/titlebar.png
deleted file mode 100644
index 57699893..00000000
Binary files a/images/titlebar.png and /dev/null differ
diff --git a/native/liblinuxbridge/CMakeLists.txt b/native/liblinuxbridge/CMakeLists.txt
deleted file mode 100644
index 243fda2c..00000000
--- a/native/liblinuxbridge/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-cmake_minimum_required(VERSION 3.0)
-project(liblinuxbridge)
-
-set (CMAKE_CXX_STANDARD 14)
-set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include")
-
-add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h)
-
-install(TARGETS linuxbridge DESTINATION .)
\ No newline at end of file
diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp
deleted file mode 100644
index ec31d09d..00000000
--- a/native/liblinuxbridge/bridge.cpp
+++ /dev/null
@@ -1,625 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#include "bridge.h"
-#include "fast_xdo.h"
-
-#include <locale.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <array>
-#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>
-extern "C" {  // Needed to avoid C++ compiler name mangling
-    #include <xdo.h>
-}
-
-/*
-This code uses the X11 Record Extension to receive keyboard
-events. Documentation of this library can be found here:
-https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html
-
-We will refer to this extension as RE from now on.
-*/
-
-/*
-This struct is needed to receive events from the RE.
-The funny thing is: it's not defined there, it should though.
-The only place this is mentioned is the libxnee library,
-so check that out if you need a reference.
-*/
-typedef union {
-    unsigned char    type ;
-    xEvent           event ;
-    xResourceReq     req   ;
-    xGenericReply    reply ;
-    xError           error ;
-    xConnSetupPrefix setup;
-} XRecordDatum;
-
-/*
-Connections to the X server, RE recommends 2 connections:
-one for recording control and one for reading the recorded data.
-*/
-Display *data_disp = NULL;
-Display *ctrl_disp = NULL;
-
-XRecordRange  *record_range;
-XRecordContext context;
-
-xdo_t * xdo_context;
-
-// Callback invoked when a new key event occur.
-void event_callback (XPointer, XRecordInterceptData*);
-int error_callback(Display *display, XErrorEvent *error);
-
-KeypressCallback keypress_callback;
-X11ErrorCallback x11_error_callback;
-void * context_instance;
-
-void register_keypress_callback(KeypressCallback callback) {
-    keypress_callback = callback;
-}
-
-void register_error_callback(X11ErrorCallback callback) {
-    x11_error_callback = callback;
-}
-
-int32_t check_x11() {
-    Display *check_disp = XOpenDisplay(NULL);
-
-    if (!check_disp) {
-        return -1;
-    }
-
-    XCloseDisplay(check_disp);
-    return 1;
-}
-
-int32_t initialize(void * _context_instance) {
-    setlocale(LC_ALL, "");
-
-    context_instance = _context_instance;
-
-    /*
-    Open the connections to the X server.
-    RE recommends to open 2 connections to the X server:
-    one for the recording control and one to read the protocol
-    data.
-    */
-    ctrl_disp = XOpenDisplay(NULL);
-    data_disp = XOpenDisplay(NULL);
-
-    if (!ctrl_disp || !data_disp) {  // Display error
-        return -1;
-    }
-
-    /*
-    We must set the ctrl_disp to sync mode, or, when we the enable
-    context in data_disp, there will be a fatal X error.
-    */
-    XSynchronize(ctrl_disp, True);
-
-    int dummy;
-
-    // Make sure the X RE is installed in this system.
-    if (!XRecordQueryVersion(ctrl_disp, &dummy, &dummy)) {
-        return -2;
-    }
-
-    // Make sure the X Keyboard Extension is installed
-    if (!XkbQueryExtension(ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy)) {
-        return -3;
-    }
-
-    // Initialize the record range, that is the kind of events we want to track.
-    record_range = XRecordAllocRange ();
-    if (!record_range) {
-        return -4;
-    }
-    record_range->device_events.first = KeyPress;
-    record_range->device_events.last = ButtonPress;
-
-    // We want to get the keys from all clients
-    XRecordClientSpec  client_spec;
-    client_spec = XRecordAllClients;
-
-    // Initialize the context
-    context = XRecordCreateContext(ctrl_disp, 0, &client_spec, 1, &record_range, 1);
-    if (!context) {
-        return -5;
-    }
-
-    if (!XRecordEnableContextAsync(data_disp, context, event_callback, NULL)) {
-        return -6;
-    }
-
-    xdo_context = xdo_new(NULL);
-
-    // Setup a custom error handler
-    XSetErrorHandler(&error_callback);
-
-    /**
-     * Note: We might never get a MappingNotify event if the
-     * modifier and keymap information was never cached in Xlib.
-     * The next line makes sure that this happens initially.
-     */
-    XKeysymToKeycode(ctrl_disp, XK_F1);
-
-    return 1;
-}
-
-int32_t eventloop() {
-    bool running = true;
-
-    int ctrl_fd = XConnectionNumber(ctrl_disp);
-    int data_fd = XConnectionNumber(data_disp);
-
-    while (running)
-    {
-        fd_set fds;
-        FD_ZERO(&fds);
-        FD_SET(ctrl_fd, &fds);
-        FD_SET(data_fd, &fds);
-        timeval timeout;
-        timeout.tv_sec = 2;
-        timeout.tv_usec = 0;
-        int retval = select(max(ctrl_fd, data_fd) + 1,
-                            &fds, NULL, NULL, &timeout);
-
-        if (FD_ISSET(data_fd, &fds)) {
-            XRecordProcessReplies(data_disp);
-        }
-        if (FD_ISSET(ctrl_fd, &fds)) {
-            XEvent event;
-            XNextEvent(ctrl_disp, &event);
-            if (event.type == MappingNotify) {
-                XMappingEvent *e = (XMappingEvent *) &event;
-                if (e->request == MappingKeyboard) {
-                    XRefreshKeyboardMapping(e);
-                }
-            }
-        }
-    }
-
-    return 1;
-}
-
-void cleanup() {
-    XRecordDisableContext(ctrl_disp, context);
-    XRecordFreeContext(ctrl_disp, context);
-    XFree (record_range);
-    XCloseDisplay(data_disp);
-    XCloseDisplay(ctrl_disp);
-    xdo_free(xdo_context);
-}
-
-void event_callback(XPointer p, XRecordInterceptData *hook)
-{
-    // Make sure the event comes from the X11 server
-    if (hook->category != XRecordFromServer) {
-        XRecordFreeData(hook);
-        return;
-    }
-
-    // Cast the event payload to a XRecordDatum, needed later to access the fields
-    // This struct was hard to find and understand. Turn's out that all the
-    // required data are included in the "event" field of this structure.
-    // The funny thing is that it's not a XEvent as one might expect,
-    // but a xEvent, a very different beast defined in the Xproto.h header.
-    // I suggest you to look at that header if you want to understand where the
-    // upcoming field where taken from.
-    XRecordDatum *data = (XRecordDatum*) hook->data;
-
-    int event_type = data->type;
-    int key_code = data->event.u.u.detail;
-
-    // In order to convert the key_code into the corresponding string,
-    // we need to synthesize an artificial XKeyEvent, to feed later to the
-    // XLookupString function.
-    XKeyEvent event;
-    event.display     = ctrl_disp;
-    event.window      = data->event.u.focus.window;
-    event.root        = XDefaultRootWindow(ctrl_disp);
-    event.subwindow   = None;
-    event.time        = data->event.u.keyButtonPointer.time;
-    event.x           = 1;
-    event.y           = 1;
-    event.x_root      = 1;
-    event.y_root      = 1;
-    event.same_screen = True;
-    event.keycode     = key_code;
-    event.state       = data->event.u.keyButtonPointer.state;
-    event.type = KeyPress;
-
-    // Extract the corresponding chars.
-    std::array<char, 10> buffer;
-    int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL);
-
-    switch (event_type) {
-        case KeyPress:
-            //printf ("Press %d %d %s\n", key_code, res, buffer.data());
-            if (res > 0 && key_code != 22) {  // Printable character, but not backspace
-                keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code);
-            }else{ // Modifier key
-                keypress_callback(context_instance, NULL, 0, 1, key_code);
-            }
-            break;
-        case ButtonPress: // Send also mouse button presses as "other events"
-            //printf ("Press button %d\n", key_code);
-            keypress_callback(context_instance, NULL, 0, 2, key_code);
-        default:
-            break;
-    }
-
-    XRecordFreeData(hook);
-}
-
-int error_callback(Display *display, XErrorEvent *error) {
-    x11_error_callback(context_instance, error->error_code, error->request_code, error->minor_code);
-    return 0;
-}
-
-void release_all_keys() {
-    char keys[32];
-    XQueryKeymap(xdo_context->xdpy, keys);  // Get the current status of the keyboard
-    for (int i = 0; i<32; i++) {
-        // Only those that show a keypress should be changed
-        if (keys[i] != 0) {
-            for (int k = 0; k<8; k++) {
-                if ((keys[i] & (1 << k)) != 0) {  // Bit by bit check
-                    int key_code = i*8 + k;
-                    XTestFakeKeyEvent(xdo_context->xdpy, key_code, false, CurrentTime);
-                }
-            }
-        }
-    }
-}
-
-void send_string(const char * string) {
-    // 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.
-    release_all_keys();
-
-    xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000);
-}
-
-void send_enter() {
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000);
-}
-
-void fast_release_all_keys() {
-    Window focused;
-    int revert_to;
-    XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
-
-    char keys[32];
-    XQueryKeymap(xdo_context->xdpy, keys);  // Get the current status of the keyboard
-    for (int i = 0; i<32; i++) {
-        // Only those that show a keypress should be changed
-        if (keys[i] != 0) {
-            for (int k = 0; k<8; k++) {
-                if ((keys[i] & (1 << k)) != 0) {  // Bit by bit check
-                    int key_code = i*8 + k;
-                    fast_send_event(xdo_context, focused, key_code, 0);
-                }
-            }
-        }
-    }
-
-    XFlush(xdo_context->xdpy);
-}
-
-void fast_send_string(const char * string, int32_t delay) {
-    // 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.
-    fast_release_all_keys();
-
-    Window focused;
-    int revert_to;
-    XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
-
-    int actual_delay = 1;
-    if (delay > 0) {
-        actual_delay = delay * 1000;
-    }
-
-    fast_enter_text_window(xdo_context, focused, string, actual_delay);
-}
-
-void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count, int32_t delay) {
-    int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode);
-
-    Window focused;
-    int revert_to;
-    XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
-
-    for (int i = 0; i<count; i++) {
-        fast_send_event(xdo_context, focused, keycode, 1);
-        fast_send_event(xdo_context, focused, keycode, 0);
-
-        if (delay > 0) {
-            usleep(delay * 1000);
-            XFlush(xdo_context->xdpy);
-        }
-    }
-
-    XFlush(xdo_context->xdpy);
-}
-
-void fast_send_enter() {
-    _fast_send_keycode_to_focused_window(XK_Return, 1, 0);
-}
-
-void delete_string(int32_t count) {
-    for (int i = 0; i<count; i++) {
-        xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 1000);
-    }
-}
-
-void fast_delete_string(int32_t count, int32_t delay) {
-    _fast_send_keycode_to_focused_window(XK_BackSpace, count, delay);
-}
-
-void left_arrow(int32_t count) {
-    for (int i = 0; i<count; i++) {
-        xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 1000);
-    }
-}
-
-void fast_left_arrow(int32_t count) {
-    _fast_send_keycode_to_focused_window(XK_Left, count, 0);
-}
-
-void trigger_paste() {
-    // Before sending the paste shortcut, trigger the press and release of the Shift key
-    // this is needed because for some triggers, for example ending with ":", the user
-    // will still have the Shift key pressed when espanso execute the pasting shortcut,
-    // therefore sending CTRL+Shift+V instead of CTRL+V.
-    // With this call, we force the shift key to be unpressed when pasting.
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000);
-
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+v", 8000);
-}
-
-void trigger_terminal_paste() {
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000);
-}
-
-void trigger_shift_ins_paste() {
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Insert", 8000);
-}
-
-void trigger_alt_shift_ins_paste() {
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
-}
-
-void trigger_ctrl_alt_paste() {
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Alt+v", 8000);
-}
-
-void trigger_copy() {
-    // Release the other keys, for an explanation, read the 'trigger_paste' method
-
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Alt", 8000);
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000);
-    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+c", 8000);
-}
-
-// SYSTEM MODULE
-
-// Function taken from the wmlib tool source code
-char *get_property(Display *disp, Window win,
-                          Atom xa_prop_type, char *prop_name, unsigned long *size)
-{
-    unsigned long ret_nitems, ret_bytes_after, tmp_size;
-    Atom xa_prop_name, xa_ret_type;
-    unsigned char *ret_prop;
-    int ret_format;
-    char *ret;
-    int size_in_byte;
-
-    xa_prop_name = XInternAtom(disp, prop_name, False);
-
-    if (XGetWindowProperty(disp, win, xa_prop_name, 0, 4096 / 4, False,
-                           xa_prop_type, &xa_ret_type, &ret_format, &ret_nitems,
-                           &ret_bytes_after, &ret_prop) != Success)
-        return NULL;
-
-    if (xa_ret_type != xa_prop_type)
-    {
-        XFree(ret_prop);
-        return NULL;
-    }
-
-    switch(ret_format) {
-        case 8:	size_in_byte = sizeof(char); break;
-        case 16:	size_in_byte = sizeof(short); break;
-        case 32:	size_in_byte = sizeof(long); break;
-    }
-
-    tmp_size = size_in_byte * ret_nitems;
-    ret = (char*) malloc(tmp_size + 1);
-    memcpy(ret, ret_prop, tmp_size);
-    ret[tmp_size] = '\0';
-
-    if (size) *size = tmp_size;
-
-    XFree(ret_prop);
-    return ret;
-}
-
-// Function taken from Window Management Library for Ruby
-char *xwm_get_win_title(Display *disp, Window win)
-{
-    char *wname = (char*)get_property(disp,win, XA_STRING, "WM_NAME", NULL);
-    char *nwname = (char*)get_property(disp,win, XInternAtom(disp,
-                                                             "UTF8_STRING", False), "_NET_WM_NAME", NULL);
-
-    return nwname ? nwname : (wname ? wname : NULL);
-}
-
-int32_t get_active_window_name(char * buffer, int32_t size) {
-    xdo_t * x = xdo_new(NULL);
-
-    if (!x) {
-        return -1;
-    }
-
-    // Get the active window
-    Window win;
-    int ret = xdo_get_active_window(x, &win);
-    int result = 1;
-    if (ret) {
-        fprintf(stderr, "xdo_get_active_window reported an error\n");
-        result = -2;
-    }else{
-        char * title = xwm_get_win_title(x->xdpy, win);
-
-        snprintf(buffer, size, "%s", title);
-
-        XFree(title);
-    }
-
-    xdo_free(x);
-
-    return result;
-}
-
-int32_t get_active_window_class(char * buffer, int32_t size) {
-    xdo_t * x = xdo_new(NULL);
-
-    if (!x) {
-        return -1;
-    }
-
-    // Get the active window
-    Window win;
-    int ret = xdo_get_active_window(x, &win);
-    int result = 1;
-    if (ret) {
-        fprintf(stderr, "xdo_get_active_window reported an error\n");
-        result = -2;
-    }else{
-        XClassHint hint;
-
-        if (XGetClassHint(x->xdpy, win, &hint)) {
-            snprintf(buffer, size, "%s", hint.res_class);
-            XFree(hint.res_name);
-            XFree(hint.res_class);
-        }
-    }
-
-    xdo_free(x);
-
-    return result;
-}
-
-int32_t get_active_window_executable(char *buffer, int32_t size) {
-    xdo_t * x = xdo_new(NULL);
-
-    if (!x) {
-        return -1;
-    }
-
-    // Get the active window
-    Window win;
-    int ret = xdo_get_active_window(x, &win);
-    int result = 1;
-    if (ret) {
-        fprintf(stderr, "xdo_get_active_window reported an error\n");
-        result = -2;
-    }else{
-        // Get the window process PID
-        char *pid_raw = (char*)get_property(x->xdpy,win, XA_CARDINAL, "_NET_WM_PID", NULL);
-        if (pid_raw == NULL) {
-            result = -3;
-        }else{
-            int pid = pid_raw[0] | pid_raw[1] << 8 | pid_raw[2] << 16 | pid_raw[3] << 24;
-
-            // Get the executable path from it
-            char proc_path[250];
-            snprintf(proc_path, 250, "/proc/%d/exe", pid);
-
-            readlink(proc_path, buffer, size);
-
-            XFree(pid_raw);
-        }
-    }
-
-    xdo_free(x);
-
-    return result;
-}
-
-int32_t is_current_window_special() {
-    char class_buffer[250];
-    int res = get_active_window_class(class_buffer, 250);
-    if (res > 0) {
-        if (strstr(class_buffer, "terminal") != NULL) {
-            return 1;
-        }else if (strstr(class_buffer, "URxvt") != NULL) {  // urxvt terminal
-            return 4;
-        }else if (strstr(class_buffer, "XTerm") != NULL) {  // XTerm and UXTerm
-            return 1;
-        }else if (strstr(class_buffer, "Termite") != NULL) {  // Termite
-            return 1;
-        }else if (strstr(class_buffer, "konsole") != NULL) {  // KDE Konsole
-            return 1;
-        }else if (strstr(class_buffer, "Terminator") != NULL) {  // Terminator
-            return 1;
-        }else if (strstr(class_buffer, "stterm") != NULL) {  // Simple terminal 3
-            return 2;
-        }else if (strstr(class_buffer, "St") != NULL) {  // Simple terminal
-            return 1;
-        }else if (strstr(class_buffer, "st") != NULL) {  // Simple terminal 2
-            return 1;
-        }else if (strstr(class_buffer, "Alacritty") != NULL) {  // Alacritty terminal
-            return 1;
-        }else if (strstr(class_buffer, "Emacs") != NULL) {  // Emacs
-            return 3;
-        }else if (strstr(class_buffer, "yakuake") != NULL) {  // Yakuake terminal
-            return 1;
-        }else if (strstr(class_buffer, "Tilix") != NULL) {  // Tilix terminal
-            return 1;
-        }else if (strstr(class_buffer, "kitty") != NULL) {  // kitty terminal
-            return 1;
-        }
-    }
-
-    return 0;
-}
-
-
diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h
deleted file mode 100644
index 5102d0c1..00000000
--- a/native/liblinuxbridge/bridge.h
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#ifndef ESPANSO_BRIDGE_H
-#define ESPANSO_BRIDGE_H
-
-#include <stdint.h>
-
-extern void * context_instance;
-
-/*
- * Check if the X11 context is available
- */
-extern "C" int32_t check_x11();
-
-/*
- * Initialize the X11 context and parameters
- */
-extern "C" int32_t initialize(void * context_instance);
-
-/*
- * Start the event loop indefinitely. Blocking call.
- */
-extern "C" int32_t eventloop();
-
-/*
- * Clean all the X11 resources allocated during the initialization.
- */
-extern "C" void cleanup();
-
-/*
- * Called when a new keypress is made, the first argument is an char array,
- * while the second is the size of the array.
- */
-typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code);
-extern KeypressCallback keypress_callback;
-
-/*
- * Register the callback that will be called when a keypress was made
- */
-extern "C" void register_keypress_callback(KeypressCallback callback);
-
-/*
- * Called when a X11 error occurs
- */
-typedef void (*X11ErrorCallback)(void * self, char error_code, char request_code, char minor_code);
-extern X11ErrorCallback x11_error_callback;
-
-/*
- * Register the callback that will be called when an X11 error occurs
- */
-extern "C" void register_error_callback(X11ErrorCallback callback);
-
-/*
- * Type the given string by simulating Key Presses
- */
-extern "C" void send_string(const char * string);
-
-/*
- * Type the given string by simulating Key Presses using a faster inject method
- */
-extern "C" void fast_send_string(const char * string, int32_t delay);
-
-/*
- * Send the backspace keypress, *count* times.
- */
-extern "C" void delete_string(int32_t count);
-
-/*
- * Send the backspace keypress, *count* times using a faster inject method
- */
-extern "C" void fast_delete_string(int32_t count, int32_t delay);
-
-/*
- * Send an Enter key press
- */
-extern "C" void send_enter();
-
-/*
- * Send an Enter key press using a faster inject method
- */
-extern "C" void fast_send_enter();
-
-/*
- * Send the left arrow keypress, *count* times.
- */
-extern "C" void left_arrow(int32_t count);
-
-/*
- * Send the left arrow keypress, *count* times using a faster inject method
- */
-extern "C" void fast_left_arrow(int32_t count);
-
-/*
- * Trigger normal paste ( Pressing CTRL+V )
- */
-extern "C" void trigger_paste();
-
-/*
- * Trigger terminal paste ( Pressing CTRL+SHIFT+V )
- */
-extern "C" void trigger_terminal_paste();
-
-/*
- * Trigger shift ins pasting( Pressing SHIFT+INS )
- */
-extern "C" void trigger_shift_ins_paste();
-
-/*
- * Trigger alt shift ins pasting( Pressing ALT+SHIFT+INS )
- */
-extern "C" void trigger_alt_shift_ins_paste();
-
-/*
- * Trigger CTRL+ALT+V pasting
- */
-extern "C" void trigger_ctrl_alt_paste();
-
-/*
- * Trigger copy shortcut ( Pressing CTRL+C )
- */
-extern "C" void trigger_copy();
-
-// SYSTEM MODULE
-
-/*
- * Return the active windows's WM_NAME
- */
-extern "C" int32_t get_active_window_name(char * buffer, int32_t size);
-
-/*
- * Return the active windows's WM_CLASS
- */
-extern "C" int32_t get_active_window_class(char * buffer, int32_t size);
-
-/*
- * Return the active windows's executable path
- */
-extern "C" int32_t get_active_window_executable(char * buffer, int32_t size);
-
-/*
- * Return a value greater than 0 if the current window needs a special paste combination, 0 otherwise.
- */
-extern "C" int32_t is_current_window_special();
-
-#endif //ESPANSO_BRIDGE_H
diff --git a/native/liblinuxbridge/fast_xdo.cpp b/native/liblinuxbridge/fast_xdo.cpp
deleted file mode 100644
index 43089414..00000000
--- a/native/liblinuxbridge/fast_xdo.cpp
+++ /dev/null
@@ -1,245 +0,0 @@
-//
-// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
-// and modified to use XSendEvent instead of XTestFakeKeyEvent.
-
-#include <locale.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "fast_xdo.h"
-
-extern "C" {  // Needed to avoid C++ compiler name mangling
-#include <xdo.h>
-}
-
-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);
-}
diff --git a/native/liblinuxbridge/fast_xdo.h b/native/liblinuxbridge/fast_xdo.h
deleted file mode 100644
index a7de8a4f..00000000
--- a/native/liblinuxbridge/fast_xdo.h
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
-// and modified to use XSendEvent instead of XTestFakeKeyEvent.
-
-#ifndef LIBLINUXBRIDGE_FAST_XDO_H
-#define LIBLINUXBRIDGE_FAST_XDO_H
-
-extern "C" {  // Needed to avoid C++ compiler name mangling
-#include <xdo.h>
-}
-
-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);
-
-#endif //LIBLINUXBRIDGE_FAST_XDO_H
diff --git a/native/libmacbridge/AppDelegate.mm b/native/libmacbridge/AppDelegate.mm
deleted file mode 100644
index 340f04a4..00000000
--- a/native/libmacbridge/AppDelegate.mm
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#import "AppDelegate.h"
-
-@implementation AppDelegate
-
-- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
-{
-    // Setup status icon
-    if (show_icon) {
-        myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
-        [self setIcon: icon_path];
-    }
-
-    // Setup key listener
-    [NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown)
-            handler:^(NSEvent *event){
-
-        if (event.type == NSEventTypeKeyDown
-            && event.keyCode != 0x33) { // Send backspace as a modifier
-
-            const char *chars = [event.characters UTF8String];
-            int len = event.characters.length;
-
-            keypress_callback(context_instance, chars, len, 0, event.keyCode);
-            //NSLog(@"keydown: %@, %d", event.characters, event.keyCode);
-        }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) {
-            // Send the mouse button clicks as "other" events, used to improve word matches reliability
-            keypress_callback(context_instance, NULL, 0, 2, event.buttonNumber);
-        }else{
-            // Because this event is triggered for both the press and release of a modifier, trigger the callback
-            // only on release
-            if (([event modifierFlags] & (NSEventModifierFlagShift | NSEventModifierFlagCommand |
-                NSEventModifierFlagControl | NSEventModifierFlagOption)) == 0) {
-
-                keypress_callback(context_instance, NULL, 0, 1, event.keyCode);
-            }
-
-            //NSLog(@"keydown: %d", event.keyCode);
-        }
-    }];
-}
-
-- (void) updateIcon: (char *)iconPath {
-    if (show_icon) {
-        [self setIcon: iconPath];
-    }
-}
-
-- (void) setIcon: (char *)iconPath {
-    if (show_icon) {
-        NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
-        NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
-        [statusImage setTemplate:YES];
-
-        [myStatusItem.button setImage:statusImage];
-        [myStatusItem setHighlightMode:YES];
-        [myStatusItem.button setAction:@selector(statusIconClick:)];
-        [myStatusItem.button setTarget:self];
-    }
-}
-
-- (IBAction) statusIconClick: (id) sender {
-    icon_click_callback(context_instance);
-}
-
-- (IBAction) contextMenuClick: (id) sender {
-    NSInteger item_id = [[sender valueForKey:@"tag"] integerValue];
-
-    context_menu_click_callback(context_instance, static_cast<int32_t>(item_id));
-}
-
-@end
\ No newline at end of file
diff --git a/native/libmacbridge/CMakeLists.txt b/native/libmacbridge/CMakeLists.txt
deleted file mode 100644
index 78689a7a..00000000
--- a/native/libmacbridge/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-cmake_minimum_required(VERSION 3.0)
-project(libmacbridge)
-
-set (CMAKE_CXX_STANDARD 11)
-set(CMAKE_C_FLAGS "-x objective-c")
-
-add_library(macbridge STATIC bridge.mm bridge.h AppDelegate.h AppDelegate.mm)
-
-install(TARGETS macbridge DESTINATION .)
\ No newline at end of file
diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h
deleted file mode 100644
index a735cf82..00000000
--- a/native/libmacbridge/bridge.h
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#ifndef ESPANSO_BRIDGE_H
-#define ESPANSO_BRIDGE_H
-
-#include <stdint.h>
-
-extern "C" {
-
-extern void * context_instance;
-extern char * icon_path;
-extern char * disabled_icon_path;
-extern int32_t show_icon;
-
-/*
-* Initialize the AppDelegate and check for accessibility permissions
-*/
-int32_t initialize(void * context, const char * icon_path, const char * disabled_icon_path, int32_t show_icon);
-
-/*
- * Start the event loop indefinitely. Blocking call.
- */
-int32_t eventloop();
-
-/*
- * Initialize the application and start the headless eventloop, used for the espanso detect command
- */
-int32_t headless_eventloop();
-
-/*
- * Called when a new keypress is made, the first argument is an char array,
- * while the second is the size of the array.
- */
-typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code);
-
-extern KeypressCallback keypress_callback;
-
-/*
- * Register the callback that will be called when a keypress was made
- */
-void register_keypress_callback(KeypressCallback callback);
-
-/*
- * Type the given string by using the CGEventKeyboardSetUnicodeString call
- */
-void send_string(const char * string);
-
-/*
- * Send the Virtual Key press
- */
-void send_vkey(int32_t vk);
-
-/*
- * Send the Virtual Key press multiple times
- */
-void send_multi_vkey(int32_t vk, int32_t count);
-
-/*
- * Send the backspace keypress, *count* times.
- */
-void delete_string(int32_t count);
-
-/*
- * Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
- */
-int32_t are_modifiers_pressed();
-
-/*
- * Trigger normal paste ( Pressing CMD+V )
- */
-void trigger_paste();
-
-/*
- * Trigger normal copy ( Pressing CMD+C )
- */
-void trigger_copy();
-
-// UI
-
-/*
- * Called when the tray icon is clicked
- */
-typedef void (*IconClickCallback)(void * self);
-extern IconClickCallback icon_click_callback;
-void register_icon_click_callback(IconClickCallback callback);
-
-// CONTEXT MENU
-
-typedef struct {
-    int32_t id;
-    int32_t type;
-    char name[100];
-} MenuItem;
-
-int32_t show_context_menu(MenuItem * items, int32_t count);
-
-/*
- * Called when the context menu is clicked
- */
-typedef void (*ContextMenuClickCallback)(void * self, int32_t id);
-extern ContextMenuClickCallback context_menu_click_callback;
-extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback);
-
-/*
- * Update the tray icon status
- */
-extern "C" void update_tray_icon(int32_t enabled);
-
-// SYSTEM
-
-/*
- * Check if espanso is authorized to control accessibility features, needed to detect key presses.
- * @return
- */
-int32_t check_accessibility();
-
-/*
- * Prompt to authorize the accessibility features.
- * @return
- */
-int32_t prompt_accessibility();
-
-/*
- * Open Security & Privacy settings panel
- * @return
- */
-void open_settings_panel();
-
-/*
- * Return the active NSRunningApplication path
- */
-int32_t get_active_app_bundle(char * buffer, int32_t size);
-
-/*
- * Return the active NSRunningApplication bundle identifier
- */
-int32_t get_active_app_identifier(char * buffer, int32_t size);
-
-// CLIPBOARD
-
-/*
- * Return the clipboard text
- */
-int32_t get_clipboard(char * buffer, int32_t size);
-
-/*
- * Set the clipboard text
- */
-int32_t set_clipboard(char * text);
-
-/*
- * Set the clipboard image to the given file
- */
-int32_t set_clipboard_image(char * path);
-
-/*
- * Set the clipboard html 
- */
-int32_t set_clipboard_html(char * html, char * fallback);
-
-/*
- * If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
- */
-int32_t get_secure_input_process(int64_t *pid);
-
-/*
- * Find the executable path corresponding to the given PID, return 0 if no process was found.
- */
-int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size);
-
-};
-#endif //ESPANSO_BRIDGE_H
diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm
deleted file mode 100644
index 72fa20bc..00000000
--- a/native/libmacbridge/bridge.mm
+++ /dev/null
@@ -1,434 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#include "bridge.h"
-
-#import <Foundation/Foundation.h>
-#include <IOKit/IOKitLib.h>
-#include "AppDelegate.h"
-#include <stdio.h>
-#include <string.h>
-#include <libproc.h>
-extern "C" {
-
-}
-
-#include <vector>
-
-void * context_instance;
-char * icon_path;
-char * disabled_icon_path;
-int32_t show_icon;
-AppDelegate * delegate_ptr;
-
-KeypressCallback keypress_callback;
-IconClickCallback icon_click_callback;
-ContextMenuClickCallback context_menu_click_callback;
-
-int32_t initialize(void * context, const char * _icon_path, const char * _disabled_icon_path, int32_t _show_icon) {
-    context_instance = context;
-    icon_path = strdup(_icon_path);
-    disabled_icon_path = strdup(_disabled_icon_path);
-    show_icon = _show_icon;
-
-    AppDelegate *delegate = [[AppDelegate alloc] init];
-    delegate_ptr = delegate;
-    NSApplication * application = [NSApplication sharedApplication];
-    [application setDelegate:delegate];
-}
-
-void register_keypress_callback(KeypressCallback callback) {
-    keypress_callback = callback;
-}
-
-void register_icon_click_callback(IconClickCallback callback) {
-    icon_click_callback = callback;
-}
-
-void register_context_menu_click_callback(ContextMenuClickCallback callback) {
-    context_menu_click_callback = callback;
-}
-
-
-int32_t eventloop() {
-    [NSApp run];
-}
-
-int32_t headless_eventloop() {
-    NSApplication * application = [NSApplication sharedApplication];
-    [NSApp run];
-    return 0;
-}
-
-void update_tray_icon(int32_t enabled) {
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        NSApplication * application = [NSApplication sharedApplication];
-        char * iconPath = icon_path;
-        if (!enabled) {
-            iconPath = disabled_icon_path;
-        }
-
-        [[application delegate] updateIcon: iconPath];
-    });
-}
-
-void send_string(const char * string) {
-    char * stringCopy = strdup(string);
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        // Convert the c string to a UniChar array as required by the CGEventKeyboardSetUnicodeString method
-        NSString *nsString = [NSString stringWithUTF8String:stringCopy];
-        CFStringRef cfString = (__bridge CFStringRef) nsString;
-        std::vector <UniChar> buffer(nsString.length);
-        CFStringGetCharacters(cfString, CFRangeMake(0, nsString.length), buffer.data());
-
-        free(stringCopy);
-
-        // Send the event
-
-        // Check if the shift key is down, and if so, release it
-        // To see why: https://github.com/federico-terzi/espanso/issues/279
-        if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, 0x38)) {
-            CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x38, false);
-            CGEventPost(kCGHIDEventTap, e2);
-            CFRelease(e2);
-
-            usleep(2000);
-        }
-
-        // Because of a bug ( or undocumented limit ) of the CGEventKeyboardSetUnicodeString method
-        // the string gets truncated after 20 characters, so we need to send multiple events.
-
-        int i = 0;
-        while (i < buffer.size()) {
-            int chunk_size = 20;
-            if ((i+chunk_size) >  buffer.size()) {
-                chunk_size = buffer.size() - i;
-            }
-
-            UniChar * offset_buffer = buffer.data() + i;
-            CGEventRef e = CGEventCreateKeyboardEvent(NULL, 0x31, true);
-            CGEventKeyboardSetUnicodeString(e, chunk_size, offset_buffer);
-            CGEventPost(kCGHIDEventTap, e);
-            CFRelease(e);
-
-            usleep(2000);
-
-            // Some applications require an explicit release of the space key
-            // For more information: https://github.com/federico-terzi/espanso/issues/159
-            CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false);
-            CGEventPost(kCGHIDEventTap, e2);
-            CFRelease(e2);
-
-            usleep(2000);
-
-            i += chunk_size;
-        }
-    });
-}
-
-void delete_string(int32_t count) {
-    send_multi_vkey(0x33, count);
-}
-
-void send_vkey(int32_t vk) {
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        CGEventRef keydown;
-        keydown = CGEventCreateKeyboardEvent(NULL, vk, true);
-        CGEventPost(kCGHIDEventTap, keydown);
-        CFRelease(keydown);
-
-        usleep(500);
-
-        CGEventRef keyup;
-        keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
-        CGEventPost(kCGHIDEventTap, keyup);
-        CFRelease(keyup);
-
-        usleep(500);
-    });
-}
-
-void send_multi_vkey(int32_t vk, int32_t count) {
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        for (int i = 0; i < count; i++) {
-            CGEventRef keydown;
-            keydown = CGEventCreateKeyboardEvent(NULL, vk, true);
-            CGEventPost(kCGHIDEventTap, keydown);
-            CFRelease(keydown);
-
-            usleep(500);
-
-            CGEventRef keyup;
-            keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
-            CGEventPost(kCGHIDEventTap, keyup);
-            CFRelease(keyup);
-
-            usleep(500);
-        }
-    });
-}
-
-void trigger_paste() {
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        CGEventRef keydown;
-        keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true);  // CMD
-        CGEventPost(kCGHIDEventTap, keydown);
-        CFRelease(keydown);
-
-        usleep(2000);
-
-        CGEventRef keydown2;
-        keydown2 = CGEventCreateKeyboardEvent(NULL, 0x09, true);  // V key
-        CGEventPost(kCGHIDEventTap, keydown2);
-        CFRelease(keydown2);
-
-        usleep(2000);
-
-        CGEventRef keyup;
-        keyup = CGEventCreateKeyboardEvent(NULL, 0x09, false);
-        CGEventPost(kCGHIDEventTap, keyup);
-        CFRelease(keyup);
-
-        usleep(2000);
-
-        CGEventRef keyup2;
-        keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false);  // CMD
-        CGEventPost(kCGHIDEventTap, keyup2);
-        CFRelease(keyup2);
-
-        usleep(2000);
-    });
-}
-
-
-void trigger_copy() {
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-        CGEventRef keydown;
-        keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true);  // CMD
-        CGEventPost(kCGHIDEventTap, keydown);
-        CFRelease(keydown);
-
-        usleep(2000);
-
-        CGEventRef keydown2;
-        keydown2 = CGEventCreateKeyboardEvent(NULL, 0x08, true);  // C key
-        CGEventPost(kCGHIDEventTap, keydown2);
-        CFRelease(keydown2);
-
-        usleep(2000);
-
-        CGEventRef keyup;
-        keyup = CGEventCreateKeyboardEvent(NULL, 0x08, false);
-        CGEventPost(kCGHIDEventTap, keyup);
-        CFRelease(keyup);
-
-        usleep(2000);
-
-        CGEventRef keyup2;
-        keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false);  // CMD
-        CGEventPost(kCGHIDEventTap, keyup2);
-        CFRelease(keyup2);
-
-        usleep(2000);
-    });
-}
-
-int32_t are_modifiers_pressed() {
-    if ((NSEventModifierFlagControl | NSEventModifierFlagOption | 
-         NSEventModifierFlagCommand | NSEventModifierFlagShift) & [NSEvent modifierFlags]) {
-        return 1;
-    }
-    return 0;
-}
-
-int32_t get_active_app_bundle(char * buffer, int32_t size) {
-    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
-    NSString *bundlePath = [frontApp bundleURL].path;
-    const char * path = [bundlePath UTF8String];
-
-    snprintf(buffer, size, "%s", path);
-
-    [bundlePath release];
-
-    return 1;
-}
-
-int32_t get_active_app_identifier(char * buffer, int32_t size) {
-    NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
-    NSString *bundleId = frontApp.bundleIdentifier;
-    const char * bundle = [bundleId UTF8String];
-
-    snprintf(buffer, size, "%s", bundle);
-
-    [bundleId release];
-
-    return 1;
-}
-
-int32_t get_clipboard(char * buffer, int32_t size) {
-    NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
-    for (id element in pasteboard.pasteboardItems) {
-        NSString *string = [element stringForType: NSPasteboardTypeString];
-        if (string != NULL) {
-            const char * text = [string UTF8String];
-            snprintf(buffer, size, "%s", text);
-
-            [string release];
-
-            return 1;
-        }
-    }
-
-    return -1;
-}
-
-int32_t set_clipboard(char * text) {
-    NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
-    NSArray *array = @[NSPasteboardTypeString];
-    [pasteboard declareTypes:array owner:nil];
-
-    NSString *nsText = [NSString stringWithUTF8String:text];
-    [pasteboard setString:nsText forType:NSPasteboardTypeString];
-}
-
-int32_t set_clipboard_image(char *path) {
-    NSString *pathString = [NSString stringWithUTF8String:path];
-    NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString];
-    int result = 0;
-
-    if (image != nil) {
-        NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
-        [pasteboard clearContents];
-        NSArray *copiedObjects = [NSArray arrayWithObject:image];
-        [pasteboard writeObjects:copiedObjects];
-        result = 1;
-    }
-    [image release];
-
-    return result;
-}
-
-int32_t set_clipboard_html(char * html, char * fallback_text) {
-    NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
-    NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString];
-    [pasteboard declareTypes:array owner:nil];
-
-    NSString *nsHtml = [NSString stringWithUTF8String:html];
-    NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil];
-    NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil];
-
-    NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length])
-                                    documentAttributes:nil];
-
-    [pasteboard setData:rtf forType:NSRTFPboardType];
-
-    NSString *nsText = [NSString stringWithUTF8String:fallback_text];
-    [pasteboard setString:nsText forType:NSPasteboardTypeString];
-}
-
-
-// CONTEXT MENU
-
-int32_t show_context_menu(MenuItem * items, int32_t count) {
-    MenuItem * item_copy = (MenuItem*)malloc(sizeof(MenuItem)*count);
-    memcpy(item_copy, items, sizeof(MenuItem)*count);
-    int32_t count_copy = count;
-
-    dispatch_async(dispatch_get_main_queue(), ^(void) {
-
-        NSMenu *espansoMenu = [[NSMenu alloc] initWithTitle:@"Espanso"];
-
-        for (int i = 0; i<count_copy; i++) {
-            if (item_copy[i].type == 1) {
-                NSString *title = [NSString stringWithUTF8String:item_copy[i].name];
-                NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:title action:@selector(contextMenuClick:) keyEquivalent:@""];
-                [newMenu setTag:(NSInteger)item_copy[i].id];
-                [espansoMenu addItem: newMenu];
-            }else{
-                [espansoMenu addItem: [NSMenuItem separatorItem]];
-            }
-        }
-
-        free(item_copy);
-
-        [delegate_ptr->myStatusItem popUpStatusItemMenu:espansoMenu];
-    });
-}
-
-// 10.9+ only, see this url for compatibility:
-// http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9
-int32_t check_accessibility() {
-    NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @NO};
-    return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
-}
-
-int32_t prompt_accessibility() {
-    NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES};
-    return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
-}
-
-void open_settings_panel() {
-    NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
-    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
-}
-
-// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys
-int32_t get_secure_input_process(int64_t *pid) {
-    NSArray *consoleUsersArray;
-    io_service_t rootService;
-    int32_t result = 0;
-
-    if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0)
-    {
-        if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil)
-        {
-            if ([consoleUsersArray isKindOfClass:[NSArray class]])  // Be careful - ensure this really is an array
-            {
-                for (NSDictionary *consoleUserDict in consoleUsersArray) {
-                    NSNumber *secureInputPID;
-
-                    if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil)
-                    {
-                        if ([secureInputPID isKindOfClass:[NSNumber class]])
-                        {
-                            *pid = ((UInt64) [secureInputPID intValue]);
-                            result = 1;
-                            break;
-                        }
-                    }
-                }
-            }
-
-            CFRelease((CFTypeRef)consoleUsersArray);
-        }
-
-        IOObjectRelease((io_object_t) rootService);
-    }
-
-    return result;
-}
-
-int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) {
-    int res = proc_pidpath((pid_t) pid, buff, buff_size);
-    if ( res <= 0 ) {
-        return 0;
-    } else {
-        return 1;
-    }
-}
\ No newline at end of file
diff --git a/native/libwinbridge/CMakeLists.txt b/native/libwinbridge/CMakeLists.txt
deleted file mode 100644
index f0ae9604..00000000
--- a/native/libwinbridge/CMakeLists.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-cmake_minimum_required(VERSION 3.0)
-project(libwinbridge)
-
-set (CMAKE_CXX_STANDARD 14)
-
-add_library(winbridge STATIC bridge.cpp bridge.h)
-
-install(TARGETS winbridge DESTINATION .)
\ No newline at end of file
diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp
deleted file mode 100644
index 11c9ab95..00000000
--- a/native/libwinbridge/bridge.cpp
+++ /dev/null
@@ -1,934 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#include "bridge.h"
-#include <stdio.h>
-#include <iostream>
-#include <string>
-#include <vector>
-#include <memory>
-#include <array>
-
-#define UNICODE
-
-#ifdef __MINGW32__
-# ifndef WINVER
-#  define WINVER 0x0606
-# endif
-# define STRSAFE_NO_DEPRECATE
-#endif
-
-#include <windows.h>
-#include <winuser.h>
-#include <strsafe.h>
-#include <shellapi.h>
-
-#pragma comment( lib, "gdiplus.lib" )
-#include <gdiplus.h>
-#include <Windows.h>
-
-// How many milliseconds must pass between keystrokes to refresh the keyboard layout
-const long refreshKeyboardLayoutInterval = 2000;
-
-void * manager_instance;
-int32_t show_icon;
-
-// Keyboard listening
-
-DWORD lastKeyboardPressTick = 0;
-HKL currentKeyboardLayout;
-HWND window;
-const wchar_t* const winclass = L"Espanso";
-
-
-
-// UI
-
-#define APPWM_ICON_CLICK (WM_APP + 1)
-#define APPWM_NOTIFICATION_POPUP (WM_APP + 2)
-#define APPWM_NOTIFICATION_CLOSE (WM_APP + 3)
-#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 4)
-
-const wchar_t* const notification_winclass = L"EspansoNotification";
-HWND nw = NULL;
-HWND hwnd_st_u = NULL;
-HBITMAP g_espanso_bmp = NULL;
-HICON g_espanso_ico = NULL;
-HICON g_espanso_red_ico = NULL;
-NOTIFYICONDATA nid = {};
-
-UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
-
-// Callbacks
-
-KeypressCallback keypress_callback = NULL;
-IconClickCallback icon_click_callback = NULL;
-ContextMenuClickCallback context_menu_click_callback = NULL;
-
-void register_keypress_callback(KeypressCallback callback) {
-    keypress_callback = callback;
-}
-
-void register_icon_click_callback(IconClickCallback callback) {
-    icon_click_callback = callback;
-}
-
-void register_context_menu_click_callback(ContextMenuClickCallback callback) {
-    context_menu_click_callback = callback;
-}
-
-/*
- * Message handler procedure for the windows
- */
-LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
-{
-    HDC hdcStatic = NULL;
-
-    switch (msg)
-    {
-        case WM_DESTROY:
-            std::cout << "\ndestroying window\n";
-            PostQuitMessage(0);
-            DeleteObject(g_espanso_bmp);
-            DeleteObject(g_espanso_ico);
-            return 0L;
-        case WM_COMMAND:  // Click on the tray icon context menu
-        {
-            UINT  idItem = (UINT)LOWORD(wp);
-            UINT  flags = (UINT)HIWORD(wp);
-
-            if (flags == 0) {
-                context_menu_click_callback(manager_instance, (int32_t)idItem);
-            }
-
-            break;
-        }
-        case APPWM_NOTIFICATION_POPUP:  // Request to show a notification
-        {
-            std::unique_ptr<wchar_t[]> ptr(reinterpret_cast<wchar_t*>(wp));
-
-            SetWindowText(hwnd_st_u, L"                                                 ");  // Clear the previous text
-            SetWindowText(hwnd_st_u, ptr.get());
-
-            // Show the window
-            ShowWindow(nw, SW_SHOWNOACTIVATE);
-            break;
-        }
-        case APPWM_NOTIFICATION_CLOSE:  // Request to close a notification
-        {
-            // Hide the window
-            ShowWindow(nw, SW_HIDE);
-            break;
-        }
-        case APPWM_SHOW_CONTEXT_MENU:  // Request to show context menu
-        {
-            HMENU hPopupMenu = CreatePopupMenu();
-
-            // Create the menu
-
-            int32_t count = static_cast<int32_t>(lp);
-            std::unique_ptr<MenuItem[]> items(reinterpret_cast<MenuItem*>(wp));
-
-            for (int i = 0; i<count; i++) {
-                if (items[i].type == 1) {
-                    InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_STRING, items[i].id, items[i].name);
-                }else{
-                    InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_SEPARATOR, items[i].id, NULL);
-                }
-            }
-
-            POINT pt;
-            GetCursorPos(&pt);
-            SetForegroundWindow(nw);
-            TrackPopupMenu(hPopupMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, nw, NULL);
-            break;
-        }
-        case APPWM_ICON_CLICK:  // Click on the tray icon
-        {
-            switch (lp)
-            {
-            case WM_LBUTTONUP:
-            case WM_RBUTTONUP:
-                icon_click_callback(manager_instance);
-                break;
-            }
-        }
-        case WM_PAINT:
-        {
-            BITMAP bm;
-            PAINTSTRUCT ps;
-
-            HDC hdc = BeginPaint(window, &ps);
-
-            HDC hdcMem = CreateCompatibleDC(hdc);
-            HBITMAP hbmOld = (HBITMAP) SelectObject(hdcMem, g_espanso_bmp);
-
-            GetObject(g_espanso_bmp, sizeof(bm), &bm);
-
-            BitBlt(hdc, 10, 10, 80, 80, hdcMem, 0, 0, SRCCOPY);
-
-            SelectObject(hdcMem, hbmOld);
-            DeleteDC(hdcMem);
-
-            EndPaint(window, &ps);
-            break;
-        }
-        case WM_CTLCOLORSTATIC:
-            hdcStatic = (HDC)wp;
-            SetTextColor(hdcStatic, RGB(0, 0, 0));
-            SetBkColor(hdcStatic, RGB(255, 255, 255));
-            //SetBkMode(hdcStatic, OPAQUE);
-
-            return (LRESULT)GetStockObject(NULL_BRUSH);
-        case WM_INPUT:  // Message relative to the RAW INPUT events
-        {
-            // Get the input size
-            UINT dwSize;
-            GetRawInputData(
-                    (HRAWINPUT)lp,
-                    RID_INPUT,
-                    NULL,
-                    &dwSize,
-                    sizeof(RAWINPUTHEADER)
-            );
-
-            // Create a proper sized structure to hold the data
-            std::vector<BYTE> lpb(dwSize);
-
-            // Request the Raw input data
-            if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize,
-                                sizeof(RAWINPUTHEADER)) != dwSize) {
-                return 0;
-            }
-
-            // Convert the input data
-            RAWINPUT* raw = reinterpret_cast<RAWINPUT*>(lpb.data());
-            // Make sure it's a keyboard type event, relative to a key press.
-            if (raw->header.dwType == RIM_TYPEKEYBOARD)
-            {
-                // We only want KEY UP AND KEY DOWN events
-                if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
-                    raw->data.keyboard.Message != WM_SYSKEYDOWN) {
-                    return 0;
-                }
-
-                // The alt key sends a SYSKEYDOWN instead of KEYDOWN event
-                int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN ||
-                                  raw->data.keyboard.Message == WM_SYSKEYDOWN;
-
-                DWORD currentTick = GetTickCount();
-
-                // If enough time has passed between the last keypress and now, refresh the keyboard layout
-                if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) {
-
-                    // Because keyboard layouts on windows are Window-specific, to get the current
-                    // layout we need to get the foreground window and get its layout.
-
-                    HWND hwnd = GetForegroundWindow();
-                    if (hwnd) {
-                        DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
-                        HKL newKeyboardLayout = GetKeyboardLayout(threadID);
-
-                        // It's not always valid, so update the current value only if available.
-                        if (newKeyboardLayout != 0) {
-                            currentKeyboardLayout = newKeyboardLayout;
-                        }
-                    }
-
-                    lastKeyboardPressTick = currentTick;
-                }
-
-                // Get keyboard state ( necessary to decode the associated Unicode char )
-                std::vector<BYTE> lpKeyState(256);
-                if (GetKeyboardState(lpKeyState.data())) {
-                    // Convert the virtual key to an unicode char
-                    std::array<WCHAR, 4> buffer;
-
-                    // This flag is needed to avoid chaning the keyboard state for some layouts.
-                    // Refer to issue: https://github.com/federico-terzi/espanso/issues/86
-                    UINT flags = 1 << 2;
-
-                    int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), flags, currentKeyboardLayout);
-
-                    //std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl;
-
-                    // We need to call the callback in two different ways based on the type of key
-                    // The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it.
-                    if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) {
-                        keypress_callback(manager_instance, reinterpret_cast<uint16_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, 0, is_key_down);
-                    }else{
-                        //std::cout << raw->data.keyboard.MakeCode << " " << raw->data.keyboard.Flags << std::endl;
-                        int variant = 0;
-                        if (raw->data.keyboard.VKey == VK_SHIFT) {
-                            // To discriminate between the left and right shift, we need to employ a workaround.
-                            // See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput
-                            if (raw->data.keyboard.MakeCode == 42) { // Left shift
-                                variant = LEFT_VARIANT;
-                            }if (raw->data.keyboard.MakeCode == 54) { // Right shift
-                                variant = RIGHT_VARIANT;
-                            }
-                        }else{
-                            // Also the ALT and CTRL key are special cases
-                            // Check out the previous Stackoverflow question for more information
-                            if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU) {
-                                if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0) {
-                                    variant = RIGHT_VARIANT;
-                                }else{
-                                    variant = LEFT_VARIANT;
-                                }
-                            }
-                        }
-
-                        keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, variant, is_key_down);
-                    }
-                }
-            }else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches
-            {
-                if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) {
-                    //std::cout << "mouse down" << std::endl;
-                    keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0, 0);
-                }
-            }
-
-            return 0;
-        }
-        default:
-            if (msg == WM_TASKBARCREATED) {  // Explorer crashed, recreate the icon
-                if (show_icon) {
-                    Shell_NotifyIcon(NIM_ADD, &nid);
-                }
-            }
-            return DefWindowProc(window, msg, wp, lp);
-    }
-}
-
-int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t _show_icon) {
-    manager_instance = self;
-    show_icon = _show_icon;
-
-    // Load the images
-    g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
-    g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
-    g_espanso_red_ico = (HICON)LoadImage(NULL, red_ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
-
-    // Make the notification capable of handling different screen definitions
-    SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
-
-    // Initialize the default keyboard layout
-    currentKeyboardLayout = GetKeyboardLayout(0);
-
-    // Initialize the Worker window
-
-    // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
-    WNDCLASSEX wndclass = {
-            sizeof(WNDCLASSEX),				// cbSize: Size of this structure
-            0,								// style: Class styles
-            window_procedure,		        // lpfnWndProc: Pointer to the window procedure
-            0,								// cbClsExtra: Number of extra bytes to allocate following the window-class structure
-            0,								// cbWndExtra: The number of extra bytes to allocate following the window instance.
-            GetModuleHandle(0),				// hInstance: A handle to the instance that contains the window procedure for the class.
-            NULL,							// hIcon: A handle to the class icon.
-            LoadCursor(0,IDC_ARROW),	    // hCursor: A handle to the class cursor.
-            NULL,							// hbrBackground: A handle to the class background brush.
-            NULL,							// lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
-            winclass,						// lpszClassName: A pointer to a null-terminated string or is an atom.
-            NULL							// hIconSm: A handle to a small icon that is associated with the window class.
-    };
-
-    // Notification Window
-
-    // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
-    WNDCLASSEX notificationwndclass = {
-            sizeof(WNDCLASSEX),				// cbSize: Size of this structure
-            0,								// style: Class styles
-            window_procedure,	            // lpfnWndProc: Pointer to the window procedure
-            0,								// cbClsExtra: Number of extra bytes to allocate following the window-class structure
-            0,								// cbWndExtra: The number of extra bytes to allocate following the window instance.
-            GetModuleHandle(0),				// hInstance: A handle to the instance that contains the window procedure for the class.
-            NULL,							// hIcon: A handle to the class icon.
-            LoadCursor(0,IDC_ARROW),	    // hCursor: A handle to the class cursor.
-            NULL,							// hbrBackground: A handle to the class background brush.
-            NULL,							// lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
-            notification_winclass,			// lpszClassName: A pointer to a null-terminated string or is an atom.
-            NULL							// hIconSm: A handle to a small icon that is associated with the window class.
-    };
-
-    if (RegisterClassEx(&wndclass) && RegisterClassEx(&notificationwndclass))
-    {
-        // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
-        window = CreateWindowEx(
-                0,										// dwExStyle: The extended window style of the window being created.
-                winclass,								// lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
-                L"Espanso Worker Window",				// lpWindowName: The window name.
-                WS_OVERLAPPEDWINDOW,					// dwStyle: The style of the window being created.
-                CW_USEDEFAULT,							// X: The initial horizontal position of the window.
-                CW_USEDEFAULT,							// Y: The initial vertical position of the window.
-                100,									// nWidth: The width, in device units, of the window.
-                100,									// nHeight: The height, in device units, of the window.
-                NULL,									// hWndParent:  handle to the parent or owner window of the window being created.
-                NULL,									// hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
-                GetModuleHandle(0),						// hInstance: A handle to the instance of the module to be associated with the window.
-                NULL									// lpParam: Pointer to a value to be passed to the window
-        );
-
-        // Register raw inputs
-        RAWINPUTDEVICE Rid[2];
-
-        Rid[0].usUsagePage = 0x01;
-        Rid[0].usUsage = 0x06;
-        Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK;   // adds HID keyboard and also ignores legacy keyboard messages
-        Rid[0].hwndTarget = window;
-
-        Rid[1].usUsagePage = 0x01;
-        Rid[1].usUsage = 0x02;
-        Rid[1].dwFlags = RIDEV_INPUTSINK;   // adds HID mouse and also ignores legacy mouse messages
-        Rid[1].hwndTarget = window;
-
-        if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) {  // Something went wrong, error.
-            return -1;
-        }
-
-        // Initialize the notification window
-        nw = CreateWindowEx(
-                WS_EX_TOOLWINDOW | WS_EX_TOPMOST,		// dwExStyle: The extended window style of the window being created.
-                notification_winclass, 					// lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
-                L"Espanso Notification",				// lpWindowName: The window name.
-                WS_POPUPWINDOW,							// dwStyle: The style of the window being created.
-                CW_USEDEFAULT,							// X: The initial horizontal position of the window.
-                CW_USEDEFAULT,							// Y: The initial vertical position of the window.
-                300,									// nWidth: The width, in device units, of the window.
-                100,									// nHeight: The height, in device units, of the window.
-                NULL,									// hWndParent:  handle to the parent or owner window of the window being created.
-                NULL,									// hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
-                GetModuleHandle(0),						// hInstance: A handle to the instance of the module to be associated with the window.
-                NULL									// lpParam: Pointer to a value to be passed to the window
-        );
-
-        if (nw)
-        {
-            int x, w, y, h;
-            y = 40; h = 30;
-            x = 100; w = 180;
-            hwnd_st_u = CreateWindowEx(0, L"static", L"ST_U",
-                                       WS_CHILD | WS_VISIBLE | WS_TABSTOP | SS_CENTER,
-                                       x, y, w, h,
-                                       nw, (HMENU)(501),
-                                       (HINSTANCE)GetWindowLong(nw, GWLP_HINSTANCE), NULL);
-
-            SetWindowText(hwnd_st_u, L"Loading...");
-
-            int posX = GetSystemMetrics(SM_CXSCREEN) - 350;
-            int posY = GetSystemMetrics(SM_CYSCREEN) - 200;
-
-            SetWindowPos(nw, HWND_TOP, posX, posY, 0, 0, SWP_NOSIZE);
-
-            // Hide the window
-            ShowWindow(nw, SW_HIDE);
-
-            // Setup the icon in the notification space
-
-            SendMessage(nw, WM_SETICON, ICON_BIG, (LPARAM)g_espanso_ico);
-            SendMessage(nw, WM_SETICON, ICON_SMALL, (LPARAM)g_espanso_ico);
-
-            //Notification
-            nid.cbSize = sizeof(nid);
-            nid.hWnd = nw;
-            nid.uID = 1;
-            nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
-            nid.uCallbackMessage = APPWM_ICON_CLICK;
-            nid.hIcon = g_espanso_ico;
-            StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"espanso");
-
-            // Show the notification.
-            if (show_icon) {
-                Shell_NotifyIcon(NIM_ADD, &nid);
-            }
-        }
-    }else{
-        // Something went wrong, error.
-        return -1;
-    }
-
-    return 1;
-}
-
-void eventloop() {
-    if (window)
-    {
-        // Hide the window
-        ShowWindow(window, SW_HIDE);
-
-        // Enter the Event loop
-        MSG msg;
-        while (GetMessage(&msg, 0, 0, 0))  DispatchMessage(&msg);
-    }
-
-    // Something went wrong, this should have been an infinite loop.
-}
-
-void update_tray_icon(int32_t enabled) {
-    if (enabled) {
-        nid.hIcon = g_espanso_ico;
-    }else{
-        nid.hIcon = g_espanso_red_ico;
-    }
-
-    // Update the icon
-    if (show_icon) {
-        Shell_NotifyIcon(NIM_MODIFY, &nid);
-    }
-}
-
-/*
- * Type the given string simulating keyboard presses.
- */
-void send_string(const wchar_t * string) {
-    std::wstring msg = string;
-
-    std::vector<INPUT> vec;
-    for (auto ch : msg)
-    {
-    	INPUT input = { 0 };
-    	input.type = INPUT_KEYBOARD;
-    	input.ki.dwFlags = KEYEVENTF_UNICODE;
-    	input.ki.wScan = ch;
-    	vec.push_back(input);
-
-    	input.ki.dwFlags |= KEYEVENTF_KEYUP;
-    	vec.push_back(input);
-    }
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-/*
- * Send the backspace keypress, *count* times.
- */
-void delete_string(int32_t count, int32_t delay) {
-    if (delay != 0) {
-        send_multi_vkey_with_delay(VK_BACK, count, delay);
-    }else{
-        send_multi_vkey(VK_BACK, count);
-    }
-}
-
-void send_vkey(int32_t vk) {
-    std::vector<INPUT> vec;
-
-    INPUT input = { 0 };
-
-    input.type = INPUT_KEYBOARD;
-    input.ki.wScan = 0;
-    input.ki.time = 0;
-    input.ki.dwExtraInfo = 0;
-    input.ki.wVk = vk;
-    input.ki.dwFlags = 0; // 0 for key press
-    vec.push_back(input);
-
-    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-    vec.push_back(input);
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-void send_multi_vkey(int32_t vk, int32_t count) {
-    std::vector<INPUT> vec;
-
-    for (int i = 0; i < count; i++) {
-        INPUT input = { 0 };
-
-        input.type = INPUT_KEYBOARD;
-        input.ki.wScan = 0;
-        input.ki.time = 0;
-        input.ki.dwExtraInfo = 0;
-        input.ki.wVk = vk;
-        input.ki.dwFlags = 0; // 0 for key press
-        vec.push_back(input);
-
-        input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-        vec.push_back(input);
-    }
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay) {
-    for (int i = 0; i < count; i++) {
-        INPUT input = { 0 };
-
-        input.type = INPUT_KEYBOARD;
-        input.ki.wScan = 0;
-        input.ki.time = 0;
-        input.ki.dwExtraInfo = 0;
-        input.ki.wVk = vk;
-        input.ki.dwFlags = 0; // 0 for key press
-        SendInput(1, &input, sizeof(INPUT));
-
-        Sleep(delay);
-
-        input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-        SendInput(1, &input, sizeof(INPUT));
-
-        Sleep(delay);
-    }
-}
-
-
-void trigger_shift_paste() {
-    std::vector<INPUT> vec;
-
-    INPUT input = { 0 };
-
-    input.type = INPUT_KEYBOARD;
-    input.ki.wScan = 0;
-    input.ki.time = 0;
-    input.ki.dwExtraInfo = 0;
-    input.ki.wVk = VK_CONTROL;
-    input.ki.dwFlags = 0; // 0 for key press
-    vec.push_back(input);
-
-    input.ki.wVk = VK_SHIFT;  // SHIFT KEY
-    vec.push_back(input);
-
-    input.ki.wVk = 0x56;  // V KEY
-    vec.push_back(input);
-
-    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-    vec.push_back(input);
-
-    input.ki.wVk = VK_SHIFT;  // SHIFT KEY
-    vec.push_back(input);
-
-    input.ki.wVk = VK_CONTROL;
-    vec.push_back(input);
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-void trigger_paste() {
-    std::vector<INPUT> vec;
-
-    INPUT input = { 0 };
-
-    input.type = INPUT_KEYBOARD;
-    input.ki.wScan = 0;
-    input.ki.time = 0;
-    input.ki.dwExtraInfo = 0;
-    input.ki.wVk = VK_CONTROL;
-    input.ki.dwFlags = 0; // 0 for key press
-    vec.push_back(input);
-
-    input.ki.wVk = 0x56;  // V KEY
-    vec.push_back(input);
-
-    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-    vec.push_back(input);
-
-    input.ki.wVk = VK_CONTROL;
-    vec.push_back(input);
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-void trigger_copy() {
-    std::vector<INPUT> vec;
-
-    INPUT input = { 0 };
-
-    input.type = INPUT_KEYBOARD;
-    input.ki.wScan = 0;
-    input.ki.time = 0;
-    input.ki.dwExtraInfo = 0;
-    input.ki.wVk = VK_CONTROL;
-    input.ki.dwFlags = 0; // 0 for key press
-    vec.push_back(input);
-
-    input.ki.wVk = 0x43;  // C KEY
-    vec.push_back(input);
-
-    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
-    vec.push_back(input);
-
-    input.ki.wVk = VK_CONTROL;
-    vec.push_back(input);
-
-    SendInput(vec.size(), vec.data(), sizeof(INPUT));
-}
-
-int32_t are_modifiers_pressed() {
-    short ctrl_pressed = GetAsyncKeyState(VK_CONTROL);
-    short enter_pressed = GetAsyncKeyState(VK_RETURN);
-    short alt_pressed = GetAsyncKeyState(VK_MENU);
-    short shift_pressed = GetAsyncKeyState(VK_SHIFT);
-    short meta_pressed = GetAsyncKeyState(VK_LWIN);
-    short rmeta_pressed = GetAsyncKeyState(VK_RWIN);
-    if (((ctrl_pressed & 0x8000) + 
-        (enter_pressed & 0x8000) + 
-        (alt_pressed & 0x8000) +
-        (shift_pressed & 0x8000) + 
-        (meta_pressed & 0x8000) + 
-        (rmeta_pressed & 0x8000)) != 0) {
-            return 1;
-        }
-    
-    return 0;
-}
-
-
-// SYSTEM
-
-int32_t get_active_window_name(wchar_t * buffer, int32_t size) {
-    HWND hwnd = GetForegroundWindow();
-
-    return GetWindowText(hwnd, buffer, size);
-}
-
-int32_t get_active_window_executable(wchar_t * buffer, int32_t size) {
-    HWND hwnd = GetForegroundWindow();
-
-    // Extract the window PID
-    DWORD windowPid;
-    GetWindowThreadProcessId(hwnd, &windowPid);
-
-    DWORD dsize = (DWORD) size;
-
-    // Extract the process executable file path
-    HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid);
-    int res = QueryFullProcessImageNameW(process, 0, buffer, &dsize);
-    CloseHandle(process);
-
-    return res;
-}
-
-// Notifications
-
-int32_t show_notification(wchar_t * message) {
-    if (nw != NULL) {
-        wchar_t * buffer = new wchar_t[100];
-        swprintf(buffer, 100, L"%ls", message);
-
-        PostMessage(nw, APPWM_NOTIFICATION_POPUP, reinterpret_cast<WPARAM>(buffer), 0);
-        return 1;
-    }
-
-    return -1;
-}
-
-void close_notification() {
-    if (nw != NULL) {
-        PostMessage(nw, APPWM_NOTIFICATION_CLOSE, 0, 0);
-    }
-}
-
-int32_t show_context_menu(MenuItem * items, int32_t count) {
-    if (nw != NULL) {
-        MenuItem * items_buffer = new MenuItem[count];
-        memcpy(items_buffer, items, sizeof(MenuItem)*count);
-
-        PostMessage(nw, APPWM_SHOW_CONTEXT_MENU, reinterpret_cast<WPARAM>(items_buffer), static_cast<LPARAM>(count));
-        return 1;
-    }
-
-    return -1;
-}
-
-void cleanup_ui() {
-    Shell_NotifyIcon(NIM_DELETE, &nid);
-}
-
-// SYSTEM
-
-int32_t start_daemon_process() {
-    wchar_t cmd[MAX_PATH];
-    swprintf(cmd, MAX_PATH, L"espanso.exe daemon");
-
-    // Get current espanso directory
-    TCHAR espansoFilePath[MAX_PATH];
-    GetModuleFileName(NULL, espansoFilePath, MAX_PATH);
-
-    STARTUPINFO si = { sizeof(si) };
-    PROCESS_INFORMATION pi;
-
-    // Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
-    BOOL res = CreateProcess(
-            espansoFilePath,
-            cmd,
-            NULL,
-            NULL,
-            FALSE,
-            DETACHED_PROCESS | CREATE_NO_WINDOW,
-            NULL,
-            NULL,
-            &si,
-            &pi
-    );
-
-    if (!res) {
-        return -1;
-    }
-
-    return 1;
-}
-
-
-int32_t start_process(wchar_t * _cmd) {
-    wchar_t cmd[MAX_PATH];
-    swprintf(cmd, MAX_PATH, _cmd);
-
-    STARTUPINFO si = { sizeof(si) };
-    PROCESS_INFORMATION pi;
-
-    // Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
-    BOOL res = CreateProcess(
-            NULL,
-            cmd,
-            NULL,
-            NULL,
-            FALSE,
-            DETACHED_PROCESS,
-            NULL,
-            NULL,
-            &si,
-            &pi
-    );
-
-    if (!res) {
-        return -1;
-    }
-
-    return 1;
-}
-
-// CLIPBOARD
-
-int32_t set_clipboard(wchar_t *text) {
-    int32_t result = 0;
-    const size_t len = wcslen(text) + 1;
-    HGLOBAL hMem =  GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t));
-    memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t));
-    GlobalUnlock(hMem);
-    if (!OpenClipboard(NULL)) {
-        return -1;
-    }
-    EmptyClipboard();
-    if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
-        result = -2;
-    }
-    CloseClipboard();
-    return result;
-}
-
-int32_t get_clipboard(wchar_t *buffer, int32_t size) {
-    int32_t result = 1;
-    if (!OpenClipboard(NULL)) {
-        return -1;
-    }
-
-    // Get handle of clipboard object for ANSI text
-    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
-    if (!hData) {
-        result = -2;
-    }else{
-        HGLOBAL hMem = GlobalLock(hData);
-        if (!hMem) {
-            result = -3;
-        }else{
-            GlobalUnlock(hMem);
-            swprintf(buffer, size, L"%s", hMem);
-        }
-    }
-
-    CloseClipboard();
-    return result;
-}
-
-int32_t set_clipboard_image(wchar_t *path) {
-    bool result = false;
-
-    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
-    ULONG_PTR gdiplusToken;
-    Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
-
-    Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path);
-    if (gdibmp)
-    {
-        HBITMAP hbitmap;
-        gdibmp->GetHBITMAP(0, &hbitmap);
-        if (OpenClipboard(NULL))
-        {
-            EmptyClipboard();
-            DIBSECTION ds;
-            if (GetObject(hbitmap, sizeof(DIBSECTION), &ds))
-            {
-                HDC hdc = GetDC(HWND_DESKTOP);
-                //create compatible bitmap (get DDB from DIB)
-                HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT,
-                                                     ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS);
-                ReleaseDC(HWND_DESKTOP, hdc);
-                SetClipboardData(CF_BITMAP, hbitmap_ddb);
-                DeleteObject(hbitmap_ddb);
-                result = true;
-            }
-            CloseClipboard();
-        }
-
-        //cleanup:
-        DeleteObject(hbitmap);
-        delete gdibmp;
-    }
-
-    Gdiplus::GdiplusShutdown(gdiplusToken);
-
-    return result;
-}
-
-// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
-int32_t set_clipboard_html(char * html, wchar_t * text_fallback) {
-    // Get clipboard id for HTML format
-    static int cfid = 0;
-    if(!cfid) {
-        cfid = RegisterClipboardFormat(L"HTML Format");
-    }
-
-    int32_t result = 0;
-    const size_t html_len = strlen(html) + 1;
-    HGLOBAL hMem =  GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char));
-    memcpy(GlobalLock(hMem), html, html_len * sizeof(char));
-    GlobalUnlock(hMem);
-
-    const size_t fallback_len = wcslen(text_fallback) + 1;
-    HGLOBAL hMemFallback =  GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t));
-    memcpy(GlobalLock(hMemFallback), text_fallback, fallback_len * sizeof(wchar_t));
-    GlobalUnlock(hMemFallback);
-
-    if (!OpenClipboard(NULL)) {
-        return -1;
-    }
-    EmptyClipboard();
-    if (!SetClipboardData(cfid, hMem)) {
-        result = -2;
-    }
-    
-    if (!SetClipboardData(CF_UNICODETEXT, hMemFallback)) {
-        result = -3;
-    }
-    CloseClipboard();
-    GlobalFree(hMem);
-    return result;
-}
diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h
deleted file mode 100644
index fadcaeb4..00000000
--- a/native/libwinbridge/bridge.h
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#ifndef ESPANSO_BRIDGE_H
-#define ESPANSO_BRIDGE_H
-
-#include <stdio.h>
-#include <stdint.h>
-
-// SYSTEM
-
-extern "C" int32_t start_daemon_process();
-
-extern void * manager_instance;
-
-/*
- * Initialize the Windows parameters
- * return: 1 if OK, -1 otherwise.
- */
-extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t show_icon);
-
-#define LEFT_VARIANT 1
-#define RIGHT_VARIANT 2
-
-/*
- * Called when a new keypress is made, the first argument is an int array,
- * while the second is the size of the array.
- */
-typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t variant, int32_t is_key_down);
-extern KeypressCallback keypress_callback;
-
-/*
- * Register the callback that will be called when a keypress was made
- */
-extern "C" void register_keypress_callback(KeypressCallback callback);
-
-/*
- * Start the event loop indefinitely. Blocking call.
- */
-extern "C" void eventloop();
-
-// Keyboard Manager
-
-/*
- * Type the given string by simulating Key Presses
- */
-extern "C" void send_string(const wchar_t * string);
-
-/*
- * Send the given Virtual Key press
- */
-extern "C" void send_vkey(int32_t vk);
-
-/*
- * Send the given Virtual Key press multiple times
- */
-extern "C" void send_multi_vkey(int32_t vk, int32_t count);
-
-/*
- * Send the given Virtual Key press multiple times adding a delay between each keypress
- */
-extern "C" void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay);
-
-/*
- * Send the backspace keypress, *count* times.
- */
-extern "C" void delete_string(int32_t count, int32_t delay);
-
-/*
- * Send the Paste keyboard shortcut (CTRL+V)
- */
-extern "C" void trigger_paste();
-
-/*
- * Send the Paste keyboard shortcut (CTRL+SHIFT+V)
- */
-extern "C" void trigger_shift_paste();
-
-/*
- * Send the copy keyboard shortcut (CTRL+C)
- */
-extern "C" void trigger_copy();
-
-/*
- * Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
- */
-extern "C" int32_t are_modifiers_pressed();
-
-// Detect current application commands
-
-/*
- * Return the active windows's title
- */
-extern "C" int32_t get_active_window_name(wchar_t * buffer, int32_t size);
-
-/*
- * Return the active windows's executable path
- */
-extern "C" int32_t get_active_window_executable(wchar_t * buffer, int32_t size);
-
-// UI
-
-/*
- * Called when the tray icon is clicked
- */
-typedef void (*IconClickCallback)(void * self);
-extern IconClickCallback icon_click_callback;
-extern "C" void register_icon_click_callback(IconClickCallback callback);
-
-// CONTEXT MENU
-
-typedef struct {
-    int32_t id;
-    int32_t type;
-    wchar_t name[100];
-} MenuItem;
-
-extern "C" int32_t show_context_menu(MenuItem * items, int32_t count);
-
-/*
- * Called when the context menu is clicked
- */
-typedef void (*ContextMenuClickCallback)(void * self, int32_t id);
-extern ContextMenuClickCallback context_menu_click_callback;
-extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback);
-
-/*
- * Hide the tray icon
- */
-extern "C" void cleanup_ui();
-
-// NOTIFICATION
-
-/*
- * Show a window containing the notification.
- */
-extern "C" int32_t show_notification(wchar_t * message);
-
-/*
- * Close the notification if present
- */
-extern "C" void close_notification();
-
-/*
- * Update the tray icon status
- */
-extern "C" void update_tray_icon(int32_t enabled);
-
-// CLIPBOARD
-
-/*
- * Return the clipboard text
- */
-extern "C" int32_t get_clipboard(wchar_t * buffer, int32_t size);
-
-/*
- * Set the clipboard text
- */
-extern "C" int32_t set_clipboard(wchar_t * text);
-
-/*
- * Set the clipboard image to the given path
- */
-extern "C" int32_t set_clipboard_image(wchar_t * path);
-
-/*
- * Set clipboard HTML. Notice how in this case, text is not a wide char but instead
- * uses the UTF8 encoding.
- * Also set the text fallback, in case some applications don't support HTML clipboard.
- */
-extern "C" int32_t set_clipboard_html(char * html, wchar_t * text_fallback);
-
-// PROCESSES
-
-extern "C" int32_t start_process(wchar_t * cmd);
-
-#endif //ESPANSO_BRIDGE_H
\ No newline at end of file
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj
deleted file mode 100644
index a3fa5be3..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,312 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 50;
-	objects = {
-
-/* Begin PBXBuildFile section */
-		B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF15232283F8005233EB /* AppDelegate.m */; };
-		B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6F9DF17232283F8005233EB /* Assets.xcassets */; };
-		B6F9DF1E232283F8005233EB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF1D232283F8005233EB /* main.m */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
-		B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EspansoNotifyHelper.app; sourceTree = BUILT_PRODUCTS_DIR; };
-		B6F9DF14232283F8005233EB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
-		B6F9DF15232283F8005233EB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
-		B6F9DF17232283F8005233EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
-		B6F9DF1C232283F8005233EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		B6F9DF1D232283F8005233EB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
-		B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EspansoNotifyHelper.entitlements; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
-		B6F9DF0E232283F8005233EB /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
-		B6F9DF08232283F8005233EB = {
-			isa = PBXGroup;
-			children = (
-				B6F9DF13232283F8005233EB /* EspansoNotifyHelper */,
-				B6F9DF12232283F8005233EB /* Products */,
-			);
-			sourceTree = "<group>";
-		};
-		B6F9DF12232283F8005233EB /* Products */ = {
-			isa = PBXGroup;
-			children = (
-				B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */,
-			);
-			name = Products;
-			sourceTree = "<group>";
-		};
-		B6F9DF13232283F8005233EB /* EspansoNotifyHelper */ = {
-			isa = PBXGroup;
-			children = (
-				B6F9DF14232283F8005233EB /* AppDelegate.h */,
-				B6F9DF15232283F8005233EB /* AppDelegate.m */,
-				B6F9DF17232283F8005233EB /* Assets.xcassets */,
-				B6F9DF1C232283F8005233EB /* Info.plist */,
-				B6F9DF1D232283F8005233EB /* main.m */,
-				B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */,
-			);
-			path = EspansoNotifyHelper;
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
-		B6F9DF10232283F8005233EB /* EspansoNotifyHelper */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */;
-			buildPhases = (
-				B6F9DF0D232283F8005233EB /* Sources */,
-				B6F9DF0E232283F8005233EB /* Frameworks */,
-				B6F9DF0F232283F8005233EB /* Resources */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-			);
-			name = EspansoNotifyHelper;
-			productName = EspansoNotifyHelper;
-			productReference = B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */;
-			productType = "com.apple.product-type.application";
-		};
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
-		B6F9DF09232283F8005233EB /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastUpgradeCheck = 1010;
-				ORGANIZATIONNAME = "Federico Terzi";
-				TargetAttributes = {
-					B6F9DF10232283F8005233EB = {
-						CreatedOnToolsVersion = 10.1;
-					};
-				};
-			};
-			buildConfigurationList = B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */;
-			compatibilityVersion = "Xcode 9.3";
-			developmentRegion = en;
-			hasScannedForEncodings = 0;
-			knownRegions = (
-				en,
-				Base,
-			);
-			mainGroup = B6F9DF08232283F8005233EB;
-			productRefGroup = B6F9DF12232283F8005233EB /* Products */;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				B6F9DF10232283F8005233EB /* EspansoNotifyHelper */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
-		B6F9DF0F232283F8005233EB /* Resources */ = {
-			isa = PBXResourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
-		B6F9DF0D232283F8005233EB /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				B6F9DF1E232283F8005233EB /* main.m in Sources */,
-				B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
-		B6F9DF20232283F8005233EB /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_ENABLE_OBJC_WEAK = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_COMMA = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_STRICT_PROTOTYPES = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				CODE_SIGN_IDENTITY = "Mac Developer";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = dwarf;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				ENABLE_TESTABILITY = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu11;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_OPTIMIZATION_LEVEL = 0;
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.14;
-				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
-				MTL_FAST_MATH = YES;
-				ONLY_ACTIVE_ARCH = YES;
-				SDKROOT = macosx;
-			};
-			name = Debug;
-		};
-		B6F9DF21232283F8005233EB /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_ENABLE_OBJC_WEAK = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_COMMA = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_STRICT_PROTOTYPES = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				CODE_SIGN_IDENTITY = "Mac Developer";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu11;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.14;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				MTL_FAST_MATH = YES;
-				SDKROOT = macosx;
-			};
-			name = Release;
-		};
-		B6F9DF23232283F8005233EB /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements;
-				CODE_SIGN_STYLE = Automatic;
-				COMBINE_HIDPI_IMAGES = YES;
-				DEVELOPMENT_TEAM = N69XJWRM3X;
-				INFOPLIST_FILE = EspansoNotifyHelper/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/../Frameworks",
-				);
-				PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Debug;
-		};
-		B6F9DF24232283F8005233EB /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements;
-				CODE_SIGN_STYLE = Automatic;
-				COMBINE_HIDPI_IMAGES = YES;
-				DEVELOPMENT_TEAM = N69XJWRM3X;
-				INFOPLIST_FILE = EspansoNotifyHelper/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/../Frameworks",
-				);
-				PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				B6F9DF20232283F8005233EB /* Debug */,
-				B6F9DF21232283F8005233EB /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				B6F9DF23232283F8005233EB /* Debug */,
-				B6F9DF24232283F8005233EB /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = B6F9DF09232283F8005233EB /* Project object */;
-}
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index b73ee34c..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
-   version = "1.0">
-   <FileRef
-      location = "self:EspansoNotifyHelper.xcodeproj">
-   </FileRef>
-</Workspace>
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index 84373c4e..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme
deleted file mode 100644
index 69a385de..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Scheme
-   LastUpgradeVersion = "1010"
-   version = "1.3">
-   <BuildAction
-      parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES">
-      <BuildActionEntries>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B6F9DF10232283F8005233EB"
-               BuildableName = "EspansoNotifyHelper.app"
-               BlueprintName = "EspansoNotifyHelper"
-               ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-      </BuildActionEntries>
-   </BuildAction>
-   <TestAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "B6F9DF10232283F8005233EB"
-            BuildableName = "EspansoNotifyHelper.app"
-            BlueprintName = "EspansoNotifyHelper"
-            ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
-   </TestAction>
-   <LaunchAction
-      buildConfiguration = "Release"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      launchStyle = "0"
-      useCustomWorkingDirectory = "NO"
-      ignoresPersistentStateOnLaunch = "NO"
-      debugDocumentVersioning = "YES"
-      debugServiceExtension = "internal"
-      allowLocationSimulation = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "B6F9DF10232283F8005233EB"
-            BuildableName = "EspansoNotifyHelper.app"
-            BlueprintName = "EspansoNotifyHelper"
-            ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
-   </LaunchAction>
-   <ProfileAction
-      buildConfiguration = "Release"
-      shouldUseLaunchSchemeArgsEnv = "YES"
-      savedToolIdentifier = ""
-      useCustomWorkingDirectory = "NO"
-      debugDocumentVersioning = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "B6F9DF10232283F8005233EB"
-            BuildableName = "EspansoNotifyHelper.app"
-            BlueprintName = "EspansoNotifyHelper"
-            ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-   </ProfileAction>
-   <AnalyzeAction
-      buildConfiguration = "Debug">
-   </AnalyzeAction>
-   <ArchiveAction
-      buildConfiguration = "Release"
-      revealArchiveInOrganizer = "YES">
-   </ArchiveAction>
-</Scheme>
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist
deleted file mode 100644
index 396e6250..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>SchemeUserState</key>
-	<dict>
-		<key>EspansoNotifyHelper.xcscheme_^#shared#^_</key>
-		<dict>
-			<key>orderHint</key>
-			<integer>0</integer>
-		</dict>
-	</dict>
-	<key>SuppressBuildableAutocreation</key>
-	<dict>
-		<key>B6F9DF10232283F8005233EB</key>
-		<dict>
-			<key>primary</key>
-			<true/>
-		</dict>
-	</dict>
-</dict>
-</plist>
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m b/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m
deleted file mode 100644
index b076bcbd..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#import "AppDelegate.h"
-
-@interface AppDelegate ()
-
-@property (weak) IBOutlet NSWindow *window;
-@end
-
-@implementation AppDelegate
-
-- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
-    [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
-    
-    NSArray *args = [[NSProcessInfo processInfo] arguments];
-    
-    NSString *title = @"Title";
-    NSString *desc = @"Description";
-    double delay = 1.5;
-    
-    if ([args count] > 3) {
-        title = args[1];
-        desc = args[2];
-        delay = [args[3] doubleValue];
-    }
-    
-    NSUserNotification *notification = [[NSUserNotification alloc] init];
-    notification.title = title;
-    notification.informativeText = desc;
-    notification.soundName = nil;
-    
-    [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
-    
-    [[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay];
-    
-    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
-        NSRunningApplication *app = [NSRunningApplication currentApplication];
-        [app terminate];
-    });
-}
-
-
-- (void)applicationWillTerminate:(NSNotification *)aNotification {
-    // Insert code here to tear down your application
-}
-
-- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification{
-    return YES;
-}
-
-
-@end
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index ce3f3570..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
-  "images" : [
-    {
-      "size" : "16x16",
-      "idiom" : "mac",
-      "filename" : "icongreen-16.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "16x16",
-      "idiom" : "mac",
-      "filename" : "icongreen-32.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "32x32",
-      "idiom" : "mac",
-      "filename" : "icongreen-32.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "32x32",
-      "idiom" : "mac",
-      "filename" : "icongreen-64.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "128x128",
-      "idiom" : "mac",
-      "filename" : "icongreen-128.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "128x128",
-      "idiom" : "mac",
-      "filename" : "icongreen-256.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "256x256",
-      "idiom" : "mac",
-      "filename" : "icongreen-256.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "256x256",
-      "idiom" : "mac",
-      "filename" : "icongreen-512.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "512x512",
-      "idiom" : "mac",
-      "filename" : "icongreen-512.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "512x512",
-      "idiom" : "mac",
-      "filename" : "icongreen-1024.png",
-      "scale" : "2x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
\ No newline at end of file
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png
deleted file mode 100644
index 3f2555ad..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png
deleted file mode 100644
index 181c5e89..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png
deleted file mode 100644
index 355addc7..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png
deleted file mode 100644
index 7423ef91..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png
deleted file mode 100644
index 520331d0..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png
deleted file mode 100644
index 4bf13655..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png
deleted file mode 100644
index e5809ef3..00000000
Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png and /dev/null differ
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json
deleted file mode 100644
index da4a164c..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
\ No newline at end of file
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements b/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements
deleted file mode 100644
index f2ef3ae0..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.security.app-sandbox</key>
-    <true/>
-    <key>com.apple.security.files.user-selected.read-only</key>
-    <true/>
-</dict>
-</plist>
diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist
deleted file mode 100644
index 6b44964d..00000000
--- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>CFBundleDevelopmentRegion</key>
-	<string>$(DEVELOPMENT_LANGUAGE)</string>
-	<key>CFBundleExecutable</key>
-	<string>$(EXECUTABLE_NAME)</string>
-	<key>CFBundleIconFile</key>
-	<string></string>
-	<key>CFBundleIdentifier</key>
-	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-	<key>CFBundleInfoDictionaryVersion</key>
-	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>$(PRODUCT_NAME)</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleShortVersionString</key>
-	<string>1.0</string>
-	<key>CFBundleVersion</key>
-	<string>1</string>
-	<key>LSApplicationCategoryType</key>
-	<string>public.app-category.utilities</string>
-	<key>LSMinimumSystemVersion</key>
-	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
-	<key>NSHumanReadableCopyright</key>
-	<string>Copyright © 2019 Federico Terzi. All rights reserved.</string>
-	<key>NSMainNibFile</key>
-	<string>MainMenu</string>
-	<key>LSUIElement</key>
-	<true/>
-	<key>NSPrincipalClass</key>
-	<string>NSApplication</string>
-</dict>
-</plist>
diff --git a/packager/mac/espanso.rb b/packager/mac/espanso.rb
deleted file mode 100644
index 10f0e2bf..00000000
--- a/packager/mac/espanso.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# Documentation: https://docs.brew.sh/Formula-Cookbook
-#                https://rubydoc.brew.sh/Formula
-# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
-class Espanso < Formula
-  desc "{{{app_desc}}}"
-  homepage "{{{app_url}}}"
-  url "https://github.com/federico-terzi/espanso/releases/download/v{{{app_version}}}/espanso-mac.tar.gz"
-  sha256 "{{{release_hash}}}"
-  version "{{{app_version}}}"
-
-  resource "modulo" do
-    url "https://github.com/federico-terzi/modulo/releases/download/v{{{modulo_version}}}/modulo-mac"
-    sha256 "{{{modulo_sha}}}"
-  end
-
-  def install
-    bin.install "espanso"
-
-    resource("modulo").stage { bin.install "modulo-mac" => "modulo" }
-  end
-end
diff --git a/packager/win/icon.ico b/packager/win/icon.ico
deleted file mode 100644
index 1d1c06c7..00000000
Binary files a/packager/win/icon.ico and /dev/null differ
diff --git a/packager/win/modpath.iss b/packager/win/modpath.iss
deleted file mode 100644
index a48ae7b2..00000000
--- a/packager/win/modpath.iss
+++ /dev/null
@@ -1,219 +0,0 @@
-// ----------------------------------------------------------------------------
-//
-// Inno Setup Ver:	5.4.2
-// Script Version:	1.4.2
-// Author:			Jared Breland <jbreland@legroom.net>
-// Homepage:		http://www.legroom.net/software
-// License:			GNU Lesser General Public License (LGPL), version 3
-//						http://www.gnu.org/licenses/lgpl.html
-//
-// Script Function:
-//	Allow modification of environmental path directly from Inno Setup installers
-//
-// Instructions:
-//	Copy modpath.iss to the same directory as your setup script
-//
-//	Add this statement to your [Setup] section
-//		ChangesEnvironment=true
-//
-//	Add this statement to your [Tasks] section
-//	You can change the Description or Flags
-//	You can change the Name, but it must match the ModPathName setting below
-//		Name: modifypath; Description: &Add application directory to your environmental path; Flags: unchecked
-//
-//	Add the following to the end of your [Code] section
-//	ModPathName defines the name of the task defined above
-//	ModPathType defines whether the 'user' or 'system' path will be modified;
-//		this will default to user if anything other than system is set
-//	setArrayLength must specify the total number of dirs to be added
-//	Result[0] contains first directory, Result[1] contains second, etc.
-//		const
-//			ModPathName = 'modifypath';
-//			ModPathType = 'user';
-//
-//		function ModPathDir(): TArrayOfString;
-//		begin
-//			setArrayLength(Result, 1);
-//			Result[0] := ExpandConstant('{app}');
-//		end;
-//		#include "modpath.iss"
-// ----------------------------------------------------------------------------
-
-procedure ModPath();
-var
-	oldpath:	String;
-	newpath:	String;
-	updatepath:	Boolean;
-	pathArr:	TArrayOfString;
-	aExecFile:	String;
-	aExecArr:	TArrayOfString;
-	i, d:		Integer;
-	pathdir:	TArrayOfString;
-	regroot:	Integer;
-	regpath:	String;
-
-begin
-	// Get constants from main script and adjust behavior accordingly
-	// ModPathType MUST be 'system' or 'user'; force 'user' if invalid
-	if ModPathType = 'system' then begin
-		regroot := HKEY_LOCAL_MACHINE;
-		regpath := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
-	end else begin
-		regroot := HKEY_CURRENT_USER;
-		regpath := 'Environment';
-	end;
-
-	// Get array of new directories and act on each individually
-	pathdir := ModPathDir();
-	for d := 0 to GetArrayLength(pathdir)-1 do begin
-		updatepath := true;
-
-		// Modify WinNT path
-		if UsingWinNT() = true then begin
-
-			// Get current path, split into an array
-			RegQueryStringValue(regroot, regpath, 'Path', oldpath);
-			oldpath := oldpath + ';';
-			i := 0;
-
-			while (Pos(';', oldpath) > 0) do begin
-				SetArrayLength(pathArr, i+1);
-				pathArr[i] := Copy(oldpath, 0, Pos(';', oldpath)-1);
-				oldpath := Copy(oldpath, Pos(';', oldpath)+1, Length(oldpath));
-				i := i + 1;
-
-				// Check if current directory matches app dir
-				if pathdir[d] = pathArr[i-1] then begin
-					// if uninstalling, remove dir from path
-					if IsUninstaller() = true then begin
-						continue;
-					// if installing, flag that dir already exists in path
-					end else begin
-						updatepath := false;
-					end;
-				end;
-
-				// Add current directory to new path
-				if i = 1 then begin
-					newpath := pathArr[i-1];
-				end else begin
-					newpath := newpath + ';' + pathArr[i-1];
-				end;
-			end;
-
-			// Append app dir to path if not already included
-			if (IsUninstaller() = false) AND (updatepath = true) then
-				newpath := newpath + ';' + pathdir[d];
-
-			// Write new path
-			RegWriteStringValue(regroot, regpath, 'Path', newpath);
-
-		// Modify Win9x path
-		end else begin
-
-			// Convert to shortened dirname
-			pathdir[d] := GetShortName(pathdir[d]);
-
-			// If autoexec.bat exists, check if app dir already exists in path
-			aExecFile := 'C:\AUTOEXEC.BAT';
-			if FileExists(aExecFile) then begin
-				LoadStringsFromFile(aExecFile, aExecArr);
-				for i := 0 to GetArrayLength(aExecArr)-1 do begin
-					if IsUninstaller() = false then begin
-						// If app dir already exists while installing, skip add
-						if (Pos(pathdir[d], aExecArr[i]) > 0) then
-							updatepath := false;
-							break;
-					end else begin
-						// If app dir exists and = what we originally set, then delete at uninstall
-						if aExecArr[i] = 'SET PATH=%PATH%;' + pathdir[d] then
-							aExecArr[i] := '';
-					end;
-				end;
-			end;
-
-			// If app dir not found, or autoexec.bat didn't exist, then (create and) append to current path
-			if (IsUninstaller() = false) AND (updatepath = true) then begin
-				SaveStringToFile(aExecFile, #13#10 + 'SET PATH=%PATH%;' + pathdir[d], True);
-
-			// If uninstalling, write the full autoexec out
-			end else begin
-				SaveStringsToFile(aExecFile, aExecArr, False);
-			end;
-		end;
-	end;
-end;
-
-// Split a string into an array using passed delimeter
-procedure MPExplode(var Dest: TArrayOfString; Text: String; Separator: String);
-var
-	i: Integer;
-begin
-	i := 0;
-	repeat
-		SetArrayLength(Dest, i+1);
-		if Pos(Separator,Text) > 0 then	begin
-			Dest[i] := Copy(Text, 1, Pos(Separator, Text)-1);
-			Text := Copy(Text, Pos(Separator,Text) + Length(Separator), Length(Text));
-			i := i + 1;
-		end else begin
-			 Dest[i] := Text;
-			 Text := '';
-		end;
-	until Length(Text)=0;
-end;
-
-
-procedure CurStepChanged(CurStep: TSetupStep);
-var
-	taskname:	String;
-begin
-	taskname := ModPathName;
-	if CurStep = ssPostInstall then
-		if WizardIsTaskSelected(taskname) then
-			ModPath();
-end;
-
-procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
-var
-	aSelectedTasks:	TArrayOfString;
-	i:				Integer;
-	taskname:		String;
-	regpath:		String;
-	regstring:		String;
-	appid:			String;
-begin
-	// only run during actual uninstall
-	if CurUninstallStep = usUninstall then begin
-		// get list of selected tasks saved in registry at install time
-		appid := '{#emit SetupSetting("AppId")}';
-		if appid = '' then appid := '{#emit SetupSetting("AppName")}';
-		regpath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\'+appid+'_is1');
-		RegQueryStringValue(HKLM, regpath, 'Inno Setup: Selected Tasks', regstring);
-		if regstring = '' then RegQueryStringValue(HKCU, regpath, 'Inno Setup: Selected Tasks', regstring);
-
-		// check each task; if matches modpath taskname, trigger patch removal
-		if regstring <> '' then begin
-			taskname := ModPathName;
-			MPExplode(aSelectedTasks, regstring, ',');
-			if GetArrayLength(aSelectedTasks) > 0 then begin
-				for i := 0 to GetArrayLength(aSelectedTasks)-1 do begin
-					if comparetext(aSelectedTasks[i], taskname) = 0 then
-						ModPath();
-				end;
-			end;
-		end;
-	end;
-end;
-
-function NeedRestart(): Boolean;
-var
-	taskname:	String;
-begin
-	taskname := ModPathName;
-	if IsTaskSelected(taskname) and not UsingWinNT() then begin
-		Result := True;
-	end else begin
-		Result := False;
-	end;
-end;
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 00000000..6f2e0751
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,1 @@
+tab_spaces = 2
\ No newline at end of file
diff --git a/scripts/build_binary.rs b/scripts/build_binary.rs
new file mode 100644
index 00000000..fa316edb
--- /dev/null
+++ b/scripts/build_binary.rs
@@ -0,0 +1,85 @@
+//! ```cargo
+//! [dependencies]
+//! envmnt = "*"
+//! ```
+
+use std::process::Command;
+
+#[derive(Debug, PartialEq)]
+enum Profile {
+  Debug,
+  Release,
+}
+
+fn main() {
+  let profile = if envmnt::get_or_panic("RELEASE") == "true" {
+    Profile::Release
+  } else {
+    Profile::Debug
+  };
+
+  println!("Using profile: {:?}", profile);
+
+  let wayland = envmnt::get_or("NO_X11", "false") == "true";
+  if wayland {
+    println!("Using Wayland feature");
+  }
+
+  let avoid_modulo = envmnt::get_or("NO_MODULO", "false") == "true";
+  if avoid_modulo {
+    println!("Skipping modulo feature");
+  }
+
+  let mut args = Vec::new();
+  args.push("build");
+
+  if profile == Profile::Release {
+    args.push("--release");
+  }
+
+  let override_target_arch = envmnt::get_or("BUILD_ARCH", "current");
+  if override_target_arch != "current" {
+    args.push("--target");
+    args.push(&override_target_arch);
+  }
+
+  args.push("--manifest-path");
+  args.push("espanso/Cargo.toml");
+
+  let mut features = Vec::new();
+  if wayland {
+    features.push("wayland");
+  }
+  if !avoid_modulo {
+    features.push("modulo");
+  }
+
+  let features_flag = features.join(" ");
+
+  args.push("-p");
+  args.push("espanso");
+  args.push("--no-default-features");
+  args.push("--features");
+  args.push(&features_flag);
+
+  println!("Calling with args: {:?}", args);
+
+  let mut cmd = Command::new("cargo");
+  cmd.args(&args);
+
+  // Remove cargo/rust-specific env variables, as otherwise they mess up the
+  // nested cargo build call.
+  let all_vars = envmnt::vars();
+  for (key, _) in all_vars {
+    if key.starts_with("CARGO") || (key.starts_with("RUST") && !key.starts_with("RUSTUP")) {
+      //println!("Removing {}", key);
+      cmd.env_remove(key);
+    }
+  }
+
+  let mut handle = cmd.spawn().expect("cargo build failed");
+  let result = handle.wait().expect("unable to read cargo exit status");
+  if !result.success() {
+    panic!("cargo build failed");
+  }
+}
diff --git a/scripts/build_windows_installer.rs b/scripts/build_windows_installer.rs
new file mode 100644
index 00000000..ac6969e3
--- /dev/null
+++ b/scripts/build_windows_installer.rs
@@ -0,0 +1,158 @@
+//! ```cargo
+//! [dependencies]
+//! glob = "0.3.0"
+//! envmnt = "*"
+//! fs_extra = "1.2.0"
+//! toml = "0.5.8"
+//! dunce = "1.0.2"
+//! ```
+
+use std::path::PathBuf;
+use std::process::Command;
+use toml::Value;
+
+const INSTALLER_NAME: &str = "Espanso-Win-Installer";
+
+const TARGET_DIR: &str = "target/windows/installer";
+const RESOURCE_DIR: &str = "target/windows/resources";
+
+fn main() {
+  // Clean the target directory
+  let _ = std::fs::remove_dir_all(TARGET_DIR);
+  // Create the target directory
+  std::fs::create_dir_all(TARGET_DIR).expect("unable to create target directory");
+  let target_dir = PathBuf::from(TARGET_DIR);
+  if !target_dir.is_dir() {
+    panic!("expected target directory, found none");
+  }
+
+  let resources_dir = PathBuf::from(RESOURCE_DIR);
+  if !resources_dir.is_dir() {
+    panic!("expected resources dir, found none");
+  }
+
+  // Check InnoSetup
+  Command::new("iscc").output().expect("Could not find Inno Setup compiler. Please install it from here: http://www.jrsoftware.org/isdl.php");
+
+  // Read the InnoSetup template
+  let makefile_path = envmnt::get_or_panic("CARGO_MAKE_MAKEFILE_PATH");
+  let makefile_path = PathBuf::from(makefile_path);
+  let project_path = makefile_path
+    .parent()
+    .expect("unable to inferproject directory");
+  let script_resources_path = project_path
+    .join("scripts")
+    .join("resources")
+    .join("windows");
+  let template_path = script_resources_path.join("setupscript.iss");
+  if !template_path.is_file() {
+    panic!("InnoSetup template not found");
+  }
+
+  let template = std::fs::read_to_string(template_path).expect("unable to read InnoSetup template");
+
+  let espanso_toml_path = project_path.join("espanso").join("Cargo.toml");
+  if !espanso_toml_path.is_file() {
+    panic!("could not find espanso Cargo.toml file");
+  }
+
+  let espanso_toml_str =
+    std::fs::read_to_string(espanso_toml_path).expect("unable to read Cargo.toml file");
+  let espanso_toml = espanso_toml_str
+    .parse::<Value>()
+    .expect("unable to parse Cargo.toml");
+  
+  let arch = envmnt::get_or_panic("BUILD_ARCH");
+  let arch = if arch == "current" {
+    std::env::consts::ARCH
+  } else {
+    &arch
+  };
+
+  // Populating template variables
+  let mut iss_setup = template;
+  iss_setup = iss_setup.replace(
+    "{{{app_version}}}",
+    espanso_toml["package"].as_table().unwrap()["version"]
+      .as_str()
+      .unwrap(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{app_url}}}",
+    espanso_toml["package"].as_table().unwrap()["homepage"]
+      .as_str()
+      .unwrap(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{app_license}}}",
+    &dunce::canonicalize(project_path.join("LICENSE"))
+      .unwrap()
+      .to_string_lossy()
+      .to_string(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{app_icon}}}",
+    &dunce::canonicalize(script_resources_path.join("icon.ico"))
+      .unwrap()
+      .to_string_lossy()
+      .to_string(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{cli_helper}}}",
+    &dunce::canonicalize(script_resources_path.join("espanso.cmd"))
+      .unwrap()
+      .to_string_lossy()
+      .to_string(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{output_dir}}}",
+    &dunce::canonicalize(TARGET_DIR)
+      .unwrap()
+      .to_string_lossy()
+      .to_string(),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{output_name}}}",
+    &format!("{}-{}", INSTALLER_NAME, arch),
+  );
+  iss_setup = iss_setup.replace(
+    "{{{executable_path}}}",
+    &dunce::canonicalize(&format!("{}.exe", envmnt::get_or_panic("EXEC_PATH")))
+      .unwrap()
+      .to_string_lossy()
+      .to_string(),
+  );
+
+  // Generate file includes
+  let mut include_paths = Vec::new();
+  for entry in glob::glob(&format!(
+    r"{}\*.dll",
+    resources_dir.to_string_lossy().to_string()
+  ))
+  .expect("unable to glob over DLLs")
+  {
+    let entry = entry.expect("unable to unwrap DLL entry");
+    include_paths.push(format!(
+      "Source: \"{}\"; DestDir: \"{{app}}\"; Flags: ignoreversion",
+      dunce::canonicalize(&entry)
+        .unwrap()
+        .to_string_lossy()
+        .to_string()
+    ));
+  }
+
+  iss_setup = iss_setup.replace("{{{dll_include}}}", &include_paths.join("\r\n"));
+
+  let iss_setup_path = target_dir.join("setupscript.iss");
+  std::fs::write(&iss_setup_path, &iss_setup).expect("unable to write InnoSetup setup script");
+
+  // Compile the installer
+
+  let status = Command::new("iscc")
+    .arg(&iss_setup_path.to_string_lossy().to_string())
+    .status()
+    .expect("unable to invoke InnoSetup compilation");
+  if !status.success() {
+    panic!("InnoSetup compilation process returned non-zero exit code");
+  }
+}
diff --git a/scripts/build_windows_portable.rs b/scripts/build_windows_portable.rs
new file mode 100644
index 00000000..a98e61a5
--- /dev/null
+++ b/scripts/build_windows_portable.rs
@@ -0,0 +1,64 @@
+//! ```cargo
+//! [dependencies]
+//! glob = "0.3.0"
+//! envmnt = "*"
+//! fs_extra = "1.2.0"
+//! ```
+
+use std::path::PathBuf;
+
+const TARGET_DIR: &str = "target/windows/portable";
+const RESOURCE_DIR: &str = "target/windows/resources";
+
+fn main() {
+  // Clean the target directory
+  let _ = std::fs::remove_dir_all(TARGET_DIR);
+  // Create the target directory
+  std::fs::create_dir_all(TARGET_DIR).expect("unable to create target directory");
+  let target_dir = PathBuf::from(TARGET_DIR);
+  if !target_dir.is_dir() {
+    panic!("expected target directory, found none");
+  }
+
+  let resources_dir = PathBuf::from(RESOURCE_DIR);
+  if !resources_dir.is_dir() {
+    panic!("expected resources dir, found none");
+  }
+
+  // Copy all the resources
+  fs_extra::dir::copy(&resources_dir, &target_dir, &fs_extra::dir::CopyOptions {
+    content_only: true,
+    ..Default::default()
+  }).expect("unable to copy resources");
+
+  // Create the launcher
+  std::fs::write(target_dir.join("START_ESPANSO.bat"), r#"start espansod.exe launcher"#).unwrap();
+
+  // Create the necessary folders
+  std::fs::create_dir_all(target_dir.join(".espanso")).expect("unable to create data directory");
+  std::fs::create_dir_all(target_dir.join(".espanso-runtime")).expect("unable to create runtime directory");
+
+  std::fs::write(target_dir.join("README.txt"), r##"Welcome to Espanso (Portable edition)!
+
+To start espanso, you can double click on "START_ESPANSO.bat"  
+
+After the first run, you will see some files in the ".espanso" directory.
+This is where your snippets and configurations should be defined.
+
+For more information, please visit the official documentation: 
+https://espanso.org/docs/
+
+IMPORTANT: Don't delete any file or directory, otherwise espanso won't work.
+
+
+FOR ADVANCED USERS:  
+
+Espanso also offers a rich CLI interface. To start it from the terminal, cd into the 
+current directory and run "espanso start". You can also run "espanso --help" for more information.
+
+You might have noticed that the directory contains both an "espansod.exe" and an "espanso.cmd" file.
+You should generally avoid running "espansod.exe" directly, and instead use the "espanso.cmd"
+wrapper (which can simply be run as "espanso" in the terminal). This is needed to correctly manage
+STD console handles on Windows.
+  "##).unwrap();
+}
diff --git a/scripts/build_windows_resources.rs b/scripts/build_windows_resources.rs
new file mode 100644
index 00000000..0c11750d
--- /dev/null
+++ b/scripts/build_windows_resources.rs
@@ -0,0 +1,76 @@
+//! ```cargo
+//! [dependencies]
+//! cc = "1.0.66"
+//! glob = "0.3.0"
+//! envmnt = "*"
+//! ```
+
+use std::path::PathBuf;
+
+const TARGET_DIR: &str = "target/windows/resources";
+
+fn main() {
+  // Clean the target directory
+  let _ = std::fs::remove_dir_all(TARGET_DIR);
+  // Create the target directory
+  std::fs::create_dir_all(TARGET_DIR).expect("unable to create target directory");
+  let target_dir = PathBuf::from(TARGET_DIR);
+  if !target_dir.is_dir() {
+    panic!("expected target directory, found none");
+  }
+
+  // We first need to find all the DLLs to redistribute with the binary
+  // These are found inside the MSVC compiler directory, usually in a place like:
+  // C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC\14.28.29910\x64\Microsoft.VC142.CRT
+  // and they include files like vcruntime140.dll and msvcp140.dll
+
+  // First, we try to find the directory containing the various versions:
+  // C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC\
+  let tool = cc::windows_registry::find_tool("msvc", "msbuild")
+    .expect("unable to locate MSVC compiler, did you install Visual Studio?");
+  let mut versions_dir = None;
+  let mut current_root = tool.path();
+  while let Some(parent) = current_root.parent() {
+    let target = parent.join("VC").join("Redist").join("MSVC");
+    if target.exists() {
+      versions_dir = Some(target);
+      break;
+    }
+    current_root = parent;
+  }
+  let versions_dir = versions_dir.expect(
+    r"unable to find path: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC",
+  );
+
+  // Then we try to find a suitable directory containing the required DLLs
+  // C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC\14.28.29910\x64\Microsoft.VC142.CRT
+  let glob_pattern = format!(
+    r"{}\*\x64\Microsoft.*\vcruntime140_1.dll",
+    versions_dir.to_string_lossy().to_string()
+  );
+  println!("glob_pattern: {}", glob_pattern);
+  let target_file = glob::glob(&glob_pattern)
+    .expect("failed to read glob pattern")
+    .next()
+    .expect("unable to find vcruntime140_1.dll file")
+    .expect("unable to extract path of vcruntime140_1.dll file");
+  
+  // Copy the DLLs in the target directory
+  let parent_dir = target_file.parent().expect("unable to obtain directory containing DLLs");
+  for entry in glob::glob(&format!(r"{}\*.dll", parent_dir.to_string_lossy().to_string())).expect("unable to glob over DLLs") {
+    let entry = entry.expect("unable to unwrap DLL entry");
+    let filename = entry.file_name().expect("unable to obtain filename");
+    std::fs::copy(&entry, target_dir.join(filename)).expect("unable to copy DLL");
+  } 
+
+  // Copy the executable
+  let exec_path = envmnt::get_or_panic("EXEC_PATH");
+  let exec_file = PathBuf::from(&format!("{}.exe", exec_path));
+  if !exec_file.is_file() {
+    panic!("expected espanso binary, found none in {:?}", exec_file);
+  }
+  std::fs::copy(exec_file, target_dir.join("espansod.exe")).unwrap();
+
+  // Create the CLI wrapper
+  std::fs::write(target_dir.join("espanso.cmd"), r#"@"%~dp0espansod.exe" %*"#).unwrap();
+}
diff --git a/scripts/create_app_image.sh b/scripts/create_app_image.sh
new file mode 100644
index 00000000..4779363a
--- /dev/null
+++ b/scripts/create_app_image.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -e
+TOOL_DIR=$(pwd)/target/linux/linuxdeploy
+TARGET_DIR=$(pwd)/target/linux/AppImage
+BUILD_DIR=$TARGET_DIR/build
+OUTPUT_DIR=$TARGET_DIR/out
+BASE_DIR=$(pwd)
+
+mkdir -p $TOOL_DIR
+
+if ls $TOOL_DIR/*.AppImage 1> /dev/null 2>&1; then
+  echo "Skipping download of linuxdeploy"
+else
+  echo "Downloading linuxdeploy tool"
+  wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -P "$TOOL_DIR"
+  chmod +x $TOOL_DIR/linuxdeploy*.AppImage
+fi
+
+rm -Rf "$TARGET_DIR"
+mkdir -p $OUTPUT_DIR
+mkdir -p $BUILD_DIR
+
+echo Building AppImage into $OUTPUT_DIR
+pushd $OUTPUT_DIR
+$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" -i "$BASE_DIR/espanso/src/res/linux/icon.png" --appdir $BUILD_DIR --output appimage
+chmod +x ./Espanso*.AppImage
+popd
\ No newline at end of file
diff --git a/scripts/create_bundle.sh b/scripts/create_bundle.sh
new file mode 100644
index 00000000..e1ee867e
--- /dev/null
+++ b/scripts/create_bundle.sh
@@ -0,0 +1,24 @@
+set -e
+
+FINAL_EXEC_PATH=$EXEC_PATH
+
+if [ $BUILD_ARCH != "current" ]; then
+  FINAL_EXEC_PATH=$(echo $EXEC_PATH | sed "s/target\//target\/$BUILD_ARCH\//g")
+fi
+
+TARGET_DIR=target/mac/Espanso.app
+
+rm -Rf $TARGET_DIR
+
+VERSION=$(cat espanso/Cargo.toml | grep version | head -1 | awk -F '"' '{ print $2 }')
+
+mkdir -p $TARGET_DIR/Contents
+mkdir -p $TARGET_DIR/Contents/MacOS
+mkdir -p $TARGET_DIR/Contents/Resources
+
+sed	-e "s/VERSION/$VERSION/" espanso/src/res/macos/Info.plist > $TARGET_DIR/Contents/Info.plist
+
+/bin/echo "APPL????" > $TARGET_DIR/Contents/PkgInfo
+
+cp -f espanso/src/res/macos/icon.icns $TARGET_DIR/Contents/Resources/icon.icns
+cp -f $FINAL_EXEC_PATH $TARGET_DIR/Contents/MacOS/espanso
\ No newline at end of file
diff --git a/scripts/join_universal_binary.sh b/scripts/join_universal_binary.sh
new file mode 100644
index 00000000..91919615
--- /dev/null
+++ b/scripts/join_universal_binary.sh
@@ -0,0 +1,9 @@
+set -e
+
+TARGET_DIR=target/universal
+
+rm -Rf $TARGET_DIR
+
+mkdir -p $TARGET_DIR
+
+lipo -create -output "$TARGET_DIR/espanso" ./target/x86_64-apple-darwin/release/espanso ./target/aarch64-apple-darwin/release/espanso
\ No newline at end of file
diff --git a/scripts/resources/windows/espanso.cmd b/scripts/resources/windows/espanso.cmd
new file mode 100644
index 00000000..8654a10d
--- /dev/null
+++ b/scripts/resources/windows/espanso.cmd
@@ -0,0 +1,1 @@
+@"%~dp0espansod.exe" %*
\ No newline at end of file
diff --git a/scripts/resources/windows/icon.ico b/scripts/resources/windows/icon.ico
new file mode 100644
index 00000000..42580669
Binary files /dev/null and b/scripts/resources/windows/icon.ico differ
diff --git a/packager/win/setupscript.iss b/scripts/resources/windows/setupscript.iss
similarity index 52%
rename from packager/win/setupscript.iss
rename to scripts/resources/windows/setupscript.iss
index b177a9d3..60a8a0c6 100644
--- a/packager/win/setupscript.iss
+++ b/scripts/resources/windows/setupscript.iss
@@ -1,11 +1,12 @@
 ; Script generated by the Inno Setup Script Wizard.
 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
 
-#define MyAppName "{{{app_name}}}"
+#define MyAppName "Espanso"
 #define MyAppVersion "{{{app_version}}}"
-#define MyAppPublisher "{{{app_publisher}}}"
+#define MyAppPublisher "Federico Terzi"
 #define MyAppURL "{{{app_url}}}"
-#define MyAppExeName "espanso.exe"
+#define MyAppExeName "espansod.exe"
+#define MyAppUserModelID "{{5E3B6C0F-1A4D-45C4-8872-D8174702101A}"
 
 [Setup]
 ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
@@ -35,31 +36,34 @@ ChangesEnvironment=yes
 Name: "english"; MessagesFile: "compiler:Default.isl"
 
 [Files]
-Source: "{{{executable_path}}}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{{{executable_path}}}"; DestDir: "{app}"; DestName: "espansod.exe"; Flags: ignoreversion
 Source: "{{{app_icon}}}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{{{cli_helper}}}"; DestDir: "{app}"; Flags: ignoreversion
 {{{dll_include}}}
+
 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "start"; IconFilename: "{app}\icon.ico"
-Name: "{userstartup}\espanso"; Filename: "{app}\espanso.exe"; Parameters: "start"; Tasks:StartMenuEntry;
+; Delete files from previous versions, if present
+[InstallDelete]
+Type: files; Name: {app}\concrt140.dll
+Type: files; Name: {app}\espanso.exe
+Type: files; Name: {app}\icon.ico
+Type: files; Name: {app}\modulo.exe
+Type: files; Name: {app}\msvcp140.dll
+Type: files; Name: {app}\msvcp140_1.dll
+Type: files; Name: {app}\msvcp140_2.dll
+Type: files; Name: {app}\msvcp140_codecvt_ids.dll
+Type: files; Name: {app}\unins000.dat
+Type: files; Name: {app}\unins000.exe
+Type: files; Name: {app}\vccorlib140.dll
+Type: files; Name: {app}\vcruntime140.dll
+Type: files; Name: {app}\vsruntime140_1.dll
 
-[Tasks]
-Name: modifypath; Description: Add espanso to PATH ( recommended );
-Name: "StartMenuEntry" ; Description: "Start espanso at Windows startup" ;
+[Icons]
+Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "launcher"; AppUserModelID: "{#MyAppUserModelID}"; IconFilename: "{app}\icon.ico";
 
-[Code]
-const
-    ModPathName = 'modifypath';
-    ModPathType = 'user';
-function ModPathDir(): TArrayOfString;
-begin
-    setArrayLength(Result, 1)
-    Result[0] := ExpandConstant('{app}');
-end;
-#include "modpath.iss"
 [Run]
-Filename: "{app}\{#MyAppExeName}"; Parameters: "start"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+Filename: "{app}\{#MyAppExeName}"; Parameters: "launcher"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
 
 [UninstallRun]
-Filename: "{cmd}"; Parameters: "/C ""taskkill /im espanso.exe /f /t"
\ No newline at end of file
+Filename: "{cmd}"; Parameters: "/C ""taskkill /im espansod.exe /f /t"
\ No newline at end of file
diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs
deleted file mode 100644
index 74562ea8..00000000
--- a/src/bridge/linux.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::os::raw::{c_char, c_void};
-
-#[allow(improper_ctypes)]
-#[link(name = "linuxbridge", kind = "static")]
-extern "C" {
-    pub fn check_x11() -> i32;
-    pub fn initialize(s: *const c_void) -> i32;
-    pub fn eventloop();
-    pub fn cleanup();
-
-    // System
-    pub fn get_active_window_name(buffer: *mut c_char, size: i32) -> i32;
-    pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32;
-    pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32;
-    pub fn is_current_window_special() -> i32;
-    pub fn register_error_callback(
-        cb: extern "C" fn(
-            _self: *mut c_void,
-            error_code: c_char,
-            request_code: c_char,
-            minor_code: c_char,
-        ),
-    );
-
-    // Keyboard
-    pub fn register_keypress_callback(
-        cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
-    );
-
-    pub fn send_string(string: *const c_char);
-    pub fn delete_string(count: i32);
-    pub fn left_arrow(count: i32);
-    pub fn send_enter();
-    pub fn trigger_paste();
-    pub fn trigger_terminal_paste();
-    pub fn trigger_shift_ins_paste();
-    pub fn trigger_alt_shift_ins_paste();
-    pub fn trigger_ctrl_alt_paste();
-    pub fn trigger_copy();
-
-    pub fn fast_send_string(string: *const c_char, delay: i32);
-    pub fn fast_delete_string(count: i32, delay: i32);
-    pub fn fast_left_arrow(count: i32);
-    pub fn fast_send_enter();
-}
diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs
deleted file mode 100644
index fd6dc44c..00000000
--- a/src/bridge/macos.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::os::raw::{c_char, c_void};
-
-#[repr(C)]
-pub struct MacMenuItem {
-    pub item_id: i32,
-    pub item_type: i32,
-    pub item_name: [c_char; 100],
-}
-
-#[allow(improper_ctypes)]
-#[link(name = "macbridge", kind = "static")]
-extern "C" {
-    pub fn initialize(
-        s: *const c_void,
-        icon_path: *const c_char,
-        disabled_icon_path: *const c_char,
-        show_icon: i32,
-    );
-    pub fn eventloop();
-    pub fn headless_eventloop();
-
-    // System
-    pub fn check_accessibility() -> i32;
-    pub fn prompt_accessibility() -> i32;
-    pub fn open_settings_panel();
-    pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32;
-    pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32;
-    pub fn get_secure_input_process(pid: *mut i64) -> i32;
-    pub fn get_path_from_pid(pid: i64, buffer: *mut c_char, size: i32) -> i32;
-
-    // Clipboard
-    pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;
-    pub fn set_clipboard(text: *const c_char) -> i32;
-    pub fn set_clipboard_image(path: *const c_char) -> i32;
-    pub fn set_clipboard_html(html: *const c_char, text_fallback: *const c_char) -> i32;
-
-    // UI
-    pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
-    pub fn show_context_menu(items: *const MacMenuItem, count: i32) -> i32;
-    pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
-    pub fn update_tray_icon(enabled: i32);
-
-    // Keyboard
-    pub fn register_keypress_callback(
-        cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
-    );
-
-    pub fn send_string(string: *const c_char);
-    pub fn send_vkey(vk: i32);
-    pub fn send_multi_vkey(vk: i32, count: i32);
-    pub fn delete_string(count: i32);
-    pub fn trigger_paste();
-    pub fn trigger_copy();
-    pub fn are_modifiers_pressed() -> i32;
-}
diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs
deleted file mode 100644
index 470dbfd8..00000000
--- a/src/bridge/windows.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::os::raw::{c_char, c_void};
-
-#[repr(C)]
-pub struct WindowsMenuItem {
-    pub item_id: i32,
-    pub item_type: i32,
-    pub item_name: [u16; 100],
-}
-
-#[allow(improper_ctypes)]
-#[link(name = "winbridge", kind = "static")]
-extern "C" {
-    pub fn start_daemon_process() -> i32;
-    pub fn initialize(
-        s: *const c_void,
-        ico_path: *const u16,
-        red_ico_path: *const u16,
-        bmp_path: *const u16,
-        show_icon: i32,
-    ) -> i32;
-
-    // SYSTEM
-    pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32;
-    pub fn get_active_window_executable(buffer: *mut u16, size: i32) -> i32;
-
-    // UI
-    pub fn show_notification(message: *const u16) -> i32;
-    pub fn close_notification();
-    pub fn show_context_menu(items: *const WindowsMenuItem, count: i32) -> i32;
-    pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
-    pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
-    pub fn cleanup_ui();
-    pub fn update_tray_icon(enabled: i32);
-
-    // CLIPBOARD
-    pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
-    pub fn set_clipboard(payload: *const u16) -> i32;
-    pub fn set_clipboard_image(path: *const u16) -> i32;
-    pub fn set_clipboard_html(html: *const c_char, text_fallback: *const u16) -> i32;
-
-    // KEYBOARD
-    pub fn register_keypress_callback(
-        cb: extern "C" fn(_self: *mut c_void, *const u16, i32, i32, i32, i32, i32),
-    );
-
-    pub fn eventloop();
-    pub fn send_string(string: *const u16);
-    pub fn send_vkey(vk: i32);
-    pub fn send_multi_vkey(vk: i32, count: i32);
-    pub fn delete_string(count: i32, delay: i32);
-    pub fn trigger_paste();
-    pub fn trigger_shift_paste();
-    pub fn trigger_copy();
-    pub fn are_modifiers_pressed() -> i32;
-
-    // PROCESSES
-
-    pub fn start_process(cmd: *const u16) -> i32;
-}
diff --git a/src/check.rs b/src/check.rs
deleted file mode 100644
index 414efbc9..00000000
--- a/src/check.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-// This functions are used to check if the required dependencies and conditions are satisfied
-// before starting espanso
-
-#[cfg(target_os = "linux")]
-pub fn check_preconditions() -> bool {
-    use std::process::Command;
-
-    let mut result = true;
-
-    // Make sure notify-send is installed
-    let status = Command::new("notify-send").arg("-v").output();
-    if status.is_err() {
-        println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it.");
-        result = false;
-    }
-
-    // Make sure xclip is installed
-    let status = Command::new("xclip").arg("-version").output();
-    if status.is_err() {
-        println!(
-            "Error: 'xclip' command is needed for espanso to work correctly, please install it."
-        );
-        result = false;
-    }
-
-    result
-}
-
-#[cfg(target_os = "macos")]
-pub fn check_preconditions() -> bool {
-    // Make sure no app is currently using secure input.
-    let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application();
-
-    if let Some((app_name, process)) = secure_input_app {
-        eprintln!("WARNING: An application is currently using SecureInput and might prevent espanso from working correctly.");
-        eprintln!();
-        eprintln!("APP: {}", app_name);
-        eprintln!("PROC: {}", process);
-        eprintln!();
-        eprintln!("Please close it or disable SecureInput for that application (most apps that use it have a");
-        eprintln!("setting to disable it).");
-        eprintln!("Until then, espanso might not work as expected.");
-    }
-
-    true
-}
-
-#[cfg(target_os = "windows")]
-pub fn check_preconditions() -> bool {
-    // Nothing needed on windows
-    true
-}
diff --git a/src/cli.rs b/src/cli.rs
deleted file mode 100644
index f2fc91b9..00000000
--- a/src/cli.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::config::ConfigSet;
-use crate::matcher::{Match, MatchContentType};
-use serde::Serialize;
-
-pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines: bool) {
-    let matches = filter_matches(config_set);
-
-    for m in matches {
-        for trigger in m.triggers.iter() {
-            if onlytriggers {
-                println!("{}", trigger);
-            } else {
-                match m.content {
-                    MatchContentType::Text(ref text) => {
-                        let replace = if preserve_newlines {
-                            text.replace.to_owned()
-                        } else {
-                            text.replace.replace("\n", " ")
-                        };
-                        println!("{} - {}", trigger, replace)
-                    }
-                    MatchContentType::Image(_) => {
-                        // Skip image matches for now
-                    }
-                }
-            }
-        }
-    }
-}
-
-#[derive(Debug, Serialize)]
-struct JsonMatchEntry {
-    triggers: Vec<String>,
-    replace: String,
-}
-
-pub fn list_matches_as_json(config_set: ConfigSet) {
-    let matches = filter_matches(config_set);
-
-    let mut entries = Vec::new();
-
-    for m in matches {
-        match m.content {
-            MatchContentType::Text(ref text) => entries.push(JsonMatchEntry {
-                triggers: m.triggers,
-                replace: text.replace.clone(),
-            }),
-            MatchContentType::Image(_) => {
-                // Skip image matches for now
-            }
-        }
-    }
-
-    let output = serde_json::to_string(&entries);
-
-    println!("{}", output.unwrap_or_default())
-}
-
-fn filter_matches(config_set: ConfigSet) -> Vec<Match> {
-    let mut output = Vec::new();
-    output.extend(config_set.default.matches);
-
-    // TODO: consider specific matches by class, title or exe path
-    //    for specific in config_set.specific {
-    //        output.extend(specific.matches)
-    //    }
-    output
-}
diff --git a/src/clipboard/linux.rs b/src/clipboard/linux.rs
deleted file mode 100644
index d051d02c..00000000
--- a/src/clipboard/linux.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 log::error;
-use std::io::Write;
-use std::path::Path;
-use std::process::{Command, Stdio};
-
-pub struct LinuxClipboardManager {}
-
-impl super::ClipboardManager for LinuxClipboardManager {
-    fn get_clipboard(&self) -> Option<String> {
-        let res = Command::new("xclip").args(&["-o", "-sel", "clip"]).output();
-
-        if let Ok(output) = res {
-            if output.status.success() {
-                let s = String::from_utf8_lossy(&output.stdout);
-                return Some((*s).to_owned());
-            }
-        }
-
-        None
-    }
-
-    fn set_clipboard(&self, payload: &str) {
-        let res = Command::new("xclip")
-            .args(&["-sel", "clip"])
-            .stdin(Stdio::piped())
-            .spawn();
-
-        if let Ok(mut child) = res {
-            let stdin = child.stdin.as_mut();
-
-            if let Some(output) = stdin {
-                let res = output.write_all(payload.as_bytes());
-
-                if let Err(e) = res {
-                    error!("Could not set clipboard: {}", e);
-                }
-
-                let res = child.wait();
-
-                if let Err(e) = res {
-                    error!("Could not set clipboard: {}", e);
-                }
-            }
-        }
-    }
-
-    fn set_clipboard_image(&self, image_path: &Path) {
-        let extension = image_path.extension();
-        let mime = match extension {
-            Some(ext) => {
-                let ext = ext.to_string_lossy().to_lowercase();
-                match ext.as_ref() {
-                    "png" => "image/png",
-                    "jpg" | "jpeg" => "image/jpeg",
-                    "gif" => "image/gif",
-                    "svg" => "image/svg",
-                    _ => "image/png",
-                }
-            }
-            None => "image/png",
-        };
-
-        let image_path = image_path.to_string_lossy().into_owned();
-
-        let res = Command::new("xclip")
-            .args(&["-selection", "clipboard", "-t", mime, "-i", &image_path])
-            .spawn();
-
-        if let Err(e) = res {
-            error!("Could not set image clipboard: {}", e);
-        }
-    }
-
-    fn set_clipboard_html(&self, html: &str) {
-        let res = Command::new("xclip")
-            .args(&["-sel", "clip", "-t", "text/html"])
-            .stdin(Stdio::piped())
-            .spawn();
-
-        if let Ok(mut child) = res {
-            let stdin = child.stdin.as_mut();
-
-            if let Some(output) = stdin {
-                let res = output.write_all(html.as_bytes());
-
-                if let Err(e) = res {
-                    error!("Could not set clipboard html: {}", e);
-                }
-
-                let res = child.wait();
-
-                if let Err(e) = res {
-                    error!("Could not set clipboard html: {}", e);
-                }
-            }
-        }
-    }
-}
-
-impl LinuxClipboardManager {
-    pub fn new() -> LinuxClipboardManager {
-        LinuxClipboardManager {}
-    }
-}
diff --git a/src/clipboard/macos.rs b/src/clipboard/macos.rs
deleted file mode 100644
index a6ef7e54..00000000
--- a/src/clipboard/macos.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::macos::*;
-use log::{error, warn};
-use std::ffi::{CStr, CString};
-use std::os::raw::c_char;
-use std::path::Path;
-
-pub struct MacClipboardManager {}
-
-impl super::ClipboardManager for MacClipboardManager {
-    fn get_clipboard(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 2000] = [0; 2000];
-            let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-
-    fn set_clipboard(&self, payload: &str) {
-        let res = CString::new(payload);
-        if let Ok(cstr) = res {
-            unsafe {
-                set_clipboard(cstr.as_ptr());
-            }
-        }
-    }
-
-    fn set_clipboard_image(&self, image_path: &Path) {
-        let path_string = image_path.to_string_lossy().into_owned();
-        let res = CString::new(path_string);
-        if let Ok(path) = res {
-            unsafe {
-                let result = set_clipboard_image(path.as_ptr());
-                if result != 1 {
-                    warn!("Couldn't set clipboard for image: {:?}", image_path)
-                }
-            }
-        }
-    }
-
-    fn set_clipboard_html(&self, html: &str) {
-        // Render the text fallback for those applications that don't support HTML clipboard
-        let decorator = html2text::render::text_renderer::TrivialDecorator::new();
-        let text_fallback =
-            html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
-        unsafe {
-            let payload_c = CString::new(html).expect("unable to create CString for html content");
-            let payload_fallback_c = CString::new(text_fallback).unwrap();
-            set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
-        }
-    }
-}
-
-impl MacClipboardManager {
-    pub fn new() -> MacClipboardManager {
-        MacClipboardManager {}
-    }
-}
diff --git a/src/clipboard/mod.rs b/src/clipboard/mod.rs
deleted file mode 100644
index a282c89d..00000000
--- a/src/clipboard/mod.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::path::Path;
-
-#[cfg(target_os = "windows")]
-mod windows;
-
-#[cfg(target_os = "linux")]
-mod linux;
-
-#[cfg(target_os = "macos")]
-mod macos;
-
-pub trait ClipboardManager {
-    fn get_clipboard(&self) -> Option<String>;
-    fn set_clipboard(&self, payload: &str);
-    fn set_clipboard_image(&self, image_path: &Path);
-    fn set_clipboard_html(&self, html: &str);
-}
-
-// LINUX IMPLEMENTATION
-#[cfg(target_os = "linux")]
-pub fn get_manager() -> impl ClipboardManager {
-    linux::LinuxClipboardManager::new()
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn get_manager() -> impl ClipboardManager {
-    windows::WindowsClipboardManager::new()
-}
-
-// MAC IMPLEMENTATION
-#[cfg(target_os = "macos")]
-pub fn get_manager() -> impl ClipboardManager {
-    macos::MacClipboardManager::new()
-}
diff --git a/src/clipboard/windows.rs b/src/clipboard/windows.rs
deleted file mode 100644
index 2341bcfc..00000000
--- a/src/clipboard/windows.rs
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::windows::{
-    get_clipboard, set_clipboard, set_clipboard_html, set_clipboard_image,
-};
-use std::{ffi::CString, path::Path};
-use widestring::U16CString;
-
-pub struct WindowsClipboardManager {}
-
-impl WindowsClipboardManager {
-    pub fn new() -> WindowsClipboardManager {
-        WindowsClipboardManager {}
-    }
-}
-
-impl super::ClipboardManager for WindowsClipboardManager {
-    fn get_clipboard(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [u16; 2000] = [0; 2000];
-            let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = U16CString::from_ptr_str(buffer.as_ptr());
-
-                let string = c_string.to_string_lossy();
-                return Some((*string).to_owned());
-            }
-        }
-
-        None
-    }
-
-    fn set_clipboard(&self, payload: &str) {
-        unsafe {
-            let payload_c = U16CString::from_str(payload).unwrap();
-            set_clipboard(payload_c.as_ptr());
-        }
-    }
-
-    fn set_clipboard_image(&self, image_path: &Path) {
-        let path_string = image_path.to_string_lossy().into_owned();
-        unsafe {
-            let payload_c = U16CString::from_str(path_string).unwrap();
-            set_clipboard_image(payload_c.as_ptr());
-        }
-    }
-
-    fn set_clipboard_html(&self, html: &str) {
-        // In order to set the HTML clipboard, we have to create a prefix with a specific format
-        // For more information, look here:
-        // https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
-        // https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
-        let mut tokens = Vec::new();
-        tokens.push("Version:0.9");
-        tokens.push("StartHTML:<<STR*#>");
-        tokens.push("EndHTML:<<END*#>");
-        tokens.push("StartFragment:<<SFG#*>");
-        tokens.push("EndFragment:<<EFG#*>");
-        tokens.push("<html>");
-        tokens.push("<body>");
-        let content = format!("<!--StartFragment-->{}<!--EndFragment-->", html);
-        tokens.push(&content);
-        tokens.push("</body>");
-        tokens.push("</html>");
-
-        let mut render = tokens.join("\r\n");
-
-        // Now replace the placeholders with the actual positions
-        render = render.replace(
-            "<<STR*#>",
-            &format!("{:0>8}", render.find("<html>").unwrap_or_default()),
-        );
-        render = render.replace("<<END*#>", &format!("{:0>8}", render.len()));
-        render = render.replace(
-            "<<SFG#*>",
-            &format!(
-                "{:0>8}",
-                render.find("<!--StartFragment-->").unwrap_or_default()
-                    + "<!--StartFragment-->".len()
-            ),
-        );
-        render = render.replace(
-            "<<EFG#*>",
-            &format!(
-                "{:0>8}",
-                render.find("<!--EndFragment-->").unwrap_or_default()
-            ),
-        );
-
-        // Render the text fallback for those applications that don't support HTML clipboard
-        let decorator = html2text::render::text_renderer::TrivialDecorator::new();
-        let text_fallback =
-            html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
-        unsafe {
-            let payload_c =
-                CString::new(render).expect("unable to create CString for html content");
-            let payload_fallback_c = U16CString::from_str(text_fallback).unwrap();
-            set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
-        }
-    }
-}
diff --git a/src/config/mod.rs b/src/config/mod.rs
deleted file mode 100644
index 7e4d1c0c..00000000
--- a/src/config/mod.rs
+++ /dev/null
@@ -1,1937 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-extern crate dirs;
-
-use crate::event::KeyModifier;
-use crate::keyboard::PasteShortcut;
-use crate::matcher::{Match, MatchVariable};
-use log::error;
-use serde::{Deserialize, Serialize};
-use std::collections::{HashMap, HashSet};
-use std::error::Error;
-use std::fmt;
-use std::fs;
-use std::fs::{create_dir_all, File};
-use std::io::Read;
-use std::path::{Path, PathBuf};
-use walkdir::{DirEntry, WalkDir};
-
-pub(crate) mod runtime;
-
-const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("../res/config.yml");
-
-pub const DEFAULT_CONFIG_FILE_NAME: &str = "default.yml";
-pub const USER_CONFIGS_FOLDER_NAME: &str = "user";
-
-// Default values for primitives
-fn default_name() -> String {
-    "default".to_owned()
-}
-fn default_parent() -> String {
-    "self".to_owned()
-}
-fn default_filter_title() -> String {
-    "".to_owned()
-}
-fn default_filter_class() -> String {
-    "".to_owned()
-}
-fn default_filter_exec() -> String {
-    "".to_owned()
-}
-fn default_log_level() -> i32 {
-    0
-}
-fn default_conflict_check() -> bool {
-    false
-}
-fn default_ipc_server_port() -> i32 {
-    34982
-}
-fn default_worker_ipc_server_port() -> i32 {
-    34983
-}
-fn default_use_system_agent() -> bool {
-    true
-}
-fn default_config_caching_interval() -> i32 {
-    800
-}
-fn default_word_separators() -> Vec<char> {
-    vec![' ', ',', '.', '?', '!', '\r', '\n', 22u8 as char]
-}
-fn default_toggle_interval() -> u32 {
-    230
-}
-fn default_toggle_key() -> KeyModifier {
-    KeyModifier::ALT
-}
-fn default_preserve_clipboard() -> bool {
-    true
-}
-fn default_passive_match_regex() -> String {
-    "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned()
-}
-fn default_passive_arg_delimiter() -> char {
-    '/'
-}
-fn default_passive_arg_escape() -> char {
-    '\\'
-}
-fn default_passive_delay() -> u64 {
-    100
-}
-fn default_passive_key() -> KeyModifier {
-    KeyModifier::OFF
-}
-fn default_enable_passive() -> bool {
-    false
-}
-fn default_enable_active() -> bool {
-    true
-}
-fn default_backspace_limit() -> i32 {
-    3
-}
-fn default_backspace_delay() -> i32 {
-    0
-}
-fn default_inject_delay() -> i32 {
-    0
-}
-fn default_restore_clipboard_delay() -> i32 {
-    300
-}
-fn default_exclude_default_entries() -> bool {
-    false
-}
-fn default_secure_input_watcher_enabled() -> bool {
-    true
-}
-fn default_secure_input_notification() -> bool {
-    true
-}
-fn default_show_notifications() -> bool {
-    true
-}
-fn default_auto_restart() -> bool {
-    true
-}
-fn default_undo_backspace() -> bool {
-    true
-}
-fn default_show_icon() -> bool {
-    true
-}
-fn default_fast_inject() -> bool {
-    true
-}
-fn default_secure_input_watcher_interval() -> i32 {
-    5000
-}
-fn default_matches() -> Vec<Match> {
-    Vec::new()
-}
-fn default_global_vars() -> Vec<MatchVariable> {
-    Vec::new()
-}
-fn default_modulo_path() -> Option<String> {
-    None
-}
-fn default_post_inject_delay() -> u64 {
-    100
-}
-
-fn default_wait_for_modifiers_release() -> bool {
-    false
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct Configs {
-    #[serde(default = "default_name")]
-    pub name: String,
-
-    #[serde(default = "default_parent")]
-    pub parent: String,
-
-    #[serde(default = "default_filter_title")]
-    pub filter_title: String,
-
-    #[serde(default = "default_filter_class")]
-    pub filter_class: String,
-
-    #[serde(default = "default_filter_exec")]
-    pub filter_exec: String,
-
-    #[serde(default = "default_log_level")]
-    pub log_level: i32,
-
-    #[serde(default = "default_conflict_check")]
-    pub conflict_check: bool,
-
-    #[serde(default = "default_ipc_server_port")]
-    pub ipc_server_port: i32,
-
-    #[serde(default = "default_worker_ipc_server_port")]
-    pub worker_ipc_server_port: i32,
-
-    #[serde(default = "default_use_system_agent")]
-    pub use_system_agent: bool,
-
-    #[serde(default = "default_config_caching_interval")]
-    pub config_caching_interval: i32,
-
-    #[serde(default = "default_word_separators")]
-    pub word_separators: Vec<char>, // TODO: add parsing test
-
-    #[serde(default = "default_toggle_key")]
-    pub toggle_key: KeyModifier,
-
-    #[serde(default = "default_toggle_interval")]
-    pub toggle_interval: u32,
-
-    #[serde(default = "default_preserve_clipboard")]
-    pub preserve_clipboard: bool,
-
-    #[serde(default = "default_passive_match_regex")]
-    pub passive_match_regex: String,
-
-    #[serde(default = "default_passive_arg_delimiter")]
-    pub passive_arg_delimiter: char,
-
-    #[serde(default = "default_passive_arg_escape")]
-    pub passive_arg_escape: char,
-
-    #[serde(default = "default_passive_key")]
-    pub passive_key: KeyModifier,
-
-    #[serde(default = "default_passive_delay")]
-    pub passive_delay: u64,
-
-    #[serde(default = "default_enable_passive")]
-    pub enable_passive: bool,
-
-    #[serde(default = "default_enable_active")]
-    pub enable_active: bool,
-
-    #[serde(default = "default_undo_backspace")]
-    pub undo_backspace: bool,
-
-    #[serde(default)]
-    pub paste_shortcut: PasteShortcut,
-
-    #[serde(default = "default_backspace_limit")]
-    pub backspace_limit: i32,
-
-    #[serde(default = "default_restore_clipboard_delay")]
-    pub restore_clipboard_delay: i32,
-
-    #[serde(default = "default_secure_input_watcher_enabled")]
-    pub secure_input_watcher_enabled: bool,
-
-    #[serde(default = "default_secure_input_watcher_interval")]
-    pub secure_input_watcher_interval: i32,
-
-    #[serde(default = "default_post_inject_delay")]
-    pub post_inject_delay: u64,
-
-    #[serde(default = "default_secure_input_notification")]
-    pub secure_input_notification: bool,
-
-    #[serde(default)]
-    pub backend: BackendType,
-
-    #[serde(default = "default_exclude_default_entries")]
-    pub exclude_default_entries: bool,
-
-    #[serde(default = "default_show_notifications")]
-    pub show_notifications: bool,
-
-    #[serde(default = "default_show_icon")]
-    pub show_icon: bool,
-
-    #[serde(default = "default_fast_inject")]
-    pub fast_inject: bool,
-
-    #[serde(default = "default_backspace_delay")]
-    pub backspace_delay: i32,
-
-    #[serde(default = "default_inject_delay")]
-    pub inject_delay: i32,
-
-    #[serde(default = "default_auto_restart")]
-    pub auto_restart: bool,
-
-    #[serde(default = "default_matches")]
-    pub matches: Vec<Match>,
-
-    #[serde(default = "default_global_vars")]
-    pub global_vars: Vec<MatchVariable>,
-
-    #[serde(default = "default_modulo_path")]
-    pub modulo_path: Option<String>,
-
-    #[serde(default = "default_wait_for_modifiers_release")]
-    pub wait_for_modifiers_release: bool,
-}
-
-// Macro used to validate config fields
-#[macro_export]
-macro_rules! validate_field {
-    ($result:expr, $field:expr, $def_value:expr) => {
-        if $field != $def_value {
-            let mut field_name = stringify!($field);
-            if field_name.starts_with("self.") {
-                field_name = &field_name[5..];  // Remove the 'self.' prefix
-            }
-            error!("Validation error, parameter '{}' is reserved and can be only used in the default.yml config file", field_name);
-            $result = false;
-        }
-    };
-}
-
-impl Configs {
-    /*
-     * Validate the Config instance.
-     * It makes sure that user defined config instances do not define
-     * attributes reserved to the default config.
-     */
-    fn validate_user_defined_config(&self) -> bool {
-        let mut result = true;
-
-        validate_field!(
-            result,
-            self.config_caching_interval,
-            default_config_caching_interval()
-        );
-        validate_field!(result, self.log_level, default_log_level());
-        validate_field!(result, self.conflict_check, default_conflict_check());
-        validate_field!(result, self.toggle_key, default_toggle_key());
-        validate_field!(result, self.toggle_interval, default_toggle_interval());
-        validate_field!(result, self.backspace_limit, default_backspace_limit());
-        validate_field!(result, self.ipc_server_port, default_ipc_server_port());
-        validate_field!(result, self.use_system_agent, default_use_system_agent());
-        validate_field!(
-            result,
-            self.preserve_clipboard,
-            default_preserve_clipboard()
-        );
-        validate_field!(
-            result,
-            self.passive_match_regex,
-            default_passive_match_regex()
-        );
-        validate_field!(
-            result,
-            self.passive_arg_delimiter,
-            default_passive_arg_delimiter()
-        );
-        validate_field!(
-            result,
-            self.passive_arg_escape,
-            default_passive_arg_escape()
-        );
-        validate_field!(result, self.passive_key, default_passive_key());
-        validate_field!(
-            result,
-            self.restore_clipboard_delay,
-            default_restore_clipboard_delay()
-        );
-        validate_field!(
-            result,
-            self.secure_input_watcher_enabled,
-            default_secure_input_watcher_enabled()
-        );
-        validate_field!(
-            result,
-            self.secure_input_watcher_interval,
-            default_secure_input_watcher_interval()
-        );
-        validate_field!(
-            result,
-            self.secure_input_notification,
-            default_secure_input_notification()
-        );
-        validate_field!(
-            result,
-            self.show_notifications,
-            default_show_notifications()
-        );
-        validate_field!(result, self.show_icon, default_show_icon());
-
-        result
-    }
-}
-
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub enum BackendType {
-    Inject,
-    Clipboard,
-
-    // On Linux systems there is a long standing issue with text injection (which
-    // in general is better than Clipboard copy/pasting) that prevents certain
-    // apps from correctly handling special characters (such as emojis or accented letters)
-    // when injected. For this reason, espanso initially defaulted on the Clipboard
-    // backend on Linux, as it was the most reliable (working in 99% of cases),
-    // even though it was less efficient and with a few inconveniences (for example, the
-    // previous clipboard content being overwritten).
-    // The Auto backend tries to take it a step further, by automatically determining
-    // when an injection is possible (only ascii characters in the replacement), and falling
-    // back to the Clipboard backend otherwise.
-    // Should only be used on Linux systems.
-    Auto,
-}
-impl Default for BackendType {
-    // The default backend varies based on the operating system.
-    // On Windows and macOS, the Inject backend is working great and should
-    // be preferred as it doesn't override the clipboard.
-    // On the other hand, on linux it has many problems due to the bugs
-    // of the libxdo used. For this reason, Clipboard will be the default
-    // backend on Linux from version v0.3.0
-
-    #[cfg(not(target_os = "linux"))]
-    fn default() -> Self {
-        BackendType::Inject
-    }
-
-    #[cfg(target_os = "linux")]
-    fn default() -> Self {
-        BackendType::Auto
-    }
-}
-
-impl Configs {
-    fn load_config(path: &Path) -> Result<Configs, ConfigLoadError> {
-        let file_res = File::open(path);
-        if let Ok(mut file) = file_res {
-            let mut contents = String::new();
-            let res = file.read_to_string(&mut contents);
-
-            if res.is_err() {
-                return Err(ConfigLoadError::UnableToReadFile);
-            }
-
-            let config_res = serde_yaml::from_str(&contents);
-
-            match config_res {
-                Ok(config) => Ok(config),
-                Err(e) => Err(ConfigLoadError::InvalidYAML(path.to_owned(), e.to_string())),
-            }
-        } else {
-            eprintln!("Error: Cannot load file {:?}", path);
-            Err(ConfigLoadError::FileNotFound)
-        }
-    }
-
-    fn merge_overwrite(&mut self, new_config: Configs) {
-        // Merge matches
-        let mut merged_matches = new_config.matches;
-        let mut match_trigger_set = HashSet::new();
-        merged_matches.iter().for_each(|m| {
-            match_trigger_set.extend(m.triggers.clone());
-        });
-        let parent_matches: Vec<Match> = self
-            .matches
-            .iter()
-            .filter(|&m| {
-                !m.triggers
-                    .iter()
-                    .any(|trigger| match_trigger_set.contains(trigger))
-            })
-            .cloned()
-            .collect();
-
-        merged_matches.extend(parent_matches);
-        self.matches = merged_matches;
-
-        // Merge global variables
-        let mut merged_global_vars = new_config.global_vars;
-        let mut vars_name_set = HashSet::new();
-        merged_global_vars.iter().for_each(|m| {
-            vars_name_set.insert(m.name.clone());
-        });
-        let parent_vars: Vec<MatchVariable> = self
-            .global_vars
-            .iter()
-            .filter(|&m| !vars_name_set.contains(&m.name))
-            .cloned()
-            .collect();
-
-        merged_global_vars.extend(parent_vars);
-        self.global_vars = merged_global_vars;
-    }
-
-    fn merge_no_overwrite(&mut self, default: &Configs) {
-        // Merge matches
-        let mut match_trigger_set = HashSet::new();
-        self.matches.iter().for_each(|m| {
-            match_trigger_set.extend(m.triggers.clone());
-        });
-        let default_matches: Vec<Match> = default
-            .matches
-            .iter()
-            .filter(|&m| {
-                !m.triggers
-                    .iter()
-                    .any(|trigger| match_trigger_set.contains(trigger))
-            })
-            .cloned()
-            .collect();
-
-        self.matches.extend(default_matches);
-
-        // Merge global variables
-        let mut vars_name_set = HashSet::new();
-        self.global_vars.iter().for_each(|m| {
-            vars_name_set.insert(m.name.clone());
-        });
-        let default_vars: Vec<MatchVariable> = default
-            .global_vars
-            .iter()
-            .filter(|&m| !vars_name_set.contains(&m.name))
-            .cloned()
-            .collect();
-
-        self.global_vars.extend(default_vars);
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct ConfigSet {
-    pub default: Configs,
-    pub specific: Vec<Configs>,
-}
-
-impl ConfigSet {
-    pub fn load(config_dir: &Path, package_dir: &Path) -> Result<ConfigSet, ConfigLoadError> {
-        if !config_dir.is_dir() {
-            return Err(ConfigLoadError::InvalidConfigDirectory);
-        }
-
-        // Load default configuration
-        let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME);
-        let default = Configs::load_config(default_file.as_path())?;
-
-        // Check that a compatible backend is used, otherwise warn the user
-        if cfg!(not(target_os = "linux")) && default.backend == BackendType::Auto {
-            eprintln!("Warning: Using Auto backend is only supported on Linux, falling back to Inject backend.");
-        }
-
-        // Analyze which config files have to be loaded
-
-        let mut target_files = Vec::new();
-
-        let specific_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME);
-        if specific_dir.exists() {
-            let dir_entry = WalkDir::new(specific_dir);
-            target_files.extend(dir_entry);
-        }
-
-        let package_files = if package_dir.exists() {
-            let dir_entry = WalkDir::new(package_dir);
-            dir_entry.into_iter().collect()
-        } else {
-            vec![]
-        };
-
-        // Load the user defined config files
-
-        let mut name_set = HashSet::new();
-        let mut children_map: HashMap<String, Vec<Configs>> = HashMap::new();
-        let mut package_map: HashMap<String, Vec<Configs>> = HashMap::new();
-        let mut root_configs = Vec::new();
-        root_configs.push(default);
-
-        let mut file_loader = |entry: walkdir::Result<DirEntry>,
-                               dest_map: &mut HashMap<String, Vec<Configs>>|
-         -> Result<(), ConfigLoadError> {
-            match entry {
-                Ok(entry) => {
-                    let path = entry.path();
-
-                    // Skip non-yaml config files
-                    if path
-                        .extension()
-                        .unwrap_or_default()
-                        .to_str()
-                        .unwrap_or_default()
-                        != "yml"
-                    {
-                        return Ok(());
-                    }
-
-                    // Skip hidden files
-                    if path
-                        .file_name()
-                        .unwrap_or_default()
-                        .to_str()
-                        .unwrap_or_default()
-                        .starts_with(".")
-                    {
-                        return Ok(());
-                    }
-
-                    let mut config = Configs::load_config(&path)?;
-
-                    // Make sure the config does not contain reserved fields
-                    if !config.validate_user_defined_config() {
-                        return Err(ConfigLoadError::InvalidParameter(path.to_owned()));
-                    }
-
-                    // No name specified, defaulting to the path name
-                    if config.name == "default" {
-                        config.name = path.to_str().unwrap_or_default().to_owned();
-                    }
-
-                    if name_set.contains(&config.name) {
-                        return Err(ConfigLoadError::NameDuplicate(path.to_owned()));
-                    }
-
-                    name_set.insert(config.name.clone());
-
-                    if config.parent == "self" {
-                        // No parent, root config
-                        root_configs.push(config);
-                    } else {
-                        // Children config
-                        let children_vec = dest_map.entry(config.parent.clone()).or_default();
-                        children_vec.push(config);
-                    }
-                }
-                Err(e) => {
-                    eprintln!("Warning: Unable to read config file: {}", e);
-                }
-            }
-
-            Ok(())
-        };
-
-        // Load the default and user specific configs
-        for entry in target_files {
-            file_loader(entry, &mut children_map)?;
-        }
-
-        // Load the package related configs
-        for entry in package_files {
-            file_loader(entry, &mut package_map)?;
-        }
-
-        // Merge the children config files
-        let mut configs_without_packages = Vec::new();
-        for root_config in root_configs {
-            let config = ConfigSet::reduce_configs(root_config, &children_map, true);
-            configs_without_packages.push(config);
-        }
-
-        // Merge package files
-        // Note: we need two different steps as the packages have a lower priority
-        //       than configs.
-        let mut configs = Vec::new();
-        for root_config in configs_without_packages {
-            let config = ConfigSet::reduce_configs(root_config, &package_map, false);
-            configs.push(config);
-        }
-
-        // Separate default from specific
-        let default = configs.get(0).unwrap().clone();
-        let mut specific = (&configs[1..]).to_vec().clone();
-
-        // Add default entries to specific configs when needed
-        for config in specific.iter_mut() {
-            if !config.exclude_default_entries {
-                config.merge_no_overwrite(&default);
-            }
-        }
-
-        // Check if some triggers are conflicting with each other
-        // For more information, see: https://github.com/federico-terzi/espanso/issues/135
-        if default.conflict_check {
-            let has_conflicts = Self::has_conflicts(&default, &specific);
-            if has_conflicts {
-                eprintln!("Warning: some triggers had conflicts and may not behave as intended");
-                eprintln!(
-                    "To turn off this check, add \"conflict_check: false\" in the configuration"
-                );
-            }
-        }
-
-        Ok(ConfigSet { default, specific })
-    }
-
-    fn reduce_configs(
-        target: Configs,
-        children_map: &HashMap<String, Vec<Configs>>,
-        higher_priority: bool,
-    ) -> Configs {
-        if children_map.contains_key(&target.name) {
-            let mut target = target;
-            for children in children_map.get(&target.name).unwrap() {
-                let children =
-                    Self::reduce_configs(children.clone(), children_map, higher_priority);
-                if higher_priority {
-                    target.merge_overwrite(children);
-                } else {
-                    target.merge_no_overwrite(&children);
-                }
-            }
-            target
-        } else {
-            target
-        }
-    }
-
-    pub fn load_default() -> Result<ConfigSet, ConfigLoadError> {
-        // Configuration related
-
-        let config_dir = crate::context::get_config_dir();
-
-        let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME);
-
-        // If config file does not exist, create one from template
-        if !default_file.exists() {
-            let result = fs::write(&default_file, DEFAULT_CONFIG_FILE_CONTENT);
-            if result.is_err() {
-                return Err(ConfigLoadError::UnableToCreateDefaultConfig);
-            }
-        }
-
-        // Create auxiliary directories
-
-        let user_config_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME);
-        if !user_config_dir.exists() {
-            let res = create_dir_all(user_config_dir.as_path());
-            if res.is_err() {
-                return Err(ConfigLoadError::UnableToCreateDefaultConfig);
-            }
-        }
-
-        // Packages
-
-        let package_dir = crate::context::get_package_dir();
-        let res = create_dir_all(package_dir.as_path());
-        if res.is_err() {
-            return Err(ConfigLoadError::UnableToCreateDefaultConfig); // TODO: change error type
-        }
-
-        return ConfigSet::load(config_dir.as_path(), package_dir.as_path());
-    }
-
-    fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool {
-        let mut sorted_triggers: Vec<String> = default
-            .matches
-            .iter()
-            .flat_map(|t| t.triggers.clone())
-            .collect();
-        sorted_triggers.sort();
-
-        let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
-
-        for s in specific.iter() {
-            let mut specific_triggers: Vec<String> =
-                s.matches.iter().flat_map(|t| t.triggers.clone()).collect();
-            specific_triggers.sort();
-            has_conflicts |= Self::list_has_conflicts(&specific_triggers);
-        }
-
-        has_conflicts
-    }
-
-    fn list_has_conflicts(sorted_list: &Vec<String>) -> bool {
-        if sorted_list.len() <= 1 {
-            return false;
-        }
-
-        let mut has_conflicts = false;
-
-        for (i, item) in sorted_list.iter().skip(1).enumerate() {
-            let previous = &sorted_list[i];
-            if item.starts_with(previous) {
-                has_conflicts = true;
-                eprintln!(
-                    "Warning: trigger '{}' is conflicting with '{}' and may not behave as intended",
-                    item, previous
-                );
-            }
-        }
-
-        has_conflicts
-    }
-}
-
-pub trait ConfigManager<'a> {
-    fn active_config(&'a self) -> &'a Configs;
-    fn default_config(&'a self) -> &'a Configs;
-    fn matches(&'a self) -> &'a Vec<Match>;
-}
-
-// Error handling
-#[derive(Debug, PartialEq)]
-pub enum ConfigLoadError {
-    FileNotFound,
-    UnableToReadFile,
-    InvalidYAML(PathBuf, String),
-    InvalidConfigDirectory,
-    InvalidParameter(PathBuf),
-    NameDuplicate(PathBuf),
-    UnableToCreateDefaultConfig,
-}
-
-impl fmt::Display for ConfigLoadError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            ConfigLoadError::FileNotFound =>  write!(f, "File not found"),
-            ConfigLoadError::UnableToReadFile =>  write!(f, "Unable to read config file"),
-            ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e),
-            ConfigLoadError::InvalidConfigDirectory =>  write!(f, "Invalid config directory"),
-            ConfigLoadError::InvalidParameter(path) =>  write!(f, "Invalid parameter in '{}', use of reserved parameters in used defined configs is not permitted", path.to_str().unwrap_or_default()),
-            ConfigLoadError::NameDuplicate(path) =>  write!(f, "Found duplicate 'name' in '{}', please use different names", path.to_str().unwrap_or_default()),
-            ConfigLoadError::UnableToCreateDefaultConfig =>  write!(f, "Could not generate default config file"),
-        }
-    }
-}
-
-impl Error for ConfigLoadError {
-    fn description(&self) -> &str {
-        match self {
-            ConfigLoadError::FileNotFound => "File not found",
-            ConfigLoadError::UnableToReadFile => "Unable to read config file",
-            ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax",
-            ConfigLoadError::InvalidConfigDirectory => "Invalid config directory",
-            ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in user defined configs is not permitted",
-            ConfigLoadError::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names",
-            ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file",
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::matcher::MatchContentType;
-    use std::io::Write;
-    use tempfile::{NamedTempFile, TempDir};
-
-    const TEST_WORKING_CONFIG_FILE: &str = include_str!("../res/test/working_config.yml");
-    const TEST_CONFIG_FILE_WITH_BAD_YAML: &str =
-        include_str!("../res/test/config_with_bad_yaml.yml");
-
-    // Test Configs
-
-    fn create_tmp_file(string: &str) -> NamedTempFile {
-        let file = NamedTempFile::new().unwrap();
-        file.as_file().write_all(string.as_bytes()).unwrap();
-        file
-    }
-
-    fn variant_eq<T>(a: &T, b: &T) -> bool {
-        std::mem::discriminant(a) == std::mem::discriminant(b)
-    }
-
-    #[test]
-    fn test_config_file_not_found() {
-        let config = Configs::load_config(Path::new("invalid/path"));
-        assert_eq!(config.is_err(), true);
-        assert_eq!(config.unwrap_err(), ConfigLoadError::FileNotFound);
-    }
-
-    #[test]
-    fn test_config_file_with_bad_yaml_syntax() {
-        let broken_config_file = create_tmp_file(TEST_CONFIG_FILE_WITH_BAD_YAML);
-        let config = Configs::load_config(broken_config_file.path());
-        match config {
-            Ok(_) => assert!(false),
-            Err(e) => {
-                match e {
-                    ConfigLoadError::InvalidYAML(p, _) => {
-                        assert_eq!(p, broken_config_file.path().to_owned())
-                    }
-                    _ => assert!(false),
-                }
-                assert!(true);
-            }
-        }
-    }
-
-    #[test]
-    fn test_validate_field_macro() {
-        let mut result = true;
-
-        validate_field!(result, 3, 3);
-        assert_eq!(result, true);
-
-        validate_field!(result, 10, 3);
-        assert_eq!(result, false);
-
-        validate_field!(result, 3, 3);
-        assert_eq!(result, false);
-    }
-
-    #[test]
-    fn test_user_defined_config_does_not_have_reserved_fields() {
-        let working_config_file = create_tmp_file(
-            r###"
-
-        backend: Clipboard
-
-        "###,
-        );
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.unwrap().validate_user_defined_config(), true);
-    }
-
-    #[test]
-    fn test_user_defined_config_has_reserved_fields_config_caching_interval() {
-        let working_config_file = create_tmp_file(
-            r###"
-
-        # This should not happen in an app-specific config
-        config_caching_interval: 100
-
-        "###,
-        );
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.unwrap().validate_user_defined_config(), false);
-    }
-
-    #[test]
-    fn test_user_defined_config_has_reserved_fields_toggle_key() {
-        let working_config_file = create_tmp_file(
-            r###"
-
-        # This should not happen in an app-specific config
-        toggle_key: CTRL
-
-        "###,
-        );
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.unwrap().validate_user_defined_config(), false);
-    }
-
-    #[test]
-    fn test_user_defined_config_has_reserved_fields_toggle_interval() {
-        let working_config_file = create_tmp_file(
-            r###"
-
-        # This should not happen in an app-specific config
-        toggle_interval: 1000
-
-        "###,
-        );
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.unwrap().validate_user_defined_config(), false);
-    }
-
-    #[test]
-    fn test_user_defined_config_has_reserved_fields_backspace_limit() {
-        let working_config_file = create_tmp_file(
-            r###"
-
-        # This should not happen in an app-specific config
-        backspace_limit: 10
-
-        "###,
-        );
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.unwrap().validate_user_defined_config(), false);
-    }
-
-    #[test]
-    fn test_config_loaded_correctly() {
-        let working_config_file = create_tmp_file(TEST_WORKING_CONFIG_FILE);
-        let config = Configs::load_config(working_config_file.path());
-        assert_eq!(config.is_ok(), true);
-    }
-
-    // Test ConfigSet
-
-    pub fn create_temp_espanso_directories() -> (TempDir, TempDir) {
-        create_temp_espanso_directories_with_default_content(DEFAULT_CONFIG_FILE_CONTENT)
-    }
-
-    pub fn create_temp_espanso_directories_with_default_content(
-        default_content: &str,
-    ) -> (TempDir, TempDir) {
-        let data_dir = TempDir::new().expect("unable to create data directory");
-        let package_dir = TempDir::new().expect("unable to create package directory");
-
-        let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
-        fs::write(default_path, default_content).unwrap();
-
-        (data_dir, package_dir)
-    }
-
-    pub fn create_temp_file_in_dir(tmp_dir: &PathBuf, name: &str, content: &str) -> PathBuf {
-        let user_defined_path = tmp_dir.join(name);
-        let user_defined_path_copy = user_defined_path.clone();
-        fs::write(user_defined_path, content).unwrap();
-
-        user_defined_path_copy
-    }
-
-    pub fn create_user_config_file(tmp_dir: &Path, name: &str, content: &str) -> PathBuf {
-        let user_config_dir = tmp_dir.join(USER_CONFIGS_FOLDER_NAME);
-        if !user_config_dir.exists() {
-            create_dir_all(&user_config_dir).unwrap();
-        }
-
-        create_temp_file_in_dir(&user_config_dir, name, content)
-    }
-
-    pub fn create_package_file(
-        package_data_dir: &Path,
-        package_name: &str,
-        filename: &str,
-        content: &str,
-    ) -> PathBuf {
-        let package_dir = package_data_dir.join(package_name);
-        if !package_dir.exists() {
-            create_dir_all(&package_dir).unwrap();
-        }
-
-        create_temp_file_in_dir(&package_dir, filename, content)
-    }
-
-    #[test]
-    fn test_config_set_default_content_should_work_correctly() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-    }
-
-    #[test]
-    fn test_config_set_load_fail_bad_directory() {
-        let config_set = ConfigSet::load(Path::new("invalid/path"), Path::new("invalid/path"));
-        assert_eq!(config_set.is_err(), true);
-        assert_eq!(
-            config_set.unwrap_err(),
-            ConfigLoadError::InvalidConfigDirectory
-        );
-    }
-
-    #[test]
-    fn test_config_set_missing_default_file() {
-        let data_dir = TempDir::new().expect("unable to create temp directory");
-        let package_dir = TempDir::new().expect("unable to create package directory");
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert_eq!(config_set.is_err(), true);
-        assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound);
-    }
-
-    #[test]
-    fn test_config_set_invalid_yaml_syntax() {
-        let (data_dir, package_dir) =
-            create_temp_espanso_directories_with_default_content(TEST_CONFIG_FILE_WITH_BAD_YAML);
-        let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        match config_set {
-            Ok(_) => assert!(false),
-            Err(e) => {
-                match e {
-                    ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path),
-                    _ => assert!(false),
-                }
-                assert!(true);
-            }
-        }
-    }
-
-    #[test]
-    fn test_config_set_specific_file_with_reserved_fields() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        let user_defined_path = create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        config_caching_interval: 10000
-        "###,
-        );
-        let user_defined_path_copy = user_defined_path.clone();
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_err());
-        assert_eq!(
-            config_set.unwrap_err(),
-            ConfigLoadError::InvalidParameter(user_defined_path_copy)
-        )
-    }
-
-    #[test]
-    fn test_config_set_specific_file_missing_name_auto_generated() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        let user_defined_path = create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        backend: Clipboard
-        "###,
-        );
-        let user_defined_path_copy = user_defined_path.clone();
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-        assert_eq!(
-            config_set.unwrap().specific[0].name,
-            user_defined_path_copy.to_str().unwrap_or_default()
-        )
-    }
-
-    #[test]
-    fn test_config_set_specific_file_duplicate_name() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: specific1
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_err());
-        assert!(variant_eq(
-            &config_set.unwrap_err(),
-            &ConfigLoadError::NameDuplicate(PathBuf::new())
-        ))
-    }
-
-    #[test]
-    fn test_user_defined_config_set_merge_with_parent_matches() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ":lol"
-              replace: "LOL"
-            - trigger: ":yess"
-              replace: "Bob"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific1.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: "hello"
-              replace: "newstring"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.default.matches.len(), 2);
-        assert_eq!(config_set.specific[0].matches.len(), 3);
-
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| x.triggers[0] == "hello")
-            .is_some());
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| x.triggers[0] == ":lol")
-            .is_some());
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| x.triggers[0] == ":yess")
-            .is_some());
-    }
-
-    #[test]
-    fn test_user_defined_config_set_merge_with_parent_matches_child_priority() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ":lol"
-              replace: "LOL"
-            - trigger: ":yess"
-              replace: "Bob"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: ":lol"
-              replace: "newstring"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.default.matches.len(), 2);
-        assert_eq!(config_set.specific[0].matches.len(), 2);
-
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| {
-                if let MatchContentType::Text(content) = &x.content {
-                    x.triggers[0] == ":lol" && content.replace == "newstring"
-                } else {
-                    false
-                }
-            })
-            .is_some());
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| x.triggers[0] == ":yess")
-            .is_some());
-    }
-
-    #[test]
-    fn test_user_defined_config_set_exclude_merge_with_parent_matches() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ":lol"
-              replace: "LOL"
-            - trigger: ":yess"
-              replace: "Bob"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: specific1
-
-        exclude_default_entries: true
-
-        matches:
-            - trigger: "hello"
-              replace: "newstring"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.default.matches.len(), 2);
-        assert_eq!(config_set.specific[0].matches.len(), 1);
-
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .find(|x| {
-                if let MatchContentType::Text(content) = &x.content {
-                    x.triggers[0] == "hello" && content.replace == "newstring"
-                } else {
-                    false
-                }
-            })
-            .is_some());
-    }
-
-    #[test]
-    fn test_only_yaml_files_are_loaded_from_config() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-            matches:
-                - trigger: ":lol"
-                  replace: "LOL"
-                - trigger: ":yess"
-                  replace: "Bob"
-            "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.zzz",
-            r###"
-        name: specific1
-
-        exclude_default_entries: true
-
-        matches:
-            - trigger: "hello"
-              replace: "newstring"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-    }
-
-    #[test]
-    fn test_hidden_files_are_ignored() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-            matches:
-                - trigger: ":lol"
-                  replace: "LOL"
-                - trigger: ":yess"
-                  replace: "Bob"
-            "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            ".specific.yml",
-            r###"
-        name: specific1
-
-        exclude_default_entries: true
-
-        matches:
-            - trigger: "hello"
-              replace: "newstring"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-    }
-
-    #[test]
-    fn test_config_set_no_parent_configs_works_correctly() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: specific2
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 2);
-    }
-
-    #[test]
-    fn test_config_set_default_parent_works_correctly() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        parent: default
-
-        matches:
-            - trigger: "hello"
-              replace: "world"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.matches.len(), 2);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hello"));
-    }
-
-    #[test]
-    fn test_config_set_no_parent_should_not_merge() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        matches:
-            - trigger: "hello"
-              replace: "world"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 1);
-        assert_eq!(config_set.default.matches.len(), 1);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(!config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hello"));
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hello"));
-    }
-
-    #[test]
-    fn test_config_set_default_nested_parent_works_correctly() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: custom1
-        parent: default
-
-        matches:
-            - trigger: "hello"
-              replace: "world"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        parent: custom1
-
-        matches:
-            - trigger: "super"
-              replace: "mario"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.matches.len(), 3);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hello"));
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "super"));
-    }
-
-    #[test]
-    fn test_config_set_parent_merge_children_priority_should_be_higher() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        parent: default
-
-        matches:
-            - trigger: "hasta"
-              replace: "world"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.matches.len(), 1);
-        assert!(config_set.default.matches.iter().any(|m| {
-            if let MatchContentType::Text(content) = &m.content {
-                m.triggers[0] == "hasta" && content.replace == "world"
-            } else {
-                false
-            }
-        }));
-    }
-
-    #[test]
-    fn test_config_set_package_configs_default_merge() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_package_file(
-            package_dir.path(),
-            "package1",
-            "package.yml",
-            r###"
-        parent: default
-
-        matches:
-            - trigger: "harry"
-              replace: "potter"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.matches.len(), 2);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "harry"));
-    }
-
-    #[test]
-    fn test_config_set_package_configs_lower_priority_than_user() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_package_file(
-            package_dir.path(),
-            "package1",
-            "package.yml",
-            r###"
-        parent: default
-
-        matches:
-            - trigger: "hasta"
-              replace: "potter"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.matches.len(), 1);
-        if let MatchContentType::Text(content) = config_set.default.matches[0].content.clone() {
-            assert_eq!(config_set.default.matches[0].triggers[0], "hasta");
-            assert_eq!(content.replace, "Hasta la vista")
-        } else {
-            panic!("invalid content");
-        }
-    }
-
-    #[test]
-    fn test_config_set_package_configs_without_merge() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_package_file(
-            package_dir.path(),
-            "package1",
-            "package.yml",
-            r###"
-        matches:
-            - trigger: "harry"
-              replace: "potter"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 1);
-        assert_eq!(config_set.default.matches.len(), 1);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "harry"));
-    }
-
-    #[test]
-    fn test_config_set_package_configs_multiple_files() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: hasta
-              replace: Hasta la vista
-        "###,
-        );
-
-        create_package_file(
-            package_dir.path(),
-            "package1",
-            "package.yml",
-            r###"
-        name: package1
-
-        matches:
-            - trigger: "harry"
-              replace: "potter"
-        "###,
-        );
-
-        create_package_file(
-            package_dir.path(),
-            "package1",
-            "addon.yml",
-            r###"
-        parent: package1
-
-        matches:
-            - trigger: "ron"
-              replace: "weasley"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 1);
-        assert_eq!(config_set.default.matches.len(), 1);
-        assert!(config_set
-            .default
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "hasta"));
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "harry"));
-        assert!(config_set.specific[0]
-            .matches
-            .iter()
-            .any(|m| m.triggers[0] == "ron"));
-    }
-
-    #[test]
-    fn test_list_has_conflict_no_conflict() {
-        assert_eq!(
-            ConfigSet::list_has_conflicts(&vec!(":ab".to_owned(), ":bc".to_owned())),
-            false
-        );
-    }
-
-    #[test]
-    fn test_list_has_conflict_conflict() {
-        let mut list = vec!["ac".to_owned(), "ab".to_owned(), "abc".to_owned()];
-        list.sort();
-        assert_eq!(ConfigSet::list_has_conflicts(&list), true);
-    }
-
-    #[test]
-    fn test_has_conflict_no_conflict() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ac
-              replace: Hasta la vista
-            - trigger: bc
-              replace: Jon
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: "hello"
-              replace: "world"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(
-            ConfigSet::has_conflicts(&config_set.default, &config_set.specific),
-            false
-        );
-    }
-
-    #[test]
-    fn test_has_conflict_conflict_in_default() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ac
-              replace: Hasta la vista
-            - trigger: bc
-              replace: Jon
-            - trigger: acb
-              replace: Error
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: "hello"
-              replace: "world"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(
-            ConfigSet::has_conflicts(&config_set.default, &config_set.specific),
-            true
-        );
-    }
-
-    #[test]
-    fn test_has_conflict_conflict_in_specific_and_default() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ac
-              replace: Hasta la vista
-            - trigger: bc
-              replace: Jon
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: "bcd"
-              replace: "Conflict"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(
-            ConfigSet::has_conflicts(&config_set.default, &config_set.specific),
-            true
-        );
-    }
-
-    #[test]
-    fn test_has_conflict_no_conflict_in_specific_and_specific() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        matches:
-            - trigger: ac
-              replace: Hasta la vista
-            - trigger: bc
-              replace: Jon
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-        name: specific1
-
-        matches:
-            - trigger: "bad"
-              replace: "Conflict"
-        "###,
-        );
-        create_user_config_file(
-            data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: specific2
-
-        matches:
-            - trigger: "badass"
-              replace: "Conflict"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(
-            ConfigSet::has_conflicts(&config_set.default, &config_set.specific),
-            false
-        );
-    }
-
-    #[test]
-    fn test_config_set_specific_inherits_default_global_vars() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        global_vars:
-            - name: testvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-         global_vars:
-            - name: specificvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 1);
-        assert_eq!(config_set.default.global_vars.len(), 1);
-        assert_eq!(config_set.specific[0].global_vars.len(), 2);
-        assert!(config_set.specific[0]
-            .global_vars
-            .iter()
-            .any(|m| m.name == "testvar"));
-        assert!(config_set.specific[0]
-            .global_vars
-            .iter()
-            .any(|m| m.name == "specificvar"));
-    }
-
-    #[test]
-    fn test_config_set_default_get_variables_from_specific() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        global_vars:
-            - name: testvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-         parent: default
-         global_vars:
-            - name: specificvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 0);
-        assert_eq!(config_set.default.global_vars.len(), 2);
-        assert!(config_set
-            .default
-            .global_vars
-            .iter()
-            .any(|m| m.name == "testvar"));
-        assert!(config_set
-            .default
-            .global_vars
-            .iter()
-            .any(|m| m.name == "specificvar"));
-    }
-
-    #[test]
-    fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() {
-        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
-            r###"
-        global_vars:
-            - name: testvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        create_user_config_file(
-            data_dir.path(),
-            "specific.yml",
-            r###"
-         exclude_default_entries: true
-
-         global_vars:
-            - name: specificvar
-              type: date
-              params:
-                format: "%m"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
-        assert_eq!(config_set.specific.len(), 1);
-        assert_eq!(config_set.default.global_vars.len(), 1);
-        assert_eq!(config_set.specific[0].global_vars.len(), 1);
-        assert!(config_set.specific[0]
-            .global_vars
-            .iter()
-            .any(|m| m.name == "specificvar"));
-    }
-}
diff --git a/src/config/runtime.rs b/src/config/runtime.rs
deleted file mode 100644
index 577897c4..00000000
--- a/src/config/runtime.rs
+++ /dev/null
@@ -1,555 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::{ConfigSet, Configs};
-use crate::matcher::Match;
-use crate::system::SystemManager;
-use log::{debug, warn};
-use regex::Regex;
-use std::cell::RefCell;
-use std::time::SystemTime;
-
-pub struct RuntimeConfigManager<'a, S: SystemManager> {
-    set: ConfigSet,
-
-    // Filter regexps
-    title_regexps: Vec<Option<Regex>>,
-    class_regexps: Vec<Option<Regex>>,
-    exec_regexps: Vec<Option<Regex>>,
-
-    system_manager: S,
-
-    // Cache
-    last_config_update: RefCell<SystemTime>,
-    last_config: RefCell<Option<&'a Configs>>,
-}
-
-impl<'a, S: SystemManager> RuntimeConfigManager<'a, S> {
-    pub fn new<'b>(set: ConfigSet, system_manager: S) -> RuntimeConfigManager<'b, S> {
-        // Compile all the regexps
-        let title_regexps = set.specific.iter().map(
-            |config| {
-                if config.filter_title.is_empty() {
-                    None
-                }else{
-                    let res = Regex::new(&config.filter_title);
-                    if let Ok(regex) = res {
-                        Some(regex)
-                    }else{
-                        warn!("Invalid regex in 'filter_title' field of configuration {}, ignoring it...", config.name);
-                        None
-                    }
-                }
-            }
-        ).collect();
-
-        let class_regexps = set.specific.iter().map(
-            |config| {
-                if config.filter_class.is_empty() {
-                    None
-                }else{
-                    let res = Regex::new(&config.filter_class);
-                    if let Ok(regex) = res {
-                        Some(regex)
-                    }else{
-                        warn!("Invalid regex in 'filter_class' field of configuration {}, ignoring it...", config.name);
-                        None
-                    }
-                }
-            }
-        ).collect();
-
-        let exec_regexps = set.specific.iter().map(
-            |config| {
-                if config.filter_exec.is_empty() {
-                    None
-                }else{
-                    let res = Regex::new(&config.filter_exec);
-                    if let Ok(regex) = res {
-                        Some(regex)
-                    }else{
-                        warn!("Invalid regex in 'filter_exec' field of configuration {}, ignoring it...", config.name);
-                        None
-                    }
-                }
-            }
-        ).collect();
-
-        let last_config_update = RefCell::new(SystemTime::now());
-        let last_config = RefCell::new(None);
-
-        RuntimeConfigManager {
-            set,
-            title_regexps,
-            class_regexps,
-            exec_regexps,
-            system_manager,
-            last_config_update,
-            last_config,
-        }
-    }
-
-    fn calculate_active_config(&'a self) -> &'a Configs {
-        // TODO: optimize performance by avoiding some of these checks if no Configs use the filters
-
-        debug!("Requested config for window:");
-
-        let active_title = self.system_manager.get_current_window_title();
-
-        if let Some(title) = active_title {
-            debug!("=> Title: '{}'", title);
-
-            for (i, regex) in self.title_regexps.iter().enumerate() {
-                if let Some(regex) = regex {
-                    if regex.is_match(&title) {
-                        debug!(
-                            "Matched 'filter_title' for '{}' config, using custom settings.",
-                            self.set.specific[i].name
-                        );
-
-                        return &self.set.specific[i];
-                    }
-                }
-            }
-        }
-
-        let active_executable = self.system_manager.get_current_window_executable();
-
-        if let Some(executable) = active_executable {
-            debug!("=> Executable: '{}'", executable);
-
-            for (i, regex) in self.exec_regexps.iter().enumerate() {
-                if let Some(regex) = regex {
-                    if regex.is_match(&executable) {
-                        debug!(
-                            "Matched 'filter_exec' for '{}' config, using custom settings.",
-                            self.set.specific[i].name
-                        );
-
-                        return &self.set.specific[i];
-                    }
-                }
-            }
-        }
-
-        let active_class = self.system_manager.get_current_window_class();
-
-        if let Some(class) = active_class {
-            debug!("=> Class: '{}'", class);
-
-            for (i, regex) in self.class_regexps.iter().enumerate() {
-                if let Some(regex) = regex {
-                    if regex.is_match(&class) {
-                        debug!(
-                            "Matched 'filter_class' for '{}' config, using custom settings.",
-                            self.set.specific[i].name
-                        );
-
-                        return &self.set.specific[i];
-                    }
-                }
-            }
-        }
-
-        // No matches, return the default mapping
-        debug!("No matches for custom configs, using default settings.");
-        &self.set.default
-    }
-}
-
-impl<'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a, S> {
-    fn active_config(&'a self) -> &'a Configs {
-        let mut last_config_update = self.last_config_update.borrow_mut();
-        if let Ok(elapsed) = (*last_config_update).elapsed() {
-            *last_config_update = SystemTime::now();
-
-            if elapsed.as_millis() < self.set.default.config_caching_interval as u128 {
-                let last_config = self.last_config.borrow();
-                if let Some(cached_config) = *last_config {
-                    debug!("Using cached config");
-                    return cached_config;
-                }
-            }
-        }
-
-        let config = self.calculate_active_config();
-
-        let mut last_config = self.last_config.borrow_mut();
-        *last_config = Some(config);
-
-        config
-    }
-
-    fn default_config(&'a self) -> &'a Configs {
-        &self.set.default
-    }
-
-    fn matches(&'a self) -> &'a Vec<Match> {
-        &self.active_config().matches
-    }
-}
-
-// TESTS
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::config::tests::{create_temp_espanso_directories, create_user_config_file};
-    use crate::config::ConfigManager;
-
-    struct DummySystemManager {
-        title: RefCell<String>,
-        class: RefCell<String>,
-        exec: RefCell<String>,
-    }
-    impl SystemManager for DummySystemManager {
-        fn get_current_window_title(&self) -> Option<String> {
-            Some(self.title.borrow().clone())
-        }
-        fn get_current_window_class(&self) -> Option<String> {
-            Some(self.class.borrow().clone())
-        }
-        fn get_current_window_executable(&self) -> Option<String> {
-            Some(self.exec.borrow().clone())
-        }
-    }
-    impl DummySystemManager {
-        pub fn new_custom(title: &str, class: &str, exec: &str) -> DummySystemManager {
-            DummySystemManager {
-                title: RefCell::new(title.to_owned()),
-                class: RefCell::new(class.to_owned()),
-                exec: RefCell::new(exec.to_owned()),
-            }
-        }
-
-        pub fn new() -> DummySystemManager {
-            DummySystemManager::new_custom("title", "class", "exec")
-        }
-
-        pub fn change(&self, title: &str, class: &str, exec: &str) {
-            *self.title.borrow_mut() = title.to_owned();
-            *self.class.borrow_mut() = class.to_owned();
-            *self.exec.borrow_mut() = exec.to_owned();
-        }
-    }
-
-    #[test]
-    fn test_runtime_constructor_regex_load_correctly() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: myname1
-        filter_exec: "Title"
-        "###,
-        );
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: myname2
-        filter_title: "Yeah"
-        filter_class: "Car"
-        "###,
-        );
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific3.yml",
-            r###"
-        name: myname3
-        filter_title: "Nice"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager = DummySystemManager::new();
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        let sp1index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname1")
-            .unwrap();
-        let sp2index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname2")
-            .unwrap();
-        let sp3index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname3")
-            .unwrap();
-
-        assert_eq!(config_manager.exec_regexps.len(), 3);
-        assert_eq!(config_manager.title_regexps.len(), 3);
-        assert_eq!(config_manager.class_regexps.len(), 3);
-
-        assert!(config_manager.class_regexps[sp1index].is_none());
-        assert!(config_manager.class_regexps[sp2index].is_some());
-        assert!(config_manager.class_regexps[sp3index].is_none());
-
-        assert!(config_manager.title_regexps[sp1index].is_none());
-        assert!(config_manager.title_regexps[sp2index].is_some());
-        assert!(config_manager.title_regexps[sp3index].is_some());
-
-        assert!(config_manager.exec_regexps[sp1index].is_some());
-        assert!(config_manager.exec_regexps[sp2index].is_none());
-        assert!(config_manager.exec_regexps[sp3index].is_none());
-    }
-
-    #[test]
-    fn test_runtime_constructor_malformed_regexes_are_ignored() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: myname1
-        filter_exec: "[`-_]"
-        "###,
-        );
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific2.yml",
-            r###"
-        name: myname2
-        filter_title: "[`-_]"
-        filter_class: "Car"
-        "###,
-        );
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific3.yml",
-            r###"
-        name: myname3
-        filter_title: "Nice"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager = DummySystemManager::new();
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        let sp1index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname1")
-            .unwrap();
-        let sp2index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname2")
-            .unwrap();
-        let sp3index = config_manager
-            .set
-            .specific
-            .iter()
-            .position(|x| x.name == "myname3")
-            .unwrap();
-
-        assert_eq!(config_manager.exec_regexps.len(), 3);
-        assert_eq!(config_manager.title_regexps.len(), 3);
-        assert_eq!(config_manager.class_regexps.len(), 3);
-
-        assert!(config_manager.class_regexps[sp1index].is_none());
-        assert!(config_manager.class_regexps[sp2index].is_some());
-        assert!(config_manager.class_regexps[sp3index].is_none());
-
-        assert!(config_manager.title_regexps[sp1index].is_none());
-        assert!(config_manager.title_regexps[sp2index].is_none());
-        assert!(config_manager.title_regexps[sp3index].is_some());
-
-        assert!(config_manager.exec_regexps[sp1index].is_none());
-        assert!(config_manager.exec_regexps[sp2index].is_none());
-        assert!(config_manager.exec_regexps[sp3index].is_none());
-    }
-
-    #[test]
-    fn test_runtime_calculate_active_config_specific_title_match() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: chrome
-        filter_title: "Chrome"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.calculate_active_config().name, "chrome");
-    }
-
-    #[test]
-    fn test_runtime_calculate_active_config_specific_class_match() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: chrome
-        filter_class: "Chrome"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.calculate_active_config().name, "chrome");
-    }
-
-    #[test]
-    fn test_runtime_calculate_active_config_specific_exec_match() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: chrome
-        filter_exec: "chrome.exe"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.calculate_active_config().name, "chrome");
-    }
-
-    #[test]
-    fn test_runtime_calculate_active_config_specific_multi_filter_match() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: chrome
-        filter_class: Browser
-        filter_exec: "firefox.exe"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Browser", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.calculate_active_config().name, "chrome");
-    }
-
-    #[test]
-    fn test_runtime_calculate_active_config_no_match() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: firefox
-        filter_title: "Firefox"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.calculate_active_config().name, "default");
-    }
-
-    #[test]
-    fn test_runtime_active_config_cache() {
-        let (data_dir, package_dir) = create_temp_espanso_directories();
-
-        create_user_config_file(
-            &data_dir.path(),
-            "specific.yml",
-            r###"
-        name: firefox
-        filter_title: "Firefox"
-        "###,
-        );
-
-        let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
-        assert!(config_set.is_ok());
-
-        let dummy_system_manager =
-            DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
-
-        let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
-
-        assert_eq!(config_manager.active_config().name, "default");
-        assert_eq!(config_manager.calculate_active_config().name, "default");
-
-        config_manager
-            .system_manager
-            .change("Firefox", "Browser", "C\\Path\\firefox.exe");
-
-        // Active config should have changed, but not cached one
-        assert_eq!(config_manager.calculate_active_config().name, "firefox");
-        assert_eq!(config_manager.active_config().name, "default");
-    }
-}
diff --git a/src/context/linux.rs b/src/context/linux.rs
deleted file mode 100644
index 2d0fe548..00000000
--- a/src/context/linux.rs
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::linux::*;
-use crate::config::Configs;
-use crate::event::KeyModifier::*;
-use crate::event::*;
-use log::{debug, error, warn};
-use std::ffi::CStr;
-use std::os::raw::{c_char, c_void};
-use std::process::exit;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering::Acquire;
-use std::sync::mpsc::Sender;
-use std::sync::Arc;
-
-#[repr(C)]
-pub struct LinuxContext {
-    pub send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-}
-
-impl LinuxContext {
-    pub fn new(
-        _: Configs,
-        send_channel: Sender<Event>,
-        is_injecting: Arc<AtomicBool>,
-    ) -> Box<LinuxContext> {
-        // Check if the X11 context is available
-        let x11_available = unsafe { check_x11() };
-
-        if x11_available < 0 {
-            error!("Error, can't connect to X11 context");
-            std::process::exit(100);
-        }
-
-        let context = Box::new(LinuxContext {
-            send_channel,
-            is_injecting,
-        });
-
-        unsafe {
-            let context_ptr = &*context as *const LinuxContext as *const c_void;
-
-            register_keypress_callback(keypress_callback);
-            register_error_callback(error_callback);
-
-            let res = initialize(context_ptr);
-            if res <= 0 {
-                error!("Could not initialize linux context, error: {}", res);
-                exit(10);
-            }
-        }
-
-        context
-    }
-}
-
-impl super::Context for LinuxContext {
-    fn eventloop(&self) {
-        unsafe {
-            eventloop();
-        }
-    }
-}
-
-impl Drop for LinuxContext {
-    fn drop(&mut self) {
-        unsafe {
-            cleanup();
-        }
-    }
-}
-
-// Native bridge code
-
-extern "C" fn keypress_callback(
-    _self: *mut c_void,
-    raw_buffer: *const u8,
-    _len: i32,
-    event_type: i32,
-    key_code: i32,
-) {
-    unsafe {
-        let _self = _self as *mut LinuxContext;
-
-        // If espanso is currently injecting text, we should avoid processing
-        // external events, as it could happen that espanso reinterpret its
-        // own input.
-        if (*_self).is_injecting.load(Acquire) {
-            debug!("Input ignored while espanso is injecting text...");
-            return;
-        }
-
-        if event_type == 0 {
-            // Char event
-            // Convert the received buffer to a string
-            let c_str = CStr::from_ptr(raw_buffer as *const c_char);
-            let char_str = c_str.to_str();
-
-            // Send the char through the channel
-            match char_str {
-                Ok(char_str) => {
-                    let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
-                    (*_self).send_channel.send(event).unwrap();
-                }
-                Err(e) => {
-                    debug!("Unable to receive char: {}", e);
-                }
-            }
-        } else if event_type == 1 {
-            // Modifier event
-
-            let modifier: Option<KeyModifier> = match key_code {
-                133 => Some(LEFT_META),
-                134 => Some(RIGHT_META),
-                50 => Some(LEFT_SHIFT),
-                62 => Some(RIGHT_SHIFT),
-                64 => Some(LEFT_ALT),
-                108 => Some(RIGHT_ALT),
-                37 => Some(LEFT_CTRL),
-                105 => Some(RIGHT_CTRL),
-                22 => Some(BACKSPACE),
-                66 => Some(CAPS_LOCK),
-                _ => None,
-            };
-
-            if let Some(modifier) = modifier {
-                let event = Event::Key(KeyEvent::Modifier(modifier));
-                (*_self).send_channel.send(event).unwrap();
-            } else {
-                // Not one of the default modifiers, send an "other" event
-                let event = Event::Key(KeyEvent::Other);
-                (*_self).send_channel.send(event).unwrap();
-            }
-        } else {
-            // Other type of event
-            let event = Event::Key(KeyEvent::Other);
-            (*_self).send_channel.send(event).unwrap();
-        }
-    }
-}
-
-extern "C" fn error_callback(
-    _self: *mut c_void,
-    error_code: c_char,
-    request_code: c_char,
-    minor_code: c_char,
-) {
-    warn!(
-        "X11 reported an error code: {}, request_code: {} and minor_code: {}",
-        error_code, request_code, minor_code
-    );
-}
diff --git a/src/context/macos.rs b/src/context/macos.rs
deleted file mode 100644
index 284e0c74..00000000
--- a/src/context/macos.rs
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::macos::*;
-use crate::config::Configs;
-use crate::event::KeyModifier::*;
-use crate::event::{ActionType, Event, KeyEvent, KeyModifier, SystemEvent};
-use crate::system::macos::MacSystemManager;
-use log::{debug, error, info};
-use std::cell::RefCell;
-use std::ffi::{CStr, CString};
-use std::os::raw::{c_char, c_void};
-use std::process::exit;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering::Acquire;
-use std::sync::mpsc::Sender;
-use std::sync::Arc;
-use std::{fs, thread};
-
-const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png");
-const DISABLED_STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icondisabled.png");
-
-pub struct MacContext {
-    pub send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-    secure_input_watcher_enabled: bool,
-    secure_input_watcher_interval: i32,
-}
-
-impl MacContext {
-    pub fn new(
-        config: Configs,
-        send_channel: Sender<Event>,
-        is_injecting: Arc<AtomicBool>,
-    ) -> Box<MacContext> {
-        // Check accessibility
-        unsafe {
-            let res = prompt_accessibility();
-
-            if res == 0 {
-                error!("Accessibility must be enabled to make espanso work on MacOS.");
-                error!(
-                    "Please allow espanso in the Security & Privacy panel, then restart espanso."
-                );
-                error!("For more information: https://espanso.org/install/mac/");
-                exit(1);
-            }
-        }
-
-        let context = Box::new(MacContext {
-            send_channel,
-            is_injecting,
-            secure_input_watcher_enabled: config.secure_input_watcher_enabled,
-            secure_input_watcher_interval: config.secure_input_watcher_interval,
-        });
-
-        // Initialize the status icon path
-        let espanso_dir = super::get_data_dir();
-        let status_icon_target = espanso_dir.join("icon.png");
-        let disabled_status_icon_target = espanso_dir.join("icondisabled.png");
-
-        if status_icon_target.exists() {
-            info!("Status icon already initialized, skipping.");
-        } else {
-            fs::write(&status_icon_target, STATUS_ICON_BINARY).unwrap_or_else(|e| {
-                error!(
-                    "Error copying the Status Icon to the espanso data directory: {}",
-                    e
-                );
-            });
-        }
-
-        if disabled_status_icon_target.exists() {
-            info!("Status icon (disabled) already initialized, skipping.");
-        } else {
-            fs::write(&disabled_status_icon_target, DISABLED_STATUS_ICON_BINARY).unwrap_or_else(
-                |e| {
-                    error!(
-                    "Error copying the Status Icon (disabled) to the espanso data directory: {}",
-                    e
-                );
-                },
-            );
-        }
-
-        unsafe {
-            let context_ptr = &*context as *const MacContext as *const c_void;
-
-            register_keypress_callback(keypress_callback);
-            register_icon_click_callback(icon_click_callback);
-            register_context_menu_click_callback(context_menu_click_callback);
-
-            let status_icon_path =
-                CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
-            let disabled_status_icon_path =
-                CString::new(disabled_status_icon_target.to_str().unwrap_or_default())
-                    .unwrap_or_default();
-            let show_icon = if config.show_icon { 1 } else { 0 };
-
-            initialize(
-                context_ptr,
-                status_icon_path.as_ptr(),
-                disabled_status_icon_path.as_ptr(),
-                show_icon,
-            );
-        }
-
-        context
-    }
-
-    fn start_secure_input_watcher(&self) {
-        let send_channel = self.send_channel.clone();
-        let secure_input_watcher_interval = self.secure_input_watcher_interval as u64;
-
-        let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || {
-            let mut last_secure_input_pid: Option<i64> = None;
-            loop {
-                let pid = MacSystemManager::get_secure_input_pid();
-
-                if let Some(pid) = pid { // Some application is currently on SecureInput
-                    let should_notify = if let Some(old_pid) = last_secure_input_pid { // We already detected a SecureInput app
-                        if old_pid != pid { // The old app is different from the current one, we should take action
-                            true
-                        }else{ // We already notified this application before
-                            false
-                        }
-                    }else{ // First time we see this SecureInput app, we should take action
-                        true
-                    };
-
-                    if should_notify {
-                        let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application();
-
-                        if let Some((app_name, path)) = secure_input_app {
-                            let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path));
-                            send_channel.send(event);
-                        }
-                    }
-
-                    last_secure_input_pid = Some(pid);
-                }else{  // No app is currently keeping SecureInput
-                    if let Some(old_pid) = last_secure_input_pid {  // If there was an app with SecureInput, notify that is now free
-                        let event = Event::System(SystemEvent::SecureInputDisabled);
-                        send_channel.send(event);
-                    }
-
-                    last_secure_input_pid = None
-                }
-
-                thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval));
-            }
-        });
-    }
-}
-
-pub fn update_icon(enabled: bool) {
-    unsafe {
-        crate::bridge::macos::update_tray_icon(if enabled { 1 } else { 0 });
-    }
-}
-
-impl super::Context for MacContext {
-    fn eventloop(&self) {
-        // Start the SecureInput watcher thread
-        if self.secure_input_watcher_enabled {
-            self.start_secure_input_watcher();
-        }
-
-        unsafe {
-            eventloop();
-        }
-    }
-}
-
-// Native bridge code
-
-extern "C" fn keypress_callback(
-    _self: *mut c_void,
-    raw_buffer: *const u8,
-    len: i32,
-    event_type: i32,
-    key_code: i32,
-) {
-    unsafe {
-        let _self = _self as *mut MacContext;
-
-        // If espanso is currently injecting text, we should avoid processing
-        // external events, as it could happen that espanso reinterpret its
-        // own input.
-        if (*_self).is_injecting.load(Acquire) {
-            debug!("Input ignored while espanso is injecting text...");
-            return;
-        }
-
-        if event_type == 0 {
-            // Char event
-            // Convert the received buffer to a string
-            let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
-            let char_str = c_str.to_str();
-
-            // Send the char through the channel
-            match char_str {
-                Ok(char_str) => {
-                    let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
-                    (*_self).send_channel.send(event).unwrap();
-                }
-                Err(e) => {
-                    error!("Unable to receive char: {}", e);
-                }
-            }
-        } else if event_type == 1 {
-            // Modifier event
-            let modifier: Option<KeyModifier> = match key_code {
-                0x37 => Some(LEFT_META),
-                0x36 => Some(RIGHT_META),
-                0x38 => Some(LEFT_SHIFT),
-                0x3C => Some(RIGHT_SHIFT),
-                0x3A => Some(LEFT_ALT),
-                0x3D => Some(RIGHT_ALT),
-                0x3B => Some(LEFT_CTRL),
-                0x3E => Some(RIGHT_CTRL),
-                0x33 => Some(BACKSPACE),
-                0x39 => Some(CAPS_LOCK),
-                _ => None,
-            };
-
-            if let Some(modifier) = modifier {
-                let event = Event::Key(KeyEvent::Modifier(modifier));
-                (*_self).send_channel.send(event).unwrap();
-            } else {
-                // Not one of the default modifiers, send an "other" event
-                let event = Event::Key(KeyEvent::Other);
-                (*_self).send_channel.send(event).unwrap();
-            }
-        } else {
-            // Other type of event
-            let event = Event::Key(KeyEvent::Other);
-            (*_self).send_channel.send(event).unwrap();
-        }
-    }
-}
-
-extern "C" fn icon_click_callback(_self: *mut c_void) {
-    unsafe {
-        let _self = _self as *mut MacContext;
-
-        let event = Event::Action(ActionType::IconClick);
-        (*_self).send_channel.send(event).unwrap();
-    }
-}
-
-extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
-    unsafe {
-        let _self = _self as *mut MacContext;
-
-        let event = Event::Action(ActionType::from(id));
-        (*_self).send_channel.send(event).unwrap();
-    }
-}
diff --git a/src/context/mod.rs b/src/context/mod.rs
deleted file mode 100644
index 0f954833..00000000
--- a/src/context/mod.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#[cfg(target_os = "windows")]
-mod windows;
-
-#[cfg(target_os = "linux")]
-mod linux;
-
-#[cfg(target_os = "macos")]
-pub(crate) mod macos;
-
-use crate::config::Configs;
-use crate::event::Event;
-use std::fs::create_dir_all;
-use std::path::PathBuf;
-use std::sync::atomic::AtomicBool;
-use std::sync::mpsc::Sender;
-use std::sync::{Arc, Once};
-
-pub trait Context {
-    fn eventloop(&self);
-}
-
-// MAC IMPLEMENTATION
-#[cfg(target_os = "macos")]
-pub fn new(
-    config: Configs,
-    send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-) -> Box<dyn Context> {
-    macos::MacContext::new(config, send_channel, is_injecting)
-}
-
-#[cfg(target_os = "macos")]
-pub fn update_icon(enabled: bool) {
-    macos::update_icon(enabled);
-}
-
-#[cfg(target_os = "macos")]
-pub fn get_icon_path() -> Option<PathBuf> {
-    None
-}
-
-// LINUX IMPLEMENTATION
-#[cfg(target_os = "linux")]
-pub fn new(
-    config: Configs,
-    send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-) -> Box<dyn Context> {
-    linux::LinuxContext::new(config, send_channel, is_injecting)
-}
-
-#[cfg(target_os = "linux")]
-pub fn update_icon(enabled: bool) {
-    // No icon on Linux
-}
-
-#[cfg(target_os = "linux")]
-pub fn get_icon_path() -> Option<PathBuf> {
-    None
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn new(
-    config: Configs,
-    send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-) -> Box<dyn Context> {
-    windows::WindowsContext::new(config, send_channel, is_injecting)
-}
-
-#[cfg(target_os = "windows")]
-pub fn update_icon(enabled: bool) {
-    windows::update_icon(enabled);
-}
-
-#[cfg(target_os = "windows")]
-pub fn get_icon_path() -> Option<PathBuf> {
-    Some(windows::get_icon_path(&get_data_dir()))
-}
-
-// espanso directories
-
-static WARING_INIT: Once = Once::new();
-
-pub fn get_data_dir() -> PathBuf {
-    let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating.");
-    let espanso_dir = data_dir.join("espanso");
-    create_dir_all(&espanso_dir).expect("Error creating espanso data directory");
-    espanso_dir
-}
-
-pub fn get_config_dir() -> PathBuf {
-    // Portable mode check
-    // Get the espanso executable path
-    let espanso_exe_path = std::env::current_exe().expect("Could not get espanso executable path");
-    let exe_dir = espanso_exe_path.parent();
-    if let Some(parent) = exe_dir {
-        let config_dir = parent.join(".espanso");
-        if config_dir.exists() {
-            println!(
-                "PORTABLE MODE, using config folder: '{}'",
-                config_dir.to_string_lossy()
-            );
-            return config_dir;
-        }
-    }
-
-    // For compatibility purposes, check if the $HOME/.espanso directory is available
-    let home_dir = dirs::home_dir().expect("Can't obtain the user home directory, terminating.");
-    let legacy_espanso_dir = home_dir.join(".espanso");
-    if legacy_espanso_dir.exists() {
-        // Avoid printing the warning multiple times with std::sync::Once
-        WARING_INIT.call_once(|| {
-            eprintln!("WARNING: using legacy espanso config location in $HOME/.espanso is DEPRECATED");
-            eprintln!("Starting from espanso v0.3.0, espanso config location is changed.");
-            eprintln!("Please check out the documentation to find out more: https://espanso.org/docs/configuration/");
-            eprintln!()
-        });
-
-        return legacy_espanso_dir;
-    }
-
-    // Check for $HOME/.config/espanso location
-    let home_config_dir = home_dir.join(".config");
-    let config_espanso_dir = home_config_dir.join("espanso");
-    if config_espanso_dir.exists() {
-        return config_espanso_dir;
-    }
-
-    // New config location, from version v0.3.0
-    // Refer to issue #73 for more information: https://github.com/federico-terzi/espanso/issues/73
-    let config_dir = dirs::config_dir().expect("Can't obtain config_dir(), terminating.");
-    let espanso_dir = config_dir.join("espanso");
-    create_dir_all(&espanso_dir).expect("Error creating espanso config directory");
-    espanso_dir
-}
-
-const PACKAGES_FOLDER_NAME: &str = "packages";
-
-pub fn get_package_dir() -> PathBuf {
-    // Deprecated $HOME/.espanso/packages directory compatibility check
-    let config_dir = get_config_dir();
-    let legacy_package_dir = config_dir.join(PACKAGES_FOLDER_NAME);
-    if legacy_package_dir.exists() {
-        return legacy_package_dir;
-    }
-
-    // New package location, starting from version v0.3.0
-    let data_dir = get_data_dir();
-    let package_dir = data_dir.join(PACKAGES_FOLDER_NAME);
-    create_dir_all(&package_dir).expect("Error creating espanso packages directory");
-    package_dir
-}
diff --git a/src/context/windows.rs b/src/context/windows.rs
deleted file mode 100644
index f871f307..00000000
--- a/src/context/windows.rs
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::windows::*;
-use crate::config::Configs;
-use crate::event::KeyModifier::*;
-use crate::event::{ActionType, Event, KeyEvent, KeyModifier};
-use log::{debug, error, info};
-use std::ffi::c_void;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering::Acquire;
-use std::sync::mpsc::Sender;
-use std::sync::Arc;
-use widestring::{U16CStr, U16CString};
-
-const BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp");
-const ICO_BINARY: &[u8] = include_bytes!("../res/win/espanso.ico");
-const RED_ICO_BINARY: &[u8] = include_bytes!("../res/win/espansored.ico");
-
-pub struct WindowsContext {
-    send_channel: Sender<Event>,
-    is_injecting: Arc<AtomicBool>,
-}
-
-impl WindowsContext {
-    pub fn new(
-        config: Configs,
-        send_channel: Sender<Event>,
-        is_injecting: Arc<AtomicBool>,
-    ) -> Box<WindowsContext> {
-        // Initialize image resources
-
-        let espanso_dir = super::get_data_dir();
-
-        info!(
-            "Initializing Espanso resources in {}",
-            espanso_dir.as_path().display()
-        );
-
-        let espanso_bmp_image = espanso_dir.join("espansoicon.bmp");
-        if espanso_bmp_image.exists() {
-            info!("BMP already initialized, skipping.");
-        } else {
-            fs::write(&espanso_bmp_image, BMP_BINARY).expect("Unable to write windows bmp file");
-
-            info!(
-                "Extracted bmp icon to: {}",
-                espanso_bmp_image.to_str().unwrap_or("error")
-            );
-        }
-
-        let espanso_ico_image = get_icon_path(&espanso_dir);
-        if espanso_ico_image.exists() {
-            info!("ICO already initialized, skipping.");
-        } else {
-            fs::write(&espanso_ico_image, ICO_BINARY).expect("Unable to write windows ico file");
-
-            info!(
-                "Extracted 'ico' icon to: {}",
-                espanso_ico_image.to_str().unwrap_or("error")
-            );
-        }
-
-        let espanso_red_ico_image = espanso_dir.join("espansored.ico");
-        if espanso_red_ico_image.exists() {
-            info!("red ICO already initialized, skipping.");
-        } else {
-            fs::write(&espanso_red_ico_image, RED_ICO_BINARY)
-                .expect("Unable to write windows ico file");
-
-            info!(
-                "Extracted 'red ico' icon to: {}",
-                espanso_red_ico_image.to_str().unwrap_or("error")
-            );
-        }
-
-        let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default();
-        let ico_icon = espanso_ico_image.to_str().unwrap_or_default();
-        let red_ico_icon = espanso_red_ico_image.to_str().unwrap_or_default();
-
-        let send_channel = send_channel;
-
-        let context = Box::new(WindowsContext {
-            send_channel,
-            is_injecting,
-        });
-
-        unsafe {
-            let context_ptr = &*context as *const WindowsContext as *const c_void;
-
-            // Register callbacks
-            register_keypress_callback(keypress_callback);
-            register_icon_click_callback(icon_click_callback);
-            register_context_menu_click_callback(context_menu_click_callback);
-
-            let ico_file_c = U16CString::from_str(ico_icon).unwrap();
-            let red_ico_file_c = U16CString::from_str(red_ico_icon).unwrap();
-            let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
-
-            let show_icon = if config.show_icon { 1 } else { 0 };
-
-            // Initialize the windows
-            let res = initialize(
-                context_ptr,
-                ico_file_c.as_ptr(),
-                red_ico_file_c.as_ptr(),
-                bmp_file_c.as_ptr(),
-                show_icon,
-            );
-            if res != 1 {
-                panic!("Can't initialize Windows context")
-            }
-        }
-
-        context
-    }
-}
-
-impl super::Context for WindowsContext {
-    fn eventloop(&self) {
-        unsafe {
-            eventloop();
-        }
-    }
-}
-
-pub fn get_icon_path(espanso_dir: &Path) -> PathBuf {
-    espanso_dir.join("espanso.ico")
-}
-
-// Native bridge code
-
-pub fn update_icon(enabled: bool) {
-    unsafe {
-        crate::bridge::windows::update_tray_icon(if enabled { 1 } else { 0 });
-    }
-}
-
-extern "C" fn keypress_callback(
-    _self: *mut c_void,
-    raw_buffer: *const u16,
-    len: i32,
-    event_type: i32,
-    key_code: i32,
-    variant: i32,
-    is_key_down: i32,
-) {
-    unsafe {
-        let _self = _self as *mut WindowsContext;
-
-        // If espanso is currently injecting text, we should avoid processing
-        // external events, as it could happen that espanso reinterpret its
-        // own input.
-        if (*_self).is_injecting.load(Acquire) {
-            debug!("Input ignored while espanso is injecting text...");
-            return;
-        }
-
-        if event_type == 0 {
-            // Char event
-            if is_key_down != 0 {
-                // KEY DOWN EVENT
-                // Convert the received buffer to a string
-                let buffer = std::slice::from_raw_parts(raw_buffer, len as usize);
-                let c_string = U16CStr::from_slice_with_nul(buffer);
-
-                if let Ok(c_string) = c_string {
-                    let string = c_string.to_string();
-
-                    // Send the char through the channel
-                    match string {
-                        Ok(string) => {
-                            let event = Event::Key(KeyEvent::Char(string));
-                            (*_self).send_channel.send(event).unwrap();
-                        }
-                        Err(e) => {
-                            error!("Unable to receive char: {}", e);
-                        }
-                    }
-                } else {
-                    error!("unable to decode widechar");
-                }
-            }
-        } else if event_type == 1 {
-            // Modifier event
-            if is_key_down == 0 {
-                let modifier: Option<KeyModifier> = match (key_code, variant) {
-                    (0x5B, _) => Some(LEFT_META),
-                    (0x5C, _) => Some(RIGHT_META),
-                    (0x10, 1) => Some(LEFT_SHIFT),
-                    (0x10, 2) => Some(RIGHT_SHIFT),
-                    (0x12, 1) => Some(LEFT_ALT),
-                    (0x12, 2) => Some(RIGHT_ALT),
-                    (0x11, 1) => Some(LEFT_CTRL),
-                    (0x11, 2) => Some(RIGHT_CTRL),
-                    (0x08, _) => Some(BACKSPACE),
-                    (0x14, _) => Some(CAPS_LOCK),
-                    _ => None,
-                };
-
-                if let Some(modifier) = modifier {
-                    let event = Event::Key(KeyEvent::Modifier(modifier));
-                    (*_self).send_channel.send(event).unwrap();
-                } else {
-                    // Not one of the default modifiers, send an "other" event
-                    let event = Event::Key(KeyEvent::Other);
-                    (*_self).send_channel.send(event).unwrap();
-                }
-            }
-        } else {
-            // Other type of event
-            let event = Event::Key(KeyEvent::Other);
-            (*_self).send_channel.send(event).unwrap();
-        }
-    }
-}
-
-extern "C" fn icon_click_callback(_self: *mut c_void) {
-    unsafe {
-        let _self = _self as *mut WindowsContext;
-
-        let event = Event::Action(ActionType::IconClick);
-        (*_self).send_channel.send(event).unwrap();
-    }
-}
-
-extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
-    unsafe {
-        let _self = _self as *mut WindowsContext;
-
-        let event = Event::Action(ActionType::from(id));
-        (*_self).send_channel.send(event).unwrap();
-    }
-}
diff --git a/src/edit.rs b/src/edit.rs
deleted file mode 100644
index fc1e802d..00000000
--- a/src/edit.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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::path::Path;
-
-#[cfg(target_os = "linux")]
-fn default_editor() -> String {
-    "/bin/nano".to_owned()
-}
-#[cfg(target_os = "macos")]
-fn default_editor() -> String {
-    "/usr/bin/nano".to_owned()
-}
-#[cfg(target_os = "windows")]
-fn default_editor() -> String {
-    "C:\\Windows\\System32\\notepad.exe".to_owned()
-}
-
-pub fn open_editor(file_path: &Path) -> bool {
-    use std::process::Command;
-
-    // Check if another editor is defined in the environment variables
-    let editor_var = std::env::var_os("EDITOR");
-    let visual_var = std::env::var_os("VISUAL");
-
-    // Prioritize the editors specified by the environment variable, use the default one
-    let editor: String = if let Some(editor_var) = editor_var {
-        editor_var.to_string_lossy().to_string()
-    } else if let Some(visual_var) = visual_var {
-        visual_var.to_string_lossy().to_string()
-    } else {
-        default_editor()
-    };
-
-    // Start the editor and wait for its termination
-    let status = if cfg!(target_os = "windows") {
-        Command::new(&editor).arg(file_path).spawn()
-    } else {
-        // On Unix, spawn the editor using the shell so that it can
-        // accept parameters. See issue #245
-        Command::new("/bin/bash")
-            .arg("-c")
-            .arg(format!("{} '{}'", editor, file_path.to_string_lossy()))
-            .spawn()
-    };
-
-    if let Ok(mut child) = status {
-        // Wait for the user to edit the configuration
-        let result = child.wait();
-
-        if let Ok(exit_status) = result {
-            exit_status.success()
-        } else {
-            false
-        }
-    } else {
-        println!("Error: could not start editor at: {}", &editor);
-        false
-    }
-}
diff --git a/src/engine.rs b/src/engine.rs
deleted file mode 100644
index 81e4fa54..00000000
--- a/src/engine.rs
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::clipboard::ClipboardManager;
-use crate::config::BackendType;
-use crate::config::{ConfigManager, Configs};
-use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
-use crate::keyboard::KeyboardManager;
-use crate::matcher::{Match, MatchReceiver};
-use crate::protocol::{send_command_or_warn, IPCCommand, Service};
-use crate::render::{RenderResult, Renderer};
-use crate::{
-    guard::InjectGuard,
-    ui::{MenuItem, MenuItemType, UIManager},
-};
-use log::{debug, error, info, warn};
-use regex::Regex;
-use std::cell::RefCell;
-use std::process::exit;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering::Release;
-use std::sync::Arc;
-
-pub struct Engine<
-    'a,
-    S: KeyboardManager,
-    C: ClipboardManager,
-    M: ConfigManager<'a>,
-    U: UIManager,
-    R: Renderer,
-> {
-    keyboard_manager: &'a S,
-    clipboard_manager: &'a C,
-    config_manager: &'a M,
-    ui_manager: &'a U,
-    renderer: &'a R,
-    is_injecting: Arc<AtomicBool>,
-
-    enabled: RefCell<bool>,
-    // Trigger string and injected text len pair
-    last_expansion_data: RefCell<Option<(String, i32)>>,
-}
-
-impl<
-        'a,
-        S: KeyboardManager,
-        C: ClipboardManager,
-        M: ConfigManager<'a>,
-        U: UIManager,
-        R: Renderer,
-    > Engine<'a, S, C, M, U, R>
-{
-    pub fn new(
-        keyboard_manager: &'a S,
-        clipboard_manager: &'a C,
-        config_manager: &'a M,
-        ui_manager: &'a U,
-        renderer: &'a R,
-        is_injecting: Arc<AtomicBool>,
-    ) -> Engine<'a, S, C, M, U, R> {
-        let enabled = RefCell::new(true);
-        let last_expansion_data = RefCell::new(None);
-
-        Engine {
-            keyboard_manager,
-            clipboard_manager,
-            config_manager,
-            ui_manager,
-            renderer,
-            is_injecting,
-            enabled,
-            last_expansion_data,
-        }
-    }
-
-    fn build_menu(&self) -> Vec<MenuItem> {
-        let mut menu = Vec::new();
-
-        let enabled = self.enabled.borrow();
-        let toggle_text = if *enabled { "Disable" } else { "Enable" }.to_owned();
-        menu.push(MenuItem {
-            item_type: MenuItemType::Button,
-            item_name: toggle_text,
-            item_id: ActionType::Toggle as i32,
-        });
-
-        menu.push(MenuItem {
-            item_type: MenuItemType::Separator,
-            item_name: "".to_owned(),
-            item_id: 998,
-        });
-
-        menu.push(MenuItem {
-            item_type: MenuItemType::Button,
-            item_name: "Reload configs".to_owned(),
-            item_id: ActionType::RestartWorker as i32,
-        });
-
-        menu.push(MenuItem {
-            item_type: MenuItemType::Separator,
-            item_name: "".to_owned(),
-            item_id: 999,
-        });
-
-        menu.push(MenuItem {
-            item_type: MenuItemType::Button,
-            item_name: "Exit espanso".to_owned(),
-            item_id: ActionType::Exit as i32,
-        });
-
-        menu
-    }
-
-    fn return_content_if_preserve_clipboard_is_enabled(&self) -> Option<String> {
-        // If the preserve_clipboard option is enabled, first save the current
-        // clipboard content in order to restore it later.
-        if self.config_manager.default_config().preserve_clipboard {
-            match self.clipboard_manager.get_clipboard() {
-                Some(clipboard) => Some(clipboard),
-                None => None,
-            }
-        } else {
-            None
-        }
-    }
-
-    fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> {
-        let config = self.config_manager.active_config();
-
-        if let Some(m) = config
-            .matches
-            .iter()
-            .find(|m| m.triggers.iter().any(|t| t == trigger))
-        {
-            Some(m.clone())
-        } else {
-            None
-        }
-    }
-
-    fn inject_text(
-        &self,
-        config: &Configs,
-        target_string: &str,
-        force_clipboard: bool,
-        is_html: bool,
-    ) {
-        let backend = if force_clipboard || is_html {
-            &BackendType::Clipboard
-        } else if config.backend == BackendType::Auto {
-            if cfg!(target_os = "linux") {
-                let all_ascii = target_string.chars().all(|c| c.is_ascii());
-                if all_ascii {
-                    debug!("All elements of the replacement are ascii, using Inject backend");
-                    &BackendType::Inject
-                } else {
-                    debug!("There are non-ascii characters, using Clipboard backend");
-                    &BackendType::Clipboard
-                }
-            } else {
-                &BackendType::Inject
-            }
-        } else {
-            &config.backend
-        };
-
-        match backend {
-            BackendType::Inject => {
-                // To handle newlines, substitute each "\n" char with an Enter key press.
-                let splits = target_string.split('\n');
-
-                for (i, split) in splits.enumerate() {
-                    if i > 0 {
-                        self.keyboard_manager.send_enter(&config);
-                    }
-
-                    self.keyboard_manager.send_string(&config, split);
-                }
-            }
-            BackendType::Clipboard => {
-                if !is_html {
-                    self.clipboard_manager.set_clipboard(&target_string);
-                } else {
-                    self.clipboard_manager.set_clipboard_html(&target_string);
-                }
-
-                self.keyboard_manager.trigger_paste(&config);
-            }
-            _ => {
-                error!("Unsupported backend type evaluation.");
-                return;
-            }
-        }
-    }
-
-    fn inject_match(
-        &self,
-        m: &Match,
-        trailing_separator: Option<char>,
-        trigger_offset: usize,
-        skip_delete: bool,
-    ) -> Option<(String, i32)> {
-        let config = self.config_manager.active_config();
-
-        if !config.enable_active {
-            return None;
-        }
-
-        // Block espanso from reinterpreting its own actions
-        let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config);
-
-        let char_count = if trailing_separator.is_none() {
-            m.triggers[trigger_offset].chars().count() as i32
-        } else {
-            m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
-        };
-
-        // If configured to do so, wait until the modifier keys are released (or timeout) so
-        // that we avoid unwanted interactions. As an example, see:
-        // https://github.com/federico-terzi/espanso/issues/470
-        if config.wait_for_modifiers_release {
-            crate::keyboard::wait_for_modifiers_release();
-        }
-
-        if !skip_delete {
-            self.keyboard_manager.delete_string(&config, char_count);
-        }
-
-        let mut previous_clipboard_content: Option<String> = None;
-
-        let rendered = self
-            .renderer
-            .render_match(m, trigger_offset, config, vec![]);
-
-        let mut expansion_data: Option<(String, i32)> = None;
-
-        match rendered {
-            RenderResult::Text(mut target_string) => {
-                // If a trailing separator was counted in the match, add it back to the target string
-                if let Some(trailing_separator) = trailing_separator {
-                    if trailing_separator == '\r' {
-                        // If the trailing separator is a carriage return,
-                        target_string.push('\n'); // convert it to new line
-                    } else {
-                        target_string.push(trailing_separator);
-                    }
-                }
-
-                // Convert Windows style newlines into unix styles
-                target_string = target_string.replace("\r\n", "\n");
-
-                // Calculate cursor rewind moves if a Cursor Hint is present
-                let index = target_string.find("$|$");
-                let cursor_rewind = if let Some(index) = index {
-                    // Convert the byte index to a char index
-                    let char_str = &target_string[0..index];
-                    let char_index = char_str.chars().count();
-                    let total_size = target_string.chars().count();
-
-                    // Remove the $|$ placeholder
-                    target_string = target_string.replace("$|$", "");
-
-                    // Calculate the amount of rewind moves needed (LEFT ARROW).
-                    // Subtract also 3, equal to the number of chars of the placeholder "$|$"
-                    let moves = (total_size - char_index - 3) as i32;
-                    Some(moves)
-                } else {
-                    None
-                };
-
-                // If the preserve_clipboard option is enabled, save the current
-                // clipboard content to restore it later.
-                previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
-
-                self.inject_text(&config, &target_string, m.force_clipboard, m.is_html);
-
-                // Disallow undo backspace if cursor positioning is used or text is HTML
-                if cursor_rewind.is_none() && !m.is_html {
-                    expansion_data = Some((
-                        m.triggers[trigger_offset].clone(),
-                        target_string.chars().count() as i32,
-                    ));
-                }
-
-                if let Some(moves) = cursor_rewind {
-                    // Simulate left arrow key presses to bring the cursor into the desired position
-                    self.keyboard_manager.move_cursor_left(&config, moves);
-                }
-            }
-            RenderResult::Image(image_path) => {
-                // If the preserve_clipboard option is enabled, save the current
-                // clipboard content to restore it later.
-                previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
-
-                self.clipboard_manager.set_clipboard_image(&image_path);
-                self.keyboard_manager.trigger_paste(&config);
-            }
-            RenderResult::Error => {
-                error!("Could not render match: {}", m.triggers[trigger_offset]);
-            }
-        }
-
-        // Restore previous clipboard content
-        if let Some(previous_clipboard_content) = previous_clipboard_content {
-            // Sometimes an expansion gets overwritten before pasting by the previous content
-            // A delay is needed to mitigate the problem
-            std::thread::sleep(std::time::Duration::from_millis(
-                config.restore_clipboard_delay as u64,
-            ));
-
-            self.clipboard_manager
-                .set_clipboard(&previous_clipboard_content);
-        }
-
-        expansion_data
-    }
-}
-
-lazy_static! {
-    static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
-}
-
-impl<
-        'a,
-        S: KeyboardManager,
-        C: ClipboardManager,
-        M: ConfigManager<'a>,
-        U: UIManager,
-        R: Renderer,
-    > MatchReceiver for Engine<'a, S, C, M, U, R>
-{
-    fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
-        let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false);
-        let mut last_expansion_data = self.last_expansion_data.borrow_mut();
-        (*last_expansion_data) = expansion_data;
-    }
-
-    fn on_undo(&self) {
-        let config = self.config_manager.active_config();
-
-        if !config.undo_backspace {
-            return;
-        }
-
-        // Block espanso from reinterpreting its own actions
-        let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config);
-
-        let last_expansion_data = self.last_expansion_data.borrow();
-        if let Some(ref last_expansion_data) = *last_expansion_data {
-            let (trigger_string, injected_text_len) = last_expansion_data;
-            // Delete the previously injected text, minus one character as it has been consumed by the backspace
-            self.keyboard_manager
-                .delete_string(&config, *injected_text_len - 1);
-            // Restore previous text
-            self.inject_text(&config, trigger_string, false, false);
-        }
-    }
-
-    fn on_enable_update(&self, status: bool) {
-        let message = if status {
-            "espanso enabled"
-        } else {
-            "espanso disabled"
-        };
-
-        info!("Toggled: {}", message);
-
-        let mut enabled_ref = self.enabled.borrow_mut();
-        *enabled_ref = status;
-
-        let config = self.config_manager.default_config();
-
-        if config.show_notifications {
-            self.ui_manager.notify(message);
-        }
-
-        // Update the icon on supported OSes.
-        crate::context::update_icon(status);
-    }
-
-    fn on_passive(&self) {
-        let config = self.config_manager.active_config();
-
-        if !config.enable_passive {
-            return;
-        }
-
-        // Block espanso from reinterpreting its own actions
-        self.is_injecting.store(true, Release);
-
-        // In order to avoid pasting previous clipboard contents, we need to check if
-        // a new clipboard was effectively copied.
-        // See issue: https://github.com/federico-terzi/espanso/issues/213
-        let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default();
-
-        // Sleep for a while, giving time to effectively copy the text
-        std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
-
-        // Clear the clipboard, for new-content detection later
-        self.clipboard_manager.set_clipboard("");
-
-        // Sleep for a while, giving time to effectively copy the text
-        std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
-
-        // Trigger a copy shortcut to transfer the content of the selection to the clipboard
-        self.keyboard_manager.trigger_copy(&config);
-
-        // Sleep for a while, giving time to effectively copy the text
-        std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
-
-        // Then get the text from the clipboard and render the match output
-        let clipboard = self.clipboard_manager.get_clipboard();
-
-        if let Some(clipboard) = clipboard {
-            // Don't expand empty clipboards, as usually they are the result of an empty passive selection
-            if clipboard.trim().is_empty() {
-                info!("Avoiding passive expansion, as the user didn't select anything");
-            } else {
-                info!("Passive mode activated");
-
-                // Restore original clipboard in case it's used during render
-                self.clipboard_manager.set_clipboard(&previous_clipboard);
-
-                let rendered = self.renderer.render_passive(&clipboard, &config);
-
-                match rendered {
-                    RenderResult::Text(payload) => {
-                        // Paste back the result in the field
-                        self.clipboard_manager.set_clipboard(&payload);
-
-                        std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
-                        self.keyboard_manager.trigger_paste(&config);
-                    }
-                    _ => warn!("Cannot expand passive match"),
-                }
-            }
-        }
-
-        std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
-
-        // Restore original clipboard
-        self.clipboard_manager.set_clipboard(&previous_clipboard);
-
-        // Re-allow espanso to interpret actions
-        self.is_injecting.store(false, Release);
-    }
-}
-
-impl<
-        'a,
-        S: KeyboardManager,
-        C: ClipboardManager,
-        M: ConfigManager<'a>,
-        U: UIManager,
-        R: Renderer,
-    > ActionEventReceiver for Engine<'a, S, C, M, U, R>
-{
-    fn on_action_event(&self, e: ActionType) {
-        let config = self.config_manager.default_config();
-        match e {
-            ActionType::IconClick => {
-                self.ui_manager.show_menu(self.build_menu());
-            }
-            ActionType::ExitWorker => {
-                info!("terminating worker process");
-                self.ui_manager.cleanup();
-                exit(0);
-            }
-            ActionType::Exit => {
-                send_command_or_warn(Service::Daemon, config.clone(), IPCCommand::exit());
-            }
-            ActionType::RestartWorker => {
-                send_command_or_warn(
-                    Service::Daemon,
-                    config.clone(),
-                    IPCCommand::restart_worker(),
-                );
-            }
-            _ => {}
-        }
-    }
-}
-
-impl<
-        'a,
-        S: KeyboardManager,
-        C: ClipboardManager,
-        M: ConfigManager<'a>,
-        U: UIManager,
-        R: Renderer,
-    > SystemEventReceiver for Engine<'a, S, C, M, U, R>
-{
-    fn on_system_event(&self, e: SystemEvent) {
-        match e {
-            // MacOS specific
-            SystemEvent::SecureInputEnabled(app_name, path) => {
-                info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
-
-                let config = self.config_manager.default_config();
-                if config.secure_input_notification && config.show_notifications {
-                    self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
-                }
-
-                crate::context::update_icon(false);
-            }
-            SystemEvent::SecureInputDisabled => {
-                info!("SecureInput has been disabled.");
-
-                let is_enabled = self.enabled.borrow();
-                crate::context::update_icon(*is_enabled);
-            }
-            SystemEvent::NotifyRequest(message) => {
-                let config = self.config_manager.default_config();
-                if config.show_notifications {
-                    self.ui_manager.notify(&message);
-                }
-            }
-            SystemEvent::Trigger(trigger) => {
-                let m = self.find_match_by_trigger(&trigger);
-                match m {
-                    Some(m) => {
-                        self.inject_match(&m, None, 0, true);
-                    }
-                    None => warn!("No match found with trigger: {}", trigger),
-                }
-            }
-        }
-    }
-}
diff --git a/src/event/manager.rs b/src/event/manager.rs
deleted file mode 100644
index 5bafec43..00000000
--- a/src/event/manager.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::event::{ActionEventReceiver, Event, KeyEventReceiver, SystemEventReceiver};
-use std::sync::mpsc::Receiver;
-
-pub trait EventManager {
-    fn eventloop(&self);
-}
-
-pub struct DefaultEventManager<'a> {
-    receive_channel: Receiver<Event>,
-    key_receivers: Vec<&'a dyn KeyEventReceiver>,
-    action_receivers: Vec<&'a dyn ActionEventReceiver>,
-    system_receivers: Vec<&'a dyn SystemEventReceiver>,
-}
-
-impl<'a> DefaultEventManager<'a> {
-    pub fn new(
-        receive_channel: Receiver<Event>,
-        key_receivers: Vec<&'a dyn KeyEventReceiver>,
-        action_receivers: Vec<&'a dyn ActionEventReceiver>,
-        system_receivers: Vec<&'a dyn SystemEventReceiver>,
-    ) -> DefaultEventManager<'a> {
-        DefaultEventManager {
-            receive_channel,
-            key_receivers,
-            action_receivers,
-            system_receivers,
-        }
-    }
-}
-
-impl<'a> EventManager for DefaultEventManager<'a> {
-    fn eventloop(&self) {
-        loop {
-            match self.receive_channel.recv() {
-                Ok(event) => match event {
-                    Event::Key(key_event) => {
-                        self.key_receivers
-                            .iter()
-                            .for_each(move |&receiver| receiver.on_key_event(key_event.clone()));
-                    }
-                    Event::Action(action_event) => {
-                        self.action_receivers
-                            .iter()
-                            .for_each(|&receiver| receiver.on_action_event(action_event.clone()));
-                    }
-                    Event::System(system_event) => {
-                        self.system_receivers.iter().for_each(move |&receiver| {
-                            receiver.on_system_event(system_event.clone())
-                        });
-                    }
-                },
-                Err(e) => panic!("Broken event channel {}", e),
-            }
-        }
-    }
-}
diff --git a/src/event/mod.rs b/src/event/mod.rs
deleted file mode 100644
index a85f3177..00000000
--- a/src/event/mod.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 Federico Terzi
- *
- * espanso is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * espanso is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-pub(crate) mod manager;
-
-use serde::{Deserialize, Serialize};
-
-#[allow(dead_code)]
-#[derive(Debug, Clone)]
-pub enum Event {
-    Action(ActionType),
-    Key(KeyEvent),
-    System(SystemEvent),
-}
-
-#[derive(Debug, Clone)]
-pub enum ActionType {
-    Noop = 0,
-    Toggle = 1,
-    Exit = 2,
-    IconClick = 3,
-    Enable = 4,
-    Disable = 5,
-    RestartWorker = 6,
-    ExitWorker = 7,
-}
-
-impl From<i32> for ActionType {
-    fn from(id: i32) -> Self {
-        match id {
-            1 => ActionType::Toggle,
-            2 => ActionType::Exit,
-            3 => ActionType::IconClick,
-            4 => ActionType::Enable,
-            5 => ActionType::Disable,
-            6 => ActionType::RestartWorker,
-            7 => ActionType::ExitWorker,
-            _ => ActionType::Noop,
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-pub enum KeyEvent {
-    Char(String),
-    Modifier(KeyModifier),
-    Other,
-}
-
-#[allow(non_camel_case_types)]
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub enum KeyModifier {
-    CTRL,
-    SHIFT,
-    ALT,
-    META,
-    BACKSPACE,
-    OFF,
-
-    // These are specific variants of the ones above. See issue: #117
-    // https://github.com/federico-terzi/espanso/issues/117
-    LEFT_CTRL,
-    RIGHT_CTRL,
-    LEFT_ALT,
-    RIGHT_ALT,
-    LEFT_META,
-    RIGHT_META,
-    LEFT_SHIFT,
-    RIGHT_SHIFT,
-
-    // Special cases, should not be used in config
-    CAPS_LOCK,
-}
-
-impl KeyModifier {
-    /// This function is used to compare KeyModifiers, considering the relations between
-    /// the generic modifier and the specific left/right variant
-    /// For example, CTRL will match with CTRL, LEFT_CTRL and RIGHT_CTRL;
-    /// but LEFT_CTRL will only match will LEFT_CTRL
-    pub fn shallow_equals(current: &KeyModifier, config: &KeyModifier) -> bool {
-        use KeyModifier::*;
-
-        match config {
-            KeyModifier::CTRL => {
-                current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL
-            }
-            KeyModifier::SHIFT => {
-                current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT
-            }
-            KeyModifier::ALT => current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT,
-            KeyModifier::META => {
-                current == &LEFT_META || current == &RIGHT_META || current == &META
-            }
-            KeyModifier::BACKSPACE => current == &BACKSPACE,
-            KeyModifier::LEFT_CTRL => current == &LEFT_CTRL,
-            KeyModifier::RIGHT_CTRL => current == &RIGHT_CTRL,
-            KeyModifier::LEFT_ALT => current == &LEFT_ALT,
-            KeyModifier::RIGHT_ALT => current == &RIGHT_ALT,
-            KeyModifier::LEFT_META => current == &LEFT_META,
-            KeyModifier::RIGHT_META => current == &RIGHT_META,
-            KeyModifier::LEFT_SHIFT => current == &LEFT_SHIFT,
-            KeyModifier::RIGHT_SHIFT => current == &RIGHT_SHIFT,
-            _ => false,
-        }
-    }
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone)]
-pub enum SystemEvent {
-    // MacOS specific
-    SecureInputEnabled(String, String), // AppName, App Path
-    SecureInputDisabled,
-
-    // Notification
-    NotifyRequest(String),
-
-    // Trigger an expansion from IPC
-    Trigger(String),
-}
-
-// Receivers
-
-pub trait KeyEventReceiver {
-    fn on_key_event(&self, e: KeyEvent);
-}
-
-pub trait ActionEventReceiver {
-    fn on_action_event(&self, e: ActionType);
-}
-
-pub trait SystemEventReceiver {
-    fn on_system_event(&self, e: SystemEvent);
-}
-
-// TESTS
-
-#[cfg(test)]
-mod tests {
-    use super::KeyModifier::*;
-    use super::*;
-
-    #[test]
-    fn test_shallow_equals_ctrl() {
-        assert!(KeyModifier::shallow_equals(&CTRL, &CTRL));
-        assert!(KeyModifier::shallow_equals(&LEFT_CTRL, &CTRL));
-        assert!(KeyModifier::shallow_equals(&RIGHT_CTRL, &CTRL));
-
-        assert!(!KeyModifier::shallow_equals(&CTRL, &LEFT_CTRL));
-        assert!(!KeyModifier::shallow_equals(&CTRL, &RIGHT_CTRL));
-    }
-
-    #[test]
-    fn test_shallow_equals_shift() {
-        assert!(KeyModifier::shallow_equals(&SHIFT, &SHIFT));
-        assert!(KeyModifier::shallow_equals(&LEFT_SHIFT, &SHIFT));
-        assert!(KeyModifier::shallow_equals(&RIGHT_SHIFT, &SHIFT));
-
-        assert!(!KeyModifier::shallow_equals(&SHIFT, &LEFT_SHIFT));
-        assert!(!KeyModifier::shallow_equals(&SHIFT, &RIGHT_SHIFT));
-    }
-
-    #[test]
-    fn test_shallow_equals_alt() {
-        assert!(KeyModifier::shallow_equals(&ALT, &ALT));
-        assert!(KeyModifier::shallow_equals(&LEFT_ALT, &ALT));
-        assert!(KeyModifier::shallow_equals(&RIGHT_ALT, &ALT));
-
-        assert!(!KeyModifier::shallow_equals(&ALT, &LEFT_ALT));
-        assert!(!KeyModifier::shallow_equals(&ALT, &RIGHT_ALT));
-    }
-
-    #[test]
-    fn test_shallow_equals_meta() {
-        assert!(KeyModifier::shallow_equals(&META, &META));
-        assert!(KeyModifier::shallow_equals(&LEFT_META, &META));
-        assert!(KeyModifier::shallow_equals(&RIGHT_META, &META));
-
-        assert!(!KeyModifier::shallow_equals(&META, &LEFT_META));
-        assert!(!KeyModifier::shallow_equals(&META, &RIGHT_META));
-    }
-
-    #[test]
-    fn test_shallow_equals_backspace() {
-        assert!(KeyModifier::shallow_equals(&BACKSPACE, &BACKSPACE));
-    }
-
-    #[test]
-    fn test_shallow_equals_off() {
-        assert!(!KeyModifier::shallow_equals(&OFF, &CTRL));
-        assert!(!KeyModifier::shallow_equals(&OFF, &ALT));
-        assert!(!KeyModifier::shallow_equals(&OFF, &META));
-        assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT));
-    }
-}
diff --git a/src/extension/clipboard.rs b/src/extension/clipboard.rs
deleted file mode 100644
index d82112da..00000000
--- a/src/extension/clipboard.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::clipboard::ClipboardManager;
-use crate::extension::ExtensionResult;
-use serde_yaml::Mapping;
-use std::collections::HashMap;
-
-use super::ExtensionOut;
-
-pub struct ClipboardExtension {
-    clipboard_manager: Box<dyn ClipboardManager>,
-}
-
-impl ClipboardExtension {
-    pub fn new(clipboard_manager: Box<dyn ClipboardManager>) -> ClipboardExtension {
-        ClipboardExtension { clipboard_manager }
-    }
-}
-
-impl super::Extension for ClipboardExtension {
-    fn name(&self) -> String {
-        String::from("clipboard")
-    }
-
-    fn calculate(
-        &self,
-        _: &Mapping,
-        _: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> ExtensionOut {
-        if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
-            Ok(Some(ExtensionResult::Single(clipboard)))
-        } else {
-            Ok(None)
-        }
-    }
-}
diff --git a/src/extension/date.rs b/src/extension/date.rs
deleted file mode 100644
index 90e1eccb..00000000
--- a/src/extension/date.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019-2020 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 crate::extension::ExtensionResult;
-use chrono::{DateTime, Duration, Local};
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-use super::ExtensionOut;
-
-pub struct DateExtension {}
-
-impl DateExtension {
-    pub fn new() -> DateExtension {
-        DateExtension {}
-    }
-}
-
-impl super::Extension for DateExtension {
-    fn name(&self) -> String {
-        String::from("date")
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        _: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> ExtensionOut {
-        let mut now: DateTime<Local> = Local::now();
-
-        // Compute the given offset
-        let offset = params.get(&Value::from("offset"));
-        if let Some(offset) = offset {
-            let seconds = offset.as_i64().unwrap_or_else(|| 0);
-            let offset = Duration::seconds(seconds);
-            now = now + offset;
-        }
-
-        let format = params.get(&Value::from("format"));
-
-        let date = if let Some(format) = format {
-            now.format(format.as_str().unwrap()).to_string()
-        } else {
-            now.to_rfc2822()
-        };
-
-        Ok(Some(ExtensionResult::Single(date)))
-    }
-}
diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs
deleted file mode 100644
index 030b71d0..00000000
--- a/src/extension/dummy.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::extension::ExtensionResult;
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-pub struct DummyExtension {
-    name: String,
-}
-
-impl DummyExtension {
-    pub fn new(name: &str) -> DummyExtension {
-        DummyExtension {
-            name: name.to_owned(),
-        }
-    }
-}
-
-impl super::Extension for DummyExtension {
-    fn name(&self) -> String {
-        self.name.clone()
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        _: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let echo = params.get(&Value::from("echo"));
-
-        if let Some(echo) = echo {
-            Ok(Some(ExtensionResult::Single(
-                echo.as_str().unwrap_or_default().to_owned(),
-            )))
-        } else {
-            Ok(None)
-        }
-    }
-}
diff --git a/src/extension/form.rs b/src/extension/form.rs
deleted file mode 100644
index ac0858d7..00000000
--- a/src/extension/form.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::{config::Configs, extension::ExtensionResult, ui::modulo::ModuloManager};
-use log::{error, warn};
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-pub struct FormExtension {
-    manager: ModuloManager,
-}
-
-impl FormExtension {
-    pub fn new(config: &Configs) -> FormExtension {
-        let manager = ModuloManager::new(config);
-        FormExtension { manager }
-    }
-}
-
-impl super::Extension for FormExtension {
-    fn name(&self) -> String {
-        "form".to_owned()
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        _: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let layout = params.get(&Value::from("layout"));
-        let layout = if let Some(value) = layout {
-            value.as_str().unwrap_or_default().to_string()
-        } else {
-            error!("invoking form extension without specifying a layout");
-            return Err(super::ExtensionError::Internal);
-        };
-
-        let mut form_config = Mapping::new();
-        form_config.insert(Value::from("title"), Value::from("espanso"));
-        form_config.insert(Value::from("layout"), Value::from(layout));
-
-        if let Some(fields) = params.get(&Value::from("fields")) {
-            form_config.insert(Value::from("fields"), fields.clone());
-        }
-
-        if let Some(icon_path) = crate::context::get_icon_path() {
-            form_config.insert(
-                Value::from("icon"),
-                Value::from(icon_path.to_string_lossy().to_string()),
-            );
-        }
-
-        let serialized_config: String =
-            serde_yaml::to_string(&form_config).expect("unable to serialize form config");
-
-        let output = self
-            .manager
-            .invoke(&["form", "-i", "-"], &serialized_config);
-
-        // On macOS and Windows, after the form closes we have to wait until the user releases the modifier keys
-        on_form_close();
-
-        if let Some(output) = output {
-            let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
-            match json {
-                Ok(json) => {
-                    // Check if the JSON is empty. In those cases, it means the user exited
-                    // the form before submitting it, therefore the expansion should stop
-                    if json.is_empty() {
-                        return Err(super::ExtensionError::Aborted);
-                    }
-
-                    return Ok(Some(ExtensionResult::Multiple(json)));
-                }
-                Err(error) => {
-                    error!("modulo json parsing error: {}", error);
-                    return Err(super::ExtensionError::Internal);
-                }
-            }
-        } else {
-            error!("modulo form didn't return any output");
-            return Err(super::ExtensionError::Internal);
-        }
-    }
-}
-
-#[cfg(target_os = "linux")]
-fn on_form_close() {
-    // NOOP on Linux
-}
-
-#[cfg(target_os = "windows")]
-fn on_form_close() {
-    let released = crate::keyboard::windows::wait_for_modifiers_release();
-    if !released {
-        warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
-    }
-}
-
-#[cfg(target_os = "macos")]
-fn on_form_close() {
-    let released = crate::keyboard::macos::wait_for_modifiers_release();
-    if !released {
-        warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
-    }
-}
diff --git a/src/extension/mod.rs b/src/extension/mod.rs
deleted file mode 100644
index a98a138c..00000000
--- a/src/extension/mod.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::{clipboard::ClipboardManager, config::Configs};
-use serde_yaml::Mapping;
-use std::collections::HashMap;
-
-mod clipboard;
-mod date;
-pub mod dummy;
-mod form;
-pub mod multiecho;
-mod random;
-mod script;
-mod shell;
-mod utils;
-pub mod vardummy;
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum ExtensionResult {
-    Single(String),
-    Multiple(HashMap<String, String>),
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum ExtensionError {
-    // Returned by an extension if an internal process occurred
-    Internal,
-    // Returned by an extension if the user aborted the expansion
-    // for example when pressing ESC inside a FormExtension.
-    Aborted,
-}
-
-pub type ExtensionOut = Result<Option<ExtensionResult>, ExtensionError>;
-
-pub trait Extension {
-    fn name(&self) -> String;
-    fn calculate(
-        &self,
-        params: &Mapping,
-        args: &Vec<String>,
-        current_vars: &HashMap<String, ExtensionResult>,
-    ) -> ExtensionOut;
-}
-
-pub fn get_extensions(
-    config: &Configs,
-    clipboard_manager: Box<dyn ClipboardManager>,
-) -> Vec<Box<dyn Extension>> {
-    vec![
-        Box::new(date::DateExtension::new()),
-        Box::new(shell::ShellExtension::new()),
-        Box::new(script::ScriptExtension::new()),
-        Box::new(random::RandomExtension::new()),
-        Box::new(multiecho::MultiEchoExtension::new()),
-        Box::new(dummy::DummyExtension::new("dummy")),
-        Box::new(dummy::DummyExtension::new("echo")),
-        Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
-        Box::new(form::FormExtension::new(config)),
-    ]
-}
diff --git a/src/extension/multiecho.rs b/src/extension/multiecho.rs
deleted file mode 100644
index 99ec02fc..00000000
--- a/src/extension/multiecho.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::extension::ExtensionResult;
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-pub struct MultiEchoExtension {}
-
-impl MultiEchoExtension {
-    pub fn new() -> MultiEchoExtension {
-        MultiEchoExtension {}
-    }
-}
-
-impl super::Extension for MultiEchoExtension {
-    fn name(&self) -> String {
-        "multiecho".to_owned()
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        _: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let mut output: HashMap<String, String> = HashMap::new();
-        for (key, value) in params.iter() {
-            if let Some(key) = key.as_str() {
-                if let Some(value) = value.as_str() {
-                    output.insert(key.to_owned(), value.to_owned());
-                }
-            }
-        }
-        Ok(Some(ExtensionResult::Multiple(output)))
-    }
-}
diff --git a/src/extension/random.rs b/src/extension/random.rs
deleted file mode 100644
index 54c21e37..00000000
--- a/src/extension/random.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::extension::ExtensionResult;
-use log::{error, warn};
-use rand::seq::SliceRandom;
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-pub struct RandomExtension {}
-
-impl RandomExtension {
-    pub fn new() -> RandomExtension {
-        RandomExtension {}
-    }
-}
-
-impl super::Extension for RandomExtension {
-    fn name(&self) -> String {
-        String::from("random")
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        args: &Vec<String>,
-        _: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let choices = params.get(&Value::from("choices"));
-        if choices.is_none() {
-            warn!("No 'choices' parameter specified for random variable");
-            return Ok(None);
-        }
-        let choices = choices.unwrap().as_sequence();
-        if let Some(choices) = choices {
-            let str_choices = choices
-                .iter()
-                .map(|arg| arg.as_str().unwrap_or_default().to_string())
-                .collect::<Vec<String>>();
-
-            // Select a random choice between the possibilities
-            let choice = str_choices.choose(&mut rand::thread_rng());
-
-            match choice {
-                Some(output) => {
-                    // Render arguments
-                    let output = crate::render::utils::render_args(output, args);
-
-                    return Ok(Some(ExtensionResult::Single(output)));
-                }
-                None => {
-                    error!("Could not select a random choice.");
-                    return Err(super::ExtensionError::Internal);
-                }
-            }
-        }
-
-        error!("choices array have an invalid format '{:?}'", choices);
-        Err(super::ExtensionError::Internal)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::extension::Extension;
-
-    #[test]
-    fn test_random_basic() {
-        let mut params = Mapping::new();
-        let choices = vec!["first", "second", "third"];
-        params.insert(Value::from("choices"), Value::from(choices.clone()));
-
-        let extension = RandomExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        let output = output.unwrap();
-
-        assert!(choices
-            .into_iter()
-            .any(|x| ExtensionResult::Single(x.to_owned()) == output));
-    }
-
-    #[test]
-    fn test_random_with_args() {
-        let mut params = Mapping::new();
-        let choices = vec!["first $0$", "second $0$", "$0$ third"];
-        params.insert(Value::from("choices"), Value::from(choices.clone()));
-
-        let extension = RandomExtension::new();
-        let output = extension
-            .calculate(&params, &vec!["test".to_owned()], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        let output = output.unwrap();
-
-        let rendered_choices = vec!["first test", "second test", "test third"];
-
-        assert!(rendered_choices
-            .into_iter()
-            .any(|x| ExtensionResult::Single(x.to_owned()) == output));
-    }
-}
diff --git a/src/extension/script.rs b/src/extension/script.rs
deleted file mode 100644
index 0c92a4cb..00000000
--- a/src/extension/script.rs
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::extension::ExtensionResult;
-use log::{error, warn};
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-use std::path::PathBuf;
-use std::process::Command;
-
-pub struct ScriptExtension {}
-
-impl ScriptExtension {
-    pub fn new() -> ScriptExtension {
-        ScriptExtension {}
-    }
-}
-
-impl super::Extension for ScriptExtension {
-    fn name(&self) -> String {
-        String::from("script")
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        user_args: &Vec<String>,
-        vars: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let args = params.get(&Value::from("args"));
-        if args.is_none() {
-            warn!("No 'args' parameter specified for script variable");
-            return Err(super::ExtensionError::Internal);
-        }
-        let args = args.unwrap().as_sequence();
-        if let Some(args) = args {
-            let mut str_args = args
-                .iter()
-                .map(|arg| arg.as_str().unwrap_or_default().to_string())
-                .collect::<Vec<String>>();
-
-            // The user has to enable argument concatenation explicitly
-            let inject_args = params
-                .get(&Value::from("inject_args"))
-                .unwrap_or(&Value::from(false))
-                .as_bool()
-                .unwrap_or(false);
-            if inject_args {
-                str_args.extend(user_args.clone());
-            }
-
-            // Replace %HOME% with current user home directory to
-            // create cross-platform paths. See issue #265
-            // Also replace %CONFIG% and %PACKAGES% path. See issue #380
-            let home_dir = dirs::home_dir().unwrap_or_default();
-            str_args.iter_mut().for_each(|arg| {
-                if arg.contains("%HOME%") {
-                    *arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string());
-                }
-                if arg.contains("%CONFIG%") {
-                    *arg = arg.replace(
-                        "%CONFIG%",
-                        &crate::context::get_config_dir()
-                            .to_string_lossy()
-                            .to_string(),
-                    );
-                }
-                if arg.contains("%PACKAGES%") {
-                    *arg = arg.replace(
-                        "%PACKAGES%",
-                        &crate::context::get_package_dir()
-                            .to_string_lossy()
-                            .to_string(),
-                    );
-                }
-
-                // On Windows, correct paths separators
-                if cfg!(target_os = "windows") {
-                    let path = PathBuf::from(&arg);
-                    if path.exists() {
-                        *arg = path.to_string_lossy().to_string()
-                    }
-                }
-            });
-
-            let mut command = Command::new(&str_args[0]);
-
-            // Set the OS-specific flags
-            crate::utils::set_command_flags(&mut command);
-
-            // Inject the $CONFIG variable
-            command.env("CONFIG", crate::context::get_config_dir());
-
-            // Inject all the env variables
-            let env_variables = super::utils::convert_to_env_variables(&vars);
-            for (key, value) in env_variables.iter() {
-                command.env(key, value);
-            }
-
-            let output = if str_args.len() > 1 {
-                command.args(&str_args[1..]).output()
-            } else {
-                command.output()
-            };
-
-            match output {
-                Ok(output) => {
-                    let mut output_str =
-                        String::from_utf8_lossy(output.stdout.as_slice()).to_string();
-                    let error_str = String::from_utf8_lossy(output.stderr.as_slice());
-                    let error_str = error_str.to_string();
-                    let error_str = error_str.trim();
-
-                    // Print stderror if present
-                    if !error_str.is_empty() {
-                        warn!("Script command reported error: \n{}", error_str);
-                    }
-
-                    // If specified, trim the output
-                    let trim_opt = params.get(&Value::from("trim"));
-                    let should_trim = if let Some(value) = trim_opt {
-                        let val = value.as_bool();
-                        val.unwrap_or(true)
-                    } else {
-                        true
-                    };
-
-                    if should_trim {
-                        output_str = output_str.trim().to_owned()
-                    }
-
-                    return Ok(Some(ExtensionResult::Single(output_str)));
-                }
-                Err(e) => {
-                    error!("Could not execute script '{:?}', error: {}", args, e);
-                    return Err(super::ExtensionError::Internal);
-                }
-            }
-        }
-
-        error!("Could not execute script with args '{:?}'", args);
-        Err(super::ExtensionError::Internal)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::extension::Extension;
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_script_basic() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("args"),
-            Value::from(vec!["echo", "hello world"]),
-        );
-
-        let extension = ScriptExtension::new();
-        let output = extension.calculate(&params, &vec![], &HashMap::new()).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_script_basic_no_trim() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("args"),
-            Value::from(vec!["echo", "hello world"]),
-        );
-        params.insert(Value::from("trim"), Value::from(false));
-
-        let extension = ScriptExtension::new();
-        let output = extension.calculate(&params, &vec![], &HashMap::new()).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world\n".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_script_inject_args_off() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("args"),
-            Value::from(vec!["echo", "hello world"]),
-        );
-
-        let extension = ScriptExtension::new();
-        let output = extension.calculate(&params, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_script_inject_args_on() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("args"),
-            Value::from(vec!["echo", "hello world"]),
-        );
-        params.insert(Value::from("inject_args"), Value::from(true));
-
-        let extension = ScriptExtension::new();
-        let output = extension.calculate(&params, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world jon".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_script_var_injection() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("args"),
-            Value::from(vec!["bash", "-c", "echo $ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]),
-        );
-
-        let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
-        let mut subvars = HashMap::new();
-        subvars.insert("name".to_owned(), "John".to_owned());
-        vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
-        vars.insert(
-            "var1".to_owned(),
-            ExtensionResult::Single("hello".to_owned()),
-        );
-
-        let extension = ScriptExtension::new();
-        let output = extension.calculate(&params, &vec![], &vars).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello John".to_owned())
-        );
-    }
-}
diff --git a/src/extension/shell.rs b/src/extension/shell.rs
deleted file mode 100644
index b5e4dbe1..00000000
--- a/src/extension/shell.rs
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::extension::ExtensionResult;
-use log::{error, info, warn};
-use regex::{Captures, Regex};
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-use std::process::{Command, Output};
-
-lazy_static! {
-    static ref UNIX_POS_ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)").unwrap();
-    static ref WIN_POS_ARG_REGEX: Regex = Regex::new("%(?P<pos>\\d+)").unwrap();
-}
-
-pub enum Shell {
-    Cmd,
-    Powershell,
-    WSL,
-    WSL2,
-    Bash,
-    Sh,
-}
-
-impl Shell {
-    fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
-        let mut is_wsl = false;
-
-        let mut command = match self {
-            Shell::Cmd => {
-                let mut command = Command::new("cmd");
-                command.args(&["/C", &cmd]);
-                command
-            }
-            Shell::Powershell => {
-                let mut command = Command::new("powershell");
-                command.args(&["-Command", &cmd]);
-                command
-            }
-            Shell::WSL => {
-                is_wsl = true;
-                let mut command = Command::new("bash");
-                command.args(&["-c", &cmd]);
-                command
-            }
-            Shell::WSL2 => {
-                is_wsl = true;
-                let mut command = Command::new("wsl");
-                command.args(&["bash", "-c", &cmd]);
-                command
-            }
-            Shell::Bash => {
-                let mut command = Command::new("bash");
-                command.args(&["-c", &cmd]);
-                command
-            }
-            Shell::Sh => {
-                let mut command = Command::new("sh");
-                command.args(&["-c", &cmd]);
-                command
-            }
-        };
-
-        // Set the OS-specific flags
-        crate::utils::set_command_flags(&mut command);
-
-        // Inject the $CONFIG variable
-        command.env("CONFIG", crate::context::get_config_dir());
-
-        // Inject all the previous variables
-        for (key, value) in vars.iter() {
-            command.env(key, value);
-        }
-
-        // In WSL environment, we have to specify which ENV variables
-        // should be passed to linux.
-        // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
-        if is_wsl {
-            let mut tokens: Vec<&str> = Vec::new();
-            tokens.push("CONFIG/p");
-
-            // Add all the previous variables
-            for (key, _) in vars.iter() {
-                tokens.push(key);
-            }
-
-            let wsl_env = tokens.join(":");
-            command.env("WSLENV", wsl_env);
-        }
-
-        command.output()
-    }
-
-    fn from_string(shell: &str) -> Option<Shell> {
-        match shell {
-            "cmd" => Some(Shell::Cmd),
-            "powershell" => Some(Shell::Powershell),
-            "wsl" => Some(Shell::WSL),
-            "wsl2" => Some(Shell::WSL2),
-            "bash" => Some(Shell::Bash),
-            "sh" => Some(Shell::Sh),
-            _ => None,
-        }
-    }
-
-    fn get_arg_regex(&self) -> &Regex {
-        let regex = match self {
-            Shell::Cmd | Shell::Powershell => &*WIN_POS_ARG_REGEX,
-            _ => &*UNIX_POS_ARG_REGEX,
-        };
-        regex
-    }
-}
-
-impl Default for Shell {
-    fn default() -> Shell {
-        if cfg!(target_os = "windows") {
-            Shell::Powershell
-        } else if cfg!(target_os = "macos") {
-            Shell::Sh
-        } else if cfg!(target_os = "linux") {
-            Shell::Bash
-        } else {
-            panic!("invalid target os for shell")
-        }
-    }
-}
-
-pub struct ShellExtension {}
-
-impl ShellExtension {
-    pub fn new() -> ShellExtension {
-        ShellExtension {}
-    }
-}
-
-impl super::Extension for ShellExtension {
-    fn name(&self) -> String {
-        String::from("shell")
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        args: &Vec<String>,
-        vars: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let cmd = params.get(&Value::from("cmd"));
-        if cmd.is_none() {
-            warn!("No 'cmd' parameter specified for shell variable");
-            return Err(super::ExtensionError::Internal);
-        }
-
-        let inject_args = params
-            .get(&Value::from("inject_args"))
-            .unwrap_or(&Value::from(false))
-            .as_bool()
-            .unwrap_or(false);
-
-        let original_cmd = cmd.unwrap().as_str().unwrap();
-
-        let shell_param = params.get(&Value::from("shell"));
-        let shell = if let Some(shell_param) = shell_param {
-            let shell_param = shell_param.as_str().expect("invalid shell parameter");
-            let shell = Shell::from_string(shell_param);
-
-            if shell.is_none() {
-                error!("Invalid shell parameter, please select a valid one.");
-                return Err(super::ExtensionError::Internal);
-            }
-
-            shell.unwrap()
-        } else {
-            Shell::default()
-        };
-
-        // Render positional parameters in args
-        let cmd = if inject_args {
-            shell
-                .get_arg_regex()
-                .replace_all(&original_cmd, |caps: &Captures| {
-                    let position_str = caps.name("pos").unwrap().as_str();
-                    let position = position_str.parse::<i32>().unwrap_or(-1);
-                    if position >= 0 && position < args.len() as i32 {
-                        args[position as usize].to_owned()
-                    } else {
-                        "".to_owned()
-                    }
-                })
-                .to_string()
-        } else {
-            original_cmd.to_owned()
-        };
-
-        let env_variables = super::utils::convert_to_env_variables(&vars);
-
-        let output = shell.execute_cmd(&cmd, &env_variables);
-
-        match output {
-            Ok(output) => {
-                let output_str = String::from_utf8_lossy(output.stdout.as_slice());
-                let mut output_str = output_str.into_owned();
-                let error_str = String::from_utf8_lossy(output.stderr.as_slice());
-                let error_str = error_str.to_string();
-                let error_str = error_str.trim();
-
-                // Print stderror if present
-                if !error_str.is_empty() {
-                    warn!("Shell command reported error: \n{}", error_str);
-                }
-
-                // Check if debug flag set, provide additional context when an error occurs.
-                let debug_opt = params.get(&Value::from("debug"));
-                let with_debug = if let Some(value) = debug_opt {
-                    let val = value.as_bool();
-                    val.unwrap_or(false)
-                } else {
-                    false
-                };
-
-                if with_debug {
-                    info!(
-                        "debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'",
-                        original_cmd, output.status, output_str, error_str
-                    );
-                }
-
-                // If specified, trim the output
-                let trim_opt = params.get(&Value::from("trim"));
-                let should_trim = if let Some(value) = trim_opt {
-                    let val = value.as_bool();
-                    val.unwrap_or(true)
-                } else {
-                    true
-                };
-
-                if should_trim {
-                    output_str = output_str.trim().to_owned()
-                }
-
-                Ok(Some(ExtensionResult::Single(output_str)))
-            }
-            Err(e) => {
-                error!("Could not execute cmd '{}', error: {}", cmd, e);
-                Err(super::ExtensionError::Internal)
-            }
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::extension::Extension;
-
-    #[test]
-    fn test_shell_not_trimmed() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
-        params.insert(Value::from("trim"), Value::from(false));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        if cfg!(target_os = "windows") {
-            assert_eq!(
-                output.unwrap(),
-                ExtensionResult::Single("hello world\r\n".to_owned())
-            );
-        } else {
-            assert_eq!(
-                output.unwrap(),
-                ExtensionResult::Single("hello world\n".to_owned())
-            );
-        }
-    }
-
-    #[test]
-    fn test_shell_basic() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    fn test_shell_trimmed_2() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("cmd"),
-            Value::from("echo \"   hello world     \""),
-        );
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    fn test_shell_trimmed_malformed() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
-        params.insert(Value::from("trim"), Value::from("error"));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_shell_pipes() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo hello world | cat"));
-        params.insert(Value::from("trim"), Value::from(true));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec![], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("hello world".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_shell_args_unix() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo $0"));
-        params.insert(Value::from("inject_args"), Value::from(true));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec!["hello".to_owned()], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
-    }
-
-    #[test]
-    #[cfg(not(target_os = "windows"))]
-    fn test_shell_no_default_inject_args_unix() {
-        let mut params = Mapping::new();
-        params.insert(
-            Value::from("cmd"),
-            Value::from("echo 'hey friend' | awk '{ print $2 }'"),
-        );
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec!["hello".to_owned()], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        assert_eq!(
-            output.unwrap(),
-            ExtensionResult::Single("friend".to_owned())
-        );
-    }
-
-    #[test]
-    #[cfg(target_os = "windows")]
-    fn test_shell_args_windows() {
-        let mut params = Mapping::new();
-        params.insert(Value::from("cmd"), Value::from("echo %0"));
-        params.insert(Value::from("inject_args"), Value::from(true));
-
-        let extension = ShellExtension::new();
-        let output = extension
-            .calculate(&params, &vec!["hello".to_owned()], &HashMap::new())
-            .unwrap();
-
-        assert!(output.is_some());
-
-        assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
-    }
-
-    #[test]
-    fn test_shell_vars_single_injection() {
-        let mut params = Mapping::new();
-        if cfg!(target_os = "windows") {
-            params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%"));
-            params.insert(Value::from("shell"), Value::from("cmd"));
-        } else {
-            params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1"));
-        }
-
-        let extension = ShellExtension::new();
-        let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
-        vars.insert(
-            "var1".to_owned(),
-            ExtensionResult::Single("hello".to_owned()),
-        );
-        let output = extension.calculate(&params, &vec![], &vars).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
-    }
-
-    #[test]
-    fn test_shell_vars_multiple_injection() {
-        let mut params = Mapping::new();
-        if cfg!(target_os = "windows") {
-            params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_FORM1_NAME%"));
-            params.insert(Value::from("shell"), Value::from("cmd"));
-        } else {
-            params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME"));
-        }
-
-        let extension = ShellExtension::new();
-        let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
-        let mut subvars = HashMap::new();
-        subvars.insert("name".to_owned(), "John".to_owned());
-        vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
-        let output = extension.calculate(&params, &vec![], &vars).unwrap();
-
-        assert!(output.is_some());
-        assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
-    }
-}
diff --git a/src/extension/utils.rs b/src/extension/utils.rs
deleted file mode 100644
index fcf5bacc..00000000
--- a/src/extension/utils.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use crate::extension::ExtensionResult;
-use std::collections::HashMap;
-
-pub fn convert_to_env_variables(
-    original_vars: &HashMap<String, ExtensionResult>,
-) -> HashMap<String, String> {
-    let mut output = HashMap::new();
-
-    for (key, result) in original_vars.iter() {
-        match result {
-            ExtensionResult::Single(value) => {
-                let name = format!("ESPANSO_{}", key.to_uppercase());
-                output.insert(name, value.clone());
-            }
-            ExtensionResult::Multiple(values) => {
-                for (sub_key, sub_value) in values.iter() {
-                    let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase());
-                    output.insert(name, sub_value.clone());
-                }
-            }
-        }
-    }
-
-    output
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::extension::Extension;
-
-    #[test]
-    fn test_convert_to_env_variables() {
-        let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
-        let mut subvars = HashMap::new();
-        subvars.insert("name".to_owned(), "John".to_owned());
-        subvars.insert("lastname".to_owned(), "Snow".to_owned());
-        vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
-        vars.insert(
-            "var1".to_owned(),
-            ExtensionResult::Single("test".to_owned()),
-        );
-
-        let output = convert_to_env_variables(&vars);
-        assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John");
-        assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow");
-        assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test");
-    }
-}
diff --git a/src/extension/vardummy.rs b/src/extension/vardummy.rs
deleted file mode 100644
index d683267a..00000000
--- a/src/extension/vardummy.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::extension::ExtensionResult;
-use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-
-pub struct VarDummyExtension {}
-
-impl VarDummyExtension {
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
-impl super::Extension for VarDummyExtension {
-    fn name(&self) -> String {
-        "vardummy".to_owned()
-    }
-
-    fn calculate(
-        &self,
-        params: &Mapping,
-        _: &Vec<String>,
-        vars: &HashMap<String, ExtensionResult>,
-    ) -> super::ExtensionOut {
-        let target = params.get(&Value::from("target"));
-
-        if let Some(target) = target {
-            let value = vars.get(target.as_str().unwrap_or_default());
-            Ok(Some(value.unwrap().clone()))
-        } else {
-            Ok(None)
-        }
-    }
-}
diff --git a/src/guard.rs b/src/guard.rs
deleted file mode 100644
index 036b2833..00000000
--- a/src/guard.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::config::Configs;
-use log::debug;
-use std::sync::atomic::Ordering::Release;
-use std::sync::{atomic::AtomicBool, Arc};
-
-pub struct InjectGuard {
-    is_injecting: Arc<AtomicBool>,
-    post_inject_delay: u64,
-}
-
-impl InjectGuard {
-    pub fn new(is_injecting: Arc<AtomicBool>, config: &Configs) -> Self {
-        debug!("enabling inject guard");
-
-        // Enable the injecting block
-        is_injecting.store(true, Release);
-
-        Self {
-            is_injecting,
-            post_inject_delay: config.post_inject_delay,
-        }
-    }
-}
-
-impl Drop for InjectGuard {
-    fn drop(&mut self) {
-        // Because the keyinjection is async, we need to wait a bit before
-        // giving back the control. Otherwise, the injected actions will be handled back
-        // by espanso itself.
-        std::thread::sleep(std::time::Duration::from_millis(self.post_inject_delay));
-
-        debug!("releasing inject guard");
-
-        // Re-allow espanso to interpret actions
-        self.is_injecting.store(false, Release);
-    }
-}
diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs
deleted file mode 100644
index dd05f678..00000000
--- a/src/keyboard/linux.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::PasteShortcut;
-use crate::bridge::linux::*;
-use crate::config::Configs;
-use log::error;
-use std::ffi::CString;
-
-pub struct LinuxKeyboardManager {}
-
-impl super::KeyboardManager for LinuxKeyboardManager {
-    fn send_string(&self, active_config: &Configs, s: &str) {
-        let res = CString::new(s);
-        match res {
-            Ok(cstr) => unsafe {
-                if active_config.fast_inject {
-                    fast_send_string(cstr.as_ptr(), active_config.inject_delay);
-                } else {
-                    send_string(cstr.as_ptr());
-                }
-            },
-            Err(e) => panic!(e.to_string()),
-        }
-    }
-
-    fn send_enter(&self, active_config: &Configs) {
-        unsafe {
-            if active_config.fast_inject {
-                fast_send_enter();
-            } else {
-                send_enter();
-            }
-        }
-    }
-
-    fn trigger_paste(&self, active_config: &Configs) {
-        unsafe {
-            match active_config.paste_shortcut {
-                PasteShortcut::Default => {
-                    let is_special = is_current_window_special();
-
-                    // Terminals use a different keyboard combination to paste from clipboard,
-                    // so we need to check the correct situation.
-                    if is_special == 0 {
-                        trigger_paste();
-                    }else if is_special == 2 {  // Special case for stterm
-                        trigger_alt_shift_ins_paste();
-                    }else if is_special == 3 {  // Special case for Emacs
-                        trigger_shift_ins_paste();
-                    }else if is_special == 4 {  // CTRL+ALT+V used in some terminals (urxvt)
-                        trigger_ctrl_alt_paste();
-                    }else{
-                        trigger_terminal_paste();
-                    }
-                },
-                PasteShortcut::CtrlV => {
-                    trigger_paste();
-                },
-                PasteShortcut::CtrlShiftV => {
-                    trigger_terminal_paste();
-                },
-                PasteShortcut::ShiftInsert=> {
-                    trigger_shift_ins_paste();
-                },
-                PasteShortcut::CtrlAltV => {
-                    trigger_ctrl_alt_paste();
-                },
-                _ => {
-                    error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
-                }
-            }
-        }
-    }
-
-    fn delete_string(&self, active_config: &Configs, count: i32) {
-        unsafe {
-            if active_config.fast_inject {
-                fast_delete_string(count, active_config.backspace_delay);
-            } else {
-                delete_string(count)
-            }
-        }
-    }
-
-    fn move_cursor_left(&self, active_config: &Configs, count: i32) {
-        unsafe {
-            if active_config.fast_inject {
-                fast_left_arrow(count);
-            } else {
-                left_arrow(count);
-            }
-        }
-    }
-
-    fn trigger_copy(&self, _: &Configs) {
-        unsafe {
-            trigger_copy();
-        }
-    }
-}
diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs
deleted file mode 100644
index b7d9699d..00000000
--- a/src/keyboard/macos.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::PasteShortcut;
-use crate::bridge::macos::*;
-use crate::config::Configs;
-use log::error;
-use std::ffi::CString;
-
-pub struct MacKeyboardManager {}
-
-impl super::KeyboardManager for MacKeyboardManager {
-    fn send_string(&self, _: &Configs, s: &str) {
-        let res = CString::new(s);
-        match res {
-            Ok(cstr) => unsafe {
-                send_string(cstr.as_ptr());
-            },
-            Err(e) => panic!(e.to_string()),
-        }
-    }
-
-    fn send_enter(&self, _: &Configs) {
-        unsafe {
-            // Send the kVK_Return key press
-            send_vkey(0x24);
-        }
-    }
-
-    fn trigger_paste(&self, active_config: &Configs) {
-        unsafe {
-            match active_config.paste_shortcut {
-                PasteShortcut::Default => {
-                    unsafe {
-                        trigger_paste();
-                    }
-                },
-                _ => {
-                    error!("MacOS backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
-                }
-            }
-        }
-    }
-
-    fn trigger_copy(&self, _: &Configs) {
-        unsafe {
-            trigger_copy();
-        }
-    }
-
-    fn delete_string(&self, _: &Configs, count: i32) {
-        unsafe { delete_string(count) }
-    }
-
-    fn move_cursor_left(&self, _: &Configs, count: i32) {
-        unsafe {
-            // Simulate the Left arrow count times
-            send_multi_vkey(0x7B, count);
-        }
-    }
-}
-
-pub fn wait_for_modifiers_release() -> bool {
-    let start = std::time::SystemTime::now();
-    while start.elapsed().unwrap_or_default().as_millis() < 3000 {
-        let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() };
-        if pressed == 0 {
-            return true;
-        }
-        std::thread::sleep(std::time::Duration::from_millis(100));
-    }
-    false
-}
diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs
deleted file mode 100644
index d9f11a4e..00000000
--- a/src/keyboard/mod.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::config::Configs;
-use log::warn;
-use serde::{Deserialize, Serialize};
-
-#[cfg(target_os = "windows")]
-pub mod windows;
-
-#[cfg(target_os = "linux")]
-mod linux;
-
-#[cfg(target_os = "macos")]
-pub mod macos;
-
-pub trait KeyboardManager {
-    fn send_string(&self, active_config: &Configs, s: &str);
-    fn send_enter(&self, active_config: &Configs);
-    fn trigger_paste(&self, active_config: &Configs);
-    fn delete_string(&self, active_config: &Configs, count: i32);
-    fn move_cursor_left(&self, active_config: &Configs, count: i32);
-    fn trigger_copy(&self, active_config: &Configs);
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub enum PasteShortcut {
-    Default,     // Default one for the current system
-    CtrlV,       // Classic Ctrl+V shortcut
-    CtrlShiftV,  // Could be used to paste without formatting in many applications
-    ShiftInsert, // Often used in Linux systems
-    CtrlAltV,    // Used in some Linux terminals (urxvt)
-    MetaV,       // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
-}
-
-impl Default for PasteShortcut {
-    fn default() -> Self {
-        PasteShortcut::Default
-    }
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn get_manager() -> impl KeyboardManager {
-    windows::WindowsKeyboardManager {}
-}
-
-// LINUX IMPLEMENTATION
-#[cfg(target_os = "linux")]
-pub fn get_manager() -> impl KeyboardManager {
-    linux::LinuxKeyboardManager {}
-}
-
-// MAC IMPLEMENTATION
-#[cfg(target_os = "macos")]
-pub fn get_manager() -> impl KeyboardManager {
-    macos::MacKeyboardManager {}
-}
-
-// These methods are used to wait until all modifiers are released (or timeout occurs)
-pub fn wait_for_modifiers_release() {
-    #[cfg(target_os = "windows")]
-    let released = crate::keyboard::windows::wait_for_modifiers_release();
-
-    #[cfg(target_os = "macos")]
-    let released = crate::keyboard::macos::wait_for_modifiers_release();
-
-    #[cfg(target_os = "linux")]
-    let released = true; // NOOP on linux (at least for now)
-
-    if !released {
-        warn!("Wait for modifiers release timed out! Please release your modifiers keys (CTRL, CMD, ALT, SHIFT) after typing the trigger");
-    }
-}
diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs
deleted file mode 100644
index 507d4dd9..00000000
--- a/src/keyboard/windows.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::PasteShortcut;
-use crate::bridge::windows::*;
-use crate::config::Configs;
-use log::error;
-use widestring::U16CString;
-
-pub struct WindowsKeyboardManager {}
-
-impl super::KeyboardManager for WindowsKeyboardManager {
-    fn send_string(&self, _: &Configs, s: &str) {
-        let res = U16CString::from_str(s);
-        match res {
-            Ok(s) => unsafe {
-                send_string(s.as_ptr());
-            },
-            Err(e) => println!("Error while sending string: {}", e.to_string()),
-        }
-    }
-
-    fn send_enter(&self, _: &Configs) {
-        unsafe {
-            // Send the VK_RETURN key press
-            send_vkey(0x0D);
-        }
-    }
-
-    fn trigger_paste(&self, active_config: &Configs) {
-        unsafe {
-            match active_config.paste_shortcut {
-                PasteShortcut::Default => {
-                    unsafe {
-                        trigger_paste();
-                    }
-                },
-                PasteShortcut::CtrlShiftV => {
-                    trigger_shift_paste();
-                },
-                _ => {
-                    error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
-                }
-            }
-        }
-    }
-
-    fn delete_string(&self, config: &Configs, count: i32) {
-        unsafe { delete_string(count, config.backspace_delay) }
-    }
-
-    fn move_cursor_left(&self, _: &Configs, count: i32) {
-        unsafe {
-            // Send the left arrow key multiple times
-            send_multi_vkey(0x25, count)
-        }
-    }
-
-    fn trigger_copy(&self, _: &Configs) {
-        unsafe {
-            trigger_copy();
-        }
-    }
-}
-
-pub fn wait_for_modifiers_release() -> bool {
-    let start = std::time::SystemTime::now();
-    while start.elapsed().unwrap_or_default().as_millis() < 3000 {
-        let pressed = unsafe { crate::bridge::windows::are_modifiers_pressed() };
-        if pressed == 0 {
-            return true;
-        }
-        std::thread::sleep(std::time::Duration::from_millis(100));
-    }
-    false
-}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index 294ad3c3..00000000
--- a/src/main.rs
+++ /dev/null
@@ -1,1449 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#![cfg_attr(not(test), windows_subsystem = "windows")]
-
-#[macro_use]
-extern crate lazy_static;
-
-use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
-use regex::Regex;
-use std::fs::{File, OpenOptions};
-use std::io::{BufRead, BufReader};
-use std::process::exit;
-use std::process::{Command, Stdio};
-use std::sync::atomic::AtomicBool;
-use std::sync::mpsc::channel;
-use std::sync::mpsc::{Receiver, RecvError, Sender};
-use std::sync::{mpsc, Arc};
-use std::thread;
-use std::time::Duration;
-
-use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
-use fs2::FileExt;
-use log::{error, info, warn, LevelFilter};
-use simplelog::{CombinedLogger, SharedLogger, TermLogger, TerminalMode, WriteLogger};
-
-use crate::config::runtime::RuntimeConfigManager;
-use crate::config::{ConfigManager, ConfigSet, Configs};
-use crate::engine::Engine;
-use crate::event::manager::{DefaultEventManager, EventManager};
-use crate::event::*;
-use crate::matcher::scrolling::ScrollingMatcher;
-use crate::package::default::DefaultPackageManager;
-use crate::package::zip::ZipPackageResolver;
-use crate::package::{InstallResult, PackageManager, RemoveResult, UpdateResult};
-use crate::protocol::*;
-use crate::system::SystemManager;
-use crate::ui::UIManager;
-
-mod bridge;
-mod check;
-mod cli;
-mod clipboard;
-mod config;
-mod context;
-mod edit;
-mod engine;
-mod event;
-mod extension;
-mod guard;
-mod keyboard;
-mod matcher;
-mod package;
-mod process;
-mod protocol;
-mod render;
-mod sysdaemon;
-mod system;
-mod ui;
-mod utils;
-
-const VERSION: &str = env!("CARGO_PKG_VERSION");
-const LOG_FILE: &str = "espanso.log";
-
-fn main() {
-    attach_console();
-
-    let install_subcommand = SubCommand::with_name("install")
-        .about("Install a package. Equivalent to 'espanso package install'")
-        .arg(
-            Arg::with_name("external")
-                .short("e")
-                .long("external")
-                .required(false)
-                .takes_value(false)
-                .help("Allow installing packages from non-verified repositories."),
-        )
-        .arg(Arg::with_name("package_name").help("Package name"))
-        .arg(
-            Arg::with_name("repository_url")
-                .help("(Optional) Link to GitHub repository")
-                .required(false)
-                .default_value("hub"),
-        )
-        .arg(
-            Arg::with_name("proxy")
-                .help("Use a proxy, should be used as --proxy=https://proxy:1234")
-                .required(false)
-                .long("proxy")
-                .takes_value(true),
-        );
-
-    let uninstall_subcommand = SubCommand::with_name("uninstall")
-        .about("Remove an installed package. Equivalent to 'espanso package uninstall'")
-        .arg(Arg::with_name("package_name").help("Package name"));
-
-    let mut clap_instance = App::new("espanso")
-        .version(VERSION)
-        .author("Federico Terzi")
-        .about("Cross-platform Text Expander written in Rust")
-        .arg(Arg::with_name("v")
-            .short("v")
-            .multiple(true)
-            .help("Sets the level of verbosity"))
-        .subcommand(SubCommand::with_name("cmd")
-            .about("Send a command to the espanso daemon.")
-            .subcommand(SubCommand::with_name("exit")
-                .about("Terminate the daemon."))
-            .subcommand(SubCommand::with_name("enable")
-                .about("Enable the espanso replacement engine."))
-            .subcommand(SubCommand::with_name("disable")
-                .about("Disable the espanso replacement engine."))
-            .subcommand(SubCommand::with_name("toggle")
-                .about("Toggle the status of the espanso replacement engine."))
-        )
-        .subcommand(SubCommand::with_name("edit")
-            .about("Open the default text editor to edit config files and reload them automatically when exiting")
-            .arg(Arg::with_name("config")
-                .help("Defaults to \"default\". The configuration file name to edit (without the .yml extension)."))
-            .arg(Arg::with_name("norestart")
-                .short("n")
-                .long("norestart")
-                .required(false)
-                .takes_value(false)
-                .help("Avoid restarting espanso after editing the file"))
-        )
-        .subcommand(SubCommand::with_name("dump")
-            .about("Prints all current configuration options."))
-        .subcommand(SubCommand::with_name("detect")
-            .about("Tool to detect current window properties, to simplify filters creation."))
-        .subcommand(SubCommand::with_name("daemon")
-            .about("Start the daemon without spawning a new process."))
-        .subcommand(SubCommand::with_name("register")
-            .about("MacOS and Linux only. Register espanso in the system daemon manager."))
-        .subcommand(SubCommand::with_name("unregister")
-            .about("MacOS and Linux only. Unregister espanso from the system daemon manager."))
-        .subcommand(SubCommand::with_name("log")
-            .about("Print the latest daemon logs."))
-        .subcommand(SubCommand::with_name("start")
-            .about("Start the daemon spawning a new process in the background."))
-        .subcommand(SubCommand::with_name("stop")
-            .about("Stop the espanso daemon."))
-        .subcommand(SubCommand::with_name("restart")
-            .about("Restart the espanso daemon."))
-        .subcommand(SubCommand::with_name("status")
-            .about("Check if the espanso daemon is running or not."))
-        .subcommand(SubCommand::with_name("path")
-            .about("Prints all the current espanso directory paths, to easily locate configuration and data paths.")
-            .subcommand(SubCommand::with_name("config")
-                .about("Print the current config folder path."))
-            .subcommand(SubCommand::with_name("packages")
-                .about("Print the current packages folder path."))
-            .subcommand(SubCommand::with_name("data")
-                .about("Print the current data folder path."))
-            .subcommand(SubCommand::with_name("default")
-                .about("Print the default configuration file path."))
-        )
-        .subcommand(SubCommand::with_name("match")
-            .about("List and execute matches from the CLI")
-            .subcommand(SubCommand::with_name("list")
-                .about("Print all matches to standard output")
-                .arg(Arg::with_name("json")
-                    .short("j")
-                    .long("json")
-                    .help("Return the matches as json")
-                    .required(false)
-                    .takes_value(false)
-                )
-                .arg(Arg::with_name("onlytriggers")
-                    .short("t")
-                    .long("onlytriggers")
-                    .help("Print only triggers without replacement")
-                    .required(false)
-                    .takes_value(false)
-                )
-                .arg(Arg::with_name("preservenewlines")
-                    .short("n")
-                    .long("preservenewlines")
-                    .help("Preserve newlines when printing replacements")
-                    .required(false)
-                    .takes_value(false)
-                )
-            )
-            .subcommand(SubCommand::with_name("exec")
-                .about("Triggers the expansion of the given match")
-                .arg(Arg::with_name("trigger")
-                    .help("The trigger of the match to be expanded")
-                )
-            )
-        )
-        // Package manager
-        .subcommand(SubCommand::with_name("package")
-            .about("Espanso package manager commands")
-            .subcommand(install_subcommand.clone())
-            .subcommand(uninstall_subcommand.clone())
-            .subcommand(SubCommand::with_name("list")
-                .about("List all installed packages")
-                .arg(Arg::with_name("full")
-                    .help("Print all package info")
-                    .long("full")))
-
-            .subcommand(SubCommand::with_name("refresh")
-                .about("Update espanso package index"))
-        )
-        .subcommand(SubCommand::with_name("worker")
-            .setting(AppSettings::Hidden)
-            .arg(Arg::with_name("reload")
-                .short("r")
-                .long("reload")
-                .required(false)
-                .takes_value(false))
-        )
-        .subcommand(install_subcommand)
-        .subcommand(uninstall_subcommand);
-
-    let matches = clap_instance.clone().get_matches();
-
-    // The edit subcommand must be run before the configuration parsing. Otherwise, if the
-    // configuration is corrupted, the edit command won't work, which makes it pretty useless.
-    if let Some(matches) = matches.subcommand_matches("edit") {
-        edit_main(matches);
-        return;
-    }
-
-    let log_level = matches.occurrences_of("v") as i32;
-
-    // Load the configuration
-    let mut config_set = ConfigSet::load_default().unwrap_or_else(|e| {
-        println!("{}", e);
-        exit(1);
-    });
-
-    config_set.default.log_level = log_level;
-
-    // Commands that require the configuration
-
-    if let Some(matches) = matches.subcommand_matches("cmd") {
-        cmd_main(config_set, matches);
-        return;
-    }
-
-    if matches.subcommand_matches("dump").is_some() {
-        println!("{:#?}", config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("detect").is_some() {
-        detect_main();
-        return;
-    }
-
-    if matches.subcommand_matches("daemon").is_some() {
-        daemon_main(config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("register").is_some() {
-        register_main(config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("unregister").is_some() {
-        unregister_main(config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("log").is_some() {
-        log_main();
-        return;
-    }
-
-    if matches.subcommand_matches("start").is_some() {
-        start_main(config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("status").is_some() {
-        status_main();
-        return;
-    }
-
-    if matches.subcommand_matches("stop").is_some() {
-        stop_main(config_set);
-        return;
-    }
-
-    if matches.subcommand_matches("restart").is_some() {
-        restart_main(config_set);
-        return;
-    }
-
-    if let Some(matches) = matches.subcommand_matches("install") {
-        install_main(config_set, matches);
-        return;
-    }
-
-    if let Some(matches) = matches.subcommand_matches("uninstall") {
-        remove_package_main(config_set, matches);
-        return;
-    }
-
-    if let Some(matches) = matches.subcommand_matches("path") {
-        path_main(config_set, matches);
-        return;
-    }
-
-    if let Some(matches) = matches.subcommand_matches("match") {
-        match_main(config_set, matches);
-        return;
-    }
-
-    if let Some(matches) = matches.subcommand_matches("package") {
-        if let Some(matches) = matches.subcommand_matches("install") {
-            install_main(config_set, matches);
-            return;
-        }
-        if let Some(matches) = matches.subcommand_matches("uninstall") {
-            remove_package_main(config_set, matches);
-            return;
-        }
-        if let Some(matches) = matches.subcommand_matches("list") {
-            list_package_main(config_set, matches);
-            return;
-        }
-        if matches.subcommand_matches("refresh").is_some() {
-            update_index_main(config_set);
-            return;
-        }
-    }
-
-    if let Some(matches) = matches.subcommand_matches("worker") {
-        worker_main(config_set, matches);
-        return;
-    }
-
-    // Defaults help print
-    clap_instance
-        .print_long_help()
-        .expect("Unable to print help");
-    println!();
-}
-
-#[cfg(target_os = "windows")]
-fn attach_console() {
-    // When using the windows subsystem we loose the terminal output.
-    // Therefore we try to attach to the current console if available.
-    unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) };
-}
-
-#[cfg(not(target_os = "windows"))]
-fn attach_console() {
-    // Not necessary on Linux and macOS
-}
-
-fn init_logger(config_set: &ConfigSet, reset: bool) {
-    // Initialize log
-    let log_level = match config_set.default.log_level {
-        0 => LevelFilter::Warn,
-        1 => LevelFilter::Info,
-        2 | _ => LevelFilter::Debug,
-    };
-
-    let mut log_outputs: Vec<Box<dyn SharedLogger>> = Vec::new();
-
-    // Initialize terminal output
-    let terminal_out =
-        TermLogger::new(log_level, simplelog::Config::default(), TerminalMode::Mixed);
-    if let Some(terminal_out) = terminal_out {
-        log_outputs.push(terminal_out);
-    }
-
-    // Initialize log file output
-    let espanso_dir = context::get_data_dir();
-    let log_file_path = espanso_dir.join(LOG_FILE);
-
-    if reset && log_file_path.exists() {
-        std::fs::remove_file(&log_file_path).expect("unable to remove log file");
-    }
-
-    let log_file = OpenOptions::new()
-        .read(true)
-        .write(true)
-        .create(true)
-        .append(true)
-        .open(log_file_path)
-        .expect("Cannot create log file.");
-    let file_out = WriteLogger::new(LevelFilter::Info, simplelog::Config::default(), log_file);
-    log_outputs.push(file_out);
-
-    CombinedLogger::init(log_outputs).expect("Error opening log destination");
-
-    // Activate logging for panics
-    log_panics::init();
-}
-
-/// Daemon subcommand, start the event loop and spawn a background thread worker
-fn daemon_main(config_set: ConfigSet) {
-    // Try to acquire lock file
-    let lock_file = acquire_lock();
-    if lock_file.is_none() {
-        println!("espanso is already running.");
-        exit(3);
-    }
-
-    precheck_guard();
-
-    init_logger(&config_set, true);
-
-    info!("espanso version {}", VERSION);
-    info!(
-        "using config path: {}",
-        context::get_config_dir().to_string_lossy()
-    );
-    info!(
-        "using package path: {}",
-        context::get_package_dir().to_string_lossy()
-    );
-
-    let (send_channel, receive_channel) = mpsc::channel();
-
-    let ipc_server = protocol::get_ipc_server(
-        Service::Daemon,
-        config_set.default.clone(),
-        send_channel.clone(),
-    );
-    ipc_server.start();
-
-    info!("spawning worker process...");
-
-    let espanso_path = std::env::current_exe().expect("unable to obtain espanso path location");
-    let mut child = crate::process::spawn_process(
-        &espanso_path.to_string_lossy().to_string(),
-        &vec!["worker".to_owned()],
-    )
-    .expect("unable to create worker process");
-
-    // Create a monitor thread that will exit with the same non-zero code if
-    // the worker thread exits
-    thread::Builder::new()
-        .name("worker monitor".to_string())
-        .spawn(move || {
-            let result = child.wait();
-            if let Ok(status) = result {
-                if let Some(code) = status.code() {
-                    if code != 0 {
-                        error!(
-                            "worker process exited with non-zero code: {}, exiting",
-                            code
-                        );
-                        std::process::exit(code);
-                    }
-                }
-            }
-        })
-        .expect("Unable to spawn worker monitor thread");
-
-    register_signals(config_set.default.clone());
-
-    std::thread::sleep(Duration::from_millis(200));
-
-    if config_set.default.auto_restart {
-        let send_channel_clone = send_channel.clone();
-        thread::Builder::new()
-            .name("watcher_background".to_string())
-            .spawn(move || {
-                watcher_background(send_channel_clone);
-            })
-            .expect("Unable to spawn watcher background thread");
-    }
-
-    loop {
-        match receive_channel.recv() {
-            Ok(event) => {
-                match event {
-                    Event::Action(ActionType::RestartWorker) => {
-                        // Terminate the worker process
-                        send_command_or_warn(
-                            Service::Worker,
-                            config_set.default.clone(),
-                            IPCCommand::exit_worker(),
-                        );
-
-                        std::thread::sleep(Duration::from_millis(500));
-
-                        // Restart the worker process
-                        crate::process::spawn_process(
-                            &espanso_path.to_string_lossy().to_string(),
-                            &vec!["worker".to_owned(), "--reload".to_owned()],
-                        );
-                    }
-                    Event::Action(ActionType::Exit) => {
-                        send_command_or_warn(
-                            Service::Worker,
-                            config_set.default.clone(),
-                            IPCCommand::exit_worker(),
-                        );
-
-                        std::thread::sleep(Duration::from_millis(200));
-
-                        info!("terminating espanso.");
-                        std::process::exit(0);
-                    }
-                    _ => {
-                        // Forward the command to the worker
-                        let command = IPCCommand::from(event);
-                        if let Some(command) = command {
-                            send_command_or_warn(
-                                Service::Worker,
-                                config_set.default.clone(),
-                                command,
-                            );
-                        }
-                    }
-                }
-            }
-            Err(e) => {
-                warn!("error while reading event in daemon process: {}", e);
-            }
-        }
-    }
-}
-
-#[cfg(target_os = "windows")]
-fn register_signals(_: Configs) {}
-
-#[cfg(not(target_os = "windows"))]
-fn register_signals(config: Configs) {
-    // On Unix, also listen for signals so that we can terminate the
-    // worker if the daemon receives a signal
-    use signal_hook::{iterator::Signals, SIGINT, SIGTERM};
-    let signals = Signals::new(&[SIGTERM, SIGINT]).expect("unable to register for signals");
-    thread::Builder::new()
-        .name("signal monitor".to_string())
-        .spawn(move || {
-            for signal in signals.forever() {
-                info!("Received signal: {:?}, terminating worker", signal);
-                send_command_or_warn(Service::Worker, config, IPCCommand::exit_worker());
-
-                std::thread::sleep(Duration::from_millis(200));
-
-                info!("terminating espanso.");
-                std::process::exit(0);
-            }
-        })
-        .expect("Unable to spawn signal monitor thread");
-}
-
-fn watcher_background(sender: Sender<Event>) {
-    // Create a channel to receive the events.
-    let (tx, rx) = channel();
-
-    let mut watcher: RecommendedWatcher =
-        Watcher::new(tx, Duration::from_secs(1)).expect("unable to create file watcher");
-
-    let config_path = crate::context::get_config_dir();
-    watcher
-        .watch(&config_path, RecursiveMode::Recursive)
-        .expect("unable to start watcher");
-
-    info!(
-        "watching for changes in path: {}",
-        config_path.to_string_lossy()
-    );
-
-    loop {
-        let should_reload = match rx.recv() {
-            Ok(event) => {
-                let path = match event {
-                    DebouncedEvent::Create(path) => Some(path),
-                    DebouncedEvent::Write(path) => Some(path),
-                    DebouncedEvent::Remove(path) => Some(path),
-                    DebouncedEvent::Rename(_, path) => Some(path),
-                    _ => None,
-                };
-
-                if let Some(path) = path {
-                    if path.extension().unwrap_or_default() == "yml"
-                        && !path
-                            .file_name()
-                            .unwrap_or_default()
-                            .to_string_lossy()
-                            .starts_with(".")
-                    {
-                        // Only load non-hidden yml files
-                        true
-                    } else {
-                        false
-                    }
-                } else {
-                    false
-                }
-            }
-            Err(e) => {
-                warn!("error while watching files: {:?}", e);
-                false
-            }
-        };
-
-        if should_reload {
-            info!("change detected, restarting worker process...");
-
-            let mut config_set = ConfigSet::load_default();
-
-            match config_set {
-                Ok(config_set) => {
-                    let event = Event::Action(ActionType::RestartWorker);
-                    sender.send(event).unwrap_or_else(|e| {
-                        warn!("unable to communicate with daemon thread: {}", e);
-                    })
-                }
-                Err(error) => {
-                    error!("Unable to reload configuration due to an error: {}", error);
-                    let event = Event::System(SystemEvent::NotifyRequest(
-                        "Unable to reload config due to an error, see the logs for more details."
-                            .to_owned(),
-                    ));
-                    sender.send(event).unwrap_or_else(|e| {
-                        warn!("unable to communicate with daemon thread: {}", e);
-                    })
-                }
-            }
-        }
-    }
-}
-
-/// Worker process main which does the actual work
-fn worker_main(config_set: ConfigSet, matches: &ArgMatches) {
-    init_logger(&config_set, false);
-
-    info!("initializing worker process...");
-
-    let is_reloading: bool = if matches.is_present("reload") {
-        true
-    } else {
-        false
-    };
-
-    let (send_channel, receive_channel) = mpsc::channel();
-
-    // This atomic bool is used to "disable" espanso when espanding its own matches, otherwise
-    // we could reinterpret the characters we are injecting
-    let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false));
-
-    let context = context::new(
-        config_set.default.clone(),
-        send_channel.clone(),
-        is_injecting.clone(),
-    );
-
-    let config_set_copy = config_set.clone();
-    thread::Builder::new()
-        .name("daemon_background".to_string())
-        .spawn(move || {
-            worker_background(receive_channel, config_set_copy, is_injecting, is_reloading);
-        })
-        .expect("Unable to spawn daemon background thread");
-
-    let ipc_server =
-        protocol::get_ipc_server(Service::Worker, config_set.default, send_channel.clone());
-    ipc_server.start();
-
-    context.eventloop();
-}
-
-/// Background thread worker for the daemon
-fn worker_background(
-    receive_channel: Receiver<Event>,
-    config_set: ConfigSet,
-    is_injecting: Arc<AtomicBool>,
-    is_reloading: bool,
-) {
-    let system_manager = system::get_manager();
-    let config_manager = RuntimeConfigManager::new(config_set, system_manager);
-
-    let ui_manager = ui::get_uimanager();
-    if config_manager.default_config().show_notifications {
-        if !is_reloading {
-            ui_manager.notify("espanso is running!");
-        } else {
-            ui_manager.notify("Reloaded config!");
-        }
-    }
-
-    let clipboard_manager = clipboard::get_manager();
-
-    let keyboard_manager = keyboard::get_manager();
-
-    let extensions = extension::get_extensions(
-        config_manager.default_config(),
-        Box::new(clipboard::get_manager()),
-    );
-
-    let renderer =
-        render::default::DefaultRenderer::new(extensions, config_manager.default_config().clone());
-
-    let engine = Engine::new(
-        &keyboard_manager,
-        &clipboard_manager,
-        &config_manager,
-        &ui_manager,
-        &renderer,
-        is_injecting,
-    );
-
-    let matcher = ScrollingMatcher::new(&config_manager, &engine);
-
-    let event_manager = DefaultEventManager::new(
-        receive_channel,
-        vec![&matcher],
-        vec![&engine, &matcher],
-        vec![&engine],
-    );
-
-    info!("worker is running!");
-
-    event_manager.eventloop();
-}
-
-/// start subcommand, spawn a background espanso process.
-fn start_main(config_set: ConfigSet) {
-    // Try to acquire lock file
-    let lock_file = acquire_lock();
-    if lock_file.is_none() {
-        println!("espanso is already running.");
-        exit(3);
-    }
-    release_lock(lock_file.unwrap());
-
-    precheck_guard();
-
-    start_daemon(config_set);
-}
-
-#[cfg(target_os = "windows")]
-fn start_daemon(_: ConfigSet) {
-    unsafe {
-        let res = bridge::windows::start_daemon_process();
-        if res < 0 {
-            println!("Error starting daemon process");
-        }
-    }
-}
-
-#[cfg(target_os = "macos")]
-fn start_daemon(config_set: ConfigSet) {
-    if config_set.default.use_system_agent {
-        use std::process::Command;
-
-        let res = Command::new("launchctl")
-            .args(&["start", "com.federicoterzi.espanso"])
-            .status();
-
-        if let Ok(status) = res {
-            if status.success() {
-                println!("Daemon started correctly!")
-            } else {
-                eprintln!("Error starting launchd daemon with status: {}", status);
-            }
-        } else {
-            eprintln!("Error starting launchd daemon: {}", res.unwrap_err());
-        }
-    } else {
-        fork_daemon(config_set);
-    }
-}
-
-#[cfg(target_os = "linux")]
-fn start_daemon(config_set: ConfigSet) {
-    use crate::sysdaemon::{verify, VerifyResult};
-    use std::process::{Command, Stdio};
-
-    // Check if Systemd is available in the system
-    let status = Command::new("systemctl")
-        .args(&["--version"])
-        .stdin(Stdio::null())
-        .stdout(Stdio::null())
-        .status();
-
-    // If Systemd is not available in the system, espanso should default to unmanaged mode
-    // See issue https://github.com/federico-terzi/espanso/issues/139
-    let force_unmanaged = if let Err(_) = status { true } else { false };
-
-    if config_set.default.use_system_agent && !force_unmanaged {
-        // Make sure espanso is currently registered in systemd
-        let res = verify();
-        match res {
-            VerifyResult::EnabledAndValid => {
-                // Do nothing, everything is ok!
-            }
-            VerifyResult::EnabledButInvalidPath => {
-                eprintln!("Updating espanso service file with new path...");
-                unregister_main(config_set.clone());
-                register_main(config_set);
-            }
-            VerifyResult::NotEnabled => {
-                use dialoguer::Confirmation;
-                if Confirmation::new()
-                    .with_text("espanso must be registered to systemd (user level) first. Do you want to proceed?")
-                    .default(true)
-                    .show_default(true)
-                    .interact().expect("Unable to read user answer") {
-
-                    register_main(config_set);
-                }else{
-                    eprintln!("Please register espanso to systemd with this command:");
-                    eprintln!("   espanso register");
-                    // TODO: enable flag to use non-managed daemon mode
-
-                    std::process::exit(4);
-                }
-            }
-        }
-
-        // Start the espanso service
-        let res = Command::new("systemctl")
-            .args(&["--user", "start", "espanso.service"])
-            .status();
-
-        if let Ok(status) = res {
-            if status.success() {
-                println!("Daemon started correctly!")
-            } else {
-                eprintln!("Error starting systemd daemon with status: {}", status);
-            }
-        } else {
-            eprintln!("Error starting systemd daemon: {}", res.unwrap_err());
-        }
-    } else {
-        if force_unmanaged {
-            eprintln!("Systemd is not available in this system, switching to unmanaged mode.");
-        }
-
-        fork_daemon(config_set);
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-fn fork_daemon(config_set: ConfigSet) {
-    unsafe {
-        let pid = libc::fork();
-        if pid < 0 {
-            println!("Unable to fork.");
-            exit(4);
-        }
-        if pid > 0 {
-            // Parent process exit
-            println!("daemon started!");
-            exit(0);
-        }
-
-        // Spawned process
-
-        // Create a new SID for the child process
-        let sid = libc::setsid();
-        if sid < 0 {
-            exit(5);
-        }
-
-        // Detach stdout and stderr
-        let null_path = std::ffi::CString::new("/dev/null").expect("CString unwrap failed");
-        let fd = libc::open(null_path.as_ptr(), libc::O_RDWR, 0);
-        if fd != -1 {
-            libc::dup2(fd, libc::STDIN_FILENO);
-            libc::dup2(fd, libc::STDOUT_FILENO);
-            libc::dup2(fd, libc::STDERR_FILENO);
-        }
-    }
-
-    daemon_main(config_set);
-}
-
-/// status subcommand, print the current espanso status
-fn status_main() {
-    let lock_file = acquire_lock();
-    if let Some(lock_file) = lock_file {
-        println!("espanso is not running");
-
-        release_lock(lock_file);
-    } else {
-        println!("espanso is running");
-    }
-}
-
-/// Stop subcommand, used to stop the daemon.
-fn stop_main(config_set: ConfigSet) {
-    // Try to acquire lock file
-    let lock_file = acquire_lock();
-    if lock_file.is_some() {
-        println!("espanso daemon is not running.");
-        release_lock(lock_file.unwrap());
-        exit(3);
-    }
-
-    send_command_or_warn(Service::Daemon, config_set.default, IPCCommand::exit());
-}
-
-/// Kill the daemon if running and start it again
-fn restart_main(config_set: ConfigSet) {
-    // Kill the daemon if running
-    let lock_file = acquire_lock();
-    if lock_file.is_none() {
-        // Terminate the current espanso daemon
-        send_command_or_warn(
-            Service::Daemon,
-            config_set.default.clone(),
-            IPCCommand::exit(),
-        );
-    } else {
-        release_lock(lock_file.unwrap());
-    }
-
-    std::thread::sleep(Duration::from_millis(500));
-
-    // Restart the daemon
-    start_main(config_set);
-}
-
-/// Cli tool used to analyze active windows to extract useful information
-/// to create configuration filters.
-#[cfg(not(target_os = "macos"))]
-fn detect_main() {
-    let system_manager = system::get_manager();
-
-    println!("Listening for changes, now focus the window you want to analyze.");
-    println!("You can terminate with CTRL+C\n");
-
-    let mut last_title: String = "".to_owned();
-    let mut last_class: String = "".to_owned();
-    let mut last_exec: String = "".to_owned();
-
-    loop {
-        let curr_title = system_manager
-            .get_current_window_title()
-            .unwrap_or_default();
-        let curr_class = system_manager
-            .get_current_window_class()
-            .unwrap_or_default();
-        let curr_exec = system_manager
-            .get_current_window_executable()
-            .unwrap_or_default();
-
-        // Check if a change occurred
-        if curr_title != last_title || curr_class != last_class || curr_exec != last_exec {
-            println!("Detected change, current window has properties:");
-            println!("==> Title: '{}'", curr_title);
-            println!("==> Class: '{}'", curr_class);
-            println!("==> Executable: '{}'", curr_exec);
-            println!();
-        }
-
-        last_title = curr_title;
-        last_class = curr_class;
-        last_exec = curr_exec;
-
-        thread::sleep(Duration::from_millis(500));
-    }
-}
-
-/// Cli tool used to analyze active windows to extract useful information
-/// to create configuration filters.
-/// On macOS version we need to start an event loop for the app to register changes.
-#[cfg(target_os = "macos")]
-fn detect_main() {
-    thread::spawn(|| {
-        use std::io::stdout;
-        use std::io::Write;
-
-        let system_manager = system::get_manager();
-
-        println!("Listening for changes, now focus the window you want to analyze.");
-        println!(
-            "Warning: stay on the window for a few seconds, as it may take a while to register."
-        );
-        println!("You can terminate with CTRL+C\n");
-
-        let mut last_title: String = "".to_owned();
-        let mut last_class: String = "".to_owned();
-        let mut last_exec: String = "".to_owned();
-
-        loop {
-            let curr_title = system_manager
-                .get_current_window_title()
-                .unwrap_or_default();
-            let curr_class = system_manager
-                .get_current_window_class()
-                .unwrap_or_default();
-            let curr_exec = system_manager
-                .get_current_window_executable()
-                .unwrap_or_default();
-
-            // Check if a change occurred
-            if curr_title != last_title || curr_class != last_class || curr_exec != last_exec {
-                println!("Detected change, current window has properties:");
-                println!("==> Title: '{}'", curr_title);
-                println!("==> Class: '{}'", curr_class);
-                println!("==> Executable: '{}'", curr_exec);
-                println!();
-            }
-
-            last_title = curr_title;
-            last_class = curr_class;
-            last_exec = curr_exec;
-
-            thread::sleep(Duration::from_millis(500));
-        }
-    });
-
-    unsafe {
-        crate::bridge::macos::headless_eventloop();
-    }
-}
-
-/// Send the given command to the espanso daemon
-fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) {
-    let command = if matches.subcommand_matches("exit").is_some() {
-        Some(IPCCommand::exit())
-    } else if matches.subcommand_matches("toggle").is_some() {
-        Some(IPCCommand {
-            id: String::from("toggle"),
-            payload: String::from(""),
-        })
-    } else if matches.subcommand_matches("enable").is_some() {
-        Some(IPCCommand {
-            id: String::from("enable"),
-            payload: String::from(""),
-        })
-    } else if matches.subcommand_matches("disable").is_some() {
-        Some(IPCCommand {
-            id: String::from("disable"),
-            payload: String::from(""),
-        })
-    } else {
-        None
-    };
-
-    if let Some(command) = command {
-        send_command_or_warn(Service::Daemon, config_set.default, command);
-    }
-
-    exit(1);
-}
-
-fn log_main() {
-    let espanso_dir = context::get_data_dir();
-    let log_file_path = espanso_dir.join(LOG_FILE);
-
-    if !log_file_path.exists() {
-        println!("No log file found.");
-        exit(2);
-    }
-
-    let log_file = File::open(log_file_path);
-    if let Ok(log_file) = log_file {
-        let reader = BufReader::new(log_file);
-        for line in reader.lines() {
-            if let Ok(line) = line {
-                println!("{}", line);
-            }
-        }
-
-        exit(0);
-    } else {
-        println!("Error reading log file");
-        exit(1);
-    }
-}
-
-fn register_main(config_set: ConfigSet) {
-    sysdaemon::register(config_set);
-}
-
-fn unregister_main(config_set: ConfigSet) {
-    sysdaemon::unregister(config_set);
-}
-
-fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
-    let package_name = matches.value_of("package_name").unwrap_or_else(|| {
-        eprintln!("Missing package name!");
-        exit(1);
-    });
-
-    let mut repository = matches.value_of("repository_url").unwrap_or("hub");
-
-    // Remove trailing .git string if present
-    // See: https://github.com/federico-terzi/espanso/issues/326
-    if repository.ends_with(".git") {
-        repository = repository.trim_end_matches(".git")
-    }
-
-    let proxy = match matches.value_of("proxy") {
-        Some(proxy) => {
-            println!("Using proxy: {}", proxy);
-            Some(proxy.to_string())
-        }
-        None => None,
-    };
-
-    let package_resolver = Box::new(ZipPackageResolver::new());
-
-    let allow_external: bool = if matches.is_present("external") {
-        println!("Allowing external repositories");
-        true
-    } else {
-        false
-    };
-
-    let mut package_manager = DefaultPackageManager::new_default(Some(package_resolver));
-
-    let res = if repository == "hub" {
-        // Installation from the Hub
-        if package_manager.is_index_outdated() {
-            println!("Updating package index...");
-            let res = package_manager.update_index(false);
-
-            match res {
-                Ok(update_result) => match update_result {
-                    UpdateResult::NotOutdated => {
-                        eprintln!("Index was already up to date");
-                    }
-                    UpdateResult::Updated => {
-                        println!("Index updated!");
-                    }
-                },
-                Err(e) => {
-                    eprintln!("{}", e);
-                    exit(2);
-                }
-            }
-        } else {
-            println!("Using cached package index, run 'espanso package refresh' to update it.")
-        }
-
-        package_manager.install_package(package_name, allow_external, proxy)
-    } else {
-        // Make sure the repo is a valid github url
-        lazy_static! {
-            static ref GITHUB_REGEX: Regex = Regex::new(r#"https://github\.com/\S*/\S*"#).unwrap();
-        };
-
-        if !GITHUB_REGEX.is_match(repository) {
-            eprintln!("repository url is not valid, it should be an HTTPS GitHub url in the following format:");
-            eprintln!("https://github.com/user/repo");
-            exit(3);
-        }
-
-        if !allow_external {
-            Ok(InstallResult::BlockedExternalPackage(repository.to_owned()))
-        } else {
-            package_manager.install_package_from_repo(package_name, repository, proxy)
-        }
-    };
-
-    match res {
-        Ok(install_result) => match install_result {
-            InstallResult::NotFoundInIndex => {
-                eprintln!("Package not found");
-            }
-            InstallResult::NotFoundInRepo => {
-                eprintln!(
-                    "Package not found in repository, are you sure the folder exist in the repo?"
-                );
-            }
-            InstallResult::UnableToParsePackageInfo => {
-                eprintln!("Unable to parse Package info from README.md");
-            }
-            InstallResult::MissingPackageVersion => {
-                eprintln!("Missing package version");
-            }
-            InstallResult::AlreadyInstalled => {
-                eprintln!("{} already installed!", package_name);
-            }
-            InstallResult::BlockedExternalPackage(repo_url) => {
-                eprintln!("Warning: the requested package is hosted on an external repository:");
-                eprintln!();
-                eprintln!("{}", repo_url);
-                eprintln!();
-                eprintln!("and its contents may not have been verified by espanso.");
-                eprintln!();
-                eprintln!("For your security, espanso blocks packages that are not verified.");
-                eprintln!("If you want to install the package anyway, you can force espanso");
-                eprintln!("to install it with the following command, but please do it only");
-                eprintln!("if you trust the source or you verified the contents of the package");
-                eprintln!("by checking out the repository listed above.");
-                eprintln!();
-
-                if repository == "hub" {
-                    eprintln!("espanso install {} --external", package_name);
-                } else {
-                    eprintln!("espanso install {} {} --external", package_name, repository);
-                }
-                eprintln!();
-            }
-            InstallResult::Installed => {
-                println!("{} successfully installed!", package_name);
-                println!();
-                println!("You need to restart espanso for changes to take effect, using:");
-                println!("  espanso restart");
-            }
-        },
-        Err(e) => {
-            eprintln!("{}", e);
-        }
-    }
-}
-
-fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) {
-    let package_name = matches.value_of("package_name").unwrap_or_else(|| {
-        eprintln!("Missing package name!");
-        exit(1);
-    });
-
-    let package_manager = DefaultPackageManager::new_default(None);
-
-    let res = package_manager.remove_package(package_name);
-
-    match res {
-        Ok(remove_result) => match remove_result {
-            RemoveResult::NotFound => {
-                eprintln!("{} package was not installed.", package_name);
-            }
-            RemoveResult::Removed => {
-                println!("{} successfully removed!", package_name);
-                println!();
-                println!("You need to restart espanso for changes to take effect, using:");
-                println!("  espanso restart");
-            }
-        },
-        Err(e) => {
-            eprintln!("{}", e);
-        }
-    }
-}
-
-fn update_index_main(_config_set: ConfigSet) {
-    let mut package_manager = DefaultPackageManager::new_default(None);
-
-    let res = package_manager.update_index(true);
-
-    match res {
-        Ok(update_result) => match update_result {
-            UpdateResult::NotOutdated => {
-                eprintln!("Index was already up to date");
-            }
-            UpdateResult::Updated => {
-                println!("Index updated!");
-            }
-        },
-        Err(e) => {
-            eprintln!("{}", e);
-            exit(2);
-        }
-    }
-}
-
-fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) {
-    let package_manager = DefaultPackageManager::new_default(None);
-
-    let list = package_manager.list_local_packages();
-
-    if matches.is_present("full") {
-        for package in list.iter() {
-            println!("{:?}", package);
-        }
-    } else {
-        for package in list.iter() {
-            println!("{} - {}", package.name, package.version);
-        }
-    }
-}
-
-fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
-    let config = crate::context::get_config_dir();
-    let packages = crate::context::get_package_dir();
-    let data = crate::context::get_data_dir();
-
-    if matches.subcommand_matches("config").is_some() {
-        println!("{}", config.to_string_lossy());
-    } else if matches.subcommand_matches("packages").is_some() {
-        println!("{}", packages.to_string_lossy());
-    } else if matches.subcommand_matches("data").is_some() {
-        println!("{}", data.to_string_lossy());
-    } else if matches.subcommand_matches("default").is_some() {
-        let default_file = config.join(crate::config::DEFAULT_CONFIG_FILE_NAME);
-        println!("{}", default_file.to_string_lossy());
-    } else {
-        println!("Config: {}", config.to_string_lossy());
-        println!("Packages: {}", packages.to_string_lossy());
-        println!("Data: {}", data.to_string_lossy());
-    }
-}
-
-fn match_main(config_set: ConfigSet, matches: &ArgMatches) {
-    if let Some(matches) = matches.subcommand_matches("list") {
-        let json = matches.is_present("json");
-        let onlytriggers = matches.is_present("onlytriggers");
-        let preserve_newlines = matches.is_present("preservenewlines");
-
-        if !json {
-            crate::cli::list_matches(config_set, onlytriggers, preserve_newlines);
-        } else {
-            crate::cli::list_matches_as_json(config_set);
-        }
-    } else if let Some(matches) = matches.subcommand_matches("exec") {
-        let trigger = matches.value_of("trigger").unwrap_or_else(|| {
-            eprintln!("missing trigger");
-            exit(1);
-        });
-
-        send_command_or_warn(
-            Service::Worker,
-            config_set.default.clone(),
-            IPCCommand::trigger(trigger),
-        );
-    }
-}
-
-fn edit_main(matches: &ArgMatches) {
-    // Determine which is the file to edit
-    let config = matches.value_of("config").unwrap_or("default");
-
-    let config_dir = crate::context::get_config_dir();
-
-    let config_path = match config {
-        "default" => config_dir.join(crate::config::DEFAULT_CONFIG_FILE_NAME),
-        name => {
-            // Otherwise, search in the user/ config folder
-            config_dir
-                .join(crate::config::USER_CONFIGS_FOLDER_NAME)
-                .join(name.to_owned() + ".yml")
-        }
-    };
-
-    println!("Editing file: {:?}", &config_path);
-
-    // Based on the fact that the file already exists or not, we should detect in different
-    // ways if a reload is needed
-    let should_reload = if config_path.exists() {
-        // Get the last modified date, so that we can detect if the user actually edits the file
-        // before reloading
-        let metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
-        let last_modified = metadata
-            .modified()
-            .expect("cannot read file last modified date");
-
-        let result = crate::edit::open_editor(&config_path);
-        if result {
-            let new_metadata =
-                std::fs::metadata(&config_path).expect("cannot gather file metadata");
-            let new_last_modified = new_metadata
-                .modified()
-                .expect("cannot read file last modified date");
-
-            if last_modified != new_last_modified {
-                println!("File has been modified, reloading configuration");
-                true
-            } else {
-                println!("File has not been modified, avoiding reload");
-                false
-            }
-        } else {
-            false
-        }
-    } else {
-        let result = crate::edit::open_editor(&config_path);
-        if result {
-            // If the file has been created, we should reload the espanso config
-            if config_path.exists() {
-                println!("A new file has been created, reloading configuration");
-                true
-            } else {
-                println!("No file has been created, avoiding reload");
-                false
-            }
-        } else {
-            false
-        }
-    };
-
-    let no_restart: bool = if matches.is_present("norestart") {
-        println!("Avoiding automatic restart");
-        true
-    } else {
-        false
-    };
-
-    if should_reload && !no_restart {
-        // Load the configuration
-        let config_set = ConfigSet::load_default().unwrap_or_else(|e| {
-            eprintln!("{}", e);
-            eprintln!("Unable to reload espanso due to previous configuration error.");
-            exit(1);
-        });
-
-        restart_main(config_set)
-    }
-}
-
-fn acquire_lock() -> Option<File> {
-    acquire_custom_lock("espanso.lock")
-}
-
-fn acquire_custom_lock(name: &str) -> Option<File> {
-    let espanso_dir = context::get_data_dir();
-    let lock_file_path = espanso_dir.join(name);
-    let file = OpenOptions::new()
-        .read(true)
-        .write(true)
-        .create(true)
-        .open(lock_file_path)
-        .expect("Cannot create reference to lock file.");
-
-    let res = file.try_lock_exclusive();
-
-    if res.is_ok() {
-        return Some(file);
-    }
-
-    None
-}
-
-fn release_lock(lock_file: File) {
-    lock_file.unlock().unwrap()
-}
-
-/// Used to make sure all the required dependencies and conditions are satisfied before starting espanso.
-fn precheck_guard() {
-    let satisfied = check::check_preconditions();
-    if !satisfied {
-        println!();
-        println!("Pre-check was not successful, espanso could not be started.");
-        exit(5);
-    }
-}
diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs
deleted file mode 100644
index c8f82865..00000000
--- a/src/matcher/mod.rs
+++ /dev/null
@@ -1,801 +0,0 @@
-/*
- * This file is part of espans{ name: (), var_type: (), params: ()}
- *
- * Copyright (C) 2019 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 crate::event::KeyEventReceiver;
-use crate::event::{KeyEvent, KeyModifier};
-use regex::{Captures, Regex};
-use serde::{Deserialize, Deserializer, Serialize};
-use serde_yaml::{Mapping, Value};
-use std::fs;
-use std::path::PathBuf;
-use std::{borrow::Cow, collections::HashMap};
-
-pub(crate) mod scrolling;
-
-#[derive(Debug, Serialize, Clone)]
-pub struct Match {
-    pub triggers: Vec<String>,
-    pub content: MatchContentType,
-    pub word: bool,
-    pub passive_only: bool,
-    pub propagate_case: bool,
-    pub force_clipboard: bool,
-    pub is_html: bool,
-
-    // Automatically calculated from the triggers, used by the matcher to check for correspondences.
-    #[serde(skip_serializing)]
-    pub _trigger_sequences: Vec<Vec<TriggerEntry>>,
-}
-
-#[derive(Debug, Serialize, Clone)]
-pub enum MatchContentType {
-    Text(TextContent),
-    Image(ImageContent),
-}
-
-#[derive(Debug, Serialize, Clone, PartialEq)]
-pub struct TextContent {
-    pub replace: String,
-    pub vars: Vec<MatchVariable>,
-
-    #[serde(skip_serializing)]
-    pub _has_vars: bool,
-}
-
-#[derive(Debug, Serialize, Clone)]
-pub struct ImageContent {
-    pub path: PathBuf,
-}
-
-impl<'de> serde::Deserialize<'de> for Match {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let auto_match = AutoMatch::deserialize(deserializer)?;
-        Ok(Match::from(&auto_match))
-    }
-}
-
-impl<'a> From<&'a AutoMatch> for Match {
-    fn from(other: &'a AutoMatch) -> Self {
-        lazy_static! {
-            static ref VAR_REGEX: Regex =
-                Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
-        };
-
-        let mut triggers = if !other.triggers.is_empty() {
-            other.triggers.clone()
-        } else if !other.trigger.is_empty() {
-            vec![other.trigger.clone()]
-        } else {
-            panic!("Match does not have any trigger defined: {:?}", other)
-        };
-
-        // If propagate_case is true, we need to generate all the possible triggers
-        // For example, specifying "hello" as a trigger, we need to have:
-        // "hello", "Hello", "HELLO"
-        if other.propagate_case {
-            // List with first letter capitalized
-            let first_capitalized: Vec<String> = triggers
-                .iter()
-                .map(|trigger| {
-                    let capitalized = trigger.clone();
-                    let mut v: Vec<char> = capitalized.chars().collect();
-
-                    // Capitalize the first alphabetic letter
-                    // See issue #244
-                    let first_alphabetic = v.iter().position(|c| c.is_alphabetic()).unwrap_or(0);
-
-                    v[first_alphabetic] = v[first_alphabetic].to_uppercase().nth(0).unwrap();
-                    v.into_iter().collect()
-                })
-                .collect();
-
-            let all_capitalized: Vec<String> = triggers
-                .iter()
-                .map(|trigger| trigger.to_uppercase())
-                .collect();
-
-            triggers.extend(first_capitalized);
-            triggers.extend(all_capitalized);
-        }
-
-        let trigger_sequences = triggers
-            .iter()
-            .map(|trigger| {
-                // Calculate the trigger sequence
-                let mut trigger_sequence = Vec::new();
-                let trigger_chars: Vec<char> = trigger.chars().collect();
-                trigger_sequence.extend(trigger_chars.into_iter().map(|c| TriggerEntry::Char(c)));
-                if other.word {
-                    // If it's a word match, end with a word separator
-                    trigger_sequence.push(TriggerEntry::WordSeparator);
-                }
-
-                trigger_sequence
-            })
-            .collect();
-
-        let (text_content, is_html) = if let Some(replace) = &other.replace {
-            (Some(Cow::from(replace)), false)
-        } else if let Some(markdown_str) = &other.markdown {
-            // Render the markdown into HTML
-            let mut html = markdown::to_html(markdown_str);
-            html = html.trim().to_owned();
-
-            if !other.paragraph {
-                // Remove the surrounding paragraph
-                if html.starts_with("<p>") {
-                    html = html.trim_start_matches("<p>").to_owned();
-                }
-                if html.ends_with("</p>") {
-                    html = html.trim_end_matches("</p>").to_owned();
-                }
-            }
-
-            (Some(Cow::from(html)), true)
-        } else if let Some(html) = &other.html {
-            (Some(Cow::from(html)), true)
-        } else {
-            (None, false)
-        };
-
-        let content = if let Some(content) = text_content {
-            // Check if the match contains variables
-            let has_vars = VAR_REGEX.is_match(&content);
-
-            let content = TextContent {
-                replace: content.to_string(),
-                vars: other.vars.clone(),
-                _has_vars: has_vars,
-            };
-
-            MatchContentType::Text(content)
-        } else if let Some(form) = &other.form {
-            // Form shorthand
-            // Replace all the form fields with actual variables
-            let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| {
-                let var_name = caps.get(1).unwrap().as_str();
-                format!("{{{{form1.{}}}}}", var_name)
-            });
-            let new_replace = new_replace.to_string();
-
-            // Convert escaped brakets in forms
-            let form = form.replace("\\{", "{ ").replace("\\}", " }");
-
-            // Convert the form data to valid variables
-            let mut params = Mapping::new();
-            if let Some(fields) = &other.form_fields {
-                let mut mapping_fields = Mapping::new();
-                fields.iter().for_each(|(key, value)| {
-                    mapping_fields.insert(Value::from(key.to_owned()), Value::from(value.clone()));
-                });
-                params.insert(Value::from("fields"), Value::from(mapping_fields));
-            }
-            params.insert(Value::from("layout"), Value::from(form));
-
-            let vars = vec![MatchVariable {
-                name: "form1".to_owned(),
-                var_type: "form".to_owned(),
-                params,
-            }];
-
-            let content = TextContent {
-                replace: new_replace,
-                vars,
-                _has_vars: true,
-            };
-
-            MatchContentType::Text(content)
-        } else if let Some(image_path) = &other.image_path {
-            // Image match
-            // On Windows, we have to replace the forward / with the backslash \ in the path
-            let new_path = if cfg!(target_os = "windows") {
-                image_path.replace("/", "\\")
-            } else {
-                image_path.to_owned()
-            };
-
-            // Calculate variables in path
-            let new_path = if new_path.contains("$CONFIG") {
-                let config_dir = crate::context::get_config_dir();
-                let config_path = fs::canonicalize(&config_dir);
-                let config_path = if let Ok(config_path) = config_path {
-                    config_path.to_string_lossy().into_owned()
-                } else {
-                    "".to_owned()
-                };
-                new_path.replace("$CONFIG", &config_path)
-            } else {
-                new_path.to_owned()
-            };
-
-            let content = ImageContent {
-                path: PathBuf::from(new_path),
-            };
-
-            MatchContentType::Image(content)
-        } else {
-            eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'markdown', 'html', image_path' or 'form'", other.trigger);
-            std::process::exit(2);
-        };
-
-        Self {
-            triggers,
-            content,
-            word: other.word,
-            passive_only: other.passive_only,
-            _trigger_sequences: trigger_sequences,
-            propagate_case: other.propagate_case,
-            force_clipboard: other.force_clipboard,
-            is_html,
-        }
-    }
-}
-
-/// Used to deserialize the Match struct before applying some custom elaboration.
-#[derive(Debug, Serialize, Deserialize, Clone)]
-struct AutoMatch {
-    #[serde(default = "default_trigger")]
-    pub trigger: String,
-
-    #[serde(default = "default_triggers")]
-    pub triggers: Vec<String>,
-
-    #[serde(default = "default_replace")]
-    pub replace: Option<String>,
-
-    #[serde(default = "default_image_path")]
-    pub image_path: Option<String>,
-
-    #[serde(default = "default_form")]
-    pub form: Option<String>,
-
-    #[serde(default = "default_form_fields")]
-    pub form_fields: Option<HashMap<String, Value>>,
-
-    #[serde(default = "default_vars")]
-    pub vars: Vec<MatchVariable>,
-
-    #[serde(default = "default_word")]
-    pub word: bool,
-
-    #[serde(default = "default_passive_only")]
-    pub passive_only: bool,
-
-    #[serde(default = "default_propagate_case")]
-    pub propagate_case: bool,
-
-    #[serde(default = "default_force_clipboard")]
-    pub force_clipboard: bool,
-
-    #[serde(default = "default_markdown")]
-    pub markdown: Option<String>,
-
-    #[serde(default = "default_paragraph")]
-    pub paragraph: bool,
-
-    #[serde(default = "default_html")]
-    pub html: Option<String>,
-}
-
-fn default_trigger() -> String {
-    "".to_owned()
-}
-fn default_triggers() -> Vec<String> {
-    Vec::new()
-}
-fn default_vars() -> Vec<MatchVariable> {
-    Vec::new()
-}
-fn default_word() -> bool {
-    false
-}
-fn default_passive_only() -> bool {
-    false
-}
-fn default_replace() -> Option<String> {
-    None
-}
-fn default_form() -> Option<String> {
-    None
-}
-fn default_form_fields() -> Option<HashMap<String, Value>> {
-    None
-}
-fn default_image_path() -> Option<String> {
-    None
-}
-fn default_propagate_case() -> bool {
-    false
-}
-fn default_force_clipboard() -> bool {
-    false
-}
-fn default_markdown() -> Option<String> {
-    None
-}
-fn default_paragraph() -> bool {
-    false
-}
-fn default_html() -> Option<String> {
-    None
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
-pub struct MatchVariable {
-    pub name: String,
-
-    #[serde(rename = "type")]
-    pub var_type: String,
-
-    #[serde(default = "default_params")]
-    pub params: Mapping,
-}
-
-fn default_params() -> Mapping {
-    Mapping::new()
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
-pub enum TriggerEntry {
-    Char(char),
-    WordSeparator,
-}
-
-pub trait MatchReceiver {
-    fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
-    fn on_enable_update(&self, status: bool);
-    fn on_passive(&self);
-    fn on_undo(&self);
-}
-
-pub trait Matcher: KeyEventReceiver {
-    fn handle_char(&self, c: &str);
-    fn handle_modifier(&self, m: KeyModifier);
-    fn handle_other(&self);
-}
-
-impl<M: Matcher> KeyEventReceiver for M {
-    fn on_key_event(&self, e: KeyEvent) {
-        match e {
-            KeyEvent::Char(c) => {
-                self.handle_char(&c);
-            }
-            KeyEvent::Modifier(m) => {
-                self.handle_modifier(m);
-            }
-            KeyEvent::Other => {
-                self.handle_other();
-            }
-        }
-    }
-}
-
-// TESTS
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_match_has_vars_should_be_false() {
-        let match_str = r###"
-        trigger: ":test"
-        replace: "There are no variables"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(content._has_vars, false);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_has_vars_should_be_true() {
-        let match_str = r###"
-        trigger: ":test"
-        replace: "There are {{one}} and {{two}} variables"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(content._has_vars, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_has_vars_with_spaces_should_be_true() {
-        let match_str = r###"
-        trigger: ":test"
-        replace: "There is {{ one }} variable"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(content._has_vars, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_trigger_sequence_without_word() {
-        let match_str = r###"
-        trigger: "test"
-        replace: "This is a test"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
-        assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
-        assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
-    }
-
-    #[test]
-    fn test_match_trigger_sequence_with_word() {
-        let match_str = r###"
-        trigger: "test"
-        replace: "This is a test"
-        word: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
-        assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
-        assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
-    }
-
-    #[test]
-    fn test_match_with_image_content() {
-        let match_str = r###"
-        trigger: "test"
-        image_path: "/path/to/file"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Image(content) => {
-                assert_eq!(content.path, PathBuf::from("/path/to/file"));
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_trigger_populates_triggers_vector() {
-        let match_str = r###"
-        trigger: ":test"
-        replace: "This is a test"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec![":test"])
-    }
-
-    #[test]
-    fn test_match_triggers_are_correctly_parsed() {
-        let match_str = r###"
-        triggers:
-          - ":test1"
-          - :test2
-        replace: "This is a test"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec![":test1", ":test2"])
-    }
-
-    #[test]
-    fn test_match_triggers_are_correctly_parsed_square_brackets() {
-        let match_str = r###"
-        triggers: [":test1", ":test2"]
-        replace: "This is a test"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec![":test1", ":test2"])
-    }
-
-    #[test]
-    fn test_match_propagate_case() {
-        let match_str = r###"
-        trigger: "hello"
-        replace: "This is a test"
-        propagate_case: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"])
-    }
-
-    #[test]
-    fn test_match_propagate_case_multi_trigger() {
-        let match_str = r###"
-        triggers: ["hello", "hi"]
-        replace: "This is a test"
-        propagate_case: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(
-            _match.triggers,
-            vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"]
-        )
-    }
-
-    #[test]
-    fn test_match_trigger_sequence_with_word_propagate_case() {
-        let match_str = r###"
-        trigger: "test"
-        replace: "This is a test"
-        word: true
-        propagate_case: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
-        assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
-        assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
-
-        assert_eq!(_match._trigger_sequences[1][0], TriggerEntry::Char('T'));
-        assert_eq!(_match._trigger_sequences[1][1], TriggerEntry::Char('e'));
-        assert_eq!(_match._trigger_sequences[1][2], TriggerEntry::Char('s'));
-        assert_eq!(_match._trigger_sequences[1][3], TriggerEntry::Char('t'));
-        assert_eq!(_match._trigger_sequences[1][4], TriggerEntry::WordSeparator);
-
-        assert_eq!(_match._trigger_sequences[2][0], TriggerEntry::Char('T'));
-        assert_eq!(_match._trigger_sequences[2][1], TriggerEntry::Char('E'));
-        assert_eq!(_match._trigger_sequences[2][2], TriggerEntry::Char('S'));
-        assert_eq!(_match._trigger_sequences[2][3], TriggerEntry::Char('T'));
-        assert_eq!(_match._trigger_sequences[2][4], TriggerEntry::WordSeparator);
-    }
-
-    #[test]
-    fn test_match_empty_replace_doesnt_crash() {
-        let match_str = r###"
-        trigger: "hello"
-        replace: ""
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-    }
-
-    #[test]
-    fn test_match_propagate_case_with_prefix_symbol() {
-        let match_str = r###"
-        trigger: ":hello"
-        replace: "This is a test"
-        propagate_case: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec![":hello", ":Hello", ":HELLO"])
-    }
-
-    #[test]
-    fn test_match_propagate_case_non_alphabetic_should_not_crash() {
-        let match_str = r###"
-        trigger: ":.."
-        replace: "This is a test"
-        propagate_case: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        assert_eq!(_match.triggers, vec![":..", ":..", ":.."])
-    }
-
-    #[test]
-    fn test_match_form_translated_correctly() {
-        let match_str = r###"
-        trigger: ":test"
-        form: "Hey {{name}}, how are you? {{greet}}"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-        match _match.content {
-            MatchContentType::Text(content) => {
-                let mut mapping = Mapping::new();
-                mapping.insert(
-                    Value::from("layout"),
-                    Value::from("Hey {{name}}, how are you? {{greet}}"),
-                );
-                assert_eq!(
-                    content,
-                    TextContent {
-                        replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
-                        _has_vars: true,
-                        vars: vec![MatchVariable {
-                            name: "form1".to_owned(),
-                            var_type: "form".to_owned(),
-                            params: mapping,
-                        }]
-                    }
-                );
-            }
-            _ => panic!("wrong content"),
-        }
-    }
-
-    #[test]
-    fn test_match_form_with_fields_translated_correctly() {
-        let match_str = r###"
-        trigger: ":test"
-        form: "Hey {{name}}, how are you? {{greet}}"
-        form_fields:
-          name:
-            multiline: true
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-        match _match.content {
-            MatchContentType::Text(content) => {
-                let mut name_mapping = Mapping::new();
-                name_mapping.insert(Value::from("multiline"), Value::Bool(true));
-                let mut submapping = Mapping::new();
-                submapping.insert(Value::from("name"), Value::from(name_mapping));
-                let mut mapping = Mapping::new();
-                mapping.insert(Value::from("fields"), Value::from(submapping));
-                mapping.insert(
-                    Value::from("layout"),
-                    Value::from("Hey {{name}}, how are you? {{greet}}"),
-                );
-                assert_eq!(
-                    content,
-                    TextContent {
-                        replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
-                        _has_vars: true,
-                        vars: vec![MatchVariable {
-                            name: "form1".to_owned(),
-                            var_type: "form".to_owned(),
-                            params: mapping,
-                        }]
-                    }
-                );
-            }
-            _ => panic!("wrong content"),
-        }
-    }
-
-    #[test]
-    fn test_match_markdown_loaded_correctly() {
-        let match_str = r###"
-        trigger: ":test"
-        markdown: "This *text* is **very bold**"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(
-                    content.replace,
-                    "This <em>text</em> is <strong>very bold</strong>"
-                );
-                assert_eq!(_match.is_html, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_markdown_keep_vars() {
-        let match_str = r###"
-        trigger: ":test"
-        markdown: "This *text* is {{variable}} **very bold**"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(
-                    content.replace,
-                    "This <em>text</em> is {{variable}} <strong>very bold</strong>"
-                );
-                assert_eq!(_match.is_html, true);
-                assert_eq!(content._has_vars, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_html_loaded_correctly() {
-        let match_str = r###"
-        trigger: ":test"
-        html: "This <i>text<i> is <b>very bold</b>"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(content.replace, "This <i>text<i> is <b>very bold</b>");
-                assert_eq!(_match.is_html, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-
-    #[test]
-    fn test_match_html_keep_vars() {
-        let match_str = r###"
-        trigger: ":test"
-        html: "This <i>text<i> is {{var}} <b>very bold</b>"
-        "###;
-
-        let _match: Match = serde_yaml::from_str(match_str).unwrap();
-
-        match _match.content {
-            MatchContentType::Text(content) => {
-                assert_eq!(
-                    content.replace,
-                    "This <i>text<i> is {{var}} <b>very bold</b>"
-                );
-                assert_eq!(_match.is_html, true);
-                assert_eq!(content._has_vars, true);
-            }
-            _ => {
-                assert!(false);
-            }
-        }
-    }
-}
diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs
deleted file mode 100644
index d78cb48f..00000000
--- a/src/matcher/scrolling.rs
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::config::ConfigManager;
-use crate::event::KeyModifier::{BACKSPACE, CAPS_LOCK, LEFT_SHIFT, RIGHT_SHIFT};
-use crate::event::{ActionEventReceiver, ActionType, KeyModifier};
-use crate::matcher::{Match, MatchReceiver, TriggerEntry};
-use std::cell::RefCell;
-use std::collections::VecDeque;
-use std::time::SystemTime;
-
-pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
-    config_manager: &'a M,
-    receiver: &'a R,
-    current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
-    toggle_press_time: RefCell<SystemTime>,
-    passive_press_time: RefCell<SystemTime>,
-    is_enabled: RefCell<bool>,
-    was_previous_char_word_separator: RefCell<bool>,
-    was_previous_char_a_match: RefCell<bool>,
-}
-
-#[derive(Clone)]
-struct MatchEntry<'a> {
-    start: usize,
-    count: usize,
-    trigger_offset: usize, // The index of the trigger in the Match that matched
-    _match: &'a Match,
-}
-
-impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
-    pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> {
-        let current_set_queue = RefCell::new(VecDeque::new());
-        let toggle_press_time = RefCell::new(SystemTime::now());
-        let passive_press_time = RefCell::new(SystemTime::now());
-
-        ScrollingMatcher {
-            config_manager,
-            receiver,
-            current_set_queue,
-            toggle_press_time,
-            passive_press_time,
-            is_enabled: RefCell::new(true),
-            was_previous_char_word_separator: RefCell::new(true),
-            was_previous_char_a_match: RefCell::new(true),
-        }
-    }
-
-    fn toggle(&self) {
-        let mut is_enabled = self.is_enabled.borrow_mut();
-        *is_enabled = !(*is_enabled);
-
-        self.receiver.on_enable_update(*is_enabled);
-    }
-
-    fn set_enabled(&self, enabled: bool) {
-        let mut is_enabled = self.is_enabled.borrow_mut();
-        *is_enabled = enabled;
-
-        self.receiver.on_enable_update(*is_enabled);
-    }
-
-    fn is_matching(
-        mtc: &Match,
-        current_char: &str,
-        start: usize,
-        trigger_offset: usize,
-        is_current_word_separator: bool,
-    ) -> bool {
-        match mtc._trigger_sequences[trigger_offset][start] {
-            TriggerEntry::Char(c) => current_char.starts_with(c),
-            TriggerEntry::WordSeparator => is_current_word_separator,
-        }
-    }
-}
-
-impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> {
-    fn handle_char(&self, c: &str) {
-        // if not enabled, avoid any processing
-        if !*(self.is_enabled.borrow()) {
-            return;
-        }
-
-        // Obtain the configuration for the active application if present,
-        // otherwise get the default one
-        let active_config = self.config_manager.active_config();
-
-        // Check if the current char is a word separator
-        let mut is_current_word_separator = active_config
-            .word_separators
-            .contains(&c.chars().nth(0).unwrap_or_default());
-
-        let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
-        (*was_previous_char_a_match) = false;
-
-        let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
-
-        let mut current_set_queue = self.current_set_queue.borrow_mut();
-
-        let mut new_matches: Vec<MatchEntry> = Vec::new();
-
-        for m in active_config.matches.iter() {
-            // only active-enabled matches are considered
-            if m.passive_only {
-                continue;
-            }
-
-            for trigger_offset in 0..m._trigger_sequences.len() {
-                let mut result =
-                    Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator);
-
-                if m.word {
-                    result = result && *was_previous_word_separator
-                }
-
-                if result {
-                    new_matches.push(MatchEntry {
-                        start: 1,
-                        count: m._trigger_sequences[trigger_offset].len(),
-                        trigger_offset,
-                        _match: &m,
-                    });
-                }
-            }
-        }
-        // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
-
-        let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
-            Some(last_matches) => {
-                let mut updated: Vec<MatchEntry> = last_matches
-                    .iter()
-                    .filter(|&x| {
-                        Self::is_matching(
-                            x._match,
-                            c,
-                            x.start,
-                            x.trigger_offset,
-                            is_current_word_separator,
-                        )
-                    })
-                    .map(|x| MatchEntry {
-                        start: x.start + 1,
-                        count: x.count,
-                        trigger_offset: x.trigger_offset,
-                        _match: &x._match,
-                    })
-                    .collect();
-
-                updated.extend(new_matches);
-                updated
-            }
-            None => new_matches,
-        };
-
-        let mut found_entry = None;
-
-        for entry in combined_matches.iter() {
-            if entry.start == entry.count {
-                found_entry = Some(entry.clone());
-                break;
-            }
-        }
-
-        current_set_queue.push_back(combined_matches);
-
-        if current_set_queue.len() as i32
-            > (self.config_manager.default_config().backspace_limit + 1)
-        {
-            current_set_queue.pop_front();
-        }
-
-        *was_previous_word_separator = is_current_word_separator;
-
-        if let Some(entry) = found_entry {
-            let mtc = entry._match;
-
-            current_set_queue.clear();
-
-            let trailing_separator = if !mtc.word {
-                // If it's not a word match, it cannot have a trailing separator
-                None
-            } else if !is_current_word_separator {
-                None
-            } else {
-                let as_char = c.chars().nth(0);
-                match as_char {
-                    Some(c) => {
-                        Some(c) // Current char is the trailing separator
-                    }
-                    None => None,
-                }
-            };
-
-            // Force espanso to consider the last char as a separator
-            *was_previous_word_separator = true;
-
-            self.receiver
-                .on_match(mtc, trailing_separator, entry.trigger_offset);
-
-            (*was_previous_char_a_match) = true;
-        }
-    }
-
-    fn handle_modifier(&self, m: KeyModifier) {
-        let config = self.config_manager.default_config();
-
-        let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
-
-        // TODO: at the moment, activating the passive key triggers the toggle key
-        // study a mechanism to avoid this problem
-
-        if KeyModifier::shallow_equals(&m, &config.toggle_key) {
-            check_interval(
-                &self.toggle_press_time,
-                u128::from(config.toggle_interval),
-                || {
-                    self.toggle();
-
-                    let is_enabled = self.is_enabled.borrow();
-
-                    if !*is_enabled {
-                        self.current_set_queue.borrow_mut().clear();
-                    }
-                },
-            );
-        } else if KeyModifier::shallow_equals(&m, &config.passive_key) {
-            check_interval(
-                &self.passive_press_time,
-                u128::from(config.toggle_interval),
-                || {
-                    self.receiver.on_passive();
-                },
-            );
-        }
-
-        // Backspace handling, basically "rewinding history"
-        if m == BACKSPACE {
-            let mut current_set_queue = self.current_set_queue.borrow_mut();
-            current_set_queue.pop_back();
-
-            if (*was_previous_char_a_match) {
-                current_set_queue.clear();
-                self.receiver.on_undo();
-            }
-        }
-
-        // Disable the "backspace undo" feature
-        (*was_previous_char_a_match) = false;
-
-        // Consider modifiers as separators to improve word matches reliability
-        if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
-            let mut was_previous_char_word_separator =
-                self.was_previous_char_word_separator.borrow_mut();
-            *was_previous_char_word_separator = true;
-        }
-    }
-
-    fn handle_other(&self) {
-        // When receiving "other" type of events, we mark them as valid separators.
-        // This dramatically improves the reliability of word matches
-        let mut was_previous_char_word_separator =
-            self.was_previous_char_word_separator.borrow_mut();
-        *was_previous_char_word_separator = true;
-
-        // Disable the "backspace undo" feature
-        let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
-        (*was_previous_char_a_match) = false;
-    }
-}
-
-impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver
-    for ScrollingMatcher<'a, R, M>
-{
-    fn on_action_event(&self, e: ActionType) {
-        match e {
-            ActionType::Toggle => {
-                self.toggle();
-            }
-            ActionType::Enable => {
-                self.set_enabled(true);
-            }
-            ActionType::Disable => {
-                self.set_enabled(false);
-            }
-            _ => {}
-        }
-    }
-}
-
-fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F)
-where
-    F: Fn(),
-{
-    let mut press_time = state_var.borrow_mut();
-    if let Ok(elapsed) = press_time.elapsed() {
-        if elapsed.as_millis() < interval {
-            elapsed_callback();
-        }
-    }
-
-    (*press_time) = SystemTime::now();
-}
diff --git a/src/package/default.rs b/src/package/default.rs
deleted file mode 100644
index 69182e3f..00000000
--- a/src/package/default.rs
+++ /dev/null
@@ -1,794 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::package::InstallResult::{AlreadyInstalled, BlockedExternalPackage, NotFoundInIndex};
-use crate::package::RemoveResult::Removed;
-use crate::package::UpdateResult::{NotOutdated, Updated};
-use crate::package::{
-    InstallResult, Package, PackageIndex, PackageResolver, RemoveResult, UpdateResult,
-};
-use regex::Regex;
-use std::collections::HashMap;
-use std::error::Error;
-use std::fs;
-use std::fs::{create_dir, File};
-use std::io::{BufRead, BufReader};
-use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-const DEFAULT_PACKAGE_INDEX_FILE: &str = "package_index.json";
-
-pub struct DefaultPackageManager {
-    package_dir: PathBuf,
-    data_dir: PathBuf,
-
-    package_resolver: Option<Box<dyn PackageResolver>>,
-
-    local_index: Option<PackageIndex>,
-}
-
-impl DefaultPackageManager {
-    pub fn new(
-        package_dir: PathBuf,
-        data_dir: PathBuf,
-        package_resolver: Option<Box<dyn PackageResolver>>,
-    ) -> DefaultPackageManager {
-        let local_index = Self::load_local_index(&data_dir);
-
-        DefaultPackageManager {
-            package_dir,
-            data_dir,
-            package_resolver,
-            local_index,
-        }
-    }
-
-    pub fn new_default(
-        package_resolver: Option<Box<dyn PackageResolver>>,
-    ) -> DefaultPackageManager {
-        DefaultPackageManager::new(
-            crate::context::get_package_dir(),
-            crate::context::get_data_dir(),
-            package_resolver,
-        )
-    }
-
-    fn get_package_index_path(data_dir: &Path) -> PathBuf {
-        data_dir.join(DEFAULT_PACKAGE_INDEX_FILE)
-    }
-
-    fn load_local_index(data_dir: &Path) -> Option<super::PackageIndex> {
-        let local_index_file = File::open(Self::get_package_index_path(data_dir));
-        if let Ok(local_index_file) = local_index_file {
-            let reader = BufReader::new(local_index_file);
-            let local_index = serde_json::from_reader(reader);
-
-            if let Ok(local_index) = local_index {
-                return local_index;
-            }
-        }
-
-        None
-    }
-
-    fn request_index() -> Result<super::PackageIndex, Box<dyn Error>> {
-        let client = reqwest::Client::new();
-        let request = client
-            .get("https://hub.espanso.org/json/")
-            .header("User-Agent", format!("espanso/{}", crate::VERSION));
-
-        let mut res = request.send()?;
-        let body = res.text()?;
-        let index: PackageIndex = serde_json::from_str(&body)?;
-
-        Ok(index)
-    }
-
-    fn parse_package_from_readme(readme_path: &Path) -> Option<Package> {
-        lazy_static! {
-            static ref FIELD_REGEX: Regex =
-                Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap();
-        }
-
-        // Read readme line by line
-        let file = File::open(readme_path);
-        if let Ok(file) = file {
-            let reader = BufReader::new(file);
-
-            let mut fields: HashMap<String, String> = HashMap::new();
-
-            let mut started = false;
-
-            for (_index, line) in reader.lines().enumerate() {
-                let line = line.unwrap();
-                if line.contains("---") {
-                    if started {
-                        break;
-                    } else {
-                        started = true;
-                    }
-                } else if started {
-                    let caps = FIELD_REGEX.captures(&line);
-                    if let Some(caps) = caps {
-                        let property = caps.get(1);
-                        let value = caps.get(2);
-                        if property.is_some() && value.is_some() {
-                            fields.insert(
-                                property.unwrap().as_str().to_owned(),
-                                value.unwrap().as_str().to_owned(),
-                            );
-                        }
-                    }
-                }
-            }
-
-            if !fields.contains_key("package_name")
-                || !fields.contains_key("package_title")
-                || !fields.contains_key("package_version")
-                || !fields.contains_key("package_repo")
-                || !fields.contains_key("package_desc")
-                || !fields.contains_key("package_author")
-            {
-                return None;
-            }
-
-            let original_repo = if fields.contains_key("package_original_repo") {
-                fields.get("package_original_repo").unwrap().clone()
-            } else {
-                fields.get("package_repo").unwrap().clone()
-            };
-
-            let is_core = if fields.contains_key("is_core") {
-                match fields.get("is_core").unwrap().clone().as_ref() {
-                    "true" => true,
-                    "false" => false,
-                    _ => false,
-                }
-            } else {
-                false
-            };
-
-            let package = Package {
-                name: fields.get("package_name").unwrap().clone(),
-                title: fields.get("package_title").unwrap().clone(),
-                version: fields.get("package_version").unwrap().clone(),
-                repo: fields.get("package_repo").unwrap().clone(),
-                desc: fields.get("package_desc").unwrap().clone(),
-                author: fields.get("package_author").unwrap().clone(),
-                is_core,
-                original_repo,
-            };
-
-            Some(package)
-        } else {
-            None
-        }
-    }
-
-    fn local_index_timestamp(&self) -> u64 {
-        if let Some(local_index) = &self.local_index {
-            return local_index.last_update;
-        }
-
-        0
-    }
-
-    fn list_local_packages_names(&self) -> Vec<String> {
-        let dir = fs::read_dir(&self.package_dir);
-        let mut output = Vec::new();
-        if let Ok(dir) = dir {
-            for entry in dir {
-                if let Ok(entry) = entry {
-                    let path = entry.path();
-                    if path.is_dir() {
-                        let name = path.file_name();
-                        if let Some(name) = name {
-                            output.push(name.to_str().unwrap().to_owned())
-                        }
-                    }
-                }
-            }
-        }
-
-        output
-    }
-
-    fn cache_local_index(&self) {
-        if let Some(local_index) = &self.local_index {
-            let serialized =
-                serde_json::to_string(local_index).expect("Unable to serialize local index");
-            let local_index_file = self.data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(local_index_file, serialized).expect("Unable to cache local index");
-        }
-    }
-}
-
-impl super::PackageManager for DefaultPackageManager {
-    fn is_index_outdated(&self) -> bool {
-        let current_time = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .expect("Time went backwards");
-        let current_timestamp = current_time.as_secs();
-
-        let local_index_timestamp = self.local_index_timestamp();
-
-        // Local index is outdated if older than a day
-        local_index_timestamp + 60 * 60 * 24 < current_timestamp
-    }
-
-    fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>> {
-        if force || self.is_index_outdated() {
-            let updated_index = DefaultPackageManager::request_index()?;
-            self.local_index = Some(updated_index);
-
-            // Save the index to file
-            self.cache_local_index();
-
-            Ok(Updated)
-        } else {
-            Ok(NotOutdated)
-        }
-    }
-
-    fn get_package(&self, name: &str) -> Option<Package> {
-        if let Some(local_index) = &self.local_index {
-            let result = local_index
-                .packages
-                .iter()
-                .find(|package| package.name == name);
-            if let Some(package) = result {
-                return Some(package.clone());
-            }
-        }
-
-        None
-    }
-
-    fn install_package(
-        &self,
-        name: &str,
-        allow_external: bool,
-        proxy: Option<String>,
-    ) -> Result<InstallResult, Box<dyn Error>> {
-        let package = self.get_package(name);
-        match package {
-            Some(package) => {
-                if package.is_core || allow_external {
-                    self.install_package_from_repo(name, &package.repo, proxy)
-                } else {
-                    Ok(BlockedExternalPackage(package.original_repo))
-                }
-            }
-            None => Ok(NotFoundInIndex),
-        }
-    }
-
-    fn install_package_from_repo(
-        &self,
-        name: &str,
-        repo_url: &str,
-        proxy: Option<String>,
-    ) -> Result<InstallResult, Box<dyn Error>> {
-        // Check if package is already installed
-        let packages = self.list_local_packages_names();
-        if packages.iter().any(|p| p == name) {
-            // Package already installed
-            return Ok(AlreadyInstalled);
-        }
-
-        let temp_dir = self
-            .package_resolver
-            .as_ref()
-            .unwrap()
-            .clone_repo_to_temp(repo_url, proxy)?;
-
-        let temp_package_dir = temp_dir.path().join(name);
-        if !temp_package_dir.exists() {
-            return Ok(InstallResult::NotFoundInRepo);
-        }
-
-        let readme_path = temp_package_dir.join("README.md");
-
-        let package = Self::parse_package_from_readme(&readme_path);
-        if package.is_none() {
-            return Ok(InstallResult::UnableToParsePackageInfo);
-        }
-        let package = package.unwrap();
-
-        let source_dir = temp_package_dir.join(package.version);
-        if !source_dir.exists() {
-            return Ok(InstallResult::MissingPackageVersion);
-        }
-
-        let target_dir = &self.package_dir.join(name);
-        create_dir(&target_dir)?;
-
-        crate::utils::copy_dir(&source_dir, target_dir)?;
-
-        let readme_dest = target_dir.join("README.md");
-        std::fs::copy(readme_path, readme_dest)?;
-
-        Ok(InstallResult::Installed)
-    }
-
-    fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>> {
-        let package_dir = self.package_dir.join(name);
-        if !package_dir.exists() {
-            return Ok(RemoveResult::NotFound);
-        }
-
-        std::fs::remove_dir_all(package_dir)?;
-
-        Ok(Removed)
-    }
-
-    fn list_local_packages(&self) -> Vec<Package> {
-        let mut output = Vec::new();
-
-        let package_names = self.list_local_packages_names();
-
-        for name in package_names.iter() {
-            let package_dir = &self.package_dir.join(name);
-            let readme_file = package_dir.join("README.md");
-            let package = Self::parse_package_from_readme(&readme_file);
-            if let Some(package) = package {
-                output.push(package);
-            }
-        }
-
-        output
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::package::zip::ZipPackageResolver;
-    use crate::package::InstallResult::*;
-    use crate::package::PackageManager;
-    use std::fs::{create_dir, create_dir_all};
-    use std::path::Path;
-    use tempfile::{NamedTempFile, TempDir};
-
-    const OUTDATED_INDEX_CONTENT: &str = include_str!("../res/test/outdated_index.json");
-    const INDEX_CONTENT_WITHOUT_UPDATE: &str =
-        include_str!("../res/test/index_without_update.json");
-    const GET_PACKAGE_INDEX: &str = include_str!("../res/test/get_package_index.json");
-    const INSTALL_PACKAGE_INDEX: &str = include_str!("../res/test/install_package_index.json");
-
-    struct TempPackageManager {
-        package_dir: TempDir,
-        data_dir: TempDir,
-        package_manager: DefaultPackageManager,
-    }
-
-    fn create_temp_package_manager<F>(setup: F) -> TempPackageManager
-    where
-        F: Fn(&Path, &Path) -> (),
-    {
-        let package_dir = TempDir::new().expect("unable to create temp directory");
-        let data_dir = TempDir::new().expect("unable to create temp directory");
-
-        setup(package_dir.path(), data_dir.path());
-
-        let package_manager = DefaultPackageManager::new(
-            package_dir.path().clone().to_path_buf(),
-            data_dir.path().clone().to_path_buf(),
-            Some(Box::new(ZipPackageResolver::new())),
-        );
-
-        TempPackageManager {
-            package_dir,
-            data_dir,
-            package_manager,
-        }
-    }
-
-    #[test]
-    fn test_download_index() {
-        create_temp_package_manager(|_, _| {});
-        let index = DefaultPackageManager::request_index();
-
-        assert!(index.is_ok());
-        assert!(index.unwrap().packages.len() > 0);
-    }
-
-    #[test]
-    fn test_outdated_index() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap();
-        });
-
-        assert!(temp.package_manager.is_index_outdated());
-    }
-
-    #[test]
-    fn test_up_to_date_index_should_not_be_updated() {
-        let mut temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            let current_time = SystemTime::now()
-                .duration_since(UNIX_EPOCH)
-                .expect("Time went backwards");
-            let current_timestamp = current_time.as_secs();
-            let new_contents =
-                INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
-            std::fs::write(index_file, new_contents).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager.update_index(false).unwrap(),
-            UpdateResult::NotOutdated
-        );
-    }
-
-    #[test]
-    fn test_up_to_date_index_with_force_should_be_updated() {
-        let mut temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            let current_time = SystemTime::now()
-                .duration_since(UNIX_EPOCH)
-                .expect("Time went backwards");
-            let current_timestamp = current_time.as_secs();
-            let new_contents =
-                INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
-            std::fs::write(index_file, new_contents).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager.update_index(true).unwrap(),
-            UpdateResult::Updated
-        );
-    }
-
-    #[test]
-    fn test_outdated_index_should_be_updated() {
-        let mut temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager.update_index(false).unwrap(),
-            UpdateResult::Updated
-        );
-    }
-
-    #[test]
-    fn test_update_index_should_create_file() {
-        let mut temp = create_temp_package_manager(|_, _| {});
-
-        assert_eq!(
-            temp.package_manager.update_index(false).unwrap(),
-            UpdateResult::Updated
-        );
-        assert!(temp
-            .data_dir
-            .path()
-            .join(DEFAULT_PACKAGE_INDEX_FILE)
-            .exists())
-    }
-
-    #[test]
-    fn test_get_package_should_be_found() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .get_package("italian-accents")
-                .unwrap()
-                .title,
-            "Italian Accents"
-        );
-    }
-
-    #[test]
-    fn test_get_package_should_not_be_found() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap();
-        });
-
-        assert!(temp.package_manager.get_package("not-existing").is_none());
-    }
-
-    #[test]
-    fn test_list_local_packages_names() {
-        let temp = create_temp_package_manager(|package_dir, _| {
-            create_dir(package_dir.join("package-1")).unwrap();
-            create_dir(package_dir.join("package2")).unwrap();
-            std::fs::write(package_dir.join("dummyfile.txt"), "test").unwrap();
-        });
-
-        let packages = temp.package_manager.list_local_packages_names();
-        assert_eq!(packages.len(), 2);
-        assert!(packages.iter().any(|p| p == "package-1"));
-        assert!(packages.iter().any(|p| p == "package2"));
-    }
-
-    #[test]
-    fn test_install_package_not_found() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("doesnotexist", false, None)
-                .unwrap(),
-            NotFoundInIndex
-        );
-    }
-
-    #[test]
-    fn test_install_package_already_installed() {
-        let temp = create_temp_package_manager(|package_dir, data_dir| {
-            create_dir(package_dir.join("italian-accents")).unwrap();
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("italian-accents", false, None)
-                .unwrap(),
-            AlreadyInstalled
-        );
-    }
-
-    #[test]
-    fn test_install_package() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("dummy-package", false, None)
-                .unwrap(),
-            Installed
-        );
-        assert!(temp.package_dir.path().join("dummy-package").exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/README.md")
-            .exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/package.yml")
-            .exists());
-    }
-
-    #[test]
-    fn test_install_package_does_not_exist_in_repo() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("not-existing", false, None)
-                .unwrap(),
-            NotFoundInRepo
-        );
-    }
-
-    #[test]
-    fn test_install_package_missing_version() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("dummy-package2", false, None)
-                .unwrap(),
-            MissingPackageVersion
-        );
-    }
-
-    #[test]
-    fn test_install_package_missing_readme_unable_to_parse_package_info() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("dummy-package3", false, None)
-                .unwrap(),
-            UnableToParsePackageInfo
-        );
-    }
-
-    #[test]
-    fn test_install_package_bad_readme_unable_to_parse_package_info() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("dummy-package4", false, None)
-                .unwrap(),
-            UnableToParsePackageInfo
-        );
-    }
-
-    #[test]
-    fn test_list_local_packages() {
-        let temp = create_temp_package_manager(|_, data_dir| {
-            let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
-            std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
-        });
-
-        assert_eq!(
-            temp.package_manager
-                .install_package("dummy-package", false, None)
-                .unwrap(),
-            Installed
-        );
-        assert!(temp.package_dir.path().join("dummy-package").exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/README.md")
-            .exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/package.yml")
-            .exists());
-
-        let list = temp.package_manager.list_local_packages();
-        assert_eq!(list.len(), 1);
-        assert_eq!(list[0].name, "dummy-package");
-    }
-
-    #[test]
-    fn test_remove_package() {
-        let temp = create_temp_package_manager(|package_dir, _| {
-            let dummy_package_dir = package_dir.join("dummy-package");
-            create_dir_all(&dummy_package_dir).unwrap();
-            std::fs::write(dummy_package_dir.join("README.md"), "readme").unwrap();
-            std::fs::write(dummy_package_dir.join("package.yml"), "name: package").unwrap();
-        });
-
-        assert!(temp.package_dir.path().join("dummy-package").exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/README.md")
-            .exists());
-        assert!(temp
-            .package_dir
-            .path()
-            .join("dummy-package/package.yml")
-            .exists());
-        assert_eq!(
-            temp.package_manager
-                .remove_package("dummy-package")
-                .unwrap(),
-            RemoveResult::Removed
-        );
-        assert!(!temp.package_dir.path().join("dummy-package").exists());
-        assert!(!temp
-            .package_dir
-            .path()
-            .join("dummy-package/README.md")
-            .exists());
-        assert!(!temp
-            .package_dir
-            .path()
-            .join("dummy-package/package.yml")
-            .exists());
-    }
-
-    #[test]
-    fn test_remove_package_not_found() {
-        let temp = create_temp_package_manager(|_, _| {});
-
-        assert_eq!(
-            temp.package_manager.remove_package("not-existing").unwrap(),
-            RemoveResult::NotFound
-        );
-    }
-
-    #[test]
-    fn test_parse_package_from_readme() {
-        let file = NamedTempFile::new().unwrap();
-        fs::write(
-            file.path(),
-            r###"
-        ---
-        package_name: "italian-accents"
-        package_title: "Italian Accents"
-        package_desc: "Include Italian accents substitutions to espanso."
-        package_version: "0.1.0"
-        package_author: "Federico Terzi"
-        package_repo: "https://github.com/federico-terzi/espanso-hub-core"
-        is_core: true
-        ---
-        "###,
-        )
-        .unwrap();
-
-        let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
-
-        let target_package = Package {
-            name: "italian-accents".to_string(),
-            title: "Italian Accents".to_string(),
-            version: "0.1.0".to_string(),
-            repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
-            desc: "Include Italian accents substitutions to espanso.".to_string(),
-            author: "Federico Terzi".to_string(),
-            original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
-            is_core: true,
-        };
-
-        assert_eq!(package, target_package);
-    }
-
-    #[test]
-    fn test_parse_package_from_readme_with_bad_metadata() {
-        let file = NamedTempFile::new().unwrap();
-        fs::write(
-            file.path(),
-            r###"
-        ---
-        package_name: italian-accents
-        package_title: "Italian Accents"
-        package_desc: "Include Italian accents substitutions to espanso."
-        package_version:"0.1.0"
-        package_author:Federico Terzi
-        package_repo: "https://github.com/federico-terzi/espanso-hub-core"
-        is_core: true
-        ---
-        Readme text
-        "###,
-        )
-        .unwrap();
-
-        let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
-
-        let target_package = Package {
-            name: "italian-accents".to_string(),
-            title: "Italian Accents".to_string(),
-            version: "0.1.0".to_string(),
-            repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
-            desc: "Include Italian accents substitutions to espanso.".to_string(),
-            author: "Federico Terzi".to_string(),
-            original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
-            is_core: true,
-        };
-
-        assert_eq!(package, target_package);
-    }
-}
diff --git a/src/package/mod.rs b/src/package/mod.rs
deleted file mode 100644
index ea6d23d4..00000000
--- a/src/package/mod.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 Federico Terzi
- *
- * espanso is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * espanso is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-pub(crate) mod default;
-pub(crate) mod zip;
-
-use serde::{Deserialize, Serialize};
-use std::error::Error;
-use tempfile::TempDir;
-
-pub trait PackageManager {
-    fn is_index_outdated(&self) -> bool;
-    fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>>;
-
-    fn get_package(&self, name: &str) -> Option<Package>;
-
-    fn install_package(
-        &self,
-        name: &str,
-        allow_external: bool,
-        proxy: Option<String>,
-    ) -> Result<InstallResult, Box<dyn Error>>;
-    fn install_package_from_repo(
-        &self,
-        name: &str,
-        repo_url: &str,
-        proxy: Option<String>,
-    ) -> Result<InstallResult, Box<dyn Error>>;
-
-    fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
-
-    fn list_local_packages(&self) -> Vec<Package>;
-}
-
-pub trait PackageResolver {
-    fn clone_repo_to_temp(
-        &self,
-        repo_url: &str,
-        proxy: Option<String>,
-    ) -> Result<TempDir, Box<dyn Error>>;
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
-pub struct Package {
-    pub name: String,
-    pub title: String,
-    pub version: String,
-    pub repo: String,
-    pub desc: String,
-    pub author: String,
-
-    #[serde(default = "default_is_core")]
-    pub is_core: bool,
-    #[serde(default = "default_original_repo")]
-    pub original_repo: String,
-}
-
-fn default_is_core() -> bool {
-    false
-}
-fn default_original_repo() -> String {
-    "".to_owned()
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
-pub struct PackageIndex {
-    #[serde(rename = "lastUpdate")]
-    pub last_update: u64,
-
-    pub packages: Vec<Package>,
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum UpdateResult {
-    NotOutdated,
-    Updated,
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum InstallResult {
-    NotFoundInIndex,
-    NotFoundInRepo,
-    UnableToParsePackageInfo,
-    MissingPackageVersion,
-    AlreadyInstalled,
-    Installed,
-    BlockedExternalPackage(String),
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum RemoveResult {
-    NotFound,
-    Removed,
-}
diff --git a/src/package/zip.rs b/src/package/zip.rs
deleted file mode 100644
index b3a197a6..00000000
--- a/src/package/zip.rs
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 log::debug;
-use std::error::Error;
-use std::io::{copy, Cursor};
-use std::{fs, io};
-use tempfile::TempDir;
-
-pub struct ZipPackageResolver;
-
-impl ZipPackageResolver {
-    pub fn new() -> ZipPackageResolver {
-        return ZipPackageResolver {};
-    }
-}
-
-impl super::PackageResolver for ZipPackageResolver {
-    fn clone_repo_to_temp(
-        &self,
-        repo_url: &str,
-        proxy: Option<String>,
-    ) -> Result<TempDir, Box<dyn Error>> {
-        let temp_dir = TempDir::new()?;
-
-        let zip_url = repo_url.to_owned() + "/archive/master.zip";
-
-        let mut client = reqwest::Client::builder();
-
-        if let Some(proxy) = proxy {
-            let proxy = reqwest::Proxy::https(&proxy).expect("unable to setup https proxy");
-            client = client.proxy(proxy);
-        };
-
-        let client = client.build().expect("unable to create http client");
-
-        // Download the archive from GitHub
-        let mut response = client.get(&zip_url).send()?;
-
-        // Extract zip file
-        let mut buffer = Vec::new();
-        copy(&mut response, &mut buffer)?;
-
-        let reader = Cursor::new(buffer);
-
-        let mut archive = zip::ZipArchive::new(reader).unwrap();
-
-        // Find the root folder name
-        let mut root_folder = {
-            let root_folder = archive.by_index(0).unwrap();
-            let root_folder = root_folder.sanitized_name();
-            root_folder.to_str().unwrap().to_owned()
-        };
-        root_folder.push(std::path::MAIN_SEPARATOR);
-
-        for i in 1..archive.len() {
-            let mut file = archive.by_index(i).unwrap();
-
-            let current_path = file.sanitized_name();
-            let current_filename = current_path.to_str().unwrap();
-            let trimmed_filename = current_filename.trim_start_matches(&root_folder);
-
-            let outpath = temp_dir.path().join(trimmed_filename);
-
-            {
-                let comment = file.comment();
-                if !comment.is_empty() {
-                    debug!("File {} comment: {}", i, comment);
-                }
-            }
-
-            if (&*file.name()).ends_with('/') {
-                debug!(
-                    "File {} extracted to \"{}\"",
-                    i,
-                    outpath.as_path().display()
-                );
-                fs::create_dir_all(&outpath).unwrap();
-            } else {
-                debug!(
-                    "File {} extracted to \"{}\" ({} bytes)",
-                    i,
-                    outpath.as_path().display(),
-                    file.size()
-                );
-                if let Some(p) = outpath.parent() {
-                    if !p.exists() {
-                        fs::create_dir_all(&p).unwrap();
-                    }
-                }
-                let mut outfile = fs::File::create(&outpath).unwrap();
-                io::copy(&mut file, &mut outfile).unwrap();
-            }
-        }
-
-        Ok(temp_dir)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::super::PackageResolver;
-    use super::*;
-
-    #[test]
-    fn test_clone_temp_repository() {
-        let resolver = ZipPackageResolver::new();
-        let cloned_dir = resolver
-            .clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core", None)
-            .unwrap();
-        assert!(cloned_dir.path().join("LICENSE").exists());
-    }
-}
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
deleted file mode 100644
index a609160a..00000000
--- a/src/protocol/mod.rs
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::config::Configs;
-use crate::event::ActionType;
-use crate::event::{Event, SystemEvent};
-use log::error;
-use serde::{Deserialize, Serialize};
-use std::error::Error;
-use std::io::{BufReader, Read, Write};
-use std::sync::mpsc::Sender;
-
-#[cfg(target_os = "windows")]
-mod windows;
-
-#[cfg(not(target_os = "windows"))]
-mod unix;
-
-pub trait IPCServer {
-    fn start(&self);
-}
-
-pub trait IPCClient {
-    fn send_command(&self, command: IPCCommand) -> Result<(), String>;
-}
-
-pub fn send_command_or_warn(service: Service, configs: Configs, command: IPCCommand) {
-    let ipc_client = get_ipc_client(service, configs);
-    if let Err(e) = ipc_client.send_command(command) {
-        error!("unable to send command to IPC server");
-    }
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-pub struct IPCCommand {
-    pub id: String,
-
-    #[serde(default)]
-    pub payload: String,
-}
-
-impl IPCCommand {
-    fn to_event(&self) -> Option<Event> {
-        match self.id.as_ref() {
-            "exit" => Some(Event::Action(ActionType::Exit)),
-            "wexit" => Some(Event::Action(ActionType::ExitWorker)),
-            "toggle" => Some(Event::Action(ActionType::Toggle)),
-            "enable" => Some(Event::Action(ActionType::Enable)),
-            "disable" => Some(Event::Action(ActionType::Disable)),
-            "restartworker" => Some(Event::Action(ActionType::RestartWorker)),
-            "notify" => Some(Event::System(SystemEvent::NotifyRequest(
-                self.payload.clone(),
-            ))),
-            "trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))),
-            _ => None,
-        }
-    }
-
-    pub fn from(event: Event) -> Option<IPCCommand> {
-        match event {
-            Event::Action(ActionType::Exit) => Some(IPCCommand {
-                id: "exit".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::Action(ActionType::ExitWorker) => Some(IPCCommand {
-                id: "wexit".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::Action(ActionType::Toggle) => Some(IPCCommand {
-                id: "toggle".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::Action(ActionType::Enable) => Some(IPCCommand {
-                id: "enable".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::Action(ActionType::Disable) => Some(IPCCommand {
-                id: "disable".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::Action(ActionType::RestartWorker) => Some(IPCCommand {
-                id: "restartworker".to_owned(),
-                payload: "".to_owned(),
-            }),
-            Event::System(SystemEvent::NotifyRequest(message)) => Some(IPCCommand {
-                id: "notify".to_owned(),
-                payload: message,
-            }),
-            Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand {
-                id: "trigger".to_owned(),
-                payload: trigger,
-            }),
-            _ => None,
-        }
-    }
-
-    pub fn exit() -> IPCCommand {
-        Self {
-            id: "exit".to_owned(),
-            payload: "".to_owned(),
-        }
-    }
-
-    pub fn exit_worker() -> IPCCommand {
-        Self {
-            id: "wexit".to_owned(),
-            payload: "".to_owned(),
-        }
-    }
-
-    pub fn restart_worker() -> IPCCommand {
-        Self {
-            id: "restartworker".to_owned(),
-            payload: "".to_owned(),
-        }
-    }
-
-    pub fn trigger(trigger: &str) -> IPCCommand {
-        Self {
-            id: "trigger".to_owned(),
-            payload: trigger.to_owned(),
-        }
-    }
-}
-
-fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) {
-    match stream {
-        Ok(stream) => {
-            let mut json_str = String::new();
-            let mut buf_reader = BufReader::new(stream);
-            let res = buf_reader.read_to_string(&mut json_str);
-
-            if res.is_ok() {
-                let command: Result<IPCCommand, serde_json::Error> =
-                    serde_json::from_str(&json_str);
-                match command {
-                    Ok(command) => {
-                        let event = command.to_event();
-                        if let Some(event) = event {
-                            event_channel.send(event).expect("Broken event channel");
-                        }
-                    }
-                    Err(e) => {
-                        error!("Error deserializing JSON command: {}", e);
-                    }
-                }
-            }
-        }
-        Err(err) => {
-            println!("Error: {}", err);
-        }
-    }
-}
-
-fn send_command<W: Write, E: Error>(
-    command: IPCCommand,
-    stream: Result<W, E>,
-) -> Result<(), String> {
-    match stream {
-        Ok(mut stream) => {
-            let json_str = serde_json::to_string(&command);
-            if let Ok(json_str) = json_str {
-                stream.write_all(json_str.as_bytes()).unwrap_or_else(|e| {
-                    println!("Can't write to IPC socket: {}", e);
-                });
-                return Ok(());
-            }
-        }
-        Err(e) => return Err(format!("Can't connect to daemon: {}", e)),
-    }
-
-    Err("Can't send command".to_owned())
-}
-
-pub enum Service {
-    Daemon,
-    Worker,
-}
-
-// UNIX IMPLEMENTATION
-#[cfg(not(target_os = "windows"))]
-pub fn get_ipc_server(
-    service: Service,
-    _: Configs,
-    event_channel: Sender<Event>,
-) -> impl IPCServer {
-    unix::UnixIPCServer::new(service, event_channel)
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
-    unix::UnixIPCClient::new(service)
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn get_ipc_server(
-    service: Service,
-    _: Configs,
-    event_channel: Sender<Event>,
-) -> impl IPCServer {
-    windows::WindowsIPCServer::new(service, event_channel)
-}
-
-#[cfg(target_os = "windows")]
-pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
-    windows::WindowsIPCClient::new(service)
-}
diff --git a/src/protocol/unix.rs b/src/protocol/unix.rs
deleted file mode 100644
index 98313da8..00000000
--- a/src/protocol/unix.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::IPCCommand;
-use log::{info, warn};
-use std::os::unix::net::{UnixListener, UnixStream};
-use std::sync::mpsc::Sender;
-
-use super::Service;
-use crate::context;
-use crate::event::*;
-use crate::protocol::{process_event, send_command};
-
-const DAEMON_UNIX_SOCKET_NAME: &str = "espanso.sock";
-const WORKER_UNIX_SOCKET_NAME: &str = "worker.sock";
-
-pub struct UnixIPCServer {
-    service: Service,
-    event_channel: Sender<Event>,
-}
-
-impl UnixIPCServer {
-    pub fn new(service: Service, event_channel: Sender<Event>) -> UnixIPCServer {
-        UnixIPCServer {
-            service,
-            event_channel,
-        }
-    }
-}
-
-fn get_unix_name(service: &Service) -> String {
-    match service {
-        Service::Daemon => DAEMON_UNIX_SOCKET_NAME.to_owned(),
-        Service::Worker => WORKER_UNIX_SOCKET_NAME.to_owned(),
-    }
-}
-
-impl super::IPCServer for UnixIPCServer {
-    fn start(&self) {
-        let event_channel = self.event_channel.clone();
-        let socket_name = get_unix_name(&self.service);
-        std::thread::Builder::new()
-            .name("ipc_server".to_string())
-            .spawn(move || {
-                let espanso_dir = context::get_data_dir();
-                let unix_socket = espanso_dir.join(socket_name);
-
-                std::fs::remove_file(unix_socket.clone()).unwrap_or_else(|e| {
-                    warn!("Unable to delete Unix socket: {}", e);
-                });
-                let listener =
-                    UnixListener::bind(unix_socket.clone()).expect("Can't bind to Unix Socket");
-
-                info!(
-                    "Binded to IPC unix socket: {}",
-                    unix_socket.as_path().display()
-                );
-
-                for stream in listener.incoming() {
-                    process_event(&event_channel, stream);
-                }
-            })
-            .expect("Unable to spawn IPC server thread");
-    }
-}
-
-pub struct UnixIPCClient {
-    service: Service,
-}
-
-impl UnixIPCClient {
-    pub fn new(service: Service) -> UnixIPCClient {
-        UnixIPCClient { service }
-    }
-}
-
-impl super::IPCClient for UnixIPCClient {
-    fn send_command(&self, command: IPCCommand) -> Result<(), String> {
-        let espanso_dir = context::get_data_dir();
-        let socket_name = get_unix_name(&self.service);
-        let unix_socket = espanso_dir.join(socket_name);
-
-        // Open the stream
-        let stream = UnixStream::connect(unix_socket);
-
-        send_command(command, stream)
-    }
-}
diff --git a/src/protocol/windows.rs b/src/protocol/windows.rs
deleted file mode 100644
index 10ff0425..00000000
--- a/src/protocol/windows.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::IPCCommand;
-use log::info;
-use std::net::{TcpListener, TcpStream};
-use std::sync::mpsc::Sender;
-
-use crate::config::Configs;
-use crate::context;
-use crate::event::*;
-use crate::protocol::{process_event, send_command, Service};
-use named_pipe::{PipeClient, PipeOptions, PipeServer};
-use std::io::Error;
-use std::path::PathBuf;
-
-const DAEMON_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansodaemon";
-const WORKER_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansoworker";
-const CLIENT_TIMEOUT: u32 = 2000;
-
-pub struct WindowsIPCServer {
-    service: Service,
-    event_channel: Sender<Event>,
-}
-
-fn get_pipe_name(service: &Service) -> String {
-    match service {
-        Service::Daemon => DAEMON_WIN_PIPE_NAME.to_owned(),
-        Service::Worker => WORKER_WIN_PIPE_NAME.to_owned(),
-    }
-}
-
-impl WindowsIPCServer {
-    pub fn new(service: Service, event_channel: Sender<Event>) -> WindowsIPCServer {
-        WindowsIPCServer {
-            service,
-            event_channel,
-        }
-    }
-}
-
-impl super::IPCServer for WindowsIPCServer {
-    fn start(&self) {
-        let event_channel = self.event_channel.clone();
-        let pipe_name = get_pipe_name(&self.service);
-        std::thread::Builder::new()
-            .name("ipc_server".to_string())
-            .spawn(move || {
-                let options = PipeOptions::new(&pipe_name);
-
-                info!("Binding to named pipe: {}", pipe_name);
-
-                loop {
-                    let server = options
-                        .single()
-                        .expect("unable to initialize IPC named pipe");
-                    let pipe_server = server.wait();
-                    process_event(&event_channel, pipe_server);
-                }
-            })
-            .expect("Unable to spawn IPC server thread");
-    }
-}
-
-pub struct WindowsIPCClient {
-    service: Service,
-}
-
-impl WindowsIPCClient {
-    pub fn new(service: Service) -> WindowsIPCClient {
-        WindowsIPCClient { service }
-    }
-}
-
-impl super::IPCClient for WindowsIPCClient {
-    fn send_command(&self, command: IPCCommand) -> Result<(), String> {
-        let pipe_name = get_pipe_name(&self.service);
-        let client = PipeClient::connect_ms(pipe_name, CLIENT_TIMEOUT);
-
-        send_command(command, client)
-    }
-}
diff --git a/src/render/default.rs b/src/render/default.rs
deleted file mode 100644
index 71e8f238..00000000
--- a/src/render/default.rs
+++ /dev/null
@@ -1,902 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::*;
-use crate::config::Configs;
-use crate::extension::{Extension, ExtensionResult};
-use crate::matcher::{Match, MatchContentType, MatchVariable};
-use log::{error, warn};
-use regex::{Captures, Regex};
-use serde_yaml::Value;
-use std::collections::{HashMap, HashSet};
-
-lazy_static! {
-    static ref VAR_REGEX: Regex =
-        Regex::new(r"\{\{\s*((?P<name>\w+)(\.(?P<subname>(\w+)))?)\s*\}\}").unwrap();
-    static ref UNKNOWN_VARIABLE: String = "".to_string();
-}
-
-pub struct DefaultRenderer {
-    extension_map: HashMap<String, Box<dyn Extension>>,
-
-    // Regex used to identify matches (and arguments) in passive expansions
-    passive_match_regex: Regex,
-}
-
-impl DefaultRenderer {
-    pub fn new(extensions: Vec<Box<dyn Extension>>, config: Configs) -> DefaultRenderer {
-        // Register all the extensions
-        let mut extension_map = HashMap::new();
-        for extension in extensions.into_iter() {
-            extension_map.insert(extension.name(), extension);
-        }
-
-        // Compile the regexes
-        let passive_match_regex = Regex::new(&config.passive_match_regex).unwrap_or_else(|e| {
-            panic!("Invalid passive match regex: {:?}", e);
-        });
-
-        DefaultRenderer {
-            extension_map,
-            passive_match_regex,
-        }
-    }
-
-    fn find_match(config: &Configs, trigger: &str) -> Option<(Match, usize)> {
-        let mut result = None;
-
-        // TODO: if performances become a problem, implement a more efficient lookup
-        for m in config.matches.iter() {
-            for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
-                if m_trigger == trigger {
-                    result = Some((m.clone(), trigger_offset));
-                    break;
-                }
-            }
-        }
-
-        result
-    }
-}
-
-impl super::Renderer for DefaultRenderer {
-    fn render_match(
-        &self,
-        m: &Match,
-        trigger_offset: usize,
-        config: &Configs,
-        args: Vec<String>,
-    ) -> RenderResult {
-        // Manage the different types of matches
-        match &m.content {
-            // Text Match
-            MatchContentType::Text(content) => {
-                let target_string = if content._has_vars {
-                    // Find all the variables that are required by the current match
-                    let mut target_vars: HashSet<String> = HashSet::new();
-
-                    for caps in VAR_REGEX.captures_iter(&content.replace) {
-                        let var_name = caps.name("name").unwrap().as_str();
-                        target_vars.insert(var_name.to_owned());
-                    }
-
-                    let match_variables: HashSet<&String> =
-                        content.vars.iter().map(|var| &var.name).collect();
-
-                    // Find the global variables that are not specified in the var list
-                    let mut missing_globals = Vec::new();
-                    let mut specified_globals: HashMap<String, &MatchVariable> = HashMap::new();
-                    for global_var in config.global_vars.iter() {
-                        if target_vars.contains(&global_var.name) {
-                            if match_variables.contains(&global_var.name) {
-                                specified_globals.insert(global_var.name.clone(), &global_var);
-                            } else {
-                                missing_globals.push(global_var);
-                            }
-                        }
-                    }
-
-                    // Determine the variable evaluation order
-                    let mut variables: Vec<&MatchVariable> = Vec::new();
-                    // First place the global that are not explicitly specified
-                    variables.extend(missing_globals);
-                    // Then the ones explicitly specified, in the given order
-                    variables.extend(&content.vars);
-
-                    // Replace variable type "global" with the actual reference
-                    let variables: Vec<&MatchVariable> = variables
-                        .into_iter()
-                        .map(|variable| {
-                            if variable.var_type == "global" {
-                                if let Some(actual_variable) = specified_globals.get(&variable.name)
-                                {
-                                    return actual_variable.clone();
-                                }
-                            }
-                            variable
-                        })
-                        .collect();
-
-                    let mut output_map: HashMap<String, ExtensionResult> = HashMap::new();
-
-                    for variable in variables.into_iter() {
-                        // In case of variables of type match, we need to recursively call
-                        // the render function
-                        if variable.var_type == "match" {
-                            // Extract the match trigger from the variable params
-                            let trigger = variable.params.get(&Value::from("trigger"));
-                            if trigger.is_none() {
-                                warn!(
-                                    "Missing param 'trigger' in match variable: {}",
-                                    variable.name
-                                );
-                                continue;
-                            }
-                            let trigger = trigger.unwrap();
-
-                            // Find the given match from the active configs
-                            let inner_match =
-                                DefaultRenderer::find_match(config, trigger.as_str().unwrap_or(""));
-
-                            if inner_match.is_none() {
-                                warn!(
-                                    "Could not find inner match with trigger: '{}'",
-                                    trigger.as_str().unwrap_or("undefined")
-                                );
-                                continue;
-                            }
-
-                            let (inner_match, trigger_offset) = inner_match.unwrap();
-
-                            // Render the inner match
-                            // TODO: inner arguments
-                            let result =
-                                self.render_match(&inner_match, trigger_offset, config, vec![]);
-
-                            // Inner matches are only supported for text-expansions, warn the user otherwise
-                            match result {
-                                RenderResult::Text(inner_content) => {
-                                    output_map.insert(variable.name.clone(), ExtensionResult::Single(inner_content));
-                                },
-                                _ => {
-                                    warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.")
-                                },
-                            }
-                        } else {
-                            // Normal extension variables
-                            let extension = self.extension_map.get(&variable.var_type);
-                            if let Some(extension) = extension {
-                                let ext_res =
-                                    extension.calculate(&variable.params, &args, &output_map);
-                                match ext_res {
-                                    Ok(ext_out) => {
-                                        if let Some(output) = ext_out {
-                                            output_map.insert(variable.name.clone(), output);
-                                        } else {
-                                            output_map.insert(
-                                                variable.name.clone(),
-                                                ExtensionResult::Single("".to_owned()),
-                                            );
-                                            warn!(
-                                                "Could not generate output for variable: {}",
-                                                variable.name
-                                            );
-                                        }
-                                    }
-                                    Err(_) => return RenderResult::Error,
-                                }
-                            } else {
-                                error!(
-                                    "No extension found for variable type: {}",
-                                    variable.var_type
-                                );
-                            }
-                        }
-                    }
-
-                    // Replace the variables
-                    let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
-                        let var_name = caps.name("name").unwrap().as_str();
-                        let var_subname = caps.name("subname");
-                        match output_map.get(var_name) {
-                            Some(result) => match result {
-                                ExtensionResult::Single(output) => output,
-                                ExtensionResult::Multiple(results) => match var_subname {
-                                    Some(var_subname) => {
-                                        let var_subname = var_subname.as_str();
-                                        results.get(var_subname).unwrap_or(&UNKNOWN_VARIABLE)
-                                    }
-                                    None => {
-                                        error!(
-                                            "nested name missing from multi-value variable: {}",
-                                            var_name
-                                        );
-                                        &UNKNOWN_VARIABLE
-                                    }
-                                },
-                            },
-                            None => &UNKNOWN_VARIABLE,
-                        }
-                    });
-
-                    result.to_string()
-                } else {
-                    // No variables, simple text substitution
-                    content.replace.clone()
-                };
-
-                // Unescape any brackets (needed to be able to insert double brackets in replacement
-                // text, without triggering the variable system). See issue #187
-                let mut target_string = target_string.replace("\\{", "{").replace("\\}", "}");
-
-                // Render any argument that may be present
-                if !args.is_empty() {
-                    target_string = utils::render_args(&target_string, &args);
-                }
-
-                // Handle case propagation
-                target_string = if m.propagate_case {
-                    let trigger = &m.triggers[trigger_offset];
-
-                    // The check should be carried out from the position of the first
-                    // alphabetic letter
-                    // See issue #244
-                    let first_alphabetic =
-                        trigger.chars().position(|c| c.is_alphabetic()).unwrap_or(0);
-
-                    let first_char = trigger.chars().nth(first_alphabetic);
-                    let second_char = trigger.chars().nth(first_alphabetic + 1);
-                    let mode: i32 = if let Some(first_char) = first_char {
-                        if first_char.is_uppercase() {
-                            if let Some(second_char) = second_char {
-                                if second_char.is_uppercase() {
-                                    2 // Full CAPITALIZATION
-                                } else {
-                                    1 // Only first letter capitalized: Capitalization
-                                }
-                            } else {
-                                2 // Single char, defaults to full CAPITALIZATION
-                            }
-                        } else {
-                            0 // Lowercase, no action
-                        }
-                    } else {
-                        0
-                    };
-
-                    match mode {
-                        1 => {
-                            // Capitalize the first letter
-                            let mut v: Vec<char> = target_string.chars().collect();
-                            v[0] = v[0].to_uppercase().nth(0).unwrap();
-                            v.into_iter().collect()
-                        }
-                        2 => {
-                            // Full capitalization
-                            target_string.to_uppercase()
-                        }
-                        _ => {
-                            // Noop
-                            target_string
-                        }
-                    }
-                } else {
-                    target_string
-                };
-
-                RenderResult::Text(target_string)
-            }
-
-            // Image Match
-            MatchContentType::Image(content) => {
-                // Make sure the image exist beforehand
-                if content.path.exists() {
-                    RenderResult::Image(content.path.clone())
-                } else {
-                    error!("Image not found in path: {:?}", content.path);
-                    RenderResult::Error
-                }
-            }
-        }
-    }
-
-    fn render_passive(&self, text: &str, config: &Configs) -> RenderResult {
-        // Render the matches
-        let result = self
-            .passive_match_regex
-            .replace_all(&text, |caps: &Captures| {
-                let match_name = if let Some(name) = caps.name("name") {
-                    name.as_str()
-                } else {
-                    ""
-                };
-
-                // Get the original matching string, useful to return the match untouched
-                let original_match = caps.get(0).unwrap().as_str();
-
-                // Find the corresponding match
-                let m = DefaultRenderer::find_match(config, match_name);
-
-                // If no match is found, leave the match without modifications
-                if m.is_none() {
-                    return original_match.to_owned();
-                }
-
-                // Compute the args by separating them
-                let match_args = if let Some(args) = caps.name("args") {
-                    args.as_str()
-                } else {
-                    ""
-                };
-                let args: Vec<String> = utils::split_args(
-                    match_args,
-                    config.passive_arg_delimiter,
-                    config.passive_arg_escape,
-                );
-
-                let (m, trigger_offset) = m.unwrap();
-                // Render the actual match
-                let result = self.render_match(&m, trigger_offset, &config, args);
-
-                match result {
-                    RenderResult::Text(out) => out,
-                    _ => original_match.to_owned(),
-                }
-            });
-
-        RenderResult::Text(result.into_owned())
-    }
-}
-
-// TESTS
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    fn get_renderer(config: Configs) -> DefaultRenderer {
-        DefaultRenderer::new(
-            vec![
-                Box::new(crate::extension::dummy::DummyExtension::new("dummy")),
-                Box::new(crate::extension::vardummy::VarDummyExtension::new()),
-                Box::new(crate::extension::multiecho::MultiEchoExtension::new()),
-            ],
-            config,
-        )
-    }
-
-    fn get_config_for(s: &str) -> Configs {
-        let config: Configs = serde_yaml::from_str(s).unwrap();
-        config
-    }
-
-    fn verify_render(rendered: RenderResult, target: &str) {
-        match rendered {
-            RenderResult::Text(rendered) => {
-                assert_eq!(rendered, target);
-            }
-            _ => assert!(false),
-        }
-    }
-
-    #[test]
-    fn test_render_passive_no_matches() {
-        let text = r###"
-        this text contains no matches
-        "###;
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: test
-              replace: result
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, text);
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_no_args() {
-        let text = "this is a :test";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':test'
-              replace: result
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is a result");
-    }
-
-    #[test]
-    fn test_render_passive_multiple_match_no_args() {
-        let text = "this is a :test and then another :test";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':test'
-              replace: result
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is a result and then another result");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_multiline_no_args() {
-        let text = r###"this is a
-        :test
-        "###;
-
-        let result = r###"this is a
-        result
-        "###;
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':test'
-              replace: result
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, result);
-    }
-
-    #[test]
-    fn test_render_passive_nested_matches_no_args() {
-        let text = ":greet";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "hi {{name}}"
-              vars:
-                - name: name
-                  type: match
-                  params:
-                    trigger: ":name"
-
-            - trigger: ':name'
-              replace: john
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "hi john");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_with_args() {
-        let text = ":greet/Jon/";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "Hi $0$"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi Jon");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_no_args_should_not_replace_args_syntax() {
-        let text = ":greet";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "Hi $0$"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi $0$");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_with_multiple_args() {
-        let text = ":greet/Jon/Snow/";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "Hi $0$, there is $1$ outside"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi Jon, there is Snow outside");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_with_escaped_args() {
-        let text = ":greet/Jon/10\\/12/";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "Hi $0$, today is $1$"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi Jon, today is 10/12");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_with_args_not_closed() {
-        let text = ":greet/Jon/Snow";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':greet'
-              replace: "Hi $0$"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi JonSnow");
-    }
-
-    #[test]
-    fn test_render_passive_local_var() {
-        let text = "this is :test";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: ':test'
-              replace: "my {{output}}"
-              vars:
-                - name: output
-                  type: dummy
-                  params:
-                    echo: "result"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is my result");
-    }
-
-    #[test]
-    fn test_render_passive_global_var() {
-        let text = "this is :test";
-
-        let config = get_config_for(
-            r###"
-        global_vars:
-            - name: output
-              type: dummy
-              params:
-                echo: "result"
-        matches:
-            - trigger: ':test'
-              replace: "my {{output}}"
-
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is my result");
-    }
-
-    #[test]
-    fn test_render_passive_global_var_is_overridden_by_local() {
-        let text = "this is :test";
-
-        let config = get_config_for(
-            r###"
-        global_vars:
-            - name: output
-              type: dummy
-              params:
-                echo: "result"
-        matches:
-            - trigger: ':test'
-              replace: "my {{output}}"
-              vars:
-                - name: "output"
-                  type: dummy
-                  params:
-                    echo: "local"
-
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is my local");
-    }
-
-    #[test]
-    fn test_render_match_with_unknown_variable_does_not_crash() {
-        let text = "this is :test";
-
-        let config = get_config_for(
-            r###"
-            matches:
-                - trigger: ':test'
-                  replace: "my {{unknown}}"
-            "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is my ");
-    }
-
-    #[test]
-    fn test_render_escaped_double_brackets_should_not_consider_them_variable() {
-        let text = "this is :test";
-
-        let config = get_config_for(
-            r###"
-            matches:
-                - trigger: ':test'
-                  replace: "my \\{\\{unknown\\}\\}"
-            "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is my {{unknown}}");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_multi_trigger_no_args() {
-        let text = "this is a :yolo and :test";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - triggers: [':test', ':yolo']
-              replace: result
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "this is a result and result");
-    }
-
-    #[test]
-    fn test_render_passive_simple_match_multi_trigger_with_args() {
-        let text = ":yolo/Jon/";
-
-        let config = get_config_for(
-            r###"
-        matches:
-            - triggers: [':greet', ':yolo']
-              replace: "Hi $0$"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let rendered = renderer.render_passive(text, &config);
-
-        verify_render(rendered, "Hi Jon");
-    }
-
-    #[test]
-    fn test_render_match_case_propagation_no_case() {
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: 'test'
-              replace: result
-              propagate_case: true
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let m = config.matches[0].clone();
-
-        let trigger_offset = m.triggers.iter().position(|x| x == "test").unwrap();
-
-        let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
-
-        verify_render(rendered, "result");
-    }
-
-    #[test]
-    fn test_render_match_case_propagation_first_capital() {
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: 'test'
-              replace: result
-              propagate_case: true
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let m = config.matches[0].clone();
-
-        let trigger_offset = m.triggers.iter().position(|x| x == "Test").unwrap();
-
-        let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
-
-        verify_render(rendered, "Result");
-    }
-
-    #[test]
-    fn test_render_match_case_propagation_all_capital() {
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: 'test'
-              replace: result
-              propagate_case: true
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-
-        let m = config.matches[0].clone();
-
-        let trigger_offset = m.triggers.iter().position(|x| x == "TEST").unwrap();
-
-        let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
-
-        verify_render(rendered, "RESULT");
-    }
-
-    #[test]
-    fn test_render_variable_order() {
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: 'test'
-              replace: "{{output}}"
-              vars:
-                - name: first
-                  type: dummy
-                  params:
-                    echo: "hello"
-                - name: output
-                  type: vardummy
-                  params:
-                    target: "first"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-        let m = config.matches[0].clone();
-        let rendered = renderer.render_match(&m, 0, &config, vec![]);
-        verify_render(rendered, "hello");
-    }
-
-    #[test]
-    fn test_render_global_variable_order() {
-        let config = get_config_for(
-            r###"
-        global_vars:
-          - name: hello
-            type: dummy
-            params:
-              echo: "hello"
-        matches:
-            - trigger: 'test'
-              replace: "{{hello}} {{output}}"
-              vars:
-                - name: first
-                  type: dummy
-                  params:
-                    echo: "world"
-                - name: output
-                  type: vardummy
-                  params:
-                    target: "first"
-                - name: hello
-                  type: global
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-        let m = config.matches[0].clone();
-        let rendered = renderer.render_match(&m, 0, &config, vec![]);
-        verify_render(rendered, "hello world");
-    }
-
-    #[test]
-    fn test_render_multiple_results() {
-        let config = get_config_for(
-            r###"
-        matches:
-            - trigger: 'test'
-              replace: "hello {{var1.name}}"
-              vars:
-                - name: var1
-                  type: multiecho
-                  params:
-                    name: "world"
-        "###,
-        );
-
-        let renderer = get_renderer(config.clone());
-        let m = config.matches[0].clone();
-        let rendered = renderer.render_match(&m, 0, &config, vec![]);
-        verify_render(rendered, "hello world");
-    }
-}
diff --git a/src/render/utils.rs b/src/render/utils.rs
deleted file mode 100644
index cfee8d7c..00000000
--- a/src/render/utils.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 regex::{Captures, Regex};
-
-lazy_static! {
-    static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap();
-}
-
-pub fn render_args(text: &str, args: &Vec<String>) -> String {
-    let result = ARG_REGEX.replace_all(text, |caps: &Captures| {
-        let position_str = caps.name("pos").unwrap().as_str();
-        let position = position_str.parse::<i32>().unwrap_or(-1);
-
-        if position >= 0 && position < args.len() as i32 {
-            args[position as usize].to_owned()
-        } else {
-            "".to_owned()
-        }
-    });
-
-    result.to_string()
-}
-
-pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> {
-    let mut output = vec![];
-
-    // Make sure the text is not empty
-    if text.is_empty() {
-        return output;
-    }
-
-    let mut last = String::from("");
-    let mut previous: char = char::from(0);
-    text.chars().into_iter().for_each(|c| {
-        if c == delimiter {
-            if previous != escape {
-                output.push(last.clone());
-                last = String::from("");
-            } else {
-                last.push(c);
-            }
-        } else if c == escape {
-            if previous == escape {
-                last.push(c);
-            }
-        } else {
-            last.push(c);
-        }
-        previous = c;
-    });
-
-    // Add the last one
-    output.push(last);
-
-    output
-}
-
-// TESTS
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_render_args_no_args() {
-        let args = vec!["hello".to_owned()];
-        assert_eq!(render_args("no args", &args), "no args")
-    }
-
-    #[test]
-    fn test_render_args_one_arg() {
-        let args = vec!["jon".to_owned()];
-        assert_eq!(render_args("hello $0$", &args), "hello jon")
-    }
-
-    #[test]
-    fn test_render_args_one_multiple_args() {
-        let args = vec!["jon".to_owned(), "snow".to_owned()];
-        assert_eq!(
-            render_args("hello $0$, the $1$ is white", &args),
-            "hello jon, the snow is white"
-        )
-    }
-
-    #[test]
-    fn test_render_args_out_of_range() {
-        let args = vec!["jon".to_owned()];
-        assert_eq!(render_args("hello $10$", &args), "hello ")
-    }
-
-    #[test]
-    fn test_split_args_one_arg() {
-        assert_eq!(split_args("jon", '/', '\\'), vec!["jon"])
-    }
-
-    #[test]
-    fn test_split_args_two_args() {
-        assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"])
-    }
-
-    #[test]
-    fn test_split_args_escaping() {
-        assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"])
-    }
-
-    #[test]
-    fn test_split_args_escaping_escape() {
-        assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"])
-    }
-
-    #[test]
-    fn test_split_args_empty() {
-        let empty_vec: Vec<String> = vec![];
-        assert_eq!(split_args("", '/', '\\'), empty_vec)
-    }
-}
diff --git a/src/res/linux/icon.png b/src/res/linux/icon.png
deleted file mode 100644
index 9bde0d24..00000000
Binary files a/src/res/linux/icon.png and /dev/null differ
diff --git a/src/res/mac/AppIcon.icns b/src/res/mac/AppIcon.icns
deleted file mode 100644
index d3f07c39..00000000
Binary files a/src/res/mac/AppIcon.icns and /dev/null differ
diff --git a/src/res/mac/EspansoNotifyHelper.zip b/src/res/mac/EspansoNotifyHelper.zip
deleted file mode 100644
index 036ba7c3..00000000
Binary files a/src/res/mac/EspansoNotifyHelper.zip and /dev/null differ
diff --git a/src/res/mac/icon.png b/src/res/mac/icon.png
deleted file mode 100644
index cde3e917..00000000
Binary files a/src/res/mac/icon.png and /dev/null differ
diff --git a/src/res/mac/icondisabled.png b/src/res/mac/icondisabled.png
deleted file mode 100644
index a289156c..00000000
Binary files a/src/res/mac/icondisabled.png and /dev/null differ
diff --git a/src/res/mac/modulo.plist b/src/res/mac/modulo.plist
deleted file mode 100644
index 3bf440aa..00000000
--- a/src/res/mac/modulo.plist
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>CFBundleDevelopmentRegion</key>
-	<string>English</string>
-	<key>CFBundleExecutable</key>
-	<string>{{{modulo_path}}}</string>
-	<key>CFBundleIconFile</key>
-	<string>AppIcon</string>
-	<key>CFBundleIconName</key>
-	<string>AppIcon</string>
-	<key>CFBundleIdentifier</key>
-	<string>com.federicoterzi.modulo</string>
-	<key>CFBundleInfoDictionaryVersion</key>
-	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>Modulo</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleSignature</key>
-	<string>????</string>
-	<key>CFBundleVersion</key>
-	<string>1.0</string>
-	<key>NSMainNibFile</key>
-	<string>MainMenu</string>
-	<key>NSPrincipalClass</key>
-	<string>NSApplication</string>
-</dict>
-</plist>
diff --git a/src/res/win/espanso.bmp b/src/res/win/espanso.bmp
deleted file mode 100644
index 882c849d..00000000
Binary files a/src/res/win/espanso.bmp and /dev/null differ
diff --git a/src/res/win/espanso.ico b/src/res/win/espanso.ico
deleted file mode 100644
index 1d1c06c7..00000000
Binary files a/src/res/win/espanso.ico and /dev/null differ
diff --git a/src/res/win/espansored.ico b/src/res/win/espansored.ico
deleted file mode 100644
index 334bab0c..00000000
Binary files a/src/res/win/espansored.ico and /dev/null differ
diff --git a/src/sysdaemon.rs b/src/sysdaemon.rs
deleted file mode 100644
index de54983c..00000000
--- a/src/sysdaemon.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-// This functions are used to register/unregister espanso from the system daemon manager.
-
-use crate::config::ConfigSet;
-use crate::sysdaemon::VerifyResult::{EnabledAndValid, EnabledButInvalidPath, NotEnabled};
-
-// INSTALLATION
-
-#[cfg(target_os = "macos")]
-const MAC_PLIST_CONTENT: &str = include_str!("res/mac/com.federicoterzi.espanso.plist");
-#[cfg(target_os = "macos")]
-const MAC_PLIST_FILENAME: &str = "com.federicoterzi.espanso.plist";
-
-#[cfg(target_os = "macos")]
-pub fn register(_config_set: ConfigSet) {
-    use std::fs::create_dir_all;
-    use std::process::{Command, ExitStatus};
-
-    let home_dir = dirs::home_dir().expect("Could not get user home directory");
-    let library_dir = home_dir.join("Library");
-    let agents_dir = library_dir.join("LaunchAgents");
-
-    // Make sure agents directory exists
-    if !agents_dir.exists() {
-        create_dir_all(agents_dir.clone()).expect("Could not create LaunchAgents directory");
-    }
-
-    let plist_file = agents_dir.join(MAC_PLIST_FILENAME);
-    if !plist_file.exists() {
-        println!(
-            "Creating LaunchAgents entry: {}",
-            plist_file.to_str().unwrap_or_default()
-        );
-
-        let espanso_path = std::env::current_exe().expect("Could not get espanso executable path");
-        println!(
-            "Entry will point to: {}",
-            espanso_path.to_str().unwrap_or_default()
-        );
-
-        let plist_content = String::from(MAC_PLIST_CONTENT).replace(
-            "{{{espanso_path}}}",
-            espanso_path.to_str().unwrap_or_default(),
-        );
-
-        // Copy the user PATH variable and inject it in the Plist file so that
-        // it gets loaded by Launchd.
-        // To see why this is necessary: https://github.com/federico-terzi/espanso/issues/233
-        let user_path = std::env::var("PATH").unwrap_or("".to_owned());
-        let plist_content = plist_content.replace("{{{PATH}}}", &user_path);
-
-        std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file");
-
-        println!("Entry created correctly!")
-    }
-
-    println!("Reloading entry...");
-
-    let res = Command::new("launchctl")
-        .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()])
-        .output();
-
-    let res = Command::new("launchctl")
-        .args(&["load", "-w", plist_file.to_str().unwrap_or_default()])
-        .status();
-
-    if let Ok(status) = res {
-        if status.success() {
-            println!("Entry loaded correctly!")
-        }
-    } else {
-        println!("Error loading new entry");
-    }
-}
-
-#[cfg(target_os = "macos")]
-pub fn unregister(_config_set: ConfigSet) {
-    use std::fs::create_dir_all;
-    use std::process::{Command, ExitStatus};
-
-    let home_dir = dirs::home_dir().expect("Could not get user home directory");
-    let library_dir = home_dir.join("Library");
-    let agents_dir = library_dir.join("LaunchAgents");
-
-    let plist_file = agents_dir.join(MAC_PLIST_FILENAME);
-    if plist_file.exists() {
-        let _res = Command::new("launchctl")
-            .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()])
-            .output();
-
-        std::fs::remove_file(&plist_file).expect("Could not remove espanso entry");
-
-        println!("Entry removed correctly!")
-    } else {
-        println!("espanso is not installed");
-    }
-}
-
-// LINUX
-
-#[cfg(target_os = "linux")]
-const LINUX_SERVICE_CONTENT: &str = include_str!("res/linux/systemd.service");
-#[cfg(target_os = "linux")]
-const LINUX_SERVICE_FILENAME: &str = "espanso.service";
-
-#[cfg(target_os = "linux")]
-pub fn register(_: ConfigSet) {
-    use std::fs::create_dir_all;
-    use std::process::Command;
-
-    // Check if espanso service is already registered
-    let res = Command::new("systemctl")
-        .args(&["--user", "is-enabled", "espanso"])
-        .output();
-    if let Ok(res) = res {
-        let output = String::from_utf8_lossy(res.stdout.as_slice());
-        let output = output.trim();
-        if res.status.success() {
-            if output == "enabled" {
-                eprintln!("espanso service is already registered to systemd");
-                eprintln!("If you want to register it again, please uninstall it first with:");
-                eprintln!("    espanso unregister");
-                std::process::exit(5);
-            }
-        } else {
-            if output == "disabled" {
-                use dialoguer::Confirmation;
-                if !Confirmation::new()
-                    .with_text("espanso is already registered but currently disabled. Do you want to override it?")
-                    .default(false)
-                    .show_default(true)
-                    .interact().expect("Unable to read user answer") {
-
-                    std::process::exit(6);
-                }
-            }
-        }
-    }
-
-    // User level systemd services should be placed in this directory:
-    // $XDG_CONFIG_HOME/systemd/user/, usually: ~/.config/systemd/user/
-    let config_dir = dirs::config_dir().expect("Could not get configuration directory");
-    let systemd_dir = config_dir.join("systemd");
-    let user_dir = systemd_dir.join("user");
-
-    // Make sure the directory exists
-    if !user_dir.exists() {
-        create_dir_all(user_dir.clone()).expect("Could not create systemd user directory");
-    }
-
-    let service_file = user_dir.join(LINUX_SERVICE_FILENAME);
-    if !service_file.exists() {
-        println!(
-            "Creating service entry: {}",
-            service_file.to_str().unwrap_or_default()
-        );
-
-        let espanso_path = std::env::current_exe().expect("Could not get espanso executable path");
-        println!(
-            "Entry will point to: {}",
-            espanso_path.to_str().unwrap_or_default()
-        );
-
-        let service_content = String::from(LINUX_SERVICE_CONTENT).replace(
-            "{{{espanso_path}}}",
-            espanso_path.to_str().unwrap_or_default(),
-        );
-
-        std::fs::write(service_file.clone(), service_content)
-            .expect("Unable to write service file");
-
-        println!("Service file created correctly!")
-    }
-
-    println!("Enabling espanso for systemd...");
-
-    let res = Command::new("systemctl")
-        .args(&["--user", "enable", "espanso"])
-        .status();
-
-    if let Ok(status) = res {
-        if status.success() {
-            println!("Service registered correctly!")
-        }
-    } else {
-        println!("Error loading espanso service");
-    }
-}
-
-pub enum VerifyResult {
-    EnabledAndValid,
-    EnabledButInvalidPath,
-    NotEnabled,
-}
-
-#[cfg(target_os = "linux")]
-pub fn verify() -> VerifyResult {
-    use regex::Regex;
-    use std::process::Command;
-
-    // Check if espanso service is already registered
-    let res = Command::new("systemctl")
-        .args(&["--user", "is-enabled", "espanso"])
-        .output();
-    if let Ok(res) = res {
-        let output = String::from_utf8_lossy(res.stdout.as_slice());
-        let output = output.trim();
-        if !res.status.success() || output != "enabled" {
-            return NotEnabled;
-        }
-    }
-
-    lazy_static! {
-        static ref EXEC_PATH_REGEX: Regex = Regex::new("ExecStart=(?P<path>.*?)\\s").unwrap();
-    }
-
-    // Check if the currently registered path is valid
-    let res = Command::new("systemctl")
-        .args(&["--user", "cat", "espanso"])
-        .output();
-    if let Ok(res) = res {
-        let output = String::from_utf8_lossy(res.stdout.as_slice());
-        let output = output.trim();
-        if res.status.success() {
-            let caps = EXEC_PATH_REGEX.captures(output).unwrap();
-            let path = caps.get(1).map_or("", |m| m.as_str());
-            let espanso_path =
-                std::env::current_exe().expect("Could not get espanso executable path");
-
-            if espanso_path.to_string_lossy() != path {
-                return EnabledButInvalidPath;
-            }
-        }
-    }
-
-    EnabledAndValid
-}
-
-#[cfg(target_os = "linux")]
-pub fn unregister(_: ConfigSet) {
-    use std::process::Command;
-
-    // Disable the service first
-    Command::new("systemctl")
-        .args(&["--user", "disable", "espanso"])
-        .status()
-        .expect("Unable to invoke systemctl");
-
-    // Then delete the espanso.service entry
-    let config_dir = dirs::config_dir().expect("Could not get configuration directory");
-    let systemd_dir = config_dir.join("systemd");
-    let user_dir = systemd_dir.join("user");
-    let service_file = user_dir.join(LINUX_SERVICE_FILENAME);
-
-    if service_file.exists() {
-        let res = std::fs::remove_file(&service_file);
-        match res {
-            Ok(_) => {
-                println!("Deleted entry at {}", service_file.to_string_lossy());
-                println!("Service unregistered successfully!");
-            }
-            Err(e) => {
-                println!(
-                    "Error, could not delete service entry at {} with error {}",
-                    service_file.to_string_lossy(),
-                    e
-                );
-            }
-        }
-    } else {
-        eprintln!("Error, could not find espanso service file");
-    }
-}
-
-// WINDOWS
-
-#[cfg(target_os = "windows")]
-pub fn register(_config_set: ConfigSet) {
-    println!("Windows does not support automatic system daemon integration.")
-}
-
-#[cfg(target_os = "windows")]
-pub fn unregister(_config_set: ConfigSet) {
-    println!("Windows does not support automatic system daemon integration.")
-}
diff --git a/src/system/linux.rs b/src/system/linux.rs
deleted file mode 100644
index 0f8b144d..00000000
--- a/src/system/linux.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::os::raw::c_char;
-
-use crate::bridge::linux::{
-    get_active_window_class, get_active_window_executable, get_active_window_name,
-};
-use std::ffi::CStr;
-
-pub struct LinuxSystemManager {}
-
-impl super::SystemManager for LinuxSystemManager {
-    fn get_current_window_title(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 256] = [0; 256];
-            let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-
-    fn get_current_window_class(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 256] = [0; 256];
-            let res = get_active_window_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-
-    fn get_current_window_executable(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 256] = [0; 256];
-            let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-}
-
-impl LinuxSystemManager {
-    pub fn new() -> LinuxSystemManager {
-        LinuxSystemManager {}
-    }
-}
diff --git a/src/system/macos.rs b/src/system/macos.rs
deleted file mode 100644
index 4480c3d7..00000000
--- a/src/system/macos.rs
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::os::raw::c_char;
-
-use crate::bridge::macos::{
-    get_active_app_bundle, get_active_app_identifier, get_path_from_pid, get_secure_input_process,
-};
-use std::ffi::CStr;
-
-pub struct MacSystemManager {}
-
-impl super::SystemManager for MacSystemManager {
-    fn get_current_window_title(&self) -> Option<String> {
-        self.get_current_window_class()
-    }
-
-    fn get_current_window_class(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 256] = [0; 256];
-            let res = get_active_app_identifier(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-
-    fn get_current_window_executable(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [c_char; 256] = [0; 256];
-            let res = get_active_app_bundle(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = CStr::from_ptr(buffer.as_ptr());
-
-                let string = c_string.to_str();
-                if let Ok(string) = string {
-                    return Some((*string).to_owned());
-                }
-            }
-        }
-
-        None
-    }
-}
-
-impl MacSystemManager {
-    pub fn new() -> MacSystemManager {
-        MacSystemManager {}
-    }
-
-    /// Check whether an application is currently holding the Secure Input.
-    /// Return None if no application has claimed SecureInput, its PID otherwise.
-    pub fn get_secure_input_pid() -> Option<i64> {
-        unsafe {
-            let mut pid: i64 = -1;
-            let res = get_secure_input_process(&mut pid as *mut i64);
-
-            if res > 0 {
-                Some(pid)
-            } else {
-                None
-            }
-        }
-    }
-
-    /// Check whether an application is currently holding the Secure Input.
-    /// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise.
-    pub fn get_secure_input_application() -> Option<(String, String)> {
-        unsafe {
-            let pid = MacSystemManager::get_secure_input_pid();
-
-            if let Some(pid) = pid {
-                // Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant.
-                // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently.
-                let mut buffer: [c_char; 4096] = [0; 4096];
-                let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32);
-
-                if res > 0 {
-                    let c_string = CStr::from_ptr(buffer.as_ptr());
-                    let string = c_string.to_str();
-                    if let Ok(path) = string {
-                        if !path.trim().is_empty() {
-                            let process = path.trim().to_string();
-                            let app_name =
-                                if let Some(name) = Self::get_app_name_from_path(&process) {
-                                    name
-                                } else {
-                                    process.to_owned()
-                                };
-
-                            return Some((app_name, process));
-                        }
-                    }
-                }
-            }
-
-            None
-        }
-    }
-
-    fn get_app_name_from_path(path: &str) -> Option<String> {
-        use regex::Regex;
-
-        lazy_static! {
-            static ref APP_REGEX: Regex = Regex::new("/([^/]+).(app|bundle)/").unwrap();
-        };
-
-        let caps = APP_REGEX.captures(&path);
-        if let Some(caps) = caps {
-            Some(caps.get(1).map_or("", |m| m.as_str()).to_owned())
-        } else {
-            None
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_get_app_name_from_path() {
-        let app_name = MacSystemManager::get_app_name_from_path(
-            "/Applications/iTerm.app/Contents/MacOS/iTerm2",
-        );
-        assert_eq!(app_name.unwrap(), "iTerm")
-    }
-
-    #[test]
-    fn test_get_app_name_from_path_no_app_name() {
-        let app_name = MacSystemManager::get_app_name_from_path("/another/directory");
-        assert!(app_name.is_none())
-    }
-
-    #[test]
-    fn test_get_app_name_from_path_security_bundle() {
-        let app_name = MacSystemManager::get_app_name_from_path("/System/Library/Frameworks/Security.framework/Versions/A/MachServices/SecurityAgent.bundle/Contents/MacOS/SecurityAgent");
-        assert_eq!(app_name.unwrap(), "SecurityAgent")
-    }
-}
diff --git a/src/system/windows.rs b/src/system/windows.rs
deleted file mode 100644
index 4d7eb271..00000000
--- a/src/system/windows.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::windows::*;
-use widestring::U16CString;
-
-pub struct WindowsSystemManager {}
-
-impl WindowsSystemManager {
-    pub fn new() -> WindowsSystemManager {
-        WindowsSystemManager {}
-    }
-}
-
-impl super::SystemManager for WindowsSystemManager {
-    fn get_current_window_title(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [u16; 256] = [0; 256];
-            let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = U16CString::from_ptr_str(buffer.as_ptr());
-
-                let string = c_string.to_string_lossy();
-                return Some((*string).to_owned());
-            }
-        }
-
-        None
-    }
-
-    fn get_current_window_class(&self) -> Option<String> {
-        self.get_current_window_executable()
-    }
-
-    fn get_current_window_executable(&self) -> Option<String> {
-        unsafe {
-            let mut buffer: [u16; 256] = [0; 256];
-            let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
-
-            if res > 0 {
-                let c_string = U16CString::from_ptr_str(buffer.as_ptr());
-
-                let string = c_string.to_string_lossy();
-                return Some((*string).to_owned());
-            }
-        }
-
-        None
-    }
-}
diff --git a/src/ui/linux.rs b/src/ui/linux.rs
deleted file mode 100644
index d62a46a0..00000000
--- a/src/ui/linux.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 super::MenuItem;
-use log::{error, info};
-use std::path::PathBuf;
-use std::process::Command;
-
-const LINUX_ICON_CONTENT: &[u8] = include_bytes!("../res/linux/icon.png");
-
-pub struct LinuxUIManager {
-    icon_path: PathBuf,
-}
-
-impl super::UIManager for LinuxUIManager {
-    fn notify(&self, message: &str) {
-        self.notify_delay(message, 2000);
-    }
-
-    fn notify_delay(&self, message: &str, duration: i32) {
-        let res = Command::new("notify-send")
-            .args(&[
-                "-i",
-                self.icon_path.to_str().unwrap_or_default(),
-                "-t",
-                &duration.to_string(),
-                "espanso",
-                message,
-            ])
-            .output();
-
-        if let Err(e) = res {
-            error!("Could not send a notification, error: {}", e);
-        }
-    }
-
-    fn show_menu(&self, _menu: Vec<MenuItem>) {
-        // Not implemented on linux
-    }
-
-    fn cleanup(&self) {
-        // Nothing to do here
-    }
-}
-
-impl LinuxUIManager {
-    pub fn new() -> LinuxUIManager {
-        // Initialize the icon if not present
-        let data_dir = crate::context::get_data_dir();
-        let icon_path = data_dir.join("icon.png");
-        if !icon_path.exists() {
-            info!(
-                "Creating espanso icon in '{}'",
-                icon_path.to_str().unwrap_or_default()
-            );
-            std::fs::write(&icon_path, LINUX_ICON_CONTENT).expect("Unable to copy espanso icon");
-        }
-
-        LinuxUIManager { icon_path }
-    }
-}
diff --git a/src/ui/macos.rs b/src/ui/macos.rs
deleted file mode 100644
index 8784f701..00000000
--- a/src/ui/macos.rs
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::macos::{show_context_menu, MacMenuItem};
-use crate::context;
-use crate::ui::{MenuItem, MenuItemType};
-use log::{debug, info, warn};
-use std::ffi::CString;
-use std::io::Cursor;
-use std::os::raw::c_char;
-use std::path::PathBuf;
-use std::process::Command;
-use std::{fs, io};
-
-const NOTIFY_HELPER_BINARY: &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip");
-
-pub struct MacUIManager {
-    notify_helper_path: PathBuf,
-}
-
-impl super::UIManager for MacUIManager {
-    fn notify(&self, message: &str) {
-        self.notify_delay(message, 1500);
-    }
-
-    fn notify_delay(&self, message: &str, duration: i32) {
-        let executable_path = self.notify_helper_path.join("Contents");
-        let executable_path = executable_path.join("MacOS");
-        let executable_path = executable_path.join("EspansoNotifyHelper");
-
-        let duration_float = duration as f64 / 1000.0;
-
-        let res = Command::new(executable_path)
-            .args(&["espanso", message, &duration_float.to_string()])
-            .spawn();
-
-        if let Err(e) = res {
-            warn!("Error while dispatching Notify Helper {}", e)
-        }
-    }
-
-    fn show_menu(&self, menu: Vec<MenuItem>) {
-        let mut raw_menu = Vec::new();
-
-        for item in menu.iter() {
-            let text = CString::new(item.item_name.clone()).unwrap_or_default();
-            let mut str_buff: [c_char; 100] = [0; 100];
-            unsafe {
-                std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), item.item_name.len());
-            }
-
-            let menu_type = match item.item_type {
-                MenuItemType::Button => 1,
-                MenuItemType::Separator => 2,
-            };
-
-            let raw_item = MacMenuItem {
-                item_id: item.item_id,
-                item_type: menu_type,
-                item_name: str_buff,
-            };
-
-            raw_menu.push(raw_item);
-        }
-
-        unsafe {
-            show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32);
-        }
-    }
-
-    fn cleanup(&self) {
-        // Nothing to do here
-    }
-}
-
-impl MacUIManager {
-    pub fn new() -> MacUIManager {
-        let notify_helper_path = MacUIManager::initialize_notify_helper();
-
-        MacUIManager { notify_helper_path }
-    }
-
-    fn initialize_notify_helper() -> PathBuf {
-        let espanso_dir = context::get_data_dir();
-
-        info!(
-            "Initializing EspansoNotifyHelper in {}",
-            espanso_dir.as_path().display()
-        );
-
-        let espanso_target = espanso_dir.join("EspansoNotifyHelper.app");
-
-        if espanso_target.exists() {
-            info!("EspansoNotifyHelper already initialized, skipping.");
-        } else {
-            // Extract zip file
-            let reader = Cursor::new(NOTIFY_HELPER_BINARY);
-
-            let mut archive = zip::ZipArchive::new(reader).unwrap();
-
-            for i in 0..archive.len() {
-                let mut file = archive.by_index(i).unwrap();
-                let outpath = espanso_dir.join(file.sanitized_name());
-
-                {
-                    let comment = file.comment();
-                    if !comment.is_empty() {
-                        debug!("File {} comment: {}", i, comment);
-                    }
-                }
-
-                if (&*file.name()).ends_with('/') {
-                    debug!(
-                        "File {} extracted to \"{}\"",
-                        i,
-                        outpath.as_path().display()
-                    );
-                    fs::create_dir_all(&outpath).unwrap();
-                } else {
-                    debug!(
-                        "File {} extracted to \"{}\" ({} bytes)",
-                        i,
-                        outpath.as_path().display(),
-                        file.size()
-                    );
-                    if let Some(p) = outpath.parent() {
-                        if !p.exists() {
-                            fs::create_dir_all(&p).unwrap();
-                        }
-                    }
-                    let mut outfile = fs::File::create(&outpath).unwrap();
-                    io::copy(&mut file, &mut outfile).unwrap();
-                }
-
-                use std::os::unix::fs::PermissionsExt;
-
-                if let Some(mode) = file.unix_mode() {
-                    fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap();
-                }
-            }
-        }
-
-        espanso_target
-    }
-}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
deleted file mode 100644
index f25491c0..00000000
--- a/src/ui/mod.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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/>.
- */
-
-#[cfg(target_os = "windows")]
-mod windows;
-
-#[cfg(target_os = "linux")]
-mod linux;
-
-#[cfg(target_os = "macos")]
-mod macos;
-
-pub mod modulo;
-
-pub trait UIManager {
-    fn notify(&self, message: &str);
-    fn notify_delay(&self, message: &str, duration: i32);
-    fn show_menu(&self, menu: Vec<MenuItem>);
-    fn cleanup(&self);
-}
-
-pub enum MenuItemType {
-    Button,
-    Separator,
-}
-
-pub struct MenuItem {
-    pub item_id: i32,
-    pub item_type: MenuItemType,
-    pub item_name: String,
-}
-
-// MAC IMPLEMENTATION
-#[cfg(target_os = "macos")]
-pub fn get_uimanager() -> impl UIManager {
-    macos::MacUIManager::new()
-}
-
-// LINUX IMPLEMENTATION
-#[cfg(target_os = "linux")]
-pub fn get_uimanager() -> impl UIManager {
-    linux::LinuxUIManager::new()
-}
-
-// WINDOWS IMPLEMENTATION
-#[cfg(target_os = "windows")]
-pub fn get_uimanager() -> impl UIManager {
-    windows::WindowsUIManager::new()
-}
diff --git a/src/ui/modulo/mac.rs b/src/ui/modulo/mac.rs
deleted file mode 100644
index 394ec889..00000000
--- a/src/ui/modulo/mac.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 log::info;
-use std::os::unix::fs::symlink;
-use std::path::PathBuf;
-
-const MODULO_APP_BUNDLE_NAME: &str = "Modulo.app";
-const MODULO_APP_BUNDLE_PLIST_CONTENT: &'static str = include_str!("../../res/mac/modulo.plist");
-const MODULO_APP_BUNDLE_ICON: &[u8] = include_bytes!("../../res/mac/AppIcon.icns");
-
-pub fn generate_modulo_app_bundle(modulo_path: &str) -> Result<PathBuf, std::io::Error> {
-    let modulo_pathbuf = PathBuf::from(modulo_path);
-    let modulo_path: String = if !modulo_pathbuf.exists() {
-        // If modulo was taken from the PATH, we need to calculate the absolute path
-        // To do so, we use the `which` command
-        let output = std::process::Command::new("which")
-            .arg("modulo")
-            .output()
-            .expect("unable to call 'which' command to determine modulo's full path");
-        let path = String::from_utf8_lossy(output.stdout.as_slice());
-        let path = path.trim();
-
-        info!("Detected modulo's full path: {:?}", &path);
-        path.to_string()
-    } else {
-        modulo_path.to_owned()
-    };
-
-    let data_dir = crate::context::get_data_dir();
-
-    let modulo_app_dir = data_dir.join(MODULO_APP_BUNDLE_NAME);
-
-    // Remove previous bundle if present
-    if modulo_app_dir.exists() {
-        std::fs::remove_dir_all(&modulo_app_dir)?;
-    }
-
-    // Recreate the App bundle stub
-    std::fs::create_dir(&modulo_app_dir)?;
-
-    let contents_dir = modulo_app_dir.join("Contents");
-    std::fs::create_dir(&contents_dir)?;
-
-    let macos_dir = contents_dir.join("MacOS");
-    std::fs::create_dir(&macos_dir)?;
-
-    let resources_dir = contents_dir.join("Resources");
-    std::fs::create_dir(&resources_dir)?;
-
-    // Generate the Plist file
-    let plist_content = MODULO_APP_BUNDLE_PLIST_CONTENT.replace("{{{modulo_path}}}", &modulo_path);
-    let plist_file = contents_dir.join("Info.plist");
-    std::fs::write(plist_file, plist_content)?;
-
-    // Copy the icon file
-    let icon_file = resources_dir.join("AppIcon.icns");
-    std::fs::write(icon_file, MODULO_APP_BUNDLE_ICON)?;
-
-    // Generate the symbolic link to the modulo binary
-    let target_link = macos_dir.join("modulo");
-    symlink(modulo_path, &target_link)?;
-
-    info!("Created Modulo APP stub at: {:?}", &target_link);
-
-    Ok(target_link)
-}
diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs
deleted file mode 100644
index 7b1544fa..00000000
--- a/src/ui/modulo/mod.rs
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2020 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 crate::config::Configs;
-use log::{error, info};
-use std::io::{Error, Write};
-use std::process::{Child, Command, Output};
-
-#[cfg(target_os = "macos")]
-mod mac;
-
-pub struct ModuloManager {
-    modulo_path: Option<String>,
-}
-
-impl ModuloManager {
-    pub fn new(config: &Configs) -> Self {
-        let mut modulo_path: Option<String> = None;
-        // Check if the `MODULO_PATH` env variable is configured
-        if let Some(_modulo_path) = std::env::var_os("MODULO_PATH") {
-            modulo_path = Some(_modulo_path.to_string_lossy().to_string())
-        } else if let Some(ref _modulo_path) = config.modulo_path {
-            // Check the configs
-            modulo_path = Some(_modulo_path.to_owned());
-        } else {
-            // Check in the same directory of espanso
-            if let Ok(exe_path) = std::env::current_exe() {
-                if let Some(parent) = exe_path.parent() {
-                    let possible_path = parent.join("modulo");
-                    let possible_path = possible_path.to_string_lossy().to_string();
-
-                    if let Ok(output) = Command::new(&possible_path).arg("--version").output() {
-                        if output.status.success() {
-                            modulo_path = Some(possible_path);
-                        }
-                    }
-                }
-            }
-
-            // Otherwise check if present in the PATH
-            if modulo_path.is_none() {
-                if let Ok(output) = Command::new("modulo").arg("--version").output() {
-                    if output.status.success() {
-                        modulo_path = Some("modulo".to_owned());
-                    }
-                }
-            }
-        }
-
-        if let Some(ref path) = modulo_path {
-            info!("Using modulo at {:?}", path);
-
-            // MacOS specific remark
-            // In order to give modulo the focus when spawning a form, modulo has to be
-            // wrapped inside an application bundle. Therefore, we generate a bundle
-            // at startup.
-            // See issue: https://github.com/federico-terzi/espanso/issues/430
-            #[cfg(target_os = "macos")]
-            {
-                modulo_path = Some(
-                    mac::generate_modulo_app_bundle(path)
-                        .expect("unable to generate modulo app stub")
-                        .to_string_lossy()
-                        .to_string(),
-                );
-            }
-        }
-
-        Self { modulo_path }
-    }
-
-    pub fn is_valid(&self) -> bool {
-        self.modulo_path.is_some()
-    }
-
-    pub fn get_version(&self) -> Option<String> {
-        if let Some(ref modulo_path) = self.modulo_path {
-            if let Ok(output) = Command::new(modulo_path).arg("--version").output() {
-                let version = String::from_utf8_lossy(&output.stdout);
-                return Some(version.to_string());
-            }
-        }
-
-        None
-    }
-
-    pub fn invoke(&self, args: &[&str], body: &str) -> Option<String> {
-        if self.modulo_path.is_none() {
-            error!("Attempt to invoke modulo even though it's not configured");
-            return None;
-        }
-
-        if let Some(ref modulo_path) = self.modulo_path {
-            let mut command = Command::new(modulo_path);
-            command
-                .args(args)
-                .stdin(std::process::Stdio::piped())
-                .stdout(std::process::Stdio::piped())
-                .stderr(std::process::Stdio::piped());
-
-            crate::utils::set_command_flags(&mut command);
-
-            let child = command.spawn();
-
-            match child {
-                Ok(mut child) => {
-                    if let Some(stdin) = child.stdin.as_mut() {
-                        match stdin.write_all(body.as_bytes()) {
-                            Ok(_) => {
-                                // Get the output
-                                match child.wait_with_output() {
-                                    Ok(child_output) => {
-                                        let output = String::from_utf8_lossy(&child_output.stdout);
-
-                                        // Check also if the program reports an error
-                                        let error = String::from_utf8_lossy(&child_output.stderr);
-                                        if !error.is_empty() {
-                                            error!("modulo reported an error: {}", error);
-                                        }
-
-                                        return Some(output.to_string());
-                                    }
-                                    Err(error) => {
-                                        error!("error while getting output from modulo: {}", error);
-                                    }
-                                }
-                            }
-                            Err(error) => {
-                                error!("error while sending body to modulo: {}", error);
-                            }
-                        }
-                    } else {
-                        error!("unable to open stdin to modulo");
-                    }
-                }
-                Err(error) => {
-                    error!("error reported when invoking modulo: {}", error);
-                }
-            }
-        }
-
-        None
-    }
-}
diff --git a/src/ui/windows.rs b/src/ui/windows.rs
deleted file mode 100644
index e0bbc99a..00000000
--- a/src/ui/windows.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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 crate::bridge::windows::{
-    cleanup_ui, close_notification, show_context_menu, show_notification, WindowsMenuItem,
-};
-use crate::ui::{MenuItem, MenuItemType};
-use log::debug;
-use std::sync::Arc;
-use std::sync::Mutex;
-use std::{thread, time};
-use widestring::U16CString;
-
-pub struct WindowsUIManager {
-    id: Arc<Mutex<i32>>,
-}
-
-impl super::UIManager for WindowsUIManager {
-    fn notify(&self, message: &str) {
-        self.notify_delay(message, 2000);
-    }
-
-    fn notify_delay(&self, message: &str, duration: i32) {
-        let current_id: i32 = {
-            let mut id = self.id.lock().unwrap();
-            *id += 1;
-            *id
-        };
-
-        let step = duration / 10;
-
-        // Setup a timeout to close the notification
-        let id = Arc::clone(&self.id);
-        let _ = thread::Builder::new()
-            .name("notification_thread".to_string())
-            .spawn(move || {
-                for _ in 1..10 {
-                    let duration = time::Duration::from_millis(step as u64);
-                    thread::sleep(duration);
-
-                    let new_id = id.lock().unwrap();
-                    if *new_id != current_id {
-                        debug!("Cancelling notification close event with id {}", current_id);
-                        return;
-                    }
-                }
-
-                unsafe {
-                    close_notification();
-                }
-            });
-
-        // Create and show a window notification
-        unsafe {
-            let message = U16CString::from_str(message).unwrap();
-            show_notification(message.as_ptr());
-        }
-    }
-
-    fn show_menu(&self, menu: Vec<MenuItem>) {
-        let mut raw_menu = Vec::new();
-
-        for item in menu.iter() {
-            let text = U16CString::from_str(item.item_name.clone()).unwrap_or_default();
-            let mut str_buff: [u16; 100] = [0; 100];
-            unsafe {
-                std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), text.len());
-            }
-
-            let menu_type = match item.item_type {
-                MenuItemType::Button => 1,
-                MenuItemType::Separator => 2,
-            };
-
-            let raw_item = WindowsMenuItem {
-                item_id: item.item_id,
-                item_type: menu_type,
-                item_name: str_buff,
-            };
-
-            raw_menu.push(raw_item);
-        }
-
-        unsafe {
-            show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32);
-        }
-    }
-
-    fn cleanup(&self) {
-        unsafe {
-            cleanup_ui();
-        }
-    }
-}
-
-impl WindowsUIManager {
-    pub fn new() -> WindowsUIManager {
-        let id = Arc::new(Mutex::new(0));
-
-        let manager = WindowsUIManager { id };
-
-        manager
-    }
-}
diff --git a/src/utils.rs b/src/utils.rs
deleted file mode 100644
index bea9fe2e..00000000
--- a/src/utils.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * This file is part of espanso.
- *
- * Copyright (C) 2019 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::error::Error;
-use std::fs::create_dir;
-use std::path::Path;
-use std::process::Command;
-
-pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>> {
-    for entry in std::fs::read_dir(source_dir)? {
-        let entry = entry?;
-        let entry = entry.path();
-        if entry.is_dir() {
-            let name = entry.file_name().expect("Error obtaining the filename");
-            let target_dir = dest_dir.join(name);
-            create_dir(&target_dir)?;
-            copy_dir(&entry, &target_dir)?;
-        } else if entry.is_file() {
-            let target_entry =
-                dest_dir.join(entry.file_name().expect("Error obtaining the filename"));
-            std::fs::copy(entry, target_entry)?;
-        }
-    }
-
-    Ok(())
-}
-
-#[cfg(target_os = "windows")]
-pub fn set_command_flags(command: &mut Command) {
-    use std::os::windows::process::CommandExt;
-    // Avoid showing the shell window
-    // See: https://github.com/federico-terzi/espanso/issues/249
-    command.creation_flags(0x08000000);
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn set_command_flags(command: &mut Command) {
-    // NOOP on Linux and macOS
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::fs::create_dir;
-    use tempfile::TempDir;
-
-    #[test]
-    fn test_copy_dir_into() {
-        let source_tmp_dir = TempDir::new().expect("Error creating temp directory");
-        let dest_tmp_dir = TempDir::new().expect("Error creating temp directory");
-
-        let source_dir = source_tmp_dir.path().join("source");
-        create_dir(&source_dir).unwrap();
-        std::fs::write(source_dir.join("file1.txt"), "file1").unwrap();
-        std::fs::write(source_dir.join("file2.txt"), "file2").unwrap();
-
-        let target_dir = dest_tmp_dir.path().join("source");
-        create_dir(&target_dir).unwrap();
-
-        copy_dir(&source_dir, &target_dir).unwrap();
-
-        assert!(dest_tmp_dir.path().join("source").exists());
-        assert!(dest_tmp_dir.path().join("source/file1.txt").exists());
-        assert!(dest_tmp_dir.path().join("source/file2.txt").exists());
-    }
-
-    #[test]
-    fn test_copy_dir_into_recursive() {
-        let source_tmp_dir = TempDir::new().expect("Error creating temp directory");
-        let dest_tmp_dir = TempDir::new().expect("Error creating temp directory");
-
-        let source_dir = source_tmp_dir.path().join("source");
-        create_dir(&source_dir).unwrap();
-        std::fs::write(source_dir.join("file1.txt"), "file1").unwrap();
-        std::fs::write(source_dir.join("file2.txt"), "file2").unwrap();
-        let nested_dir = source_dir.join("nested");
-        create_dir(&nested_dir).unwrap();
-        std::fs::write(nested_dir.join("nestedfile.txt"), "nestedfile1").unwrap();
-
-        let target_dir = dest_tmp_dir.path().join("source");
-        create_dir(&target_dir).unwrap();
-
-        copy_dir(&source_dir, &target_dir).unwrap();
-
-        assert!(dest_tmp_dir.path().join("source").exists());
-        assert!(dest_tmp_dir.path().join("source/file1.txt").exists());
-        assert!(dest_tmp_dir.path().join("source/file2.txt").exists());
-
-        assert!(dest_tmp_dir.path().join("source/nested").exists());
-        assert!(dest_tmp_dir
-            .path()
-            .join("source/nested/nestedfile.txt")
-            .exists());
-    }
-}


Test patch:
diff --git a/ci/test.yml b/ci/test.yml
deleted file mode 100644
index d76dd3d5..00000000
--- a/ci/test.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-parameters:
-  rust_version: stable
-
-steps:
-  - script: |
-      echo Master check
-    displayName: Master branch check
-    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
-
-  - template: install-rust.yml
-
-  - script: |
-      set -e
-      cargo test --release
-    displayName: Cargo tests on Unix
-    condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
-
-  - script: |
-      cargo test --release
-    displayName: Cargo tests on Windows
-    condition: eq(variables['Agent.OS'], 'Windows_NT')
\ No newline at end of file
diff --git a/src/res/test/config_with_bad_yaml.yml b/espanso-config/src/legacy/res/test/config_with_bad_yaml.yml
similarity index 100%
rename from src/res/test/config_with_bad_yaml.yml
rename to espanso-config/src/legacy/res/test/config_with_bad_yaml.yml
diff --git a/src/res/config.yml b/espanso-config/src/legacy/res/test/default.yml
similarity index 100%
rename from src/res/config.yml
rename to espanso-config/src/legacy/res/test/default.yml
diff --git a/src/res/test/working_config.yml b/espanso-config/src/legacy/res/test/working_config.yml
similarity index 100%
rename from src/res/test/working_config.yml
rename to espanso-config/src/legacy/res/test/working_config.yml
diff --git a/espanso-migrate/test/README.md b/espanso-migrate/test/README.md
new file mode 100644
index 00000000..5ccf659f
--- /dev/null
+++ b/espanso-migrate/test/README.md
@@ -0,0 +1,1 @@
+This directory contains the files used to test the migration strategy.
\ No newline at end of file
diff --git a/espanso-migrate/test/all_params/expected/config/default.yml b/espanso-migrate/test/all_params/expected/config/default.yml
new file mode 100644
index 00000000..110e976e
--- /dev/null
+++ b/espanso-migrate/test/all_params/expected/config/default.yml
@@ -0,0 +1,26 @@
+toggle_interval: 500
+toggle_key: "RIGHT_CTRL"
+
+preserve_clipboard: false
+
+backspace_limit: 5
+
+inject_delay: 10
+restore_clipboard_delay: 400
+key_delay: 300
+
+secure_input_notification: false
+
+show_notifications: false
+show_icon: false
+
+auto_restart: false
+undo_backspace: false
+
+disable_x11_fast_inject: true
+
+paste_shortcut: "CTRL+ALT+V"
+
+backend: Clipboard
+
+word_separators: ['.']
\ No newline at end of file
diff --git a/espanso-migrate/test/all_params/expected/match/base.yml b/espanso-migrate/test/all_params/expected/match/base.yml
new file mode 100644
index 00000000..f89c90c2
--- /dev/null
+++ b/espanso-migrate/test/all_params/expected/match/base.yml
@@ -0,0 +1,12 @@
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/all_params/legacy/default.yml b/espanso-migrate/test/all_params/legacy/default.yml
new file mode 100644
index 00000000..a788446b
--- /dev/null
+++ b/espanso-migrate/test/all_params/legacy/default.yml
@@ -0,0 +1,46 @@
+config_caching_interval: 1200
+
+toggle_interval: 500
+toggle_key: "RIGHT_CTRL"
+
+preserve_clipboard: false
+
+backspace_limit: 5
+
+inject_delay: 10
+restore_clipboard_delay: 400
+backspace_delay: 300
+
+secure_input_watcher_enabled: false
+secure_input_notification: false
+secure_input_watcher_interval: 3000
+
+show_notifications: false
+show_icon: false
+
+auto_restart: false
+undo_backspace: false
+
+fast_inject: false
+
+post_inject_delay: 200
+
+paste_shortcut: CtrlAltV
+
+backend: Clipboard
+
+use_system_agent: false
+word_separators: ['.']
+
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/config/default.yml b/espanso-migrate/test/base/expected/config/default.yml
new file mode 100644
index 00000000..3b6eb106
--- /dev/null
+++ b/espanso-migrate/test/base/expected/config/default.yml
@@ -0,0 +1,1 @@
+backend: Clipboard
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/config/disabled.yml b/espanso-migrate/test/base/expected/config/disabled.yml
new file mode 100644
index 00000000..e6d05691
--- /dev/null
+++ b/espanso-migrate/test/base/expected/config/disabled.yml
@@ -0,0 +1,3 @@
+filter_title: "Disabled program"
+
+enable: false
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/config/extended.yml b/espanso-migrate/test/base/expected/config/extended.yml
new file mode 100644
index 00000000..e1a3e61c
--- /dev/null
+++ b/espanso-migrate/test/base/expected/config/extended.yml
@@ -0,0 +1,4 @@
+filter_exec: "Executable"
+
+extra_includes:
+  - "../match/_extended.yml"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/config/standalone.yml b/espanso-migrate/test/base/expected/config/standalone.yml
new file mode 100644
index 00000000..6697559b
--- /dev/null
+++ b/espanso-migrate/test/base/expected/config/standalone.yml
@@ -0,0 +1,4 @@
+filter_class: "Standalone"
+
+includes:
+  - "../match/_standalone.yml"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/_extended.yml b/espanso-migrate/test/base/expected/match/_extended.yml
new file mode 100644
index 00000000..5e95626f
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/_extended.yml
@@ -0,0 +1,9 @@
+global_vars:
+  - name: "test"
+    type: "date"
+    params:
+      format: "%H"
+
+matches:
+  - trigger: "extend"
+    replace: "match"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/_standalone.yml b/espanso-migrate/test/base/expected/match/_standalone.yml
new file mode 100644
index 00000000..c2049305
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/_standalone.yml
@@ -0,0 +1,3 @@
+matches:
+  - trigger: "hello"
+    replace: "world"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/base.yml b/espanso-migrate/test/base/expected/match/base.yml
new file mode 100644
index 00000000..f89c90c2
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/base.yml
@@ -0,0 +1,12 @@
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/packages/foo/custom.yml b/espanso-migrate/test/base/expected/match/packages/foo/custom.yml
new file mode 100644
index 00000000..1f2697ae
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/packages/foo/custom.yml
@@ -0,0 +1,3 @@
+matches:
+  - trigger: "custom"
+    replace: "match"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/packages/foo/package.yml b/espanso-migrate/test/base/expected/match/packages/foo/package.yml
new file mode 100644
index 00000000..dc0b3c52
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/packages/foo/package.yml
@@ -0,0 +1,3 @@
+matches:
+  - trigger: "package_foo"
+    replace: "package_bar"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/expected/match/split.yml b/espanso-migrate/test/base/expected/match/split.yml
new file mode 100644
index 00000000..c5d3bd30
--- /dev/null
+++ b/espanso-migrate/test/base/expected/match/split.yml
@@ -0,0 +1,4 @@
+matches:
+  - trigger: "jon"
+    replace: "snow"
+
diff --git a/espanso-migrate/test/base/legacy/default.yml b/espanso-migrate/test/base/legacy/default.yml
new file mode 100644
index 00000000..6afad09b
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/default.yml
@@ -0,0 +1,14 @@
+backend: Clipboard
+
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/packages/foo/README.md b/espanso-migrate/test/base/legacy/packages/foo/README.md
new file mode 100644
index 00000000..b82846c6
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/packages/foo/README.md
@@ -0,0 +1,9 @@
+---
+package_name: "foo"
+package_title: "Foo"
+package_desc: "Foo package"
+package_version: "0.1.0"
+package_author: "Federico Terzi"
+package_repo: "https://github.com/federico-terzi/espanso-hub-core"
+---
+Test package
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/packages/foo/custom.yml b/espanso-migrate/test/base/legacy/packages/foo/custom.yml
new file mode 100644
index 00000000..13c3b1da
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/packages/foo/custom.yml
@@ -0,0 +1,6 @@
+name: foo
+parent: default
+
+matches:
+  - trigger: "custom"
+    replace: "match"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/packages/foo/package.yml b/espanso-migrate/test/base/legacy/packages/foo/package.yml
new file mode 100644
index 00000000..d2161350
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/packages/foo/package.yml
@@ -0,0 +1,6 @@
+name: foo
+parent: default
+
+matches:
+  - trigger: "package_foo"
+    replace: "package_bar"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/user/disabled.yml b/espanso-migrate/test/base/legacy/user/disabled.yml
new file mode 100644
index 00000000..030e7834
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/user/disabled.yml
@@ -0,0 +1,3 @@
+filter_title: "Disabled program"
+
+enable_active: false
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/user/extended.yml b/espanso-migrate/test/base/legacy/user/extended.yml
new file mode 100644
index 00000000..08e2fd90
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/user/extended.yml
@@ -0,0 +1,11 @@
+filter_exec: "Executable"
+
+global_vars:
+  - name: "test"
+    type: "date"
+    params:
+      format: "%H"
+
+matches:
+  - trigger: "extend"
+    replace: "match"
\ No newline at end of file
diff --git a/espanso-migrate/test/base/legacy/user/split.yml b/espanso-migrate/test/base/legacy/user/split.yml
new file mode 100644
index 00000000..5e1eab46
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/user/split.yml
@@ -0,0 +1,5 @@
+parent: default
+
+matches:
+  - trigger: "jon"
+    replace: "snow"
diff --git a/espanso-migrate/test/base/legacy/user/standalone.yml b/espanso-migrate/test/base/legacy/user/standalone.yml
new file mode 100644
index 00000000..bb41d986
--- /dev/null
+++ b/espanso-migrate/test/base/legacy/user/standalone.yml
@@ -0,0 +1,7 @@
+filter_class: "Standalone"
+
+exclude_default_entries: true
+
+matches:
+  - trigger: "hello"
+    replace: "world"
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/expected/config/default.yml b/espanso-migrate/test/other_dirs/expected/config/default.yml
new file mode 100644
index 00000000..3b6eb106
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/expected/config/default.yml
@@ -0,0 +1,1 @@
+backend: Clipboard
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/expected/config/disabled.yml b/espanso-migrate/test/other_dirs/expected/config/disabled.yml
new file mode 100644
index 00000000..e6d05691
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/expected/config/disabled.yml
@@ -0,0 +1,3 @@
+filter_title: "Disabled program"
+
+enable: false
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/expected/match/base.yml b/espanso-migrate/test/other_dirs/expected/match/base.yml
new file mode 100644
index 00000000..f89c90c2
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/expected/match/base.yml
@@ -0,0 +1,12 @@
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/expected/scripts/test.py b/espanso-migrate/test/other_dirs/expected/scripts/test.py
new file mode 100644
index 00000000..0832ecae
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/expected/scripts/test.py
@@ -0,0 +1,1 @@
+print("this is a test")
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/legacy/default.yml b/espanso-migrate/test/other_dirs/legacy/default.yml
new file mode 100644
index 00000000..6afad09b
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/legacy/default.yml
@@ -0,0 +1,14 @@
+backend: Clipboard
+
+global_vars:
+  - name: "name"
+    type: "dummy"
+    params:
+      echo: "John"
+
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/legacy/scripts/test.py b/espanso-migrate/test/other_dirs/legacy/scripts/test.py
new file mode 100644
index 00000000..0832ecae
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/legacy/scripts/test.py
@@ -0,0 +1,1 @@
+print("this is a test")
\ No newline at end of file
diff --git a/espanso-migrate/test/other_dirs/legacy/user/disabled.yml b/espanso-migrate/test/other_dirs/legacy/user/disabled.yml
new file mode 100644
index 00000000..030e7834
--- /dev/null
+++ b/espanso-migrate/test/other_dirs/legacy/user/disabled.yml
@@ -0,0 +1,3 @@
+filter_title: "Disabled program"
+
+enable_active: false
\ No newline at end of file
diff --git a/espanso-migrate/test/simple/expected/config/default.yml b/espanso-migrate/test/simple/expected/config/default.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/espanso-migrate/test/simple/expected/match/base.yml b/espanso-migrate/test/simple/expected/match/base.yml
new file mode 100644
index 00000000..b5fb8291
--- /dev/null
+++ b/espanso-migrate/test/simple/expected/match/base.yml
@@ -0,0 +1,6 @@
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/espanso-migrate/test/simple/legacy/default.yml b/espanso-migrate/test/simple/legacy/default.yml
new file mode 100644
index 00000000..b5fb8291
--- /dev/null
+++ b/espanso-migrate/test/simple/legacy/default.yml
@@ -0,0 +1,6 @@
+matches:
+  - name: ":hi"
+    trigger: "Hello"
+  
+  - name: ":greet"
+    trigger: "Hi {{name}}"
\ No newline at end of file
diff --git a/scripts/test_binary.rs b/scripts/test_binary.rs
new file mode 100644
index 00000000..876bc5e7
--- /dev/null
+++ b/scripts/test_binary.rs
@@ -0,0 +1,78 @@
+//! ```cargo
+//! [dependencies]
+//! envmnt = "*"
+//! ```
+
+use std::process::Command;
+
+#[derive(Debug, PartialEq)]
+enum Profile {
+  Debug,
+  Release,
+}
+
+fn main() {
+  let profile = if envmnt::get_or_panic("RELEASE") == "true" {
+    Profile::Release
+  } else {
+    Profile::Debug
+  };
+
+  println!("Using profile: {:?}", profile);
+
+  let wayland = envmnt::get_or("NO_X11", "false") == "true";
+  if wayland {
+    println!("Using Wayland feature");
+  }
+
+  let mut args = Vec::new();
+
+  args.push("test");
+  args.push("--workspace");
+  args.push("--exclude");
+  args.push("espanso-modulo");
+  args.push("--exclude");
+  args.push("espanso-ipc");
+
+  if profile == Profile::Release {
+    args.push("--release");
+  }
+
+  let override_target_arch = envmnt::get_or("BUILD_ARCH", "current");
+  if override_target_arch != "current" {
+    args.push("--target");
+    args.push(&override_target_arch);
+  }
+
+  let mut features = Vec::new();
+  if wayland {
+    features.push("wayland");
+  }
+
+  let features_flag = features.join(" ");
+
+  args.push("--no-default-features");
+  args.push("--features");
+  args.push(&features_flag);
+
+  println!("Calling with args: {:?}", args);
+
+  let mut cmd = Command::new("cargo");
+  cmd.args(&args);
+
+  // Remove cargo/rust-specific env variables, as otherwise they mess up the
+  // nested cargo build call.
+  let all_vars = envmnt::vars();
+  for (key, _) in all_vars {
+    if key.starts_with("CARGO") || (key.starts_with("RUST") && !key.starts_with("RUSTUP")) {
+      //println!("Removing {}", key);
+      cmd.env_remove(key);
+    }
+  }
+
+  let mut handle = cmd.spawn().expect("cargo test failed");
+  let result = handle.wait().expect("unable to read cargo exit status");
+  if !result.success() {
+    panic!("cargo test failed");
+  }
+}
diff --git a/src/res/test/get_package_index.json b/src/res/test/get_package_index.json
deleted file mode 100644
index 428ed4ec..00000000
--- a/src/res/test/get_package_index.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-    "lastUpdate": 1565437389,
-    "packages": [
-
-        {
-            "name": "basic-emojis",
-            "title": "Basic Emojis",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "A package to include some basic emojis in espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-
-        {
-            "name": "italian-accents",
-            "title": "Italian Accents",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Include Italian accents substitutions to espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        }
-
-
-]}
\ No newline at end of file
diff --git a/src/res/test/index_without_update.json b/src/res/test/index_without_update.json
deleted file mode 100644
index 7d83c465..00000000
--- a/src/res/test/index_without_update.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-    "lastUpdate": XXXX,
-    "packages": [
-
-        {
-            "name": "basic-emojis",
-            "title": "Basic Emojis",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "A package to include some basic emojis in espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-
-        {
-            "name": "italian-accents",
-            "title": "Italian Accents",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Include Italian accents substitutions to espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        }
-
-
-]}
\ No newline at end of file
diff --git a/src/res/test/install_package_index.json b/src/res/test/install_package_index.json
deleted file mode 100644
index 63222f7f..00000000
--- a/src/res/test/install_package_index.json
+++ /dev/null
@@ -1,70 +0,0 @@
-{
-    "lastUpdate": 1565437389,
-    "packages": [
-
-        {
-            "name": "dummy-package",
-            "title": "Dummy Package",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Dummy package",
-            "author": "Federico Terzi",
-            "is_core": true
-        },
-
-        {
-            "name": "dummy-package2",
-            "title": "Dummy Package",
-            "version": "9.9.9",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Dummy package",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-        {
-            "name": "dummy-package3",
-            "title": "Dummy Package",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Dummy package",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-        {
-            "name": "dummy-package4",
-            "title": "Dummy Package",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Dummy package",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-
-        {
-            "name": "italian-accents",
-            "title": "Italian Accents",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Include Italian accents substitutions to espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-        {
-            "name": "not-existing",
-            "title": "Not Existing",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Package that does not exist in the repo",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        }
-    ]}
\ No newline at end of file
diff --git a/src/res/test/outdated_index.json b/src/res/test/outdated_index.json
deleted file mode 100644
index 428ed4ec..00000000
--- a/src/res/test/outdated_index.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-    "lastUpdate": 1565437389,
-    "packages": [
-
-        {
-            "name": "basic-emojis",
-            "title": "Basic Emojis",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "A package to include some basic emojis in espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        },
-
-
-        {
-            "name": "italian-accents",
-            "title": "Italian Accents",
-            "version": "0.1.0",
-            "repo": "https://github.com/federico-terzi/espanso-hub-core",
-            "desc": "Include Italian accents substitutions to espanso.",
-            "author": "Federico Terzi",
-            "is_core": true
-
-        }
-
-
-]}
\ No newline at end of file