@@ -5,16 +5,92 @@ 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+ /// 是否处于按键录制模式
92+ var isRecordingKey = false
93+
1894 func startMonitoring( ) {
1995 // 单元测试环境下跳过监听(避免弹出辅助功能权限窗口)
2096 if ProcessInfo . processInfo. environment [ " XCTestConfigurationFilePath " ] != nil {
@@ -42,9 +118,15 @@ class KeyMonitor {
42118 return
43119 }
44120
45- // 监听 flagsChanged 事件(修饰键变化)
46- let eventMask : CGEventMask = ( 1 << CGEventType . flagsChanged. rawValue)
47- logger. debug ( " 事件掩码: \( eventMask, privacy: . public) " )
121+ // 监听所有需要的事件类型(修饰键 + 普通键)
122+ let eventMask : CGEventMask =
123+ ( 1 << CGEventType . flagsChanged. rawValue) |
124+ ( 1 << CGEventType . keyDown. rawValue) |
125+ ( 1 << CGEventType . keyUp. rawValue)
126+
127+ // 刷新触发键配置
128+ triggerConfig = . current
129+ logger. info ( " 触发键: \( self . triggerConfig. displayName, privacy: . public) " )
48130
49131 guard let tap = CGEvent . tapCreate (
50132 tap: . cgSessionEventTap,
@@ -71,8 +153,15 @@ class KeyMonitor {
71153 CFRunLoopAddSource ( CFRunLoopGetCurrent ( ) , runLoopSource, . commonModes)
72154 CGEvent . tapEnable ( tap: tap, enable: true )
73155
74- logger. info ( " Fn 键监听已启动,按下 Fn 键测试 " )
75- logger. debug ( " 提示:如果你使用外接键盘,Fn 键检测可能不同 " )
156+ logger. info ( " 触发键监听已启动( \( self . triggerConfig. displayName, privacy: . public) ) " )
157+ }
158+
159+ /// 切换触发键后重新启动监听
160+ func restartWithNewTriggerKey( ) {
161+ stopMonitoring ( )
162+ isTriggerPressed = false
163+ triggerConfig = . current
164+ startMonitoring ( )
76165 }
77166
78167 private func handleEvent( proxy: CGEventTapProxy , type: CGEventType , event: CGEvent ) -> Unmanaged < CGEvent > ? {
@@ -85,31 +174,163 @@ class KeyMonitor {
85174 return Unmanaged . passRetained ( event)
86175 }
87176
177+ // 按键录制模式:捕获下一个按键作为新触发键
178+ if isRecordingKey {
179+ if let config = captureKeyConfig ( type: type, event: event) {
180+ isRecordingKey = false
181+ logger. info ( " 录制到新触发键: \( config. displayName, privacy: . public) (keyCode= \( config. keyCode) ) " )
182+ DispatchQueue . main. async {
183+ self . onKeyRecorded ? ( config)
184+ }
185+ return nil // 吞掉录制的按键事件
186+ }
187+ return Unmanaged . passRetained ( event)
188+ }
189+
190+ // 正常触发键检测模式
191+ if let pressed = triggerConfig. matches ( type: type, event: event) {
192+ handleTriggerState ( pressed: pressed)
193+ // 非修饰键需要吞掉事件,防止触发系统默认行为
194+ if !triggerConfig. isModifierKey {
195+ return nil
196+ }
197+ }
198+
199+ return Unmanaged . passRetained ( event)
200+ }
201+
202+ /// 从事件中捕获按键配置(用于录制模式)
203+ private func captureKeyConfig( type: CGEventType , event: CGEvent ) -> TriggerKeyConfig ? {
204+ let keyCode = event. getIntegerValueField ( . keyboardEventKeycode)
205+
88206 if type == . flagsChanged {
207+ // 修饰键:检测哪个 flag 变化了
89208 let flags = event. flags
90209
91- // 检测 Fn 键状态
92- // Fn 键通过 secondaryFn 标志检测
93- let fnPressed = flags. contains ( . maskSecondaryFn)
210+ if flags. contains ( . maskSecondaryFn) {
211+ return TriggerKeyConfig ( isModifierKey: true , keyCode: - 1 ,
212+ flagMask: CGEventFlags . maskSecondaryFn. rawValue, displayName: " Fn " )
213+ }
214+ if flags. contains ( . maskCommand) {
215+ let name = keyCode == 54 ? " Right Command ⌘ " : " Left Command ⌘ "
216+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
217+ flagMask: CGEventFlags . maskCommand. rawValue, displayName: name)
218+ }
219+ if flags. contains ( . maskAlternate) {
220+ let name = keyCode == 61 ? " Right Option ⌥ " : " Left Option ⌥ "
221+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
222+ flagMask: CGEventFlags . maskAlternate. rawValue, displayName: name)
223+ }
224+ if flags. contains ( . maskControl) {
225+ let name = keyCode == 62 ? " Right Control ⌃ " : " Left Control ⌃ "
226+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
227+ flagMask: CGEventFlags . maskControl. rawValue, displayName: name)
228+ }
229+ if flags. contains ( . maskShift) {
230+ let name = keyCode == 60 ? " Right Shift ⇧ " : " Left Shift ⇧ "
231+ return TriggerKeyConfig ( isModifierKey: true , keyCode: keyCode,
232+ flagMask: CGEventFlags . maskShift. rawValue, displayName: name)
233+ }
234+ return nil
235+ }
94236
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- }
237+ if type == . keyDown {
238+ // 普通键:用 keyCode 标识
239+ let name = keyCodeDisplayName ( keyCode)
240+ return TriggerKeyConfig ( isModifierKey: false , keyCode: keyCode,
241+ flagMask: 0 , displayName: name)
242+ }
243+
244+ return nil
245+ }
246+
247+ /// 将 keyCode 转为人类可读名称
248+ private func keyCodeDisplayName( _ keyCode: Int64 ) -> String {
249+ // 常见特殊键
250+ switch keyCode {
251+ case 110 : return " App / Menu "
252+ case 53 : return " Escape "
253+ case 36 : return " Return "
254+ case 48 : return " Tab "
255+ case 49 : return " Space "
256+ case 51 : return " Delete "
257+ case 117 : return " Forward Delete "
258+ case 114 : return " Insert "
259+ case 115 : return " Home "
260+ case 119 : return " End "
261+ case 116 : return " Page Up "
262+ case 121 : return " Page Down "
263+ case 123 : return " ← "
264+ case 124 : return " → "
265+ case 125 : return " ↓ "
266+ case 126 : return " ↑ "
267+ case 122 : return " F1 "
268+ case 120 : return " F2 "
269+ case 99 : return " F3 "
270+ case 118 : return " F4 "
271+ case 96 : return " F5 "
272+ case 97 : return " F6 "
273+ case 98 : return " F7 "
274+ case 100 : return " F8 "
275+ case 101 : return " F9 "
276+ case 109 : return " F10 "
277+ case 103 : return " F11 "
278+ case 111 : return " F12 "
279+ case 105 : return " F13 "
280+ case 107 : return " F14 "
281+ case 113 : return " F15 "
282+ default :
283+ // 尝试用 Carbon 获取字符名称
284+ if let char = keyCodeToCharacter ( keyCode) {
285+ return String ( char) . uppercased ( )
109286 }
287+ return " Key \( keyCode) "
110288 }
289+ }
111290
112- return Unmanaged . passRetained ( event)
291+ /// 将 keyCode 转为字符(用于字母/数字键)
292+ private func keyCodeToCharacter( _ keyCode: Int64 ) -> Character ? {
293+ let source = TISCopyCurrentKeyboardLayoutInputSource ( ) ? . takeRetainedValue ( )
294+ guard let layoutData = TISGetInputSourceProperty ( source!, kTISPropertyUnicodeKeyLayoutData) else { return nil }
295+ let layout = unsafeBitCast ( layoutData, to: CFData . self)
296+ let keyboardLayout = unsafeBitCast ( CFDataGetBytePtr ( layout) , to: UnsafePointer< UCKeyboardLayout> . self )
297+
298+ var deadKeyState : UInt32 = 0
299+ var chars = [ UniChar] ( repeating: 0 , count: 4 )
300+ var length : Int = 0
301+
302+ let status = UCKeyTranslate (
303+ keyboardLayout,
304+ UInt16 ( keyCode) ,
305+ UInt16 ( kUCKeyActionDown) ,
306+ 0 , // no modifiers
307+ UInt32 ( LMGetKbdType ( ) ) ,
308+ UInt32 ( kUCKeyTranslateNoDeadKeysMask) ,
309+ & deadKeyState,
310+ chars. count,
311+ & length,
312+ & chars
313+ )
314+
315+ guard status == noErr, length > 0 else { return nil }
316+ return Character ( UnicodeScalar ( chars [ 0 ] ) !)
317+ }
318+
319+ /// 统一处理触发键状态变化
320+ private func handleTriggerState( pressed: Bool ) {
321+ if pressed && !isTriggerPressed {
322+ isTriggerPressed = true
323+ logger. info ( " \( self . triggerConfig. displayName, privacy: . public) 按下,触发录音 " )
324+ DispatchQueue . main. async {
325+ self . onKeyDown ? ( )
326+ }
327+ } else if !pressed && isTriggerPressed {
328+ isTriggerPressed = false
329+ logger. info ( " \( self . triggerConfig. displayName, privacy: . public) 松开,停止录音 " )
330+ DispatchQueue . main. async {
331+ self . onKeyUp ? ( )
332+ }
333+ }
113334 }
114335
115336 func stopMonitoring( ) {
0 commit comments