#![cfg_attr(not(all(desktop, feature = "global-shortcuts")), allow(dead_code))]
pub(crate) enum ShortcutAction<Msg> {
Message(Msg),
ShowWindow,
}
pub struct GlobalShortcut<Msg> {
pub(crate) keys: String,
pub(crate) action: ShortcutAction<Msg>,
}
impl<Msg> GlobalShortcut<Msg> {
pub fn new(keys: impl Into<String>, message: Msg) -> Self {
Self {
keys: keys.into(),
action: ShortcutAction::Message(message),
}
}
pub fn show_window(keys: impl Into<String>) -> Self {
Self {
keys: keys.into(),
action: ShortcutAction::ShowWindow,
}
}
}
pub(crate) fn canonical(keys: &str, macos: bool) -> Result<String, String> {
let (mut ctrl, mut alt, mut shift, mut sup) = (false, false, false, false);
let mut key: Option<String> = None;
for token in keys.split('+').map(str::trim) {
if token.is_empty() {
return Err(format!("{keys:?}: an empty part"));
}
match token.to_ascii_uppercase().as_str() {
"CTRL" | "CONTROL" => ctrl = true,
"ALT" | "OPTION" => alt = true,
"SHIFT" => shift = true,
"SUPER" | "CMD" | "COMMAND" | "META" => sup = true,
"PRIMARY" | "CMDORCTRL" | "COMMANDORCONTROL" => {
if macos {
sup = true;
} else {
ctrl = true;
}
}
other => {
if key.is_some() {
return Err(format!("{keys:?}: more than one key"));
}
key = Some(other.to_string());
}
}
}
let Some(key) = key else {
return Err(format!("{keys:?}: no key"));
};
if !(ctrl || alt || sup) {
return Err(format!(
"{keys:?}: a global shortcut needs Ctrl, Alt or Super, or it takes typing from every \
other application"
));
}
let mut parts = Vec::new();
for (on, name) in [
(ctrl, "CONTROL"),
(alt, "ALT"),
(shift, "SHIFT"),
(sup, "SUPER"),
] {
if on {
parts.push(name.to_string());
}
}
parts.push(key);
Ok(parts.join("+"))
}
#[cfg(all(desktop, feature = "global-shortcuts"))]
pub(crate) mod native {
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex, Once};
use global_hotkey::hotkey::HotKey;
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
use winit::window::Window;
#[derive(Default)]
pub(crate) struct Registered {
manager: Option<GlobalHotKeyManager>,
held: HashMap<u32, (String, HotKey)>,
refused: Vec<String>,
}
static PRESSED: Mutex<Vec<u32>> = Mutex::new(Vec::new());
static WINDOW: Mutex<Option<Arc<Window>>> = Mutex::new(None);
fn listen() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
GlobalHotKeyEvent::set_event_handler(Some(|event: GlobalHotKeyEvent| {
if event.state() != HotKeyState::Pressed {
return;
}
if let Ok(mut pressed) = PRESSED.lock() {
pressed.push(event.id());
}
if let Some(window) = WINDOW.lock().ok().and_then(|window| window.clone()) {
window.request_redraw();
}
}));
});
}
pub(crate) fn take_pressed(registered: &Registered) -> Vec<String> {
let ids = PRESSED
.lock()
.map(|mut pressed| std::mem::take(&mut *pressed))
.unwrap_or_default();
ids.into_iter()
.filter_map(|id| registered.held.get(&id).map(|(keys, _)| keys.clone()))
.collect()
}
pub(crate) fn sync(registered: &mut Registered, wanted: &[String], window: &Arc<Window>) {
if let Ok(mut slot) = WINDOW.lock() {
*slot = Some(window.clone());
}
if wanted.is_empty() && registered.held.is_empty() {
return;
}
if registered.manager.is_none() {
match GlobalHotKeyManager::new() {
Ok(manager) => registered.manager = Some(manager),
Err(err) => {
log::warn!("global shortcuts: unavailable ({err})");
return;
}
}
listen();
}
let Some(manager) = registered.manager.as_ref() else {
return;
};
registered.held.retain(|_, (keys, hotkey)| {
let keep = wanted.contains(keys);
if !keep {
let _ = manager.unregister(*hotkey);
}
keep
});
for keys in wanted {
if registered.held.values().any(|(held, _)| held == keys)
|| registered.refused.contains(keys)
{
continue;
}
let hotkey = match HotKey::from_str(keys) {
Ok(hotkey) => hotkey,
Err(err) => {
log::warn!("global shortcuts: {keys} is not a combination ({err})");
registered.refused.push(keys.clone());
continue;
}
};
match manager.register(hotkey) {
Ok(()) => {
log::debug!("global shortcuts: {keys} registered");
registered.held.insert(hotkey.id(), (keys.clone(), hotkey));
}
Err(err) => {
log::warn!(
"global shortcuts: {keys} could not be registered — another \
application may hold it ({err})"
);
registered.refused.push(keys.clone());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_combination_has_one_spelling() {
assert_eq!(
canonical("Ctrl+Shift+K", false).as_deref(),
Ok("CONTROL+SHIFT+K")
);
assert_eq!(
canonical("shift + ctrl + k", false).as_deref(),
Ok("CONTROL+SHIFT+K")
);
assert_eq!(canonical("Alt+Option+F5", false).as_deref(), Ok("ALT+F5"));
assert_eq!(canonical("Cmd+Space", true).as_deref(), Ok("SUPER+SPACE"));
}
#[test]
fn primary_is_the_platforms_own() {
assert_eq!(
canonical("Primary+Shift+Space", true).as_deref(),
Ok("SHIFT+SUPER+SPACE")
);
assert_eq!(
canonical("Primary+Shift+Space", false).as_deref(),
Ok("CONTROL+SHIFT+SPACE")
);
}
#[test]
fn what_would_take_typing_is_refused() {
assert!(canonical("K", false).is_err());
assert!(canonical("Shift+K", false).is_err());
assert!(canonical("Ctrl+Shift", false).is_err(), "no key");
assert!(canonical("Ctrl+K+L", false).is_err(), "two keys");
assert!(canonical("Ctrl++K", false).is_err(), "an empty part");
assert!(canonical("Ctrl+", false).is_err(), "an empty key");
assert!(canonical("Alt+K", false).is_ok());
assert!(canonical("Super+K", false).is_ok());
}
#[cfg(all(desktop, feature = "global-shortcuts"))]
#[test]
fn the_spelling_is_the_systems() {
use std::str::FromStr;
for keys in [
"Ctrl+Shift+K",
"Primary+Alt+Space",
"Alt+F5",
"Ctrl+Alt+Shift+ArrowUp",
"Super+1",
] {
let spelled = canonical(keys, cfg!(target_os = "macos")).unwrap();
assert!(
global_hotkey::hotkey::HotKey::from_str(&spelled).is_ok(),
"{keys} -> {spelled}"
);
}
}
}