Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

188 lines
5.6 KiB
Swift

import Carbon
import MacToolsPluginKit
@MainActor
final class GlobalShortcutManager {
struct Registration: Equatable {
let shortcutID: String
let binding: ShortcutBinding
}
private struct RegisteredHotKey {
let binding: ShortcutBinding
let reference: EventHotKeyRef
let carbonID: UInt32
var shortcutIDs: [String]
}
private static let signature: OSType = 0x4D43544C
var onShortcutTriggered: ((String) -> Void)?
var onShortcutReleased: ((String) -> Void)?
private var handlerRef: EventHandlerRef?
private var registeredHotKeys: [ShortcutBinding: RegisteredHotKey] = [:]
private var shortcutIDsByCarbonID: [UInt32: [String]] = [:]
private var nextCarbonID: UInt32 = 1
init() {
installHandlerIfNeeded()
}
func updateBindings(_ registrations: [Registration]) {
installHandlerIfNeeded()
let targetGroups = registrations.reduce(into: [ShortcutBinding: [String]]()) { result, registration in
if result[registration.binding]?.contains(registration.shortcutID) == true {
return
}
result[registration.binding, default: []].append(registration.shortcutID)
}
for binding in Array(registeredHotKeys.keys) where targetGroups[binding] == nil {
unregister(binding: binding)
}
for (binding, shortcutIDs) in targetGroups {
if var existing = registeredHotKeys[binding] {
existing.shortcutIDs = shortcutIDs
registeredHotKeys[binding] = existing
shortcutIDsByCarbonID[existing.carbonID] = shortcutIDs
continue
}
register(binding: binding, shortcutIDs: shortcutIDs)
}
}
private func installHandlerIfNeeded() {
guard handlerRef == nil else {
return
}
let eventTypes = [
EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed)
),
EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyReleased)
)
]
// Register/listen on the event dispatcher target, matching Magnet and MASShortcut. In this
// app's run loop, global hotkey events are not routed to `GetApplicationEventTarget()`, so
// hotkeys can register successfully but never trigger callbacks.
_ = eventTypes.withUnsafeBufferPointer { buffer in
InstallEventHandler(
GetEventDispatcherTarget(),
Self.hotKeyHandler,
buffer.count,
buffer.baseAddress,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
&handlerRef
)
}
}
private func register(binding: ShortcutBinding, shortcutIDs: [String]) {
var hotKeyReference: EventHotKeyRef?
let carbonID = nextCarbonID
nextCarbonID += 1
let hotKeyID = EventHotKeyID(
signature: Self.signature,
id: carbonID
)
let status = RegisterEventHotKey(
UInt32(binding.keyCode),
binding.modifiers.carbonFlags,
hotKeyID,
GetEventDispatcherTarget(),
0,
&hotKeyReference
)
guard status == noErr, let hotKeyReference else {
return
}
registeredHotKeys[binding] = RegisteredHotKey(
binding: binding,
reference: hotKeyReference,
carbonID: carbonID,
shortcutIDs: shortcutIDs
)
shortcutIDsByCarbonID[carbonID] = shortcutIDs
}
private func unregister(binding: ShortcutBinding) {
guard let registered = registeredHotKeys.removeValue(forKey: binding) else {
return
}
shortcutIDsByCarbonID.removeValue(forKey: registered.carbonID)
UnregisterEventHotKey(registered.reference)
}
private func unregisterAll() {
for binding in Array(registeredHotKeys.keys) {
unregister(binding: binding)
}
}
private func dispatchShortcut(carbonID: UInt32, isReleased: Bool) {
guard let shortcutIDs = shortcutIDsByCarbonID[carbonID] else {
return
}
for shortcutID in shortcutIDs {
if isReleased {
onShortcutReleased?(shortcutID)
} else {
onShortcutTriggered?(shortcutID)
}
}
}
private nonisolated static let hotKeyHandler: EventHandlerUPP = { _, event, userData in
guard
let event,
let userData
else {
return OSStatus(eventNotHandledErr)
}
var hotKeyID = EventHotKeyID(signature: 0, id: 0)
let status = GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&hotKeyID
)
guard status == noErr else {
return status
}
guard hotKeyID.signature == 0x4D43544C else {
return OSStatus(eventNotHandledErr)
}
let manager = Unmanaged<GlobalShortcutManager>.fromOpaque(userData).takeUnretainedValue()
let isReleased = GetEventKind(event) == UInt32(kEventHotKeyReleased)
Task { @MainActor in
manager.dispatchShortcut(carbonID: hotKeyID.id, isReleased: isReleased)
}
return noErr
}
}