├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── encodings.xml ├── xcode.xml ├── vcs.xml ├── dictionaries │ └── ianbytchek.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── runConfigurations │ └── Observatory.xml ├── .gitmodules ├── Observatory.playground ├── playground.xcworkspace │ └── contents.xcworkspacedata ├── contents.xcplayground └── Contents.swift ├── Observatory.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ ├── Demo.xcscheme │ │ ├── Observatory-Test.xcscheme │ │ └── Observatory.xcscheme └── project.pbxproj ├── Observatory.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ ├── swiftpm │ └── Package.resolved │ └── Observatory.xcscmblueprint ├── .gitignore ├── source ├── Observatory │ ├── Observer │ │ ├── Observer.swift │ │ ├── Observer.HandlerDefinition.swift │ │ ├── Notification │ │ │ ├── Observer.Notification.Handler.swift │ │ │ └── Observer.Notification.swift │ │ ├── Hotkey │ │ │ ├── Observer.Hotkey.Handler.swift │ │ │ └── Observer.Hotkey.swift │ │ └── Event │ │ │ ├── Observer.Event.swift │ │ │ └── Observer.Event.Handler.swift │ ├── Test │ │ ├── Keyboard │ │ │ ├── Test.Keyboard.Hotkey.swift │ │ │ ├── Test.Keyboard.Modifier.swift │ │ │ └── Test.Keyboard.Key.swift │ │ ├── Test.swift │ │ ├── Observer │ │ │ ├── Test.Observer.Notification.swift │ │ │ ├── Test.Observer.Event.swift │ │ │ └── Test.Observer.Hotkey.swift │ │ └── Shortcut │ │ │ └── Test.Shortcut.swift │ ├── Testing │ │ ├── Testing.Event.swift │ │ └── Testing.Observation.swift │ ├── Shortcut │ │ ├── Shortcut.Recorder.swift │ │ ├── Shortcut.swift │ │ ├── Shortcut.Center.swift │ │ └── Shortcut.Recorder.Button.swift │ └── Keyboard │ │ ├── Keyboard.Hotkey.swift │ │ ├── Keyboard.Modifier.swift │ │ └── Keyboard.Key.swift └── Demo │ └── Entrypoint.swift ├── Observatory.podspec ├── accessory └── info │ ├── test.plist │ ├── framework.plist │ └── application.plist ├── LICENSE.md ├── .swiftlint.yml ├── Package.swift ├── Package.resolved ├── README.md └── .github └── workflows └── main.yml /.idea/.name: -------------------------------------------------------------------------------- 1 | Observatory -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependency/Git/xcconfigs"] 2 | path = dependency/Git/xcconfigs 3 | url = https://github.com/xcconfigs/xcconfigs 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/xcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Observatory.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Observatory.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Observatory.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/dictionaries/ianbytchek.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | deinitialising 5 | hotkey 6 | initialiser 7 | observee 8 | 9 | 10 | -------------------------------------------------------------------------------- /Observatory.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Observatory.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Observatory.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | /*.xcodeproj/project.xcworkspace 3 | /*.xcodeproj/**/xcuserdata 4 | /*.xcworkspace/**/xcuserdata 5 | /*.playground/**/xcuserdata 6 | 7 | # AppCode 8 | /.idea/*.iml 9 | /.idea/modules.xml 10 | /.idea/tasks.xml 11 | /.idea/vcs.xml 12 | /.idea/workspace.xml 13 | /.idea/dataSources 14 | /.idea/dataSources.ids 15 | /.idea/dataSources.local.xml 16 | /.idea/dictionaries 17 | /.idea/runConfigurations 18 | 19 | # SPM 20 | /.build 21 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Observer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Observer { 4 | 5 | /// Indicates whether observer is active or not. 6 | var isActive: Bool { get } 7 | } 8 | 9 | extension Observer { 10 | public var isInactive: Bool { 11 | !self.isActive 12 | } 13 | } 14 | 15 | open class AbstractObserver: Observer { 16 | public init() {} 17 | 18 | open internal(set) var isActive: Bool = false 19 | } 20 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Observer.HandlerDefinition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Handler definition provides a way of storing and managing individual notification handlers, most properties 4 | /// represent arguments passed into `NotificationCenter.addObserverForName` method. 5 | public protocol ObserverHandlerDefinition { 6 | var isActive: Bool { get } 7 | } 8 | 9 | extension ObserverHandlerDefinition { 10 | public var isInactive: Bool { 11 | !self.isActive 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Observatory/Test/Keyboard/Test.Keyboard.Hotkey.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | import Observatory 4 | import Quick 5 | 6 | internal class KeyboardHotkeySpec: Spec { 7 | override internal func spec() { 8 | it("must correctly initialise with raw value") { 9 | let hotkeyFoo: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.a, modifier: KeyboardModifier.commandKey) 10 | let hotkeyBar: KeyboardHotkey = KeyboardHotkey(rawValue: hotkeyFoo.rawValue) 11 | expect(hotkeyFoo).to(equal(hotkeyBar)) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /source/Observatory/Testing/Testing.Event.swift: -------------------------------------------------------------------------------- 1 | import Carbon 2 | 3 | internal class Event { 4 | internal static func postMouseEvent(type: CGEventType, position: CGPoint? = nil, tap: CGEventTapLocation? = nil) { 5 | let event: CGEvent = CGEvent(mouseEventSource: nil, mouseType: type, mouseCursorPosition: position ?? CGPoint(x: -10000, y: -10000), mouseButton: CGMouseButton.center)! 6 | event.post(tap: tap ?? CGEventTapLocation.cghidEventTap) 7 | self.wait() 8 | } 9 | 10 | private static func wait() { 11 | RunCurrentEventLoop(1 / 1000) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Observatory/Testing/Testing.Observation.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | 3 | /// Simple observation object for tracking down and asserting observations during testing. 4 | internal class Observation { 5 | internal private(set) var count: Int = 0 6 | 7 | internal func make() { 8 | self.count += 1 9 | } 10 | 11 | internal func reset() { 12 | self.count = 0 13 | } 14 | 15 | internal func assert(count: Int? = nil, reset: Bool? = nil) { 16 | if let count: Int = count { 17 | expect(self.count).to(equal(count)) 18 | } else { 19 | expect(self.count).to(beGreaterThan(0)) 20 | } 21 | 22 | if reset ?? true { 23 | self.reset() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Observatory.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Observatory' 3 | spec.version = '0.2.5' 4 | spec.summary = 'Cocoa and Carbon event, notification and hotkey observing framework 🔭 in pure Swift 💯' 5 | spec.license = { :type => 'MIT' } 6 | spec.homepage = 'https://github.com/swifteroid/stone' 7 | spec.authors = { 'Ian Bytchek' => 'ianbytchek@gmail.com' } 8 | 9 | spec.platform = :osx, '10.11' 10 | 11 | spec.source = { :git => 'https://github.com/swifteroid/stone.git', :tag => "#{spec.version}" } 12 | spec.source_files = 'source/Observatory/**/*.{swift,h,m}' 13 | spec.exclude_files = 'source/Observatory/{Test,Testing}/**/*' 14 | spec.swift_version = '4' 15 | 16 | spec.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS[config=Release]' => '-suppress-warnings' } 17 | end -------------------------------------------------------------------------------- /accessory/info/test.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /accessory/info/framework.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Observatory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Ian Bytchek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/realm/SwiftLint#configuration 2 | # https://realm.github.io/SwiftLint/rule-directory.html 3 | # swiftlint rules 4 | 5 | included: source 6 | 7 | analyzer_rules: 8 | - explicit_self 9 | 10 | opt_in_rules: 11 | - explicit_top_level_acl 12 | - modifier_order 13 | - prefer_self_type_over_type_of_self 14 | 15 | disabled_rules: 16 | - cyclomatic_complexity 17 | - discarded_notification_center_observer 18 | - duplicate_imports 19 | - file_length 20 | - force_cast 21 | - force_try 22 | - function_body_length 23 | - function_parameter_count 24 | - identifier_name 25 | - large_tuple 26 | - line_length 27 | - nesting 28 | - opening_brace 29 | - private_over_fileprivate 30 | - redundant_discardable_let 31 | - type_body_length 32 | - type_name 33 | - unused_closure_parameter 34 | - unused_setter_value # https://github.com/realm/SwiftLint/issues/2585 35 | - weak_delegate 36 | 37 | trailing_comma: 38 | mandatory_comma: true 39 | 40 | vertical_whitespace: 41 | max_empty_lines: 2 42 | 43 | switch_case_alignment: 44 | indented_cases: true 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Observatory", 6 | platforms: [ 7 | .macOS(.v10_13) 8 | ], 9 | products: [ 10 | .library(name: "Observatory", targets: ["Observatory"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/Quick/Nimble.git", from: "11.0.0"), 14 | .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "Observatory", 19 | path: "source/Observatory", 20 | exclude: ["Test", "Testing"] 21 | ), 22 | .testTarget( 23 | name: "Observatory-Test", 24 | dependencies: ["Observatory", "Quick", "Nimble"], 25 | path: "source/Observatory", 26 | exclude: ["Keyboard", "Observer", "Shortcut"] 27 | // sources: ["Test", "Testing"] // Since tools 5.3 this alone doesn't work and produces a warning… no need for it really… 28 | ), 29 | ], 30 | swiftLanguageVersions: [.v5] 31 | ) 32 | -------------------------------------------------------------------------------- /accessory/info/application.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © Swifteroid. All rights reserved. 29 | NSMainStoryboardFile 30 | demo 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", 10 | "version": "2.1.1" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", 19 | "version": "2.1.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "b7f6c49acdb247e3158198c5448b38c3cc595533", 28 | "version": "11.2.1" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "f9d519828bb03dfc8125467d8f7b93131951124c", 37 | "version": "5.0.1" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Observatory.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cwlcatchexception", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state" : { 8 | "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", 9 | "version" : "2.1.1" 10 | } 11 | }, 12 | { 13 | "identity" : "cwlpreconditiontesting", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state" : { 17 | "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", 18 | "version" : "2.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "nimble", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/Quick/Nimble.git", 25 | "state" : { 26 | "revision" : "b7f6c49acdb247e3158198c5448b38c3cc595533", 27 | "version" : "11.2.1" 28 | } 29 | }, 30 | { 31 | "identity" : "quick", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/Quick/Quick.git", 34 | "state" : { 35 | "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", 36 | "version" : "5.0.1" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Observatory.xcworkspace/xcshareddata/Observatory.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "5B180034B55A95756EE394C97170F0057EF486DE", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "5B180034B55A95756EE394C97170F0057EF486DE" : 9223372036854775807, 8 | "FB92343999F4DECE54225E041E190E57FF949DB8" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "A1415912-30D9-46F9-843E-00D947CDCCD7", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "5B180034B55A95756EE394C97170F0057EF486DE" : "observatory\/", 13 | }, 14 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Observatory", 15 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 16 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Observatory.xcworkspace", 17 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 18 | { 19 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Swifteroid\/Observatory.git", 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "5B180034B55A95756EE394C97170F0057EF486DE" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /source/Observatory/Shortcut/Shortcut.Recorder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ShortcutRecorder: AnyObject { 4 | 5 | /// Specifies whether recorder is currently recording or not. 6 | var isRecording: Bool { get set } 7 | 8 | /// Current shortcut. 9 | var shortcut: Shortcut? { get set } 10 | } 11 | 12 | extension ShortcutRecorder { 13 | /// Posted prior `ShortcutRecorder`'s `shortcut` property gets changed, this gets posted, both, when the shortcut is changed 14 | /// by outside code and when the new hotkey gets recorded. 15 | public static var shortcutWillChangeNotification: Notification.Name { Notification.Name("\(ShortcutRecorder.self)ShortcutWillChangeNotification") } 16 | 17 | /// Posted after `ShortcutRecorder`'s `shortcut` value gets changed, this gets posted, both, when the shortcut is changed 18 | /// by outside code and when the new hotkey gets recorded. 19 | public static var shortcutDidChangeNotification: Notification.Name { Notification.Name("\(ShortcutRecorder.self)ShortcutDidChangeNotification") } 20 | 21 | /// Posted after `ShortcutRecorder`'s associated hotkey gets recorded and after `shortcutWillChange` and `shortcutDidChange` 22 | /// notifications. This is posted only when new hotkey gets recoded by the user. 23 | public static var hotkeyDidRecordNotification: Notification.Name { Notification.Name("\(ShortcutRecorder.self)HotkeyDidRecordNotification") } 24 | } 25 | -------------------------------------------------------------------------------- /source/Observatory/Keyboard/Keyboard.Hotkey.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Foundation 3 | 4 | /// Hotkey stores key and modifier into first and second half of 32-bit raw value integer. 5 | public struct KeyboardHotkey: RawRepresentable { 6 | public init(rawValue: Int) { 7 | self.init(key: KeyboardKey(UInt16(truncatingIfNeeded: rawValue)), modifier: KeyboardModifier(rawValue: Int(truncatingIfNeeded: rawValue >> 16))) 8 | } 9 | 10 | public init(key: KeyboardKey, modifier: KeyboardModifier) { 11 | self.key = key 12 | self.modifier = modifier 13 | } 14 | 15 | public init(_ rawValue: Int) { 16 | self.init(rawValue: rawValue) 17 | } 18 | 19 | public init?(_ event: NSEvent) { 20 | if event.type == .keyUp || event.type == .keyDown || event.type == .flagsChanged { 21 | self.init(key: KeyboardKey(event), modifier: KeyboardModifier(event)) 22 | } else { 23 | return nil 24 | } 25 | } 26 | 27 | public var key: KeyboardKey 28 | public var modifier: KeyboardModifier 29 | public var rawValue: Int { self.modifier.rawValue << 16 | self.key.rawValue } 30 | } 31 | 32 | extension KeyboardHotkey: Equatable, Hashable { 33 | public func hash(into hasher: inout Hasher) { hasher.combine(self.rawValue) } 34 | } 35 | 36 | extension KeyboardHotkey: CustomStringConvertible { 37 | public var description: String { "\(self.modifier.name ?? "")\(self.key.name ?? "")" } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Observatory 2 | 3 | Observatory is a micro-framework for easier event, notification and hotkey management in Swift. 4 | 5 | - Standardised approach for event, notification and hotkey observing. 6 | - Simple enabling and disabling of observers. 7 | - Rich choice of handler signatures. 8 | - Handle local / global / both events. 9 | - Chaining support. 10 | 11 | Observe global hotkeys. 12 | 13 | ```swift 14 | let observer: HotkeyObserver = HotkeyObserver(active: true) 15 | let fooHotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.five, modifier: [.commandKey, .shiftKey]) 16 | let barHotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.six, modifier: [.commandKey, .shiftKey]) 17 | 18 | observer 19 | .add(hotkey: fooHotkey) { Swift.print("Such foo…") } 20 | .add(hotkey: barHotkey) { Swift.print("So bar…") } 21 | ``` 22 | 23 | Observe notifications, chose between plain `() -> ()` or standard `(Notification) -> ()` signatures. 24 | 25 | ```swift 26 | let observer: NotificationObserver = NotificationObserver(active: true) 27 | let observee: AnyObject = NSObject() 28 | 29 | observer 30 | .add(name: Notification.Name("foo"), observee: observee) { Swift.print("Foo captain!") } 31 | .add(names: [Notification.Name("bar"), Notification.Name("baz")], observee: observee) { Swift.print("Yes \($0.name)!") } 32 | ``` 33 | 34 | Observe events, like with notifications, you can chose between plain `() -> ()` and standard local `(NSEvent) -> NSEvent?` or global `(NSEvent) -> ()` signatures. 35 | 36 | ```swift 37 | let observer: EventObserver = EventObserver(active: true) 38 | 39 | observer 40 | .add(mask: .any, handler: { Swift.print("Any is better than none.") }) 41 | .add(mask: [.leftMouseDown, .leftMouseUp], handler: { Swift.print("It's a \($0.type) event!") }) 42 | ``` 43 | 44 | Checkout `Observatory.playground` for information and examples. 45 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables 2 | # https://github.com/peripheryapp/periphery/blob/master/.github/workflows/test.yml 3 | 4 | name: Lint & Test 5 | 6 | on: 7 | push: { branches: [ main ] } 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | skip: 13 | name: Pre-Check & Skip 14 | continue-on-error: true 15 | runs-on: ubuntu-latest 16 | outputs: 17 | should_skip: ${{ steps.check.outputs.should_skip }} 18 | steps: 19 | - name: Skip duplicate actions 20 | id: check 21 | uses: fkirc/skip-duplicate-actions@v5 22 | 23 | main: 24 | name: Lint & Test 25 | needs: skip 26 | if: needs.skip.outputs.should_skip != 'true' 27 | runs-on: macos-latest 28 | 29 | steps: 30 | - name: Checkout Git 31 | uses: actions/checkout@v6 32 | with: 33 | submodules: recursive 34 | 35 | 36 | # ⚙️ Tools 37 | 38 | - name: Install Homebrew 39 | uses: tecolicom/actions-use-homebrew-tools@v1 40 | with: 41 | tools: xcbeautify swiftlint 42 | cache: yes 43 | 44 | 45 | # 📦 Cache 46 | 47 | - name: Cache SPM 48 | id: cache-spm 49 | uses: actions/cache@v4 50 | with: 51 | path: .build 52 | key: ${{ runner.os }}-spm-${{ hashFiles('Package.swift', 'Package.resolved') }} 53 | restore-keys: ${{ runner.os }}-spm- 54 | 55 | 56 | # 💅 Lint 57 | 58 | - name: Run SwiftLint 59 | run: swiftlint lint --quiet --strict # --reporter github-actions-logging 60 | 61 | 62 | # 🧪 Resolve, Build, Test SPM 63 | 64 | - name: Resolve SPM 65 | if: steps.cache-spm.outputs.cache-hit != 'true' 66 | run: swift package resolve 67 | 68 | - name: Build SPM 69 | run: swift build --build-tests 70 | 71 | - name: Test SPM 72 | run: swift test 2>&1 | xcbeautify 73 | -------------------------------------------------------------------------------- /source/Observatory/Test/Test.swift: -------------------------------------------------------------------------------- 1 | import Carbon 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | internal class Spec: QuickSpec { 7 | 8 | /// This was something cool and used in some other testing, can't remember… Leaving as a reminder. 9 | private func sendHotkeyEvent(identifier: EventHotKeyID) { 10 | let eventHotKeyIDPointer: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) 11 | eventHotKeyIDPointer.initialize(to: identifier) 12 | 13 | var eventPointer: OpaquePointer? 14 | CreateEvent(nil, UInt32(kEventClassKeyboard), UInt32(kEventHotKeyPressed), 0, 0, &eventPointer) 15 | SetEventParameter(eventPointer, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), MemoryLayout.size, eventHotKeyIDPointer) 16 | 17 | // We send event directly to our application target only. 18 | 19 | SendEventToEventTarget(eventPointer, GetApplicationEventTarget()) 20 | } 21 | 22 | /// Posts a real hotkey event, make sure to not invoke dangerous hotkeys with it… ⚠️ 23 | internal func postHotkeyEvent(key: CGKeyCode, flag: CGEventFlags) { 24 | let downEvent: CGEvent = CGEvent(keyboardEventSource: nil, virtualKey: key, keyDown: true)! 25 | let upEvent: CGEvent = CGEvent(keyboardEventSource: nil, virtualKey: key, keyDown: false)! 26 | downEvent.flags = flag 27 | upEvent.flags = flag 28 | 29 | downEvent.post(tap: CGEventTapLocation.cghidEventTap) 30 | upEvent.post(tap: CGEventTapLocation.cghidEventTap) 31 | 32 | var eventType: [EventTypeSpec] = [ 33 | EventTypeSpec(eventClass: UInt32(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), 34 | EventTypeSpec(eventClass: UInt32(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)), 35 | ] 36 | 37 | var pointer: OpaquePointer? 38 | 39 | while ReceiveNextEvent(eventType.count, &eventType, 50 / 1000, true, &pointer) == Darwin.noErr { 40 | SendEventToEventTarget(pointer, GetApplicationEventTarget()) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Observatory.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > **To use the playground make sure to open it from within `Observatory.xcworkspace` and 3 | > build the `Observatory` scheme first, results will appear the debug area.** 4 | */ 5 | 6 | import Foundation 7 | import Observatory 8 | 9 | let center: NotificationCenter = NotificationCenter.default 10 | let queue: OperationQueue = OperationQueue() 11 | let observee: NSObject = NSObject() 12 | 13 | /*: 14 | By default observer is not activated, you can activate it with initialiser or 15 | by setting `active` property. 16 | */ 17 | 18 | var observer: NotificationObserver? = NotificationObserver(active: true) 19 | 20 | /*: 21 | Adding handlers for notifications is nearly the same as with the notification 22 | center, but gives more options and flexibility – can add multiple notifications in one 23 | go, can omit `notification` parameter in the callback, can use chaining, etc. 24 | */ 25 | 26 | observer! 27 | .add(name: Notification.Name("foo"), observee: observee) { Swift.print("foo") } 28 | .add(names: [Notification.Name("bar"), Notification.Name("baz")], observee: observee) { (notification: Notification) in Swift.print(notification.name) } 29 | 30 | center.post(name: Notification.Name("foo"), object: observee) 31 | center.post(name: Notification.Name("bar"), object: observee) 32 | center.post(name: Notification.Name("baz"), object: observee) 33 | 34 | /*: 35 | When the observer is no longer needed it can be deactivated and reactivated later, this is 36 | handy when, for example, observer must be active only when the view is visible. 37 | */ 38 | 39 | observer!.isActive = false 40 | 41 | /*: 42 | Handlers can be removed on more than one way – all by notification name, all by observable 43 | object or using a combinations. 44 | */ 45 | 46 | observer!.remove(name: Notification.Name("foo")) 47 | observer!.remove(name: Notification.Name("bar"), observee: observee) 48 | observer!.remove(observee: observee) 49 | 50 | /*: 51 | If the observer is no longer needed it can be simply dismissed. It will automatically deactivate 52 | and remove all handlers, so you don't have to worry about that. 53 | */ 54 | 55 | observer = nil 56 | -------------------------------------------------------------------------------- /source/Observatory/Test/Observer/Test.Observer.Notification.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | import Observatory 4 | import Quick 5 | 6 | internal class NotificationObserverSpec: Spec { 7 | override internal func spec() { 8 | it("can observe Foundation notifications in active state") { 9 | let center: NotificationCenter = NotificationCenter.default 10 | let observer: NotificationObserver = NotificationObserver(active: true) 11 | let observee: AnyObject = NSObject() 12 | let fooName: Notification.Name = .init("foo") 13 | let barName: Notification.Name = .init("bar") 14 | 15 | var foo: Int = 0 16 | var bar: Int = 0 17 | 18 | observer.add(name: fooName, observee: nil) { foo += 1 } 19 | observer.add(name: barName, observee: observee) { bar += 1 } 20 | 21 | // Foo will get caught on all objects, bar will only be caught on observable. 22 | 23 | center.post(name: fooName, object: nil) 24 | center.post(name: fooName, object: observee) 25 | center.post(name: barName, object: nil) 26 | center.post(name: barName, object: observee) 27 | 28 | expect(foo).to(equal(2)) 29 | expect(bar).to(equal(1)) 30 | 31 | // Deactivated observer must not catch anything. 32 | 33 | observer.isActive = false 34 | 35 | center.post(name: fooName, object: observee) 36 | center.post(name: barName, object: observee) 37 | 38 | expect(foo).to(equal(2)) 39 | expect(bar).to(equal(1)) 40 | 41 | // Reactivated observer must work… 42 | 43 | observer.isActive = true 44 | 45 | center.post(name: fooName, object: observee) 46 | center.post(name: barName, object: observee) 47 | 48 | expect(foo).to(equal(3)) 49 | expect(bar).to(equal(2)) 50 | } 51 | } 52 | 53 | private func readmeSample() { 54 | let observer: NotificationObserver = NotificationObserver(active: true) 55 | let observee: AnyObject = NSObject() 56 | 57 | observer 58 | .add(name: Notification.Name("foo"), observee: observee) { Swift.print("Foo captain!") } 59 | .add(names: [Notification.Name("bar"), Notification.Name("baz")], observee: observee) { Swift.print("Yes \($0.name)!") } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Notification/Observer.Notification.Handler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NotificationObserver { 4 | public struct Handler { 5 | public typealias Signature = (Notification) -> Void 6 | 7 | open class Definition: ObserverHandlerDefinition { 8 | deinit { 9 | self.deactivate() 10 | } 11 | 12 | init(name: Notification.Name, observee: AnyObject?, queue: OperationQueue?, handler: @escaping Signature) { 13 | self.name = name 14 | self.observee = observee 15 | self.queue = queue 16 | self.handler = handler 17 | } 18 | 19 | public let name: Notification.Name 20 | open private(set) weak var observee: AnyObject? 21 | public let queue: OperationQueue? 22 | public let handler: Signature 23 | 24 | open private(set) var monitor: AnyObject? 25 | open private(set) var center: NotificationCenter? 26 | 27 | open private(set) var isActive: Bool = false 28 | 29 | @discardableResult open func activate(_ newValue: Bool = true, center: NotificationCenter? = nil) -> Self { 30 | if newValue == self.isActive { return self } 31 | 32 | if newValue { 33 | let center = center ?? self.center ?? NotificationCenter.default 34 | self.monitor = center.addObserver(forName: self.name, object: self.observee, queue: self.queue, using: self.handler) 35 | self.center = center 36 | } else if let center = self.center, let monitor = self.monitor { 37 | center.removeObserver(monitor) 38 | self.monitor = nil 39 | self.center = nil 40 | } 41 | 42 | self.isActive = newValue 43 | return self 44 | } 45 | 46 | @discardableResult open func deactivate() -> Self { 47 | self.activate(false) 48 | } 49 | } 50 | } 51 | } 52 | 53 | /// Convenience initializers. 54 | extension NotificationObserver.Handler.Definition { 55 | public convenience init(name: Notification.Name, observee: AnyObject? = nil, queue: OperationQueue? = nil, handler: @escaping () -> Void) { 56 | self.init(name: name, observee: observee, queue: queue, handler: { _ in handler() }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/Observatory/Test/Keyboard/Test.Keyboard.Modifier.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import CoreGraphics 3 | import Foundation 4 | import Nimble 5 | import Observatory 6 | import Quick 7 | 8 | internal class KeyboardModifierSpec: Spec { 9 | override internal func spec() { 10 | it("can return keys") { 11 | expect(KeyboardModifier.capsLockKey.keys) == [.capsLock] 12 | expect(KeyboardModifier.commandKey.keys) == [.command] 13 | expect(KeyboardModifier.controlKey.keys) == [.control] 14 | expect(KeyboardModifier.optionKey.keys) == [.option] 15 | expect(KeyboardModifier.shiftKey.keys) == [.shift] 16 | } 17 | 18 | it("can return name") { 19 | expect(KeyboardModifier.capsLockKey.name) == "⇪" 20 | expect(KeyboardModifier.commandKey.name) == "⌘" 21 | expect(KeyboardModifier.controlKey.name) == "⌃" 22 | expect(KeyboardModifier.optionKey.name) == "⌥" 23 | expect(KeyboardModifier.shiftKey.name) == "⇧" 24 | } 25 | 26 | it("should return keys and name in correct order") { 27 | let modifier: KeyboardModifier = [.capsLockKey, .commandKey, .controlKey, .optionKey, .shiftKey] 28 | expect(modifier.keys) == [.control, .option, .capsLock, .shift, .command] 29 | expect(modifier.name) == "⌃⌥⇪⇧⌘" 30 | } 31 | 32 | it("can convert into CGEventFlags") { 33 | expect(CGEventFlags(modifier: KeyboardModifier.capsLockKey)) == .maskAlphaShift 34 | expect(CGEventFlags(modifier: KeyboardModifier.commandKey)) == .maskCommand 35 | expect(CGEventFlags(modifier: KeyboardModifier.controlKey)) == .maskControl 36 | expect(CGEventFlags(modifier: KeyboardModifier.optionKey)) == .maskAlternate 37 | expect(CGEventFlags(modifier: KeyboardModifier.shiftKey)) == .maskShift 38 | let modifier: KeyboardModifier = [.capsLockKey, .commandKey, .controlKey, .optionKey, .shiftKey] 39 | expect(CGEventFlags(modifier: modifier)) == [.maskAlphaShift, .maskCommand, .maskControl, .maskAlternate, .maskShift] 40 | } 41 | 42 | it("can convert into NSEvent.ModifierFlags") { 43 | expect(NSEvent.ModifierFlags(modifier: KeyboardModifier.capsLockKey)) == .capsLock 44 | expect(NSEvent.ModifierFlags(modifier: KeyboardModifier.commandKey)) == .command 45 | expect(NSEvent.ModifierFlags(modifier: KeyboardModifier.controlKey)) == .control 46 | expect(NSEvent.ModifierFlags(modifier: KeyboardModifier.optionKey)) == .option 47 | expect(NSEvent.ModifierFlags(modifier: KeyboardModifier.shiftKey)) == .shift 48 | let modifier: KeyboardModifier = [.capsLockKey, .commandKey, .controlKey, .optionKey, .shiftKey] 49 | expect(NSEvent.ModifierFlags(modifier: modifier)) == [.capsLock, .command, .control, .option, .shift] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /source/Demo/Entrypoint.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Observatory 3 | 4 | @NSApplicationMain public class AppDelegate: NSObject, NSApplicationDelegate { 5 | } 6 | 7 | open class ViewController: NSViewController { 8 | @IBOutlet private weak var buttonFoo: ShortcutRecorderButton! 9 | @IBOutlet private weak var buttonBar: ShortcutRecorderButton! 10 | @IBOutlet private weak var buttonBaz: ShortcutRecorderButton! 11 | @IBOutlet private weak var buttonQux: ShortcutRecorderButton! 12 | @IBOutlet private weak var buttonFex: ShortcutRecorderButton! 13 | 14 | /// Shortcut center observer. 15 | private lazy var observer: NotificationObserver = NotificationObserver(active: true) 16 | 17 | override open func viewDidLoad() { 18 | self.buttonFoo.shortcut = .foo 19 | self.buttonBar.shortcut = .bar 20 | self.buttonBaz.shortcut = .baz 21 | self.buttonQux.shortcut = .qux 22 | self.buttonFex.shortcut = .fex 23 | 24 | observer.add(name: ShortcutCenter.willInvokeShortcutNotification, observee: ShortcutCenter.default, handler: { [weak self] in self?.handleShortcutCenterNotification(notification: $0) }) 25 | observer.add(name: ShortcutCenter.didInvokeShortcutNotification, observee: ShortcutCenter.default, handler: { [weak self] in self?.handleShortcutCenterNotification(notification: $0) }) 26 | } 27 | 28 | override open func viewDidAppear() { 29 | /// Reset first responder, do it asynchronously, because the window will modify it once presented. 30 | DispatchQueue.main.async(execute: { self.view.window?.makeFirstResponder(nil) }) 31 | } 32 | 33 | private func handleShortcutCenterNotification(notification: Notification) { 34 | let info: [String: Any] = notification.userInfo as! [String: Any] 35 | let shortcut: Shortcut = info[ShortcutCenter.shortcutUserInfo] as! Shortcut 36 | Swift.print("\(notification.name.rawValue): \(shortcut)") 37 | } 38 | 39 | @IBAction private func reset(_ sender: Any?) { 40 | self.buttonFoo.shortcut?.hotkey = .foo 41 | self.buttonBar.shortcut?.hotkey = .bar 42 | self.buttonBaz.shortcut?.hotkey = .baz 43 | self.buttonQux.shortcut?.hotkey = .qux 44 | self.buttonFex.shortcut?.hotkey = nil 45 | } 46 | } 47 | 48 | extension KeyboardHotkey { 49 | fileprivate static let foo = KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey]) 50 | fileprivate static let bar = KeyboardHotkey(key: .two, modifier: [.commandKey, .shiftKey]) 51 | fileprivate static let baz = KeyboardHotkey(key: .three, modifier: [.commandKey, .shiftKey]) 52 | fileprivate static let qux = KeyboardHotkey(key: .four, modifier: [.commandKey, .shiftKey]) 53 | } 54 | 55 | extension Shortcut { 56 | fileprivate static let foo = Shortcut(.foo) 57 | fileprivate static let bar = Shortcut(.bar) 58 | fileprivate static let baz = Shortcut(.baz) 59 | fileprivate static let qux = Shortcut(.qux) 60 | fileprivate static let fex = Shortcut() 61 | } 62 | -------------------------------------------------------------------------------- /source/Observatory/Test/Observer/Test.Observer.Event.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Carbon 3 | import Foundation 4 | import Nimble 5 | import Observatory 6 | import Quick 7 | 8 | /// Wow, this turned out to be a serious pain in the ass – testing events is not a joke… Doing it properly requires running 9 | /// a loop, as far as I understand there's no way testing global event dispatch, because, quoting, handler will not be called 10 | /// for events that are sent to your own application. Instead, we check that observers sets everything up correctly. 11 | internal class EventObserverSpec: Spec { 12 | override internal func spec() { 13 | it("can observe AppKit events in active state") { 14 | let observer: EventObserver = EventObserver(active: true) 15 | 16 | observer.add(mask: NSEvent.EventTypeMask.any, handler: {}) 17 | expect(observer.appKitDefinitions[0].handler.global).toNot(beNil()) 18 | expect(observer.appKitDefinitions[0].handler.local).toNot(beNil()) 19 | expect(observer.appKitDefinitions[0].monitor).toNot(beNil()) 20 | 21 | observer.add(mask: NSEvent.EventTypeMask.any, global: {}) 22 | expect(observer.appKitDefinitions[1].handler.global).toNot(beNil()) 23 | expect(observer.appKitDefinitions[1].handler.local).to(beNil()) 24 | expect(observer.appKitDefinitions[1].monitor).toNot(beNil()) 25 | 26 | observer.add(mask: NSEvent.EventTypeMask.any, local: {}) 27 | expect(observer.appKitDefinitions[2].handler.global).to(beNil()) 28 | expect(observer.appKitDefinitions[2].handler.local).toNot(beNil()) 29 | expect(observer.appKitDefinitions[2].monitor).toNot(beNil()) 30 | 31 | observer.isActive = false 32 | 33 | expect(observer.appKitDefinitions[0].monitor).to(beNil()) 34 | expect(observer.appKitDefinitions[1].monitor).to(beNil()) 35 | expect(observer.appKitDefinitions[2].monitor).to(beNil()) 36 | 37 | observer.isActive = true 38 | 39 | expect(observer.appKitDefinitions[0].monitor).toNot(beNil()) 40 | expect(observer.appKitDefinitions[1].monitor).toNot(beNil()) 41 | expect(observer.appKitDefinitions[2].monitor).toNot(beNil()) 42 | } 43 | 44 | it("can observe Carbon events in active state") { 45 | let observer: EventObserver = EventObserver(active: true) 46 | let observation: Observation = Observation() 47 | 48 | observer.add(mask: NSEvent.EventTypeMask.leftMouseDown.union(.rightMouseDown).rawValue, handler: { observation.make() }) 49 | Event.postMouseEvent(type: CGEventType.leftMouseDown) 50 | Event.postMouseEvent(type: CGEventType.leftMouseUp) 51 | Event.postMouseEvent(type: CGEventType.rightMouseDown) 52 | Event.postMouseEvent(type: CGEventType.rightMouseUp) 53 | observation.assert(count: 2) 54 | 55 | observer.isActive = false 56 | Event.postMouseEvent(type: CGEventType.leftMouseDown) 57 | Event.postMouseEvent(type: CGEventType.leftMouseUp) 58 | observation.assert(count: 0) 59 | } 60 | } 61 | 62 | private func readmeSample() { 63 | let observer: EventObserver = EventObserver(active: true) 64 | 65 | observer 66 | .add(mask: .any, handler: { Swift.print("Any is better than none.") }) 67 | .add(mask: [.leftMouseDown, .leftMouseUp], handler: { Swift.print("It's a \($0.type) event!") }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /source/Observatory/Test/Keyboard/Test.Keyboard.Key.swift: -------------------------------------------------------------------------------- 1 | import Carbon 2 | import Foundation 3 | import Nimble 4 | import Observatory 5 | import Quick 6 | 7 | internal class KeyboardKeySpec: Spec { 8 | override internal func spec() { 9 | it("can init with string") { 10 | expect(KeyboardKey("")).to(beNil()) 11 | expect(KeyboardKey("…")).to(beNil()) 12 | expect(KeyboardKey("a")) == .a 13 | expect(KeyboardKey("A")) == .a 14 | expect(KeyboardKey("⎋")) == .escape 15 | expect(KeyboardKey("\u{001B}", layout: .ascii)) == .escape 16 | expect(KeyboardKey("Esc", custom: [.escape: "Esc"])) == .escape 17 | } 18 | 19 | it("can init with string in specified keyboard layout") { 20 | let layout = UCKeyboardLayout.data(for: "com.apple.keylayout.Ukrainian") 21 | expect(layout).toNot(beNil()) 22 | expect(KeyboardKey("ф", layout: layout)) == .a 23 | expect(KeyboardKey("Ф", layout: layout)) == .a 24 | } 25 | 26 | it("can return human-readable key name") { 27 | expect(KeyboardKey.a.name) == "A" 28 | expect(KeyboardKey.a.name(layout: .ascii)) == "A" 29 | expect(KeyboardKey.escape.name) == "⎋" 30 | expect(KeyboardKey.escape.name(layout: .ascii)) == "\u{001B}" 31 | expect(KeyboardKey.escape.name(custom: [.escape: "Esc"])) == "Esc" 32 | } 33 | 34 | it("can return key name in specified keyboard layout") { 35 | let layout = UCKeyboardLayout.data(for: "com.apple.keylayout.Ukrainian") 36 | expect(layout).toNot(beNil()) 37 | expect(KeyboardKey.a.name(layout: layout)) == "Ф" 38 | } 39 | 40 | it("can handle multi-threading") { 41 | let queue = OperationQueue() 42 | queue.maxConcurrentOperationCount = 25 43 | for _ in 0 ..< 100 { 44 | queue.addOperation({ 45 | for _ in 0 ..< 1 { 46 | autoreleasepool { 47 | _ = KeyboardKey.a.name(layout: .ascii) 48 | _ = KeyboardKey.escape.name(layout: .ascii) 49 | } 50 | } 51 | }) 52 | } 53 | queue.waitUntilAllOperationsAreFinished() 54 | } 55 | 56 | it("can get layout data quickly") { 57 | let time = CFAbsoluteTimeGetCurrent() 58 | let iterationCount = 25_000 59 | for _ in 0 ..< iterationCount { autoreleasepool { _ = KeyboardKey.Layout.allCases.randomElement()!.data } } 60 | let duration = (CFAbsoluteTimeGetCurrent() - time) * 1000 61 | expect(duration) <= 500 62 | // Swift.print(String(format: "Getting layout data for \(iterationCount) times took %.3f ms.", duration)) 63 | } 64 | } 65 | } 66 | 67 | extension UCKeyboardLayout { 68 | fileprivate static func data(for id: String) -> Data? { 69 | // Using properties filter to get the language doesn't work as expected and returns a different input… 🤔 70 | let inputSources = TISCreateInputSourceList(nil, true).takeRetainedValue() as? [TISInputSource] 71 | guard let inputSource = inputSources?.first(where: { unsafeBitCast(TISGetInputSourceProperty($0, kTISPropertyInputSourceID), to: CFString.self) as String == id }) else { return nil } 72 | guard let data = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData) else { return nil } 73 | guard let data = Unmanaged.fromOpaque(data).takeUnretainedValue() as? NSData, data.count > 0 else { return nil } 74 | return Data(referencing: data) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Observatory.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Notification/Observer.Notification.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Notification observer provides an interface for registering and managing multiple notification handlers. When we register 4 | /// notification handler observer creates handler definition – it manages that specific notification-handler association. 5 | open class NotificationObserver: AbstractObserver { 6 | public init(active: Bool = false, center: NotificationCenter? = nil) { 7 | super.init() 8 | self.center = center 9 | self.activate(active) 10 | } 11 | 12 | open var center: NotificationCenter? 13 | 14 | open private(set) var definitions: [Handler.Definition] = [] 15 | 16 | @discardableResult open func add(definition: Handler.Definition) -> Self { 17 | self.definitions.append(definition.activate(self.isActive)) 18 | return self 19 | } 20 | 21 | @discardableResult open func add(definitions: [Handler.Definition]) -> Self { 22 | for definition in definitions { self.add(definition: definition) } 23 | return self 24 | } 25 | 26 | @discardableResult open func remove(definition: Handler.Definition) -> Self { 27 | self.definitions.enumerated().first(where: { $0.1 === definition }).map({ self.definitions.remove(at: $0.0) })?.deactivate() 28 | return self 29 | } 30 | 31 | @discardableResult open func remove(definitions: [Handler.Definition]) -> Self { 32 | for definition in definitions { self.remove(definition: definition) } 33 | return self 34 | } 35 | 36 | override open var isActive: Bool { 37 | get { super.isActive } 38 | set { self.activate(newValue) } 39 | } 40 | 41 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 42 | if newValue == self.isActive { return self } 43 | for definition in self.definitions { definition.activate(newValue, center: self.center) } 44 | super.isActive = newValue 45 | return self 46 | } 47 | 48 | @discardableResult open func deactivate() -> Self { 49 | self.activate(false) 50 | } 51 | 52 | // MARK: - 53 | 54 | @discardableResult open func add(name: Notification.Name, observee: AnyObject? = nil, queue: OperationQueue? = nil, handler: @escaping () -> Void) -> Self { 55 | self.add(definition: .init(name: name, observee: observee, queue: queue, handler: handler)) 56 | } 57 | 58 | @discardableResult open func add(name: Notification.Name, observee: AnyObject? = nil, queue: OperationQueue? = nil, handler: @escaping (Notification) -> Void) -> Self { 59 | self.add(definition: .init(name: name, observee: observee, queue: queue, handler: handler)) 60 | } 61 | 62 | @discardableResult open func add(names: [Notification.Name], observee: AnyObject? = nil, queue: OperationQueue? = nil, handler: @escaping () -> Void) -> Self { 63 | self.add(definitions: names.map({ .init(name: $0, observee: observee, queue: queue, handler: handler) })) 64 | } 65 | 66 | @discardableResult open func add(names: [Notification.Name], observee: AnyObject? = nil, queue: OperationQueue? = nil, handler: @escaping (Notification) -> Void) -> Self { 67 | self.add(definitions: names.map({ .init(name: $0, observee: observee, queue: queue, handler: handler) })) 68 | } 69 | 70 | @discardableResult open func remove(name: Notification.Name? = nil, observee: AnyObject? = nil, queue: OperationQueue? = nil) -> Self { 71 | self.remove(definitions: self.definitions.filter({ 72 | (name != nil || observee != nil || queue != nil) 73 | && (name == nil || $0.name == name) 74 | && (observee == nil || $0.observee === observee) 75 | && (queue == nil || $0.queue == queue) 76 | })) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | -------------------------------------------------------------------------------- /source/Observatory/Test/Shortcut/Test.Shortcut.swift: -------------------------------------------------------------------------------- 1 | import Carbon 2 | import Foundation 3 | import Nimble 4 | import Observatory 5 | import Quick 6 | 7 | internal class ShortcutSpec: Spec { 8 | override internal func spec() { 9 | 10 | // Do this prior running any tests to "flush" the `NSWillBecomeMultiThreadedNotification` notification, which fails tests 11 | // that expect certain notifications. 12 | self.postHotkeyEvent(key: CGKeyCode(KeyboardKey.escape.rawValue), flag: []) 13 | 14 | // Cleanup registered shortcuts after each test. 15 | afterEach({ ShortcutCenter.default.shortcuts.forEach({ $0.isEnabled = false }) }) 16 | 17 | it("can update registration") { 18 | var shortcut: Shortcut 19 | 20 | shortcut = Shortcut() 21 | expect(shortcut.isRegistered) == false 22 | 23 | shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey]), isEnabled: false) 24 | expect(shortcut.isRegistered) == false 25 | 26 | shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey])) 27 | expect(shortcut.isRegistered) == true 28 | shortcut.isEnabled = false 29 | expect(shortcut.isRegistered) == false 30 | 31 | shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey])) 32 | expect(shortcut.isRegistered) == true 33 | shortcut.hotkey = nil 34 | expect(shortcut.isRegistered) == false 35 | 36 | shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: .shiftKey)) 37 | expect(shortcut.isRegistered) == false 38 | 39 | shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: .capsLockKey)) 40 | expect(shortcut.isRegistered) == false 41 | } 42 | 43 | it("can add and remove observations") { 44 | let shortcut: Shortcut = Shortcut(KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey])) 45 | let observation: Any? 46 | var callbacks: Int = 0 47 | 48 | observation = shortcut.observe({ _ in callbacks += 1 }) 49 | self.postHotkeyEvent(key: CGKeyCode(KeyboardKey.one.rawValue), flag: [.maskCommand, .maskShift]) 50 | expect(callbacks) == 1 51 | 52 | shortcut.unobserve(observation!) 53 | self.postHotkeyEvent(key: CGKeyCode(KeyboardKey.one.rawValue), flag: [.maskCommand, .maskShift]) 54 | expect(callbacks) == 1 55 | } 56 | 57 | it("must post notifications when hotkey gets changed") { 58 | // Keep shortcut disabled to avoid `ShortcutCenter` registration notifications. 59 | let shortcut = Shortcut(isEnabled: false) 60 | let hotkey = KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey]) 61 | 62 | expect({ shortcut.hotkey = hotkey }).to(postNotifications(equal([ 63 | Notification(name: Shortcut.hotkeyWillChangeNotification, object: shortcut, userInfo: [Shortcut.hotkeyUserInfo: hotkey]), 64 | Notification(name: Shortcut.hotkeyDidChangeNotification, object: shortcut, userInfo: [Shortcut.hotkeyUserInfo: hotkey]), 65 | ]))) 66 | 67 | expect({ shortcut.hotkey = nil }).to(postNotifications(equal([ 68 | Notification(name: Shortcut.hotkeyWillChangeNotification, object: shortcut, userInfo: [Shortcut.hotkeyUserInfo: nil as KeyboardHotkey? as Any]), 69 | Notification(name: Shortcut.hotkeyDidChangeNotification, object: shortcut, userInfo: [Shortcut.hotkeyUserInfo: nil as KeyboardHotkey? as Any]), 70 | ]))) 71 | } 72 | 73 | it("must post notifications when registered shortcut gets invoked") { 74 | let hotkey = KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey]) 75 | let shortcut = Shortcut(hotkey) 76 | let center: ShortcutCenter = .default 77 | 78 | expect({ self.postHotkeyEvent(key: CGKeyCode(KeyboardKey.one.rawValue), flag: [.maskCommand, .maskShift]) }).to(postNotifications(equal([ 79 | Notification(name: ShortcutCenter.willInvokeShortcutNotification, object: center, userInfo: [ShortcutCenter.shortcutUserInfo: shortcut, ShortcutCenter.hotkeyUserInfo: hotkey]), 80 | Notification(name: ShortcutCenter.didInvokeShortcutNotification, object: center, userInfo: [ShortcutCenter.shortcutUserInfo: shortcut, ShortcutCenter.hotkeyUserInfo: hotkey]), 81 | ]))) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /source/Observatory/Test/Observer/Test.Observer.Hotkey.swift: -------------------------------------------------------------------------------- 1 | import Carbon 2 | import Foundation 3 | import Nimble 4 | import Observatory 5 | import Quick 6 | 7 | internal class HotkeyObserverSpec: Spec { 8 | override internal func spec() { 9 | it("can observe hotkeys in active state") { 10 | let observer: HotkeyObserver = HotkeyObserver(active: true) 11 | let modifier: CGEventFlags = [.maskCommand, .maskShift] 12 | let fooKey: CGKeyCode = CGKeyCode(KeyboardKey.one.rawValue) 13 | let barKey: CGKeyCode = CGKeyCode(KeyboardKey.two.rawValue) 14 | 15 | var foo: Int = 0 16 | var bar: Int = 0 17 | 18 | expect(observer.isActive) == true 19 | 20 | observer.add(hotkey: KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey])) { foo += 1 } 21 | observer.add(hotkey: KeyboardHotkey(key: .two, modifier: [.commandKey, .shiftKey])) { bar += 1 } 22 | 23 | self.postHotkeyEvent(key: fooKey, flag: modifier) 24 | self.postHotkeyEvent(key: barKey, flag: modifier) 25 | 26 | expect(foo).to(equal(1)) 27 | expect(bar).to(equal(1)) 28 | 29 | // Deactivated observer must not catch anything. 30 | 31 | observer.deactivate() 32 | expect(observer.isActive) == false 33 | 34 | self.postHotkeyEvent(key: fooKey, flag: modifier) 35 | self.postHotkeyEvent(key: barKey, flag: modifier) 36 | 37 | expect(foo).to(equal(1)) 38 | expect(bar).to(equal(1)) 39 | 40 | // Reactivated observer must work… 41 | 42 | observer.activate() 43 | expect(observer.isActive) == true 44 | 45 | self.postHotkeyEvent(key: fooKey, flag: modifier) 46 | self.postHotkeyEvent(key: barKey, flag: modifier) 47 | 48 | expect(foo).to(equal(2)) 49 | expect(bar).to(equal(2)) 50 | 51 | // Removing must work. 52 | 53 | observer.remove(hotkey: KeyboardHotkey(key: .one, modifier: [.commandKey, .shiftKey])) 54 | 55 | self.postHotkeyEvent(key: fooKey, flag: modifier) 56 | 57 | expect(foo).to(equal(2)) 58 | } 59 | 60 | it("must not produce definition error when adding valid hotkey observation") { 61 | let hotkeys: [KeyboardHotkey] = [ 62 | KeyboardHotkey(key: KeyboardKey.a, modifier: []), // Apparently regular keys can be registered without modifiers. 63 | KeyboardHotkey(key: KeyboardKey.f5, modifier: []), // Function keys can be registered without modifiers. 64 | KeyboardHotkey(key: KeyboardKey.a, modifier: .commandKey), 65 | KeyboardHotkey(key: KeyboardKey.a, modifier: .optionKey), 66 | KeyboardHotkey(key: KeyboardKey.a, modifier: .controlKey), 67 | ] 68 | 69 | for hotkey in hotkeys { 70 | expect(HotkeyObserver(active: true).add(hotkey: hotkey, handler: {}).definitions.first?.error).to(beNil(), description: String(describing: hotkey)) 71 | } 72 | } 73 | 74 | it("must produce definition error when adding invalid hotkey observation") { 75 | let hotkeys: [KeyboardHotkey] = [ 76 | KeyboardHotkey(key: KeyboardKey.a, modifier: .capsLockKey), // Caps lock is not a valid modifier. 77 | KeyboardHotkey(key: KeyboardKey.a, modifier: [.capsLockKey, .controlKey]), // Or any combination. 78 | ] 79 | 80 | for hotkey in hotkeys { 81 | expect(HotkeyObserver(active: true).add(hotkey: hotkey, handler: {}).definitions.first?.error).toNot(beNil(), description: String(describing: hotkey)) 82 | } 83 | } 84 | 85 | it("must produce definition error when adding the same hotkey twice") { 86 | let observerFoo: HotkeyObserver = HotkeyObserver(active: true) 87 | let observerBar: HotkeyObserver = HotkeyObserver(active: true) 88 | let hotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.one, modifier: [.commandKey, .shiftKey]) 89 | 90 | observerFoo.add(hotkey: hotkey, handler: {}) 91 | observerBar.add(hotkey: hotkey, handler: {}) 92 | expect(observerBar.definitions.first?.error).toNot(beNil()) 93 | } 94 | } 95 | 96 | private func readmeSample() { 97 | let observer: HotkeyObserver = HotkeyObserver(active: true) 98 | let fooHotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.one, modifier: [.commandKey, .shiftKey]) 99 | let barHotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey.two, modifier: [.commandKey, .shiftKey]) 100 | 101 | observer 102 | .add(hotkey: fooHotkey) { Swift.print("Such foo…") } 103 | .add(hotkey: barHotkey) { Swift.print("So bar…") } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Observatory.xcodeproj/xcshareddata/xcschemes/Observatory-Test.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /source/Observatory/Shortcut/Shortcut.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Shortcut provides a convenient hotkey observation model. Shortcut has two control points: a `hotkey` value, which is optional 4 | /// and can be `nil`, and `isEnabled` flag. For the shortcut to be globally registered it must be, both, enabled and have a valid 5 | /// hotkey. 6 | open class Shortcut { 7 | public init(_ hotkey: KeyboardHotkey? = nil, isEnabled: Bool? = nil) { 8 | self.hotkey = hotkey 9 | if let isEnabled = isEnabled { self.isEnabled = isEnabled } 10 | self.update() 11 | } 12 | 13 | /// The shortcut's hotkey. If hotkey is not provided or not valid the shortcut will not registered in the default 14 | /// `ShortcutCenter`. 15 | open var hotkey: KeyboardHotkey? { 16 | willSet { 17 | if newValue == self.hotkey { return } 18 | NotificationCenter.default.post(name: Self.hotkeyWillChangeNotification, object: self, userInfo: [Self.hotkeyUserInfo: newValue as Any]) 19 | } 20 | didSet { 21 | if self.hotkey == oldValue { return } 22 | NotificationCenter.default.post(name: Self.hotkeyDidChangeNotification, object: self, userInfo: [Self.hotkeyUserInfo: self.hotkey as Any]) 23 | self.update() 24 | } 25 | } 26 | 27 | /// Specifies whether the shortcut is enabled or not. When not enabled shortcut will not be registered in the default 28 | /// `ShortcutCenter`. 29 | open var isEnabled: Bool = true { 30 | didSet { self.update() } 31 | } 32 | 33 | /// Checks whether the shortcut is registered in the default `ShortcutCenter` or not. 34 | open var isRegistered: Bool { 35 | ShortcutCenter.default.shortcuts.contains(self) 36 | } 37 | 38 | /// Checks whether the shortcut is valid. The default implementation checks if the modifier contains command, control or option keys, 39 | /// thus, disallowing "plain" combinations with shift and caps-lock modifiers. 40 | open var isValid: Bool { 41 | guard let modifier = self.hotkey?.modifier else { return false } 42 | return modifier.contains(.commandKey) || modifier.contains(.controlKey) || modifier.contains(.optionKey) 43 | } 44 | 45 | /// Updates registration in the default `ShortcutCenter`. 46 | private func update() { 47 | ShortcutCenter.default.update(self) 48 | } 49 | 50 | 51 | // MARK: Observing 52 | 53 | 54 | /// Registered shortcut observers. 55 | fileprivate var observations: [Observation] = [] 56 | 57 | /// Creates a new observations. You don't need to retain the value to keep it alive, but it's needed to remove 58 | /// the observation. 59 | @discardableResult open func observe(_ action: @escaping Action) -> Any { 60 | let observation = Observation(action) 61 | self.observations.append(observation) 62 | return observation 63 | } 64 | 65 | /// Removes the observation. 66 | open func unobserve(_ observation: Any) { 67 | guard let observation = observation as? Observation else { return } 68 | guard let index = self.observations.firstIndex(where: { $0 === observation }) else { return } 69 | self.observations.remove(at: index) 70 | } 71 | 72 | /// Invokes the shortcut's registered observations. 73 | internal func invoke() { 74 | self.observations.forEach({ $0.action(self) }) 75 | } 76 | } 77 | 78 | extension Shortcut: Hashable { 79 | public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } 80 | public static func == (lhs: Shortcut, rhs: Shortcut) -> Bool { lhs === rhs } 81 | } 82 | 83 | extension Shortcut: CustomStringConvertible { 84 | public var description: String { "<\(Self.self): 0x\(String(Int(bitPattern: Unmanaged.passUnretained(self).toOpaque()), radix: 16)), hotkey: \(self.hotkey?.description ?? "nil"), observations: \(self.observations.count)>" } 85 | } 86 | 87 | extension Shortcut { 88 | /// Shortcut observation action gets called when the shortcut is triggered. 89 | public typealias Action = (Shortcut) -> Void 90 | 91 | fileprivate final class Observation { 92 | fileprivate init(_ action: @escaping Action) { self.action = action } 93 | fileprivate let action: Action 94 | } 95 | } 96 | 97 | extension Shortcut { 98 | /// Posted prior updating the shortcut property. Includes `userInfo` with the `hotkey` key. 99 | public static let hotkeyWillChangeNotification = Notification.Name("\(Shortcut.self)HotkeyWillChangeNotification") 100 | 101 | /// Posted after updating the shortcut property. Includes `userInfo` with the `hotkey` key. 102 | public static let hotkeyDidChangeNotification = Notification.Name("\(Shortcut.self)HotkeyDidChangeNotification") 103 | 104 | /// Notification `userInfo` key containing the `KeyboardHotkey` value. 105 | public static let hotkeyUserInfo = "hotkey" 106 | } 107 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Hotkey/Observer.Hotkey.Handler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Carbon 3 | 4 | extension HotkeyObserver { 5 | public struct Handler { 6 | public typealias Signature = (KeyboardHotkey) -> Void 7 | 8 | open class Definition: ObserverHandlerDefinition { 9 | deinit { 10 | self.deactivate() 11 | } 12 | 13 | public init(hotkey: KeyboardHotkey, handler: @escaping Signature) { 14 | self.hotkey = hotkey 15 | self.handler = handler 16 | } 17 | 18 | /// Keeps global count track of unique ids used for hotkeys. 19 | private static var uniqueHotkeyIdentifier: UInt32 = 0 20 | 21 | private static func constructUniqueHotkeyIdentifier() -> UInt32 { 22 | defer { self.uniqueHotkeyIdentifier += 1 } 23 | return self.uniqueHotkeyIdentifier 24 | } 25 | 26 | public let hotkey: KeyboardHotkey 27 | public let handler: Signature 28 | 29 | open private(set) var hotkeyIdentifier: EventHotKeyID! 30 | open private(set) var hotkeyReference: EventHotKeyRef! 31 | open private(set) var eventHandler: EventHandlerRef! 32 | open private(set) var error: Swift.Error? 33 | 34 | open private(set) var isActive: Bool = false 35 | 36 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 37 | if newValue == self.isActive { return self } 38 | return self.update(active: newValue, ignored: self.isIgnored) 39 | } 40 | 41 | @discardableResult open func deactivate() -> Self { 42 | self.activate(false) 43 | } 44 | 45 | open private(set) var isIgnored: Bool = false 46 | 47 | @discardableResult open func ignore(_ newValue: Bool = true) -> Self { 48 | if newValue == self.isIgnored { return self } 49 | return self.update(active: self.isActive, ignored: newValue) 50 | } 51 | 52 | @discardableResult open func unignore() -> Self { 53 | self.ignore(false) 54 | } 55 | 56 | @discardableResult private func update(active: Bool, ignored: Bool) -> Self { 57 | let newActive: Bool = active && !ignored 58 | let oldActive: Bool = self.hotkeyIdentifier ?? nil != nil && self.hotkeyReference ?? nil != nil 59 | 60 | do { 61 | if newActive && !oldActive { 62 | try self.registerEventHotkey() 63 | } else if !newActive && oldActive { 64 | try self.unregisterEventHotkey() 65 | } 66 | 67 | self.error = nil 68 | self.isActive = active 69 | self.isIgnored = ignored 70 | } catch { 71 | self.error = error 72 | } 73 | 74 | return self 75 | } 76 | 77 | private func registerEventHotkey() throws { 78 | 79 | // Todo: should use proper signature, find examples… 80 | 81 | let identifier: EventHotKeyID = EventHotKeyID(signature: 0, id: Self.constructUniqueHotkeyIdentifier()) 82 | var reference: EventHotKeyRef? 83 | 84 | let status: OSStatus = RegisterEventHotKey(UInt32(self.hotkey.key.rawValue), UInt32(self.hotkey.modifier.rawValue), identifier, GetApplicationEventTarget(), OptionBits(0), &reference) 85 | 86 | if Int(status) == eventHotKeyExistsErr { 87 | throw Error.hotkeyAlreadyRegistered 88 | } else if status != Darwin.noErr { 89 | throw Error.cannotRegisterHotkey(status: status) 90 | } 91 | 92 | self.hotkeyIdentifier = identifier 93 | self.hotkeyReference = reference 94 | } 95 | 96 | private func unregisterEventHotkey() throws { 97 | let status: OSStatus = UnregisterEventHotKey(self.hotkeyReference) 98 | guard status == Darwin.noErr else { throw Error.cannotUnregisterHotkey(status: status) } 99 | 100 | self.hotkeyIdentifier = nil 101 | self.hotkeyReference = nil 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// Convenience initializers. 108 | extension HotkeyObserver.Handler.Definition { 109 | public convenience init(hotkey: KeyboardHotkey, handler: @escaping () -> Void) { 110 | self.init(hotkey: hotkey, handler: { _ in handler() }) 111 | } 112 | } 113 | 114 | extension HotkeyObserver.Handler.Definition { 115 | public enum Error: Swift.Error { 116 | case hotkeyAlreadyRegistered 117 | case cannotRegisterHotkey(status: OSStatus) 118 | case cannotUnregisterHotkey(status: OSStatus) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Observatory.xcodeproj/xcshareddata/xcschemes/Observatory.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 90 | 91 | 97 | 98 | 99 | 102 | 103 | 104 | 110 | 111 | 117 | 118 | 119 | 120 | 122 | 123 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /source/Observatory/Keyboard/Keyboard.Modifier.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Carbon 3 | 4 | /// The `KeyboardModifier` uses Carbon key constants as raw values and not `CGEventFlags` nor `NSEvent.ModifierFlags`, 5 | /// because the modifier's raw values are used directly for hotkeys. Carbon's corresponding raw values are not directly 6 | /// compatible with the ones / in AppKit or Core Graphics. Some keys are not available in macOS – the standard modifiers 7 | /// for hotkeys are considered to be Caps Lock, Command, Control, Option and Shift keys, hence using only them here. 8 | public struct KeyboardModifier: RawRepresentable, OptionSet { 9 | public init(rawValue: Int) { self.rawValue = rawValue } 10 | public init(_ rawValue: Int) { self.init(rawValue: rawValue) } 11 | public init(_ event: NSEvent) { self.init(event.modifierFlags) } 12 | 13 | public init(_ flags: NSEvent.ModifierFlags) { 14 | var rawValue: Int = 0 15 | 16 | // ✊ Leaving this as a reminder for future generations. Apparently, if you used to deal with CoreGraphics you'd know 17 | // what the fuck modifier flags are made of, otherwise, you are doomed. And made they are of CoreGraphics event 18 | // source flags state, or `CGEventSource.flagsState(.hidSystemState)` to be precise. So, an empty flag will have 19 | // raw value not of `0` but of `UInt(CGEventSource.flagsState(.hidSystemState).rawValue)`… For that reason, it's a real 20 | // pain in the ass to compare self-made modifier flags with ones coming from an `NSEvent`. 21 | 22 | // ✊ Also, there's a funny Caps Lock behavior – it's not included in a key down event when `.command` flag is also 23 | // present. This might be done on purpose, but probably not what you're expecting. However, the Caps Lock flag remains 24 | // available inside `NSEvent.modifierFlags`… 🤯 So, if you need Caps Lock info – initialize your modifier from that 25 | // and not the actual `NSEvent` instance… or do a union… or handle it else how. 26 | 27 | if flags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue != 0 { 28 | if flags.contains(.capsLock) { rawValue |= Carbon.alphaLock } 29 | if flags.contains(.command) { rawValue |= Carbon.cmdKey } 30 | if flags.contains(.control) { rawValue |= Carbon.controlKey } 31 | if flags.contains(.option) { rawValue |= Carbon.optionKey } 32 | if flags.contains(.shift) { rawValue |= Carbon.shiftKey } 33 | } 34 | 35 | self = KeyboardModifier(rawValue) 36 | } 37 | 38 | public let rawValue: Int 39 | 40 | public static let none: KeyboardModifier = .init(0) 41 | public static let capsLockKey: KeyboardModifier = .init(Carbon.alphaLock) 42 | public static let commandKey: KeyboardModifier = .init(Carbon.cmdKey) 43 | public static let controlKey: KeyboardModifier = .init(Carbon.controlKey) 44 | public static let optionKey: KeyboardModifier = .init(Carbon.optionKey) 45 | public static let shiftKey: KeyboardModifier = .init(Carbon.shiftKey) 46 | 47 | /// The name of the modifier. 48 | public var name: String? { 49 | let name = self.keys.compactMap({ $0.name }).joined(separator: "") 50 | return name == "" ? nil : name 51 | } 52 | 53 | /// Returns keys associated with the modifier. Note, different keys can result in the same modifier, if you need 54 | /// the precise keys and in precise order they were pressed, this needs to be tracked done with event tracking. 55 | public var keys: [KeyboardKey] { 56 | // Keep the order: https://developer.apple.com/design/human-interface-guidelines/inputs/keyboards/#custom-keyboard-shortcuts 57 | // > List modifier keys in the correct order. If you use more than one modifier key in a custom 58 | // > shortcut, always list them in this order: Control, Option, Shift, Command. 59 | var keys = [KeyboardKey]() 60 | if self.contains(.controlKey) { keys.append(KeyboardKey.control) } 61 | if self.contains(.optionKey) { keys.append(KeyboardKey.option) } 62 | if self.contains(.capsLockKey) { keys.append(KeyboardKey.capsLock) } 63 | if self.contains(.shiftKey) { keys.append(KeyboardKey.shift) } 64 | if self.contains(.commandKey) { keys.append(KeyboardKey.command) } 65 | return keys 66 | } 67 | } 68 | 69 | extension KeyboardModifier: Equatable, Hashable { 70 | public func hash(into hasher: inout Hasher) { hasher.combine(self.rawValue) } 71 | } 72 | 73 | extension KeyboardModifier: CustomStringConvertible { 74 | public var description: String { self.name ?? "" } 75 | } 76 | 77 | extension CGEventFlags { 78 | public init(modifier: KeyboardModifier) { 79 | self.init() 80 | if modifier.contains(.controlKey) { self.insert(.maskControl) } 81 | if modifier.contains(.optionKey) { self.insert(.maskAlternate) } 82 | if modifier.contains(.capsLockKey) { self.insert(.maskAlphaShift) } 83 | if modifier.contains(.shiftKey) { self.insert(.maskShift) } 84 | if modifier.contains(.commandKey) { self.insert(.maskCommand) } 85 | } 86 | } 87 | 88 | extension NSEvent.ModifierFlags { 89 | public init(modifier: KeyboardModifier) { 90 | self.init() 91 | if modifier.contains(.controlKey) { self.insert(.control) } 92 | if modifier.contains(.optionKey) { self.insert(.option) } 93 | if modifier.contains(.capsLockKey) { self.insert(.capsLock) } 94 | if modifier.contains(.shiftKey) { self.insert(.shift) } 95 | if modifier.contains(.commandKey) { self.insert(.command) } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /source/Observatory/Shortcut/Shortcut.Center.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The shortcut center provides an interface for registering predefined hotkey commands. It sub-manages active hotkey 4 | /// recorder and all hotkey observers. There's an important detail to keep in mind, that all observers get disabled 5 | /// whilst `recorder` value is not `nil`, see related property for details. 6 | open class ShortcutCenter { 7 | public static var `default`: ShortcutCenter = ShortcutCenter() 8 | 9 | /// The hotkey observer. 10 | private let observer: HotkeyObserver = HotkeyObserver(active: true) 11 | 12 | /// Current hotkey recorder, normally is set and unset by the assigned value itself. Whilst it's set all registered 13 | /// observers get disabled, to avoid triggering commands during recording. 14 | open var recorder: ShortcutRecorder? { 15 | didSet { 16 | if self.recorder === oldValue { return } 17 | /// Disables registered hotkey observers if there's an active hotkey recorder and enables them if there's not. 18 | let isIgnored: Bool = self.recorder != nil 19 | self.observer.isActive = !isIgnored 20 | } 21 | } 22 | 23 | /// Independently stores registered shortcut hotkeys. 24 | private var registrations: [Registration] = [] 25 | 26 | /// All shortcuts registered in the center. 27 | open var shortcuts: [Shortcut] { self.registrations.lazy.map({ $0.shortcut }) } 28 | 29 | /// Registers the shortcut-hotkey pair. 30 | private func add(_ shortcut: Shortcut, _ hotkey: KeyboardHotkey) { 31 | if self.registrations.contains(where: { $0.shortcut == shortcut || $0.definition.hotkey == hotkey }) { 32 | return self.notify(Self.cannotRegisterShortcutNotification, shortcut, hotkey) 33 | } 34 | 35 | if self.observer.error != nil { 36 | return self.notify(Self.cannotRegisterShortcutNotification, shortcut, hotkey) 37 | } 38 | 39 | let definition = HotkeyObserver.Handler.Definition(hotkey: hotkey, handler: { [weak self] in self?.invoke($0) }) 40 | self.observer.add(definition: definition) 41 | 42 | if definition.error != nil { 43 | self.observer.remove(definition: definition) 44 | return self.notify(Self.cannotRegisterShortcutNotification, shortcut, hotkey) 45 | } 46 | 47 | self.registrations.append(Registration(shortcut, definition)) 48 | self.notify(Self.didRegisterShortcutNotification, shortcut, hotkey) 49 | } 50 | 51 | /// Unregisters the shortcut. 52 | private func remove(_ shortcut: Shortcut) { 53 | guard let index = self.registrations.firstIndex(where: { $0.shortcut == shortcut }) else { 54 | return 55 | } 56 | 57 | let registration = self.registrations.remove(at: index) 58 | self.observer.remove(definition: registration.definition) 59 | self.notify(Self.didUnregisterShortcutNotification, shortcut, registration.definition.hotkey) 60 | } 61 | 62 | /// Updates the shortcut registration. 63 | internal func update(_ shortcut: Shortcut) { 64 | let oldHotkey = self.registrations.first(where: { $0.shortcut == shortcut })?.definition.hotkey 65 | let newHotkey = shortcut.hotkey 66 | 67 | // Need to register if the new hotkey is okay. 68 | let needsRegister = newHotkey != nil && shortcut.isValid && shortcut.isEnabled 69 | // Need to unregister if hotkey is already registered but doesn't need to be or if hotkeys are different. 70 | let needsUnregister = oldHotkey != nil && !needsRegister || newHotkey != oldHotkey 71 | 72 | if needsUnregister { self.remove(shortcut) } 73 | if needsRegister, let hotkey = newHotkey { self.add(shortcut, hotkey) } 74 | } 75 | 76 | /// Invokes a registered shortcut with the hotkey. 77 | private func invoke(_ hotkey: KeyboardHotkey) { 78 | guard let shortcut = self.shortcuts.first(where: { $0.hotkey == hotkey }) else { return } 79 | self.notify(Self.willInvokeShortcutNotification, shortcut, hotkey) 80 | shortcut.invoke() 81 | self.notify(Self.didInvokeShortcutNotification, shortcut, hotkey) 82 | } 83 | } 84 | 85 | extension ShortcutCenter { 86 | fileprivate struct Registration { 87 | fileprivate init(_ shortcut: Shortcut, _ definition: HotkeyObserver.Handler.Definition) { 88 | self.shortcut = shortcut 89 | self.definition = definition 90 | } 91 | fileprivate let shortcut: Shortcut 92 | fileprivate let definition: HotkeyObserver.Handler.Definition 93 | } 94 | } 95 | 96 | extension ShortcutCenter { 97 | /// Convenience notification posting. 98 | fileprivate func notify(_ name: Notification.Name, _ shortcut: Shortcut, _ hotkey: KeyboardHotkey) { 99 | NotificationCenter.default.post(name: name, object: self, userInfo: [Self.shortcutUserInfo: shortcut, Self.hotkeyUserInfo: hotkey]) 100 | } 101 | } 102 | 103 | extension ShortcutCenter { 104 | /// Posted after failing to register a shortcut. Includes `userInfo` with `shortcut` and `hotkey` keys. 105 | public static let cannotRegisterShortcutNotification = Notification.Name("\(ShortcutCenter.self)CannotNotRegisterShortcutNotification") 106 | 107 | /// Posted after successfully registering a shortcut. Includes `userInfo` with `shortcut` and `hotkey` keys. 108 | public static let didRegisterShortcutNotification = Notification.Name("\(ShortcutCenter.self)DidRegisterShortcutNotification") 109 | 110 | /// Posted after successfully unregistering a shortcut. Includes `userInfo` with `shortcut` and `hotkey` keys. 111 | public static let didUnregisterShortcutNotification = Notification.Name("\(ShortcutCenter.self)DidUnregisterShortcutNotification") 112 | 113 | /// Posted prior invoking a registered shortcut. Includes `userInfo` with `shortcut` and `hotkey` keys. 114 | public static let willInvokeShortcutNotification = Notification.Name("\(ShortcutCenter.self)WillInvokeShortcutNotification") 115 | 116 | /// Posted after invoking a registered shortcut. Includes `userInfo` with `shortcut` and `hotkey` keys. 117 | public static let didInvokeShortcutNotification = Notification.Name("\(ShortcutCenter.self)DidInvokeShortcutNotification") 118 | 119 | /// Notification `userInfo` key containing the `Shortcut` object. 120 | public static let shortcutUserInfo = "shortcut" 121 | 122 | /// Notification `userInfo` key containing the `KeyboardHotkey` object. This is used alongside `shortcut` key to provide 123 | /// the `KeyboardHotkey` value when it might be already changed in the `Shortcut` object. 124 | public static let hotkeyUserInfo = "hotkey" 125 | } 126 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Hotkey/Observer.Hotkey.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Carbon 3 | 4 | /// Useful resources on how to implement shortcuts and work with carbon events. 5 | /// 6 | /// https://github.com/nathan/hush/blob/master/Hush/HotKey.swift 7 | /// http://dbachrach.com/blog/2005/11/program-global-hotkeys-in-cocoa-easily/ 8 | /// http://stackoverflow.com/a/401244/458356 – How to Capture / Post system-wide Keyboard / Mouse events under Mac OS X? 9 | /// http://stackoverflow.com/a/4640190/458356 – OSX keyboard shortcut background application 10 | /// http://stackoverflow.com/a/34864422/458356 11 | private func hotkey(for event: EventRef) -> EventHotKeyID { 12 | let pointer: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) 13 | GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout.size, nil, pointer) 14 | return pointer.pointee 15 | } 16 | 17 | open class HotkeyObserver: AbstractObserver { 18 | private typealias EventHandler = EventHandlerUPP 19 | private typealias EventHandlerPointer = EventHandlerRef 20 | private typealias EventHotkeyHandler = (EventHotKeyID) -> Void 21 | private typealias EventHotkeyHandlerPointer = UnsafeMutablePointer 22 | 23 | public convenience init(active: Bool) { 24 | self.init() 25 | self.activate(active) 26 | } 27 | 28 | private var eventHandlerPointer: EventHandlerPointer? 29 | private var eventHotkeyHandlerPointer: EventHotkeyHandlerPointer? 30 | 31 | open internal(set) var definitions: [Handler.Definition] = [] 32 | 33 | @discardableResult open func add(definition: Handler.Definition) -> Self { 34 | self.definitions.append(definition.activate(self.isActive)) 35 | return self 36 | } 37 | 38 | @discardableResult open func add(definitions: [Handler.Definition]) -> Self { 39 | for definition in definitions { self.add(definition: definition) } 40 | return self 41 | } 42 | 43 | @discardableResult open func remove(definition: Handler.Definition) -> Self { 44 | self.definitions.enumerated().first(where: { $0.1 === definition }).map({ self.definitions.remove(at: $0.0) })?.deactivate() 45 | return self 46 | } 47 | 48 | @discardableResult open func remove(definitions: [Handler.Definition]) -> Self { 49 | for definition in definitions { self.remove(definition: definition) } 50 | return self 51 | } 52 | 53 | override open var isActive: Bool { 54 | get { super.isActive } 55 | set { self.activate(newValue) } 56 | } 57 | 58 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 59 | if newValue == self.isActive { return self } 60 | return self.update(active: newValue) 61 | } 62 | 63 | @discardableResult open func deactivate() -> Self { 64 | self.activate(false) 65 | } 66 | 67 | open private(set) var error: Swift.Error? 68 | 69 | @discardableResult private func update(active: Bool) -> Self { 70 | 71 | // Before we can register any hot keys we must register an event handler with carbon framework. Deactivation goes 72 | // in reverse, first deactivate definitions then event handler. 73 | 74 | do { 75 | if active { 76 | let (eventHandler, eventHotkeyHandler) = try self.constructEventHandler() 77 | self.eventHandlerPointer = eventHandler 78 | self.eventHotkeyHandlerPointer = eventHotkeyHandler 79 | for definition in self.definitions { definition.activate() } 80 | } else { 81 | for definition in self.definitions { definition.deactivate() } 82 | try self.destructEventHandler(self.eventHandlerPointer!, eventHotkeyHandler: self.eventHotkeyHandlerPointer!) 83 | self.eventHandlerPointer = nil 84 | self.eventHotkeyHandlerPointer = nil 85 | } 86 | 87 | self.error = nil 88 | super.isActive = active 89 | } catch { 90 | self.error = error 91 | } 92 | 93 | return self 94 | } 95 | 96 | private func constructEventHandler() throws -> (EventHandlerPointer, EventHotkeyHandlerPointer) { 97 | var eventType: EventTypeSpec = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)) 98 | 99 | let eventHandler: EventHandler 100 | var eventHandlerPointer: EventHandlerPointer? 101 | let eventHotkeyHandlerPointer: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) 102 | 103 | eventHandler = { (nextHandler: EventHandlerCallRef?, event: EventRef?, pointer: UnsafeMutableRawPointer?) -> OSStatus in 104 | UnsafeMutablePointer(OpaquePointer(pointer!)).pointee(hotkey(for: event!)) 105 | return CallNextEventHandler(nextHandler, event) 106 | } 107 | 108 | eventHotkeyHandlerPointer.initialize(to: { [weak self] (identifier: EventHotKeyID) in 109 | self?.definitions.filter({ $0.hotkeyIdentifier == identifier }).forEach({ $0.handler($0.hotkey) }) 110 | }) 111 | 112 | // Create universal procedure pointer, so it can be passed to C. 113 | 114 | let status: OSStatus = InstallEventHandler(GetApplicationEventTarget(), eventHandler, 1, &eventType, eventHotkeyHandlerPointer, &eventHandlerPointer) 115 | guard status == Darwin.noErr else { throw Error.cannotInstallUPP } 116 | 117 | return (eventHandlerPointer!, eventHotkeyHandlerPointer) 118 | } 119 | 120 | private func destructEventHandler(_ eventHandler: EventHandlerPointer, eventHotkeyHandler: EventHotkeyHandlerPointer) throws { 121 | let status: OSStatus = RemoveEventHandler(eventHandler) 122 | guard status == Darwin.noErr else { throw Error.cannotUninstallUPP } 123 | 124 | eventHotkeyHandler.deinitialize(count: 1) 125 | eventHotkeyHandler.deallocate() 126 | } 127 | 128 | 129 | // MARK: - 130 | 131 | 132 | @discardableResult open func add(hotkey: KeyboardHotkey, handler: @escaping () -> Void) -> Self { 133 | self.add(definition: Handler.Definition(hotkey: hotkey, handler: handler)) 134 | } 135 | 136 | @discardableResult open func add(hotkey: KeyboardHotkey, handler: @escaping (KeyboardHotkey) -> Void) -> Self { 137 | self.add(definition: Handler.Definition(hotkey: hotkey, handler: handler)) 138 | } 139 | 140 | @discardableResult open func remove(hotkey: KeyboardHotkey) -> Self { 141 | self.remove(definitions: self.definitions.filter({ $0.hotkey == hotkey })) 142 | } 143 | } 144 | 145 | extension HotkeyObserver { 146 | public enum Error: Swift.Error { 147 | /// Cannot install universal procedure pointer. 148 | case cannotInstallUPP 149 | /// Cannot remove universal procedure pointer. 150 | case cannotUninstallUPP 151 | } 152 | } 153 | 154 | /// Here we make EventHotKeyID instances comparable using `==` operator. 155 | extension EventHotKeyID: Equatable { 156 | public static func == (lhs: Self, rhs: Self) -> Bool { lhs.signature == rhs.signature && lhs.id == rhs.id } 157 | } 158 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Event/Observer.Event.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Foundation 3 | import CoreGraphics 4 | 5 | /// Event observer provides a flexible interface for registering and managing multiple event handlers in, both, global 6 | /// and local contexts. 7 | open class EventObserver: AbstractObserver { 8 | deinit { 9 | self.deactivate() 10 | } 11 | 12 | public convenience init(active: Bool) { 13 | self.init() 14 | self.activate(active) 15 | } 16 | 17 | open internal(set) var appKitDefinitions: [Handler.AppKit.Definition] = [] 18 | open internal(set) var carbonDefinitions: [Handler.Carbon.Definition] = [] 19 | 20 | @discardableResult open func add(definition: Handler.AppKit.Definition) -> Self { 21 | self.appKitDefinitions.append(definition.activate(self.isActive)) 22 | return self 23 | } 24 | 25 | @discardableResult open func add(definitions: [Handler.AppKit.Definition]) -> Self { 26 | for definition in definitions { self.add(definition: definition) } 27 | return self 28 | } 29 | 30 | @discardableResult open func add(definition: Handler.Carbon.Definition) -> Self { 31 | self.carbonDefinitions.append(definition.activate(self.isActive)) 32 | return self 33 | } 34 | 35 | @discardableResult open func add(definitions: [Handler.Carbon.Definition]) -> Self { 36 | for definition in definitions { self.add(definition: definition) } 37 | return self 38 | } 39 | 40 | @discardableResult open func remove(definition: Handler.AppKit.Definition) -> Self { 41 | self.appKitDefinitions.enumerated().first(where: { $0.1 === definition }).map({ self.appKitDefinitions.remove(at: $0.0) })?.deactivate() 42 | return self 43 | } 44 | 45 | @discardableResult open func remove(definitions: [Handler.AppKit.Definition]) -> Self { 46 | for definition in definitions { self.remove(definition: definition) } 47 | return self 48 | } 49 | 50 | @discardableResult open func remove(definition: Handler.Carbon.Definition) -> Self { 51 | self.carbonDefinitions.enumerated().first(where: { $0.1 === definition }).map({ self.appKitDefinitions.remove(at: $0.0) })?.deactivate() 52 | return self 53 | } 54 | 55 | @discardableResult open func remove(definitions: [Handler.Carbon.Definition]) -> Self { 56 | for definition in definitions { self.remove(definition: definition) } 57 | return self 58 | } 59 | 60 | override open var isActive: Bool { 61 | get { super.isActive } 62 | set { self.activate(newValue) } 63 | } 64 | 65 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 66 | 67 | // Todo: we should use common store for all definitions where they would be kept in the order 68 | // todo: of adding, so we can maintain that order during activation / deactivation. 69 | 70 | if newValue == self.isActive { return self } 71 | for definition in self.carbonDefinitions { definition.activate(newValue) } 72 | for definition in self.appKitDefinitions { definition.activate(newValue) } 73 | super.isActive = newValue 74 | return self 75 | } 76 | 77 | @discardableResult open func deactivate() -> Self { 78 | self.activate(false) 79 | } 80 | 81 | 82 | // MARK: NSEvent with local + global handler 83 | 84 | 85 | /// Register AppKit local + global handler. 86 | @discardableResult open func add(mask: NSEvent.EventTypeMask, local: ((NSEvent) -> NSEvent?)?, global: ((NSEvent) -> Void)?) -> Self { 87 | self.add(definition: .init(mask: mask, local: local, global: global)) 88 | } 89 | 90 | /// Register AppKit local + global handler with manual local event forwarding. 91 | @discardableResult open func add(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> NSEvent?) -> Self { 92 | self.add(definition: .init(mask: mask, handler: handler)) 93 | } 94 | 95 | /// Register AppKit local + global handler with custom local event forwarding. 96 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 97 | @discardableResult open func add(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> Void, forward: Bool = true) -> Self { 98 | self.add(definition: .init(mask: mask, handler: handler, forward: forward)) 99 | } 100 | 101 | /// Register AppKit local + global handler with custom local event forwarding. 102 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 103 | @discardableResult open func add(mask: NSEvent.EventTypeMask, handler: @escaping () -> Void, forward: Bool = true) -> Self { 104 | self.add(definition: .init(mask: mask, handler: handler, forward: forward)) 105 | } 106 | 107 | /// Remove all handlers with specified mask. 108 | @discardableResult open func remove(mask: NSEvent.EventTypeMask) -> Self { 109 | self.remove(definitions: self.appKitDefinitions.filter({ $0.mask == mask })) 110 | } 111 | 112 | 113 | // MARK: NSEvent with local handler 114 | 115 | 116 | /// Register AppKit local handler with manual event forwarding. 117 | @discardableResult open func add(mask: NSEvent.EventTypeMask, local: @escaping (NSEvent) -> NSEvent?) -> Self { 118 | self.add(definition: .init(mask: mask, local: local)) 119 | } 120 | 121 | /// Register AppKit local handler with custom local event forwarding. 122 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 123 | @discardableResult open func add(mask: NSEvent.EventTypeMask, local: @escaping (NSEvent) -> Void, forward: Bool = true) -> Self { 124 | self.add(definition: .init(mask: mask, local: local, forward: forward)) 125 | } 126 | 127 | /// Register AppKit local handler with custom local event forwarding. 128 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 129 | @discardableResult open func add(mask: NSEvent.EventTypeMask, local: @escaping () -> Void, forward: Bool = true) -> Self { 130 | self.add(definition: .init(mask: mask, local: local, forward: forward)) 131 | } 132 | 133 | 134 | // MARK: NSEvent with global handler 135 | 136 | 137 | /// Register AppKit global handler. 138 | @discardableResult open func add(mask: NSEvent.EventTypeMask, global: @escaping (NSEvent) -> Void) -> Self { 139 | self.add(definition: .init(mask: mask, global: global)) 140 | } 141 | 142 | /// Register AppKit global handler. 143 | @discardableResult open func add(mask: NSEvent.EventTypeMask, global: @escaping () -> Void) -> Self { 144 | self.add(definition: .init(mask: mask, global: global)) 145 | } 146 | 147 | 148 | // MARK: CGEvent with CGEventMask 149 | 150 | /// Note, for this to work the process must have Accessibility Control permission enabled 151 | /// in Security & Privacy system preferences. 152 | fileprivate func add(definition: Handler.Carbon.Definition?) -> Self { 153 | if let definition: Handler.Carbon.Definition = definition { return self.add(definition: definition) } else { return self } 154 | } 155 | 156 | /// Register CoreGraphics handler with manual event forwarding. Note, for this to work the process must have 157 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 158 | @discardableResult open func add(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> CGEvent?) -> Self { 159 | self.add(definition: Handler.Carbon.Definition(mask: mask, location: location, placement: placement, options: options, handler: handler)) 160 | } 161 | 162 | /// Register CoreGraphics handler with automatic event forwarding. Note, for this to work the process must have 163 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 164 | @discardableResult open func add(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> Void) -> Self { 165 | self.add(definition: Handler.Carbon.Definition(mask: mask, location: location, placement: placement, options: options, handler: handler)) 166 | } 167 | 168 | /// Register CoreGraphics handler with automatic event forwarding. Note, for this to work the process must have 169 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 170 | @discardableResult open func add(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping () -> Void) -> Self { 171 | self.add(definition: Handler.Carbon.Definition(mask: mask, location: location, placement: placement, options: options, handler: handler)) 172 | } 173 | 174 | @discardableResult open func remove(mask: CGEventMask) -> Self { 175 | self.remove(definitions: self.carbonDefinitions.filter({ $0.mask == mask })) 176 | } 177 | 178 | 179 | // MARK: CGEvent with NSEvent.EventTypeMask 180 | 181 | 182 | /// Register CoreGraphics handler with manual event forwarding. Note, for this to work the process must have 183 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 184 | @discardableResult open func add(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> CGEvent?) -> Self { 185 | self.add(definition: Handler.Carbon.Definition(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler)) 186 | } 187 | 188 | /// Register CoreGraphics handler with automatic event forwarding. Note, for this to work the process must have 189 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 190 | @discardableResult open func add(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> Void) -> Self { 191 | self.add(definition: Handler.Carbon.Definition(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler)) 192 | } 193 | 194 | /// Register CoreGraphics handler with automatic event forwarding. Note, for this to work the process must have 195 | /// Accessibility Control permission enabled in Security & Privacy system preferences. 196 | @discardableResult open func add(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping () -> Void) -> Self { 197 | self.add(definition: Handler.Carbon.Definition(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler)) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /source/Observatory/Shortcut/Shortcut.Recorder.Button.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | import Carbon 4 | 5 | /// NSButton-based control for recording shortcut hotkeys. Unlike regular button, it will send actions 6 | /// when the associated hotkey gets modified as the result of the user input. 7 | open class ShortcutRecorderButton: NSButton, ShortcutRecorder { 8 | override public init(frame frameRect: NSRect) { super.init(frame: frameRect); self._init() } 9 | public required init?(coder: NSCoder) { super.init(coder: coder); self._init() } 10 | 11 | private func _init() { 12 | self.update() 13 | } 14 | 15 | /// Shortcut and window notification observer. 16 | private let observer: NotificationObserver = NotificationObserver(active: true) 17 | 18 | open var shortcut: Shortcut? { 19 | willSet { 20 | if newValue == self.shortcut { return } 21 | NotificationCenter.default.post(name: Self.shortcutWillChangeNotification, object: self) 22 | } 23 | didSet { 24 | if self.shortcut == oldValue { return } 25 | // Update shortcut hotkey change observation. 26 | if let shortcut = oldValue { self.observer.remove(observee: shortcut) } 27 | if let shortcut = self.shortcut { self.observer.add(name: Shortcut.hotkeyDidChangeNotification, observee: shortcut, handler: { [weak self] in self?.update() }) } 28 | // Should cancel recording if the hotkey gets set during active recording. 29 | if self.isRecording != false { self.isRecording = false } else { self.update() } 30 | NotificationCenter.default.post(name: Self.shortcutDidChangeNotification, object: self) 31 | } 32 | } 33 | 34 | /// Stores temporary modifier while hotkey is being recorded. 35 | private var modifier: KeyboardModifier? { 36 | didSet { 37 | if self.modifier == oldValue { return } 38 | self.update() 39 | } 40 | } 41 | 42 | open var isRecording: Bool = false { 43 | didSet { 44 | if self.isRecording == oldValue { return } 45 | if self.isRecording { self.makeFirstResponder() } else { self.restoreFirstResponder() } 46 | if self.modifier != nil { self.modifier = nil } else { self.update() } 47 | // Let the shortcut center know that current recorder has changed. 48 | if self.isRecording { 49 | ShortcutCenter.default.recorder = self 50 | } else if ShortcutCenter.default.recorder === self { 51 | ShortcutCenter.default.recorder = nil 52 | } 53 | } 54 | } 55 | 56 | /// Temporarily stores the reference to original first responder during hotkey recording. 57 | private weak var originalFirstResponder: NSResponder? 58 | 59 | // Makes self as the the first responder and stores the original first responder reference for later restoration. 60 | private func makeFirstResponder() { 61 | // Check if another instance of `Self` is already the first responder and use it's value instead. 62 | var currentFirstResponder: NSResponder? = self.window?.firstResponder 63 | if let recorder = currentFirstResponder as? Self, recorder.isRecording { currentFirstResponder = recorder.originalFirstResponder } 64 | if self.originalFirstResponder == nil { self.originalFirstResponder = currentFirstResponder } 65 | if currentFirstResponder !== self { self.window?.makeFirstResponder(self) } 66 | } 67 | 68 | // Restores the original first responder if self is the current first responder and clears the original first responder reference. 69 | private func restoreFirstResponder() { 70 | let currentFirstResponder: NSResponder? = self.window?.firstResponder 71 | if currentFirstResponder === self && currentFirstResponder !== self.originalFirstResponder { self.window?.makeFirstResponder(self.originalFirstResponder) } 72 | self.originalFirstResponder = nil 73 | } 74 | 75 | /// Updates the button's title and appearance. 76 | private func update() { 77 | // ✊ Even when the title is empty, we still need a valid paragraph style. 78 | let style: NSMutableParagraphStyle = self.attributedTitle.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as! NSMutableParagraphStyle? ?? NSMutableParagraphStyle(alignment: self.alignment) 79 | let modifier = self.modifier == nil || self.modifier == [] ? nil : self.modifier 80 | let hotkey = self.shortcut?.hotkey 81 | let color: NSColor 82 | let title: String 83 | 84 | if self.shortcut == nil { 85 | title = "INVALID" 86 | color = NSColor.red 87 | } else if self.isRecording { 88 | title = modifier.map({ self.title(forModifier: $0) }) ?? hotkey.map({ self.title(forHotkey: $0) }) ?? "Record shortcut" 89 | color = self.modifier == nil ? NSColor.tertiaryLabelColor : NSColor.secondaryLabelColor 90 | } else { 91 | title = hotkey.map({ self.title(forHotkey: $0) }) ?? "Click to record shortcut" 92 | color = NSColor.labelColor 93 | } 94 | 95 | if title == "" { NSLog("\(self) attempted to set empty title, this shouldn't be happening…") } 96 | self.attributedTitle = NSAttributedString(string: title, attributes: [.foregroundColor: color, .paragraphStyle: style, .font: self.font ?? NSFont.systemFont(ofSize: NSFont.systemFontSize)]) 97 | } 98 | 99 | /// Returns the receiver's title for the keyboard modifier. 100 | open func title(forModifier modifier: KeyboardModifier) -> String { 101 | String(describing: modifier) 102 | } 103 | 104 | /// Returns the receiver's title for the keyboard key. 105 | open func title(forKey key: KeyboardKey) -> String { 106 | String(describing: key) 107 | } 108 | 109 | /// Returns the receiver's title for the keyboard hotkey. 110 | open func title(forHotkey hotkey: KeyboardHotkey) -> String { 111 | "\(self.title(forModifier: hotkey.modifier))\(self.title(forKey: hotkey.key))" 112 | } 113 | 114 | override open func resignFirstResponder() -> Bool { 115 | self.isRecording = false 116 | return super.resignFirstResponder() 117 | } 118 | 119 | override open var acceptsFirstResponder: Bool { 120 | self.isEnabled 121 | } 122 | 123 | override open func mouseDown(with event: NSEvent) { 124 | // Don't invoke super to avoid action sending. 125 | // super.mouseDown(with: event) 126 | self.isRecording = true 127 | } 128 | 129 | /// Handles hotkey recording and returns true when any custom logic was invoked. 130 | override open func performKeyEquivalent(with event: NSEvent) -> Bool { 131 | if !self.isEnabled { return false } 132 | 133 | // Pressing delete key without any modifiers clears current shortcut. 134 | if (self.isRecording || self.window?.firstResponder === self) && self.modifier == nil && self.shortcut != nil && KeyboardKey(event) == KeyboardKey.delete { 135 | self.shortcut?.hotkey = nil 136 | self.isRecording = false 137 | NotificationCenter.default.post(name: Self.hotkeyDidRecordNotification, object: self) 138 | let _ = self.sendAction(self.action, to: self.target) 139 | return true 140 | } 141 | 142 | // Pressing escape without modifiers during recording cancels it, pressing space while not recording starts it. 143 | if self.isRecording && (self.modifier == nil && KeyboardKey(event) == KeyboardKey.escape || self.isKeyEquivalent(event)) { 144 | self.isRecording = false 145 | return true 146 | } else if !self.isRecording && (self.window?.firstResponder === self && self.modifier == nil && KeyboardKey(event) == KeyboardKey.space || self.isKeyEquivalent(event)) { 147 | self.isRecording = true 148 | return true 149 | } 150 | 151 | // If not recording, there's nothing else to do… 152 | if !self.isRecording { 153 | return super.performKeyEquivalent(with: event) 154 | } 155 | 156 | // Pressing any key without modifiers is not a valid shortcut. 157 | if let modifier: KeyboardModifier = self.modifier { 158 | let hotkey: KeyboardHotkey = KeyboardHotkey(key: KeyboardKey(event), modifier: modifier) 159 | let shortcut: Shortcut? = self.shortcut 160 | if ShortcutCenter.default.shortcuts.contains(where: { $0 !== shortcut && $0.hotkey == hotkey }) { 161 | NSSound.beep() 162 | } else { 163 | self.shortcut?.hotkey = hotkey 164 | self.isRecording = false 165 | NotificationCenter.default.post(name: Self.hotkeyDidRecordNotification, object: self) 166 | let _ = self.sendAction(self.action, to: self.target) 167 | } 168 | } else { 169 | NSSound.beep() 170 | } 171 | 172 | return true 173 | } 174 | 175 | /// You must invoke super when overriding this method. 176 | override open func flagsChanged(with event: NSEvent) { 177 | if self.isRecording { 178 | let modifier: KeyboardModifier = KeyboardModifier(event).intersection([.commandKey, .controlKey, .optionKey, .shiftKey]) 179 | self.modifier = modifier == [] ? nil : modifier 180 | } 181 | super.flagsChanged(with: event) 182 | } 183 | 184 | /// You must invoke super when overriding this method. 185 | override open func viewWillMove(toWindow newWindow: NSWindow?) { 186 | if let oldWindow: NSWindow = self.window { 187 | self.observer.remove(observee: oldWindow) 188 | } 189 | if let newWindow: NSWindow = newWindow { 190 | self.observer.add(name: NSWindow.didResignKeyNotification, observee: newWindow, handler: { [weak self] in self?.isRecording = false }) 191 | } 192 | } 193 | } 194 | 195 | extension NSMutableParagraphStyle { 196 | fileprivate convenience init(alignment: NSTextAlignment) { 197 | self.init() 198 | self.alignment = alignment 199 | } 200 | } 201 | 202 | extension NSButton { 203 | /// Checks if the event matches the button's key equivalent configuration. 204 | public func isKeyEquivalent(_ event: NSEvent) -> Bool { 205 | // 1. NSButton stores key equivalent string either in upper or lower case depending on whether Shift key 206 | // was pressed or not. At the same time the modifier is doesn't include Shift. 207 | // 2. NSEvent characters are returned in lower case when Shift key is pressed WITH other modifiers. Using 208 | // characters ignoring modifiers returns the string in correct case. 209 | 210 | // Todo: This still might need better testing with other modifiers, like CapsLock. 211 | event.charactersIgnoringModifiers == self.keyEquivalent && KeyboardModifier(event.modifierFlags) == KeyboardModifier(self.keyEquivalent, self.keyEquivalentModifierMask) 212 | } 213 | } 214 | 215 | extension KeyboardModifier { 216 | /// Creates new keyboard modifier from the key equivalent string and modifier flags. If the key equivalent is an uppercase string 217 | /// the shift modifier flag will be included into the modifier flags. 218 | fileprivate init(_ keyEquivalent: String, _ flags: NSEvent.ModifierFlags) { 219 | let isUppercase = keyEquivalent.allSatisfy({ $0.isUppercase }) 220 | self.init(isUppercase ? flags.union(.shift) : flags) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /source/Observatory/Observer/Event/Observer.Event.Handler.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Foundation 3 | 4 | extension EventObserver { 5 | public struct Handler { 6 | } 7 | } 8 | 9 | extension EventObserver.Handler { 10 | public struct AppKit { 11 | /// Global signature without event forwarding. 12 | public typealias GlobalSignature = (NSEvent) -> Void 13 | 14 | /// Local signature with event forwarding. 15 | public typealias LocalSignature = (NSEvent) -> NSEvent? 16 | 17 | open class Definition: ObserverHandlerDefinition { 18 | public typealias Handler = (global: GlobalSignature?, local: LocalSignature?) 19 | public typealias Monitor = (global: Any?, local: Any?) 20 | 21 | init(mask: NSEvent.EventTypeMask, handler: Handler) { 22 | self.mask = mask 23 | self.handler = handler 24 | } 25 | 26 | deinit { 27 | self.deactivate() 28 | } 29 | 30 | public let mask: NSEvent.EventTypeMask 31 | public let handler: Handler 32 | 33 | open private(set) var monitor: Monitor? 34 | 35 | open private(set) var isActive: Bool = false 36 | 37 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 38 | if newValue == self.isActive { return self } 39 | 40 | if newValue { 41 | self.monitor = Monitor( 42 | global: self.handler.global.map({ NSEvent.addGlobalMonitorForEvents(matching: self.mask, handler: $0) as Any }), 43 | local: self.handler.local.map({ NSEvent.addLocalMonitorForEvents(matching: self.mask, handler: $0) as Any }) 44 | ) 45 | } else if let monitor = self.monitor { 46 | monitor.local.map({ NSEvent.removeMonitor($0) }) 47 | monitor.global.map({ NSEvent.removeMonitor($0) }) 48 | self.monitor = nil 49 | } 50 | 51 | self.isActive = newValue 52 | return self 53 | } 54 | 55 | @discardableResult open func deactivate() -> Self { 56 | self.activate(false) 57 | } 58 | } 59 | } 60 | } 61 | 62 | /// Convenience initializers. 63 | extension EventObserver.Handler.AppKit.Definition { 64 | 65 | /// Initialize with local + global handler. 66 | public convenience init(mask: NSEvent.EventTypeMask, local: ((NSEvent) -> NSEvent?)?, global: ((NSEvent) -> Void)?) { 67 | self.init(mask: mask, handler: (global: global, local: local)) 68 | } 69 | 70 | /// Initialize with local + global handler with manual local event forwarding. 71 | public convenience init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> NSEvent?) { 72 | self.init(mask: mask, local: handler, global: { _ = handler($0) }) 73 | } 74 | 75 | /// Initialize with local + global handler with custom local event forwarding. 76 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 77 | public convenience init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> Void, forward: Bool = true) { 78 | /*@formatter:off*/ self.init(mask: mask, local: { handler($0); return forward ? $0 : nil }, global: handler) /*@formatter:on*/ 79 | } 80 | 81 | /// Initialize with local + global handler with custom local event forwarding. 82 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 83 | public convenience init(mask: NSEvent.EventTypeMask, handler: @escaping () -> Void, forward: Bool = true) { 84 | /*@formatter:off*/ self.init(mask: mask, local: { handler(); return forward ? $0 : nil }, global: { _ in handler() }) /*@formatter:on*/ 85 | } 86 | 87 | /// Initialize with local handler with manual event forwarding. 88 | public convenience init(mask: NSEvent.EventTypeMask, local: @escaping (NSEvent) -> NSEvent?) { 89 | self.init(mask: mask, local: local, global: nil) 90 | } 91 | 92 | /// Initialize with local handler with custom local event forwarding. 93 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 94 | public convenience init(mask: NSEvent.EventTypeMask, local: @escaping (NSEvent) -> Void, forward: Bool = true) { 95 | /*@formatter:off*/ self.init(mask: mask, local: { local($0); return forward ? $0 : nil }, global: nil) /*@formatter:on*/ 96 | } 97 | 98 | /// Initialize with local handler with custom local event forwarding. 99 | /// - parameter forward: Specifies whether to forward the event or not, default is `true`. 100 | public convenience init(mask: NSEvent.EventTypeMask, local: @escaping () -> Void, forward: Bool = true) { 101 | /*@formatter:off*/ self.init(mask: mask, local: { local(); return forward ? $0 : nil }, global: nil) /*@formatter:on*/ 102 | } 103 | 104 | /// Initialize with global handler. 105 | public convenience init(mask: NSEvent.EventTypeMask, global: @escaping (NSEvent) -> Void) { 106 | self.init(mask: mask, local: nil, global: global) 107 | } 108 | 109 | /// Initialize with global handler. 110 | public convenience init(mask: NSEvent.EventTypeMask, global: @escaping () -> Void) { 111 | self.init(mask: mask, local: nil, global: { _ in global() }) 112 | } 113 | } 114 | 115 | extension EventObserver.Handler { 116 | public struct Carbon { 117 | public typealias Signature = (CGEvent) -> CGEvent? 118 | 119 | open class Definition: ObserverHandlerDefinition { 120 | public init?(mask: CGEventMask, location: CGEventTapLocation, placement: CGEventTapPlacement, options: CGEventTapOptions, handler: @escaping Signature) { 121 | 122 | // We may receive events that we didn't ask for, like null, tapDisabledByTimeout or tapDisabledByUserInput. To avoid it we must check 123 | // that event mask matches the specified, kudos to https://bugs.swift.org/browse/SR-4073. 124 | 125 | let userInfo: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) 126 | userInfo.initialize(to: { mask & CGEventMask(1 << $0.type.rawValue) > 0 ? handler($0) : $0 }) 127 | 128 | let callback: CGEventTapCallBack = { (proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, handler: UnsafeMutableRawPointer?) -> Unmanaged? in 129 | UnsafeMutablePointer(OpaquePointer(handler!)).pointee(event).map({ Unmanaged.passUnretained($0) }) 130 | } 131 | 132 | // Tap should be initially disabled – it shouldn't play any role unless it's added to a loop, frankly it never did until recent, 133 | // but it messes up event flow and sometimes events get blocked, for example, when adding inactive mouse down observer. 134 | 135 | guard let tap: CFMachPort = CGEvent.tapCreate(tap: location, place: placement, options: options, eventsOfInterest: mask, callback: callback, userInfo: userInfo) else { return nil } 136 | CGEvent.tapEnable(tap: tap, enable: false) 137 | 138 | self.loop = CFRunLoopGetCurrent() 139 | self.source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) 140 | self.tap = tap 141 | self.userInfo = userInfo 142 | 143 | self.mask = mask 144 | self.location = location 145 | self.placement = placement 146 | self.options = options 147 | self.handler = handler 148 | } 149 | 150 | deinit { 151 | self.deactivate() 152 | self.userInfo.deinitialize(count: 1) 153 | self.userInfo.deallocate() 154 | } 155 | 156 | public let mask: CGEventMask 157 | public let location: CGEventTapLocation 158 | public let placement: CGEventTapPlacement 159 | public let options: CGEventTapOptions 160 | public let handler: Signature 161 | 162 | private let loop: CFRunLoop 163 | private let source: CFRunLoopSource 164 | private let tap: CFMachPort 165 | private let userInfo: UnsafeMutablePointer 166 | 167 | open var isActive: Bool { 168 | CGEvent.tapIsEnabled(tap: self.tap) && CFRunLoopContainsSource(self.loop, self.source, .commonModes) 169 | } 170 | 171 | @discardableResult open func activate(_ newValue: Bool = true) -> Self { 172 | if newValue == self.isActive { return self } 173 | 174 | if newValue { 175 | CFRunLoopAddSource(self.loop, self.source, .commonModes) 176 | CGEvent.tapEnable(tap: self.tap, enable: true) 177 | } else { 178 | CGEvent.tapEnable(tap: self.tap, enable: false) 179 | CFRunLoopRemoveSource(self.loop, self.source, .commonModes) 180 | } 181 | 182 | return self 183 | } 184 | 185 | @discardableResult open func deactivate() -> Self { 186 | self.activate(false) 187 | } 188 | } 189 | } 190 | } 191 | 192 | /// Convenience initializers. 193 | extension EventObserver.Handler.Carbon.Definition { 194 | 195 | /// Initialize with manual event forwarding. 196 | public convenience init?(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> CGEvent?) { 197 | self.init(mask: mask, location: location ?? .cgSessionEventTap, placement: placement ?? .headInsertEventTap, options: options ?? .defaultTap, handler: handler) 198 | } 199 | 200 | /// Initialize with automatic event forwarding. 201 | public convenience init?(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> Void) { 202 | /*@formatter:off*/ self.init(mask: mask, location: location, placement: placement, options: options, handler: { handler($0); return $0 } as EventObserver.Handler.Carbon.Signature) /*@formatter:on*/ 203 | } 204 | 205 | /// Initialize with automatic event forwarding. 206 | public convenience init?(mask: CGEventMask, location: CGEventTapLocation? = nil, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping () -> Void) { 207 | /*@formatter:off*/ self.init(mask: mask, location: location, placement: placement, options: options, handler: { handler(); return $0 } as EventObserver.Handler.Carbon.Signature) /*@formatter:on*/ 208 | } 209 | 210 | /// Initialize with manual event forwarding. 211 | public convenience init?(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> CGEvent?) { 212 | self.init(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler) 213 | } 214 | 215 | /// Initialize with automatic event forwarding. 216 | public convenience init?(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping (CGEvent) -> Void) { 217 | self.init(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler) 218 | } 219 | 220 | /// Initialize with automatic event forwarding. 221 | public convenience init?(mask: NSEvent.EventTypeMask, location: CGEventTapLocation?, placement: CGEventTapPlacement? = nil, options: CGEventTapOptions? = nil, handler: @escaping () -> Void) { 222 | self.init(mask: mask.rawValue, location: location, placement: placement, options: options, handler: handler) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /source/Observatory/Keyboard/Keyboard.Key.swift: -------------------------------------------------------------------------------- 1 | import AppKit.NSEvent 2 | import Carbon 3 | 4 | /// Represents a keyboard key. Don't forget – there are different keyboard layouts for handling different languages. 5 | public struct KeyboardKey: RawRepresentable { 6 | public init(rawValue: Int) { self.rawValue = rawValue } 7 | public init(_ rawValue: Int) { self.init(rawValue: rawValue) } 8 | public init(_ keyCode: CGKeyCode) { self.init(Int(keyCode)) } 9 | public init(_ event: NSEvent) { self.init(Int(event.keyCode)) } 10 | 11 | public init?(_ name: String) { self.init(name, custom: Self.names) } 12 | public init?(_ name: String, custom map: [KeyboardKey: String]) { self.init(name, layout: .ascii, custom: map) } 13 | public init?(_ name: String, layout: Layout, custom map: [KeyboardKey: String]? = nil) { self.init(name, layout: layout.data, custom: map) } 14 | 15 | /// Attempts to create a KeyboardKey from the corresponding name in the specified (or default, otherwise) keyboard layout. 16 | public init?(_ name: String, layout: Data?, custom map: [KeyboardKey: String]? = nil) { 17 | if name.isEmpty { return nil } 18 | // 😍 Oh this is priceless… Couldn't find an easy way to map a character into key code, but obviously this should be possible 19 | // by somehow digesting the keyboard layout data. A sillsdev/Ukelele might be a good source of inspiration along with 20 | // the CarbonCore/UnicodeUtilities.h. With some inspiration from browserstack/OSXVNC we can also just iterate through 21 | // all available key codes and compare the name, of which there should be only 127 (0x7F) tops based on the highest virtual 22 | // key code constant in HIToolbox/Events.h. This probably has some limitations, including performance, but should be a decent 23 | // solution for the most typical situations. 24 | let string = name.uppercased() 25 | for i in 0x00 ... 0x7F where KeyboardKey(i).name(layout: layout, custom: map)?.uppercased() == string { 26 | self.init(i) 27 | return 28 | } 29 | return nil 30 | } 31 | 32 | public let rawValue: Int 33 | 34 | public static let a: KeyboardKey = .init(kVK_ANSI_A) 35 | public static let b: KeyboardKey = .init(kVK_ANSI_B) 36 | public static let c: KeyboardKey = .init(kVK_ANSI_C) 37 | public static let d: KeyboardKey = .init(kVK_ANSI_D) 38 | public static let e: KeyboardKey = .init(kVK_ANSI_E) 39 | public static let f: KeyboardKey = .init(kVK_ANSI_F) 40 | public static let g: KeyboardKey = .init(kVK_ANSI_G) 41 | public static let h: KeyboardKey = .init(kVK_ANSI_H) 42 | public static let i: KeyboardKey = .init(kVK_ANSI_I) 43 | public static let j: KeyboardKey = .init(kVK_ANSI_J) 44 | public static let k: KeyboardKey = .init(kVK_ANSI_K) 45 | public static let l: KeyboardKey = .init(kVK_ANSI_L) 46 | public static let m: KeyboardKey = .init(kVK_ANSI_M) 47 | public static let n: KeyboardKey = .init(kVK_ANSI_N) 48 | public static let o: KeyboardKey = .init(kVK_ANSI_O) 49 | public static let p: KeyboardKey = .init(kVK_ANSI_P) 50 | public static let q: KeyboardKey = .init(kVK_ANSI_Q) 51 | public static let r: KeyboardKey = .init(kVK_ANSI_R) 52 | public static let s: KeyboardKey = .init(kVK_ANSI_S) 53 | public static let t: KeyboardKey = .init(kVK_ANSI_T) 54 | public static let u: KeyboardKey = .init(kVK_ANSI_U) 55 | public static let v: KeyboardKey = .init(kVK_ANSI_V) 56 | public static let w: KeyboardKey = .init(kVK_ANSI_W) 57 | public static let x: KeyboardKey = .init(kVK_ANSI_X) 58 | public static let y: KeyboardKey = .init(kVK_ANSI_Y) 59 | public static let z: KeyboardKey = .init(kVK_ANSI_Z) 60 | 61 | public static let zero: KeyboardKey = .init(kVK_ANSI_0) 62 | public static let one: KeyboardKey = .init(kVK_ANSI_1) 63 | public static let two: KeyboardKey = .init(kVK_ANSI_2) 64 | public static let three: KeyboardKey = .init(kVK_ANSI_3) 65 | public static let four: KeyboardKey = .init(kVK_ANSI_4) 66 | public static let five: KeyboardKey = .init(kVK_ANSI_5) 67 | public static let six: KeyboardKey = .init(kVK_ANSI_6) 68 | public static let seven: KeyboardKey = .init(kVK_ANSI_7) 69 | public static let eight: KeyboardKey = .init(kVK_ANSI_8) 70 | public static let nine: KeyboardKey = .init(kVK_ANSI_9) 71 | 72 | public static let equal: KeyboardKey = .init(kVK_ANSI_Equal) 73 | public static let minus: KeyboardKey = .init(kVK_ANSI_Minus) 74 | public static let rightBracket: KeyboardKey = .init(kVK_ANSI_RightBracket) 75 | public static let leftBracket: KeyboardKey = .init(kVK_ANSI_LeftBracket) 76 | public static let quote: KeyboardKey = .init(kVK_ANSI_Quote) 77 | public static let semicolon: KeyboardKey = .init(kVK_ANSI_Semicolon) 78 | public static let backslash: KeyboardKey = .init(kVK_ANSI_Backslash) 79 | public static let comma: KeyboardKey = .init(kVK_ANSI_Comma) 80 | public static let slash: KeyboardKey = .init(kVK_ANSI_Slash) 81 | public static let period: KeyboardKey = .init(kVK_ANSI_Period) 82 | public static let grave: KeyboardKey = .init(kVK_ANSI_Grave) 83 | 84 | public static let keypadDecimal: KeyboardKey = .init(kVK_ANSI_KeypadDecimal) 85 | public static let keypadMultiply: KeyboardKey = .init(kVK_ANSI_KeypadMultiply) 86 | public static let keypadPlus: KeyboardKey = .init(kVK_ANSI_KeypadPlus) 87 | public static let keypadClear: KeyboardKey = .init(kVK_ANSI_KeypadClear) 88 | public static let keypadDivide: KeyboardKey = .init(kVK_ANSI_KeypadDivide) 89 | public static let keypadEnter: KeyboardKey = .init(kVK_ANSI_KeypadEnter) 90 | public static let keypadMinus: KeyboardKey = .init(kVK_ANSI_KeypadMinus) 91 | public static let keypadEquals: KeyboardKey = .init(kVK_ANSI_KeypadEquals) 92 | 93 | public static let keypad0: KeyboardKey = .init(kVK_ANSI_Keypad0) 94 | public static let keypad1: KeyboardKey = .init(kVK_ANSI_Keypad1) 95 | public static let keypad2: KeyboardKey = .init(kVK_ANSI_Keypad2) 96 | public static let keypad3: KeyboardKey = .init(kVK_ANSI_Keypad3) 97 | public static let keypad4: KeyboardKey = .init(kVK_ANSI_Keypad4) 98 | public static let keypad5: KeyboardKey = .init(kVK_ANSI_Keypad5) 99 | public static let keypad6: KeyboardKey = .init(kVK_ANSI_Keypad6) 100 | public static let keypad7: KeyboardKey = .init(kVK_ANSI_Keypad7) 101 | public static let keypad8: KeyboardKey = .init(kVK_ANSI_Keypad8) 102 | public static let keypad9: KeyboardKey = .init(kVK_ANSI_Keypad9) 103 | 104 | public static let capsLock: KeyboardKey = .init(kVK_CapsLock) 105 | public static let command: KeyboardKey = .init(kVK_Command) 106 | public static let control: KeyboardKey = .init(kVK_Control) 107 | public static let option: KeyboardKey = .init(kVK_Option) 108 | public static let shift: KeyboardKey = .init(kVK_Shift) 109 | 110 | public static let function: KeyboardKey = .init(kVK_Function) 111 | public static let mute: KeyboardKey = .init(kVK_Mute) 112 | public static let volumeDown: KeyboardKey = .init(kVK_VolumeDown) 113 | public static let volumeUp: KeyboardKey = .init(kVK_VolumeUp) 114 | public static let rightControl: KeyboardKey = .init(kVK_RightControl) 115 | public static let rightOption: KeyboardKey = .init(kVK_RightOption) 116 | public static let rightShift: KeyboardKey = .init(kVK_RightShift) 117 | 118 | public static let delete: KeyboardKey = .init(kVK_Delete) 119 | public static let downArrow: KeyboardKey = .init(kVK_DownArrow) 120 | public static let end: KeyboardKey = .init(kVK_End) 121 | public static let escape: KeyboardKey = .init(kVK_Escape) 122 | public static let forwardDelete: KeyboardKey = .init(kVK_ForwardDelete) 123 | public static let help: KeyboardKey = .init(kVK_Help) 124 | public static let home: KeyboardKey = .init(kVK_Home) 125 | public static let leftArrow: KeyboardKey = .init(kVK_LeftArrow) 126 | public static let pageDown: KeyboardKey = .init(kVK_PageDown) 127 | public static let pageUp: KeyboardKey = .init(kVK_PageUp) 128 | public static let `return`: KeyboardKey = .init(kVK_Return) 129 | public static let rightArrow: KeyboardKey = .init(kVK_RightArrow) 130 | public static let space: KeyboardKey = .init(kVK_Space) 131 | public static let tab: KeyboardKey = .init(kVK_Tab) 132 | public static let upArrow: KeyboardKey = .init(kVK_UpArrow) 133 | 134 | public static let f1: KeyboardKey = .init(kVK_F1) 135 | public static let f2: KeyboardKey = .init(kVK_F2) 136 | public static let f3: KeyboardKey = .init(kVK_F3) 137 | public static let f4: KeyboardKey = .init(kVK_F4) 138 | public static let f5: KeyboardKey = .init(kVK_F5) 139 | public static let f6: KeyboardKey = .init(kVK_F6) 140 | public static let f7: KeyboardKey = .init(kVK_F7) 141 | public static let f8: KeyboardKey = .init(kVK_F8) 142 | public static let f9: KeyboardKey = .init(kVK_F9) 143 | public static let f10: KeyboardKey = .init(kVK_F10) 144 | public static let f11: KeyboardKey = .init(kVK_F11) 145 | public static let f12: KeyboardKey = .init(kVK_F12) 146 | public static let f13: KeyboardKey = .init(kVK_F13) 147 | public static let f14: KeyboardKey = .init(kVK_F14) 148 | public static let f15: KeyboardKey = .init(kVK_F15) 149 | public static let f16: KeyboardKey = .init(kVK_F16) 150 | public static let f17: KeyboardKey = .init(kVK_F17) 151 | public static let f18: KeyboardKey = .init(kVK_F18) 152 | public static let f19: KeyboardKey = .init(kVK_F19) 153 | public static let f20: KeyboardKey = .init(kVK_F20) 154 | 155 | public var name: String? { 156 | self.name(custom: Self.names) 157 | } 158 | 159 | public func name(custom map: [KeyboardKey: String]) -> String? { 160 | self.name(layout: .ascii, custom: map) 161 | } 162 | 163 | public func name(layout: Layout, custom map: [KeyboardKey: String]? = nil) -> String? { 164 | self.name(layout: layout.data, custom: map) 165 | } 166 | 167 | public func name(layout: Data?, custom map: [KeyboardKey: String]? = nil) -> String? { 168 | if let name = map?[self] { return name } 169 | guard let layout = layout ?? Layout.ascii.data else { return nil } 170 | 171 | let maxStringLength = 4 as Int 172 | var stringBuffer = [UniChar](repeating: 0, count: maxStringLength) 173 | var stringLength = 0 as Int 174 | 175 | let modifierKeys = 0 as UInt32 176 | var deadKeys = 0 as UInt32 177 | let keyboardType = UInt32(LMGetKbdType()) 178 | 179 | guard let layout = layout.withUnsafeBytes({ $0.baseAddress?.assumingMemoryBound(to: UCKeyboardLayout.self) }) else { return nil } 180 | let status = UCKeyTranslate(layout, CGKeyCode(self.rawValue), CGKeyCode(kUCKeyActionDown), modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask), &deadKeys, maxStringLength, &stringLength, &stringBuffer) 181 | guard status == Darwin.noErr else { return nil } 182 | 183 | return String(utf16CodeUnits: stringBuffer, count: stringLength).uppercased() 184 | } 185 | } 186 | 187 | extension KeyboardKey { 188 | /// Predefined name map for keys, mostly the ones that are language independent and not available via `UCKeyTranslate`. 189 | public static let names: [KeyboardKey: String] = [ 190 | .keypadClear: "⌧", 191 | .keypadEnter: "⌅", 192 | 193 | .capsLock: "⇪", 194 | .command: "⌘", 195 | .control: "⌃", .rightControl: "⌃", 196 | .option: "⌥", .rightOption: "⌥", 197 | .shift: "⇧", .rightShift: "⇧", 198 | 199 | .delete: "⌫", 200 | .downArrow: "↓", 201 | .end: "↘", 202 | .escape: "⎋", 203 | .forwardDelete: "⌦", 204 | .help: "?⃝", 205 | .home: "↖", 206 | .leftArrow: "←", 207 | .pageDown: "⇟", 208 | .pageUp: "⇞", 209 | .return: "↩", 210 | .rightArrow: "→", 211 | .space: "␣", 212 | .tab: "⇥", 213 | .upArrow: "↑", 214 | 215 | .f1: "F1", 216 | .f2: "F2", 217 | .f3: "F3", 218 | .f4: "F4", 219 | .f5: "F5", 220 | .f6: "F6", 221 | .f7: "F7", 222 | .f8: "F8", 223 | .f9: "F9", 224 | .f10: "F10", 225 | .f11: "F11", 226 | .f12: "F12", 227 | .f13: "F13", 228 | .f14: "F14", 229 | .f15: "F15", 230 | .f16: "F16", 231 | .f17: "F17", 232 | .f18: "F18", 233 | .f19: "F19", 234 | .f20: "F20", 235 | ] 236 | } 237 | 238 | extension KeyboardKey: Equatable, Hashable { 239 | public func hash(into hasher: inout Hasher) { hasher.combine(self.rawValue) } 240 | } 241 | 242 | extension KeyboardKey: CustomStringConvertible { 243 | public var description: String { self.name(custom: Self.names) ?? "" } 244 | } 245 | 246 | extension KeyboardKey { 247 | public enum Layout: CaseIterable { 248 | case ascii 249 | case current 250 | } 251 | } 252 | 253 | extension KeyboardKey.Layout { 254 | /// Needed for serializing access to Carbon’s TIS keyboard layout APIs, which can crash under concurrent calls, 255 | /// ensuring layout data retrieval is thread-safe. 256 | private static let lock = NSLock() 257 | 258 | /// The unicode keyboard layout, with some great insight from: 259 | /// - https://jongampark.wordpress.com/2015/07/17. 260 | /// - https://github.com/cocoabits/MASShortcut/issues/60 261 | public var data: Data? { 262 | Self.lock.lock() 263 | defer { Self.lock.unlock() } 264 | 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. 268 | 269 | var inputSource: TISInputSource? 270 | switch self { 271 | case .ascii: inputSource = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue() 272 | case .current: inputSource = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue() 273 | } 274 | guard let inputSource else { return nil } 275 | guard let data = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData) else { return nil } 276 | guard let data = Unmanaged.fromOpaque(data).takeUnretainedValue() as? NSData, data.count > 0 else { return nil } 277 | return Data(referencing: data) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /Observatory.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 17FD00154E7DD4027591C5D2 /* Testing.Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0B7E4C2ACF3D110CDA9D /* Testing.Observation.swift */; }; 11 | 17FD00CB84FB669705C7E473 /* Keyboard.Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0A54F5667F4CE29E2B39 /* Keyboard.Key.swift */; }; 12 | 17FD012B602860A1D459E05A /* Observer.Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0C0324E5560F5F80CA45 /* Observer.Hotkey.swift */; }; 13 | 17FD0148328306B2C2A1C505 /* Test.Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD06581333B3906BE6258A /* Test.Shortcut.swift */; }; 14 | 17FD0156D1758DD5200998BE /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD078E512F21A8ECE8F335 /* Test.swift */; }; 15 | 17FD0162D167D444D8E5ED9A /* Entrypoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD08D461EB26AE885B82AC /* Entrypoint.swift */; }; 16 | 17FD019C5C7935EDF1E25882 /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0FB54AAAEB24AB193225 /* Shortcut.swift */; }; 17 | 17FD01BE2C8AF0115AE9DF8E /* Test.Observer.Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0549D3EA10134DC784A3 /* Test.Observer.Notification.swift */; }; 18 | 17FD01D475A336AA93EC9F2F /* Keyboard.Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0A7C14DC5A257B15375D /* Keyboard.Modifier.swift */; }; 19 | 17FD028A9B91583E857EF5EF /* Test.Observer.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD08E9489864D454E503B4 /* Test.Observer.Event.swift */; }; 20 | 17FD0328BCF34B7BF64C7670 /* Observer.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD03560E1CD18D16062849 /* Observer.Event.swift */; }; 21 | 17FD057B880D845188EA2DC8 /* Testing.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0CC52C2436784A911E89 /* Testing.Event.swift */; }; 22 | 17FD062C3246D204FE4BD93B /* Observer.Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD009919B2E0C5C02AE1FB /* Observer.Notification.swift */; }; 23 | 17FD066C556A49EB63E661CC /* Observer.Notification.Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0916F82FAC336830F9E0 /* Observer.Notification.Handler.swift */; }; 24 | 17FD088C42F60A74F1C81846 /* Test.Keyboard.Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD04EB71CD7024929F934F /* Test.Keyboard.Key.swift */; }; 25 | 17FD08933236D7505E7FCF22 /* Shortcut.Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD01D08B3B6F5738B54CEE /* Shortcut.Recorder.swift */; }; 26 | 17FD0895BDF08CF3ED5FEA49 /* Shortcut.Recorder.Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD04AABD313E9B5316521D /* Shortcut.Recorder.Button.swift */; }; 27 | 17FD08BD0A91FD8A01FBD698 /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0EC2D909CCE6E3BC8A9E /* Observer.swift */; }; 28 | 17FD093654260D5761C05F6E /* Shortcut.Center.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD09AD38D24C3C1205E9BE /* Shortcut.Center.swift */; }; 29 | 17FD09D3A526167E7A8F531D /* Test.Observer.Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0AE28489C20294DE5E48 /* Test.Observer.Hotkey.swift */; }; 30 | 17FD0A6270752A2959E61CF9 /* Keyboard.Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD017771C4B54857A16DAD /* Keyboard.Hotkey.swift */; }; 31 | 17FD0A7B3145FFD47C972C40 /* Observer.Event.Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0007A26B7B85FC5190C0 /* Observer.Event.Handler.swift */; }; 32 | 17FD0BCDD00D6AF24E42FBD6 /* Observer.HandlerDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0D539B72A4A1B5DFCEBE /* Observer.HandlerDefinition.swift */; }; 33 | 17FD0C1A4329D5233941DDE4 /* Test.Keyboard.Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD01507DE6860A0089FEAF /* Test.Keyboard.Hotkey.swift */; }; 34 | 17FD0E596C7B56AEB4865F6A /* Observer.Hotkey.Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0F7C4A53D96FBDB8674E /* Observer.Hotkey.Handler.swift */; }; 35 | 196807DF7B07BBFAF1E7BCB4 /* Test.Keyboard.Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1968096E1E1189C984B6F696 /* Test.Keyboard.Modifier.swift */; }; 36 | 48517292289869DC00B66C61 /* Observatory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6D7642A1CDDBA100071CCB0 /* Observatory.framework */; }; 37 | E609DC8B1E7BC8CA00F919DD /* demo.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E609DC891E7BC8C400F919DD /* demo.storyboard */; }; 38 | E690D12A267D1ADC002FD479 /* Observatory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6D7642A1CDDBA100071CCB0 /* Observatory.framework */; }; 39 | E690D12B267D1AFE002FD479 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E6FA8A8B267D08C200F0C878 /* Nimble */; }; 40 | E690D12C267D1AFE002FD479 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = E6FA8A8D267D08D900F0C878 /* Quick */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | 087EE7A21E1C26EE00093E0E /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = E6D764211CDDBA100071CCB0 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = E6D764291CDDBA100071CCB0; 49 | remoteInfo = Observatory; 50 | }; 51 | E69200F91CDDE19200C4FEAD /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = E6D764211CDDBA100071CCB0 /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = E6D764291CDDBA100071CCB0; 56 | remoteInfo = Observatory; 57 | }; 58 | /* End PBXContainerItemProxy section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | 087EE7921E1C1FF500093E0E /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 17FD0007A26B7B85FC5190C0 /* Observer.Event.Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Event.Handler.swift; sourceTree = ""; }; 63 | 17FD009919B2E0C5C02AE1FB /* Observer.Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Notification.swift; sourceTree = ""; }; 64 | 17FD01507DE6860A0089FEAF /* Test.Keyboard.Hotkey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Keyboard.Hotkey.swift; sourceTree = ""; }; 65 | 17FD0152B633954A227E8C79 /* Observatory.podspec */ = {isa = PBXFileReference; lastKnownFileType = file.podspec; path = Observatory.podspec; sourceTree = ""; }; 66 | 17FD017771C4B54857A16DAD /* Keyboard.Hotkey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.Hotkey.swift; sourceTree = ""; }; 67 | 17FD01D08B3B6F5738B54CEE /* Shortcut.Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcut.Recorder.swift; sourceTree = ""; }; 68 | 17FD03560E1CD18D16062849 /* Observer.Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Event.swift; sourceTree = ""; }; 69 | 17FD049B29A650BABBC10CC6 /* test.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = test.plist; sourceTree = ""; }; 70 | 17FD04AABD313E9B5316521D /* Shortcut.Recorder.Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcut.Recorder.Button.swift; sourceTree = ""; }; 71 | 17FD04EB71CD7024929F934F /* Test.Keyboard.Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Keyboard.Key.swift; sourceTree = ""; }; 72 | 17FD0549D3EA10134DC784A3 /* Test.Observer.Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Observer.Notification.swift; sourceTree = ""; }; 73 | 17FD06581333B3906BE6258A /* Test.Shortcut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Shortcut.swift; sourceTree = ""; }; 74 | 17FD078E512F21A8ECE8F335 /* Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; 75 | 17FD0872935075E62744FA2B /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 76 | 17FD08D461EB26AE885B82AC /* Entrypoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entrypoint.swift; sourceTree = ""; }; 77 | 17FD08E9489864D454E503B4 /* Test.Observer.Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Observer.Event.swift; sourceTree = ""; }; 78 | 17FD0916F82FAC336830F9E0 /* Observer.Notification.Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Notification.Handler.swift; sourceTree = ""; }; 79 | 17FD094B1DAE4CF98579274D /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 80 | 17FD09AD38D24C3C1205E9BE /* Shortcut.Center.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcut.Center.swift; sourceTree = ""; }; 81 | 17FD0A54F5667F4CE29E2B39 /* Keyboard.Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.Key.swift; sourceTree = ""; }; 82 | 17FD0A7C14DC5A257B15375D /* Keyboard.Modifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.Modifier.swift; sourceTree = ""; }; 83 | 17FD0AE28489C20294DE5E48 /* Test.Observer.Hotkey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Observer.Hotkey.swift; sourceTree = ""; }; 84 | 17FD0B7E4C2ACF3D110CDA9D /* Testing.Observation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Testing.Observation.swift; sourceTree = ""; }; 85 | 17FD0C0324E5560F5F80CA45 /* Observer.Hotkey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Hotkey.swift; sourceTree = ""; }; 86 | 17FD0CC52C2436784A911E89 /* Testing.Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Testing.Event.swift; sourceTree = ""; }; 87 | 17FD0D539B72A4A1B5DFCEBE /* Observer.HandlerDefinition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.HandlerDefinition.swift; sourceTree = ""; }; 88 | 17FD0EC2D909CCE6E3BC8A9E /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = ""; }; 89 | 17FD0F7C4A53D96FBDB8674E /* Observer.Hotkey.Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.Hotkey.Handler.swift; sourceTree = ""; }; 90 | 17FD0F994461E499BAC659EB /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 91 | 17FD0FB54AAAEB24AB193225 /* Shortcut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; 92 | 1968096E1E1189C984B6F696 /* Test.Keyboard.Modifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Test.Keyboard.Modifier.swift; sourceTree = ""; }; 93 | 691492DAECCDA8F37886F020 /* framework.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = framework.plist; sourceTree = ""; }; 94 | 692C0EE3AC3E3172836E1B96 /* application.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = application.plist; sourceTree = ""; }; 95 | 697EDBC98FFDECF7FF264F3A /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 96 | E609DC891E7BC8C400F919DD /* demo.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = demo.storyboard; sourceTree = ""; }; 97 | E690D111267D19F6002FD479 /* macOS-Application.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-Application.xcconfig"; sourceTree = ""; }; 98 | E690D112267D19F6002FD479 /* macOS-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-Framework.xcconfig"; sourceTree = ""; }; 99 | E690D113267D19F6002FD479 /* macOS-StaticLibrary.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-StaticLibrary.xcconfig"; sourceTree = ""; }; 100 | E690D114267D19F6002FD479 /* macOS-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-Base.xcconfig"; sourceTree = ""; }; 101 | E690D115267D19F6002FD479 /* macOS-XCTest.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-XCTest.xcconfig"; sourceTree = ""; }; 102 | E690D116267D19F6002FD479 /* macOS-DynamicLibrary.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "macOS-DynamicLibrary.xcconfig"; sourceTree = ""; }; 103 | E690D124267D1A73002FD479 /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Profile.xcconfig; sourceTree = ""; }; 104 | E690D125267D1A73002FD479 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 105 | E690D126267D1A73002FD479 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 106 | E690D127267D1A73002FD479 /* Test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Test.xcconfig; sourceTree = ""; }; 107 | E690D12D267D1B0C002FD479 /* Package.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Package.resolved; sourceTree = ""; }; 108 | E690D12E267D1B14002FD479 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 109 | E69200F31CDDE19100C4FEAD /* Observatory-Test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Observatory-Test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 110 | E6D7642A1CDDBA100071CCB0 /* Observatory.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Observatory.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 111 | /* End PBXFileReference section */ 112 | 113 | /* Begin PBXFrameworksBuildPhase section */ 114 | 48517290289869D700B66C61 /* Frameworks */ = { 115 | isa = PBXFrameworksBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | 48517292289869DC00B66C61 /* Observatory.framework in Frameworks */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | E690D128267D1AD5002FD479 /* Frameworks */ = { 123 | isa = PBXFrameworksBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | E690D12A267D1ADC002FD479 /* Observatory.framework in Frameworks */, 127 | E690D12C267D1AFE002FD479 /* Quick in Frameworks */, 128 | E690D12B267D1AFE002FD479 /* Nimble in Frameworks */, 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | /* End PBXFrameworksBuildPhase section */ 133 | 134 | /* Begin PBXGroup section */ 135 | 17FD00AF91A500EDFD52854A /* Observer */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 17FD08E9489864D454E503B4 /* Test.Observer.Event.swift */, 139 | 17FD0AE28489C20294DE5E48 /* Test.Observer.Hotkey.swift */, 140 | 17FD0549D3EA10134DC784A3 /* Test.Observer.Notification.swift */, 141 | ); 142 | path = Observer; 143 | sourceTree = ""; 144 | }; 145 | 17FD00CF6AE1E6CB712F9CBA /* Shortcut */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 17FD0FB54AAAEB24AB193225 /* Shortcut.swift */, 149 | 17FD09AD38D24C3C1205E9BE /* Shortcut.Center.swift */, 150 | 17FD01D08B3B6F5738B54CEE /* Shortcut.Recorder.swift */, 151 | 17FD04AABD313E9B5316521D /* Shortcut.Recorder.Button.swift */, 152 | ); 153 | path = Shortcut; 154 | sourceTree = ""; 155 | }; 156 | 17FD030F7F2C5CA3191B2040 /* accessory */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | E690D10F267D19E0002FD479 /* configuration */, 160 | 692739BC3F963DB95B757C64 /* info */, 161 | 69FE70738BA9A7D2F71070B9 /* storyboard */, 162 | ); 163 | path = accessory; 164 | sourceTree = ""; 165 | }; 166 | 17FD04EDF9644FF010EB95D3 /* Shortcut */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 17FD06581333B3906BE6258A /* Test.Shortcut.swift */, 170 | ); 171 | path = Shortcut; 172 | sourceTree = ""; 173 | }; 174 | 17FD05AECFC1FAE469284C0F /* Keyboard */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 17FD017771C4B54857A16DAD /* Keyboard.Hotkey.swift */, 178 | 17FD0A54F5667F4CE29E2B39 /* Keyboard.Key.swift */, 179 | 17FD0A7C14DC5A257B15375D /* Keyboard.Modifier.swift */, 180 | ); 181 | path = Keyboard; 182 | sourceTree = ""; 183 | }; 184 | 17FD075D9838C94D43A9D2A5 /* Demo */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 17FD08D461EB26AE885B82AC /* Entrypoint.swift */, 188 | ); 189 | path = Demo; 190 | sourceTree = ""; 191 | }; 192 | 17FD0865B2ECB4388EC47754 /* Testing */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 17FD0CC52C2436784A911E89 /* Testing.Event.swift */, 196 | 17FD0B7E4C2ACF3D110CDA9D /* Testing.Observation.swift */, 197 | ); 198 | path = Testing; 199 | sourceTree = ""; 200 | }; 201 | 17FD09950698908EEF7D7425 /* Notification */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 17FD009919B2E0C5C02AE1FB /* Observer.Notification.swift */, 205 | 17FD0916F82FAC336830F9E0 /* Observer.Notification.Handler.swift */, 206 | ); 207 | path = Notification; 208 | sourceTree = ""; 209 | }; 210 | 17FD09F540CD71F300A55FCB /* Observatory */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 17FD05AECFC1FAE469284C0F /* Keyboard */, 214 | 17FD0B3FA5AE2DB923C5697E /* Observer */, 215 | 17FD00CF6AE1E6CB712F9CBA /* Shortcut */, 216 | 17FD0E1C100943CB2E5A1608 /* Test */, 217 | 17FD0865B2ECB4388EC47754 /* Testing */, 218 | ); 219 | path = Observatory; 220 | sourceTree = ""; 221 | }; 222 | 17FD09FAB36FCB8416A21EDB /* Keyboard */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 17FD01507DE6860A0089FEAF /* Test.Keyboard.Hotkey.swift */, 226 | 17FD04EB71CD7024929F934F /* Test.Keyboard.Key.swift */, 227 | 1968096E1E1189C984B6F696 /* Test.Keyboard.Modifier.swift */, 228 | ); 229 | path = Keyboard; 230 | sourceTree = ""; 231 | }; 232 | 17FD0A502C015FEFB24C8941 /* Event */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 17FD03560E1CD18D16062849 /* Observer.Event.swift */, 236 | 17FD0007A26B7B85FC5190C0 /* Observer.Event.Handler.swift */, 237 | ); 238 | path = Event; 239 | sourceTree = ""; 240 | }; 241 | 17FD0B3FA5AE2DB923C5697E /* Observer */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 17FD0A502C015FEFB24C8941 /* Event */, 245 | 17FD0E8CA6D8B4653615047D /* Hotkey */, 246 | 17FD09950698908EEF7D7425 /* Notification */, 247 | 17FD0EC2D909CCE6E3BC8A9E /* Observer.swift */, 248 | 17FD0D539B72A4A1B5DFCEBE /* Observer.HandlerDefinition.swift */, 249 | ); 250 | path = Observer; 251 | sourceTree = ""; 252 | }; 253 | 17FD0BB4EDB0784084E6A631 /* source */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 17FD075D9838C94D43A9D2A5 /* Demo */, 257 | 17FD09F540CD71F300A55FCB /* Observatory */, 258 | ); 259 | path = source; 260 | sourceTree = ""; 261 | }; 262 | 17FD0E1C100943CB2E5A1608 /* Test */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | 17FD09FAB36FCB8416A21EDB /* Keyboard */, 266 | 17FD00AF91A500EDFD52854A /* Observer */, 267 | 17FD04EDF9644FF010EB95D3 /* Shortcut */, 268 | 17FD078E512F21A8ECE8F335 /* Test.swift */, 269 | ); 270 | path = Test; 271 | sourceTree = ""; 272 | }; 273 | 17FD0E8CA6D8B4653615047D /* Hotkey */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | 17FD0C0324E5560F5F80CA45 /* Observer.Hotkey.swift */, 277 | 17FD0F7C4A53D96FBDB8674E /* Observer.Hotkey.Handler.swift */, 278 | ); 279 | path = Hotkey; 280 | sourceTree = ""; 281 | }; 282 | 692739BC3F963DB95B757C64 /* info */ = { 283 | isa = PBXGroup; 284 | children = ( 285 | 692C0EE3AC3E3172836E1B96 /* application.plist */, 286 | 691492DAECCDA8F37886F020 /* framework.plist */, 287 | 17FD049B29A650BABBC10CC6 /* test.plist */, 288 | ); 289 | path = info; 290 | sourceTree = ""; 291 | }; 292 | 69FE70738BA9A7D2F71070B9 /* storyboard */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | E609DC891E7BC8C400F919DD /* demo.storyboard */, 296 | ); 297 | path = storyboard; 298 | sourceTree = ""; 299 | }; 300 | E690D10F267D19E0002FD479 /* configuration */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | E690D123267D1A73002FD479 /* Base */, 304 | E690D110267D19F6002FD479 /* macOS */, 305 | ); 306 | name = configuration; 307 | sourceTree = ""; 308 | }; 309 | E690D110267D19F6002FD479 /* macOS */ = { 310 | isa = PBXGroup; 311 | children = ( 312 | E690D114267D19F6002FD479 /* macOS-Base.xcconfig */, 313 | E690D113267D19F6002FD479 /* macOS-StaticLibrary.xcconfig */, 314 | E690D116267D19F6002FD479 /* macOS-DynamicLibrary.xcconfig */, 315 | E690D112267D19F6002FD479 /* macOS-Framework.xcconfig */, 316 | E690D111267D19F6002FD479 /* macOS-Application.xcconfig */, 317 | E690D115267D19F6002FD479 /* macOS-XCTest.xcconfig */, 318 | ); 319 | name = macOS; 320 | path = dependency/Git/xcconfigs/macOS; 321 | sourceTree = SOURCE_ROOT; 322 | }; 323 | E690D123267D1A73002FD479 /* Base */ = { 324 | isa = PBXGroup; 325 | children = ( 326 | E690D125267D1A73002FD479 /* Debug.xcconfig */, 327 | E690D126267D1A73002FD479 /* Release.xcconfig */, 328 | E690D127267D1A73002FD479 /* Test.xcconfig */, 329 | E690D124267D1A73002FD479 /* Profile.xcconfig */, 330 | ); 331 | name = Base; 332 | path = dependency/Git/xcconfigs/Base/Configurations; 333 | sourceTree = SOURCE_ROOT; 334 | }; 335 | E6D764201CDDBA100071CCB0 = { 336 | isa = PBXGroup; 337 | children = ( 338 | 17FD030F7F2C5CA3191B2040 /* accessory */, 339 | E6D7642B1CDDBA100071CCB0 /* product */, 340 | 17FD0BB4EDB0784084E6A631 /* source */, 341 | 17FD0872935075E62744FA2B /* .gitignore */, 342 | 17FD094B1DAE4CF98579274D /* .swiftlint.yml */, 343 | 17FD0F994461E499BAC659EB /* Package.swift */, 344 | E690D12D267D1B0C002FD479 /* Package.resolved */, 345 | 17FD0152B633954A227E8C79 /* Observatory.podspec */, 346 | E690D12E267D1B14002FD479 /* LICENSE.md */, 347 | 697EDBC98FFDECF7FF264F3A /* README.md */, 348 | ); 349 | sourceTree = ""; 350 | }; 351 | E6D7642B1CDDBA100071CCB0 /* product */ = { 352 | isa = PBXGroup; 353 | children = ( 354 | E6D7642A1CDDBA100071CCB0 /* Observatory.framework */, 355 | E69200F31CDDE19100C4FEAD /* Observatory-Test.xctest */, 356 | 087EE7921E1C1FF500093E0E /* Demo.app */, 357 | ); 358 | name = product; 359 | sourceTree = ""; 360 | }; 361 | /* End PBXGroup section */ 362 | 363 | /* Begin PBXNativeTarget section */ 364 | 087EE7911E1C1FF500093E0E /* Demo */ = { 365 | isa = PBXNativeTarget; 366 | buildConfigurationList = 087EE7A01E1C1FF500093E0E /* Build configuration list for PBXNativeTarget "Demo" */; 367 | buildPhases = ( 368 | 087EE78E1E1C1FF500093E0E /* Sources */, 369 | 087EE7901E1C1FF500093E0E /* Resources */, 370 | 48517290289869D700B66C61 /* Frameworks */, 371 | ); 372 | buildRules = ( 373 | ); 374 | dependencies = ( 375 | 087EE7A31E1C26EE00093E0E /* PBXTargetDependency */, 376 | ); 377 | name = Demo; 378 | productName = Hotkey; 379 | productReference = 087EE7921E1C1FF500093E0E /* Demo.app */; 380 | productType = "com.apple.product-type.application"; 381 | }; 382 | E69200F21CDDE19100C4FEAD /* Observatory-Test */ = { 383 | isa = PBXNativeTarget; 384 | buildConfigurationList = E69200FD1CDDE19200C4FEAD /* Build configuration list for PBXNativeTarget "Observatory-Test" */; 385 | buildPhases = ( 386 | E69200EF1CDDE19100C4FEAD /* Sources */, 387 | E690D128267D1AD5002FD479 /* Frameworks */, 388 | ); 389 | buildRules = ( 390 | ); 391 | dependencies = ( 392 | E69200FA1CDDE19200C4FEAD /* PBXTargetDependency */, 393 | ); 394 | name = "Observatory-Test"; 395 | packageProductDependencies = ( 396 | E6FA8A8B267D08C200F0C878 /* Nimble */, 397 | E6FA8A8D267D08D900F0C878 /* Quick */, 398 | ); 399 | productName = Observatoryt; 400 | productReference = E69200F31CDDE19100C4FEAD /* Observatory-Test.xctest */; 401 | productType = "com.apple.product-type.bundle.unit-test"; 402 | }; 403 | E6D764291CDDBA100071CCB0 /* Observatory */ = { 404 | isa = PBXNativeTarget; 405 | buildConfigurationList = E6D764321CDDBA100071CCB0 /* Build configuration list for PBXNativeTarget "Observatory" */; 406 | buildPhases = ( 407 | E6D764251CDDBA100071CCB0 /* Sources */, 408 | ); 409 | buildRules = ( 410 | ); 411 | dependencies = ( 412 | ); 413 | name = Observatory; 414 | productName = Observatory; 415 | productReference = E6D7642A1CDDBA100071CCB0 /* Observatory.framework */; 416 | productType = "com.apple.product-type.framework"; 417 | }; 418 | /* End PBXNativeTarget section */ 419 | 420 | /* Begin PBXProject section */ 421 | E6D764211CDDBA100071CCB0 /* Project object */ = { 422 | isa = PBXProject; 423 | attributes = { 424 | LastSwiftUpdateCheck = 0730; 425 | LastUpgradeCheck = 1250; 426 | ORGANIZATIONNAME = Swifteroid; 427 | TargetAttributes = { 428 | 087EE7911E1C1FF500093E0E = { 429 | CreatedOnToolsVersion = 7.3.1; 430 | LastSwiftMigration = 1020; 431 | }; 432 | E69200F21CDDE19100C4FEAD = { 433 | CreatedOnToolsVersion = 7.3; 434 | LastSwiftMigration = 1020; 435 | }; 436 | E6D764291CDDBA100071CCB0 = { 437 | CreatedOnToolsVersion = 7.3; 438 | LastSwiftMigration = 1020; 439 | ProvisioningStyle = Automatic; 440 | }; 441 | }; 442 | }; 443 | buildConfigurationList = E6D764241CDDBA100071CCB0 /* Build configuration list for PBXProject "Observatory" */; 444 | compatibilityVersion = "Xcode 12.0"; 445 | developmentRegion = English; 446 | hasScannedForEncodings = 0; 447 | knownRegions = ( 448 | English, 449 | Base, 450 | ); 451 | mainGroup = E6D764201CDDBA100071CCB0; 452 | packageReferences = ( 453 | E6FA8A8A267D08C200F0C878 /* XCRemoteSwiftPackageReference "Nimble" */, 454 | E6FA8A8C267D08D900F0C878 /* XCRemoteSwiftPackageReference "Quick" */, 455 | ); 456 | productRefGroup = E6D7642B1CDDBA100071CCB0 /* product */; 457 | projectDirPath = ""; 458 | projectRoot = ""; 459 | targets = ( 460 | 087EE7911E1C1FF500093E0E /* Demo */, 461 | E6D764291CDDBA100071CCB0 /* Observatory */, 462 | E69200F21CDDE19100C4FEAD /* Observatory-Test */, 463 | ); 464 | }; 465 | /* End PBXProject section */ 466 | 467 | /* Begin PBXResourcesBuildPhase section */ 468 | 087EE7901E1C1FF500093E0E /* Resources */ = { 469 | isa = PBXResourcesBuildPhase; 470 | buildActionMask = 2147483647; 471 | files = ( 472 | E609DC8B1E7BC8CA00F919DD /* demo.storyboard in Resources */, 473 | ); 474 | runOnlyForDeploymentPostprocessing = 0; 475 | }; 476 | /* End PBXResourcesBuildPhase section */ 477 | 478 | /* Begin PBXSourcesBuildPhase section */ 479 | 087EE78E1E1C1FF500093E0E /* Sources */ = { 480 | isa = PBXSourcesBuildPhase; 481 | buildActionMask = 2147483647; 482 | files = ( 483 | 17FD0162D167D444D8E5ED9A /* Entrypoint.swift in Sources */, 484 | ); 485 | runOnlyForDeploymentPostprocessing = 0; 486 | }; 487 | E69200EF1CDDE19100C4FEAD /* Sources */ = { 488 | isa = PBXSourcesBuildPhase; 489 | buildActionMask = 2147483647; 490 | files = ( 491 | 17FD0156D1758DD5200998BE /* Test.swift in Sources */, 492 | 17FD057B880D845188EA2DC8 /* Testing.Event.swift in Sources */, 493 | 17FD00154E7DD4027591C5D2 /* Testing.Observation.swift in Sources */, 494 | 17FD0C1A4329D5233941DDE4 /* Test.Keyboard.Hotkey.swift in Sources */, 495 | 17FD088C42F60A74F1C81846 /* Test.Keyboard.Key.swift in Sources */, 496 | 196807DF7B07BBFAF1E7BCB4 /* Test.Keyboard.Modifier.swift in Sources */, 497 | 17FD028A9B91583E857EF5EF /* Test.Observer.Event.swift in Sources */, 498 | 17FD09D3A526167E7A8F531D /* Test.Observer.Hotkey.swift in Sources */, 499 | 17FD01BE2C8AF0115AE9DF8E /* Test.Observer.Notification.swift in Sources */, 500 | 17FD0148328306B2C2A1C505 /* Test.Shortcut.swift in Sources */, 501 | ); 502 | runOnlyForDeploymentPostprocessing = 0; 503 | }; 504 | E6D764251CDDBA100071CCB0 /* Sources */ = { 505 | isa = PBXSourcesBuildPhase; 506 | buildActionMask = 2147483647; 507 | files = ( 508 | 17FD00CB84FB669705C7E473 /* Keyboard.Key.swift in Sources */, 509 | 17FD0A6270752A2959E61CF9 /* Keyboard.Hotkey.swift in Sources */, 510 | 17FD01D475A336AA93EC9F2F /* Keyboard.Modifier.swift in Sources */, 511 | 17FD08BD0A91FD8A01FBD698 /* Observer.swift in Sources */, 512 | 17FD0BCDD00D6AF24E42FBD6 /* Observer.HandlerDefinition.swift in Sources */, 513 | 17FD0328BCF34B7BF64C7670 /* Observer.Event.swift in Sources */, 514 | 17FD0A7B3145FFD47C972C40 /* Observer.Event.Handler.swift in Sources */, 515 | 17FD012B602860A1D459E05A /* Observer.Hotkey.swift in Sources */, 516 | 17FD0E596C7B56AEB4865F6A /* Observer.Hotkey.Handler.swift in Sources */, 517 | 17FD062C3246D204FE4BD93B /* Observer.Notification.swift in Sources */, 518 | 17FD066C556A49EB63E661CC /* Observer.Notification.Handler.swift in Sources */, 519 | 17FD019C5C7935EDF1E25882 /* Shortcut.swift in Sources */, 520 | 17FD093654260D5761C05F6E /* Shortcut.Center.swift in Sources */, 521 | 17FD08933236D7505E7FCF22 /* Shortcut.Recorder.swift in Sources */, 522 | 17FD0895BDF08CF3ED5FEA49 /* Shortcut.Recorder.Button.swift in Sources */, 523 | ); 524 | runOnlyForDeploymentPostprocessing = 0; 525 | }; 526 | /* End PBXSourcesBuildPhase section */ 527 | 528 | /* Begin PBXTargetDependency section */ 529 | 087EE7A31E1C26EE00093E0E /* PBXTargetDependency */ = { 530 | isa = PBXTargetDependency; 531 | target = E6D764291CDDBA100071CCB0 /* Observatory */; 532 | targetProxy = 087EE7A21E1C26EE00093E0E /* PBXContainerItemProxy */; 533 | }; 534 | E69200FA1CDDE19200C4FEAD /* PBXTargetDependency */ = { 535 | isa = PBXTargetDependency; 536 | target = E6D764291CDDBA100071CCB0 /* Observatory */; 537 | targetProxy = E69200F91CDDE19200C4FEAD /* PBXContainerItemProxy */; 538 | }; 539 | /* End PBXTargetDependency section */ 540 | 541 | /* Begin XCBuildConfiguration section */ 542 | 087EE79E1E1C1FF500093E0E /* Debug */ = { 543 | isa = XCBuildConfiguration; 544 | baseConfigurationReference = E690D111267D19F6002FD479 /* macOS-Application.xcconfig */; 545 | buildSettings = { 546 | CODE_SIGN_IDENTITY = "-"; 547 | CODE_SIGN_STYLE = Automatic; 548 | DEVELOPMENT_TEAM = ""; 549 | INFOPLIST_FILE = "$(inherited)/application.plist"; 550 | PROVISIONING_PROFILE_SPECIFIER = ""; 551 | }; 552 | name = Debug; 553 | }; 554 | 087EE79F1E1C1FF500093E0E /* Release */ = { 555 | isa = XCBuildConfiguration; 556 | baseConfigurationReference = E690D111267D19F6002FD479 /* macOS-Application.xcconfig */; 557 | buildSettings = { 558 | CODE_SIGN_IDENTITY = "-"; 559 | CODE_SIGN_STYLE = Automatic; 560 | DEVELOPMENT_TEAM = ""; 561 | INFOPLIST_FILE = "$(inherited)/application.plist"; 562 | PROVISIONING_PROFILE_SPECIFIER = ""; 563 | }; 564 | name = Release; 565 | }; 566 | E69200FB1CDDE19200C4FEAD /* Debug */ = { 567 | isa = XCBuildConfiguration; 568 | baseConfigurationReference = E690D115267D19F6002FD479 /* macOS-XCTest.xcconfig */; 569 | buildSettings = { 570 | INFOPLIST_FILE = "$(inherited)/test.plist"; 571 | MACOSX_DEPLOYMENT_TARGET = 10.15; 572 | PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).test"; 573 | }; 574 | name = Debug; 575 | }; 576 | E69200FC1CDDE19200C4FEAD /* Release */ = { 577 | isa = XCBuildConfiguration; 578 | baseConfigurationReference = E690D115267D19F6002FD479 /* macOS-XCTest.xcconfig */; 579 | buildSettings = { 580 | INFOPLIST_FILE = "$(inherited)/test.plist"; 581 | MACOSX_DEPLOYMENT_TARGET = 10.15; 582 | PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).test"; 583 | }; 584 | name = Release; 585 | }; 586 | E6D764301CDDBA100071CCB0 /* Debug */ = { 587 | isa = XCBuildConfiguration; 588 | baseConfigurationReference = E690D125267D1A73002FD479 /* Debug.xcconfig */; 589 | buildSettings = { 590 | DEBUG_INFORMATION_FORMAT = dwarf; 591 | GCC_TREAT_WARNINGS_AS_ERRORS = NO; 592 | INFOPLIST_FILE = accessory/info; 593 | MACOSX_DEPLOYMENT_TARGET = 10.13; 594 | PRODUCT_BUNDLE_IDENTIFIER = com.swifteroid.observatory; 595 | PRODUCT_NAME = "$(TARGET_NAME)"; 596 | SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; 597 | SWIFT_VERSION = 5.0; 598 | }; 599 | name = Debug; 600 | }; 601 | E6D764311CDDBA100071CCB0 /* Release */ = { 602 | isa = XCBuildConfiguration; 603 | baseConfigurationReference = E690D126267D1A73002FD479 /* Release.xcconfig */; 604 | buildSettings = { 605 | GCC_TREAT_WARNINGS_AS_ERRORS = NO; 606 | INFOPLIST_FILE = accessory/info; 607 | MACOSX_DEPLOYMENT_TARGET = 10.13; 608 | PRODUCT_BUNDLE_IDENTIFIER = com.swifteroid.observatory; 609 | PRODUCT_NAME = "$(TARGET_NAME)"; 610 | SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; 611 | SWIFT_VERSION = 5.0; 612 | }; 613 | name = Release; 614 | }; 615 | E6D764331CDDBA100071CCB0 /* Debug */ = { 616 | isa = XCBuildConfiguration; 617 | baseConfigurationReference = E690D112267D19F6002FD479 /* macOS-Framework.xcconfig */; 618 | buildSettings = { 619 | INFOPLIST_FILE = "$(inherited)/framework.plist"; 620 | MACH_O_TYPE = staticlib; 621 | }; 622 | name = Debug; 623 | }; 624 | E6D764341CDDBA100071CCB0 /* Release */ = { 625 | isa = XCBuildConfiguration; 626 | baseConfigurationReference = E690D112267D19F6002FD479 /* macOS-Framework.xcconfig */; 627 | buildSettings = { 628 | INFOPLIST_FILE = "$(inherited)/framework.plist"; 629 | MACH_O_TYPE = staticlib; 630 | }; 631 | name = Release; 632 | }; 633 | /* End XCBuildConfiguration section */ 634 | 635 | /* Begin XCConfigurationList section */ 636 | 087EE7A01E1C1FF500093E0E /* Build configuration list for PBXNativeTarget "Demo" */ = { 637 | isa = XCConfigurationList; 638 | buildConfigurations = ( 639 | 087EE79E1E1C1FF500093E0E /* Debug */, 640 | 087EE79F1E1C1FF500093E0E /* Release */, 641 | ); 642 | defaultConfigurationIsVisible = 0; 643 | defaultConfigurationName = Release; 644 | }; 645 | E69200FD1CDDE19200C4FEAD /* Build configuration list for PBXNativeTarget "Observatory-Test" */ = { 646 | isa = XCConfigurationList; 647 | buildConfigurations = ( 648 | E69200FB1CDDE19200C4FEAD /* Debug */, 649 | E69200FC1CDDE19200C4FEAD /* Release */, 650 | ); 651 | defaultConfigurationIsVisible = 0; 652 | defaultConfigurationName = Release; 653 | }; 654 | E6D764241CDDBA100071CCB0 /* Build configuration list for PBXProject "Observatory" */ = { 655 | isa = XCConfigurationList; 656 | buildConfigurations = ( 657 | E6D764301CDDBA100071CCB0 /* Debug */, 658 | E6D764311CDDBA100071CCB0 /* Release */, 659 | ); 660 | defaultConfigurationIsVisible = 0; 661 | defaultConfigurationName = Release; 662 | }; 663 | E6D764321CDDBA100071CCB0 /* Build configuration list for PBXNativeTarget "Observatory" */ = { 664 | isa = XCConfigurationList; 665 | buildConfigurations = ( 666 | E6D764331CDDBA100071CCB0 /* Debug */, 667 | E6D764341CDDBA100071CCB0 /* Release */, 668 | ); 669 | defaultConfigurationIsVisible = 0; 670 | defaultConfigurationName = Release; 671 | }; 672 | /* End XCConfigurationList section */ 673 | 674 | /* Begin XCRemoteSwiftPackageReference section */ 675 | E6FA8A8A267D08C200F0C878 /* XCRemoteSwiftPackageReference "Nimble" */ = { 676 | isa = XCRemoteSwiftPackageReference; 677 | repositoryURL = "https://github.com/Quick/Nimble.git"; 678 | requirement = { 679 | kind = upToNextMajorVersion; 680 | minimumVersion = 11.0.0; 681 | }; 682 | }; 683 | E6FA8A8C267D08D900F0C878 /* XCRemoteSwiftPackageReference "Quick" */ = { 684 | isa = XCRemoteSwiftPackageReference; 685 | repositoryURL = "https://github.com/Quick/Quick.git"; 686 | requirement = { 687 | kind = upToNextMajorVersion; 688 | minimumVersion = 5.0.0; 689 | }; 690 | }; 691 | /* End XCRemoteSwiftPackageReference section */ 692 | 693 | /* Begin XCSwiftPackageProductDependency section */ 694 | E6FA8A8B267D08C200F0C878 /* Nimble */ = { 695 | isa = XCSwiftPackageProductDependency; 696 | package = E6FA8A8A267D08C200F0C878 /* XCRemoteSwiftPackageReference "Nimble" */; 697 | productName = Nimble; 698 | }; 699 | E6FA8A8D267D08D900F0C878 /* Quick */ = { 700 | isa = XCSwiftPackageProductDependency; 701 | package = E6FA8A8C267D08D900F0C878 /* XCRemoteSwiftPackageReference "Quick" */; 702 | productName = Quick; 703 | }; 704 | /* End XCSwiftPackageProductDependency section */ 705 | }; 706 | rootObject = E6D764211CDDBA100071CCB0 /* Project object */; 707 | } 708 | --------------------------------------------------------------------------------