Files
tinystt/Sources/TinySTT/UI/HotkeyRecorder.swift
T

86 lines
2.6 KiB
Swift

import AppKit
import SwiftUI
@MainActor
final class HotkeyRecorderModel: ObservableObject {
@Published private(set) var isRecording = false
var onCapture: ((HotkeySpec) -> Void)?
private var monitor: Any?
private var pendingModifier: Int?
func begin() {
guard !isRecording else { return }
isRecording = true
NotificationCenter.default.post(name: .init("tinystt.recorder"), object: true)
pendingModifier = nil
monitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
guard let self else { return event }
self.handle(event)
return nil
}
}
func end() {
if let monitor {
NSEvent.removeMonitor(monitor)
}
monitor = nil
pendingModifier = nil
isRecording = false
NotificationCenter.default.post(name: .init("tinystt.recorder"), object: false)
}
private func handle(_ event: NSEvent) {
if event.type == .keyDown {
let code = Int(event.keyCode)
if code == 53 {
end()
return
}
let flags = event.modifierFlags.intersection([.command, .option, .control, .shift])
guard !flags.isEmpty else {
NSSound.beep()
return
}
onCapture?(HotkeySpec.make(kind: .combo, keyCode: code, modifiers: flags.cgEventRawValue))
end()
return
}
let code = Int(event.keyCode)
guard HotkeySpec.isModifierKeyCode(code) else { return }
let flag = HotkeySpec.nsFlag(forKeyCode: code)
if event.modifierFlags.contains(flag) {
pendingModifier = code
} else if pendingModifier == code {
onCapture?(HotkeySpec.make(kind: .modifier, keyCode: code, modifiers: 0))
end()
}
}
}
extension HotkeySpec {
static func nsFlag(forKeyCode code: Int) -> NSEvent.ModifierFlags {
switch code {
case 54, 55: return .command
case 56, 60: return .shift
case 58, 61: return .option
case 59, 62: return .control
case 63: return .function
default: return []
}
}
}
extension NSEvent.ModifierFlags {
var cgEventRawValue: UInt64 {
var raw: UInt64 = 0
if contains(.control) { raw |= CGEventFlags.maskControl.rawValue }
if contains(.option) { raw |= CGEventFlags.maskAlternate.rawValue }
if contains(.shift) { raw |= CGEventFlags.maskShift.rawValue }
if contains(.command) { raw |= CGEventFlags.maskCommand.rawValue }
return raw
}
}