86 lines
2.6 KiB
Swift
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
|
|
}
|
|
}
|