import Carbon.HIToolbox import Foundation import Observation /// Global talk key (works while Musubi is in the background). Default: ⌃⌥ Space. /// /// Uses Carbon hot keys, which need no Accessibility permission and report press and release. @MainActor @Observable final class TalkKey { enum Mode: String, CaseIterable { case hold, toggle } var mode: Mode = Mode(rawValue: UserDefaults.standard.string(forKey: "talkMode") ?? "") ?? .hold { didSet { UserDefaults.standard.set(mode.rawValue, forKey: "talkMode") } } private(set) var isActive = false var onChange: ((Bool) -> Void)? let displayName = "⌃⌥ Leertaste" private var hotKey: EventHotKeyRef? private var handler: EventHandlerRef? func register() { var types = [ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)), ] InstallEventHandler(GetApplicationEventTarget(), talkKeyHandler, types.count, &types, Unmanaged.passUnretained(self).toOpaque(), &handler) let id = EventHotKeyID(signature: OSType(0x4D534249), id: 1) // 'MSBI' RegisterEventHotKey(UInt32(kVK_Space), UInt32(controlKey | optionKey), id, GetApplicationEventTarget(), 0, &hotKey) } /// Button or hot key pressed. func press() { switch mode { case .hold: set(true) case .toggle: set(!isActive) } } /// Button or hot key released. func release() { if mode == .hold { set(false) } } func set(_ active: Bool) { guard active != isActive else { return } isActive = active onChange?(active) } fileprivate func handle(kind: UInt32) { if kind == UInt32(kEventHotKeyPressed) { press() } else { release() } } } private func talkKeyHandler(_: EventHandlerCallRef?, event: EventRef?, userData: UnsafeMutableRawPointer?) -> OSStatus { guard let event, let userData else { return OSStatus(eventNotHandledErr) } let kind = GetEventKind(event) let key = Unmanaged.fromOpaque(userData).takeUnretainedValue() MainActor.assumeIsolated { key.handle(kind: kind) } return noErr }