StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::{collections::HashMap, rc::Rc, str::FromStr, sync::Arc, thread}; |
| 2 | |
| 3 | use crate::keymap; |
| 4 | use crate::windowing::winit::app::CustomEvent; |
| 5 | use parking_lot::Mutex; |
| 6 | use winit::event_loop::EventLoopProxy; |
| 7 | |
| 8 | use global_hotkey::{ |
| 9 | hotkey::{Code, HotKey, Modifiers}, |
| 10 | GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, |
| 11 | }; |
| 12 | |
| 13 | /// Responsible for registering system-wide (global) hotkeys with the platform. |
| 14 | pub struct GlobalHotKeyHandler { |
| 15 | platform_manager: std::cell::OnceCell<GlobalHotKeyManager>, |
| 16 | /// Maps the [`global_hotkey::hotkey::HotKey::id`], an opaque, hash-based integer, to our |
| 17 | /// [`keymap::Keystroke`]. |
| 18 | hotkey_map: Arc<Mutex<HashMap<u32, keymap::Keystroke>>>, |
| 19 | event_loop_proxy: EventLoopProxy<CustomEvent>, |
| 20 | } |
| 21 | |
| 22 | impl GlobalHotKeyHandler { |
| 23 | pub fn new( |
| 24 | event_loop_proxy: EventLoopProxy<CustomEvent>, |
| 25 | ) -> Result<Self, global_hotkey::Error> { |
| 26 | Ok(Self { |
| 27 | platform_manager: Default::default(), |
| 28 | hotkey_map: Default::default(), |
| 29 | event_loop_proxy, |
| 30 | }) |
| 31 | } |
| 32 | |
| 33 | pub fn register(&self, shortcut: keymap::Keystroke) { |
| 34 | let hotkey = match hotkey_for_keystroke(&shortcut) { |
| 35 | Ok(hotkey) => hotkey, |
| 36 | Err(e) => { |
| 37 | log::error!("invalid global hotkey: {e:?}"); |
| 38 | return; |
| 39 | } |
| 40 | }; |
| 41 | self.platform_manager().register(hotkey); |
| 42 | self.hotkey_map.lock().insert(hotkey.id(), shortcut); |
| 43 | } |
| 44 | |
| 45 | pub fn unregister(&self, shortcut: &keymap::Keystroke) { |
| 46 | let hotkey = match hotkey_for_keystroke(shortcut) { |
| 47 | Ok(hotkey) => hotkey, |
| 48 | Err(e) => { |
| 49 | log::error!("invalid global hotkey: {e:?}"); |
| 50 | return; |
| 51 | } |
| 52 | }; |
| 53 | self.platform_manager().unregister(hotkey); |
| 54 | self.hotkey_map.lock().remove(&hotkey.id()); |
| 55 | } |
| 56 | |
| 57 | /// Returns a reference to a lazily-instantiated [`GlobalHotKeyManager`]. |
| 58 | /// |
| 59 | /// We do this lazily because the [`GlobalHotKeyManager`] can interfere |
| 60 | /// with other libraries that use Xlib, leading to crashes. We don't want |
| 61 | /// to run the risk of this happening for users who haven't set any global |
| 62 | /// hotkeys. |
| 63 | fn platform_manager(&self) -> &GlobalHotKeyManager { |
| 64 | self.platform_manager.get_or_init(|| { |
| 65 | let platform_manager = |
| 66 | GlobalHotKeyManager::new().expect("x11 implementation never actually fails"); |
| 67 | let thread_hotkey_map = self.hotkey_map.clone(); |
| 68 | // When global hotkeys are triggered, events get published to a crossbeam channel. |
| 69 | // Since crossbeam channels are not async, we don't want to receive this on our |
| 70 | // background executor's thread pool, as that would block a thread. Therefore, we spawn |
| 71 | // a dedicated thread for receiving these events. |
| 72 | let event_loop_proxy = self.event_loop_proxy.clone(); |
| 73 | thread::spawn(move || { |
| 74 | while let Ok(event) = GlobalHotKeyEvent::receiver().recv() { |
| 75 | // Trigger when the hotkey is released, _not_ pressed. This is due to an X11 |
| 76 | // quirk where focus is transferred out of Warp windows after a global hotkey |
| 77 | // is pressed. This breaks our quake mode logic. However, focus is restored |
| 78 | // when the hotkey is released. |
| 79 | if event.state == HotKeyState::Released { |
| 80 | // Lookup the hash-based hotkey ID to the actual keystroke from our |
| 81 | // map. |
| 82 | if let Some(keystroke) = thread_hotkey_map.lock().get(&event.id) { |
| 83 | event_loop_proxy.send_event(CustomEvent::GlobalShortcutTriggered( |
| 84 | keystroke.clone(), |
| 85 | )); |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | }); |
| 90 | platform_manager |
| 91 | }) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | fn hotkey_for_keystroke( |
| 96 | keystroke: &keymap::Keystroke, |
| 97 | ) -> std::result::Result<HotKey, anyhow::Error> { |
| 98 | let mut mods = Modifiers::empty(); |
| 99 | if keystroke.alt { |
| 100 | mods |= Modifiers::ALT; |
| 101 | } |
| 102 | if keystroke.cmd { |
| 103 | mods |= Modifiers::SUPER; |
| 104 | } |
| 105 | if keystroke.shift { |
| 106 | mods |= Modifiers::SHIFT; |
| 107 | } |
| 108 | if keystroke.ctrl { |
| 109 | mods |= Modifiers::CONTROL; |
| 110 | } |
| 111 | if keystroke.meta { |
| 112 | mods |= Modifiers::META; |
| 113 | } |
| 114 | let key = if keystroke.key.len() == 1 { |
| 115 | let c = keystroke |
| 116 | .key |
| 117 | .chars() |
| 118 | .next() |
| 119 | .expect("validated length already"); |
| 120 | match c { |
| 121 | '`' | '~' => Code::Backquote, |
| 122 | '-' | '_' => Code::Minus, |
| 123 | '=' | '+' => Code::Equal, |
| 124 | '0'..='9' => Code::from_str(&format!("Digit{c}"))?, |
| 125 | '\t' => Code::Tab, |
| 126 | '!' => Code::Digit1, |
| 127 | '@' => Code::Digit2, |
| 128 | '#' => Code::Digit3, |
| 129 | '$' => Code::Digit4, |
| 130 | '%' => Code::Digit5, |
| 131 | '^' => Code::Digit6, |
| 132 | '&' => Code::Digit7, |
| 133 | '*' => Code::Digit8, |
| 134 | '(' => Code::Digit9, |
| 135 | ')' => Code::Digit0, |
| 136 | 'a'..='z' | 'A'..='Z' => Code::from_str(&format!("Key{}", c.to_ascii_uppercase()))?, |
| 137 | '[' | '{' => Code::BracketLeft, |
| 138 | ']' | '}' => Code::BracketRight, |
| 139 | '\\' | '|' => Code::Backslash, |
| 140 | ';' => Code::Semicolon, |
| 141 | '\'' | '"' => Code::Quote, |
| 142 | ',' | '<' => Code::Comma, |
| 143 | '.' | '>' => Code::Period, |
| 144 | '/' | '?' => Code::Slash, |
| 145 | 'ろ' => Code::IntlRo, |
| 146 | '¥' => Code::IntlYen, |
| 147 | ' ' => Code::Space, |
| 148 | _ => anyhow::bail!("Invalid global hotkey: {c}"), |
| 149 | } |
| 150 | } else { |
| 151 | // Must map each of [`keymap::VALID_SPECIAL_KEYS`] to [`global_hotkey::hotkey::Code`]. |
| 152 | match keystroke.key.as_str() { |
| 153 | "backspace" => Code::Backspace, |
| 154 | "tab" => Code::Tab, |
| 155 | "enter" => Code::Enter, |
| 156 | "up" => Code::ArrowUp, |
| 157 | "down" => Code::ArrowDown, |
| 158 | "left" => Code::ArrowLeft, |
| 159 | "right" => Code::ArrowRight, |
| 160 | "home" => Code::Home, |
| 161 | "end" => Code::End, |
| 162 | "pageup" => Code::PageUp, |
| 163 | "pagedown" => Code::PageDown, |
| 164 | "insert" => Code::Insert, |
| 165 | "delete" => Code::Delete, |
| 166 | "escape" => Code::Escape, |
| 167 | "numpadenter" => Code::NumpadEnter, |
| 168 | "f1" => Code::F1, |
| 169 | "f2" => Code::F2, |
| 170 | "f3" => Code::F3, |
| 171 | "f4" => Code::F4, |
| 172 | "f5" => Code::F5, |
| 173 | "f6" => Code::F6, |
| 174 | "f7" => Code::F7, |
| 175 | "f8" => Code::F8, |
| 176 | "f9" => Code::F9, |
| 177 | "f10" => Code::F10, |
| 178 | "f11" => Code::F11, |
| 179 | "f12" => Code::F12, |
| 180 | "f13" => Code::F13, |
| 181 | "f14" => Code::F14, |
| 182 | "f15" => Code::F15, |
| 183 | "f16" => Code::F16, |
| 184 | "f17" => Code::F17, |
| 185 | "f18" => Code::F18, |
| 186 | "f19" => Code::F19, |
| 187 | "f20" => Code::F20, |
| 188 | s => anyhow::bail!("Invalid global hotkey: {s}"), |
| 189 | } |
| 190 | }; |
| 191 | |
| 192 | Ok(HotKey::new(Some(mods), key)) |
| 193 | } |
| 194 |