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