├── .changes ├── config.json └── readme.md ├── .github └── workflows │ ├── audit.yml │ ├── clippy-fmt.yml │ ├── covector-status.yml │ ├── covector-version-or-publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── LICENSE.spdx ├── README.md ├── examples ├── egui.rs ├── iced.rs ├── tao.rs └── winit.rs ├── renovate.json └── src ├── error.rs ├── hotkey.rs ├── lib.rs └── platform_impl ├── macos ├── ffi.rs └── mod.rs ├── mod.rs ├── no-op.rs ├── windows └── mod.rs └── x11 └── mod.rs /.changes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitSiteUrl": "https://www.github.com/tauri-apps/global-hotkey/", 3 | "timeout": 3600000, 4 | "pkgManagers": { 5 | "rust": { 6 | "version": true, 7 | "getPublishedVersion": "cargo search ${ pkg.pkg } --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -", 8 | "publish": [ 9 | { 10 | "command": "cargo package --no-verify", 11 | "dryRunCommand": true 12 | }, 13 | { 14 | "command": "echo '
\n

Cargo Publish

\n\n```'", 15 | "dryRunCommand": true, 16 | "pipe": true 17 | }, 18 | { 19 | "command": "cargo publish", 20 | "dryRunCommand": "cargo publish --dry-run", 21 | "pipe": true 22 | }, 23 | { 24 | "command": "echo '```\n\n
\n'", 25 | "dryRunCommand": true, 26 | "pipe": true 27 | } 28 | ], 29 | "postpublish": [ 30 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f", 31 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f", 32 | "git push --tags -f" 33 | ] 34 | } 35 | }, 36 | "packages": { 37 | "global-hotkey": { 38 | "path": ".", 39 | "manager": "rust", 40 | "assets": [ 41 | { 42 | "path": "${ pkg.path }/target/package/global-hotkey-${ pkgFile.version }.crate", 43 | "name": "${ pkg.pkg }-${ pkgFile.version }.crate" 44 | } 45 | ] 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /.changes/readme.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ##### via https://github.com/jbolda/covector 4 | 5 | As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend it represents the overall change for our sanity. 6 | 7 | When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. 8 | 9 | Use the following format: 10 | 11 | ```md 12 | --- 13 | "global-hotkey": patch 14 | --- 15 | 16 | Change summary goes here 17 | ``` 18 | 19 | Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. 20 | 21 | Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). 22 | 23 | Given a version number MAJOR.MINOR.PATCH, increment the: 24 | 25 | - MAJOR version when you make incompatible API changes, 26 | - MINOR version when you add functionality in a backwards compatible manner, and 27 | - PATCH version when you make backwards compatible bug fixes. 28 | 29 | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). 30 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: audit 6 | 7 | on: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | push: 12 | branches: 13 | - dev 14 | paths: 15 | - 'Cargo.lock' 16 | - 'Cargo.toml' 17 | pull_request: 18 | paths: 19 | - 'Cargo.lock' 20 | - 'Cargo.toml' 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | audit: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: rustsec/audit-check@v2 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/clippy-fmt.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: clippy & fmt 6 | 7 | on: 8 | push: 9 | branches: 10 | - dev 11 | pull_request: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | clippy: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | platform: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | runs-on: ${{ matrix.platform }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: install system deps 29 | if: matrix.platform == 'ubuntu-latest' 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y libgtk-3-dev libxdo-dev 33 | 34 | - uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy 37 | 38 | - run: cargo clippy --all-targets --all-features -- -D warnings 39 | 40 | fmt: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: rustfmt 47 | 48 | - run: cargo fmt --all -- --check 49 | -------------------------------------------------------------------------------- /.github/workflows/covector-status.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: covector status 6 | on: [pull_request] 7 | 8 | jobs: 9 | covector: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: covector status 15 | uses: jbolda/covector/packages/action@covector-v0 16 | id: covector 17 | with: 18 | command: "status" 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | comment: true -------------------------------------------------------------------------------- /.github/workflows/covector-version-or-publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: covector version or publish 6 | 7 | on: 8 | push: 9 | branches: 10 | - dev 11 | 12 | jobs: 13 | version-or-publish: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 65 16 | outputs: 17 | change: ${{ steps.covector.outputs.change }} 18 | commandRan: ${{ steps.covector.outputs.commandRan }} 19 | successfulPublish: ${{ steps.covector.outputs.successfulPublish }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: cargo login 27 | run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }} 28 | 29 | - name: git config 30 | run: | 31 | git config --global user.name "${{ github.event.pusher.name }}" 32 | git config --global user.email "${{ github.event.pusher.email }}" 33 | 34 | - name: covector version or publish (publish when no change files present) 35 | uses: jbolda/covector/packages/action@covector-v0 36 | id: covector 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }} 39 | with: 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | command: 'version-or-publish' 42 | createRelease: true 43 | recognizeContributors: true 44 | 45 | - name: Sync Cargo.lock 46 | if: steps.covector.outputs.commandRan == 'version' 47 | run: cargo tree --depth 0 48 | 49 | - name: Create Pull Request With Versions Bumped 50 | if: steps.covector.outputs.commandRan == 'version' 51 | uses: tauri-apps/create-pull-request@v3 52 | with: 53 | token: ${{ secrets.GITHUB_TOKEN }} 54 | title: Apply Version Updates From Current Changes 55 | commit-message: 'apply version updates' 56 | labels: 'version updates' 57 | branch: 'release' 58 | body: ${{ steps.covector.outputs.change }} 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: test 6 | 7 | on: 8 | push: 9 | branches: 10 | - dev 11 | pull_request: 12 | 13 | env: 14 | RUST_BACKTRACE: 1 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | test: 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | platform: ["windows-latest", "macos-latest", "ubuntu-latest"] 26 | 27 | runs-on: ${{ matrix.platform }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: install system deps 33 | if: matrix.platform == 'ubuntu-latest' 34 | run: | 35 | sudo apt-get update 36 | sudo apt-get install -y libgtk-3-dev libxdo-dev 37 | 38 | - uses: dtolnay/rust-toolchain@1.71 39 | - run: cargo build 40 | 41 | - uses: dtolnay/rust-toolchain@stable 42 | - run: cargo test --all-features 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[0.7.0] 4 | 5 | - [`77dbe4e`](https://www.github.com/tauri-apps/global-hotkey/commit/77dbe4ebe5911f9ee41f3264ecb11295d7e6abe7) ([#150](https://www.github.com/tauri-apps/global-hotkey/pull/150) by [@Exidex](https://www.github.com/tauri-apps/global-hotkey/../../Exidex)) Use `x11rb` crate instead of `x11-dl` for linux (x11) backend. 6 | 7 | ## \[0.6.4] 8 | 9 | - [`a25c485`](https://www.github.com/tauri-apps/global-hotkey/commit/a25c485b6fce488799510c7f70563db3ebcdb72f) ([#120](https://www.github.com/tauri-apps/global-hotkey/pull/120) by [@FabianLars](https://www.github.com/tauri-apps/global-hotkey/../../FabianLars)) Update `objc2` to `0.6`. This raises the MSRV to 1.71 which is now also set in `rust-version`. 10 | 11 | ## \[0.6.3] 12 | 13 | - [`ddf5515`](https://www.github.com/tauri-apps/global-hotkey/commit/ddf5515712f85e887e715bda7da40becc9159ac9) ([#112](https://www.github.com/tauri-apps/global-hotkey/pull/112) by [@amrbashir](https://www.github.com/tauri-apps/global-hotkey/../../amrbashir)) Support using `Pause` or `PauseBreak` key on Windows and Linux. 14 | 15 | ## \[0.6.2] 16 | 17 | - [`2c7397b`](https://www.github.com/tauri-apps/global-hotkey/commit/2c7397b27ccb2efd4589bb364e611a80635413c8) ([#106](https://www.github.com/tauri-apps/global-hotkey/pull/106) by [@FabianLars](https://www.github.com/tauri-apps/global-hotkey/../../FabianLars)) Fixed an issue causing compilation to fail for 32-bit targets. 18 | 19 | ## \[0.6.1] 20 | 21 | - [`7d15d09`](https://www.github.com/tauri-apps/global-hotkey/commit/7d15d09e518130bf0a1b44e3512cb6f5ed361164) ([#99](https://www.github.com/tauri-apps/global-hotkey/pull/99) by [@madsmtm](https://www.github.com/tauri-apps/global-hotkey/../../madsmtm)) Use `objc2` internally, leading to slightly better memory- and type-safety. 22 | 23 | ## \[0.6.0] 24 | 25 | - [`8b13a61`](https://www.github.com/tauri-apps/global-hotkey/commit/8b13a6159d776a6a282ad7ca5c4b896cc91e325a) Removed `Sync` and `Send` implementation for `GlobalHotKeyManager` 26 | - [`8b13a61`](https://www.github.com/tauri-apps/global-hotkey/commit/8b13a6159d776a6a282ad7ca5c4b896cc91e325a) Update `windows-sys` crate to `0.59` 27 | 28 | ## \[0.5.5] 29 | 30 | - [`c750004`](https://www.github.com/tauri-apps/global-hotkey/commit/c7500047fb62154cf861878efb334c61bd98988a) ([#92](https://www.github.com/tauri-apps/global-hotkey/pull/92) by [@IAmJSD](https://www.github.com/tauri-apps/global-hotkey/../../IAmJSD)) Fix a panic when parsing `HotKey` from a string and return an error instead, if the hotkey string consists of only modifiers and doesn't contain a key. 31 | 32 | ## \[0.5.4] 33 | 34 | - [`e9d263c`](https://www.github.com/tauri-apps/global-hotkey/commit/e9d263c2d9b9535af8d64c7b8950308d16b57b94) Fix parsing of `MEDIATRACKPREV` and `MEDIATRACKPREVIOUS` keys. 35 | 36 | ## \[0.5.3] 37 | 38 | - [`a468ede`](https://www.github.com/tauri-apps/global-hotkey/commit/a468ede66aa2102f146bebd71ad618eff550997a)([#75](https://www.github.com/tauri-apps/global-hotkey/pull/75)) Add `serde` feature flag and implement `Deserialize` and `Serialize` for `GlobalHotKeyEvent`, `HotKeyState` and `HotKey` types. 39 | - [`a468ede`](https://www.github.com/tauri-apps/global-hotkey/commit/a468ede66aa2102f146bebd71ad618eff550997a)([#75](https://www.github.com/tauri-apps/global-hotkey/pull/75)) Add `HotKey::into_string` method and implement `Display` for `HotKey`. 40 | 41 | ## \[0.5.2] 42 | 43 | - [`c530be0`](https://www.github.com/tauri-apps/global-hotkey/commit/c530be0dbf939d2dd8d05eacc2071f493769a834)([#71](https://www.github.com/tauri-apps/global-hotkey/pull/71)) Support registering media play/pause/stop/next/prev keys. 44 | - [`24f41b0`](https://www.github.com/tauri-apps/global-hotkey/commit/24f41b0fd9f54e822e6397bc95d9e717c67aab72)([#73](https://www.github.com/tauri-apps/global-hotkey/pull/73)) Always service all pending events to avoid a queue of events from building up. 45 | 46 | ## \[0.5.1] 47 | 48 | - [`89199d9`](https://www.github.com/tauri-apps/global-hotkey/commit/89199d930db3a71f1e19a29d6c1d6ff2e8cffb11)([#64](https://www.github.com/tauri-apps/global-hotkey/pull/64)) Add no-op implementations for unsupported targets. 49 | 50 | ## \[0.5.0] 51 | 52 | - [`7d99bd7`](https://www.github.com/tauri-apps/global-hotkey/commit/7d99bd78a383e11ae6bb8fce0525afcc9e427c8f)([#61](https://www.github.com/tauri-apps/global-hotkey/pull/61)) Refactored the errors when parsing accelerator from string: 53 | 54 | - Added `HotKeyParseError` error enum. 55 | - Removed `Error::UnrecognizedHotKeyCode` enum variant 56 | - Removed `Error::EmptyHotKeyToken` enum variant 57 | - Removed `Error::UnexpectedHotKeyFormat` enum variant 58 | - Changed `Error::HotKeyParseError` inner value from `String` to the newly added `HotKeyParseError` enum. 59 | - [`7d99bd7`](https://www.github.com/tauri-apps/global-hotkey/commit/7d99bd78a383e11ae6bb8fce0525afcc9e427c8f)([#61](https://www.github.com/tauri-apps/global-hotkey/pull/61)) Avoid panicing when parsing an invalid `HotKey` from a string such as `SHIFT+SHIFT` and return an error instead. 60 | 61 | ## \[0.4.2] 62 | 63 | - [`b538534`](https://www.github.com/tauri-apps/global-hotkey/commit/b538534f9181ccd38e76d93368378fc6ed3a3a08) Changed window class name used interally so it wouldn't conflict with `tray-icon` crate implementation. 64 | 65 | ## \[0.4.1] 66 | 67 | - [`1f9be3e`](https://www.github.com/tauri-apps/global-hotkey/commit/1f9be3e0631817a9c96a4d98289158286cb689e8)([#47](https://www.github.com/tauri-apps/global-hotkey/pull/47)) Add support for `Code::Backquote` on Linux. 68 | - [`1f9be3e`](https://www.github.com/tauri-apps/global-hotkey/commit/1f9be3e0631817a9c96a4d98289158286cb689e8)([#47](https://www.github.com/tauri-apps/global-hotkey/pull/47)) On Linux, fix hotkey `press/release` events order and sometimes missing `release` event when the modifiers have been already released before the key itself has been released. 69 | - [`1f9be3e`](https://www.github.com/tauri-apps/global-hotkey/commit/1f9be3e0631817a9c96a4d98289158286cb689e8)([#47](https://www.github.com/tauri-apps/global-hotkey/pull/47)) On Linux, improve the performance of `GlobalHotKeyManager::register_all` and `GlobalHotKeyManager::unregister_all` to 2711x faster. 70 | 71 | ## \[0.4.0] 72 | 73 | - [`53961a1`](https://www.github.com/tauri-apps/global-hotkey/commit/53961a1ade623bb97ce96db71fbe1193ffc9d6a7)([#35](https://www.github.com/tauri-apps/global-hotkey/pull/35)) Support Pressed and Released stats of the hotkey, you can check the newly added `state` field or using the `state()` method on the `GlobalHotKeyEvent`. 74 | 75 | ## \[0.3.0] 76 | 77 | - [`fa47029`](https://www.github.com/tauri-apps/global-hotkey/commit/fa47029435ed953b07f5809d9e521bcd2c24bf54) Update `keyboard-types` to `0.7` 78 | 79 | ## \[0.2.4] 80 | 81 | - [`b0975f9`](https://www.github.com/tauri-apps/global-hotkey/commit/b0975f9983aa023df3cd72bbd8d3158165e9f6eb) Export `CMD_OR_CTRL` const. 82 | - [`dc9e619`](https://www.github.com/tauri-apps/global-hotkey/commit/dc9e6197362164ef6b8aae90df41a6a2b459f5fb) Add `GlobalHotKeyEvent::id` method. 83 | - [`b960609`](https://www.github.com/tauri-apps/global-hotkey/commit/b96060952daf8959939f07c968b8bd58e33f4abd) Impl `TryFrom<&str>` and `TryFrom` for `HotKey`. 84 | 85 | ## \[0.2.3] 86 | 87 | - [`589ecd9`](https://www.github.com/tauri-apps/global-hotkey/commit/589ecd9afd79aab93b25b357b4c70afdf69f9f6d)([#25](https://www.github.com/tauri-apps/global-hotkey/pull/25)) Fix `GlobalHotKeyManager::unregister_all` actually registering the hotkeys instead of unregistering. 88 | 89 | ## \[0.2.2] 90 | 91 | - [`bbd3ffb`](https://www.github.com/tauri-apps/global-hotkey/commit/bbd3ffbea2a76eaae7cd344a019a942456f94a26)([#23](https://www.github.com/tauri-apps/global-hotkey/pull/23)) Generate a hash-based id for hotkeys. Previously each hotkey had a unique id which is not necessary given that only one hotkey with the same combination can be used at a time. 92 | 93 | ## \[0.2.1] 94 | 95 | - [`b503530`](https://www.github.com/tauri-apps/global-hotkey/commit/b503530eb49a7fe8da3e49080e3f72f82a70b7a2)([#20](https://www.github.com/tauri-apps/global-hotkey/pull/20)) Make `GlobalHotKeyManager` Send + Sync on macOS. 96 | 97 | ## \[0.2.0] 98 | 99 | - Support more variants for `HotKey::from_str` and support case-insensitive htokey. 100 | - [25cbda5](https://www.github.com/tauri-apps/global-hotkey/commit/25cbda58c503b8230af00c6192e87d5ce1fc2742) feat: add more variants and case-insensitive hotkey parsing ([#19](https://www.github.com/tauri-apps/global-hotkey/pull/19)) on 2023-04-19 101 | 102 | ## \[0.1.2] 103 | 104 | - On Windows, fix registering htokeys failing all the time. 105 | - [65d1f6d](https://www.github.com/tauri-apps/global-hotkey/commit/65d1f6dffd54bafe46d1ae776639b5dd10e78b96) fix(window): correctly check error result on 2023-02-13 106 | - Fix crash on wayland, and emit a warning instead. 107 | - [4c08d82](https://www.github.com/tauri-apps/global-hotkey/commit/4c08d82fa4a20c82988b49f718688ec29de8a781) fix: emit error on non x11 window systems on 2023-02-13 108 | 109 | ## \[0.1.1] 110 | 111 | - Update docs 112 | - [6409e5d](https://www.github.com/tauri-apps/global-hotkey/commit/6409e5dd351e1cae808c0042f4507e9afad70a05) docs: update docs on 2023-02-08 113 | 114 | ## \[0.1.0] 115 | 116 | - Initial Release. 117 | - [72873f6](https://www.github.com/tauri-apps/global-hotkey/commit/72873f629b47565888d5f2a4264476c9974686b6) chore: add initial release change file on 2023-01-16 118 | - [d0f1d9c](https://www.github.com/tauri-apps/global-hotkey/commit/d0f1d9c58eba60015f658f7a742c200c2d1bd55e) chore: adjust change file on 2023-01-16 119 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "global-hotkey" 3 | version = "0.7.0" 4 | description = "Global hotkeys for Desktop Applications" 5 | edition = "2021" 6 | keywords = ["windowing", "global", "global-hotkey", "hotkey"] 7 | license = "Apache-2.0 OR MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/amrbashir/global-hotkey" 10 | documentation = "https://docs.rs/global-hotkey" 11 | categories = ["gui"] 12 | rust-version = "1.71" 13 | 14 | [features] 15 | serde = ["dep:serde"] 16 | tracing = ["dep:tracing"] 17 | 18 | [dependencies] 19 | crossbeam-channel = "0.5" 20 | keyboard-types = "0.7" 21 | once_cell = "1" 22 | thiserror = "2" 23 | serde = { version = "1", optional = true, features = ["derive"] } 24 | tracing = { version = "0.1", optional = true } 25 | 26 | [target.'cfg(target_os = "macos")'.dependencies] 27 | objc2 = "0.6.0" 28 | objc2-app-kit = { version = "0.3.0", default-features = false, features = [ 29 | "std", 30 | "NSEvent", 31 | ] } 32 | 33 | [target.'cfg(target_os = "windows")'.dependencies.windows-sys] 34 | version = "0.59" 35 | features = [ 36 | "Win32_UI_WindowsAndMessaging", 37 | "Win32_Foundation", 38 | "Win32_System_SystemServices", 39 | "Win32_Graphics_Gdi", 40 | "Win32_UI_Shell", 41 | "Win32_UI_Input_KeyboardAndMouse", 42 | ] 43 | 44 | [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] 45 | x11rb = { version = "0.13.1", features = ["xkb"] } 46 | xkeysym = "0.2.1" 47 | 48 | [dev-dependencies] 49 | winit = "0.30" 50 | tao = "0.30" 51 | eframe = "0.27" 52 | iced = "0.13.1" 53 | async-std = "1.12.0" 54 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.1 2 | DataLicense: CC0-1.0 3 | PackageName: global-hotkey 4 | DataFormat: SPDXRef-1 5 | PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy 6 | PackageHomePage: https://tauri.app 7 | PackageLicenseDeclared: Apache-2.0 8 | PackageLicenseDeclared: MIT 9 | PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy 10 | PackageSummary: Menu Utilities for Desktop Applications. 11 | 12 | PackageComment: The package includes the following libraries; see 13 | Relationship information. 14 | 15 | Created: 2022-12-05T09:00:00Z 16 | PackageDownloadLocation: git://github.com/tauri-apps/global-hotkey 17 | PackageDownloadLocation: git+https://github.com/tauri-apps/global-hotkey.git 18 | PackageDownloadLocation: git+ssh://github.com/tauri-apps/global-hotkey.git 19 | Creator: Person: Daniel Thompson-Yvetot -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | global_hotkey lets you register Global HotKeys for Desktop Applications. 2 | 3 | ## Platforms-supported: 4 | 5 | - Windows 6 | - macOS 7 | - Linux (X11 Only) 8 | 9 | ## Platform-specific notes: 10 | 11 | - On Windows a win32 event loop must be running on the thread. It doesn't need to be the main thread but you have to create the global hotkey manager on the same thread as the event loop. 12 | - On macOS, an event loop must be running on the main thread so you also need to create the global hotkey manager on the main thread. 13 | 14 | ## Example 15 | 16 | ```rs 17 | use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}}; 18 | 19 | // initialize the hotkeys manager 20 | let manager = GlobalHotKeyManager::new().unwrap(); 21 | 22 | // construct the hotkey 23 | let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); 24 | 25 | // register it 26 | manager.register(hotkey); 27 | ``` 28 | 29 | ## Processing global hotkey events 30 | 31 | You can also listen for the menu events using `GlobalHotKeyEvent::receiver` to get events for the hotkey pressed events. 32 | 33 | ```rs 34 | use global_hotkey::GlobalHotKeyEvent; 35 | 36 | if let Ok(event) = GlobalHotKeyEvent::receiver().try_recv() { 37 | println!("{:?}", event); 38 | } 39 | ``` 40 | 41 | ## License 42 | 43 | Apache-2.0/MIT 44 | -------------------------------------------------------------------------------- /examples/egui.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release 2 | 3 | use std::time::Duration; 4 | 5 | use eframe::egui; 6 | use global_hotkey::{hotkey::HotKey, GlobalHotKeyEvent, GlobalHotKeyManager}; 7 | use keyboard_types::{Code, Modifiers}; 8 | 9 | fn main() -> Result<(), eframe::Error> { 10 | let manager = GlobalHotKeyManager::new().unwrap(); 11 | let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); 12 | 13 | manager.register(hotkey).unwrap(); 14 | let receiver = GlobalHotKeyEvent::receiver(); 15 | std::thread::spawn(|| loop { 16 | if let Ok(event) = receiver.try_recv() { 17 | println!("tray event: {event:?}"); 18 | } 19 | std::thread::sleep(Duration::from_millis(100)); 20 | }); 21 | 22 | eframe::run_native( 23 | "My egui App", 24 | eframe::NativeOptions::default(), 25 | Box::new(|_cc| Box::::default()), 26 | ) 27 | } 28 | 29 | struct MyApp { 30 | name: String, 31 | age: u32, 32 | } 33 | 34 | impl Default for MyApp { 35 | fn default() -> Self { 36 | Self { 37 | name: "Arthur".to_owned(), 38 | age: 42, 39 | } 40 | } 41 | } 42 | 43 | impl eframe::App for MyApp { 44 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 45 | egui::CentralPanel::default().show(ctx, |ui| { 46 | ui.heading("My egui Application"); 47 | ui.horizontal(|ui| { 48 | let name_label = ui.label("Your name: "); 49 | ui.text_edit_singleline(&mut self.name) 50 | .labelled_by(name_label.id); 51 | }); 52 | ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); 53 | if ui.button("Click each year").clicked() { 54 | self.age += 1; 55 | } 56 | ui.label(format!("Hello '{}', age {}", self.name, self.age)); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/iced.rs: -------------------------------------------------------------------------------- 1 | use global_hotkey::hotkey::{Code, HotKey, Modifiers}; 2 | use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager}; 3 | 4 | use iced::futures::{SinkExt, Stream}; 5 | use iced::stream::channel; 6 | use iced::widget::{container, row, text}; 7 | use iced::{application, Element, Subscription, Task, Theme}; 8 | 9 | fn main() -> iced::Result { 10 | application("Iced Example!", update, view) 11 | .subscription(subscription) 12 | .theme(|_| Theme::Dark) 13 | .run_with(new) 14 | } 15 | 16 | struct Example { 17 | last_pressed: String, 18 | // store the global manager otherwise it will be dropped and events will not be emitted 19 | _manager: GlobalHotKeyManager, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | enum ProgramCommands { 24 | // message received when the subscription calls back to the main gui thread 25 | Received(String), 26 | } 27 | 28 | fn new() -> (Example, Task) { 29 | let manager = GlobalHotKeyManager::new().unwrap(); 30 | let hotkey_1 = HotKey::new(Some(Modifiers::CONTROL), Code::ArrowRight); 31 | let hotkey_2 = HotKey::new(None, Code::ArrowUp); 32 | 33 | manager.register(hotkey_1).unwrap(); 34 | manager.register(hotkey_2).unwrap(); 35 | 36 | ( 37 | Example { 38 | last_pressed: "".to_string(), 39 | _manager: manager, 40 | }, 41 | Task::none(), 42 | ) 43 | } 44 | 45 | fn update(state: &mut Example, msg: ProgramCommands) -> Task { 46 | match msg { 47 | ProgramCommands::Received(code) => { 48 | // update the text widget 49 | state.last_pressed = code.to_string(); 50 | 51 | Task::none() 52 | } 53 | } 54 | } 55 | 56 | fn view(state: &Example) -> Element<'_, ProgramCommands> { 57 | container(row![ 58 | text("You pressed: "), 59 | text(state.last_pressed.clone()) 60 | ]) 61 | .into() 62 | } 63 | 64 | fn subscription(_state: &Example) -> Subscription { 65 | Subscription::run(hotkey_sub) 66 | } 67 | 68 | fn hotkey_sub() -> impl Stream { 69 | channel(32, |mut sender| async move { 70 | let receiver = GlobalHotKeyEvent::receiver(); 71 | // poll for global hotkey events every 50ms 72 | loop { 73 | if let Ok(event) = receiver.try_recv() { 74 | sender 75 | .send(ProgramCommands::Received(format!("{:?}", event))) 76 | .await 77 | .unwrap(); 78 | } 79 | async_std::task::sleep(std::time::Duration::from_millis(50)).await; 80 | } 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /examples/tao.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use global_hotkey::{ 6 | hotkey::{Code, HotKey, Modifiers}, 7 | GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, 8 | }; 9 | use tao::event_loop::{ControlFlow, EventLoopBuilder}; 10 | 11 | fn main() { 12 | let event_loop = EventLoopBuilder::new().build(); 13 | 14 | let hotkeys_manager = GlobalHotKeyManager::new().unwrap(); 15 | 16 | let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); 17 | let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD); 18 | let hotkey3 = HotKey::new(None, Code::KeyF); 19 | let hotkey4 = { 20 | #[cfg(target_os = "macos")] 21 | { 22 | HotKey::new( 23 | Some(Modifiers::SHIFT | Modifiers::ALT), 24 | Code::MediaPlayPause, 25 | ) 26 | } 27 | #[cfg(not(target_os = "macos"))] 28 | { 29 | HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::MediaPlay) 30 | } 31 | }; 32 | 33 | hotkeys_manager.register(hotkey).unwrap(); 34 | hotkeys_manager.register(hotkey2).unwrap(); 35 | hotkeys_manager.register(hotkey3).unwrap(); 36 | hotkeys_manager.register(hotkey4).unwrap(); 37 | 38 | let global_hotkey_channel = GlobalHotKeyEvent::receiver(); 39 | 40 | event_loop.run(move |_event, _, control_flow| { 41 | *control_flow = ControlFlow::Poll; 42 | 43 | if let Ok(event) = global_hotkey_channel.try_recv() { 44 | println!("{event:?}"); 45 | 46 | if hotkey2.id() == event.id && event.state == HotKeyState::Released { 47 | hotkeys_manager.unregister(hotkey2).unwrap(); 48 | } 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /examples/winit.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use global_hotkey::{ 6 | hotkey::{Code, HotKey, Modifiers}, 7 | GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, 8 | }; 9 | use winit::{ 10 | application::ApplicationHandler, 11 | event::WindowEvent, 12 | event_loop::{ActiveEventLoop, EventLoop}, 13 | window::WindowId, 14 | }; 15 | 16 | fn main() { 17 | let hotkeys_manager = GlobalHotKeyManager::new().unwrap(); 18 | 19 | let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); 20 | let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD); 21 | let hotkey3 = HotKey::new(None, Code::KeyF); 22 | 23 | hotkeys_manager.register(hotkey).unwrap(); 24 | hotkeys_manager.register(hotkey2).unwrap(); 25 | hotkeys_manager.register(hotkey3).unwrap(); 26 | 27 | let event_loop = EventLoop::::with_user_event().build().unwrap(); 28 | let proxy = event_loop.create_proxy(); 29 | 30 | GlobalHotKeyEvent::set_event_handler(Some(move |event| { 31 | let _ = proxy.send_event(AppEvent::HotKey(event)); 32 | })); 33 | 34 | let mut app = App { 35 | hotkeys_manager, 36 | hotkey2, 37 | }; 38 | 39 | event_loop.run_app(&mut app).unwrap() 40 | } 41 | 42 | #[derive(Debug)] 43 | enum AppEvent { 44 | HotKey(GlobalHotKeyEvent), 45 | } 46 | 47 | struct App { 48 | hotkeys_manager: GlobalHotKeyManager, 49 | hotkey2: HotKey, 50 | } 51 | 52 | impl ApplicationHandler for App { 53 | fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} 54 | 55 | fn window_event( 56 | &mut self, 57 | _event_loop: &ActiveEventLoop, 58 | _window_id: WindowId, 59 | _event: WindowEvent, 60 | ) { 61 | } 62 | 63 | fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) { 64 | match event { 65 | AppEvent::HotKey(event) => { 66 | println!("{event:?}"); 67 | 68 | if self.hotkey2.id() == event.id && event.state == HotKeyState::Released { 69 | self.hotkeys_manager.unregister(self.hotkey2).unwrap(); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", ":disableDependencyDashboard"] 4 | } -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use thiserror::Error; 6 | 7 | use crate::hotkey::HotKey; 8 | 9 | /// Errors returned by tray-icon. 10 | #[non_exhaustive] 11 | #[derive(Error, Debug)] 12 | pub enum Error { 13 | #[error(transparent)] 14 | OsError(#[from] std::io::Error), 15 | #[error("{0}")] 16 | HotKeyParseError(String), 17 | #[error("Couldn't recognize \"{0}\" as a valid HotKey Code, if you feel like it should be, please report this to https://github.com/tauri-apps/global-hotkey")] 18 | UnrecognizedHotKeyCode(String), 19 | #[error("Unexpected empty token while parsing hotkey: \"{0}\"")] 20 | EmptyHotKeyToken(String), 21 | #[error("Unexpected hotkey string format: \"{0}\", a hotkey should have the modifiers first and only contain one main key")] 22 | UnexpectedHotKeyFormat(String), 23 | #[error("Unable to register hotkey: {0}")] 24 | FailedToRegister(String), 25 | #[error("Failed to unregister hotkey: {0:?}")] 26 | FailedToUnRegister(HotKey), 27 | #[error("HotKey already registered: {0:?}")] 28 | AlreadyRegistered(HotKey), 29 | #[error("Failed to watch media key event")] 30 | FailedToWatchMediaKeyEvent, 31 | } 32 | 33 | /// Convenient type alias of Result type for tray-icon. 34 | pub type Result = std::result::Result; 35 | -------------------------------------------------------------------------------- /src/hotkey.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | //! HotKeys describe keyboard global shortcuts. 6 | //! 7 | //! [`HotKey`s](crate::hotkey::HotKey) are used to define a keyboard shortcut consisting 8 | //! of an optional combination of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and 9 | //! one key ([`Code`](crate::hotkey::Code)). 10 | //! 11 | //! # Examples 12 | //! They can be created directly 13 | //! ```no_run 14 | //! # use global_hotkey::hotkey::{HotKey, Modifiers, Code}; 15 | //! let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyQ); 16 | //! let hotkey_without_mods = HotKey::new(None, Code::KeyQ); 17 | //! ``` 18 | //! or from `&str`, note that all modifiers 19 | //! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal, 20 | //! whereas `shift+q+alt` is not. 21 | //! ```no_run 22 | //! # use global_hotkey::hotkey::{HotKey}; 23 | //! let hotkey: HotKey = "shift+alt+KeyQ".parse().unwrap(); 24 | //! # // This assert exists to ensure a test breaks once the 25 | //! # // statement above about ordering is no longer valid. 26 | //! # assert!("shift+KeyQ+alt".parse::().is_err()); 27 | //! ``` 28 | //! 29 | 30 | pub use keyboard_types::{Code, Modifiers}; 31 | use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr}; 32 | 33 | #[cfg(target_os = "macos")] 34 | pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER; 35 | #[cfg(not(target_os = "macos"))] 36 | pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL; 37 | 38 | #[derive(thiserror::Error, Debug)] 39 | pub enum HotKeyParseError { 40 | #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")] 41 | UnsupportedKey(String), 42 | #[error("Found empty token while parsing hotkey: {0}")] 43 | EmptyToken(String), 44 | #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")] 45 | InvalidFormat(String), 46 | } 47 | 48 | /// A keyboard shortcut that consists of an optional combination 49 | /// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and 50 | /// one key ([`Code`](crate::hotkey::Code)). 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 52 | pub struct HotKey { 53 | /// The hotkey modifiers. 54 | pub mods: Modifiers, 55 | /// The hotkey key. 56 | pub key: Code, 57 | /// The hotkey id. 58 | pub id: u32, 59 | } 60 | 61 | #[cfg(feature = "serde")] 62 | impl<'de> serde::Deserialize<'de> for HotKey { 63 | fn deserialize(deserializer: D) -> Result 64 | where 65 | D: serde::Deserializer<'de>, 66 | { 67 | let hotkey = String::deserialize(deserializer)?; 68 | hotkey 69 | .parse() 70 | .map_err(|e: HotKeyParseError| serde::de::Error::custom(e.to_string())) 71 | } 72 | } 73 | 74 | #[cfg(feature = "serde")] 75 | impl serde::Serialize for HotKey { 76 | fn serialize(&self, serializer: S) -> Result 77 | where 78 | S: serde::Serializer, 79 | { 80 | self.to_string().serialize(serializer) 81 | } 82 | } 83 | 84 | impl HotKey { 85 | /// Creates a new hotkey to define keyboard shortcuts throughout your application. 86 | /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] 87 | pub fn new(mods: Option, key: Code) -> Self { 88 | let mut mods = mods.unwrap_or_else(Modifiers::empty); 89 | if mods.contains(Modifiers::META) { 90 | mods.remove(Modifiers::META); 91 | mods.insert(Modifiers::SUPER); 92 | } 93 | 94 | Self { 95 | mods, 96 | key, 97 | id: (mods.bits() << 16) | key as u32, 98 | } 99 | } 100 | 101 | /// Returns the id associated with this hotKey 102 | /// which is a hash of the string represention of modifiers and key within this hotKey. 103 | pub fn id(&self) -> u32 { 104 | self.id 105 | } 106 | 107 | /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. 108 | pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { 109 | // Should be a const but const bit_or doesn't work here. 110 | let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; 111 | let modifiers = modifiers.borrow(); 112 | let key = key.borrow(); 113 | self.mods == *modifiers & base_mods && self.key == *key 114 | } 115 | 116 | /// Converts this hotkey into a string. 117 | pub fn into_string(self) -> String { 118 | let mut hotkey = String::new(); 119 | if self.mods.contains(Modifiers::SHIFT) { 120 | hotkey.push_str("shift+") 121 | } 122 | if self.mods.contains(Modifiers::CONTROL) { 123 | hotkey.push_str("control+") 124 | } 125 | if self.mods.contains(Modifiers::ALT) { 126 | hotkey.push_str("alt+") 127 | } 128 | if self.mods.contains(Modifiers::SUPER) { 129 | hotkey.push_str("super+") 130 | } 131 | hotkey.push_str(&self.key.to_string()); 132 | hotkey 133 | } 134 | } 135 | 136 | impl Display for HotKey { 137 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 138 | write!(f, "{}", self.into_string()) 139 | } 140 | } 141 | 142 | // HotKey::from_str is available to be backward 143 | // compatible with tauri and it also open the option 144 | // to generate hotkey from string 145 | impl FromStr for HotKey { 146 | type Err = HotKeyParseError; 147 | fn from_str(hotkey_string: &str) -> Result { 148 | parse_hotkey(hotkey_string) 149 | } 150 | } 151 | 152 | impl TryFrom<&str> for HotKey { 153 | type Error = HotKeyParseError; 154 | 155 | fn try_from(value: &str) -> Result { 156 | parse_hotkey(value) 157 | } 158 | } 159 | 160 | impl TryFrom for HotKey { 161 | type Error = HotKeyParseError; 162 | 163 | fn try_from(value: String) -> Result { 164 | parse_hotkey(&value) 165 | } 166 | } 167 | 168 | fn parse_hotkey(hotkey: &str) -> Result { 169 | let tokens = hotkey.split('+').collect::>(); 170 | 171 | let mut mods = Modifiers::empty(); 172 | let mut key = None; 173 | 174 | match tokens.len() { 175 | // single key hotkey 176 | 1 => { 177 | key = Some(parse_key(tokens[0])?); 178 | } 179 | // modifiers and key comobo hotkey 180 | _ => { 181 | for raw in tokens { 182 | let token = raw.trim(); 183 | 184 | if token.is_empty() { 185 | return Err(HotKeyParseError::EmptyToken(hotkey.to_string())); 186 | } 187 | 188 | if key.is_some() { 189 | // At this point we have parsed the modifiers and a main key, so by reaching 190 | // this code, the function either received more than one main key or 191 | // the hotkey is not in the right order 192 | // examples: 193 | // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. 194 | // 2. "Ctrl+C+Shift" => wrong order 195 | return Err(HotKeyParseError::InvalidFormat(hotkey.to_string())); 196 | } 197 | 198 | match token.to_uppercase().as_str() { 199 | "OPTION" | "ALT" => { 200 | mods |= Modifiers::ALT; 201 | } 202 | "CONTROL" | "CTRL" => { 203 | mods |= Modifiers::CONTROL; 204 | } 205 | "COMMAND" | "CMD" | "SUPER" => { 206 | mods |= Modifiers::SUPER; 207 | } 208 | "SHIFT" => { 209 | mods |= Modifiers::SHIFT; 210 | } 211 | #[cfg(target_os = "macos")] 212 | "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { 213 | mods |= Modifiers::SUPER; 214 | } 215 | #[cfg(not(target_os = "macos"))] 216 | "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { 217 | mods |= Modifiers::CONTROL; 218 | } 219 | _ => { 220 | key = Some(parse_key(token)?); 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | Ok(HotKey::new( 228 | Some(mods), 229 | key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, 230 | )) 231 | } 232 | 233 | fn parse_key(key: &str) -> Result { 234 | use Code::*; 235 | match key.to_uppercase().as_str() { 236 | "BACKQUOTE" | "`" => Ok(Backquote), 237 | "BACKSLASH" | "\\" => Ok(Backslash), 238 | "BRACKETLEFT" | "[" => Ok(BracketLeft), 239 | "BRACKETRIGHT" | "]" => Ok(BracketRight), 240 | "PAUSE" | "PAUSEBREAK" => Ok(Pause), 241 | "COMMA" | "," => Ok(Comma), 242 | "DIGIT0" | "0" => Ok(Digit0), 243 | "DIGIT1" | "1" => Ok(Digit1), 244 | "DIGIT2" | "2" => Ok(Digit2), 245 | "DIGIT3" | "3" => Ok(Digit3), 246 | "DIGIT4" | "4" => Ok(Digit4), 247 | "DIGIT5" | "5" => Ok(Digit5), 248 | "DIGIT6" | "6" => Ok(Digit6), 249 | "DIGIT7" | "7" => Ok(Digit7), 250 | "DIGIT8" | "8" => Ok(Digit8), 251 | "DIGIT9" | "9" => Ok(Digit9), 252 | "EQUAL" | "=" => Ok(Equal), 253 | "KEYA" | "A" => Ok(KeyA), 254 | "KEYB" | "B" => Ok(KeyB), 255 | "KEYC" | "C" => Ok(KeyC), 256 | "KEYD" | "D" => Ok(KeyD), 257 | "KEYE" | "E" => Ok(KeyE), 258 | "KEYF" | "F" => Ok(KeyF), 259 | "KEYG" | "G" => Ok(KeyG), 260 | "KEYH" | "H" => Ok(KeyH), 261 | "KEYI" | "I" => Ok(KeyI), 262 | "KEYJ" | "J" => Ok(KeyJ), 263 | "KEYK" | "K" => Ok(KeyK), 264 | "KEYL" | "L" => Ok(KeyL), 265 | "KEYM" | "M" => Ok(KeyM), 266 | "KEYN" | "N" => Ok(KeyN), 267 | "KEYO" | "O" => Ok(KeyO), 268 | "KEYP" | "P" => Ok(KeyP), 269 | "KEYQ" | "Q" => Ok(KeyQ), 270 | "KEYR" | "R" => Ok(KeyR), 271 | "KEYS" | "S" => Ok(KeyS), 272 | "KEYT" | "T" => Ok(KeyT), 273 | "KEYU" | "U" => Ok(KeyU), 274 | "KEYV" | "V" => Ok(KeyV), 275 | "KEYW" | "W" => Ok(KeyW), 276 | "KEYX" | "X" => Ok(KeyX), 277 | "KEYY" | "Y" => Ok(KeyY), 278 | "KEYZ" | "Z" => Ok(KeyZ), 279 | "MINUS" | "-" => Ok(Minus), 280 | "PERIOD" | "." => Ok(Period), 281 | "QUOTE" | "'" => Ok(Quote), 282 | "SEMICOLON" | ";" => Ok(Semicolon), 283 | "SLASH" | "/" => Ok(Slash), 284 | "BACKSPACE" => Ok(Backspace), 285 | "CAPSLOCK" => Ok(CapsLock), 286 | "ENTER" => Ok(Enter), 287 | "SPACE" => Ok(Space), 288 | "TAB" => Ok(Tab), 289 | "DELETE" => Ok(Delete), 290 | "END" => Ok(End), 291 | "HOME" => Ok(Home), 292 | "INSERT" => Ok(Insert), 293 | "PAGEDOWN" => Ok(PageDown), 294 | "PAGEUP" => Ok(PageUp), 295 | "PRINTSCREEN" => Ok(PrintScreen), 296 | "SCROLLLOCK" => Ok(ScrollLock), 297 | "ARROWDOWN" | "DOWN" => Ok(ArrowDown), 298 | "ARROWLEFT" | "LEFT" => Ok(ArrowLeft), 299 | "ARROWRIGHT" | "RIGHT" => Ok(ArrowRight), 300 | "ARROWUP" | "UP" => Ok(ArrowUp), 301 | "NUMLOCK" => Ok(NumLock), 302 | "NUMPAD0" | "NUM0" => Ok(Numpad0), 303 | "NUMPAD1" | "NUM1" => Ok(Numpad1), 304 | "NUMPAD2" | "NUM2" => Ok(Numpad2), 305 | "NUMPAD3" | "NUM3" => Ok(Numpad3), 306 | "NUMPAD4" | "NUM4" => Ok(Numpad4), 307 | "NUMPAD5" | "NUM5" => Ok(Numpad5), 308 | "NUMPAD6" | "NUM6" => Ok(Numpad6), 309 | "NUMPAD7" | "NUM7" => Ok(Numpad7), 310 | "NUMPAD8" | "NUM8" => Ok(Numpad8), 311 | "NUMPAD9" | "NUM9" => Ok(Numpad9), 312 | "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd), 313 | "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal), 314 | "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide), 315 | "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter), 316 | "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual), 317 | "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply), 318 | "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract), 319 | "ESCAPE" | "ESC" => Ok(Escape), 320 | "F1" => Ok(F1), 321 | "F2" => Ok(F2), 322 | "F3" => Ok(F3), 323 | "F4" => Ok(F4), 324 | "F5" => Ok(F5), 325 | "F6" => Ok(F6), 326 | "F7" => Ok(F7), 327 | "F8" => Ok(F8), 328 | "F9" => Ok(F9), 329 | "F10" => Ok(F10), 330 | "F11" => Ok(F11), 331 | "F12" => Ok(F12), 332 | "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown), 333 | "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), 334 | "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), 335 | "MEDIAPLAY" => Ok(MediaPlay), 336 | "MEDIAPAUSE" => Ok(MediaPause), 337 | "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), 338 | "MEDIASTOP" => Ok(MediaStop), 339 | "MEDIATRACKNEXT" => Ok(MediaTrackNext), 340 | "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious), 341 | "F13" => Ok(F13), 342 | "F14" => Ok(F14), 343 | "F15" => Ok(F15), 344 | "F16" => Ok(F16), 345 | "F17" => Ok(F17), 346 | "F18" => Ok(F18), 347 | "F19" => Ok(F19), 348 | "F20" => Ok(F20), 349 | "F21" => Ok(F21), 350 | "F22" => Ok(F22), 351 | "F23" => Ok(F23), 352 | "F24" => Ok(F24), 353 | 354 | _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), 355 | } 356 | } 357 | 358 | #[test] 359 | fn test_parse_hotkey() { 360 | macro_rules! assert_parse_hotkey { 361 | ($key:literal, $lrh:expr) => { 362 | let r = parse_hotkey($key).unwrap(); 363 | let l = $lrh; 364 | assert_eq!(r.mods, l.mods); 365 | assert_eq!(r.key, l.key); 366 | }; 367 | } 368 | 369 | assert_parse_hotkey!( 370 | "KeyX", 371 | HotKey { 372 | mods: Modifiers::empty(), 373 | key: Code::KeyX, 374 | id: 0, 375 | } 376 | ); 377 | 378 | assert_parse_hotkey!( 379 | "CTRL+KeyX", 380 | HotKey { 381 | mods: Modifiers::CONTROL, 382 | key: Code::KeyX, 383 | id: 0, 384 | } 385 | ); 386 | 387 | assert_parse_hotkey!( 388 | "SHIFT+KeyC", 389 | HotKey { 390 | mods: Modifiers::SHIFT, 391 | key: Code::KeyC, 392 | id: 0, 393 | } 394 | ); 395 | 396 | assert_parse_hotkey!( 397 | "SHIFT+KeyC", 398 | HotKey { 399 | mods: Modifiers::SHIFT, 400 | key: Code::KeyC, 401 | id: 0, 402 | } 403 | ); 404 | 405 | assert_parse_hotkey!( 406 | "super+ctrl+SHIFT+alt+ArrowUp", 407 | HotKey { 408 | mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT, 409 | key: Code::ArrowUp, 410 | id: 0, 411 | } 412 | ); 413 | assert_parse_hotkey!( 414 | "Digit5", 415 | HotKey { 416 | mods: Modifiers::empty(), 417 | key: Code::Digit5, 418 | id: 0, 419 | } 420 | ); 421 | assert_parse_hotkey!( 422 | "KeyG", 423 | HotKey { 424 | mods: Modifiers::empty(), 425 | key: Code::KeyG, 426 | id: 0, 427 | } 428 | ); 429 | 430 | assert_parse_hotkey!( 431 | "SHiFT+F12", 432 | HotKey { 433 | mods: Modifiers::SHIFT, 434 | key: Code::F12, 435 | id: 0, 436 | } 437 | ); 438 | 439 | assert_parse_hotkey!( 440 | "CmdOrCtrl+Space", 441 | HotKey { 442 | #[cfg(target_os = "macos")] 443 | mods: Modifiers::SUPER, 444 | #[cfg(not(target_os = "macos"))] 445 | mods: Modifiers::CONTROL, 446 | key: Code::Space, 447 | id: 0, 448 | } 449 | ); 450 | 451 | // Ensure that if it is just multiple modifiers, we do not panic. 452 | // This would be a regression if this happened. 453 | if HotKey::from_str("Shift+Ctrl").is_ok() { 454 | panic!("This is not a valid hotkey"); 455 | } 456 | } 457 | 458 | #[test] 459 | fn test_equality() { 460 | let h1 = parse_hotkey("Shift+KeyR").unwrap(); 461 | let h2 = parse_hotkey("Shift+KeyR").unwrap(); 462 | let h3 = HotKey::new(Some(Modifiers::SHIFT), Code::KeyR); 463 | let h4 = parse_hotkey("Alt+KeyR").unwrap(); 464 | let h5 = parse_hotkey("Alt+KeyR").unwrap(); 465 | let h6 = parse_hotkey("KeyR").unwrap(); 466 | 467 | assert!(h1 == h2 && h2 == h3 && h3 != h4 && h4 == h5 && h5 != h6); 468 | assert!( 469 | h1.id() == h2.id() 470 | && h2.id() == h3.id() 471 | && h3.id() != h4.id() 472 | && h4.id() == h5.id() 473 | && h5.id() != h6.id() 474 | ); 475 | } 476 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #![allow(clippy::uninlined_format_args)] 6 | 7 | //! global_hotkey lets you register Global HotKeys for Desktop Applications. 8 | //! 9 | //! ## Platforms-supported: 10 | //! 11 | //! - Windows 12 | //! - macOS 13 | //! - Linux (X11 Only) 14 | //! 15 | //! ## Platform-specific notes: 16 | //! 17 | //! - On Windows a win32 event loop must be running on the thread. It doesn't need to be the main thread but you have to create the global hotkey manager on the same thread as the event loop. 18 | //! - On macOS, an event loop must be running on the main thread so you also need to create the global hotkey manager on the main thread. 19 | //! 20 | //! # Example 21 | //! 22 | //! ```no_run 23 | //! use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}}; 24 | //! 25 | //! // initialize the hotkeys manager 26 | //! let manager = GlobalHotKeyManager::new().unwrap(); 27 | //! 28 | //! // construct the hotkey 29 | //! let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); 30 | //! 31 | //! // register it 32 | //! manager.register(hotkey); 33 | //! ``` 34 | //! 35 | //! 36 | //! # Processing global hotkey events 37 | //! 38 | //! You can also listen for the menu events using [`GlobalHotKeyEvent::receiver`] to get events for the hotkey pressed events. 39 | //! ```no_run 40 | //! use global_hotkey::GlobalHotKeyEvent; 41 | //! 42 | //! if let Ok(event) = GlobalHotKeyEvent::receiver().try_recv() { 43 | //! println!("{:?}", event); 44 | //! } 45 | //! ``` 46 | //! 47 | //! # Platforms-supported: 48 | //! 49 | //! - Windows 50 | //! - macOS 51 | //! - Linux (X11 Only) 52 | 53 | use crossbeam_channel::{unbounded, Receiver, Sender}; 54 | use once_cell::sync::{Lazy, OnceCell}; 55 | 56 | mod error; 57 | pub mod hotkey; 58 | mod platform_impl; 59 | 60 | pub use self::error::*; 61 | use hotkey::HotKey; 62 | 63 | /// Describes the state of the [`HotKey`]. 64 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 65 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 66 | pub enum HotKeyState { 67 | /// The [`HotKey`] is pressed (the key is down). 68 | Pressed, 69 | /// The [`HotKey`] is released (the key is up). 70 | Released, 71 | } 72 | 73 | /// Describes a global hotkey event emitted when a [`HotKey`] is pressed or released. 74 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 75 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 76 | pub struct GlobalHotKeyEvent { 77 | /// Id of the associated [`HotKey`]. 78 | pub id: u32, 79 | /// State of the associated [`HotKey`]. 80 | pub state: HotKeyState, 81 | } 82 | 83 | /// A reciever that could be used to listen to global hotkey events. 84 | pub type GlobalHotKeyEventReceiver = Receiver; 85 | type GlobalHotKeyEventHandler = Box; 86 | 87 | static GLOBAL_HOTKEY_CHANNEL: Lazy<(Sender, GlobalHotKeyEventReceiver)> = 88 | Lazy::new(unbounded); 89 | static GLOBAL_HOTKEY_EVENT_HANDLER: OnceCell> = OnceCell::new(); 90 | 91 | impl GlobalHotKeyEvent { 92 | /// Returns the id of the associated [`HotKey`]. 93 | pub fn id(&self) -> u32 { 94 | self.id 95 | } 96 | 97 | /// Returns the state of the associated [`HotKey`]. 98 | pub fn state(&self) -> HotKeyState { 99 | self.state 100 | } 101 | 102 | /// Gets a reference to the event channel's [`GlobalHotKeyEventReceiver`] 103 | /// which can be used to listen for global hotkey events. 104 | /// 105 | /// ## Note 106 | /// 107 | /// This will not receive any events if [`GlobalHotKeyEvent::set_event_handler`] has been called with a `Some` value. 108 | pub fn receiver<'a>() -> &'a GlobalHotKeyEventReceiver { 109 | &GLOBAL_HOTKEY_CHANNEL.1 110 | } 111 | 112 | /// Set a handler to be called for new events. Useful for implementing custom event sender. 113 | /// 114 | /// ## Note 115 | /// 116 | /// Calling this function with a `Some` value, 117 | /// will not send new events to the channel associated with [`GlobalHotKeyEvent::receiver`] 118 | pub fn set_event_handler(f: Option) { 119 | if let Some(f) = f { 120 | let _ = GLOBAL_HOTKEY_EVENT_HANDLER.set(Some(Box::new(f))); 121 | } else { 122 | let _ = GLOBAL_HOTKEY_EVENT_HANDLER.set(None); 123 | } 124 | } 125 | 126 | pub(crate) fn send(event: GlobalHotKeyEvent) { 127 | if let Some(handler) = GLOBAL_HOTKEY_EVENT_HANDLER.get_or_init(|| None) { 128 | handler(event); 129 | } else { 130 | let _ = GLOBAL_HOTKEY_CHANNEL.0.send(event); 131 | } 132 | } 133 | } 134 | 135 | pub struct GlobalHotKeyManager { 136 | platform_impl: platform_impl::GlobalHotKeyManager, 137 | } 138 | 139 | impl GlobalHotKeyManager { 140 | pub fn new() -> crate::Result { 141 | Ok(Self { 142 | platform_impl: platform_impl::GlobalHotKeyManager::new()?, 143 | }) 144 | } 145 | 146 | pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { 147 | self.platform_impl.register(hotkey) 148 | } 149 | 150 | pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { 151 | self.platform_impl.unregister(hotkey) 152 | } 153 | 154 | pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 155 | self.platform_impl.register_all(hotkeys)?; 156 | Ok(()) 157 | } 158 | 159 | pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 160 | self.platform_impl.unregister_all(hotkeys)?; 161 | Ok(()) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/platform_impl/macos/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_upper_case_globals)] 3 | #![allow(non_snake_case)] 4 | #![allow(unused)] 5 | 6 | /* taken from https://github.com/wusyong/carbon-bindgen/blob/467fca5d71047050b632fbdfb41b1f14575a8499/bindings.rs */ 7 | 8 | use std::ffi::{c_long, c_void}; 9 | 10 | use objc2::encode::{Encode, Encoding, RefEncode}; 11 | 12 | pub type UInt32 = ::std::os::raw::c_uint; 13 | pub type SInt32 = ::std::os::raw::c_int; 14 | pub type OSStatus = SInt32; 15 | pub type FourCharCode = UInt32; 16 | pub type OSType = FourCharCode; 17 | pub type ByteCount = ::std::os::raw::c_ulong; 18 | pub type ItemCount = ::std::os::raw::c_ulong; 19 | pub type OptionBits = UInt32; 20 | pub type EventKind = UInt32; 21 | #[repr(C)] 22 | #[derive(Debug, Copy, Clone)] 23 | pub struct OpaqueEventRef { 24 | _unused: [u8; 0], 25 | } 26 | pub type EventRef = *mut OpaqueEventRef; 27 | pub type EventParamName = OSType; 28 | pub type EventParamType = OSType; 29 | #[repr(C)] 30 | #[derive(Debug, Copy, Clone)] 31 | pub struct OpaqueEventHandlerRef { 32 | _unused: [u8; 0], 33 | } 34 | pub type EventHandlerRef = *mut OpaqueEventHandlerRef; 35 | #[repr(C)] 36 | #[derive(Debug, Copy, Clone)] 37 | pub struct OpaqueEventHandlerCallRef { 38 | _unused: [u8; 0], 39 | } 40 | pub type EventHandlerCallRef = *mut OpaqueEventHandlerCallRef; 41 | #[repr(C)] 42 | #[derive(Debug, Copy, Clone)] 43 | pub struct OpaqueEventTargetRef { 44 | _unused: [u8; 0], 45 | } 46 | pub type EventTargetRef = *mut OpaqueEventTargetRef; 47 | pub type EventHandlerProcPtr = ::std::option::Option< 48 | unsafe extern "C" fn( 49 | inHandlerCallRef: EventHandlerCallRef, 50 | inEvent: EventRef, 51 | inUserData: *mut ::std::os::raw::c_void, 52 | ) -> OSStatus, 53 | >; 54 | pub type EventHandlerUPP = EventHandlerProcPtr; 55 | #[repr(C)] 56 | #[derive(Debug, Copy, Clone)] 57 | pub struct OpaqueEventHotKeyRef { 58 | _unused: [u8; 0], 59 | } 60 | pub type EventHotKeyRef = *mut OpaqueEventHotKeyRef; 61 | 62 | pub type _bindgen_ty_1637 = ::std::os::raw::c_uint; 63 | pub const kEventParamDirectObject: _bindgen_ty_1637 = 757935405; 64 | pub const kEventParamDragRef: _bindgen_ty_1637 = 1685217639; 65 | pub type _bindgen_ty_1921 = ::std::os::raw::c_uint; 66 | pub const typeEventHotKeyID: _bindgen_ty_1921 = 1751869796; 67 | pub type _bindgen_ty_1939 = ::std::os::raw::c_uint; 68 | pub const kEventClassKeyboard: _bindgen_ty_1939 = 1801812322; 69 | pub type _bindgen_ty_1980 = ::std::os::raw::c_uint; 70 | pub const kEventHotKeyPressed: _bindgen_ty_1980 = 5; 71 | pub type _bindgen_ty_1981 = ::std::os::raw::c_uint; 72 | pub const kEventHotKeyReleased: _bindgen_ty_1981 = 6; 73 | pub type _bindgen_ty_1 = ::std::os::raw::c_uint; 74 | pub const noErr: _bindgen_ty_1 = 0; 75 | 76 | #[repr(C, packed(2))] 77 | #[derive(Debug, Copy, Clone)] 78 | pub struct EventHotKeyID { 79 | pub signature: OSType, 80 | pub id: UInt32, 81 | } 82 | 83 | #[repr(C, packed(2))] 84 | #[derive(Debug, Copy, Clone)] 85 | pub struct EventTypeSpec { 86 | pub eventClass: OSType, 87 | pub eventKind: EventKind, 88 | } 89 | 90 | #[link(name = "Carbon", kind = "framework")] 91 | extern "C" { 92 | pub fn GetEventParameter( 93 | inEvent: EventRef, 94 | inName: EventParamName, 95 | inDesiredType: EventParamType, 96 | outActualType: *mut EventParamType, 97 | inBufferSize: ByteCount, 98 | outActualSize: *mut ByteCount, 99 | outData: *mut ::std::os::raw::c_void, 100 | ) -> OSStatus; 101 | pub fn GetEventKind(inEvent: EventRef) -> EventKind; 102 | pub fn GetApplicationEventTarget() -> EventTargetRef; 103 | pub fn InstallEventHandler( 104 | inTarget: EventTargetRef, 105 | inHandler: EventHandlerUPP, 106 | inNumTypes: ItemCount, 107 | inList: *const EventTypeSpec, 108 | inUserData: *mut ::std::os::raw::c_void, 109 | outRef: *mut EventHandlerRef, 110 | ) -> OSStatus; 111 | pub fn RemoveEventHandler(inHandlerRef: EventHandlerRef) -> OSStatus; 112 | pub fn RegisterEventHotKey( 113 | inHotKeyCode: UInt32, 114 | inHotKeyModifiers: UInt32, 115 | inHotKeyID: EventHotKeyID, 116 | inTarget: EventTargetRef, 117 | inOptions: OptionBits, 118 | outRef: *mut EventHotKeyRef, 119 | ) -> OSStatus; 120 | pub fn UnregisterEventHotKey(inHotKey: EventHotKeyRef) -> OSStatus; 121 | } 122 | 123 | /* Core Graphics */ 124 | 125 | /// Possible tapping points for events. 126 | #[repr(C)] 127 | #[derive(Clone, Copy, Debug)] 128 | pub enum CGEventTapLocation { 129 | Hid, 130 | Session, 131 | AnnotatedSession, 132 | } 133 | 134 | // The next three enums are taken from: 135 | // [Ref](https://github.com/phracker/MacOSX-SDKs/blob/ef9fe35d5691b6dd383c8c46d867a499817a01b6/MacOSX10.15.sdk/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Headers/CGEventTypes.h) 136 | /* Constants that specify where a new event tap is inserted into the list of active event taps. */ 137 | #[repr(u32)] 138 | #[derive(Clone, Copy, Debug)] 139 | pub enum CGEventTapPlacement { 140 | HeadInsertEventTap = 0, 141 | TailAppendEventTap, 142 | } 143 | 144 | /* Constants that specify whether a new event tap is an active filter or a passive listener. */ 145 | #[repr(u32)] 146 | #[derive(Clone, Copy, Debug)] 147 | pub enum CGEventTapOptions { 148 | Default = 0x00000000, 149 | ListenOnly = 0x00000001, 150 | } 151 | 152 | /// Constants that specify the different types of input events. 153 | /// 154 | /// [Ref](http://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-700/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h) 155 | #[repr(u32)] 156 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 157 | pub enum CGEventType { 158 | Null = 0, 159 | 160 | // Mouse events. 161 | LeftMouseDown = 1, 162 | LeftMouseUp = 2, 163 | RightMouseDown = 3, 164 | RightMouseUp = 4, 165 | MouseMoved = 5, 166 | LeftMouseDragged = 6, 167 | RightMouseDragged = 7, 168 | 169 | // Keyboard events. 170 | KeyDown = 10, 171 | KeyUp = 11, 172 | FlagsChanged = 12, 173 | 174 | // Composite events. 175 | AppKitDefined = 13, 176 | SystemDefined = 14, 177 | ApplicationDefined = 15, 178 | 179 | // Specialized control devices. 180 | ScrollWheel = 22, 181 | TabletPointer = 23, 182 | TabletProximity = 24, 183 | OtherMouseDown = 25, 184 | OtherMouseUp = 26, 185 | OtherMouseDragged = 27, 186 | 187 | // Out of band event types. These are delivered to the event tap callback 188 | // to notify it of unusual conditions that disable the event tap. 189 | TapDisabledByTimeout = 0xFFFFFFFE, 190 | TapDisabledByUserInput = 0xFFFFFFFF, 191 | } 192 | 193 | pub type CGEventMask = u64; 194 | #[macro_export] 195 | macro_rules! CGEventMaskBit { 196 | ($eventType:expr) => { 197 | 1 << $eventType as CGEventMask 198 | }; 199 | } 200 | 201 | pub enum CGEvent {} 202 | pub type CGEventRef = *const CGEvent; 203 | 204 | unsafe impl RefEncode for CGEvent { 205 | const ENCODING_REF: Encoding = Encoding::Pointer(&Encoding::Struct("__CGEvent", &[])); 206 | } 207 | 208 | pub type CGEventTapProxy = *const c_void; 209 | type CGEventTapCallBack = unsafe extern "C" fn( 210 | proxy: CGEventTapProxy, 211 | etype: CGEventType, 212 | event: CGEventRef, 213 | user_info: *const c_void, 214 | ) -> CGEventRef; 215 | 216 | #[link(name = "CoreGraphics", kind = "framework")] 217 | extern "C" { 218 | pub fn CGEventTapCreate( 219 | tap: CGEventTapLocation, 220 | place: CGEventTapPlacement, 221 | options: CGEventTapOptions, 222 | events_of_interest: CGEventMask, 223 | callback: CGEventTapCallBack, 224 | user_info: *const c_void, 225 | ) -> CFMachPortRef; 226 | pub fn CGEventTapEnable(tap: CFMachPortRef, enable: bool); 227 | } 228 | 229 | /* Core Foundation */ 230 | 231 | pub enum CFAllocator {} 232 | pub type CFAllocatorRef = *mut CFAllocator; 233 | pub enum CFRunLoop {} 234 | pub type CFRunLoopRef = *mut CFRunLoop; 235 | pub type CFRunLoopMode = CFStringRef; 236 | pub enum CFRunLoopObserver {} 237 | pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; 238 | pub enum CFRunLoopTimer {} 239 | pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; 240 | pub enum CFRunLoopSource {} 241 | pub type CFRunLoopSourceRef = *mut CFRunLoopSource; 242 | pub enum CFString {} 243 | pub type CFStringRef = *const CFString; 244 | 245 | pub enum CFMachPort {} 246 | pub type CFMachPortRef = *mut CFMachPort; 247 | 248 | pub type CFIndex = c_long; 249 | 250 | #[link(name = "CoreFoundation", kind = "framework")] 251 | extern "C" { 252 | pub static kCFRunLoopCommonModes: CFRunLoopMode; 253 | pub static kCFAllocatorDefault: CFAllocatorRef; 254 | 255 | pub fn CFRunLoopGetMain() -> CFRunLoopRef; 256 | 257 | pub fn CFMachPortCreateRunLoopSource( 258 | allocator: CFAllocatorRef, 259 | port: CFMachPortRef, 260 | order: CFIndex, 261 | ) -> CFRunLoopSourceRef; 262 | pub fn CFMachPortInvalidate(port: CFMachPortRef); 263 | pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); 264 | pub fn CFRunLoopRemoveSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); 265 | pub fn CFRelease(cftype: *const c_void); 266 | } 267 | -------------------------------------------------------------------------------- /src/platform_impl/macos/mod.rs: -------------------------------------------------------------------------------- 1 | use keyboard_types::{Code, Modifiers}; 2 | use objc2::{msg_send, rc::Retained, ClassType}; 3 | use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType}; 4 | use std::{ 5 | collections::{BTreeMap, HashSet}, 6 | ffi::c_void, 7 | ptr, 8 | sync::{Arc, Mutex}, 9 | }; 10 | 11 | use crate::{ 12 | hotkey::HotKey, 13 | platform_impl::platform::ffi::{ 14 | kCFAllocatorDefault, kCFRunLoopCommonModes, CFMachPortCreateRunLoopSource, 15 | CFRunLoopAddSource, CFRunLoopGetMain, CGEventMask, CGEventRef, CGEventTapCreate, 16 | CGEventTapEnable, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, 17 | CGEventTapProxy, CGEventType, 18 | }, 19 | CGEventMaskBit, GlobalHotKeyEvent, 20 | }; 21 | 22 | use self::ffi::{ 23 | kEventClassKeyboard, kEventHotKeyPressed, kEventHotKeyReleased, kEventParamDirectObject, noErr, 24 | typeEventHotKeyID, CFMachPortInvalidate, CFMachPortRef, CFRelease, CFRunLoopRemoveSource, 25 | CFRunLoopSourceRef, EventHandlerCallRef, EventHandlerRef, EventHotKeyID, EventHotKeyRef, 26 | EventRef, EventTypeSpec, GetApplicationEventTarget, GetEventKind, GetEventParameter, 27 | InstallEventHandler, OSStatus, RegisterEventHotKey, RemoveEventHandler, UnregisterEventHotKey, 28 | }; 29 | 30 | mod ffi; 31 | 32 | pub struct GlobalHotKeyManager { 33 | event_handler_ptr: EventHandlerRef, 34 | hotkeys: Mutex>, 35 | event_tap: Mutex>, 36 | event_tap_source: Mutex>, 37 | media_hotkeys: Arc>>, 38 | } 39 | 40 | unsafe impl Send for GlobalHotKeyManager {} 41 | unsafe impl Sync for GlobalHotKeyManager {} 42 | 43 | impl GlobalHotKeyManager { 44 | pub fn new() -> crate::Result { 45 | let pressed_event_type = EventTypeSpec { 46 | eventClass: kEventClassKeyboard, 47 | eventKind: kEventHotKeyPressed, 48 | }; 49 | let released_event_type = EventTypeSpec { 50 | eventClass: kEventClassKeyboard, 51 | eventKind: kEventHotKeyReleased, 52 | }; 53 | let event_types = [pressed_event_type, released_event_type]; 54 | 55 | let ptr = unsafe { 56 | let mut handler_ref: EventHandlerRef = std::mem::zeroed(); 57 | 58 | let result = InstallEventHandler( 59 | GetApplicationEventTarget(), 60 | Some(hotkey_handler), 61 | 2, 62 | event_types.as_ptr(), 63 | std::ptr::null_mut(), 64 | &mut handler_ref, 65 | ); 66 | 67 | if result != noErr as _ { 68 | return Err(crate::Error::OsError(std::io::Error::last_os_error())); 69 | } 70 | 71 | handler_ref 72 | }; 73 | 74 | Ok(Self { 75 | event_handler_ptr: ptr, 76 | hotkeys: Mutex::new(BTreeMap::new()), 77 | event_tap: Mutex::new(None), 78 | event_tap_source: Mutex::new(None), 79 | media_hotkeys: Arc::new(Mutex::new(HashSet::new())), 80 | }) 81 | } 82 | 83 | pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { 84 | let mut mods: u32 = 0; 85 | if hotkey.mods.contains(Modifiers::SHIFT) { 86 | mods |= 512; 87 | } 88 | if hotkey.mods.intersects(Modifiers::SUPER | Modifiers::META) { 89 | mods |= 256; 90 | } 91 | if hotkey.mods.contains(Modifiers::ALT) { 92 | mods |= 2048; 93 | } 94 | if hotkey.mods.contains(Modifiers::CONTROL) { 95 | mods |= 4096; 96 | } 97 | 98 | if let Some(scan_code) = key_to_scancode(hotkey.key) { 99 | let hotkey_id = EventHotKeyID { 100 | id: hotkey.id(), 101 | signature: { 102 | let mut res: u32 = 0; 103 | // can't find a resource for "htrs" so we construct it manually 104 | // the construction method below is taken from https://github.com/soffes/HotKey/blob/c13662730cb5bc28de4a799854bbb018a90649bf/Sources/HotKey/HotKeysController.swift#L27 105 | // and confirmed by applying the same method to `kEventParamDragRef` which is equal to `drag` in C 106 | // and converted to `1685217639` by rust-bindgen. 107 | for c in "htrs".chars() { 108 | res = (res << 8) + c as u32; 109 | } 110 | res 111 | }, 112 | }; 113 | 114 | let ptr = unsafe { 115 | let mut hotkey_ref: EventHotKeyRef = std::mem::zeroed(); 116 | let result = RegisterEventHotKey( 117 | scan_code, 118 | mods, 119 | hotkey_id, 120 | GetApplicationEventTarget(), 121 | 0, 122 | &mut hotkey_ref, 123 | ); 124 | 125 | if result != noErr as _ { 126 | return Err(crate::Error::FailedToRegister(format!( 127 | "RegisterEventHotKey failed for {}", 128 | hotkey.key 129 | ))); 130 | } 131 | 132 | hotkey_ref 133 | }; 134 | 135 | self.hotkeys 136 | .lock() 137 | .unwrap() 138 | .insert(hotkey.id(), HotKeyWrapper { ptr, hotkey }); 139 | Ok(()) 140 | } else if is_media_key(hotkey.key) { 141 | { 142 | let mut media_hotkeys = self.media_hotkeys.lock().unwrap(); 143 | if !media_hotkeys.insert(hotkey) { 144 | return Err(crate::Error::AlreadyRegistered(hotkey)); 145 | } 146 | } 147 | self.start_watching_media_keys() 148 | } else { 149 | Err(crate::Error::FailedToRegister(format!( 150 | "Unknown scancode for {}", 151 | hotkey.key 152 | ))) 153 | } 154 | } 155 | 156 | pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { 157 | if is_media_key(hotkey.key) { 158 | let mut media_hotkey = self.media_hotkeys.lock().unwrap(); 159 | media_hotkey.remove(&hotkey); 160 | if media_hotkey.is_empty() { 161 | self.stop_watching_media_keys(); 162 | } 163 | } else if let Some(hotkeywrapper) = self.hotkeys.lock().unwrap().remove(&hotkey.id()) { 164 | unsafe { self.unregister_hotkey_ptr(hotkeywrapper.ptr, hotkey) }?; 165 | } 166 | 167 | Ok(()) 168 | } 169 | 170 | pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 171 | for hotkey in hotkeys { 172 | self.register(*hotkey)?; 173 | } 174 | Ok(()) 175 | } 176 | 177 | pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 178 | for hotkey in hotkeys { 179 | self.unregister(*hotkey)?; 180 | } 181 | Ok(()) 182 | } 183 | 184 | unsafe fn unregister_hotkey_ptr( 185 | &self, 186 | ptr: EventHotKeyRef, 187 | hotkey: HotKey, 188 | ) -> crate::Result<()> { 189 | if UnregisterEventHotKey(ptr) != noErr as _ { 190 | return Err(crate::Error::FailedToUnRegister(hotkey)); 191 | } 192 | 193 | Ok(()) 194 | } 195 | 196 | fn start_watching_media_keys(&self) -> crate::Result<()> { 197 | let mut event_tap = self.event_tap.lock().unwrap(); 198 | let mut event_tap_source = self.event_tap_source.lock().unwrap(); 199 | 200 | if event_tap.is_some() || event_tap_source.is_some() { 201 | return Ok(()); 202 | } 203 | 204 | unsafe { 205 | let event_mask: CGEventMask = CGEventMaskBit!(CGEventType::SystemDefined); 206 | let tap = CGEventTapCreate( 207 | CGEventTapLocation::Session, 208 | CGEventTapPlacement::HeadInsertEventTap, 209 | CGEventTapOptions::Default, 210 | event_mask, 211 | media_key_event_callback, 212 | Arc::into_raw(self.media_hotkeys.clone()) as *const c_void, 213 | ); 214 | if tap.is_null() { 215 | return Err(crate::Error::FailedToWatchMediaKeyEvent); 216 | } 217 | *event_tap = Some(tap); 218 | 219 | let loop_source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0); 220 | if loop_source.is_null() { 221 | // cleanup event_tap 222 | CFMachPortInvalidate(tap); 223 | CFRelease(tap as *const c_void); 224 | *event_tap = None; 225 | 226 | return Err(crate::Error::FailedToWatchMediaKeyEvent); 227 | } 228 | *event_tap_source = Some(loop_source); 229 | 230 | let run_loop = CFRunLoopGetMain(); 231 | CFRunLoopAddSource(run_loop, loop_source, kCFRunLoopCommonModes); 232 | CGEventTapEnable(tap, true); 233 | 234 | Ok(()) 235 | } 236 | } 237 | 238 | fn stop_watching_media_keys(&self) { 239 | unsafe { 240 | if let Some(event_tap_source) = self.event_tap_source.lock().unwrap().take() { 241 | let run_loop = CFRunLoopGetMain(); 242 | CFRunLoopRemoveSource(run_loop, event_tap_source, kCFRunLoopCommonModes); 243 | CFRelease(event_tap_source as *const c_void); 244 | } 245 | if let Some(event_tap) = self.event_tap.lock().unwrap().take() { 246 | CFMachPortInvalidate(event_tap); 247 | CFRelease(event_tap as *const c_void); 248 | } 249 | } 250 | } 251 | } 252 | 253 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 254 | #[allow(non_camel_case_types)] 255 | enum NX_KEYTYPE { 256 | Play = 16, // Actually it's Play/Pause 257 | Next = 17, 258 | Previous = 18, 259 | Fast = 19, 260 | Rewind = 20, 261 | } 262 | 263 | impl TryFrom for NX_KEYTYPE { 264 | type Error = String; 265 | 266 | fn try_from(value: isize) -> Result { 267 | match value { 268 | 16 => Ok(NX_KEYTYPE::Play), 269 | 17 => Ok(NX_KEYTYPE::Next), 270 | 18 => Ok(NX_KEYTYPE::Previous), 271 | 19 => Ok(NX_KEYTYPE::Fast), 272 | 20 => Ok(NX_KEYTYPE::Rewind), 273 | _ => Err(String::from("Not defined media key")), 274 | } 275 | } 276 | } 277 | 278 | impl From for Code { 279 | fn from(nx_keytype: NX_KEYTYPE) -> Self { 280 | match nx_keytype { 281 | NX_KEYTYPE::Play => Code::MediaPlayPause, 282 | NX_KEYTYPE::Next => Code::MediaTrackNext, 283 | NX_KEYTYPE::Previous => Code::MediaTrackPrevious, 284 | NX_KEYTYPE::Fast => Code::MediaFastForward, 285 | NX_KEYTYPE::Rewind => Code::MediaRewind, 286 | } 287 | } 288 | } 289 | 290 | impl Drop for GlobalHotKeyManager { 291 | fn drop(&mut self) { 292 | let hotkeys = self.hotkeys.lock().unwrap().clone(); 293 | for (_, hotkeywrapper) in hotkeys { 294 | let _ = self.unregister(hotkeywrapper.hotkey); 295 | } 296 | unsafe { 297 | RemoveEventHandler(self.event_handler_ptr); 298 | } 299 | self.stop_watching_media_keys() 300 | } 301 | } 302 | 303 | unsafe extern "C" fn hotkey_handler( 304 | _next_handler: EventHandlerCallRef, 305 | event: EventRef, 306 | _user_data: *mut c_void, 307 | ) -> OSStatus { 308 | let mut event_hotkey: EventHotKeyID = std::mem::zeroed(); 309 | 310 | let result = GetEventParameter( 311 | event, 312 | kEventParamDirectObject, 313 | typeEventHotKeyID, 314 | std::ptr::null_mut(), 315 | std::mem::size_of::() as _, 316 | std::ptr::null_mut(), 317 | &mut event_hotkey as *mut _ as *mut _, 318 | ); 319 | 320 | if result == noErr as _ { 321 | let event_kind = GetEventKind(event); 322 | match event_kind { 323 | #[allow(non_upper_case_globals)] 324 | kEventHotKeyPressed => GlobalHotKeyEvent::send(GlobalHotKeyEvent { 325 | id: event_hotkey.id, 326 | state: crate::HotKeyState::Pressed, 327 | }), 328 | #[allow(non_upper_case_globals)] 329 | kEventHotKeyReleased => GlobalHotKeyEvent::send(GlobalHotKeyEvent { 330 | id: event_hotkey.id, 331 | state: crate::HotKeyState::Released, 332 | }), 333 | _ => {} 334 | }; 335 | } 336 | 337 | noErr as _ 338 | } 339 | 340 | unsafe extern "C" fn media_key_event_callback( 341 | _proxy: CGEventTapProxy, 342 | ev_type: CGEventType, 343 | event: CGEventRef, 344 | user_info: *const c_void, 345 | ) -> CGEventRef { 346 | if ev_type != CGEventType::SystemDefined { 347 | return event; 348 | } 349 | 350 | let ns_event: Retained = msg_send![NSEvent::class(), eventWithCGEvent: event]; 351 | let event_type = ns_event.r#type(); 352 | let event_subtype = ns_event.subtype(); 353 | 354 | if event_type == NSEventType::SystemDefined && event_subtype == NSEventSubtype::ScreenChanged { 355 | // Key 356 | let data_1 = ns_event.data1(); 357 | let nx_keytype = NX_KEYTYPE::try_from((data_1 & 0xFFFF0000) >> 16); 358 | if nx_keytype.is_err() { 359 | return event; 360 | } 361 | let nx_keytype = nx_keytype.unwrap(); 362 | 363 | // Modifiers 364 | let flags = ns_event.modifierFlags(); 365 | let mut mods = Modifiers::empty(); 366 | if flags.contains(NSEventModifierFlags::Shift) { 367 | mods |= Modifiers::SHIFT; 368 | } 369 | if flags.contains(NSEventModifierFlags::Control) { 370 | mods |= Modifiers::CONTROL; 371 | } 372 | if flags.contains(NSEventModifierFlags::Option) { 373 | mods |= Modifiers::ALT; 374 | } 375 | if flags.contains(NSEventModifierFlags::Command) { 376 | mods |= Modifiers::META; 377 | } 378 | 379 | // Generate hotkey for matching 380 | let hotkey = HotKey::new(Some(mods), nx_keytype.into()); 381 | 382 | // Prevent Arc been releaded after callback returned 383 | let media_hotkeys = &*(user_info as *const Mutex>); 384 | 385 | if let Some(media_hotkey) = media_hotkeys.lock().unwrap().get(&hotkey) { 386 | let key_flags = data_1 & 0x0000FFFF; 387 | let is_pressed: bool = ((key_flags & 0xFF00) >> 8) == 0xA; 388 | GlobalHotKeyEvent::send(GlobalHotKeyEvent { 389 | id: media_hotkey.id(), 390 | state: match is_pressed { 391 | true => crate::HotKeyState::Pressed, 392 | false => crate::HotKeyState::Released, 393 | }, 394 | }); 395 | 396 | // Hotkey was found, return null to stop propagate event 397 | return ptr::null(); 398 | } 399 | } 400 | 401 | event 402 | } 403 | 404 | #[derive(Clone, Copy, Debug)] 405 | struct HotKeyWrapper { 406 | ptr: EventHotKeyRef, 407 | hotkey: HotKey, 408 | } 409 | 410 | // can be found in https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h 411 | pub fn key_to_scancode(code: Code) -> Option { 412 | match code { 413 | Code::KeyA => Some(0x00), 414 | Code::KeyS => Some(0x01), 415 | Code::KeyD => Some(0x02), 416 | Code::KeyF => Some(0x03), 417 | Code::KeyH => Some(0x04), 418 | Code::KeyG => Some(0x05), 419 | Code::KeyZ => Some(0x06), 420 | Code::KeyX => Some(0x07), 421 | Code::KeyC => Some(0x08), 422 | Code::KeyV => Some(0x09), 423 | Code::KeyB => Some(0x0b), 424 | Code::KeyQ => Some(0x0c), 425 | Code::KeyW => Some(0x0d), 426 | Code::KeyE => Some(0x0e), 427 | Code::KeyR => Some(0x0f), 428 | Code::KeyY => Some(0x10), 429 | Code::KeyT => Some(0x11), 430 | Code::Digit1 => Some(0x12), 431 | Code::Digit2 => Some(0x13), 432 | Code::Digit3 => Some(0x14), 433 | Code::Digit4 => Some(0x15), 434 | Code::Digit6 => Some(0x16), 435 | Code::Digit5 => Some(0x17), 436 | Code::Equal => Some(0x18), 437 | Code::Digit9 => Some(0x19), 438 | Code::Digit7 => Some(0x1a), 439 | Code::Minus => Some(0x1b), 440 | Code::Digit8 => Some(0x1c), 441 | Code::Digit0 => Some(0x1d), 442 | Code::BracketRight => Some(0x1e), 443 | Code::KeyO => Some(0x1f), 444 | Code::KeyU => Some(0x20), 445 | Code::BracketLeft => Some(0x21), 446 | Code::KeyI => Some(0x22), 447 | Code::KeyP => Some(0x23), 448 | Code::Enter => Some(0x24), 449 | Code::KeyL => Some(0x25), 450 | Code::KeyJ => Some(0x26), 451 | Code::Quote => Some(0x27), 452 | Code::KeyK => Some(0x28), 453 | Code::Semicolon => Some(0x29), 454 | Code::Backslash => Some(0x2a), 455 | Code::Comma => Some(0x2b), 456 | Code::Slash => Some(0x2c), 457 | Code::KeyN => Some(0x2d), 458 | Code::KeyM => Some(0x2e), 459 | Code::Period => Some(0x2f), 460 | Code::Tab => Some(0x30), 461 | Code::Space => Some(0x31), 462 | Code::Backquote => Some(0x32), 463 | Code::Backspace => Some(0x33), 464 | Code::Escape => Some(0x35), 465 | Code::F17 => Some(0x40), 466 | Code::NumpadDecimal => Some(0x41), 467 | Code::NumpadMultiply => Some(0x43), 468 | Code::NumpadAdd => Some(0x45), 469 | Code::NumLock => Some(0x47), 470 | Code::AudioVolumeUp => Some(0x48), 471 | Code::AudioVolumeDown => Some(0x49), 472 | Code::AudioVolumeMute => Some(0x4a), 473 | Code::NumpadDivide => Some(0x4b), 474 | Code::NumpadEnter => Some(0x4c), 475 | Code::NumpadSubtract => Some(0x4e), 476 | Code::F18 => Some(0x4f), 477 | Code::F19 => Some(0x50), 478 | Code::NumpadEqual => Some(0x51), 479 | Code::Numpad0 => Some(0x52), 480 | Code::Numpad1 => Some(0x53), 481 | Code::Numpad2 => Some(0x54), 482 | Code::Numpad3 => Some(0x55), 483 | Code::Numpad4 => Some(0x56), 484 | Code::Numpad5 => Some(0x57), 485 | Code::Numpad6 => Some(0x58), 486 | Code::Numpad7 => Some(0x59), 487 | Code::F20 => Some(0x5a), 488 | Code::Numpad8 => Some(0x5b), 489 | Code::Numpad9 => Some(0x5c), 490 | Code::F5 => Some(0x60), 491 | Code::F6 => Some(0x61), 492 | Code::F7 => Some(0x62), 493 | Code::F3 => Some(0x63), 494 | Code::F8 => Some(0x64), 495 | Code::F9 => Some(0x65), 496 | Code::F11 => Some(0x67), 497 | Code::F13 => Some(0x69), 498 | Code::F16 => Some(0x6a), 499 | Code::F14 => Some(0x6b), 500 | Code::F10 => Some(0x6d), 501 | Code::F12 => Some(0x6f), 502 | Code::F15 => Some(0x71), 503 | Code::Insert => Some(0x72), 504 | Code::Home => Some(0x73), 505 | Code::PageUp => Some(0x74), 506 | Code::Delete => Some(0x75), 507 | Code::F4 => Some(0x76), 508 | Code::End => Some(0x77), 509 | Code::F2 => Some(0x78), 510 | Code::PageDown => Some(0x79), 511 | Code::F1 => Some(0x7a), 512 | Code::ArrowLeft => Some(0x7b), 513 | Code::ArrowRight => Some(0x7c), 514 | Code::ArrowDown => Some(0x7d), 515 | Code::ArrowUp => Some(0x7e), 516 | Code::CapsLock => Some(0x39), 517 | Code::PrintScreen => Some(0x46), 518 | _ => None, 519 | } 520 | } 521 | 522 | fn is_media_key(code: Code) -> bool { 523 | matches!( 524 | code, 525 | Code::MediaPlayPause 526 | | Code::MediaTrackNext 527 | | Code::MediaTrackPrevious 528 | | Code::MediaFastForward 529 | | Code::MediaRewind 530 | ) 531 | } 532 | -------------------------------------------------------------------------------- /src/platform_impl/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #[cfg(target_os = "windows")] 6 | #[path = "windows/mod.rs"] 7 | mod platform; 8 | #[cfg(any( 9 | target_os = "linux", 10 | target_os = "dragonfly", 11 | target_os = "freebsd", 12 | target_os = "openbsd", 13 | target_os = "netbsd" 14 | ))] 15 | #[path = "x11/mod.rs"] 16 | mod platform; 17 | #[cfg(target_os = "macos")] 18 | #[path = "macos/mod.rs"] 19 | mod platform; 20 | 21 | #[cfg(not(any( 22 | target_os = "windows", 23 | target_os = "linux", 24 | target_os = "dragonfly", 25 | target_os = "freebsd", 26 | target_os = "openbsd", 27 | target_os = "netbsd", 28 | target_os = "macos" 29 | )))] 30 | #[path = "no-op.rs"] 31 | mod platform; 32 | 33 | pub(crate) use self::platform::*; 34 | -------------------------------------------------------------------------------- /src/platform_impl/no-op.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 5 | // SPDX-License-Identifier: Apache-2.0 6 | // SPDX-License-Identifier: MIT 7 | 8 | use crate::hotkey::HotKey; 9 | 10 | pub struct GlobalHotKeyManager {} 11 | 12 | impl GlobalHotKeyManager { 13 | pub fn new() -> crate::Result { 14 | Ok(Self {}) 15 | } 16 | 17 | pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { 18 | Ok(()) 19 | } 20 | 21 | pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { 22 | Ok(()) 23 | } 24 | 25 | pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 26 | for hotkey in hotkeys { 27 | self.register(*hotkey)?; 28 | } 29 | Ok(()) 30 | } 31 | 32 | pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 33 | for hotkey in hotkeys { 34 | self.unregister(*hotkey)?; 35 | } 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/platform_impl/windows/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::ptr; 6 | 7 | use keyboard_types::{Code, Modifiers}; 8 | use windows_sys::Win32::{ 9 | Foundation::{ERROR_HOTKEY_ALREADY_REGISTERED, HWND, LPARAM, LRESULT, WIN32_ERROR, WPARAM}, 10 | UI::{ 11 | Input::KeyboardAndMouse::*, 12 | WindowsAndMessaging::{ 13 | CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW, CW_USEDEFAULT, 14 | WM_HOTKEY, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, 15 | WS_EX_TRANSPARENT, WS_OVERLAPPED, 16 | }, 17 | }, 18 | }; 19 | 20 | use crate::{hotkey::HotKey, GlobalHotKeyEvent}; 21 | 22 | pub struct GlobalHotKeyManager { 23 | hwnd: HWND, 24 | } 25 | 26 | impl Drop for GlobalHotKeyManager { 27 | fn drop(&mut self) { 28 | unsafe { DestroyWindow(self.hwnd) }; 29 | } 30 | } 31 | 32 | impl GlobalHotKeyManager { 33 | pub fn new() -> crate::Result { 34 | let class_name = encode_wide("global_hotkey_app"); 35 | unsafe { 36 | let hinstance = get_instance_handle(); 37 | 38 | let wnd_class = WNDCLASSW { 39 | lpfnWndProc: Some(global_hotkey_proc), 40 | lpszClassName: class_name.as_ptr(), 41 | hInstance: hinstance, 42 | ..std::mem::zeroed() 43 | }; 44 | 45 | RegisterClassW(&wnd_class); 46 | 47 | let hwnd = CreateWindowExW( 48 | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | 49 | // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which 50 | // we want to avoid. If you remove this style, this window won't show up in the 51 | // taskbar *initially*, but it can show up at some later point. This can sometimes 52 | // happen on its own after several hours have passed, although this has proven 53 | // difficult to reproduce. Alternatively, it can be manually triggered by killing 54 | // `explorer.exe` and then starting the process back up. 55 | // It is unclear why the bug is triggered by waiting for several hours. 56 | WS_EX_TOOLWINDOW, 57 | class_name.as_ptr(), 58 | ptr::null(), 59 | WS_OVERLAPPED, 60 | CW_USEDEFAULT, 61 | 0, 62 | CW_USEDEFAULT, 63 | 0, 64 | std::ptr::null_mut(), 65 | std::ptr::null_mut(), 66 | hinstance, 67 | std::ptr::null_mut(), 68 | ); 69 | if hwnd.is_null() { 70 | return Err(crate::Error::OsError(std::io::Error::last_os_error())); 71 | } 72 | 73 | Ok(Self { hwnd }) 74 | } 75 | } 76 | 77 | pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { 78 | let mut mods = MOD_NOREPEAT; 79 | if hotkey.mods.contains(Modifiers::SHIFT) { 80 | mods |= MOD_SHIFT; 81 | } 82 | if hotkey.mods.intersects(Modifiers::SUPER | Modifiers::META) { 83 | mods |= MOD_WIN; 84 | } 85 | if hotkey.mods.contains(Modifiers::ALT) { 86 | mods |= MOD_ALT; 87 | } 88 | if hotkey.mods.contains(Modifiers::CONTROL) { 89 | mods |= MOD_CONTROL; 90 | } 91 | 92 | // get key scan code 93 | match key_to_vk(&hotkey.key) { 94 | Some(vk_code) => { 95 | let result = 96 | unsafe { RegisterHotKey(self.hwnd, hotkey.id() as _, mods, vk_code as _) }; 97 | if result == 0 { 98 | let error = std::io::Error::last_os_error(); 99 | 100 | return match error.raw_os_error() { 101 | Some(raw_os_error) => { 102 | let win32error = WIN32_ERROR::try_from(raw_os_error); 103 | if let Ok(ERROR_HOTKEY_ALREADY_REGISTERED) = win32error { 104 | Err(crate::Error::AlreadyRegistered(hotkey)) 105 | } else { 106 | Err(crate::Error::OsError(error)) 107 | } 108 | } 109 | _ => Err(crate::Error::OsError(error)), 110 | }; 111 | } 112 | } 113 | _ => { 114 | return Err(crate::Error::FailedToRegister(format!( 115 | "Unknown VKCode for {}", 116 | hotkey.key 117 | ))) 118 | } 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { 125 | let result = unsafe { UnregisterHotKey(self.hwnd, hotkey.id() as _) }; 126 | if result == 0 { 127 | return Err(crate::Error::FailedToUnRegister(hotkey)); 128 | } 129 | Ok(()) 130 | } 131 | 132 | pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 133 | for hotkey in hotkeys { 134 | self.register(*hotkey)?; 135 | } 136 | Ok(()) 137 | } 138 | 139 | pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 140 | for hotkey in hotkeys { 141 | self.unregister(*hotkey)?; 142 | } 143 | Ok(()) 144 | } 145 | } 146 | unsafe extern "system" fn global_hotkey_proc( 147 | hwnd: HWND, 148 | msg: u32, 149 | wparam: WPARAM, 150 | lparam: LPARAM, 151 | ) -> LRESULT { 152 | if msg == WM_HOTKEY { 153 | GlobalHotKeyEvent::send(GlobalHotKeyEvent { 154 | id: wparam as _, 155 | state: crate::HotKeyState::Pressed, 156 | }); 157 | std::thread::spawn(move || loop { 158 | let state = GetAsyncKeyState(HIWORD(lparam as u32) as i32); 159 | if state == 0 { 160 | GlobalHotKeyEvent::send(GlobalHotKeyEvent { 161 | id: wparam as _, 162 | state: crate::HotKeyState::Released, 163 | }); 164 | break; 165 | } 166 | }); 167 | } 168 | 169 | DefWindowProcW(hwnd, msg, wparam, lparam) 170 | } 171 | 172 | #[inline(always)] 173 | #[allow(non_snake_case)] 174 | const fn HIWORD(x: u32) -> u16 { 175 | ((x >> 16) & 0xFFFF) as u16 176 | } 177 | 178 | pub fn encode_wide>(string: S) -> Vec { 179 | std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) 180 | .chain(std::iter::once(0)) 181 | .collect() 182 | } 183 | 184 | pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE { 185 | // Gets the instance handle by taking the address of the 186 | // pseudo-variable created by the microsoft linker: 187 | // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 188 | 189 | // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: 190 | // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance 191 | 192 | extern "C" { 193 | static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; 194 | } 195 | 196 | unsafe { &__ImageBase as *const _ as _ } 197 | } 198 | 199 | // used to build accelerators table from Key 200 | fn key_to_vk(key: &Code) -> Option { 201 | Some(match key { 202 | Code::KeyA => VK_A, 203 | Code::KeyB => VK_B, 204 | Code::KeyC => VK_C, 205 | Code::KeyD => VK_D, 206 | Code::KeyE => VK_E, 207 | Code::KeyF => VK_F, 208 | Code::KeyG => VK_G, 209 | Code::KeyH => VK_H, 210 | Code::KeyI => VK_I, 211 | Code::KeyJ => VK_J, 212 | Code::KeyK => VK_K, 213 | Code::KeyL => VK_L, 214 | Code::KeyM => VK_M, 215 | Code::KeyN => VK_N, 216 | Code::KeyO => VK_O, 217 | Code::KeyP => VK_P, 218 | Code::KeyQ => VK_Q, 219 | Code::KeyR => VK_R, 220 | Code::KeyS => VK_S, 221 | Code::KeyT => VK_T, 222 | Code::KeyU => VK_U, 223 | Code::KeyV => VK_V, 224 | Code::KeyW => VK_W, 225 | Code::KeyX => VK_X, 226 | Code::KeyY => VK_Y, 227 | Code::KeyZ => VK_Z, 228 | Code::Digit0 => VK_0, 229 | Code::Digit1 => VK_1, 230 | Code::Digit2 => VK_2, 231 | Code::Digit3 => VK_3, 232 | Code::Digit4 => VK_4, 233 | Code::Digit5 => VK_5, 234 | Code::Digit6 => VK_6, 235 | Code::Digit7 => VK_7, 236 | Code::Digit8 => VK_8, 237 | Code::Digit9 => VK_9, 238 | Code::Equal => VK_OEM_PLUS, 239 | Code::Comma => VK_OEM_COMMA, 240 | Code::Minus => VK_OEM_MINUS, 241 | Code::Period => VK_OEM_PERIOD, 242 | Code::Semicolon => VK_OEM_1, 243 | Code::Slash => VK_OEM_2, 244 | Code::Backquote => VK_OEM_3, 245 | Code::BracketLeft => VK_OEM_4, 246 | Code::Backslash => VK_OEM_5, 247 | Code::BracketRight => VK_OEM_6, 248 | Code::Quote => VK_OEM_7, 249 | Code::Backspace => VK_BACK, 250 | Code::Tab => VK_TAB, 251 | Code::Space => VK_SPACE, 252 | Code::Enter => VK_RETURN, 253 | Code::CapsLock => VK_CAPITAL, 254 | Code::Escape => VK_ESCAPE, 255 | Code::PageUp => VK_PRIOR, 256 | Code::PageDown => VK_NEXT, 257 | Code::End => VK_END, 258 | Code::Home => VK_HOME, 259 | Code::ArrowLeft => VK_LEFT, 260 | Code::ArrowUp => VK_UP, 261 | Code::ArrowRight => VK_RIGHT, 262 | Code::ArrowDown => VK_DOWN, 263 | Code::PrintScreen => VK_SNAPSHOT, 264 | Code::Insert => VK_INSERT, 265 | Code::Delete => VK_DELETE, 266 | Code::F1 => VK_F1, 267 | Code::F2 => VK_F2, 268 | Code::F3 => VK_F3, 269 | Code::F4 => VK_F4, 270 | Code::F5 => VK_F5, 271 | Code::F6 => VK_F6, 272 | Code::F7 => VK_F7, 273 | Code::F8 => VK_F8, 274 | Code::F9 => VK_F9, 275 | Code::F10 => VK_F10, 276 | Code::F11 => VK_F11, 277 | Code::F12 => VK_F12, 278 | Code::F13 => VK_F13, 279 | Code::F14 => VK_F14, 280 | Code::F15 => VK_F15, 281 | Code::F16 => VK_F16, 282 | Code::F17 => VK_F17, 283 | Code::F18 => VK_F18, 284 | Code::F19 => VK_F19, 285 | Code::F20 => VK_F20, 286 | Code::F21 => VK_F21, 287 | Code::F22 => VK_F22, 288 | Code::F23 => VK_F23, 289 | Code::F24 => VK_F24, 290 | Code::NumLock => VK_NUMLOCK, 291 | Code::Numpad0 => VK_NUMPAD0, 292 | Code::Numpad1 => VK_NUMPAD1, 293 | Code::Numpad2 => VK_NUMPAD2, 294 | Code::Numpad3 => VK_NUMPAD3, 295 | Code::Numpad4 => VK_NUMPAD4, 296 | Code::Numpad5 => VK_NUMPAD5, 297 | Code::Numpad6 => VK_NUMPAD6, 298 | Code::Numpad7 => VK_NUMPAD7, 299 | Code::Numpad8 => VK_NUMPAD8, 300 | Code::Numpad9 => VK_NUMPAD9, 301 | Code::NumpadAdd => VK_ADD, 302 | Code::NumpadDecimal => VK_DECIMAL, 303 | Code::NumpadDivide => VK_DIVIDE, 304 | Code::NumpadEnter => VK_RETURN, 305 | Code::NumpadEqual => VK_E, 306 | Code::NumpadMultiply => VK_MULTIPLY, 307 | Code::NumpadSubtract => VK_SUBTRACT, 308 | Code::ScrollLock => VK_SCROLL, 309 | Code::AudioVolumeDown => VK_VOLUME_DOWN, 310 | Code::AudioVolumeUp => VK_VOLUME_UP, 311 | Code::AudioVolumeMute => VK_VOLUME_MUTE, 312 | Code::MediaPlay => VK_PLAY, 313 | Code::MediaPause => VK_PAUSE, 314 | Code::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, 315 | Code::MediaStop => VK_MEDIA_STOP, 316 | Code::MediaTrackNext => VK_MEDIA_NEXT_TRACK, 317 | Code::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, 318 | Code::Pause => VK_PAUSE, 319 | _ => return None, 320 | }) 321 | } 322 | -------------------------------------------------------------------------------- /src/platform_impl/x11/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::collections::BTreeMap; 6 | 7 | use crossbeam_channel::{unbounded, Receiver, Sender}; 8 | use keyboard_types::{Code, Modifiers}; 9 | use x11rb::connection::Connection; 10 | use x11rb::errors::ReplyError; 11 | use x11rb::protocol::xproto::{ConnectionExt, GrabMode, KeyButMask, Keycode, ModMask, Window}; 12 | use x11rb::protocol::{xkb, ErrorKind, Event}; 13 | use x11rb::rust_connection::RustConnection; 14 | use xkeysym::RawKeysym; 15 | 16 | use crate::{hotkey::HotKey, Error, GlobalHotKeyEvent}; 17 | 18 | enum ThreadMessage { 19 | RegisterHotKey(HotKey, Sender>), 20 | RegisterHotKeys(Vec, Sender>), 21 | UnRegisterHotKey(HotKey, Sender>), 22 | UnRegisterHotKeys(Vec, Sender>), 23 | DropThread, 24 | } 25 | 26 | pub struct GlobalHotKeyManager { 27 | thread_tx: Sender, 28 | } 29 | 30 | impl GlobalHotKeyManager { 31 | pub fn new() -> crate::Result { 32 | let (thread_tx, thread_rx) = unbounded(); 33 | std::thread::spawn(|| { 34 | if let Err(_err) = events_processor(thread_rx) { 35 | #[cfg(feature = "tracing")] 36 | tracing::error!("{}", _err); 37 | } 38 | }); 39 | Ok(Self { thread_tx }) 40 | } 41 | 42 | pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { 43 | let (tx, rx) = crossbeam_channel::bounded(1); 44 | let _ = self 45 | .thread_tx 46 | .send(ThreadMessage::RegisterHotKey(hotkey, tx)); 47 | 48 | if let Ok(result) = rx.recv() { 49 | result?; 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { 56 | let (tx, rx) = crossbeam_channel::bounded(1); 57 | let _ = self 58 | .thread_tx 59 | .send(ThreadMessage::UnRegisterHotKey(hotkey, tx)); 60 | 61 | if let Ok(result) = rx.recv() { 62 | result?; 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 69 | let (tx, rx) = crossbeam_channel::bounded(1); 70 | let _ = self 71 | .thread_tx 72 | .send(ThreadMessage::RegisterHotKeys(hotkeys.to_vec(), tx)); 73 | 74 | if let Ok(result) = rx.recv() { 75 | result?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { 82 | let (tx, rx) = crossbeam_channel::bounded(1); 83 | let _ = self 84 | .thread_tx 85 | .send(ThreadMessage::UnRegisterHotKeys(hotkeys.to_vec(), tx)); 86 | 87 | if let Ok(result) = rx.recv() { 88 | result?; 89 | } 90 | 91 | Ok(()) 92 | } 93 | } 94 | 95 | impl Drop for GlobalHotKeyManager { 96 | fn drop(&mut self) { 97 | let _ = self.thread_tx.send(ThreadMessage::DropThread); 98 | } 99 | } 100 | 101 | // XGrabKey works only with the exact state (modifiers) 102 | // and since X11 considers NumLock, ScrollLock and CapsLock a modifier when it is ON, 103 | // we also need to register our shortcut combined with these extra modifiers as well 104 | fn ignored_mods() -> [ModMask; 4] { 105 | [ 106 | ModMask::default(), // modifier only 107 | ModMask::M2, // NumLock 108 | ModMask::LOCK, // CapsLock 109 | ModMask::M2 | ModMask::LOCK, 110 | ] 111 | } 112 | 113 | #[inline] 114 | fn register_hotkey( 115 | conn: &RustConnection, 116 | root: Window, 117 | hotkeys: &mut BTreeMap>, 118 | hotkey: HotKey, 119 | ) -> crate::Result<()> { 120 | let (mods, key) = ( 121 | modifiers_to_x11_mods(hotkey.mods), 122 | keycode_to_x11_keysym(hotkey.key), 123 | ); 124 | 125 | let Some(key) = key else { 126 | return Err(Error::FailedToRegister(format!( 127 | "Unknown scancode for key: {}", 128 | hotkey.key 129 | ))); 130 | }; 131 | 132 | let keycode = keysym_to_keycode(conn, key).map_err(Error::FailedToRegister)?; 133 | 134 | let Some(keycode) = keycode else { 135 | return Err(Error::FailedToRegister(format!( 136 | "Unable to find keycode for key: {}", 137 | hotkey.key 138 | ))); 139 | }; 140 | 141 | for m in ignored_mods() { 142 | let result = conn 143 | .grab_key( 144 | false, 145 | root, 146 | mods | m, 147 | keycode, 148 | GrabMode::ASYNC, 149 | GrabMode::ASYNC, 150 | ) 151 | .map_err(|err| Error::FailedToRegister(err.to_string()))?; 152 | 153 | if let Err(err) = result.check() { 154 | return match err { 155 | ReplyError::ConnectionError(err) => Err(Error::FailedToRegister(err.to_string())), 156 | ReplyError::X11Error(err) => { 157 | if let ErrorKind::Access = err.error_kind { 158 | for m in ignored_mods() { 159 | if let Ok(result) = conn.ungrab_key(keycode, root, mods | m) { 160 | result.ignore_error(); 161 | } 162 | } 163 | 164 | Err(Error::AlreadyRegistered(hotkey)) 165 | } else { 166 | Err(Error::FailedToRegister(format!("{err:?}"))) 167 | } 168 | } 169 | }; 170 | } 171 | } 172 | 173 | let entry = hotkeys.entry(keycode).or_default(); 174 | match entry.iter().find(|e| e.mods == mods) { 175 | None => { 176 | let state = HotKeyState { 177 | id: hotkey.id(), 178 | mods, 179 | pressed: false, 180 | }; 181 | entry.push(state); 182 | Ok(()) 183 | } 184 | Some(_) => Err(Error::AlreadyRegistered(hotkey)), 185 | } 186 | } 187 | 188 | #[inline] 189 | fn unregister_hotkey( 190 | conn: &RustConnection, 191 | root: Window, 192 | hotkeys: &mut BTreeMap>, 193 | hotkey: HotKey, 194 | ) -> crate::Result<()> { 195 | let (modifiers, key) = ( 196 | modifiers_to_x11_mods(hotkey.mods), 197 | keycode_to_x11_keysym(hotkey.key), 198 | ); 199 | 200 | let Some(key) = key else { 201 | return Err(Error::FailedToUnRegister(hotkey)); 202 | }; 203 | 204 | let keycode = keysym_to_keycode(conn, key).map_err(|_err| Error::FailedToUnRegister(hotkey))?; 205 | 206 | let Some(keycode) = keycode else { 207 | return Err(Error::FailedToUnRegister(hotkey)); 208 | }; 209 | 210 | for m in ignored_mods() { 211 | if let Ok(result) = conn.ungrab_key(keycode, root, modifiers | m) { 212 | result.ignore_error(); 213 | } 214 | } 215 | 216 | let entry = hotkeys.entry(keycode).or_default(); 217 | entry.retain(|k| k.mods != modifiers); 218 | Ok(()) 219 | } 220 | 221 | struct HotKeyState { 222 | id: u32, 223 | pressed: bool, 224 | mods: ModMask, 225 | } 226 | 227 | fn events_processor(thread_rx: Receiver) -> Result<(), String> { 228 | let mut hotkeys = BTreeMap::>::new(); 229 | 230 | let (conn, screen) = RustConnection::connect(None) 231 | .map_err(|err| format!("Unable to open x11 connection, maybe you are not running under X11? Other window systems on Linux are not supported by `global-hotkey` crate: {err}"))?; 232 | 233 | xkb::ConnectionExt::xkb_use_extension(&conn, 1, 0) 234 | .map_err(|err| format!("Unable to send xkb_use_extension request to x11 server: {err}"))? 235 | .reply() 236 | .map_err(|err| format!("xkb_use_extension request to x11 server has failed: {err}"))?; 237 | 238 | xkb::ConnectionExt::xkb_per_client_flags( 239 | &conn, 240 | xkb::ID::USE_CORE_KBD.into(), 241 | xkb::PerClientFlag::DETECTABLE_AUTO_REPEAT, 242 | xkb::PerClientFlag::DETECTABLE_AUTO_REPEAT, 243 | Default::default(), 244 | Default::default(), 245 | Default::default(), 246 | ) 247 | .map_err(|err| format!("Unable to send xkb_per_client_flags request to x11 server: {err}"))? 248 | .reply() 249 | .map_err(|err| format!("xkb_per_client_flags request to x11 server has failed: {err}"))?; 250 | 251 | let root = conn.setup().roots[screen].root; 252 | 253 | // X11 sends masks for Lock keys as well, and we only care about the 4 below 254 | let full_mask = KeyButMask::CONTROL | KeyButMask::SHIFT | KeyButMask::MOD4 | KeyButMask::MOD1; 255 | 256 | loop { 257 | while let Ok(Some(event)) = conn.poll_for_event() { 258 | match event { 259 | Event::KeyPress(event) => { 260 | let keycode = event.detail; 261 | 262 | let event_mods = event.state & full_mask; 263 | let event_mods = ModMask::from(event_mods.bits()); 264 | 265 | if let Some(entry) = hotkeys.get_mut(&keycode) { 266 | for state in entry { 267 | if event_mods == state.mods && !state.pressed { 268 | GlobalHotKeyEvent::send(GlobalHotKeyEvent { 269 | id: state.id, 270 | state: crate::HotKeyState::Pressed, 271 | }); 272 | state.pressed = true; 273 | } 274 | } 275 | } 276 | } 277 | Event::KeyRelease(event) => { 278 | let keycode = event.detail; 279 | 280 | if let Some(entry) = hotkeys.get_mut(&keycode) { 281 | for state in entry { 282 | if state.pressed { 283 | GlobalHotKeyEvent::send(GlobalHotKeyEvent { 284 | id: state.id, 285 | state: crate::HotKeyState::Released, 286 | }); 287 | state.pressed = false; 288 | } 289 | } 290 | } 291 | } 292 | _ => {} 293 | } 294 | } 295 | 296 | if let Ok(msg) = thread_rx.try_recv() { 297 | match msg { 298 | ThreadMessage::RegisterHotKey(hotkey, tx) => { 299 | let _ = tx.send(register_hotkey(&conn, root, &mut hotkeys, hotkey)); 300 | } 301 | ThreadMessage::RegisterHotKeys(keys, tx) => { 302 | for hotkey in keys { 303 | if let Err(e) = register_hotkey(&conn, root, &mut hotkeys, hotkey) { 304 | let _ = tx.send(Err(e)); 305 | } 306 | } 307 | let _ = tx.send(Ok(())); 308 | } 309 | ThreadMessage::UnRegisterHotKey(hotkey, tx) => { 310 | let _ = tx.send(unregister_hotkey(&conn, root, &mut hotkeys, hotkey)); 311 | } 312 | ThreadMessage::UnRegisterHotKeys(keys, tx) => { 313 | for hotkey in keys { 314 | if let Err(e) = unregister_hotkey(&conn, root, &mut hotkeys, hotkey) { 315 | let _ = tx.send(Err(e)); 316 | } 317 | } 318 | let _ = tx.send(Ok(())); 319 | } 320 | ThreadMessage::DropThread => { 321 | return Ok(()); 322 | } 323 | } 324 | } 325 | 326 | std::thread::sleep(std::time::Duration::from_millis(50)); 327 | } 328 | } 329 | 330 | fn keycode_to_x11_keysym(key: Code) -> Option { 331 | Some(match key { 332 | Code::KeyA => xkeysym::key::A, 333 | Code::KeyB => xkeysym::key::B, 334 | Code::KeyC => xkeysym::key::C, 335 | Code::KeyD => xkeysym::key::D, 336 | Code::KeyE => xkeysym::key::E, 337 | Code::KeyF => xkeysym::key::F, 338 | Code::KeyG => xkeysym::key::G, 339 | Code::KeyH => xkeysym::key::H, 340 | Code::KeyI => xkeysym::key::I, 341 | Code::KeyJ => xkeysym::key::J, 342 | Code::KeyK => xkeysym::key::K, 343 | Code::KeyL => xkeysym::key::L, 344 | Code::KeyM => xkeysym::key::M, 345 | Code::KeyN => xkeysym::key::N, 346 | Code::KeyO => xkeysym::key::O, 347 | Code::KeyP => xkeysym::key::P, 348 | Code::KeyQ => xkeysym::key::Q, 349 | Code::KeyR => xkeysym::key::R, 350 | Code::KeyS => xkeysym::key::S, 351 | Code::KeyT => xkeysym::key::T, 352 | Code::KeyU => xkeysym::key::U, 353 | Code::KeyV => xkeysym::key::V, 354 | Code::KeyW => xkeysym::key::W, 355 | Code::KeyX => xkeysym::key::X, 356 | Code::KeyY => xkeysym::key::Y, 357 | Code::KeyZ => xkeysym::key::Z, 358 | Code::Backslash => xkeysym::key::backslash, 359 | Code::BracketLeft => xkeysym::key::bracketleft, 360 | Code::BracketRight => xkeysym::key::bracketright, 361 | Code::Backquote => xkeysym::key::quoteleft, 362 | Code::Comma => xkeysym::key::comma, 363 | Code::Digit0 => xkeysym::key::_0, 364 | Code::Digit1 => xkeysym::key::_1, 365 | Code::Digit2 => xkeysym::key::_2, 366 | Code::Digit3 => xkeysym::key::_3, 367 | Code::Digit4 => xkeysym::key::_4, 368 | Code::Digit5 => xkeysym::key::_5, 369 | Code::Digit6 => xkeysym::key::_6, 370 | Code::Digit7 => xkeysym::key::_7, 371 | Code::Digit8 => xkeysym::key::_8, 372 | Code::Digit9 => xkeysym::key::_9, 373 | Code::Equal => xkeysym::key::equal, 374 | Code::Minus => xkeysym::key::minus, 375 | Code::Period => xkeysym::key::period, 376 | Code::Quote => xkeysym::key::leftsinglequotemark, 377 | Code::Semicolon => xkeysym::key::semicolon, 378 | Code::Slash => xkeysym::key::slash, 379 | Code::Backspace => xkeysym::key::BackSpace, 380 | Code::CapsLock => xkeysym::key::Caps_Lock, 381 | Code::Enter => xkeysym::key::Return, 382 | Code::Space => xkeysym::key::space, 383 | Code::Tab => xkeysym::key::Tab, 384 | Code::Delete => xkeysym::key::Delete, 385 | Code::End => xkeysym::key::End, 386 | Code::Home => xkeysym::key::Home, 387 | Code::Insert => xkeysym::key::Insert, 388 | Code::PageDown => xkeysym::key::Page_Down, 389 | Code::PageUp => xkeysym::key::Page_Up, 390 | Code::ArrowDown => xkeysym::key::Down, 391 | Code::ArrowLeft => xkeysym::key::Left, 392 | Code::ArrowRight => xkeysym::key::Right, 393 | Code::ArrowUp => xkeysym::key::Up, 394 | Code::Numpad0 => xkeysym::key::KP_0, 395 | Code::Numpad1 => xkeysym::key::KP_1, 396 | Code::Numpad2 => xkeysym::key::KP_2, 397 | Code::Numpad3 => xkeysym::key::KP_3, 398 | Code::Numpad4 => xkeysym::key::KP_4, 399 | Code::Numpad5 => xkeysym::key::KP_5, 400 | Code::Numpad6 => xkeysym::key::KP_6, 401 | Code::Numpad7 => xkeysym::key::KP_7, 402 | Code::Numpad8 => xkeysym::key::KP_8, 403 | Code::Numpad9 => xkeysym::key::KP_9, 404 | Code::NumpadAdd => xkeysym::key::KP_Add, 405 | Code::NumpadDecimal => xkeysym::key::KP_Decimal, 406 | Code::NumpadDivide => xkeysym::key::KP_Divide, 407 | Code::NumpadMultiply => xkeysym::key::KP_Multiply, 408 | Code::NumpadSubtract => xkeysym::key::KP_Subtract, 409 | Code::Escape => xkeysym::key::Escape, 410 | Code::PrintScreen => xkeysym::key::Print, 411 | Code::ScrollLock => xkeysym::key::Scroll_Lock, 412 | Code::NumLock => xkeysym::key::F1, 413 | Code::F1 => xkeysym::key::F1, 414 | Code::F2 => xkeysym::key::F2, 415 | Code::F3 => xkeysym::key::F3, 416 | Code::F4 => xkeysym::key::F4, 417 | Code::F5 => xkeysym::key::F5, 418 | Code::F6 => xkeysym::key::F6, 419 | Code::F7 => xkeysym::key::F7, 420 | Code::F8 => xkeysym::key::F8, 421 | Code::F9 => xkeysym::key::F9, 422 | Code::F10 => xkeysym::key::F10, 423 | Code::F11 => xkeysym::key::F11, 424 | Code::F12 => xkeysym::key::F12, 425 | Code::AudioVolumeDown => xkeysym::key::XF86_AudioLowerVolume, 426 | Code::AudioVolumeMute => xkeysym::key::XF86_AudioMute, 427 | Code::AudioVolumeUp => xkeysym::key::XF86_AudioRaiseVolume, 428 | Code::MediaPlay => xkeysym::key::XF86_AudioPlay, 429 | Code::MediaPause => xkeysym::key::XF86_AudioPause, 430 | Code::MediaStop => xkeysym::key::XF86_AudioStop, 431 | Code::MediaTrackNext => xkeysym::key::XF86_AudioNext, 432 | Code::MediaTrackPrevious => xkeysym::key::XF86_AudioPrev, 433 | Code::Pause => xkeysym::key::Pause, 434 | _ => return None, 435 | }) 436 | } 437 | 438 | fn modifiers_to_x11_mods(modifiers: Modifiers) -> ModMask { 439 | let mut x11mods = ModMask::default(); 440 | if modifiers.contains(Modifiers::SHIFT) { 441 | x11mods |= ModMask::SHIFT; 442 | } 443 | if modifiers.intersects(Modifiers::SUPER | Modifiers::META) { 444 | x11mods |= ModMask::M4; 445 | } 446 | if modifiers.contains(Modifiers::ALT) { 447 | x11mods |= ModMask::M1; 448 | } 449 | if modifiers.contains(Modifiers::CONTROL) { 450 | x11mods |= ModMask::CONTROL; 451 | } 452 | x11mods 453 | } 454 | 455 | fn keysym_to_keycode(conn: &RustConnection, keysym: RawKeysym) -> Result, String> { 456 | let setup = conn.setup(); 457 | let min_keycode = setup.min_keycode; 458 | let max_keycode = setup.max_keycode; 459 | let count = max_keycode - min_keycode + 1; 460 | 461 | let mapping = conn 462 | .get_keyboard_mapping(min_keycode, count) 463 | .map_err(|err| err.to_string())? 464 | .reply() 465 | .map_err(|err| err.to_string())?; 466 | 467 | let keysyms_per_keycode = mapping.keysyms_per_keycode as usize; 468 | 469 | for (i, keysyms) in mapping.keysyms.chunks(keysyms_per_keycode).enumerate() { 470 | if keysyms.contains(&keysym) { 471 | return Ok(Some(min_keycode + i as u8)); 472 | } 473 | } 474 | 475 | Ok(None) 476 | } 477 | --------------------------------------------------------------------------------