// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/global-shortcut/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut) //! //! Register global shortcuts. //! //! - Supported platforms: Windows, Linux and macOS. #![doc( html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] #![cfg(not(any(target_os = "android", target_os = "ios")))] use std::{ collections::HashMap, str::FromStr, sync::{Arc, Mutex}, }; pub use global_hotkey::hotkey::{Code, HotKey as Shortcut, Modifiers}; use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager}; use tauri::{ ipc::Channel, plugin::{Builder as PluginBuilder, TauriPlugin}, AppHandle, Manager, Runtime, State, Window, }; mod error; pub use error::Error; type Result = std::result::Result; type HotKeyId = u32; type HandlerFn = Box, &Shortcut) + Send + Sync + 'static>; pub struct ShortcutWrapper(Shortcut); impl From for ShortcutWrapper { fn from(value: Shortcut) -> Self { Self(value) } } impl TryFrom<&str> for ShortcutWrapper { type Error = global_hotkey::Error; fn try_from(value: &str) -> std::result::Result { Shortcut::from_str(value).map(ShortcutWrapper) } } struct RegisteredShortcut { shortcut: Shortcut, handler: Option>>, } pub struct GlobalShortcut { #[allow(dead_code)] app: AppHandle, manager: GlobalHotKeyManager, shortcuts: Arc>>>, } impl GlobalShortcut { fn register_internal, &Shortcut) + Send + Sync + 'static>( &self, shortcut: Shortcut, handler: Option, ) -> Result<()> { let id = shortcut.id(); let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn)); self.manager.register(shortcut)?; self.shortcuts .lock() .unwrap() .insert(id, RegisteredShortcut { shortcut, handler }); Ok(()) } fn register_all_internal(&self, shortcuts: S, handler: Option) -> Result<()> where S: IntoIterator, F: Fn(&AppHandle, &Shortcut) + Send + Sync + 'static, { let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn)); let hotkeys = shortcuts.into_iter().collect::>(); let mut shortcuts = self.shortcuts.lock().unwrap(); for shortcut in hotkeys { self.manager.register(shortcut)?; shortcuts.insert( shortcut.id(), RegisteredShortcut { shortcut, handler: handler.clone(), }, ); } Ok(()) } /// Register a shortcut. pub fn register(&self, shortcut: S) -> Result<()> where S: TryInto, S::Error: std::error::Error, { self.register_internal( try_into_shortcut(shortcut)?, None::, &Shortcut)>, ) } /// Register a shortcut with a handler. pub fn on_shortcut(&self, shortcut: S, handler: F) -> Result<()> where S: TryInto, S::Error: std::error::Error, F: Fn(&AppHandle, &Shortcut) + Send + Sync + 'static, { self.register_internal(try_into_shortcut(shortcut)?, Some(handler)) } /// Register multiple shortcuts. pub fn register_all(&self, shortcuts: S) -> Result<()> where S: IntoIterator, T: TryInto, T::Error: std::error::Error, { let mut s = Vec::new(); for shortcut in shortcuts { s.push(try_into_shortcut(shortcut)?); } self.register_all_internal(s, None::, &Shortcut)>) } /// Register multiple shortcuts with a handler. pub fn on_all_shortcuts(&self, shortcuts: S, handler: F) -> Result<()> where S: IntoIterator, T: TryInto, T::Error: std::error::Error, F: Fn(&AppHandle, &Shortcut) + Send + Sync + 'static, { let mut s = Vec::new(); for shortcut in shortcuts { s.push(try_into_shortcut(shortcut)?); } self.register_all_internal(s, Some(handler)) } pub fn unregister>(&self, shortcut: S) -> Result<()> where S::Error: std::error::Error, { let shortcut = try_into_shortcut(shortcut)?; self.manager.unregister(shortcut)?; self.shortcuts.lock().unwrap().remove(&shortcut.id()); Ok(()) } pub fn unregister_all, S: IntoIterator>( &self, shortcuts: S, ) -> Result<()> where T::Error: std::error::Error, { let mut s = Vec::new(); for shortcut in shortcuts { s.push(try_into_shortcut(shortcut)?); } self.manager.unregister_all(&s)?; let mut shortcuts = self.shortcuts.lock().unwrap(); for s in s { shortcuts.remove(&s.id()); } Ok(()) } /// Determines whether the given shortcut is registered by this application or not. /// /// If the shortcut is registered by another application, it will still return `false`. pub fn is_registered>(&self, shortcut: S) -> bool where S::Error: std::error::Error, { if let Ok(shortcut) = try_into_shortcut(shortcut) { self.shortcuts.lock().unwrap().contains_key(&shortcut.id()) } else { false } } } pub trait GlobalShortcutExt { fn global_shortcut(&self) -> &GlobalShortcut; } impl> GlobalShortcutExt for T { fn global_shortcut(&self) -> &GlobalShortcut { self.state::>().inner() } } fn parse_shortcut>(shortcut: S) -> Result { shortcut.as_ref().parse().map_err(Into::into) } fn try_into_shortcut>(shortcut: S) -> Result where S::Error: std::error::Error, { shortcut .try_into() .map(|s| s.0) .map_err(|e| Error::GlobalHotkey(e.to_string())) } #[tauri::command] fn register( _window: Window, global_shortcut: State<'_, GlobalShortcut>, shortcut: String, handler: Channel, ) -> Result<()> { global_shortcut.register_internal( parse_shortcut(&shortcut)?, Some(move |_app: &AppHandle, _shortcut: &Shortcut| { let _ = handler.send(&shortcut); }), ) } #[tauri::command] fn register_all( _window: Window, global_shortcut: State<'_, GlobalShortcut>, shortcuts: Vec, handler: Channel, ) -> Result<()> { let mut hotkeys = Vec::new(); let mut shortcut_map = HashMap::new(); for shortcut in shortcuts { let hotkey = parse_shortcut(&shortcut)?; shortcut_map.insert(hotkey.id(), shortcut); hotkeys.push(hotkey); } global_shortcut.register_all_internal( hotkeys, Some(move |_app: &AppHandle, shortcut: &Shortcut| { if let Some(shortcut_str) = shortcut_map.get(&shortcut.id()) { let _ = handler.send(shortcut_str); } }), ) } #[tauri::command] fn unregister( _app: AppHandle, global_shortcut: State<'_, GlobalShortcut>, shortcut: String, ) -> Result<()> { global_shortcut.unregister(parse_shortcut(shortcut)?) } #[tauri::command] fn unregister_all( _app: AppHandle, global_shortcut: State<'_, GlobalShortcut>, shortcuts: Vec, ) -> Result<()> { let mut hotkeys = Vec::new(); for shortcut in shortcuts { hotkeys.push(parse_shortcut(&shortcut)?); } global_shortcut.unregister_all(hotkeys) } #[tauri::command] fn is_registered( _app: AppHandle, global_shortcut: State<'_, GlobalShortcut>, shortcut: String, ) -> Result { Ok(global_shortcut.is_registered(parse_shortcut(shortcut)?)) } pub struct Builder { shortcuts: Vec, handler: Option>, } impl Default for Builder { fn default() -> Self { Self { shortcuts: Vec::new(), handler: Default::default(), } } } impl Builder { pub fn new() -> Self { Self::default() } /// Add a shortcut to be registerd. pub fn with_shortcut(mut self, shortcut: T) -> Result where T: TryInto, T::Error: std::error::Error, { self.shortcuts.push(try_into_shortcut(shortcut)?); Ok(self) } /// Add multiple shortcuts to be registerd. pub fn with_shortcuts(mut self, shortcuts: S) -> Result where S: IntoIterator, T: TryInto, T::Error: std::error::Error, { for shortcut in shortcuts { self.shortcuts.push(try_into_shortcut(shortcut)?); } Ok(self) } /// Specify a global shortcut handler that will be triggered for any and all shortcuts. pub fn with_handler, &Shortcut) + Send + Sync + 'static>( mut self, handler: F, ) -> Self { self.handler.replace(Box::new(handler)); self } pub fn build(self) -> TauriPlugin { let handler = self.handler; let shortcuts = self.shortcuts; PluginBuilder::new("global-shortcut") .js_init_script(include_str!("api-iife.js").to_string()) .invoke_handler(tauri::generate_handler![ register, register_all, unregister, unregister_all, is_registered ]) .setup(move |app, _api| { let manager = GlobalHotKeyManager::new()?; let mut store = HashMap::>::new(); for shortcut in shortcuts { manager.register(shortcut)?; store.insert( shortcut.id(), RegisteredShortcut { shortcut, handler: None, }, ); } let shortcuts = Arc::new(Mutex::new(store)); let shortcuts_ = shortcuts.clone(); let app_handle = app.clone(); GlobalHotKeyEvent::set_event_handler(Some(move |e: GlobalHotKeyEvent| { if let Some(shortcut) = shortcuts_.lock().unwrap().get(&e.id) { if let Some(handler) = &shortcut.handler { handler(&app_handle, &shortcut.shortcut); } if let Some(handler) = &handler { handler(&app_handle, &shortcut.shortcut); } } })); app.manage(GlobalShortcut { app: app.clone(), manager, shortcuts, }); Ok(()) }) .build() } }