Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1f8e2a9
Add initial wayland support with global shortcuts portal
Adamskye Sep 13, 2025
8529d8a
Detect changes in wayland hotkeys. Prevent key repeat.
Adamskye Sep 13, 2025
1be9b36
Fix up conditional compilation and test windows compilation
Adamskye Sep 13, 2025
1b5df4a
Fix clippy warnings and tweak macros
Adamskye Sep 13, 2025
ef27a67
Add documentation for Wayland
Adamskye Sep 14, 2025
aed7728
Add Wayland example
Adamskye Sep 14, 2025
bfe8993
Fix clippy warnings
Adamskye Sep 14, 2025
8f8c7c2
Acknowledge Wayland support in lib.rs documentation
Adamskye Sep 14, 2025
9c50c6c
Acknowledge Wayland support in README.md
Adamskye Sep 14, 2025
f1e6821
Revert tao example
Adamskye Sep 14, 2025
d89e123
Tweak lib.rs documentation.
Adamskye Sep 14, 2025
b25e0ed
Merge branch 'wayland_support' of https://github.com/Adamskye/global-…
Adamskye Sep 14, 2025
b43d4b2
Allow unregistering keybind on Wayland
Adamskye Sep 14, 2025
0547e2d
Deduplicate new Wayland shortcuts.
Adamskye Sep 14, 2025
06ad3b0
Document wl_unregister_all
Adamskye Sep 14, 2025
460dbb3
Mention app ids in documentation
Adamskye Sep 14, 2025
2d1243e
Fix meta/super key and clippy warnings
Adamskye Sep 14, 2025
915ceca
Fix clippy warning
Adamskye Sep 16, 2025
e56f173
Specify app ids when first specifying a global shortcut
Adamskye Sep 16, 2025
fd276c5
Don't initialise global shortcuts until first register
Adamskye Sep 16, 2025
377c030
Improve documentation for `wl_register_all` and `wl_unregister_all`
Adamskye Sep 16, 2025
8041e95
Fix compiling on Windows/MacOS
Adamskye Sep 17, 2025
ddfb503
Fixed bug where the first press of a hotkey doesn't register
Adamskye Oct 30, 2025
603c858
Fix doc test for wl_register_all
Adamskye Nov 3, 2025
9e532e0
Specify which hotkeys have changed in a hotkey changed event
Adamskye Nov 3, 2025
191957f
Hand WlHotKeysChangedEvent receiver directly to user
Adamskye Nov 3, 2025
9df14c1
Test creating a new changed event
Adamskye Nov 3, 2025
bf7fa6d
Update documentation
Adamskye Nov 3, 2025
21ab168
Initialise app id inside GlobalShortcutsState::new
Adamskye Nov 3, 2025
1eb522c
Bump Rust version to 1.77
Adamskye Nov 5, 2025
0dfef12
Fix building on Windows (again)
Adamskye Nov 6, 2025
c181e92
Scoped out the doc example to specifically target linux, as the HWND …
coral Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 334 additions & 15 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
95 changes: 94 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,
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<String>, 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<String>, _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([])
}
}
}
44 changes: 44 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
@@ -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;
142 changes: 142 additions & 0 deletions src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -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<WlNewHotKeyAction>, String, Sender<crate::Result<()>>),
WlUnRegisterHotKeys(Vec<u32>),
WlGetHotKeys(Sender<Box<[WlHotKeyAction]>>),

RegisterHotKey(HotKey, Sender<crate::Result<()>>),
RegisterHotKeys(Vec<HotKey>, Sender<crate::Result<()>>),
UnRegisterHotKey(HotKey, Sender<crate::Result<()>>),
UnRegisterHotKeys(Vec<HotKey>, Sender<crate::Result<()>>),
DropThread,
}

pub struct GlobalHotKeyManager {
thread_tx: Sender<ThreadMessage>,
}

impl GlobalHotKeyManager {
pub fn new() -> crate::Result<Self> {
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<String>,
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);
}
}
Loading
Loading