global-hotkeys
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGlobal Hotkeys — System-Wide Keyboard Shortcuts
全局快捷键 — 系统级键盘快捷键
Critical Constraints
关键约束
- ❌ DO NOT use only → ✅ Also add
addGlobalMonitorForEvents(global doesn't fire when YOUR app is focused)addLocalMonitorForEvents - ❌ DO NOT forget Accessibility permission → ✅ Global event monitoring requires
AXIsProcessTrusted() - ❌ DO NOT compare raw → ✅ Mask with
modifierFlagsfirst.deviceIndependentFlagsMask - ❌ DO NOT use NSEvent for system-wide hotkeys in sandboxed apps → ✅ Use Carbon for reliability
RegisterEventHotKey
- ❌ 请勿仅使用→ ✅ 同时添加
addGlobalMonitorForEvents(当你的应用处于焦点时,全局监控不会触发)addLocalMonitorForEvents - ❌ 请勿忘记辅助功能权限 → ✅ 全局事件监控需要授权
AXIsProcessTrusted() - ❌ 请勿直接比较原始→ ✅ 先使用
modifierFlags进行掩码处理.deviceIndependentFlagsMask - ❌ 沙盒应用中请勿使用NSEvent实现系统级快捷键 → ✅ 使用Carbon 以确保可靠性
RegisterEventHotKey
Decision Tree
决策树
In-app shortcut only?
└── SwiftUI .onKeyPress or .keyboardShortcut
System-wide, non-sandboxed?
├── Simple → NSEvent global + local monitors
└── Reliable → Carbon RegisterEventHotKey
System-wide, sandboxed (App Store)?
└── Carbon RegisterEventHotKey + Accessibility entitlement仅需应用内快捷键?
└── SwiftUI .onKeyPress 或 .keyboardShortcut
系统级需求,非沙盒应用?
├── 简单实现 → NSEvent 全局+本地监控
└── 可靠实现 → Carbon RegisterEventHotKey
系统级需求,沙盒应用(App Store)?
└── Carbon RegisterEventHotKey + 辅助功能权限Method 1: NSEvent Monitors (Simple)
方法1:NSEvent监控(简单实现)
swift
import AppKit
class HotkeyManager {
private var globalMonitor: Any?
private var localMonitor: Any?
var onHotkey: (() -> Void)?
var modifiers: NSEvent.ModifierFlags = [.command, .shift]
var keyCode: UInt16 = 49 // Space
func start() {
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.handleEvent(event)
}
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.handleEvent(event)
return event
}
}
func stop() {
if let m = globalMonitor { NSEvent.removeMonitor(m) }
if let m = localMonitor { NSEvent.removeMonitor(m) }
}
private func handleEvent(_ event: NSEvent) {
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if flags == modifiers && event.keyCode == keyCode {
DispatchQueue.main.async { self.onHotkey?() }
}
}
}swift
import AppKit
class HotkeyManager {
private var globalMonitor: Any?
private var localMonitor: Any?
var onHotkey: (() -> Void)?
var modifiers: NSEvent.ModifierFlags = [.command, .shift]
var keyCode: UInt16 = 49 // Space
func start() {
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.handleEvent(event)
}
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.handleEvent(event)
return event
}
}
func stop() {
if let m = globalMonitor { NSEvent.removeMonitor(m) }
if let m = localMonitor { NSEvent.removeMonitor(m) }
}
private func handleEvent(_ event: NSEvent) {
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if flags == modifiers && event.keyCode == keyCode {
DispatchQueue.main.async { self.onHotkey?() }
}
}
}Method 2: Carbon API (Reliable, System-Wide)
方法2:Carbon API(可靠的系统级实现)
swift
import Carbon
class CarbonHotkeyManager {
private var hotkeyRef: EventHotKeyRef?
private var eventHandler: EventHandlerRef?
var onHotkey: (() -> Void)?
func register(keyCode: UInt32, modifiers: UInt32) {
unregister()
var hotkeyID = EventHotKeyID()
hotkeyID.signature = OSType("HTKY".fourCharCodeValue)
hotkeyID.id = 1
var eventType = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed)
)
let handler: EventHandlerUPP = { _, event, userData in
let mgr = Unmanaged<CarbonHotkeyManager>.fromOpaque(userData!).takeUnretainedValue()
mgr.onHotkey?()
return noErr
}
InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType,
Unmanaged.passUnretained(self).toOpaque(), &eventHandler)
RegisterEventHotKey(keyCode, modifiers, hotkeyID,
GetApplicationEventTarget(), 0, &hotkeyRef)
}
func unregister() {
if let ref = hotkeyRef { UnregisterEventHotKey(ref) }
if let h = eventHandler { RemoveEventHandler(h) }
}
}
extension String {
var fourCharCodeValue: FourCharCode {
utf8.reduce(0) { ($0 << 8) + FourCharCode($1) }
}
}swift
import Carbon
class CarbonHotkeyManager {
private var hotkeyRef: EventHotKeyRef?
private var eventHandler: EventHandlerRef?
var onHotkey: (() -> Void)?
func register(keyCode: UInt32, modifiers: UInt32) {
unregister()
var hotkeyID = EventHotKeyID()
hotkeyID.signature = OSType("HTKY".fourCharCodeValue)
hotkeyID.id = 1
var eventType = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed)
)
let handler: EventHandlerUPP = { _, event, userData in
let mgr = Unmanaged<CarbonHotkeyManager>.fromOpaque(userData!).takeUnretainedValue()
mgr.onHotkey?()
return noErr
}
InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType,
Unmanaged.passUnretained(self).toOpaque(), &eventHandler)
RegisterEventHotKey(keyCode, modifiers, hotkeyID,
GetApplicationEventTarget(), 0, &hotkeyRef)
}
func unregister() {
if let ref = hotkeyRef { UnregisterEventHotKey(ref) }
if let h = eventHandler { RemoveEventHandler(h) }
}
}
extension String {
var fourCharCodeValue: FourCharCode {
utf8.reduce(0) { ($0 << 8) + FourCharCode($1) }
}
}Carbon Modifier Constants
Carbon修饰符常量
swift
// Carbon modifier flags for RegisterEventHotKey
let cmdKey: UInt32 = UInt32(cmdKey) // 256
let shiftKey: UInt32 = UInt32(shiftKey) // 512
let optionKey: UInt32 = UInt32(optionKey) // 2048
let controlKey: UInt32 = UInt32(controlKey) // 4096
// Example: Cmd+Shift+Space
register(keyCode: 49, modifiers: UInt32(cmdKey) | UInt32(shiftKey))swift
// 用于RegisterEventHotKey的Carbon修饰符标志
let cmdKey: UInt32 = UInt32(cmdKey) // 256
let shiftKey: UInt32 = UInt32(shiftKey) // 512
let optionKey: UInt32 = UInt32(optionKey) // 2048
let controlKey: UInt32 = UInt32(controlKey) // 4096
// 示例:Cmd+Shift+Space
register(keyCode: 49, modifiers: UInt32(cmdKey) | UInt32(shiftKey))Common Key Codes
常用按键代码
| Key | Code | Key | Code | Key | Code |
|---|---|---|---|---|---|
| Space | 49 | Return | 36 | Escape | 53 |
| Tab | 48 | Delete | 51 | A | 0 |
| S | 1 | D | 2 | F | 3 |
| J | 38 | K | 40 | L | 37 |
| 按键 | 代码 | 按键 | 代码 | 按键 | 代码 |
|---|---|---|---|---|---|
| 空格 | 49 | 回车 | 36 | 退出 | 53 |
| 制表符 | 48 | 删除 | 51 | A | 0 |
| S | 1 | D | 2 | F | 3 |
| J | 38 | K | 40 | L | 37 |
SwiftUI In-App Shortcuts
SwiftUI应用内快捷键
swift
// Keyboard shortcut on button
Button("Save") { save() }
.keyboardShortcut("s", modifiers: .command)
// Raw key press handler
.onKeyPress(.escape) { dismiss(); return .handled }
.onKeyPress(.upArrow) { moveUp(); return .handled }
.onKeyPress(.downArrow) { moveDown(); return .handled }swift
// 按钮绑定键盘快捷键
Button("保存") { save() }
.keyboardShortcut("s", modifiers: .command)
// 原生按键事件处理
.onKeyPress(.escape) { dismiss(); return .handled }
.onKeyPress(.upArrow) { moveUp(); return .handled }
.onKeyPress(.downArrow) { moveDown(); return .handled }Accessibility Permission Check
辅助功能权限检查
swift
func checkAccessibilityPermissions() -> Bool {
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true]
return AXIsProcessTrustedWithOptions(options as CFDictionary)
}
// Non-prompting check
func isAccessibilityGranted() -> Bool {
AXIsProcessTrusted()
}swift
func checkAccessibilityPermissions() -> Bool {
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true]
return AXIsProcessTrustedWithOptions(options as CFDictionary)
}
// 无提示检查
func isAccessibilityGranted() -> Bool {
AXIsProcessTrusted()
}Common Mistakes & Fixes
常见错误与修复方案
| Mistake | Fix |
|---|---|
| Hotkey works everywhere except own app | Add local monitor alongside global monitor |
| Modifier comparison fails | Mask with |
| Hotkey fires twice | Debounce with timestamp check (0.3s threshold) |
| Doesn't work on first launch | Check/request Accessibility permission |
| 错误 | 修复方案 |
|---|---|
| 快捷键在其他应用正常,但在自身应用中无效 | 同时添加本地监控与全局监控 |
| 修饰符比较失败 | 使用 |
| 快捷键触发两次 | 通过时间戳检查实现防抖(阈值0.3秒) |
| 首次启动时无法工作 | 检查/请求辅助功能权限 |