You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
64 lines
2.3 KiB
64 lines
2.3 KiB
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<TalkKey>.fromOpaque(userData).takeUnretainedValue()
|
|
MainActor.assumeIsolated { key.handle(kind: kind) }
|
|
return noErr
|
|
}
|