diff --git a/Cargo.lock b/Cargo.lock index 43f9384..ce957ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,15 @@ dependencies = [ "winit 0.29.15", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.0" @@ -101,7 +110,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -113,7 +122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -242,6 +251,23 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "serde", + "serde_repr", + "tokio", + "url", + "zbus 5.9.0", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -354,7 +380,7 @@ dependencies = [ "polling 2.8.0", "rustix 0.37.28", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] @@ -594,6 +620,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -2004,9 +2045,27 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.5+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gio" version = "0.18.4" @@ -2107,10 +2166,13 @@ dependencies = [ name = "global-hotkey" version = "0.7.0" dependencies = [ + "ashpd", "async-std", "crossbeam-channel", "eframe", + "futures", "iced", + "itertools", "keyboard-types", "objc2 0.6.0", "objc2-app-kit 0.3.0", @@ -2118,6 +2180,7 @@ dependencies = [ "serde", "tao", "thiserror 2.0.11", + "tokio", "tracing", "windows-sys 0.59.0", "winit 0.30.9", @@ -2805,6 +2868,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "libc", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "jni" version = "0.21.1" @@ -2901,9 +2984,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -3064,6 +3147,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "0.19.2" @@ -3164,6 +3258,19 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -3515,6 +3622,15 @@ dependencies = [ "cc", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -3691,7 +3807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -3909,6 +4025,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -3916,8 +4038,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -3927,7 +4059,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -3936,7 +4078,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] @@ -4035,7 +4186,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -4091,6 +4242,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4413,6 +4570,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -4608,7 +4775,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand 2.3.0", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix 0.38.44", "windows-sys 0.59.0", @@ -4727,6 +4894,37 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "toml" version = "0.8.2" @@ -4927,6 +5125,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4981,6 +5180,24 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.5+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5949,6 +6166,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wit-bindgen" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" + [[package]] name = "write16" version = "1.0.0" @@ -6101,7 +6324,7 @@ dependencies = [ "nix 0.26.4", "once_cell", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6139,7 +6362,7 @@ dependencies = [ "hex", "nix 0.29.0", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6153,6 +6376,34 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zbus" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +dependencies = [ + "async-broadcast 0.7.2", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-lite 2.6.0", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.6", + "zbus_macros 5.11.0", + "zbus_names 4.2.0", + "zvariant 5.7.0", +] + [[package]] name = "zbus_macros" version = "3.15.2" @@ -6180,6 +6431,21 @@ dependencies = [ "zvariant_utils 2.1.0", ] +[[package]] +name = "zbus_macros" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.96", + "zbus_names 4.2.0", + "zvariant 5.7.0", + "zvariant_utils 3.2.1", +] + [[package]] name = "zbus_names" version = "2.6.1" @@ -6202,6 +6468,18 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.6", + "zvariant 5.7.0", +] + [[package]] name = "zeno" version = "0.2.3" @@ -6299,6 +6577,21 @@ dependencies = [ "zvariant_derive 4.2.0", ] +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.6", + "zvariant_derive 5.7.0", + "zvariant_utils 3.2.1", +] + [[package]] name = "zvariant_derive" version = "3.15.2" @@ -6325,6 +6618,19 @@ dependencies = [ "zvariant_utils 2.1.0", ] +[[package]] +name = "zvariant_derive" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.96", + "zvariant_utils 3.2.1", +] + [[package]] name = "zvariant_utils" version = "1.0.1" @@ -6346,3 +6652,16 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.96", + "winnow 0.7.6", +] diff --git a/Cargo.toml b/Cargo.toml index 20c57ef..ade562a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" repository = "https://github.com/amrbashir/global-hotkey" documentation = "https://docs.rs/global-hotkey" categories = ["gui"] -rust-version = "1.71" +rust-version = "1.77" [features] serde = ["dep:serde"] @@ -22,6 +22,7 @@ once_cell = "1" thiserror = "2" serde = { version = "1", optional = true, features = ["derive"] } tracing = { version = "0.1", optional = true } +itertools = "0.14.0" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.0" @@ -44,6 +45,9 @@ features = [ [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] x11rb = { version = "0.13.1", features = ["xkb"] } xkeysym = "0.2.1" +ashpd = "0.12.0" +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } +futures = "0.3.31" [dev-dependencies] winit = "0.30" diff --git a/README.md b/README.md index b276a5f..2833ddd 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ global_hotkey lets you register Global HotKeys for Desktop Applications. - Windows - macOS -- Linux (X11 Only) +- Linux (X11/Wayland) ## Platform-specific notes: - 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. - 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. +- Global HotKeys work differently on Linux/Wayland. See the [wayland](https://docs.rs/global-hotkey/latest/global_hotkey/wayland/index.html) module for more details. + ## Example diff --git a/src/lib.rs b/src/lib.rs index 4e04181..d4674ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,12 +10,13 @@ //! //! - Windows //! - macOS -//! - Linux (X11 Only) +//! - Linux (X11/Wayland) //! //! ## Platform-specific notes: //! //! - 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. //! - 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. +//! - Global HotKeys work differently on Linux/Wayland. See the [`wayland`] module for more details. //! //! # Example //! @@ -55,7 +56,15 @@ use once_cell::sync::{Lazy, OnceCell}; mod error; pub mod hotkey; +pub(crate) mod macros; mod platform_impl; +pub mod wayland; + +use crate::macros::not_on_linux_cfg; +use crate::macros::on_linux; +use crate::macros::on_linux_cfg; +use crate::wayland::WlHotKeyAction; +use crate::wayland::WlNewHotKeyAction; pub use self::error::*; use hotkey::HotKey; @@ -160,4 +169,88 @@ impl GlobalHotKeyManager { self.platform_impl.unregister_all(hotkeys)?; Ok(()) } + + /// Register a set of hotkey actions on Wayland. + /// + /// # Arguments + /// + /// * `app_id` - a constant string to identify the application. See the [official GNOME + /// documentation](https://developer.gnome.org/documentation/tutorials/application-id.html) for + /// more details about how to create an app id. This app id should correspond to the base + /// name of the .desktop file for your app, installed in a standard location (e.g. + /// `~/.local/share/applications/`). + /// + /// If registering the app id fails, a warning will be thrown using the `tracing` library (if + /// the `tracing` feature is enabled). This warning may be ignored in sandboxed applications + /// (e.g. Flatpaks). + /// + /// This argument will be ignored after the first call to this function. + /// + /// * `hotkeys` - a list of hotkey actions to register. Ideally, you should register all of + /// your application's actions in one call to this function. + /// + /// See the [`wayland`] module for more information about how to register hotkeys on Wayland. + /// + /// ## Note + /// + /// This function has no effect if the user is not using Wayland. + pub fn wl_register_all( + &self, + app_id: impl Into, + hotkeys: &[WlNewHotKeyAction], + ) -> crate::Result<()> { + self.wl_register_all_impl(app_id, hotkeys) + } + + /// Unregister a set of hotkey actions on Wayland. + /// + /// # Arguments + /// + /// * `hotkey_action_ids` - a list of ids corresponding to actions previously registered with + /// [`GlobalHotKeyManager::wl_register_all`]. + /// + /// ## Note + /// + /// This doesn't necessarily delete the specified actions from the user's system's settings; it + /// just prevents any more events from being received from them. + /// + /// This function has no effect if the user is not using Wayland. + pub fn wl_unregister_all(&self, hotkey_action_ids: &[u32]) { + self.wl_unregister_all_impl(hotkey_action_ids) + } + + on_linux_cfg! { + fn wl_register_all_impl(&self, app_id: impl Into, hotkeys: &[WlNewHotKeyAction]) -> crate::Result<()> { + self.platform_impl.wl_register_all(app_id, hotkeys)?; + Ok(()) + } + } + + not_on_linux_cfg! { + fn wl_register_all_impl(&self, _app_id: impl Into, _hotkeys: &[WlNewHotKeyAction]) -> crate::Result<()> { + Ok(()) + } + } + + on_linux_cfg! { + fn wl_unregister_all_impl(&self, hotkey_action_ids: &[u32]) { + self.platform_impl.wl_unregister_all(hotkey_action_ids); + } + } + + not_on_linux_cfg! { + fn wl_unregister_all_impl(&self, _hotkey_action_ids: &[u32]) {} + } + + on_linux_cfg! { + pub fn wl_get_hotkeys(&self) -> Box<[WlHotKeyAction]> { + self.platform_impl.wl_get_hotkeys() + } + } + + not_on_linux_cfg! { + pub fn wl_get_hotkeys(&self) -> Box<[WlHotKeyAction]> { + Box::new([]) + } + } } diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..62020c1 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,44 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +macro_rules! on_linux { + () => { + cfg!(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + )) + }; +} +macro_rules! on_linux_cfg { + ($i:item) => { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + ))] + $i + }; +} + +macro_rules! not_on_linux_cfg { + ($x:item) => { + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + )))] + $x + }; +} + +pub(crate) use not_on_linux_cfg; +pub(crate) use on_linux; +pub(crate) use on_linux_cfg; diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs new file mode 100644 index 0000000..c996101 --- /dev/null +++ b/src/platform_impl/linux/mod.rs @@ -0,0 +1,142 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crossbeam_channel::{unbounded, Sender}; + +use crate::{ + hotkey::HotKey, + wayland::{using_wayland, WlHotKeyAction, WlNewHotKeyAction}, +}; + +mod wayland; +mod x11; + +pub(crate) use wayland::wl_hotkeys_changed_receiver; + +enum ThreadMessage { + WlRegisterHotKeys(Vec, String, Sender>), + WlUnRegisterHotKeys(Vec), + WlGetHotKeys(Sender>), + + RegisterHotKey(HotKey, Sender>), + RegisterHotKeys(Vec, Sender>), + UnRegisterHotKey(HotKey, Sender>), + UnRegisterHotKeys(Vec, Sender>), + DropThread, +} + +pub struct GlobalHotKeyManager { + thread_tx: Sender, +} + +impl GlobalHotKeyManager { + pub fn new() -> crate::Result { + let (thread_tx, thread_rx) = unbounded(); + std::thread::spawn(|| { + if let Err(_err) = if using_wayland() { + wayland::events_processor(thread_rx) + } else { + x11::events_processor(thread_rx) + } { + #[cfg(feature = "tracing")] + tracing::error!("{}", _err); + } + }); + Ok(Self { thread_tx }) + } + + pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self + .thread_tx + .send(ThreadMessage::RegisterHotKey(hotkey, tx)); + + if let Ok(result) = rx.recv() { + result?; + } + + Ok(()) + } + + pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self + .thread_tx + .send(ThreadMessage::UnRegisterHotKey(hotkey, tx)); + + if let Ok(result) = rx.recv() { + result?; + } + + Ok(()) + } + + pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self + .thread_tx + .send(ThreadMessage::RegisterHotKeys(hotkeys.to_vec(), tx)); + + if let Ok(result) = rx.recv() { + result?; + } + + Ok(()) + } + + pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self + .thread_tx + .send(ThreadMessage::UnRegisterHotKeys(hotkeys.to_vec(), tx)); + + if let Ok(result) = rx.recv() { + result?; + } + + Ok(()) + } + + pub fn wl_register_all( + &self, + app_id: impl Into, + hotkeys: &[WlNewHotKeyAction], + ) -> crate::Result<()> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self.thread_tx.send(ThreadMessage::WlRegisterHotKeys( + hotkeys.to_vec(), + app_id.into(), + tx, + )); + + if let Ok(result) = rx.recv() { + result?; + } + + Ok(()) + } + + pub fn wl_unregister_all(&self, ids: &[u32]) { + let _ = self + .thread_tx + .send(ThreadMessage::WlUnRegisterHotKeys(ids.to_vec())); + } + + pub fn wl_get_hotkeys(&self) -> Box<[WlHotKeyAction]> { + let (tx, rx) = crossbeam_channel::bounded(1); + let _ = self.thread_tx.send(ThreadMessage::WlGetHotKeys(tx)); + + if let Ok(result) = rx.recv() { + result + } else { + Box::new([]) + } + } +} + +impl Drop for GlobalHotKeyManager { + fn drop(&mut self) { + let _ = self.thread_tx.send(ThreadMessage::DropThread); + } +} diff --git a/src/platform_impl/linux/wayland.rs b/src/platform_impl/linux/wayland.rs new file mode 100644 index 0000000..81b8383 --- /dev/null +++ b/src/platform_impl/linux/wayland.rs @@ -0,0 +1,559 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashMap, num::ParseIntError, str::FromStr}; + +use ashpd::{ + desktop::{ + global_shortcuts::{ + Activated, Deactivated, GlobalShortcuts, NewShortcut, Shortcut, ShortcutsChanged, + }, + Session, + }, + AppID, +}; +use crossbeam_channel::{bounded, unbounded, Receiver, Select, Sender}; +use futures::{stream::select_all, Stream, StreamExt}; +use itertools::Itertools; +use keyboard_types::{Code, Modifiers}; +use once_cell::sync::Lazy; + +use crate::{ + hotkey::HotKey, + platform_impl::platform::ThreadMessage, + wayland::{WlChangedHotKey, WlHotKeyAction, WlHotKeysChangedEvent, WlNewHotKeyAction}, + Error, GlobalHotKeyEvent, HotKeyState, +}; + +enum GSEvent { + Activated(Activated), + Deactivated(Deactivated), + Changed(ShortcutsChanged), +} + +struct GlobalShortcutsState<'a> { + proxy: GlobalShortcuts<'a>, + session: Session<'a, GlobalShortcuts<'a>>, +} + +impl GlobalShortcutsState<'_> { + pub async fn new( + app_id: impl Into, + event_sender: Sender, + ) -> Result { + match AppID::from_str(&app_id.into()) { + Ok(app_id) => { + if let Err(_e) = ashpd::register_host_app(app_id).await { + #[cfg(feature = "tracing")] + tracing::warn!("Failed to register app id: {:?}", _e); + } + } + Err(_e) => { + #[cfg(feature = "tracing")] + tracing::warn!("Failed to parse app id: {:?}", _e); + } + } + + let proxy = GlobalShortcuts::new() + .await + .map_err(|e| format!("Failed to start global shortcuts portal proxy: {e}"))?; + + let session = proxy + .create_session() + .await + .map_err(|e| format!("Failed to start global shortcuts portal session: {e}"))?; + + // combining the activated, deactivated, and shortcuts changed events into one stream + let mut gs_event_stream = Self::get_event_stream(&proxy).await?; + + // listening for global shortcuts events in a separate thread + tokio::spawn(async move { + while let Some(ev) = gs_event_stream.next().await { + let _ = event_sender.send(ev); + } + }); + + Ok(Self { proxy, session }) + } + + async fn get_event_stream( + proxy: &GlobalShortcuts<'_>, + ) -> Result + Unpin + Send>, String> { + let activated: Box + Unpin + Send> = Box::new( + proxy + .receive_activated() + .await + .map_err(|e| { + format!("Failed to receive global shortcuts portal activated stream: {e}") + })? + .map(GSEvent::Activated), + ); + let deactivated = Box::new( + proxy + .receive_deactivated() + .await + .map_err(|e| { + format!("Failed to receive global shortcuts portal deactivated stream: {e}") + })? + .map(GSEvent::Deactivated), + ); + let changed = Box::new( + proxy + .receive_shortcuts_changed() + .await + .map_err(|e| { + format!( + "Failed to receive global shortcuts portal shortcuts changed stream: {e}" + ) + })? + .map(GSEvent::Changed), + ); + + Ok(Box::new(select_all([activated, deactivated, changed]))) + } +} + +#[tokio::main] +pub async fn events_processor(thread_rx: Receiver) -> Result<(), String> { + let mut registered_hotkeys = Vec::::new(); + let mut hotkey_states = HashMap::::new(); + + let (gs_event_sender, gs_event_receiver) = unbounded(); + let mut gs_state: Option = None; + + let mut select = Select::new(); + let thread_rx_idx = select.recv(&thread_rx); + let gs_rx_idx = select.recv(&gs_event_receiver); + loop { + let selected_oper = select.select(); + match selected_oper.index() { + i if i == thread_rx_idx => match selected_oper.recv(&thread_rx) { + Ok(ThreadMessage::WlRegisterHotKeys(hotkeys, app_id, tx)) => { + if let Some(gs) = &mut gs_state { + let _ = tx + .send(reregister_hotkeys(gs, &mut registered_hotkeys, &hotkeys).await); + } else { + let _ = match GlobalShortcutsState::new(app_id, gs_event_sender.clone()) + .await + { + Ok(mut new_gs) => { + let res = tx.send( + reregister_hotkeys( + &mut new_gs, + &mut registered_hotkeys, + &hotkeys, + ) + .await, + ); + gs_state = Some(new_gs); + res + } + Err(e) => tx.send(Err(Error::FailedToRegister(e))), + }; + } + } + Ok(ThreadMessage::WlUnRegisterHotKeys(ids)) => { + registered_hotkeys.retain(|rh| !ids.contains(&rh.id())) + } + Ok(ThreadMessage::WlGetHotKeys(tx)) => { + let _ = tx.send(registered_hotkeys.clone().into()); + } + Ok(ThreadMessage::DropThread) => return Ok(()), + _ => {} + }, + i if i == gs_rx_idx => { + match selected_oper.recv(&gs_event_receiver) { + Ok(GSEvent::Activated(activated)) => { + // only send event if (1) shortcut id can be parsed as u32 and (2) if the + // shortcut has been registered + if let Some(id) = activated + .shortcut_id() + .parse::() + .ok() + .filter(|id| registered_hotkeys.iter().any(|rh| rh.id() == *id)) + { + // only send event if not already pressed (or an event has not yet been + // received for this hotkey) + let hotkey_state_opt = hotkey_states.get(&id); + if hotkey_state_opt.is_none() + || hotkey_state_opt.filter(|pressed| !*pressed).is_some() + { + // update hotkey state before sending event + hotkey_states.insert(id, true); + + GlobalHotKeyEvent::send(GlobalHotKeyEvent { + id, + state: HotKeyState::Pressed, + }); + } + } + } + Ok(GSEvent::Deactivated(deactivated)) => { + // only send event if (1) shortcut id can be parsed as u32 and (2) if the + // shortcut has been registered + if let Some(id) = deactivated + .shortcut_id() + .parse::() + .ok() + .filter(|id| registered_hotkeys.iter().any(|rh| rh.id() == *id)) + { + // update hotkey state before sending event + hotkey_states.insert(id, false); + + GlobalHotKeyEvent::send(GlobalHotKeyEvent { + id, + state: HotKeyState::Released, + }); + } + } + Ok(GSEvent::Changed(new_event)) => { + // strip out shortcuts that weren't already registered and convert them to + // WlChangedHotkeys + let new_shortcuts: Vec = new_event + .shortcuts() + .iter() + .filter_map(|ns| WlChangedHotKey::try_from(ns.clone()).ok()) + .filter(|ns| registered_hotkeys.iter().any(|rh| rh.id == ns.id)) + .collect(); + + // change registered shortcuts + let mut something_changed = false; + for new in &new_shortcuts { + if let Some(hk) = + registered_hotkeys.iter_mut().find(|rh| rh.id() == new.id) + { + hk.hotkey_description = new.hotkey_description.clone(); + something_changed = true; + } + } + + if something_changed { + WL_HOTKEYS_CHANGED_CHANNEL.0.update_and_send(new_shortcuts); + } + } + Err(_) => {} + } + } + _ => unreachable!(), + } + } +} + +async fn reregister_hotkeys( + gs_state: &mut GlobalShortcutsState<'_>, + registered_hotkeys: &mut Vec, + new_hotkeys: &[WlNewHotKeyAction], +) -> Result<(), Error> { + gs_state.session.close().await.map_err(|e| { + Error::FailedToRegister(format!("Failed to close old global shortcuts session: {e}")) + })?; + + gs_state.session = gs_state.proxy.create_session().await.map_err(|e| { + Error::FailedToRegister(format!( + "Failed to start global shortcuts portal session: {e}" + )) + })?; + + // reregister all hotkeys in registered_hotkeys, plus everything in new_hotkeys that hasn't + // already been registered + let hotkeys_to_register = registered_hotkeys + .iter() + .cloned() + .map(Into::into) + .chain( + new_hotkeys + .iter() + .unique_by(|nh| nh.id()) + .filter(|&nh| !registered_hotkeys.iter().any(|rh| rh.id() == nh.id())) + .cloned() + .map(Into::into), + ) + .collect::>(); + + // not handling error from BindShortcuts due to GNOME 48 bug (fixed in GNOME 49): + // https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/issues/177 + let _ = gs_state + .proxy + .bind_shortcuts(&gs_state.session, &hotkeys_to_register, None) + .await + .map(|r| r.response()); + + // update registered_shortcuts array + if let Ok(ls) = gs_state + .proxy + .list_shortcuts(&gs_state.session) + .await + .and_then(|r| r.response()) + { + registered_hotkeys.extend( + ls.shortcuts() + .iter() + .filter(|sh| new_hotkeys.iter().any(|nh| nh.id().to_string() == sh.id())) + .filter_map(|sh| sh.clone().try_into().ok()), + ) + } + + Ok(()) +} + +#[derive(Clone)] +struct WlHotKeysChangedEventSender(Sender); + +impl WlHotKeysChangedEventSender { + fn update_and_send(&self, new_shortcuts: Vec) { + let Ok(old_ev) = WL_HOTKEYS_CHANGED_CHANNEL.1.try_recv() else { + let _ = self.0.send(WlHotKeysChangedEvent { + changed_hotkeys: new_shortcuts, + }); + return; + }; + + // if there was an event sent previously which wasn't received + // anywhere, then remove it and add all the changed hotkeys from it + // to the new event (excluding any hotkeys that are included in the + // new event) + let changed_hotkeys = old_ev + .changed_hotkeys + .into_iter() + .filter(|old_ch| !new_shortcuts.iter().any(|ns| ns.id == old_ch.id)) + .chain(new_shortcuts.iter().cloned()) + .collect(); + + let _ = self.0.send(WlHotKeysChangedEvent { changed_hotkeys }); + } +} + +static WL_HOTKEYS_CHANGED_CHANNEL: Lazy<( + WlHotKeysChangedEventSender, + Receiver, +)> = Lazy::new(|| { + let (tx, rx) = bounded(1); + (WlHotKeysChangedEventSender(tx), rx) +}); + +pub(crate) fn wl_hotkeys_changed_receiver() -> Receiver { + WL_HOTKEYS_CHANGED_CHANNEL.1.clone() +} + +impl TryFrom for WlHotKeyAction { + type Error = ParseIntError; + + fn try_from(value: Shortcut) -> Result { + let id = value.id().parse::()?; + + Ok(Self { + id, + action_description: value.description().into(), + hotkey_description: value.trigger_description().into(), + }) + } +} + +impl From for NewShortcut { + fn from(wl_hotkey: WlHotKeyAction) -> Self { + NewShortcut::new(wl_hotkey.id().to_string(), wl_hotkey.action_description()) + } +} + +impl From for NewShortcut { + fn from(wl_hotkey: WlNewHotKeyAction) -> Self { + NewShortcut::new(wl_hotkey.id().to_string(), wl_hotkey.description()).preferred_trigger( + wl_hotkey + .preferred_hotkey() + .and_then(hotkey_to_wayland_trigger) + .as_deref(), + ) + } +} + +fn hotkey_to_wayland_trigger(hotkey: HotKey) -> Option { + let mut mods = "".to_string(); + + if hotkey.mods.ctrl() { + mods += "CTRL+"; + } + if hotkey.mods.shift() { + mods += "SHIFT+"; + } + if hotkey.mods.alt() { + mods += "ALT+"; + } + if hotkey.mods.contains(Modifiers::SUPER) { + mods += "LOGO+"; + } + + let keycode = match hotkey.key { + Code::KeyA => "A", + Code::KeyB => "B", + Code::KeyC => "C", + Code::KeyD => "D", + Code::KeyE => "E", + Code::KeyF => "F", + Code::KeyG => "G", + Code::KeyH => "H", + Code::KeyI => "I", + Code::KeyJ => "J", + Code::KeyK => "K", + Code::KeyL => "L", + Code::KeyM => "M", + Code::KeyN => "N", + Code::KeyO => "O", + Code::KeyP => "P", + Code::KeyQ => "Q", + Code::KeyR => "R", + Code::KeyS => "S", + Code::KeyT => "T", + Code::KeyU => "U", + Code::KeyV => "V", + Code::KeyW => "W", + Code::KeyX => "X", + Code::KeyY => "Y", + Code::KeyZ => "Z", + Code::Backslash => "backslash", + Code::BracketLeft => "bracketleft", + Code::BracketRight => "bracketright", + Code::Backquote => "grave", + Code::Comma => "comma", + Code::Digit0 => "0", + Code::Digit1 => "1", + Code::Digit2 => "2", + Code::Digit3 => "3", + Code::Digit4 => "4", + Code::Digit5 => "5", + Code::Digit6 => "6", + Code::Digit7 => "7", + Code::Digit8 => "8", + Code::Digit9 => "9", + Code::Equal => "equal", + Code::Minus => "minus", + Code::Period => "period", + Code::Quote => "apostrophe", + Code::Semicolon => "semicolon", + Code::Slash => "slash", + Code::Backspace => "BackSpace", + Code::CapsLock => "Caps_Lock", + Code::Enter => "Return", + Code::Space => "space", + Code::Tab => "Tab", + Code::Delete => "Delete", + Code::End => "End", + Code::Home => "Home", + Code::Insert => "Insert", + Code::PageDown => "Page_Down", + Code::PageUp => "Page_Up", + Code::ArrowDown => "downarrow", + Code::ArrowLeft => "leftarrow", + Code::ArrowRight => "rightarrow", + Code::ArrowUp => "uparrow", + Code::Numpad0 => "KP_0", + Code::Numpad1 => "KP_1", + Code::Numpad2 => "KP_2", + Code::Numpad3 => "KP_3", + Code::Numpad4 => "KP_4", + Code::Numpad5 => "KP_5", + Code::Numpad6 => "KP_6", + Code::Numpad7 => "KP_7", + Code::Numpad8 => "KP_8", + Code::Numpad9 => "KP_9", + Code::NumpadAdd => "KP_Add", + Code::NumpadDecimal => "KP_Decimal", + Code::NumpadDivide => "KP_Divide", + Code::NumpadMultiply => "KP_Multiply", + Code::NumpadSubtract => "KP_Subtract", + Code::Escape => "Escape", + Code::PrintScreen => "Print", + Code::ScrollLock => "Scroll_Lock", + Code::NumLock => "Num_lock", + Code::F1 => "F1", + Code::F2 => "F2", + Code::F3 => "F3", + Code::F4 => "F4", + Code::F5 => "F5", + Code::F6 => "F6", + Code::F7 => "F7", + Code::F8 => "F8", + Code::F9 => "F9", + Code::F10 => "F10", + Code::F11 => "F11", + Code::F12 => "F12", + Code::AudioVolumeDown => "XF86AudioLowerVolume", + Code::AudioVolumeMute => "XF86AudioMute", + Code::AudioVolumeUp => "XF86AudioRaiseVolume", + Code::MediaPlay => "XF86AudioPlay", + Code::MediaPause => "XF86AudioPause", + Code::MediaStop => "XF86AudioStop", + Code::MediaTrackNext => "XF86AudioNext", + Code::MediaTrackPrevious => "XF86AudioPrev", + Code::Pause => "Pause", + _ => return None, + }; + + Some(mods + keycode) +} + +impl TryFrom for WlChangedHotKey { + type Error = ParseIntError; + + fn try_from(value: Shortcut) -> Result { + Ok(Self { + id: value.id().parse::()?, + hotkey_description: value.trigger_description().into(), + }) + } +} + +#[allow(unused)] +mod tests { + use keyboard_types::Modifiers; + + use super::*; + + #[test] + fn hotkey_to_wl_trigger_test() { + let modifiers = Modifiers::SHIFT | Modifiers::META; + let trigger_desc = hotkey_to_wayland_trigger(HotKey::new(Some(modifiers), Code::KeyD)); + assert_eq!(trigger_desc.as_deref(), Some("SHIFT+LOGO+D")); + + let modifiers = Modifiers::SHIFT | Modifiers::META | Modifiers::CONTROL | Modifiers::ALT; + let trigger_desc = hotkey_to_wayland_trigger(HotKey::new(Some(modifiers), Code::Backslash)); + assert_eq!( + trigger_desc.as_deref(), + Some("CTRL+SHIFT+ALT+LOGO+backslash") + ) + } + + #[test] + fn hotkey_change_event_updates_correctly() { + let sender = WL_HOTKEYS_CHANGED_CHANNEL.0.clone(); + let receiver = WL_HOTKEYS_CHANGED_CHANNEL.1.clone(); + let first_event = vec![ + WlChangedHotKey { + id: 1, + hotkey_description: "CTRL+A".into(), + }, + WlChangedHotKey { + id: 2, + hotkey_description: "CTRL+B".into(), + }, + ]; + sender.update_and_send(first_event); + + let second_event = vec![WlChangedHotKey { + id: 1, + hotkey_description: "CTRL+C".into(), + }]; + sender.update_and_send(second_event); + + let ev = receiver.try_recv().unwrap(); + assert_eq!(ev.changed_hotkeys.len(), 2); + assert!(ev.changed_hotkeys.contains(&WlChangedHotKey { + id: 1, + hotkey_description: "CTRL+C".into(), + })); + assert!(ev.changed_hotkeys.contains(&WlChangedHotKey { + id: 2, + hotkey_description: "CTRL+B".into(), + })); + } +} diff --git a/src/platform_impl/x11/mod.rs b/src/platform_impl/linux/x11.rs similarity index 86% rename from src/platform_impl/x11/mod.rs rename to src/platform_impl/linux/x11.rs index a3b0228..f25b696 100644 --- a/src/platform_impl/x11/mod.rs +++ b/src/platform_impl/linux/x11.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; -use crossbeam_channel::{unbounded, Receiver, Sender}; +use crossbeam_channel::Receiver; use keyboard_types::{Code, Modifiers}; use x11rb::connection::Connection; use x11rb::errors::ReplyError; @@ -13,91 +13,9 @@ use x11rb::protocol::{xkb, ErrorKind, Event}; use x11rb::rust_connection::RustConnection; use xkeysym::RawKeysym; +use crate::platform_impl::platform::ThreadMessage; use crate::{hotkey::HotKey, Error, GlobalHotKeyEvent}; -enum ThreadMessage { - RegisterHotKey(HotKey, Sender>), - RegisterHotKeys(Vec, Sender>), - UnRegisterHotKey(HotKey, Sender>), - UnRegisterHotKeys(Vec, Sender>), - DropThread, -} - -pub struct GlobalHotKeyManager { - thread_tx: Sender, -} - -impl GlobalHotKeyManager { - pub fn new() -> crate::Result { - let (thread_tx, thread_rx) = unbounded(); - std::thread::spawn(|| { - if let Err(_err) = events_processor(thread_rx) { - #[cfg(feature = "tracing")] - tracing::error!("{}", _err); - } - }); - Ok(Self { thread_tx }) - } - - pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { - let (tx, rx) = crossbeam_channel::bounded(1); - let _ = self - .thread_tx - .send(ThreadMessage::RegisterHotKey(hotkey, tx)); - - if let Ok(result) = rx.recv() { - result?; - } - - Ok(()) - } - - pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { - let (tx, rx) = crossbeam_channel::bounded(1); - let _ = self - .thread_tx - .send(ThreadMessage::UnRegisterHotKey(hotkey, tx)); - - if let Ok(result) = rx.recv() { - result?; - } - - Ok(()) - } - - pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { - let (tx, rx) = crossbeam_channel::bounded(1); - let _ = self - .thread_tx - .send(ThreadMessage::RegisterHotKeys(hotkeys.to_vec(), tx)); - - if let Ok(result) = rx.recv() { - result?; - } - - Ok(()) - } - - pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { - let (tx, rx) = crossbeam_channel::bounded(1); - let _ = self - .thread_tx - .send(ThreadMessage::UnRegisterHotKeys(hotkeys.to_vec(), tx)); - - if let Ok(result) = rx.recv() { - result?; - } - - Ok(()) - } -} - -impl Drop for GlobalHotKeyManager { - fn drop(&mut self) { - let _ = self.thread_tx.send(ThreadMessage::DropThread); - } -} - // XGrabKey works only with the exact state (modifiers) // and since X11 considers NumLock, ScrollLock and CapsLock a modifier when it is ON, // we also need to register our shortcut combined with these extra modifiers as well @@ -224,7 +142,7 @@ struct HotKeyState { mods: ModMask, } -fn events_processor(thread_rx: Receiver) -> Result<(), String> { +pub fn events_processor(thread_rx: Receiver) -> Result<(), String> { let mut hotkeys = BTreeMap::>::new(); let (conn, screen) = RustConnection::connect(None) @@ -320,6 +238,7 @@ fn events_processor(thread_rx: Receiver) -> Result<(), String> { ThreadMessage::DropThread => { return Ok(()); } + _ => {} } } diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index cf02ed6..f75419e 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -12,7 +12,7 @@ mod platform; target_os = "openbsd", target_os = "netbsd" ))] -#[path = "x11/mod.rs"] +#[path = "linux/mod.rs"] mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] diff --git a/src/wayland.rs b/src/wayland.rs new file mode 100644 index 0000000..82b0dde --- /dev/null +++ b/src/wayland.rs @@ -0,0 +1,226 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! This module is for Wayland-specific functions. +//! +//! # How Hotkeys on Wayland Work +//! +//! Wayland makes use of the XDG GlobalShortcuts portal ([see the official portal documentation for +//! more +//! details](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html)). +//! +//! On Wayland, you define a set of actions, where each action has any number of associated +//! hotkeys to trigger it which are configured externally, not by the application. +//! +//! The first time some actions are registered, the user is shown a prompt with a list of actions, +//! a description for what each action does, and they are given an option to configure each action's +//! hotkeys (which may have a default setting). The user can configure these hotkeys later in their +//! system's settings. +//! +//! An application can request a list of each action along with the associated hotkeys, as well as +//! receive events whenever the user changes any of these hotkeys. +//! +//! # How to Use this Module +//! +//! You can verify if the user is using Wayland with the [`using_wayland`] function. +//! +//! Register all your actions using +//! [`GlobalHotKeyManager::wl_register_all`](crate::GlobalHotKeyManager::wl_register_all). This +//! should be used to register all your application's hotkey actions at once, since each call to +//! this function could create a popup. +//! +//! Use [`GlobalHotKeyManager::wl_get_hotkeys`](crate::GlobalHotKeyManager::wl_get_hotkeys) to get +//! the current list of registered hotkeys. +//! +//! Use [`WlHotKeysChangedEvent::receiver()`] to get notified whenever the user changes a hotkey +//! while your app is running. +//! +//! Unregister hotkey actions using +//! [`GlobalHotKeyManager::wl_unregister_all`](crate::GlobalHotKeyManager::wl_unregister_all). +//! +//! # Notes +//! +//! - If you can't register any shortcuts, make sure: +//! - You are running an xdg-desktop-portal backend that supports global shortcuts. +//! - The app id is set correctly. See the documentation for [`GlobalHotKeyManager::wl_register_all`](crate::GlobalHotKeyManager::wl_register_all) for more information. +//! +//! # Example +//! +//! ```no_run +//! use global_hotkey::{ +//! hotkey::{Code, HotKey, Modifiers}, +//! wayland::{WlNewHotKeyAction, WlHotKeysChangedEvent}, +//! GlobalHotKeyEvent, GlobalHotKeyManager, +//! }; +//! +//! const MY_ACTION_ID: u32 = 1; +//! +//! # #[cfg(target_os = "linux")] +//! fn main() { +//! // initialize hotkey manager +//! let hotkey_manager = GlobalHotKeyManager::new().unwrap(); +//! +//! // registering an action with CTRL+META+O as the preferred hotkey +//! let my_action = WlNewHotKeyAction::new( +//! MY_ACTION_ID, +//! "Do cool stuff.", +//! Some(HotKey::new( +//! Some(Modifiers::CONTROL | Modifiers::META), +//! Code::KeyO, +//! )), +//! ); +//! +//! // register all your application's hotkey actions +//! hotkey_manager.wl_register_all("com.github.example.ExampleAppID", &[my_action]).unwrap(); +//! +//! // listening to hotkey change events on another thread like how you would listen to regular +//! // hotkey events. +//! std::thread::spawn(move || { +//! let Some(receiver) = WlHotKeysChangedEvent::receiver() else { +//! return; +//! }; +//! while let Ok(new_hotkeys) = receiver.recv() { +//! println!( +//! "Some hotkeys were changed, here is what changed: {:?}", +//! hotkey_manager.wl_get_hotkeys() +//! ); +//! } +//! }); +//! +//! // receiving global hotkey events (i.e. hotkey presses/releases) on main thread +//! let event_receiver = GlobalHotKeyEvent::receiver(); +//! while let Ok(event) = event_receiver.recv() { +//! println!("{event:?}"); +//! } +//! } +//! # #[cfg(not(target_os = "linux"))] +//! # fn main() {} +//! ``` + +use crossbeam_channel::Receiver; +use std::env; + +use crate::{ + hotkey::HotKey, + macros::{not_on_linux_cfg, on_linux_cfg}, + on_linux, +}; + +on_linux_cfg! { + use crate::platform_impl::wl_hotkeys_changed_receiver; +} + +/// Returns `true` if `WAYLAND_DISPLAY` is set and running on Linux/BSD. +pub fn using_wayland() -> bool { + on_linux!() && env::var("WAYLAND_DISPLAY").is_ok() +} + +pub struct WlHotKeysChangedEvent { + pub changed_hotkeys: Vec, +} + +impl WlHotKeysChangedEvent { + /// Gets receiver for WlHotKeysChangedEvent, which will allow you to listen to any changes the + /// user makes to the registered hotkeys. + /// + /// Will return `None` if not using Linux and Wayland. + pub fn receiver() -> Option> { + Self::receiver_impl() + } + + on_linux_cfg! { + fn receiver_impl() -> Option> { + Some(wl_hotkeys_changed_receiver()).filter(|_| using_wayland()) + } + } + + not_on_linux_cfg! { + fn receiver_impl() -> Option> { + None + } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct WlChangedHotKey { + pub id: u32, + pub hotkey_description: String, +} + +/// Used to register a new action under Wayland which can have associated hotkeys. +#[derive(Debug, Clone)] +pub struct WlNewHotKeyAction { + id: u32, + description: String, + preferred_hotkey: Option, +} + +impl WlNewHotKeyAction { + /// Creates a new [`WlNewHotKeyAction`]. + /// + /// # Arguments + /// + /// * `id` - a unique [`u32`] to identify this action and all its associated hotkeys. + /// * `description` - a short, human-readable description detailing what triggering this action does. + /// * `preferred_hotkey` - an optional recommended hotkey that the user will be presented with + /// when registering this action for the first time. If the hotkey cannot be parsed, it will be + /// ignored. + pub fn new(id: u32, description: S, preferred_hotkey: Option) -> Self + where + S: Into, + { + Self { + id, + description: description.into(), + preferred_hotkey, + } + } + + /// A unique numerical id to identify the hotkeys associated with this action. + pub fn id(&self) -> u32 { + self.id + } + + /// A human-readable description detailing what triggering this action does. + pub fn description(&self) -> &str { + &self.description + } + + /// The optional recommended key-combination that the user will be presented with when + /// registering this hotkey for the first time. + pub fn preferred_hotkey(&self) -> Option { + self.preferred_hotkey + } +} + +/// A registered hotkey action that can have any number of associated hotkeys. +#[derive(Debug, Clone)] +pub struct WlHotKeyAction { + pub(crate) id: u32, + pub(crate) action_description: String, + pub(crate) hotkey_description: String, +} + +impl WlHotKeyAction { + /// A unique numerical id to identify this action and its associated hotkeys. + pub fn id(&self) -> u32 { + self.id + } + + /// A human-readable description detailing what the action does. + pub fn action_description(&self) -> &str { + &self.action_description + } + + /// Description of the hotkeys to trigger this action (e.g. `CTRL+ALT+U`). + /// + /// ## Note + /// + /// It can contain any number of hotkeys, including none at all. See the [shortcuts XDG + /// specification](https://specifications.freedesktop.org/shortcuts-spec/latest/) for more + /// information about how each hotkey is formatted. + pub fn hotkey_description(&self) -> &str { + &self.hotkey_description + } +}