Skip to content
global-hotkey logo

Global Hotkey

global-hotkey

Cross-platform global keyboard shortcuts

SKILL.md

Full skill instructions

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 hotkey
  • unregister(hotkey: HotKey) - Unregister a hotkey
  • register_all(&[HotKey]) - Register multiple hotkeys
  • unregister_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 keys
  • key: Code - The main key
  • id: u32 - Unique ID (hash of modifiers + key)

Methods:

  • id() - Get the hotkey's unique ID
  • matches(modifiers, key) - Check if modifiers/​key match
  • into_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::CONTROL
  • Modifiers::META (Cmd on macOS, Win on Windows)
  • Modifiers::SHIFT
  • Modifiers::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 triggered
  • state: HotKeyState - Pressed or Released

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 hotkey
  • FailedToRegister(String) - System rejected (reserved hotkey)
  • FailedToUnRegister(HotKey) - Unregister failed
  • OsError(io::Error) - Platform-specific error
  • HotKeyParseError(String) - Invalid hotkey string
  • UnrecognizedHotKeyCode(String) - Unknown key code

Usage in script-kit-gpui

File Structure

  • src/​hotkeys.rs - Main hotkey management with unified routing
  • src/​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 HotKey
  • tracing - Add tracing instrumentation

See Also

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View