├── .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 |
4 |
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 |
4 |
5 |
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 |
6 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
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 |
--------------------------------------------------------------------------------