tauri_plugin_global_shortcut/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Register global shortcuts.
6//!
7//! - Supported platforms: Windows, Linux and macOS.
8
9#![doc(
10    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
11    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
12)]
13#![cfg(not(any(target_os = "android", target_os = "ios")))]
14
15use std::{
16    collections::HashMap,
17    str::FromStr,
18    sync::{Arc, Mutex},
19};
20
21use global_hotkey::GlobalHotKeyEvent;
22pub use global_hotkey::{
23    hotkey::{Code, HotKey as Shortcut, Modifiers},
24    GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState,
25};
26use serde::Serialize;
27use tauri::{
28    ipc::Channel,
29    plugin::{Builder as PluginBuilder, TauriPlugin},
30    AppHandle, Manager, Runtime, State,
31};
32
33mod error;
34
35pub use error::Error;
36type Result<T> = std::result::Result<T, Error>;
37
38type HotKeyId = u32;
39type HandlerFn<R> = Box<dyn Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static>;
40
41pub struct ShortcutWrapper(Shortcut);
42
43impl From<Shortcut> for ShortcutWrapper {
44    fn from(value: Shortcut) -> Self {
45        Self(value)
46    }
47}
48
49impl TryFrom<&str> for ShortcutWrapper {
50    type Error = global_hotkey::hotkey::HotKeyParseError;
51    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
52        Shortcut::from_str(value).map(ShortcutWrapper)
53    }
54}
55
56struct RegisteredShortcut<R: Runtime> {
57    shortcut: Shortcut,
58    handler: Option<Arc<HandlerFn<R>>>,
59}
60
61struct GlobalHotKeyManager(global_hotkey::GlobalHotKeyManager);
62
63/// SAFETY: we ensure it is run on main thread only
64unsafe impl Send for GlobalHotKeyManager {}
65/// SAFETY: we ensure it is run on main thread only
66unsafe impl Sync for GlobalHotKeyManager {}
67
68pub struct GlobalShortcut<R: Runtime> {
69    #[allow(dead_code)]
70    app: AppHandle<R>,
71    manager: Arc<GlobalHotKeyManager>,
72    shortcuts: Arc<Mutex<HashMap<HotKeyId, RegisteredShortcut<R>>>>,
73}
74
75macro_rules! run_main_thread {
76    ($handle:expr, $manager:expr, |$m:ident| $ex:expr) => {{
77        let (tx, rx) = std::sync::mpsc::channel();
78        let manager = $manager.clone();
79        let task = move || {
80            let f = |$m: &GlobalHotKeyManager| $ex;
81            let _ = tx.send(f(&*manager));
82        };
83        $handle.run_on_main_thread(task)?;
84        rx.recv()?
85    }};
86}
87
88impl<R: Runtime> GlobalShortcut<R> {
89    fn register_internal<F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static>(
90        &self,
91        shortcut: Shortcut,
92        handler: Option<F>,
93    ) -> Result<()> {
94        let id = shortcut.id();
95        let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn<R>));
96        run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?;
97        self.shortcuts
98            .lock()
99            .unwrap()
100            .insert(id, RegisteredShortcut { shortcut, handler });
101        Ok(())
102    }
103
104    fn register_multiple_internal<S, F>(&self, shortcuts: S, handler: Option<F>) -> Result<()>
105    where
106        S: IntoIterator<Item = Shortcut>,
107        F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static,
108    {
109        let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn<R>));
110
111        let hotkeys = shortcuts.into_iter().collect::<Vec<_>>();
112
113        let mut shortcuts = self.shortcuts.lock().unwrap();
114        for shortcut in hotkeys {
115            run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?;
116            shortcuts.insert(
117                shortcut.id(),
118                RegisteredShortcut {
119                    shortcut,
120                    handler: handler.clone(),
121                },
122            );
123        }
124
125        Ok(())
126    }
127}
128
129impl<R: Runtime> GlobalShortcut<R> {
130    /// Register a shortcut.
131    pub fn register<S>(&self, shortcut: S) -> Result<()>
132    where
133        S: TryInto<ShortcutWrapper>,
134        S::Error: std::error::Error,
135    {
136        self.register_internal(
137            try_into_shortcut(shortcut)?,
138            None::<fn(&AppHandle<R>, &Shortcut, ShortcutEvent)>,
139        )
140    }
141
142    /// Register a shortcut with a handler.
143    pub fn on_shortcut<S, F>(&self, shortcut: S, handler: F) -> Result<()>
144    where
145        S: TryInto<ShortcutWrapper>,
146        S::Error: std::error::Error,
147        F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static,
148    {
149        self.register_internal(try_into_shortcut(shortcut)?, Some(handler))
150    }
151
152    /// Register multiple shortcuts.
153    pub fn register_multiple<S, T>(&self, shortcuts: S) -> Result<()>
154    where
155        S: IntoIterator<Item = T>,
156        T: TryInto<ShortcutWrapper>,
157        T::Error: std::error::Error,
158    {
159        let mut s = Vec::new();
160        for shortcut in shortcuts {
161            s.push(try_into_shortcut(shortcut)?);
162        }
163        self.register_multiple_internal(s, None::<fn(&AppHandle<R>, &Shortcut, ShortcutEvent)>)
164    }
165
166    /// Register multiple shortcuts with a handler.
167    pub fn on_shortcuts<S, T, F>(&self, shortcuts: S, handler: F) -> Result<()>
168    where
169        S: IntoIterator<Item = T>,
170        T: TryInto<ShortcutWrapper>,
171        T::Error: std::error::Error,
172        F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static,
173    {
174        let mut s = Vec::new();
175        for shortcut in shortcuts {
176            s.push(try_into_shortcut(shortcut)?);
177        }
178        self.register_multiple_internal(s, Some(handler))
179    }
180
181    /// Unregister a shortcut
182    pub fn unregister<S: TryInto<ShortcutWrapper>>(&self, shortcut: S) -> Result<()>
183    where
184        S::Error: std::error::Error,
185    {
186        let shortcut = try_into_shortcut(shortcut)?;
187        run_main_thread!(self.app, self.manager, |m| m.0.unregister(shortcut))?;
188        self.shortcuts.lock().unwrap().remove(&shortcut.id());
189        Ok(())
190    }
191
192    /// Unregister multiple shortcuts.
193    pub fn unregister_multiple<T: TryInto<ShortcutWrapper>, S: IntoIterator<Item = T>>(
194        &self,
195        shortcuts: S,
196    ) -> Result<()>
197    where
198        T::Error: std::error::Error,
199    {
200        let mut mapped_shortcuts = Vec::new();
201        for shortcut in shortcuts {
202            mapped_shortcuts.push(try_into_shortcut(shortcut)?);
203        }
204
205        {
206            let mapped_shortcuts = mapped_shortcuts.clone();
207            #[rustfmt::skip]
208            run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(&mapped_shortcuts))?;
209        }
210
211        let mut shortcuts = self.shortcuts.lock().unwrap();
212        for s in mapped_shortcuts {
213            shortcuts.remove(&s.id());
214        }
215
216        Ok(())
217    }
218
219    /// Unregister all registered shortcuts.
220    pub fn unregister_all(&self) -> Result<()> {
221        let mut shortcuts = self.shortcuts.lock().unwrap();
222        let hotkeys = std::mem::take(&mut *shortcuts);
223        let hotkeys = hotkeys.values().map(|s| s.shortcut).collect::<Vec<_>>();
224        #[rustfmt::skip]
225        let res = run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(hotkeys.as_slice()));
226        res.map_err(Into::into)
227    }
228
229    /// Determines whether the given shortcut is registered by this application or not.
230    ///
231    /// If the shortcut is registered by another application, it will still return `false`.
232    pub fn is_registered<S: TryInto<ShortcutWrapper>>(&self, shortcut: S) -> bool
233    where
234        S::Error: std::error::Error,
235    {
236        if let Ok(shortcut) = try_into_shortcut(shortcut) {
237            self.shortcuts.lock().unwrap().contains_key(&shortcut.id())
238        } else {
239            false
240        }
241    }
242}
243
244pub trait GlobalShortcutExt<R: Runtime> {
245    fn global_shortcut(&self) -> &GlobalShortcut<R>;
246}
247
248impl<R: Runtime, T: Manager<R>> GlobalShortcutExt<R> for T {
249    fn global_shortcut(&self) -> &GlobalShortcut<R> {
250        self.state::<GlobalShortcut<R>>().inner()
251    }
252}
253
254fn parse_shortcut<S: AsRef<str>>(shortcut: S) -> Result<Shortcut> {
255    shortcut.as_ref().parse().map_err(Into::into)
256}
257
258fn try_into_shortcut<S: TryInto<ShortcutWrapper>>(shortcut: S) -> Result<Shortcut>
259where
260    S::Error: std::error::Error,
261{
262    shortcut
263        .try_into()
264        .map(|s| s.0)
265        .map_err(|e| Error::GlobalHotkey(e.to_string()))
266}
267
268#[derive(Clone, Serialize)]
269struct ShortcutJsEvent {
270    shortcut: String,
271    id: u32,
272    state: ShortcutState,
273}
274
275#[tauri::command]
276fn register<R: Runtime>(
277    _app: AppHandle<R>,
278    global_shortcut: State<'_, GlobalShortcut<R>>,
279    shortcuts: Vec<String>,
280    handler: Channel<ShortcutJsEvent>,
281) -> Result<()> {
282    let mut hotkeys = Vec::new();
283
284    let mut shortcut_map = HashMap::new();
285    for shortcut in shortcuts {
286        let hotkey = parse_shortcut(&shortcut)?;
287        shortcut_map.insert(hotkey.id(), shortcut);
288        hotkeys.push(hotkey);
289    }
290
291    global_shortcut.register_multiple_internal(
292        hotkeys,
293        Some(
294            move |_app: &AppHandle<R>, shortcut: &Shortcut, e: ShortcutEvent| {
295                let js_event = ShortcutJsEvent {
296                    id: e.id,
297                    state: e.state,
298                    shortcut: shortcut.into_string(),
299                };
300                let _ = handler.send(js_event);
301            },
302        ),
303    )
304}
305
306#[tauri::command]
307fn unregister<R: Runtime>(
308    _app: AppHandle<R>,
309    global_shortcut: State<'_, GlobalShortcut<R>>,
310    shortcuts: Vec<String>,
311) -> Result<()> {
312    let mut hotkeys = Vec::new();
313    for shortcut in shortcuts {
314        hotkeys.push(parse_shortcut(&shortcut)?);
315    }
316    global_shortcut.unregister_multiple(hotkeys)
317}
318
319#[tauri::command]
320fn unregister_all<R: Runtime>(
321    _app: AppHandle<R>,
322    global_shortcut: State<'_, GlobalShortcut<R>>,
323) -> Result<()> {
324    global_shortcut.unregister_all()
325}
326
327#[tauri::command]
328fn is_registered<R: Runtime>(
329    _app: AppHandle<R>,
330    global_shortcut: State<'_, GlobalShortcut<R>>,
331    shortcut: String,
332) -> Result<bool> {
333    Ok(global_shortcut.is_registered(parse_shortcut(shortcut)?))
334}
335
336pub struct Builder<R: Runtime> {
337    shortcuts: Vec<Shortcut>,
338    handler: Option<HandlerFn<R>>,
339}
340
341impl<R: Runtime> Default for Builder<R> {
342    fn default() -> Self {
343        Self {
344            shortcuts: Vec::new(),
345            handler: Default::default(),
346        }
347    }
348}
349
350impl<R: Runtime> Builder<R> {
351    pub fn new() -> Self {
352        Self::default()
353    }
354
355    /// Add a shortcut to be registerd.
356    pub fn with_shortcut<T>(mut self, shortcut: T) -> Result<Self>
357    where
358        T: TryInto<ShortcutWrapper>,
359        T::Error: std::error::Error,
360    {
361        self.shortcuts.push(try_into_shortcut(shortcut)?);
362        Ok(self)
363    }
364
365    /// Add multiple shortcuts to be registerd.
366    pub fn with_shortcuts<S, T>(mut self, shortcuts: S) -> Result<Self>
367    where
368        S: IntoIterator<Item = T>,
369        T: TryInto<ShortcutWrapper>,
370        T::Error: std::error::Error,
371    {
372        for shortcut in shortcuts {
373            self.shortcuts.push(try_into_shortcut(shortcut)?);
374        }
375
376        Ok(self)
377    }
378
379    /// Specify a global shortcut handler that will be triggered for any and all shortcuts.
380    pub fn with_handler<F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static>(
381        mut self,
382        handler: F,
383    ) -> Self {
384        self.handler.replace(Box::new(handler));
385        self
386    }
387
388    pub fn build(self) -> TauriPlugin<R> {
389        let handler = self.handler;
390        let shortcuts = self.shortcuts;
391        PluginBuilder::new("global-shortcut")
392            .invoke_handler(tauri::generate_handler![
393                register,
394                unregister,
395                unregister_all,
396                is_registered,
397            ])
398            .setup(move |app, _api| {
399                let manager = global_hotkey::GlobalHotKeyManager::new()?;
400                let mut store = HashMap::<HotKeyId, RegisteredShortcut<R>>::new();
401                for shortcut in shortcuts {
402                    manager.register(shortcut)?;
403                    store.insert(
404                        shortcut.id(),
405                        RegisteredShortcut {
406                            shortcut,
407                            handler: None,
408                        },
409                    );
410                }
411
412                let shortcuts = Arc::new(Mutex::new(store));
413                let shortcuts_ = shortcuts.clone();
414
415                let app_handle = app.clone();
416                GlobalHotKeyEvent::set_event_handler(Some(move |e: GlobalHotKeyEvent| {
417                    if let Some(shortcut) = shortcuts_.lock().unwrap().get(&e.id) {
418                        if let Some(handler) = &shortcut.handler {
419                            handler(&app_handle, &shortcut.shortcut, e);
420                        }
421                        if let Some(handler) = &handler {
422                            handler(&app_handle, &shortcut.shortcut, e);
423                        }
424                    }
425                }));
426
427                app.manage(GlobalShortcut {
428                    app: app.clone(),
429                    manager: Arc::new(GlobalHotKeyManager(manager)),
430                    shortcuts,
431                });
432                Ok(())
433            })
434            .build()
435    }
436}