@@ -5,16 +5,96 @@ import os
55
66private let logger = Logger ( subsystem: " com.typeless.app " , category: " KeyMonitor " )
77
8- /// 监听全局 Fn 键按下/松开
8+ /// 触发键配置,支持修饰键和普通键
9+ struct TriggerKeyConfig : Codable , Equatable {
10+ /// 事件类型:flagsChanged (修饰键) 或 keyDown/keyUp (普通键)
11+ let isModifierKey : Bool
12+ /// 按键的 keyCode
13+ let keyCode : Int64
14+ /// 修饰键的 flag 掩码(仅修饰键有效,如 .maskSecondaryFn = 0x800000)
15+ let flagMask : UInt64
16+ /// 用户可读的按键名称
17+ let displayName : String
18+
19+ /// 默认触发键:Fn
20+ static let defaultFn = TriggerKeyConfig (
21+ isModifierKey: true ,
22+ keyCode: - 1 , // Fn 不使用 keyCode,而是检查 flagMask
23+ flagMask: CGEventFlags . maskSecondaryFn. rawValue,
24+ displayName: " Fn "
25+ )
26+
27+ /// 从 UserDefaults 读取,默认 Fn
28+ static var current : TriggerKeyConfig {
29+ guard let data = UserDefaults . standard. data ( forKey: " triggerKeyConfig " ) ,
30+ let config = try ? JSONDecoder ( ) . decode ( TriggerKeyConfig . self, from: data) else {
31+ return . defaultFn
32+ }
33+ return config
34+ }
35+
36+ /// 保存到 UserDefaults
37+ func save( ) {
38+ if let data = try ? JSONEncoder ( ) . encode ( self ) {
39+ UserDefaults . standard. set ( data, forKey: " triggerKeyConfig " )
40+ }
41+ }
42+
43+ /// 检查事件是否匹配此触发键
44+ func matches( type: CGEventType , event: CGEvent ) -> Bool ? {
45+ if isModifierKey {
46+ guard type == . flagsChanged else { return nil }
47+ if keyCode >= 0 {
48+ // 区分左右修饰键(如 Right Command keyCode=54)
49+ let eventKeyCode = event. getIntegerValueField ( . keyboardEventKeycode)
50+ guard eventKeyCode == keyCode else { return nil }
51+ }
52+ return event. flags. rawValue & flagMask != 0
53+ } else {
54+ let eventKeyCode = event. getIntegerValueField ( . keyboardEventKeycode)
55+ guard eventKeyCode == keyCode else { return nil }
56+ if type == . keyDown { return true }
57+ if type == . keyUp { return false }
58+ return nil
59+ }
60+ }
61+ }
62+
63+ // MARK: - Notification for trigger key changes
64+
65+ extension Notification . Name {
66+ static let triggerKeyChanged = Notification . Name ( " triggerKeyChanged " )
67+ /// Settings UI 请求进入按键录制模式
68+ static let triggerKeyRecordingRequested = Notification . Name ( " triggerKeyRecordingRequested " )
69+ /// Settings UI 取消按键录制
70+ static let triggerKeyRecordingCancelled = Notification . Name ( " triggerKeyRecordingCancelled " )
71+ /// KeyMonitor 录制到新按键后通知 Settings UI(object 为 TriggerKeyConfig)
72+ static let triggerKeyRecorded = Notification . Name ( " triggerKeyRecorded " )
73+ }
74+
75+ /// 监听全局触发键按下/松开(支持任意按键)
976class KeyMonitor {
1077 var onKeyDown : ( ( ) -> Void ) ?
1178 var onKeyUp : ( ( ) -> Void ) ?
1279
80+ /// 按键录制模式的回调:录制到一个按键后回调
81+ var onKeyRecorded : ( ( TriggerKeyConfig ) -> Void ) ?
82+
1383 private var eventTap : CFMachPort ?
1484 private var runLoopSource : CFRunLoopSource ?
15- private var isFnPressed = false
85+ private var isTriggerPressed = false
1686 private var permissionCheckTimer : Timer ?
1787
88+ /// 当前配置的触发键
89+ private var triggerConfig : TriggerKeyConfig = . current
90+
91+ /// 是否处于按键录制模式(通过锁保护,因为该值在 CGEvent tap 回调线程读取、主线程写入)
92+ private let _isRecordingKey = OSAllocatedUnfairLock ( initialState: false )
93+ var isRecordingKey : Bool {
94+ get { _isRecordingKey. withLock { $0 } }
95+ set { _isRecordingKey. withLock { $0 = newValue } }
96+ }
97+
1898 func startMonitoring( ) {
1999 // 单元测试环境下跳过监听(避免弹出辅助功能权限窗口)
20100 if ProcessInfo . processInfo. environment [ " XCTestConfigurationFilePath " ] != nil {
@@ -42,9 +122,15 @@ class KeyMonitor {
42122 return
43123 }
44124
45- // 监听 flagsChanged 事件(修饰键变化)
46- let eventMask : CGEventMask = ( 1 << CGEventType . flagsChanged. rawValue)
47- logger. debug ( " 事件掩码: \( eventMask, privacy: . public) " )
125+ // 监听所有需要的事件类型(修饰键 + 普通键)
126+ let eventMask : CGEventMask =
127+ ( 1 << CGEventType . flagsChanged. rawValue) |
128+ ( 1 << CGEventType . keyDown. rawValue) |
129+ ( 1 << CGEventType . keyUp. rawValue)
130+
131+ // 刷新触发键配置
132+ triggerConfig = . current
133+ logger. info ( " 触发键: \( self . triggerConfig. displayName, privacy: . public) " )
48134
49135 guard let tap = CGEvent . tapCreate (
50136 tap: . cgSessionEventTap,
@@ -71,8 +157,20 @@ class KeyMonitor {
71157 CFRunLoopAddSource ( CFRunLoopGetCurrent ( ) , runLoopSource, . commonModes)
72158 CGEvent . tapEnable ( tap: tap, enable: true )
73159
74- logger. info ( " Fn 键监听已启动,按下 Fn 键测试 " )
75- logger. debug ( " 提示:如果你使用外接键盘,Fn 键检测可能不同 " )
160+ logger. info ( " 触发键监听已启动( \( self . triggerConfig. displayName, privacy: . public) ) " )
161+ }
162+
163+ /// 切换触发键后重新启动监听。
164+ /// ⚠️ 必须通过 DispatchQueue.main.async 异步调用,禁止在 CGEvent tap 回调中同步调用,
165+ /// 因为 stopMonitoring() 会销毁当前正在执行回调的 event tap,导致未定义行为。
166+ func restartWithNewTriggerKey( ) {
167+ if isTriggerPressed {
168+ onKeyUp ? ( )
169+ }
170+ stopMonitoring ( )
171+ isTriggerPressed = false
172+ triggerConfig = . current
173+ startMonitoring ( )
76174 }
77175
78176 private func handleEvent( proxy: CGEventTapProxy , type: CGEventType , event: CGEvent ) -> Unmanaged < CGEvent > ? {
@@ -85,31 +183,164 @@ class KeyMonitor {
85183 return Unmanaged . passRetained ( event)
86184 }
87185
186+ // 按键录制模式:捕获下一个按键作为新触发键
187+ if isRecordingKey {
188+ if let config = captureKeyConfig ( type: type, event: event) {
189+ isRecordingKey = false
190+ logger. info ( " 录制到新触发键: \( config. displayName, privacy: . public) (keyCode= \( config. keyCode) ) " )
191+ DispatchQueue . main. async {
192+ self . onKeyRecorded ? ( config)
193+ }
194+ return nil // 吞掉录制的按键事件
195+ }
196+ return Unmanaged . passRetained ( event)
197+ }
198+
199+ // 正常触发键检测模式
200+ if let pressed = triggerConfig. matches ( type: type, event: event) {
201+ handleTriggerState ( pressed: pressed)
202+ // 非修饰键需要吞掉事件,防止触发系统默认行为
203+ if !triggerConfig. isModifierKey {
204+ return nil
205+ }
206+ }
207+
208+ return Unmanaged . passRetained ( event)
209+ }
210+
211+ /// 从事件中捕获按键配置(用于录制模式)
212+ private func captureKeyConfig( type: CGEventType , event: CGEvent ) -> TriggerKeyConfig ? {
213+ let keyCode = event. getIntegerValueField ( . keyboardEventKeycode)
214+
88215 if type == . flagsChanged {
216+ // 修饰键:检测哪个 flag 变化了
89217 let flags = event. flags
90218
91- // 检测 Fn 键状态
92- // Fn 键通过 secondaryFn 标志检测
93- let fnPressed = flags. contains ( . maskSecondaryFn)
219+ if flags. contains ( . maskSecondaryFn) {
220+ return TriggerKeyConfig ( isModifierKey: true , keyCode: - 1 ,
221+ flagMask: CGEventFlags . maskSecondaryFn. rawValue, displayName: " Fn " )
222+ }
223+ if flags. contains ( . maskCommand) {
224+ let name = keyCode == 54 ? " Right Command ⌘ " : " Left Command ⌘ "
225+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
226+ flagMask: CGEventFlags . maskCommand. rawValue, displayName: name)
227+ }
228+ if flags. contains ( . maskAlternate) {
229+ let name = keyCode == 61 ? " Right Option ⌥ " : " Left Option ⌥ "
230+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
231+ flagMask: CGEventFlags . maskAlternate. rawValue, displayName: name)
232+ }
233+ if flags. contains ( . maskControl) {
234+ let name = keyCode == 62 ? " Right Control ⌃ " : " Left Control ⌃ "
235+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
236+ flagMask: CGEventFlags . maskControl. rawValue, displayName: name)
237+ }
238+ if flags. contains ( . maskShift) {
239+ let name = keyCode == 60 ? " Right Shift ⇧ " : " Left Shift ⇧ "
240+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
241+ flagMask: CGEventFlags . maskShift. rawValue, displayName: name)
242+ }
243+ return nil
244+ }
94245
95- if fnPressed && !isFnPressed {
96- // Fn 键按下
97- isFnPressed = true
98- logger. info ( " Fn 键按下,触发录音 " )
99- DispatchQueue . main. async {
100- self . onKeyDown ? ( )
101- }
102- } else if !fnPressed && isFnPressed {
103- // Fn 键松开
104- isFnPressed = false
105- logger. info ( " Fn 键松开,停止录音 " )
106- DispatchQueue . main. async {
107- self . onKeyUp ? ( )
108- }
246+ if type == . keyDown {
247+ // 普通键:用 keyCode 标识
248+ let name = keyCodeDisplayName ( keyCode)
249+ return TriggerKeyConfig ( isModifierKey: false , keyCode: keyCode,
250+ flagMask: 0 , displayName: name)
251+ }
252+
253+ return nil
254+ }
255+
256+ /// 将 keyCode 转为人类可读名称
257+ private func keyCodeDisplayName( _ keyCode: Int64 ) -> String {
258+ // 常见特殊键
259+ switch keyCode {
260+ case 110 : return " App / Menu "
261+ case 53 : return " Escape "
262+ case 36 : return " Return "
263+ case 48 : return " Tab "
264+ case 49 : return " Space "
265+ case 51 : return " Delete "
266+ case 117 : return " Forward Delete "
267+ case 114 : return " Insert "
268+ case 115 : return " Home "
269+ case 119 : return " End "
270+ case 116 : return " Page Up "
271+ case 121 : return " Page Down "
272+ case 123 : return " ← "
273+ case 124 : return " → "
274+ case 125 : return " ↓ "
275+ case 126 : return " ↑ "
276+ case 122 : return " F1 "
277+ case 120 : return " F2 "
278+ case 99 : return " F3 "
279+ case 118 : return " F4 "
280+ case 96 : return " F5 "
281+ case 97 : return " F6 "
282+ case 98 : return " F7 "
283+ case 100 : return " F8 "
284+ case 101 : return " F9 "
285+ case 109 : return " F10 "
286+ case 103 : return " F11 "
287+ case 111 : return " F12 "
288+ case 105 : return " F13 "
289+ case 107 : return " F14 "
290+ case 113 : return " F15 "
291+ default :
292+ // 尝试用 Carbon 获取字符名称
293+ if let char = keyCodeToCharacter ( keyCode) {
294+ return String ( char) . uppercased ( )
109295 }
296+ return " Key \( keyCode) "
110297 }
298+ }
111299
112- return Unmanaged . passRetained ( event)
300+ /// 将 keyCode 转为字符(用于字母/数字键)
301+ private func keyCodeToCharacter( _ keyCode: Int64 ) -> Character ? {
302+ guard let source = TISCopyCurrentKeyboardLayoutInputSource ( ) ? . takeRetainedValue ( ) else { return nil }
303+ guard let layoutData = TISGetInputSourceProperty ( source, kTISPropertyUnicodeKeyLayoutData) else { return nil }
304+ let layout = unsafeBitCast ( layoutData, to: CFData . self)
305+ let keyboardLayout = unsafeBitCast ( CFDataGetBytePtr ( layout) , to: UnsafePointer< UCKeyboardLayout> . self )
306+
307+ var deadKeyState : UInt32 = 0
308+ var chars = [ UniChar] ( repeating: 0 , count: 4 )
309+ var length : Int = 0
310+
311+ let status = UCKeyTranslate (
312+ keyboardLayout,
313+ UInt16 ( keyCode) ,
314+ UInt16 ( kUCKeyActionDown) ,
315+ 0 , // no modifiers
316+ UInt32 ( LMGetKbdType ( ) ) ,
317+ UInt32 ( kUCKeyTranslateNoDeadKeysMask) ,
318+ & deadKeyState,
319+ chars. count,
320+ & length,
321+ & chars
322+ )
323+
324+ guard status == noErr, length > 0 else { return nil }
325+ guard let scalar = UnicodeScalar ( chars [ 0 ] ) else { return nil }
326+ return Character ( scalar)
327+ }
328+
329+ /// 统一处理触发键状态变化
330+ private func handleTriggerState( pressed: Bool ) {
331+ if pressed && !isTriggerPressed {
332+ isTriggerPressed = true
333+ logger. info ( " \( self . triggerConfig. displayName, privacy: . public) 按下,触发录音 " )
334+ DispatchQueue . main. async {
335+ self . onKeyDown ? ( )
336+ }
337+ } else if !pressed && isTriggerPressed {
338+ isTriggerPressed = false
339+ logger. info ( " \( self . triggerConfig. displayName, privacy: . public) 松开,停止录音 " )
340+ DispatchQueue . main. async {
341+ self . onKeyUp ? ( )
342+ }
343+ }
113344 }
114345
115346 func stopMonitoring( ) {
0 commit comments