From 8c6fe3eb07e48eaaa327ef49ce175c0bb9cbf78f Mon Sep 17 00:00:00 2001 From: ShihChi Huang Date: Sun, 26 Apr 2026 10:57:47 -0700 Subject: [PATCH 1/2] Make recognition hotkey configurable --- README.md | 7 +- TypelessMLX/Sources/AppState.swift | 16 +- TypelessMLX/Sources/HotkeyManager.swift | 155 ++++++++++++++++- TypelessMLX/Sources/RecognitionHotkey.swift | 159 ++++++++++++++++++ TypelessMLX/Sources/SettingsView.swift | 33 +++- .../Sources/SetupWindowController.swift | 2 +- TypelessMLX/Sources/StatusBarController.swift | 7 +- build-app.sh | 16 +- 8 files changed, 373 insertions(+), 22 deletions(-) create mode 100644 TypelessMLX/Sources/RecognitionHotkey.swift diff --git a/README.md b/README.md index e1be690..e1959d7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## 功能特色 -- 🎙️ **一鍵聽寫**:按右 Option(可自訂)開始/停止錄音,自動貼入游標位置 +- 🎙️ **一鍵聽寫**:按可自訂快捷鍵開始/停止錄音,自動貼入游標位置 - 🇹🇼 **台灣中文優化**:使用Mac 語音辨識,可選擇 Breeze-ASR-25 模型,繁體中文效果最佳 - ✏️ **自動標點符號**:語音辨識後自動補上逗號、句號、問號等標點 - 🤖 **AI 文字修正**(選用):macOS 26+ 可啟用 Apple Intelligence 進一步精煉標點與語句 @@ -85,7 +85,7 @@ export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" 1. 啟動後選單列出現麥克風圖示 2. 在任何輸入框游標點一下(讓視窗取得焦點) -3. **按住右 Option** → 說話 → **放開**,文字自動打入 +3. **按住設定中的辨識快捷鍵** → 說話 → **放開**,文字自動打入 > 可在設定中切換為「切換模式」(按一下開始,再按一下停止) @@ -104,7 +104,7 @@ export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" ## 架構說明 ``` -按快捷鍵 (Right Option) +按辨識快捷鍵 └─ HotkeyManager (NSEvent.flagsChanged) └─ AudioRecorder (AVAudioEngine, WAV) └─ WhisperBridge (stdin/stdout JSON-RPC) @@ -146,6 +146,7 @@ backend/ |------|------| | 模型 | 選擇語音辨識模型 | | 語言 | 自動偵測 / 指定語言代碼(如 `zh`、`en`) | +| 辨識快捷鍵 | 選擇用來開始/停止錄音的 modifier key | | 快捷鍵模式 | 按住模式 / 切換模式 | | 浮動指示器 | 錄音時顯示/隱藏底部面板 | | 文字修正 | 啟用 Apple Intelligence 標點修正(需 macOS 26+) | diff --git a/TypelessMLX/Sources/AppState.swift b/TypelessMLX/Sources/AppState.swift index 0f6b6cc..da6f14f 100644 --- a/TypelessMLX/Sources/AppState.swift +++ b/TypelessMLX/Sources/AppState.swift @@ -57,7 +57,7 @@ class AppState: ObservableObject { @AppStorage("showFloatingOverlay") var showFloatingOverlay: Bool = true @AppStorage("playSounds") var playSounds: Bool = false @AppStorage("launchAtLogin") var launchAtLogin: Bool = false - @AppStorage("hotkeyKeyCode") var hotkeyKeyCode: Int = 61 // Right Option + @AppStorage("hotkeyKeyCode") var hotkeyKeyCode: Int = RecognitionHotkey.defaultKeyCode @AppStorage("language") var language: String = "auto" @AppStorage("hotkeyMode") var hotkeyMode: String = "toggle" // "toggle" or "hold" @AppStorage("maxHistoryCount") var maxHistoryCount: Int = 50 @@ -113,6 +113,20 @@ class AppState: ObservableObject { Self.availableModels.first { $0.id == selectedModelID } ?? Self.availableModels[0] } + var recognitionHotkey: RecognitionHotkey { + RecognitionHotkey.resolve(keyCode: hotkeyKeyCode) + } + + var hotkeyDisplayName: String { + recognitionHotkey.displayName + } + + func setRecognitionHotkey(_ hotkey: RecognitionHotkey) { + guard hotkeyKeyCode != hotkey.keyCode else { return } + objectWillChange.send() + hotkeyKeyCode = hotkey.keyCode + } + /// Resolved model path or HF repo for Python backend var resolvedModelPath: String { let model = selectedModel diff --git a/TypelessMLX/Sources/HotkeyManager.swift b/TypelessMLX/Sources/HotkeyManager.swift index 70984b8..c1ea711 100644 --- a/TypelessMLX/Sources/HotkeyManager.swift +++ b/TypelessMLX/Sources/HotkeyManager.swift @@ -7,11 +7,14 @@ class HotkeyManager { private var appState: AppState? private var flagsMonitor: Any? private var localFlagsMonitor: Any? + private var eventTap: CFMachPort? + private var eventTapRunLoopSource: CFRunLoopSource? private var isRecording = false private var recordingStartTime: Date? private var overlay: RecordingOverlay? private let lock = NSLock() private var isProcessing = false + private var hotkeyIsDown = false private var consecutiveFailures = 0 private static let maxConsecutiveFailures = 3 @@ -30,6 +33,14 @@ class HotkeyManager { logInfo("HotkeyManager", "Setup. keyCode=\(appState.hotkeyKeyCode), mode=\(appState.hotkeyMode)") } + func refreshMonitors() { + lock.lock() + hotkeyIsDown = false + lock.unlock() + setupMonitors() + logInfo("HotkeyManager", "Monitors refreshed") + } + @objc private func handleAudioDeviceLost() { logError("HotkeyManager", "Audio device lost during recording") guard let appState = appState else { return } @@ -37,6 +48,10 @@ class HotkeyManager { } private func setupMonitors() { + teardownMonitors() + + setupEventTap() + // Global monitor — fires when other apps are focused flagsMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged]) { [weak self] event in self?.handleFlagsChanged(event) @@ -50,19 +65,86 @@ class HotkeyManager { } private func handleFlagsChanged(_ event: NSEvent) { - guard let appState = appState else { return } - let keyCode = Int(event.keyCode) - guard keyCode == appState.hotkeyKeyCode else { return } + let flags = event.modifierFlags + let flagsRaw = UInt64(flags.rawValue) + handleHotkeyTransition( + keyCode: keyCode, + flagsRaw: flagsRaw, + modifierIsActive: { hotkey in hotkey.modifier.isActive(flags) }, + explicitKeyState: nil, + source: "NSEvent.flagsChanged" + ) + } + + private func handleCGEvent(_ event: CGEvent, type: CGEventType) { + let keyCode = Int(event.getIntegerValueField(.keyboardEventKeycode)) + let flags = event.flags + let flagsRaw = UInt64(flags.rawValue) + let explicitKeyState: Bool? + + switch type { + case .keyDown: + explicitKeyState = true + case .keyUp: + explicitKeyState = false + default: + explicitKeyState = nil + } + + handleHotkeyTransition( + keyCode: keyCode, + flagsRaw: flagsRaw, + modifierIsActive: { hotkey in hotkey.modifier.isActive(flags) }, + explicitKeyState: explicitKeyState, + source: "CGEvent.\(type.rawValue)" + ) + } + + private func handleHotkeyTransition( + keyCode: Int, + flagsRaw: UInt64, + modifierIsActive: (RecognitionHotkey) -> Bool, + explicitKeyState: Bool?, + source: String + ) { + guard let appState = appState else { return } - let isKeyPressed = event.modifierFlags.contains(.option) let mode = appState.hotkeyMode + let configuredHotkey = appState.recognitionHotkey lock.lock() + let wasHotkeyDown = hotkeyIsDown + guard configuredHotkey.isRelevant( + eventKeyCode: keyCode, + flagsRaw: flagsRaw, + wasHotkeyDown: wasHotkeyDown + ) else { + lock.unlock() + if isConfiguredModifierFamilyEvent(hotkey: configuredHotkey, keyCode: keyCode, flagsRaw: flagsRaw) { + logInfo("HotkeyManager", "Observed non-hotkey \(configuredHotkey.displayName) family event. keyCode=\(keyCode), configured=\(configuredHotkey.keyCode), flags=0x\(String(flagsRaw, radix: 16))") + } + return + } + + let keyEventState = configuredHotkey.relatedKeyCodes.contains(keyCode) ? explicitKeyState : nil + let isKeyPressed = keyEventState ?? configuredHotkey.isPressed( + eventKeyCode: keyCode, + flagsRaw: flagsRaw, + modifierIsActive: modifierIsActive(configuredHotkey) + ) + guard hotkeyIsDown != isKeyPressed else { + lock.unlock() + logDebug("HotkeyManager", "Ignoring duplicate \(source) keyCode=\(keyCode), down=\(isKeyPressed), flags=0x\(String(flagsRaw, radix: 16))") + return + } + hotkeyIsDown = isKeyPressed let currentlyRecording = isRecording let processing = isProcessing lock.unlock() + logInfo("HotkeyManager", "Hotkey \(isKeyPressed ? "pressed" : "released") via \(source). keyCode=\(keyCode), configured=\(configuredHotkey.keyCode), hotkey=\(configuredHotkey.displayName), mode=\(mode), recording=\(currentlyRecording), processing=\(processing), flags=0x\(String(flagsRaw, radix: 16))") + DispatchQueue.main.async { if mode == "hold" { // Hold-to-talk: press=start, release=stop @@ -85,6 +167,68 @@ class HotkeyManager { } } + private func isConfiguredModifierFamilyEvent(hotkey: RecognitionHotkey, keyCode: Int, flagsRaw: UInt64) -> Bool { + hotkey.relatedKeyCodes.contains(keyCode) || hotkey.isRelevant(eventKeyCode: keyCode, flagsRaw: flagsRaw, wasHotkeyDown: false) + } + + private func setupEventTap() { + let mask = CGEventMask(1 << CGEventType.flagsChanged.rawValue) | + CGEventMask(1 << CGEventType.keyDown.rawValue) | + CGEventMask(1 << CGEventType.keyUp.rawValue) + let userInfo = Unmanaged.passUnretained(self).toOpaque() + + guard let tap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .listenOnly, + eventsOfInterest: mask, + callback: { _, type, event, userInfo in + guard let userInfo else { + return Unmanaged.passUnretained(event) + } + + let manager = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() + manager.handleCGEvent(event, type: type) + return Unmanaged.passUnretained(event) + }, + userInfo: userInfo + ) else { + logWarn("HotkeyManager", "CGEvent tap unavailable; falling back to NSEvent flagsChanged monitors") + return + } + + guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else { + CFMachPortInvalidate(tap) + logWarn("HotkeyManager", "Failed to create CGEvent tap run-loop source") + return + } + + eventTap = tap + eventTapRunLoopSource = source + CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes) + CGEvent.tapEnable(tap: tap, enable: true) + logInfo("HotkeyManager", "CGEvent tap registered") + } + + private func teardownMonitors() { + if let monitor = flagsMonitor { + NSEvent.removeMonitor(monitor) + flagsMonitor = nil + } + if let monitor = localFlagsMonitor { + NSEvent.removeMonitor(monitor) + localFlagsMonitor = nil + } + if let source = eventTapRunLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes) + eventTapRunLoopSource = nil + } + if let tap = eventTap { + CFMachPortInvalidate(tap) + eventTap = nil + } + } + private func startRecording() { guard let appState = appState else { return } @@ -303,8 +447,7 @@ class HotkeyManager { } deinit { - if let monitor = flagsMonitor { NSEvent.removeMonitor(monitor) } - if let monitor = localFlagsMonitor { NSEvent.removeMonitor(monitor) } + teardownMonitors() NotificationCenter.default.removeObserver(self) } } diff --git a/TypelessMLX/Sources/RecognitionHotkey.swift b/TypelessMLX/Sources/RecognitionHotkey.swift new file mode 100644 index 0000000..1088473 --- /dev/null +++ b/TypelessMLX/Sources/RecognitionHotkey.swift @@ -0,0 +1,159 @@ +import AppKit +import Carbon + +struct RecognitionHotkey: Identifiable, Equatable { + private enum DeviceMask { + static let leftControl: UInt64 = 0x00000001 + static let leftShift: UInt64 = 0x00000002 + static let rightShift: UInt64 = 0x00000004 + static let leftCommand: UInt64 = 0x00000008 + static let rightCommand: UInt64 = 0x00000010 + static let leftOption: UInt64 = 0x00000020 + static let rightOption: UInt64 = 0x00000040 + static let rightControl: UInt64 = 0x00002000 + static let function: UInt64 = 0x00800000 + } + + enum ModifierKind { + case option + case control + case command + case shift + case function + + func isActive(_ flags: NSEvent.ModifierFlags) -> Bool { + switch self { + case .option: + return flags.contains(.option) + case .control: + return flags.contains(.control) + case .command: + return flags.contains(.command) + case .shift: + return flags.contains(.shift) + case .function: + return flags.contains(.function) + } + } + + func isActive(_ flags: CGEventFlags) -> Bool { + switch self { + case .option: + return flags.contains(.maskAlternate) + case .control: + return flags.contains(.maskControl) + case .command: + return flags.contains(.maskCommand) + case .shift: + return flags.contains(.maskShift) + case .function: + return flags.contains(.maskSecondaryFn) + } + } + } + + let keyCode: Int + let displayName: String + let modifier: ModifierKind + let relatedKeyCodes: Set + let deviceMask: UInt64? + + var id: Int { keyCode } + + static let defaultKeyCode = kVK_RightOption + + static let all: [RecognitionHotkey] = [ + RecognitionHotkey( + keyCode: kVK_RightOption, + displayName: "Right Option", + modifier: .option, + relatedKeyCodes: [kVK_Option, kVK_RightOption], + deviceMask: DeviceMask.rightOption + ), + RecognitionHotkey( + keyCode: kVK_Option, + displayName: "Left Option", + modifier: .option, + relatedKeyCodes: [kVK_Option, kVK_RightOption], + deviceMask: DeviceMask.leftOption + ), + RecognitionHotkey( + keyCode: kVK_RightControl, + displayName: "Right Control", + modifier: .control, + relatedKeyCodes: [kVK_Control, kVK_RightControl], + deviceMask: DeviceMask.rightControl + ), + RecognitionHotkey( + keyCode: kVK_Control, + displayName: "Left Control", + modifier: .control, + relatedKeyCodes: [kVK_Control, kVK_RightControl], + deviceMask: DeviceMask.leftControl + ), + RecognitionHotkey( + keyCode: kVK_RightCommand, + displayName: "Right Command", + modifier: .command, + relatedKeyCodes: [kVK_Command, kVK_RightCommand], + deviceMask: DeviceMask.rightCommand + ), + RecognitionHotkey( + keyCode: kVK_Command, + displayName: "Left Command", + modifier: .command, + relatedKeyCodes: [kVK_Command, kVK_RightCommand], + deviceMask: DeviceMask.leftCommand + ), + RecognitionHotkey( + keyCode: kVK_RightShift, + displayName: "Right Shift", + modifier: .shift, + relatedKeyCodes: [kVK_Shift, kVK_RightShift], + deviceMask: DeviceMask.rightShift + ), + RecognitionHotkey( + keyCode: kVK_Shift, + displayName: "Left Shift", + modifier: .shift, + relatedKeyCodes: [kVK_Shift, kVK_RightShift], + deviceMask: DeviceMask.leftShift + ), + RecognitionHotkey( + keyCode: kVK_Function, + displayName: "Fn", + modifier: .function, + relatedKeyCodes: [kVK_Function], + deviceMask: DeviceMask.function + ) + ] + + static func resolve(keyCode: Int) -> RecognitionHotkey { + all.first { $0.keyCode == keyCode } ?? all[0] + } + + func isRelevant(eventKeyCode: Int, flagsRaw: UInt64, wasHotkeyDown: Bool) -> Bool { + if eventKeyCode == keyCode { + return true + } + if isDeviceMaskActive(flagsRaw) { + return true + } + return wasHotkeyDown && relatedKeyCodes.contains(eventKeyCode) + } + + func isPressed(eventKeyCode: Int, flagsRaw: UInt64, modifierIsActive: Bool) -> Bool { + if isDeviceMaskActive(flagsRaw) { + return true + } + if eventKeyCode == keyCode { + return modifierIsActive + } + return false + } + + private func isDeviceMaskActive(_ flagsRaw: UInt64) -> Bool { + guard let deviceMask else { return false } + return flagsRaw & deviceMask != 0 + } +} diff --git a/TypelessMLX/Sources/SettingsView.swift b/TypelessMLX/Sources/SettingsView.swift index 97481f8..dacc4b1 100644 --- a/TypelessMLX/Sources/SettingsView.swift +++ b/TypelessMLX/Sources/SettingsView.swift @@ -40,7 +40,29 @@ struct GeneralSettingsTab: View { HStack { Text("辨識快捷鍵") Spacer() - Text(hotkeyDisplayName).foregroundColor(.secondary) + Menu { + ForEach(RecognitionHotkey.all) { hotkey in + Button { + selectHotkey(hotkey) + } label: { + if appState.recognitionHotkey == hotkey { + Label(hotkey.displayName, systemImage: "checkmark") + } else { + Text(hotkey.displayName) + } + } + } + } label: { + HStack(spacing: 6) { + Image(systemName: "keyboard") + Text(appState.hotkeyDisplayName) + Image(systemName: "chevron.down") + .font(.caption2) + } + .frame(minWidth: 160, alignment: .trailing) + } + .buttonStyle(.bordered) + .controlSize(.small) } Picker("模式", selection: $appState.hotkeyMode) { @@ -108,12 +130,9 @@ struct GeneralSettingsTab: View { return "\(version) (build \(build))" } - private var hotkeyDisplayName: String { - switch appState.hotkeyKeyCode { - case 61: return "Right ⌥ Option" - case 58: return "Left ⌥ Option" - default: return "Key \(appState.hotkeyKeyCode)" - } + private func selectHotkey(_ hotkey: RecognitionHotkey) { + appState.setRecognitionHotkey(hotkey) + HotkeyManager.shared.refreshMonitors() } private func setLaunchAtLogin(_ enabled: Bool) { diff --git a/TypelessMLX/Sources/SetupWindowController.swift b/TypelessMLX/Sources/SetupWindowController.swift index c25920d..9e203d0 100644 --- a/TypelessMLX/Sources/SetupWindowController.swift +++ b/TypelessMLX/Sources/SetupWindowController.swift @@ -98,7 +98,7 @@ class SetupViewModel: ObservableObject { } self.appendLog("") - self.appendLog("🎉 設定完成!請按 Right Option 開始錄音。") + self.appendLog("🎉 設定完成!請按 \(AppState.shared.hotkeyDisplayName) 開始錄音。") DispatchQueue.main.async { self.isRunning = false diff --git a/TypelessMLX/Sources/StatusBarController.swift b/TypelessMLX/Sources/StatusBarController.swift index 2dc8fb4..6cf4aae 100644 --- a/TypelessMLX/Sources/StatusBarController.swift +++ b/TypelessMLX/Sources/StatusBarController.swift @@ -41,6 +41,11 @@ class StatusBarController { .sink { [weak self] _ in self?.updateMenu() } .store(in: &cancellables) + NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.updateMenu() } + .store(in: &cancellables) + appState.$errorMessage .compactMap { $0 } .receive(on: DispatchQueue.main) @@ -108,7 +113,7 @@ class StatusBarController { // Hotkey hint let modeText = appState.hotkeyMode == "hold" ? "按住錄音" : "按一下切換錄音" - let hintItem = NSMenuItem(title: "Right ⌥ Option → \(modeText)", action: nil, keyEquivalent: "") + let hintItem = NSMenuItem(title: "\(appState.hotkeyDisplayName) → \(modeText)", action: nil, keyEquivalent: "") hintItem.isEnabled = false menu.addItem(hintItem) diff --git a/build-app.sh b/build-app.sh index 83723ba..bede061 100755 --- a/build-app.sh +++ b/build-app.sh @@ -12,6 +12,7 @@ DMG_PATH="$BUILD_DIR/${APP_NAME}-${APP_VERSION}.dmg" INSTALL_APP=0 ALLOW_ADHOC_SIGNING="${ALLOW_ADHOC_SIGNING:-0}" SIGN_IDENTITY="${SIGN_IDENTITY:-}" +LOCAL_SIGN_IDENTITY="${LOCAL_SIGN_IDENTITY:-TypelessMLX Local Code Signing}" usage() { cat </dev/null | - awk -F '"' ' + awk -F '"' -v local_identity="$LOCAL_SIGN_IDENTITY" ' /"Apple Development: / { print $2; found = 1; exit } /"Developer ID Application: / && fallback == "" { fallback = $2 } - END { if (!found && fallback != "") print fallback } + $2 == local_identity && local_fallback == "" { local_fallback = $2 } + END { + if (!found && fallback != "") print fallback + else if (!found && local_fallback != "") print local_fallback + } ' } @@ -76,6 +83,9 @@ if [ -z "$SIGN_IDENTITY" ]; then echo " export SIGN_IDENTITY=\"Apple Development: Your Name (TEAMID)\"" echo " $0 --install" echo "" + echo "Or create/use a stable local identity named:" + echo " $LOCAL_SIGN_IDENTITY" + echo "" echo "For disposable builds only:" echo " ALLOW_ADHOC_SIGNING=1 $0 --install" exit 1 @@ -177,4 +187,4 @@ echo " 2. 授權輔助使用(系統設定 → 隱私權 → 輔助使用)" echo " 3. 授權輸入監控(系統設定 → 隱私權 → 輸入監控)" echo " 4. App 啟動後會自動顯示設定視窗,安裝 Python 環境" echo " 5. 下載並轉換 Breeze-ASR-25 模型(約 10-20 分鐘)" -echo " 6. 完成後按 Right Option 即可開始錄音" +echo " 6. 完成後按偏好設定中的辨識快捷鍵即可開始錄音" From e9f860c3e8e3c64510de5cf0ecc8af789d57b48c Mon Sep 17 00:00:00 2001 From: ShihChi Huang Date: Sun, 26 Apr 2026 14:37:40 -0700 Subject: [PATCH 2/2] Remove Fn recognition hotkey support --- README.md | 4 ++-- TypelessMLX/Sources/HotkeyManager.swift | 21 +++------------------ TypelessMLX/Sources/RecognitionHotkey.swift | 13 ------------- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e1959d7..ab17370 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" |------|------| | 🎙️ 麥克風 | 錄製語音 | | ♿ 輔助使用 | 模擬 Cmd+V 貼入文字 | -| ⌨️ 輸入監控 | 偵測全域 Right Option 快捷鍵 | +| ⌨️ 輸入監控 | 偵測全域辨識快捷鍵 | --- @@ -105,7 +105,7 @@ export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" ``` 按辨識快捷鍵 - └─ HotkeyManager (NSEvent.flagsChanged) + └─ HotkeyManager (CGEvent/NSEvent flagsChanged) └─ AudioRecorder (AVAudioEngine, WAV) └─ WhisperBridge (stdin/stdout JSON-RPC) └─ transcribe_server.py diff --git a/TypelessMLX/Sources/HotkeyManager.swift b/TypelessMLX/Sources/HotkeyManager.swift index c1ea711..ce50dd6 100644 --- a/TypelessMLX/Sources/HotkeyManager.swift +++ b/TypelessMLX/Sources/HotkeyManager.swift @@ -72,31 +72,20 @@ class HotkeyManager { keyCode: keyCode, flagsRaw: flagsRaw, modifierIsActive: { hotkey in hotkey.modifier.isActive(flags) }, - explicitKeyState: nil, source: "NSEvent.flagsChanged" ) } private func handleCGEvent(_ event: CGEvent, type: CGEventType) { + guard type == .flagsChanged else { return } let keyCode = Int(event.getIntegerValueField(.keyboardEventKeycode)) let flags = event.flags let flagsRaw = UInt64(flags.rawValue) - let explicitKeyState: Bool? - - switch type { - case .keyDown: - explicitKeyState = true - case .keyUp: - explicitKeyState = false - default: - explicitKeyState = nil - } handleHotkeyTransition( keyCode: keyCode, flagsRaw: flagsRaw, modifierIsActive: { hotkey in hotkey.modifier.isActive(flags) }, - explicitKeyState: explicitKeyState, source: "CGEvent.\(type.rawValue)" ) } @@ -105,7 +94,6 @@ class HotkeyManager { keyCode: Int, flagsRaw: UInt64, modifierIsActive: (RecognitionHotkey) -> Bool, - explicitKeyState: Bool?, source: String ) { guard let appState = appState else { return } @@ -127,8 +115,7 @@ class HotkeyManager { return } - let keyEventState = configuredHotkey.relatedKeyCodes.contains(keyCode) ? explicitKeyState : nil - let isKeyPressed = keyEventState ?? configuredHotkey.isPressed( + let isKeyPressed = configuredHotkey.isPressed( eventKeyCode: keyCode, flagsRaw: flagsRaw, modifierIsActive: modifierIsActive(configuredHotkey) @@ -172,9 +159,7 @@ class HotkeyManager { } private func setupEventTap() { - let mask = CGEventMask(1 << CGEventType.flagsChanged.rawValue) | - CGEventMask(1 << CGEventType.keyDown.rawValue) | - CGEventMask(1 << CGEventType.keyUp.rawValue) + let mask = CGEventMask(1 << CGEventType.flagsChanged.rawValue) let userInfo = Unmanaged.passUnretained(self).toOpaque() guard let tap = CGEvent.tapCreate( diff --git a/TypelessMLX/Sources/RecognitionHotkey.swift b/TypelessMLX/Sources/RecognitionHotkey.swift index 1088473..cfaa5dc 100644 --- a/TypelessMLX/Sources/RecognitionHotkey.swift +++ b/TypelessMLX/Sources/RecognitionHotkey.swift @@ -11,7 +11,6 @@ struct RecognitionHotkey: Identifiable, Equatable { static let leftOption: UInt64 = 0x00000020 static let rightOption: UInt64 = 0x00000040 static let rightControl: UInt64 = 0x00002000 - static let function: UInt64 = 0x00800000 } enum ModifierKind { @@ -19,7 +18,6 @@ struct RecognitionHotkey: Identifiable, Equatable { case control case command case shift - case function func isActive(_ flags: NSEvent.ModifierFlags) -> Bool { switch self { @@ -31,8 +29,6 @@ struct RecognitionHotkey: Identifiable, Equatable { return flags.contains(.command) case .shift: return flags.contains(.shift) - case .function: - return flags.contains(.function) } } @@ -46,8 +42,6 @@ struct RecognitionHotkey: Identifiable, Equatable { return flags.contains(.maskCommand) case .shift: return flags.contains(.maskShift) - case .function: - return flags.contains(.maskSecondaryFn) } } } @@ -118,13 +112,6 @@ struct RecognitionHotkey: Identifiable, Equatable { modifier: .shift, relatedKeyCodes: [kVK_Shift, kVK_RightShift], deviceMask: DeviceMask.leftShift - ), - RecognitionHotkey( - keyCode: kVK_Function, - displayName: "Fn", - modifier: .function, - relatedKeyCodes: [kVK_Function], - deviceMask: DeviceMask.function ) ]