11import AppKit. NSEvent
22import Carbon
33
4- /// Represents a keyboard key. Don't forget – there are different keyboard layouts for handling different languages.
4+ /// Represents a physical keyboard key. Don't forget – there are different keyboard layouts for handling different languages.
55public struct KeyboardKey : RawRepresentable {
66 public init ( rawValue: Int ) { self . rawValue = rawValue }
77 public init ( _ rawValue: Int ) { self . init ( rawValue: rawValue) }
@@ -29,6 +29,7 @@ public struct KeyboardKey: RawRepresentable {
2929 return nil
3030 }
3131
32+ /// The virtual key code, CGKeyCode.
3233 public let rawValue : Int
3334
3435 public static let a : KeyboardKey = . init( kVK_ANSI_A)
@@ -152,12 +153,14 @@ public struct KeyboardKey: RawRepresentable {
152153 public static let f19 : KeyboardKey = . init( kVK_F19)
153154 public static let f20 : KeyboardKey = . init( kVK_F20)
154155
156+ /// Returns the key name in current ASCII layout if available, and if not, falls back to the current layout no matter whether ASCII or not.
155157 public var name : String ? {
156158 self . name ( custom: Self . names)
157159 }
158160
161+ /// Returns the key name in current ASCII layout if available, and if not, falls back to the current layout no matter whether ASCII or not.
159162 public func name( custom map: [ KeyboardKey : String ] ) -> String ? {
160- self . name ( layout: . ascii, custom: map)
163+ self . name ( layout: . ascii, custom: map) ?? self . name ( layout : . current , custom : map )
161164 }
162165
163166 public func name( layout: Layout , custom map: [ KeyboardKey : String ] ? = nil ) -> String ? {
@@ -240,7 +243,7 @@ extension KeyboardKey: Equatable, Hashable {
240243}
241244
242245extension KeyboardKey : CustomStringConvertible {
243- public var description : String { self . name ( custom: Self . names) ?? " " }
246+ public var description : String { self . name ( custom: Self . names) ?? " Key Code: \( self . rawValue ) " }
244247}
245248
246249extension KeyboardKey {
@@ -251,29 +254,78 @@ extension KeyboardKey {
251254}
252255
253256extension KeyboardKey . Layout {
254- /// Needed for serializing access to Carbon’ s TIS keyboard layout APIs, which can crash under concurrent calls,
257+ /// Needed for serializing access to Carbon' s TIS keyboard layout APIs, which can crash under concurrent calls,
255258 /// ensuring layout data retrieval is thread-safe.
256259 private static let lock = NSLock ( )
257260
261+ /// Cache layout data by input source – this minimizes calls to Carbon's TIS, which are super-unstable and thread-unsafe…
262+ fileprivate static var cache : [ Self : Data ] = [ : ]
263+ private static func invalidateCaches( ) { Self . lock. withLock ( { Self . cache = [ : ] } ) }
264+
265+ /// Input source change observers.
266+ private static var observers : [ NSObjectProtocol ] = [ ]
267+ private static func observe( ) -> [ NSObjectProtocol ] {
268+ // https://leopard-adc.pepas.com/documentation/TextFonts/Reference/TextInputSourcesReference/TextInputSourcesReference.pdf
269+ let notifications = [ kTISNotifySelectedKeyboardInputSourceChanged, kTISNotifyEnabledKeyboardInputSourcesChanged] . compactMap ( { Notification . Name ( $0 as String ) } )
270+ return notifications. map ( { DistributedNotificationCenter . default ( ) . addObserver ( forName: $0, object: nil , queue: nil , using: { _ in Self . invalidateCaches ( ) } ) } )
271+ }
272+
273+
258274 /// The unicode keyboard layout, with some great insight from:
259275 /// - https://jongampark.wordpress.com/2015/07/17.
260276 /// - https://github.com/cocoabits/MASShortcut/issues/60
261277 public var data : Data ? {
262- Self . lock. lock ( )
263- defer { Self . lock. unlock ( ) }
278+ // We still want the locking, but not while waiting for the main-thread dispatch, as it can produce short-deadlocks.
279+
280+ var data : Data ? = Self . lock. withLock ( {
281+ if let data = Self . cache [ self ] { return data }
282+ if Self . observers. isEmpty { Self . observers = Self . observe ( ) }
283+ return nil
284+ } )
285+
286+ do {
287+ data = try Thread . mainly ( timeout: . milliseconds( 50 ) , {
288+ Self . lock. withLock ( {
289+ // ✊ What is interesting is that kTISPropertyUnicodeKeyLayoutData is still used when it queries last ASCII capable keyboard. It
290+ // is TISCopyCurrentASCIICapableKeyboardLayoutInputSource() not TISCopyCurrentASCIICapableKeyboardInputSource() to call. The latter
291+ // does not guarantee that it would return an keyboard input with a layout.
292+ let inputSource = switch self {
293+ case . ascii: TISCopyCurrentASCIICapableKeyboardLayoutInputSource ( ) ? . takeRetainedValue ( )
294+ case . current: TISCopyCurrentKeyboardInputSource ( ) ? . takeRetainedValue ( )
295+ }
296+ guard let inputSource else { return nil }
297+ guard let data = TISGetInputSourceProperty ( inputSource, kTISPropertyUnicodeKeyLayoutData) else { return nil }
298+ guard let data = Unmanaged < AnyObject > . fromOpaque ( data) . takeUnretainedValue ( ) as? NSData , data. count > 0 else { return nil }
299+ // Hard-copy the data to avoid any external modifications.
300+ return Data ( data as Data )
301+ } )
302+ } )
303+ } catch {
304+ NSLog ( " Failed to retrieve keyboard layout data: TIS API couldn't be called on the main thread. " )
305+ }
264306
265- // ✊ What is interesting is that kTISPropertyUnicodeKeyLayoutData is still used when it queries last ASCII capable keyboard. It
266- // is TISCopyCurrentASCIICapableKeyboardLayoutInputSource() not TISCopyCurrentASCIICapableKeyboardInputSource() to call. The latter
267- // does not guarantee that it would return an keyboard input with a layout.
307+ if let data { Self . lock. withLock ( { Self . cache [ self ] = data } ) }
308+ return data
309+ }
310+ }
268311
269- var inputSource : TISInputSource ?
270- switch self {
271- case . ascii: inputSource = TISCopyCurrentASCIICapableKeyboardLayoutInputSource ( ) ? . takeRetainedValue ( )
272- case . current: inputSource = TISCopyCurrentKeyboardInputSource ( ) ? . takeRetainedValue ( )
312+ extension Thread {
313+ fileprivate enum Error : Swift . Error { case timeout }
314+ @discardableResult fileprivate static func mainly< T> ( timeout: DispatchTimeInterval , _ action: @escaping ( ) -> T ) throws -> T {
315+ if Thread . isMainThread { return action ( ) }
316+ let semaphore = ( dispatch: DispatchSemaphore ( value: 0 ) , work: DispatchSemaphore ( value: 0 ) )
317+ var result : T ?
318+ var item : DispatchWorkItem ?
319+ item = DispatchWorkItem {
320+ // If we timed out before the item even started, we cancel it and it should no-op.
321+ semaphore. dispatch. signal ( )
322+ defer { semaphore. work. signal ( ) }
323+ if item? . isCancelled == false { result = action ( ) }
273324 }
274- guard let inputSource else { return nil }
275- guard let data = TISGetInputSourceProperty ( inputSource, kTISPropertyUnicodeKeyLayoutData) else { return nil }
276- guard let data = Unmanaged < AnyObject > . fromOpaque ( data) . takeUnretainedValue ( ) as? NSData , data. count > 0 else { return nil }
277- return Data ( referencing: data)
325+ // Only time out if the main queue couldn't begin running our block. But once started, wait for completion…
326+ if let item { DispatchQueue . main. async ( execute: item) }
327+ if semaphore. dispatch. wait ( timeout: . now( ) + timeout) == . timedOut { item? . cancel ( ) ; throw Error . timeout }
328+ semaphore. work. wait ( )
329+ if let result { return result } else { throw Error . timeout }
278330 }
279331}
0 commit comments