Agent skill
global-hotkey
Cross-platform global keyboard shortcuts
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/global-hotkey
SKILL.md
global-hotkey
Cross-platform Rust library for registering system-wide keyboard shortcuts that work even when your application isn't focused. Part of the Tauri ecosystem.
Crate: global-hotkey (v0.7.0) Docs: https://docs.rs/global-hotkey
Platforms Supported
- Windows: Requires a win32 event loop running on the thread
- macOS: Requires an event loop on the main thread
- Linux: X11 only (Wayland not supported)
Key Types
GlobalHotKeyManager
The central manager that handles hotkey registration with the OS.
use global_hotkey::GlobalHotKeyManager;
let manager = GlobalHotKeyManager::new().unwrap();
Methods:
new()- Create a new manager (must be on correct thread per platform)register(hotkey: HotKey)- Register a single hotkeyunregister(hotkey: HotKey)- Unregister a hotkeyregister_all(&[HotKey])- Register multiple hotkeysunregister_all(&[HotKey])- Unregister multiple hotkeys
HotKey
Represents a keyboard shortcut (modifiers + key).
use global_hotkey::hotkey::{HotKey, Code, Modifiers};
// Programmatic construction
let hotkey = HotKey::new(Some(Modifiers::SHIFT | Modifiers::META), Code::KeyK);
// Parse from string (modifiers before key)
let hotkey: HotKey = "shift+alt+KeyQ".parse().unwrap();
Fields:
mods: Modifiers- Modifier keyskey: Code- The main keyid: u32- Unique ID (hash of modifiers + key)
Methods:
id()- Get the hotkey's unique IDmatches(modifiers, key)- Check if modifiers/key matchinto_string()- Convert to string representation
Modifiers
Bitflags for modifier keys:
use global_hotkey::hotkey::Modifiers;
let mods = Modifiers::META | Modifiers::SHIFT;
let mods = Modifiers::CONTROL | Modifiers::ALT;
let mods = Modifiers::empty(); // No modifiers
Modifiers::ALT(Option on macOS)Modifiers::CONTROLModifiers::META(Cmd on macOS, Win on Windows)Modifiers::SHIFTModifiers::SUPER(alias for META)
Constant: CMD_OR_CTRL - META on macOS, CONTROL elsewhere
Code
Physical key codes (from keyboard-types crate):
use global_hotkey::hotkey::Code;
// Letters
Code::KeyA, Code::KeyB, ..., Code::KeyZ
// Numbers
Code::Digit0, Code::Digit1, ..., Code::Digit9
// Function keys
Code::F1, Code::F2, ..., Code::F12
// Special keys
Code::Space, Code::Enter, Code::Tab, Code::Escape
Code::Backspace, Code::Delete
Code::ArrowUp, Code::ArrowDown, Code::ArrowLeft, Code::ArrowRight
Code::Home, Code::End, Code::PageUp, Code::PageDown
// Punctuation
Code::Semicolon, Code::Quote, Code::Comma, Code::Period
Code::Slash, Code::Backslash, Code::Minus, Code::Equal
Code::BracketLeft, Code::BracketRight, Code::Backquote
GlobalHotKeyEvent
Event emitted when a hotkey is pressed or released.
use global_hotkey::{GlobalHotKeyEvent, HotKeyState};
let receiver = GlobalHotKeyEvent::receiver();
// Blocking receive
if let Ok(event) = receiver.recv() {
if event.state == HotKeyState::Pressed {
println!("Hotkey {} pressed", event.id);
}
}
// Non-blocking
if let Ok(event) = receiver.try_recv() {
// Handle event
}
Fields:
id: u32- The hotkey ID that was triggeredstate: HotKeyState-PressedorReleased
Error Types
use global_hotkey::Error;
match manager.register(hotkey) {
Ok(()) => println!("Registered"),
Err(Error::AlreadyRegistered(hk)) => {
println!("Hotkey {} already registered", hk.id());
}
Err(Error::FailedToRegister(msg)) => {
println!("System rejected: {}", msg);
}
Err(Error::OsError(e)) => {
println!("OS error: {}", e);
}
Err(e) => println!("Other error: {}", e),
}
Variants:
AlreadyRegistered(HotKey)- Another app has this hotkeyFailedToRegister(String)- System rejected (reserved hotkey)FailedToUnRegister(HotKey)- Unregister failedOsError(io::Error)- Platform-specific errorHotKeyParseError(String)- Invalid hotkey stringUnrecognizedHotKeyCode(String)- Unknown key code
Usage in script-kit-gpui
File Structure
src/hotkeys.rs- Main hotkey management with unified routingsrc/shortcuts/hotkey_compat.rs- Shortcut string parsing
Architecture
script-kit-gpui uses a unified routing table pattern:
// Global manager stored in OnceLock
static MAIN_MANAGER: OnceLock<Mutex<GlobalHotKeyManager>> = OnceLock::new();
// Routing table maps hotkey ID -> action
struct HotkeyRoutes {
routes: HashMap<u32, RegisteredHotkey>,
script_paths: HashMap<String, u32>, // Reverse lookup
main_id: Option<u32>,
notes_id: Option<u32>,
ai_id: Option<u32>,
}
Hotkey Actions
pub enum HotkeyAction {
Main, // Main launcher
Notes, // Notes window
Ai, // AI window
Script(String), // Run script at path
}
Registration Pattern
Transactional rebind - register new BEFORE unregistering old:
fn rebind_hotkey_transactional(
manager: &GlobalHotKeyManager,
action: HotkeyAction,
mods: Modifiers,
code: Code,
display: &str,
) -> bool {
let new_hotkey = HotKey::new(Some(mods), code);
// 1. Register new first (fail-safe)
if let Err(e) = manager.register(new_hotkey) {
return false; // Keep existing hotkey working
}
// 2. Update routing table
// 3. Unregister old (best-effort)
}
Shortcut String Parsing
The parse_shortcut function supports flexible formats:
// All equivalent:
parse_shortcut("cmd shift k") // Space-separated
parse_shortcut("cmd+shift+k") // Plus-separated
parse_shortcut("Cmd + Shift + K") // Mixed with spaces
// Modifier aliases:
// cmd, command, meta, super, win -> Modifiers::META
// ctrl, control, ctl -> Modifiers::CONTROL
// alt, opt, option -> Modifiers::ALT
// shift, shft -> Modifiers::SHIFT
Event Loop Integration
On macOS, script-kit-gpui uses GCD dispatch for immediate main-thread execution:
#[cfg(target_os = "macos")]
mod gcd {
pub fn dispatch_to_main<F: FnOnce() + Send + 'static>(f: F) {
// Uses dispatch_async_f to run on main thread
// Works even before GPUI event loop is "warmed up"
}
}
Listener Thread
pub(crate) fn start_hotkey_listener(config: Config) {
std::thread::spawn(move || {
let manager = GlobalHotKeyManager::new().unwrap();
// Register builtin hotkeys (main, notes, ai)
// Register script shortcuts
let receiver = GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = receiver.recv() {
if event.state != HotKeyState::Pressed {
continue;
}
// Look up action in routing table
let action = routes().read().unwrap().get_action(event.id);
match action {
Some(HotkeyAction::Main) => { /* toggle main window */ }
Some(HotkeyAction::Script(path)) => { /* run script */ }
// ...
}
}
}
});
}
Platform Differences
macOS
- Thread requirement: Manager must be created on main thread
- Event loop: Requires NSApplication run loop
- Reserved shortcuts: Some system shortcuts can't be overridden (Cmd+Tab, Cmd+Space if Spotlight enabled)
- Accessibility: May require accessibility permissions
Windows
- Thread requirement: Manager must be on same thread as win32 event loop
- Not necessarily main thread: Can be any thread with a message pump
- Reserved shortcuts: Win+L (lock), Ctrl+Alt+Del, etc.
Linux (X11 only)
- Wayland: NOT SUPPORTED - use X11
- X11 extensions: Requires XInput for hotkey capture
- Root window: Grabs are registered on root window
Anti-patterns
1. Creating Manager on Wrong Thread
// WRONG on macOS - will fail silently
std::thread::spawn(|| {
let manager = GlobalHotKeyManager::new(); // May work but events won't fire
});
// CORRECT - create on main thread, store globally
let manager = GlobalHotKeyManager::new().unwrap();
static MANAGER: OnceLock<Mutex<GlobalHotKeyManager>> = OnceLock::new();
MANAGER.set(Mutex::new(manager)).unwrap();
2. Not Storing HotKey for Unregister
// WRONG - can't unregister later
manager.register(HotKey::new(Some(mods), code));
// CORRECT - store the HotKey object
let hotkey = HotKey::new(Some(mods), code);
stored_hotkeys.insert(hotkey.id(), hotkey);
manager.register(hotkey);
// Later...
manager.unregister(stored_hotkeys.remove(&id).unwrap());
3. Ignoring Registration Errors
// WRONG - silent failure
let _ = manager.register(hotkey);
// CORRECT - handle errors
match manager.register(hotkey) {
Ok(()) => log!("Registered {}", shortcut),
Err(Error::AlreadyRegistered(_)) => {
log!("Conflict: {} used by another app", shortcut);
}
Err(e) => log!("Failed: {}", e),
}
4. Unregistering Before New Registration
// WRONG - user loses hotkey if new registration fails
manager.unregister(old_hotkey);
if manager.register(new_hotkey).is_err() {
// Old hotkey is gone, new didn't work - user has no hotkey!
}
// CORRECT - transactional: register new first
if manager.register(new_hotkey).is_ok() {
manager.unregister(old_hotkey); // Safe to remove old
} else {
// Old hotkey still works
}
5. Not Handling HotKeyState
// WRONG - fires twice (press and release)
if let Ok(event) = receiver.recv() {
run_action(event.id);
}
// CORRECT - only handle press events
if let Ok(event) = receiver.recv() {
if event.state == HotKeyState::Pressed {
run_action(event.id);
}
}
6. Blocking Main Thread with recv()
// WRONG on macOS - blocks the main thread event loop
fn main() {
let receiver = GlobalHotKeyEvent::receiver();
loop {
receiver.recv(); // Blocks forever, NSApplication never runs
}
}
// CORRECT - run listener in separate thread
std::thread::spawn(|| {
let receiver = GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = receiver.recv() {
// Dispatch to main thread
}
}
});
// Main thread runs the GUI event loop
Feature Flags
serde- Enable serialization for HotKeytracing- Add tracing instrumentation
See Also
- keyboard-types - Key code definitions
- tao - Window library (Tauri)
- winit - Alternative window library
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?