1#![cfg_attr(not(all(desktop, feature = "global-shortcuts")), allow(dead_code))]
42
43pub(crate) enum ShortcutAction<Msg> {
45 Message(Msg),
47 ShowWindow,
49}
50
51pub struct GlobalShortcut<Msg> {
53 pub(crate) keys: String,
55 pub(crate) action: ShortcutAction<Msg>,
56}
57
58impl<Msg> GlobalShortcut<Msg> {
59 pub fn new(keys: impl Into<String>, message: Msg) -> Self {
61 Self {
62 keys: keys.into(),
63 action: ShortcutAction::Message(message),
64 }
65 }
66
67 pub fn show_window(keys: impl Into<String>) -> Self {
70 Self {
71 keys: keys.into(),
72 action: ShortcutAction::ShowWindow,
73 }
74 }
75}
76
77pub(crate) fn canonical(keys: &str, macos: bool) -> Result<String, String> {
81 let (mut ctrl, mut alt, mut shift, mut sup) = (false, false, false, false);
82 let mut key: Option<String> = None;
83 for token in keys.split('+').map(str::trim) {
84 if token.is_empty() {
85 return Err(format!("{keys:?}: an empty part"));
86 }
87 match token.to_ascii_uppercase().as_str() {
88 "CTRL" | "CONTROL" => ctrl = true,
89 "ALT" | "OPTION" => alt = true,
90 "SHIFT" => shift = true,
91 "SUPER" | "CMD" | "COMMAND" | "META" => sup = true,
92 "PRIMARY" | "CMDORCTRL" | "COMMANDORCONTROL" => {
93 if macos {
94 sup = true;
95 } else {
96 ctrl = true;
97 }
98 }
99 other => {
100 if key.is_some() {
101 return Err(format!("{keys:?}: more than one key"));
102 }
103 key = Some(other.to_string());
104 }
105 }
106 }
107 let Some(key) = key else {
108 return Err(format!("{keys:?}: no key"));
109 };
110 if !(ctrl || alt || sup) {
111 return Err(format!(
112 "{keys:?}: a global shortcut needs Ctrl, Alt or Super, or it takes typing from every \
113 other application"
114 ));
115 }
116 let mut parts = Vec::new();
117 for (on, name) in [
118 (ctrl, "CONTROL"),
119 (alt, "ALT"),
120 (shift, "SHIFT"),
121 (sup, "SUPER"),
122 ] {
123 if on {
124 parts.push(name.to_string());
125 }
126 }
127 parts.push(key);
128 Ok(parts.join("+"))
129}
130
131#[cfg(all(desktop, feature = "global-shortcuts"))]
133pub(crate) mod native {
134 use std::collections::HashMap;
135 use std::str::FromStr;
136 use std::sync::{Arc, Mutex, Once};
137
138 use global_hotkey::hotkey::HotKey;
139 use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
140 use winit::window::Window;
141
142 #[derive(Default)]
145 pub(crate) struct Registered {
146 manager: Option<GlobalHotKeyManager>,
147 held: HashMap<u32, (String, HotKey)>,
149 refused: Vec<String>,
151 }
152
153 static PRESSED: Mutex<Vec<u32>> = Mutex::new(Vec::new());
155 static WINDOW: Mutex<Option<Arc<Window>>> = Mutex::new(None);
157
158 fn listen() {
159 static ONCE: Once = Once::new();
160 ONCE.call_once(|| {
161 GlobalHotKeyEvent::set_event_handler(Some(|event: GlobalHotKeyEvent| {
162 if event.state() != HotKeyState::Pressed {
163 return;
164 }
165 if let Ok(mut pressed) = PRESSED.lock() {
166 pressed.push(event.id());
167 }
168 if let Some(window) = WINDOW.lock().ok().and_then(|window| window.clone()) {
169 window.request_redraw();
170 }
171 }));
172 });
173 }
174
175 pub(crate) fn take_pressed(registered: &Registered) -> Vec<String> {
177 let ids = PRESSED
178 .lock()
179 .map(|mut pressed| std::mem::take(&mut *pressed))
180 .unwrap_or_default();
181 ids.into_iter()
182 .filter_map(|id| registered.held.get(&id).map(|(keys, _)| keys.clone()))
183 .collect()
184 }
185
186 pub(crate) fn sync(registered: &mut Registered, wanted: &[String], window: &Arc<Window>) {
189 if let Ok(mut slot) = WINDOW.lock() {
190 *slot = Some(window.clone());
191 }
192 if wanted.is_empty() && registered.held.is_empty() {
193 return;
194 }
195 if registered.manager.is_none() {
196 match GlobalHotKeyManager::new() {
197 Ok(manager) => registered.manager = Some(manager),
198 Err(err) => {
199 log::warn!("global shortcuts: unavailable ({err})");
200 return;
201 }
202 }
203 listen();
204 }
205 let Some(manager) = registered.manager.as_ref() else {
206 return;
207 };
208 registered.held.retain(|_, (keys, hotkey)| {
209 let keep = wanted.contains(keys);
210 if !keep {
211 let _ = manager.unregister(*hotkey);
212 }
213 keep
214 });
215 for keys in wanted {
216 if registered.held.values().any(|(held, _)| held == keys)
217 || registered.refused.contains(keys)
218 {
219 continue;
220 }
221 let hotkey = match HotKey::from_str(keys) {
222 Ok(hotkey) => hotkey,
223 Err(err) => {
224 log::warn!("global shortcuts: {keys} is not a combination ({err})");
225 registered.refused.push(keys.clone());
226 continue;
227 }
228 };
229 match manager.register(hotkey) {
230 Ok(()) => {
231 log::debug!("global shortcuts: {keys} registered");
232 registered.held.insert(hotkey.id(), (keys.clone(), hotkey));
233 }
234 Err(err) => {
235 log::warn!(
236 "global shortcuts: {keys} could not be registered — another \
237 application may hold it ({err})"
238 );
239 registered.refused.push(keys.clone());
240 }
241 }
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
253 fn a_combination_has_one_spelling() {
254 assert_eq!(
255 canonical("Ctrl+Shift+K", false).as_deref(),
256 Ok("CONTROL+SHIFT+K")
257 );
258 assert_eq!(
259 canonical("shift + ctrl + k", false).as_deref(),
260 Ok("CONTROL+SHIFT+K")
261 );
262 assert_eq!(canonical("Alt+Option+F5", false).as_deref(), Ok("ALT+F5"));
263 assert_eq!(canonical("Cmd+Space", true).as_deref(), Ok("SUPER+SPACE"));
264 }
265
266 #[test]
268 fn primary_is_the_platforms_own() {
269 assert_eq!(
270 canonical("Primary+Shift+Space", true).as_deref(),
271 Ok("SHIFT+SUPER+SPACE")
272 );
273 assert_eq!(
274 canonical("Primary+Shift+Space", false).as_deref(),
275 Ok("CONTROL+SHIFT+SPACE")
276 );
277 }
278
279 #[test]
282 fn what_would_take_typing_is_refused() {
283 assert!(canonical("K", false).is_err());
284 assert!(canonical("Shift+K", false).is_err());
285 assert!(canonical("Ctrl+Shift", false).is_err(), "no key");
286 assert!(canonical("Ctrl+K+L", false).is_err(), "two keys");
287 assert!(canonical("Ctrl++K", false).is_err(), "an empty part");
288 assert!(canonical("Ctrl+", false).is_err(), "an empty key");
289 assert!(canonical("Alt+K", false).is_ok());
290 assert!(canonical("Super+K", false).is_ok());
291 }
292
293 #[cfg(all(desktop, feature = "global-shortcuts"))]
296 #[test]
297 fn the_spelling_is_the_systems() {
298 use std::str::FromStr;
299 for keys in [
300 "Ctrl+Shift+K",
301 "Primary+Alt+Space",
302 "Alt+F5",
303 "Ctrl+Alt+Shift+ArrowUp",
304 "Super+1",
305 ] {
306 let spelled = canonical(keys, cfg!(target_os = "macos")).unwrap();
307 assert!(
308 global_hotkey::hotkey::HotKey::from_str(&spelled).is_ok(),
309 "{keys} -> {spelled}"
310 );
311 }
312 }
313}