This commit is contained in:
Sindre Sorhus 2020-05-07 22:49:06 +08:00
commit affae1cd71
34 changed files with 3676 additions and 0 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

3
.github/funding.yml vendored Normal file
View File

@ -0,0 +1,3 @@
github: sindresorhus
open_collective: sindresorhus
custom: https://sindresorhus.com/donate

14
.github/workflows/jazzy.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Public docs
on:
release:
types: [published]
jobs:
deploy:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Publish docs
uses: steven0351/publish-jazzy-docs@v1
with:
personal_access_token: ${{ secrets.ACCESS_TOKEN }}
config: .jazzy.yml

14
.github/workflows/swiftlint.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: SwiftLint
on:
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/.build
/Packages
xcuserdata
project.xcworkspace

12
.jazzy.yml Normal file
View File

@ -0,0 +1,12 @@
module: KeyboardShortcuts
github_url: https://github.com/sindresorhus/KeyboardShortcuts
root_url: https://sindresorhus.com/KeyboardShortcuts
copyright: MIT License © [Sindre Sorhus](https://sindresorhus.com)
swift_build_tool: spm
build_tool_arguments:
- '-Xswiftc'
- '-swift-version'
- '-Xswiftc'
- '5'
theme: fullwidth
hide_documentation_coverage: true

187
.swiftlint.yml Normal file
View File

@ -0,0 +1,187 @@
whitelist_rules:
- anyobject_protocol
- array_init
- attributes
- block_based_kvo
- class_delegate_protocol
- closing_brace
- closure_end_indentation
- closure_parameter_position
- closure_spacing
- collection_alignment
- colon
- comma
- compiler_protocol_init
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- control_statement
- custom_rules
- deployment_target
- discarded_notification_center_observer
- discouraged_direct_init
- discouraged_object_literal
- discouraged_optional_boolean
- discouraged_optional_collection
- duplicate_enum_cases
- duplicate_imports
- dynamic_inline
- empty_collection_literal
- empty_count
- empty_enum_arguments
- empty_parameters
- empty_parentheses_with_trailing_closure
- empty_string
- empty_xctest_method
- enum_case_associated_value_count
- explicit_init
- fallthrough
- fatal_error_message
- first_where
- flatmap_over_map_reduce
- for_where
- generic_type_name
- identical_operands
- identifier_name
- implicit_getter
- implicit_return
- inert_defer
- is_disjoint
- joined_default_parameter
- last_where
- leading_whitespace
- legacy_cggeometry_functions
- legacy_constant
- legacy_constructor
- legacy_hashing
- legacy_multiple
- legacy_nsgeometry_functions
- legacy_random
- literal_expression_end_indentation
- lower_acl_than_parent
- mark
- modifier_order
- multiline_arguments
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- nimble_operator
- no_extension_access_modifier
- no_fallthrough_only
- no_space_in_method_call
- notification_center_detachment
- nsobject_prefer_isequal
- number_separator
- object_literal
- opening_brace
- operator_usage_whitespace
- operator_whitespace
- orphaned_doc_comment
- overridden_super_call
- pattern_matching_keywords
- prefer_self_type_over_type_of_self
- private_action
- private_outlet
- private_unit_test
- prohibited_nan_comparison
- prohibited_super_call
- protocol_property_accessors_order
- reduce_boolean
- reduce_into
- redundant_discardable_let
- redundant_nil_coalescing
- redundant_objc_attribute
- redundant_optional_initialization
- redundant_set_access_control
- redundant_string_enum_value
- redundant_type_annotation
- redundant_void_return
- required_enum_case
- return_value_from_void_function
- return_arrow_whitespace
- shorthand_operator
- sorted_first_last
- statement_position
- static_operator
- strong_iboutlet
- superfluous_disable_command
- switch_case_alignment
- switch_case_on_newline
- syntactic_sugar
- toggle_bool
- trailing_closure
- trailing_comma
- trailing_newline
- trailing_semicolon
- trailing_whitespace
- tuple_pattern
- unavailable_function
- unneeded_break_in_switch
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- unused_capture_list
- unused_closure_parameter
- unused_control_flow_label
- unused_enumerated
- unused_optional_binding
- unused_setter_value
- valid_ibinspectable
- vertical_parameter_alignment
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
- void_return
- weak_delegate
- xct_specific_matcher
- xctfail_message
- yoda_condition
analyzer_rules:
- unused_declaration
- unused_import
force_cast: warning
force_try: warning
force_unwrapping: warning
number_separator:
minimum_length: 5
object_literal:
image_literal: false
discouraged_object_literal:
color_literal: false
identifier_name:
max_length:
warning: 100
error: 100
min_length:
warning: 2
error: 2
validates_start_with_lowercase: false
allowed_symbols:
- '_'
excluded:
- 'x'
- 'y'
- 'a'
- 'b'
- 'x1'
- 'x2'
- 'y1'
- 'y2'
deployment_target:
macOS_deployment_target: '10.11'
custom_rules:
no_nsrect:
regex: '\bNSRect\b'
match_kinds: typeidentifier
message: 'Use CGRect instead of NSRect'
no_nssize:
regex: '\bNSSize\b'
match_kinds: typeidentifier
message: 'Use CGSize instead of NSSize'
no_nspoint:
regex: '\bNSPoint\b'
match_kinds: typeidentifier
message: 'Use CGPoint instead of NSPoint'

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

14
KeyboardShortcuts.podspec Normal file
View File

@ -0,0 +1,14 @@
Pod::Spec.new do |s|
s.name = 'KeyboardShortcuts'
s.version = '0.0.0'
s.summary = 'Add user-customizable global keyboard shortcuts to your macOS app in minutes'
s.license = 'MIT'
s.homepage = 'https://github.com/sindresorhus/KeyboardShortcuts'
s.social_media_url = 'https://twitter.com/sindresorhus'
s.authors = { 'Sindre Sorhus' => 'sindresorhus@gmail.com' }
s.source = { :git => 'https://github.com/sindresorhus/KeyboardShortcuts.git', :tag => "v#{s.version}" }
s.source_files = 'Sources/**/*.swift'
s.swift_version = '5.2'
s.platform = :macos, '10.11'
s.weak_framework = 'Combine'
end

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@ -0,0 +1,692 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objects = {
/* Begin PBXBuildFile section */
E38103FE246449180023E9A8 /* Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38103FD246449180023E9A8 /* Name.swift */; };
E3BF5627245C23840024D9BF /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF5626245C23840024D9BF /* Recorder.swift */; };
E3BF5629245C24450024D9BF /* RecorderCocoa.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF5628245C24450024D9BF /* RecorderCocoa.swift */; };
E3BF562B245C28BD0024D9BF /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF562A245C28BD0024D9BF /* Shortcut.swift */; };
E3BF562D245C29C30024D9BF /* CarbonKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF562C245C29C30024D9BF /* CarbonKeyboardShortcuts.swift */; };
E3BF5635245C2BB30024D9BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF5634245C2BB30024D9BF /* AppDelegate.swift */; };
E3BF5637245C2BB30024D9BF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF5636245C2BB30024D9BF /* ContentView.swift */; };
E3BF5639245C2BB50024D9BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E3BF5638245C2BB50024D9BF /* Assets.xcassets */; };
E3BF563F245C2BB50024D9BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E3BF563D245C2BB50024D9BF /* Main.storyboard */; };
E3BF5646245C2D550024D9BF /* KeyboardShortcuts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "KeyboardShortcuts::KeyboardShortcuts::Product" /* KeyboardShortcuts.framework */; };
E3BF5647245C2D550024D9BF /* KeyboardShortcuts.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = "KeyboardShortcuts::KeyboardShortcuts::Product" /* KeyboardShortcuts.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E3BF564C245C34B30024D9BF /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BF564B245C34B30024D9BF /* Key.swift */; };
OBJ_23 /* KeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* KeyboardShortcuts.swift */; };
OBJ_24 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* util.swift */; };
OBJ_31 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
E3BF5648245C2D550024D9BF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = OBJ_1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = "KeyboardShortcuts::KeyboardShortcuts";
remoteInfo = KeyboardShortcuts;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
E3BF564A245C2D550024D9BF /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
E3BF5647245C2D550024D9BF /* KeyboardShortcuts.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
E38103FD246449180023E9A8 /* Name.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Name.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF5626245C23840024D9BF /* Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Recorder.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF5628245C24450024D9BF /* RecorderCocoa.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RecorderCocoa.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF562A245C28BD0024D9BF /* Shortcut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Shortcut.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF562C245C29C30024D9BF /* CarbonKeyboardShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CarbonKeyboardShortcuts.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF5632245C2BB30024D9BF /* KeyboardShortcutsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeyboardShortcutsExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
E3BF5634245C2BB30024D9BF /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF5636245C2BB30024D9BF /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ContentView.swift; sourceTree = "<group>"; usesTabs = 1; };
E3BF5638245C2BB50024D9BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
E3BF563E245C2BB50024D9BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
E3BF5640245C2BB50024D9BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E3BF5641245C2BB50024D9BF /* KeyboardShortcutsExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KeyboardShortcutsExample.entitlements; sourceTree = "<group>"; };
E3BF564B245C34B30024D9BF /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Key.swift; sourceTree = "<group>"; usesTabs = 1; };
"KeyboardShortcuts::KeyboardShortcuts::Product" /* KeyboardShortcuts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeyboardShortcuts.framework; sourceTree = BUILT_PRODUCTS_DIR; };
OBJ_10 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = "<group>"; usesTabs = 1; };
OBJ_16 /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; lineEnding = 0; path = readme.md; sourceTree = "<group>"; usesTabs = 1; };
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; lineEnding = 0; path = Package.swift; sourceTree = "<group>"; usesTabs = 1; };
OBJ_9 /* KeyboardShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = KeyboardShortcuts.swift; sourceTree = "<group>"; usesTabs = 1; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
E3BF562F245C2BB30024D9BF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E3BF5646245C2D550024D9BF /* KeyboardShortcuts.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_25 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
E3BF5633245C2BB30024D9BF /* KeyboardShortcutsExample */ = {
isa = PBXGroup;
children = (
E3BF5634245C2BB30024D9BF /* AppDelegate.swift */,
E3BF5636245C2BB30024D9BF /* ContentView.swift */,
E3BF5638245C2BB50024D9BF /* Assets.xcassets */,
E3BF563D245C2BB50024D9BF /* Main.storyboard */,
E3BF5640245C2BB50024D9BF /* Info.plist */,
E3BF5641245C2BB50024D9BF /* KeyboardShortcutsExample.entitlements */,
);
path = KeyboardShortcutsExample;
sourceTree = "<group>";
};
E3BF5645245C2D550024D9BF /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
OBJ_11 /* Tests */ = {
isa = PBXGroup;
children = (
);
name = Tests;
sourceTree = SOURCE_ROOT;
};
OBJ_12 /* Products */ = {
isa = PBXGroup;
children = (
"KeyboardShortcuts::KeyboardShortcuts::Product" /* KeyboardShortcuts.framework */,
E3BF5632245C2BB30024D9BF /* KeyboardShortcutsExample.app */,
);
name = Products;
sourceTree = BUILT_PRODUCTS_DIR;
};
OBJ_5 = {
isa = PBXGroup;
children = (
OBJ_16 /* readme.md */,
OBJ_6 /* Package.swift */,
OBJ_7 /* Sources */,
OBJ_11 /* Tests */,
E3BF5633245C2BB30024D9BF /* KeyboardShortcutsExample */,
OBJ_12 /* Products */,
E3BF5645245C2D550024D9BF /* Frameworks */,
);
sourceTree = "<group>";
};
OBJ_7 /* Sources */ = {
isa = PBXGroup;
children = (
OBJ_8 /* KeyboardShortcuts */,
);
name = Sources;
sourceTree = SOURCE_ROOT;
};
OBJ_8 /* KeyboardShortcuts */ = {
isa = PBXGroup;
children = (
OBJ_9 /* KeyboardShortcuts.swift */,
E3BF562C245C29C30024D9BF /* CarbonKeyboardShortcuts.swift */,
E38103FD246449180023E9A8 /* Name.swift */,
E3BF564B245C34B30024D9BF /* Key.swift */,
E3BF562A245C28BD0024D9BF /* Shortcut.swift */,
E3BF5628245C24450024D9BF /* RecorderCocoa.swift */,
E3BF5626245C23840024D9BF /* Recorder.swift */,
OBJ_10 /* util.swift */,
);
name = KeyboardShortcuts;
path = Sources/KeyboardShortcuts;
sourceTree = SOURCE_ROOT;
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
E3BF5631245C2BB30024D9BF /* KeyboardShortcutsExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = E3BF5642245C2BB50024D9BF /* Build configuration list for PBXNativeTarget "KeyboardShortcutsExample" */;
buildPhases = (
E381040124645AC80023E9A8 /* SwiftLint */,
E3BF562E245C2BB30024D9BF /* Sources */,
E3BF562F245C2BB30024D9BF /* Frameworks */,
E3BF5630245C2BB30024D9BF /* Resources */,
E3BF564A245C2D550024D9BF /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
E3BF5649245C2D550024D9BF /* PBXTargetDependency */,
);
name = KeyboardShortcutsExample;
productName = KeyboardShortcutsExample;
productReference = E3BF5632245C2BB30024D9BF /* KeyboardShortcutsExample.app */;
productType = "com.apple.product-type.application";
};
"KeyboardShortcuts::KeyboardShortcuts" /* KeyboardShortcuts */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_19 /* Build configuration list for PBXNativeTarget "KeyboardShortcuts" */;
buildPhases = (
E38103FF24645A410023E9A8 /* SwiftLint */,
OBJ_22 /* Sources */,
OBJ_25 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = KeyboardShortcuts;
productName = KeyboardShortcuts;
productReference = "KeyboardShortcuts::KeyboardShortcuts::Product" /* KeyboardShortcuts.framework */;
productType = "com.apple.product-type.framework";
};
"KeyboardShortcuts::SwiftPMPackageDescription" /* KeyboardShortcutsPackageDescription */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_27 /* Build configuration list for PBXNativeTarget "KeyboardShortcutsPackageDescription" */;
buildPhases = (
OBJ_30 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = KeyboardShortcutsPackageDescription;
productName = KeyboardShortcutsPackageDescription;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
OBJ_1 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftMigration = 9999;
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 9999;
TargetAttributes = {
E3BF5631245C2BB30024D9BF = {
CreatedOnToolsVersion = 11.4.1;
};
};
};
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "KeyboardShortcuts" */;
compatibilityVersion = "Xcode 11.4";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = OBJ_5;
productRefGroup = OBJ_12 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
"KeyboardShortcuts::KeyboardShortcuts" /* KeyboardShortcuts */,
"KeyboardShortcuts::SwiftPMPackageDescription" /* KeyboardShortcutsPackageDescription */,
E3BF5631245C2BB30024D9BF /* KeyboardShortcutsExample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
E3BF5630245C2BB30024D9BF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E3BF563F245C2BB50024D9BF /* Main.storyboard in Resources */,
E3BF5639245C2BB50024D9BF /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
E38103FF24645A410023E9A8 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftLint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed\"\nfi\n";
};
E381040124645AC80023E9A8 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftLint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
E3BF562E245C2BB30024D9BF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E3BF5637245C2BB30024D9BF /* ContentView.swift in Sources */,
E3BF5635245C2BB30024D9BF /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_22 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_23 /* KeyboardShortcuts.swift in Sources */,
E3BF562D245C29C30024D9BF /* CarbonKeyboardShortcuts.swift in Sources */,
OBJ_24 /* util.swift in Sources */,
E3BF5629245C24450024D9BF /* RecorderCocoa.swift in Sources */,
E3BF564C245C34B30024D9BF /* Key.swift in Sources */,
E3BF5627245C23840024D9BF /* Recorder.swift in Sources */,
E38103FE246449180023E9A8 /* Name.swift in Sources */,
E3BF562B245C28BD0024D9BF /* Shortcut.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_30 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_31 /* Package.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
E3BF5649245C2D550024D9BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = "KeyboardShortcuts::KeyboardShortcuts" /* KeyboardShortcuts */;
targetProxy = E3BF5648245C2D550024D9BF /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
E3BF563D245C2BB50024D9BF /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
E3BF563E245C2BB50024D9BF /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
E3BF5643245C2BB50024D9BF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = KeyboardShortcutsExample/KeyboardShortcutsExample.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = KeyboardShortcutsExample/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.KeyboardShortcutsExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
E3BF5644245C2BB50024D9BF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = KeyboardShortcutsExample/KeyboardShortcutsExample.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = KeyboardShortcutsExample/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.KeyboardShortcutsExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
OBJ_20 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = KeyboardShortcuts.xcodeproj/KeyboardShortcuts_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
);
MARKETING_VERSION = 0.0.0;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = KeyboardShortcuts;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = KeyboardShortcuts;
TVOS_DEPLOYMENT_TARGET = 9.0;
WATCHOS_DEPLOYMENT_TARGET = 2.0;
};
name = Debug;
};
OBJ_21 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = KeyboardShortcuts.xcodeproj/KeyboardShortcuts_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
);
MARKETING_VERSION = 0.0.0;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = KeyboardShortcuts;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = KeyboardShortcuts;
TVOS_DEPLOYMENT_TARGET = 9.0;
WATCHOS_DEPLOYMENT_TARGET = 2.0;
};
name = Release;
};
OBJ_28 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
OBJ_29 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0";
SWIFT_VERSION = 5.0;
};
name = Release;
};
OBJ_3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_ARC = YES;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_NS_ASSERTIONS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"SWIFT_PACKAGE=1",
"DEBUG=1",
);
MACOSX_DEPLOYMENT_TARGET = 10.11;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,
);
OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
USE_HEADERMAP = NO;
VALID_ARCHS = x86_64;
};
name = Debug;
};
OBJ_4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_ARC = YES;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_OPTIMIZATION_LEVEL = s;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"SWIFT_PACKAGE=1",
);
MACOSX_DEPLOYMENT_TARGET = 10.11;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,
);
OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
USE_HEADERMAP = NO;
VALID_ARCHS = x86_64;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
E3BF5642245C2BB50024D9BF /* Build configuration list for PBXNativeTarget "KeyboardShortcutsExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E3BF5643245C2BB50024D9BF /* Debug */,
E3BF5644245C2BB50024D9BF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_19 /* Build configuration list for PBXNativeTarget "KeyboardShortcuts" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_20 /* Debug */,
OBJ_21 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_2 /* Build configuration list for PBXProject "KeyboardShortcuts" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_3 /* Debug */,
OBJ_4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_27 /* Build configuration list for PBXNativeTarget "KeyboardShortcutsPackageDescription" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_28 /* Debug */,
OBJ_29 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = OBJ_1 /* Project object */;
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "9999"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "KeyboardShortcuts::KeyboardShortcuts"
BuildableName = "KeyboardShortcuts.framework"
BlueprintName = "KeyboardShortcuts"
ReferencedContainer = "container:KeyboardShortcuts.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E3BF5631245C2BB30024D9BF"
BuildableName = "KeyboardShortcutsExample.app"
BlueprintName = "KeyboardShortcutsExample"
ReferencedContainer = "container:KeyboardShortcuts.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E3BF5631245C2BB30024D9BF"
BuildableName = "KeyboardShortcutsExample.app"
BlueprintName = "KeyboardShortcutsExample"
ReferencedContainer = "container:KeyboardShortcuts.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E3BF5631245C2BB30024D9BF"
BuildableName = "KeyboardShortcutsExample.app"
BlueprintName = "KeyboardShortcutsExample"
ReferencedContainer = "container:KeyboardShortcuts.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,29 @@
import Cocoa
import SwiftUI
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ notification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: CGRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [
.titled,
.closable,
.miniaturizable,
.fullSizeContentView
],
backing: .buffered,
defer: false
)
window.title = "KeyboardShortcuts Example"
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}

View File

@ -0,0 +1,58 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,337 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="KeyboardShortcutsExample" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="KeyboardShortcutsExample" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About KeyboardShortcutsExample" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide KeyboardShortcutsExample" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit KeyboardShortcutsExample" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="KeyboardShortcutsExample Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="KeyboardShortcutsExample" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,52 @@
import SwiftUI
import KeyboardShortcuts
extension KeyboardShortcuts.Name {
static let testShortcut1 = Name("testShortcut1")
static let testShortcut2 = Name("testShortcut2")
}
struct ContentView: View {
@State private var isPressed1 = false
@State private var isPressed2 = false
var body: some View {
VStack {
HStack {
KeyboardShortcuts.Recorder(for: .testShortcut1)
Text("Is pressed? \(isPressed1 ? "Yes" : "No")")
.frame(width: 100, alignment: .leading)
}
HStack {
KeyboardShortcuts.Recorder(for: .testShortcut2)
Text("Is pressed? \(isPressed2 ? "Yes" : "No")")
.frame(width: 100, alignment: .leading)
}
}
.frame(maxWidth: 300)
.padding(60)
.onAppear {
KeyboardShortcuts.onKeyDown(for: .testShortcut1) {
self.isPressed1 = true
}
KeyboardShortcuts.onKeyUp(for: .testShortcut1) {
self.isPressed1 = false
}
KeyboardShortcuts.onKeyDown(for: .testShortcut2) {
self.isPressed2 = true
}
KeyboardShortcuts.onKeyUp(for: .testShortcut2) {
self.isPressed2 = false
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

22
Package.swift Normal file
View File

@ -0,0 +1,22 @@
// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "KeyboardShortcuts",
platforms: [
.macOS(.v10_11)
],
products: [
.library(
name: "KeyboardShortcuts",
targets: [
"KeyboardShortcuts"
]
)
],
targets: [
.target(
name: "KeyboardShortcuts"
)
]
)

View File

@ -0,0 +1,181 @@
import Carbon.HIToolbox
private func carbonKeyboardShortcutsEventHandler(eventHandlerCall: EventHandlerCallRef?, event: EventRef?, userData: UnsafeMutableRawPointer?) -> OSStatus {
CarbonKeyboardShortcuts.handleEvent(event)
}
final class CarbonKeyboardShortcuts {
private final class HotKey {
let shortcut: KeyboardShortcuts.Shortcut
let carbonHotKeyId: Int
let carbonHotKey: EventHotKeyRef
let onKeyDown: (KeyboardShortcuts.Shortcut) -> Void
let onKeyUp: (KeyboardShortcuts.Shortcut) -> Void
init(
shortcut: KeyboardShortcuts.Shortcut,
carbonHotKeyID: Int,
carbonHotKey: EventHotKeyRef,
onKeyDown: @escaping (KeyboardShortcuts.Shortcut) -> Void,
onKeyUp: @escaping (KeyboardShortcuts.Shortcut) -> Void
) {
self.shortcut = shortcut
self.carbonHotKeyId = carbonHotKeyID
self.carbonHotKey = carbonHotKey
self.onKeyDown = onKeyDown
self.onKeyUp = onKeyUp
}
}
private static var hotKeys = [Int: HotKey]()
// `SSKS` is just short for `Sindre Sorhus Keyboard Shortcuts`.
private static let hotKeySignature = UTGetOSTypeFromString("SSKS" as CFString)
private static var hotKeyId = 0
private static var eventHandler: EventHandlerRef?
private static func setUpEventHandlerIfNeeded() {
guard
eventHandler == nil,
let dispatcher = GetEventDispatcherTarget()
else {
return
}
let eventSpecs = [
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)),
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased))
]
InstallEventHandler(
dispatcher,
carbonKeyboardShortcutsEventHandler,
eventSpecs.count,
eventSpecs,
nil,
&eventHandler
)
}
static func register(
_ shortcut: KeyboardShortcuts.Shortcut,
onKeyDown: @escaping (KeyboardShortcuts.Shortcut) -> Void,
onKeyUp: @escaping (KeyboardShortcuts.Shortcut) -> Void
) {
hotKeyId += 1
var eventHotKey: EventHotKeyRef?
let registerError = RegisterEventHotKey(
UInt32(shortcut.carbonKeyCode),
UInt32(shortcut.carbonModifiers),
EventHotKeyID(signature: hotKeySignature, id: UInt32(hotKeyId)),
GetEventDispatcherTarget(),
0,
&eventHotKey
)
guard
registerError == noErr,
let carbonHotKey = eventHotKey
else {
return
}
hotKeys[hotKeyId] = HotKey(
shortcut: shortcut,
carbonHotKeyID: hotKeyId,
carbonHotKey: carbonHotKey,
onKeyDown: onKeyDown,
onKeyUp: onKeyUp
)
setUpEventHandlerIfNeeded()
}
private static func unregisterHotKey(_ hotKey: HotKey) {
UnregisterEventHotKey(hotKey.carbonHotKey)
hotKeys.removeValue(forKey: hotKey.carbonHotKeyId)
}
static func unregister(_ shortcut: KeyboardShortcuts.Shortcut) {
for hotKey in hotKeys.values where hotKey.shortcut == shortcut {
unregisterHotKey(hotKey)
}
}
static func unregisterAll() {
for hotKey in hotKeys.values {
unregisterHotKey(hotKey)
}
}
fileprivate static func handleEvent(_ event: EventRef?) -> OSStatus {
guard let event = event else {
return OSStatus(eventNotHandledErr)
}
var eventHotKeyId = EventHotKeyID()
let error = GetEventParameter(
event,
UInt32(kEventParamDirectObject),
UInt32(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&eventHotKeyId
)
guard error == noErr else {
return error
}
guard
eventHotKeyId.signature == hotKeySignature,
let hotKey = hotKeys[Int(eventHotKeyId.id)]
else {
return OSStatus(eventNotHandledErr)
}
switch Int(GetEventKind(event)) {
case kEventHotKeyPressed:
hotKey.onKeyDown(hotKey.shortcut)
return noErr
case kEventHotKeyReleased:
hotKey.onKeyUp(hotKey.shortcut)
return noErr
default:
break
}
return OSStatus(eventNotHandledErr)
}
}
extension CarbonKeyboardShortcuts {
static var system: [KeyboardShortcuts.Shortcut] {
var shortcutsUnmanaged: Unmanaged<CFArray>?
guard
CopySymbolicHotKeys(&shortcutsUnmanaged) == noErr,
let shortcuts = shortcutsUnmanaged?.takeRetainedValue() as? [[String: Any]]
else {
assertionFailure("Could not get system keyboard shortcuts")
return []
}
return shortcuts.compactMap {
guard
($0[kHISymbolicHotKeyEnabled] as? Bool) == true,
let carbonKeyCode = $0[kHISymbolicHotKeyCode] as? Int,
let carbonModifiers = $0[kHISymbolicHotKeyModifiers] as? Int
else {
return nil
}
return KeyboardShortcuts.Shortcut(
carbonKeyCode: carbonKeyCode,
carbonModifiers: carbonModifiers
)
}
}
}

View File

@ -0,0 +1,743 @@
import Cocoa
import Carbon.HIToolbox
extension KeyboardShortcuts {
// swiftlint:disable identifier_name
/// Represents a key on the keyboard.
public enum Key: RawRepresentable {
case a
case b
case c
case d
case e
case f
case g
case h
case i
case j
case k
case l
case m
case n
case o
case p
case q
case r
case s
case t
case u
case v
case w
case x
case y
case z
case zero
case one
case two
case three
case four
case five
case six
case seven
case eight
case nine
case backslash
case backtick
case comma
case equal
case escape
case leftBracket
case minus
case period
case quote
case rightBracket
case semicolon
case slash
case space
// `NSEvent.SpecialKey`
case backTab
case backspace
case begin
case `break`
case carriageReturn
case clearDisplay
case clearLine
case delete
case deleteCharacter
case deleteForward
case deleteLine
case downArrow
case end
case enter
case execute
case f1
case f2
case f3
case f4
case f5
case f6
case f7
case f8
case f9
case f10
case f11
case f12
case f13
case f14
case f15
case f16
case f17
case f18
case f19
case f20
case f21
case f22
case f23
case f24
case f25
case f26
case f27
case f28
case f29
case f30
case f31
case f32
case f33
case f34
case f35
case find
case formFeed
case help
case home
case insert
case insertCharacter
case insertLine
case leftArrow
case lineSeparator
case menu
case modeSwitch
case newline
case next
case pageDown
case pageUp
case paragraphSeparator
case pause
case prev
case print
case printScreen
case redo
case reset
case rightArrow
case scrollLock
case select
case stop
case sysReq
case system
case tab
case undo
case upArrow
case user
// swiftlint:enable identifier_name
/// Create a `Key` from a key code.
public init?(rawValue: Int) {
switch rawValue {
case kVK_ANSI_A:
self = .a
case kVK_ANSI_B:
self = .b
case kVK_ANSI_C:
self = .c
case kVK_ANSI_D:
self = .d
case kVK_ANSI_E:
self = .e
case kVK_ANSI_F:
self = .f
case kVK_ANSI_G:
self = .g
case kVK_ANSI_H:
self = .h
case kVK_ANSI_I:
self = .i
case kVK_ANSI_J:
self = .j
case kVK_ANSI_K:
self = .k
case kVK_ANSI_L:
self = .l
case kVK_ANSI_M:
self = .m
case kVK_ANSI_N:
self = .n
case kVK_ANSI_O:
self = .o
case kVK_ANSI_P:
self = .p
case kVK_ANSI_Q:
self = .q
case kVK_ANSI_R:
self = .r
case kVK_ANSI_S:
self = .s
case kVK_ANSI_T:
self = .t
case kVK_ANSI_U:
self = .u
case kVK_ANSI_V:
self = .v
case kVK_ANSI_W:
self = .w
case kVK_ANSI_X:
self = .x
case kVK_ANSI_Y:
self = .y
case kVK_ANSI_Z:
self = .z
case kVK_ANSI_0:
self = .zero
case kVK_ANSI_1:
self = .one
case kVK_ANSI_2:
self = .two
case kVK_ANSI_3:
self = .three
case kVK_ANSI_4:
self = .four
case kVK_ANSI_5:
self = .five
case kVK_ANSI_6:
self = .six
case kVK_ANSI_7:
self = .seven
case kVK_ANSI_8:
self = .eight
case kVK_ANSI_9:
self = .nine
case kVK_ANSI_Backslash:
self = .backslash
case kVK_ANSI_Grave:
self = .backtick
case kVK_ANSI_Comma:
self = .comma
case kVK_ANSI_Equal:
self = .equal
case kVK_Escape:
self = .escape
case kVK_ANSI_LeftBracket:
self = .leftBracket
case kVK_ANSI_Minus:
self = .minus
case kVK_ANSI_Period:
self = .period
case kVK_ANSI_Quote:
self = .quote
case kVK_ANSI_RightBracket:
self = .rightBracket
case kVK_ANSI_Semicolon:
self = .semicolon
case kVK_ANSI_Slash:
self = .slash
case kVK_Space:
self = .space
default:
self.init(specialKey: .init(rawValue: rawValue))
}
}
private init?(specialKey: NSEvent.SpecialKey) {
switch specialKey {
case .backTab:
self = .backTab
case .backspace:
self = .backspace
case .begin:
self = .begin
case .break:
self = .break
case .carriageReturn:
self = .carriageReturn
case .clearDisplay:
self = .clearDisplay
case .clearLine:
self = .clearLine
case .delete:
self = .delete
case .deleteCharacter:
self = .deleteCharacter
case .deleteForward:
self = .deleteForward
case .deleteLine:
self = .deleteLine
case .downArrow:
self = .downArrow
case .end:
self = .end
case .enter:
self = .enter
case .execute:
self = .execute
case .f1:
self = .f1
case .f2:
self = .f2
case .f3:
self = .f3
case .f4:
self = .f4
case .f5:
self = .f5
case .f6:
self = .f6
case .f7:
self = .f7
case .f8:
self = .f8
case .f9:
self = .f9
case .f10:
self = .f10
case .f11:
self = .f11
case .f12:
self = .f12
case .f13:
self = .f13
case .f14:
self = .f14
case .f15:
self = .f15
case .f16:
self = .f16
case .f17:
self = .f17
case .f18:
self = .f18
case .f19:
self = .f19
case .f20:
self = .f20
case .f21:
self = .f21
case .f22:
self = .f22
case .f23:
self = .f23
case .f24:
self = .f24
case .f25:
self = .f25
case .f26:
self = .f26
case .f27:
self = .f27
case .f28:
self = .f28
case .f29:
self = .f29
case .f30:
self = .f30
case .f31:
self = .f31
case .f32:
self = .f32
case .f33:
self = .f33
case .f34:
self = .f34
case .f35:
self = .f35
case .find:
self = .find
case .formFeed:
self = .formFeed
case .help:
self = .help
case .home:
self = .home
case .insert:
self = .insert
case .insertCharacter:
self = .insertCharacter
case .insertLine:
self = .insertLine
case .leftArrow:
self = .leftArrow
case .lineSeparator:
self = .lineSeparator
case .menu:
self = .menu
case .modeSwitch:
self = .modeSwitch
case .newline:
self = .newline
case .next:
self = .next
case .pageDown:
self = .pageDown
case .pageUp:
self = .pageUp
case .paragraphSeparator:
self = .paragraphSeparator
case .pause:
self = .pause
case .prev:
self = .prev
case .print:
self = .print
case .printScreen:
self = .printScreen
case .redo:
self = .redo
case .reset:
self = .reset
case .rightArrow:
self = .rightArrow
case .scrollLock:
self = .scrollLock
case .select:
self = .select
case .stop:
self = .stop
case .sysReq:
self = .sysReq
case .system:
self = .system
case .tab:
self = .tab
case .undo:
self = .undo
case .upArrow:
self = .upArrow
case .user:
self = .user
default:
return nil
}
}
private var specialKey: NSEvent.SpecialKey? {
switch self {
case .backTab:
return .backTab
case .backspace:
return .backspace
case .begin:
return .begin
case .break:
return .break
case .carriageReturn:
return .carriageReturn
case .clearDisplay:
return .clearDisplay
case .clearLine:
return .clearLine
case .delete:
return .delete
case .deleteCharacter:
return .deleteCharacter
case .deleteForward:
return .deleteForward
case .deleteLine:
return .deleteLine
case .downArrow:
return .downArrow
case .end:
return .end
case .enter:
return .enter
case .execute:
return .execute
case .f1:
return .f1
case .f2:
return .f2
case .f3:
return .f3
case .f4:
return .f4
case .f5:
return .f5
case .f6:
return .f6
case .f7:
return .f7
case .f8:
return .f8
case .f9:
return .f9
case .f10:
return .f10
case .f11:
return .f11
case .f12:
return .f12
case .f13:
return .f13
case .f14:
return .f14
case .f15:
return .f15
case .f16:
return .f16
case .f17:
return .f17
case .f18:
return .f18
case .f19:
return .f19
case .f20:
return .f20
case .f21:
return .f21
case .f22:
return .f22
case .f23:
return .f23
case .f24:
return .f24
case .f25:
return .f25
case .f26:
return .f26
case .f27:
return .f27
case .f28:
return .f28
case .f29:
return .f29
case .f30:
return .f30
case .f31:
return .f31
case .f32:
return .f32
case .f33:
return .f33
case .f34:
return .f34
case .f35:
return .f35
case .find:
return .find
case .formFeed:
return .formFeed
case .help:
return .help
case .home:
return .home
case .insert:
return .insert
case .insertCharacter:
return .insertCharacter
case .insertLine:
return .insertLine
case .leftArrow:
return .leftArrow
case .lineSeparator:
return .lineSeparator
case .menu:
return .menu
case .modeSwitch:
return .modeSwitch
case .newline:
return .newline
case .next:
return .next
case .pageDown:
return .pageDown
case .pageUp:
return .pageUp
case .paragraphSeparator:
return .paragraphSeparator
case .pause:
return .pause
case .prev:
return .prev
case .print:
return .print
case .printScreen:
return .printScreen
case .redo:
return .redo
case .reset:
return .reset
case .rightArrow:
return .rightArrow
case .scrollLock:
return .scrollLock
case .select:
return .select
case .stop:
return .stop
case .sysReq:
return .sysReq
case .system:
return .system
case .tab:
return .tab
case .undo:
return .undo
case .upArrow:
return .upArrow
case .user:
return .user
default:
return nil
}
}
/// The raw key code.
public var rawValue: Int {
switch self {
case .a:
return kVK_ANSI_A
case .b:
return kVK_ANSI_B
case .c:
return kVK_ANSI_C
case .d:
return kVK_ANSI_D
case .e:
return kVK_ANSI_E
case .f:
return kVK_ANSI_F
case .g:
return kVK_ANSI_G
case .h:
return kVK_ANSI_H
case .i:
return kVK_ANSI_I
case .j:
return kVK_ANSI_J
case .k:
return kVK_ANSI_K
case .l:
return kVK_ANSI_L
case .m:
return kVK_ANSI_M
case .n:
return kVK_ANSI_N
case .o:
return kVK_ANSI_O
case .p:
return kVK_ANSI_P
case .q:
return kVK_ANSI_Q
case .r:
return kVK_ANSI_R
case .s:
return kVK_ANSI_S
case .t:
return kVK_ANSI_T
case .u:
return kVK_ANSI_U
case .v:
return kVK_ANSI_V
case .w:
return kVK_ANSI_W
case .x:
return kVK_ANSI_X
case .y:
return kVK_ANSI_Y
case .z:
return kVK_ANSI_Z
case .zero:
return kVK_ANSI_0
case .one:
return kVK_ANSI_1
case .two:
return kVK_ANSI_2
case .three:
return kVK_ANSI_3
case .four:
return kVK_ANSI_4
case .five:
return kVK_ANSI_5
case .six:
return kVK_ANSI_6
case .seven:
return kVK_ANSI_7
case .eight:
return kVK_ANSI_8
case .nine:
return kVK_ANSI_9
case .backslash:
return kVK_ANSI_Backslash
case .backtick:
return kVK_ANSI_Grave
case .comma:
return kVK_ANSI_Comma
case .equal:
return kVK_ANSI_Equal
case .escape:
return kVK_Escape
case .leftBracket:
return kVK_ANSI_LeftBracket
case .minus:
return kVK_ANSI_Minus
case .period:
return kVK_ANSI_Period
case .quote:
return kVK_ANSI_Quote
case .rightBracket:
return kVK_ANSI_RightBracket
case .semicolon:
return kVK_ANSI_Semicolon
case .slash:
return kVK_ANSI_Slash
case .space:
return kVK_Space
case .backTab, .backspace, .begin, .break, .carriageReturn, .clearDisplay, .clearLine, .delete, .deleteCharacter, .deleteForward, .deleteLine, .downArrow, .end, .enter, .execute, .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12, .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20, .f21, .f22, .f23, .f24, .f25, .f26, .f27, .f28, .f29, .f30, .f31, .f32, .f33, .f34, .f35, .find, .formFeed, .help, .home, .insert, .insertCharacter, .insertLine, .leftArrow, .lineSeparator, .menu, .modeSwitch, .newline, .next, .pageDown, .pageUp, .paragraphSeparator, .pause, .prev, .print, .printScreen, .redo, .reset, .rightArrow, .scrollLock, .select, .stop, .sysReq, .system, .tab, .undo, .upArrow, .user:
return specialKey?.rawValue ?? 0
}
}
}
}
extension KeyboardShortcuts.Key {
/// All the function keys.
static let functionKeys: Set<Self> = [
.f1,
.f2,
.f3,
.f4,
.f5,
.f6,
.f7,
.f8,
.f9,
.f10,
.f11,
.f12,
.f13,
.f14,
.f15,
.f16,
.f17,
.f18,
.f19,
.f20,
.f21,
.f22,
.f23,
.f24,
.f25,
.f26,
.f27,
.f28,
.f29,
.f30,
.f31,
.f32,
.f33,
.f34,
.f35
]
/// Returns true if the key is a function key. For example, `F1`.
var isFunctionKey: Bool { Self.functionKeys.contains(self) }
}

View File

@ -0,0 +1,201 @@
import Cocoa
/**
Global keyboard shortcuts for your macOS app.
*/
public final class KeyboardShortcuts {
/// :nodoc:
public typealias KeyAction = () -> Void
private static var registeredShortcuts = Set<Shortcut>()
// Not currently used. For the future.
private static var keyDownHandlers = [Shortcut: [KeyAction]]()
private static var keyUpHandlers = [Shortcut: [KeyAction]]()
private static var userDefaultsKeyDownHandlers = [Name: [KeyAction]]()
private static var userDefaultsKeyUpHandlers = [Name: [KeyAction]]()
private static func register(_ shortcut: Shortcut) {
guard !registeredShortcuts.contains(shortcut) else {
return
}
CarbonKeyboardShortcuts.register(
shortcut,
onKeyDown: handleOnKeyDown,
onKeyUp: handleOnKeyUp
)
registeredShortcuts.insert(shortcut)
}
private static func unregister(_ shortcut: Shortcut) {
CarbonKeyboardShortcuts.unregister(shortcut)
registeredShortcuts.remove(shortcut)
}
// TODO: Doc comment and make this public.
static func unregisterAll() {
CarbonKeyboardShortcuts.unregisterAll()
registeredShortcuts.removeAll()
// TODO: Should remove user defaults too.
}
// TODO: Also add `.isEnabled(_ name: Name)`.
static func disable(_ name: Name) {
guard let shortcut = userDefaultsGet(name: name) else {
return
}
unregister(shortcut)
}
static func enable(_ name: Name) {
guard let shortcut = userDefaultsGet(name: name) else {
return
}
register(shortcut)
}
private static func handleOnKeyDown(_ shortcut: Shortcut) {
if let handlers = keyDownHandlers[shortcut] {
for handler in handlers {
handler()
}
}
for (name, handlers) in userDefaultsKeyDownHandlers {
guard userDefaultsGet(name: name) == shortcut else {
continue
}
for handler in handlers {
handler()
}
}
}
private static func handleOnKeyUp(_ shortcut: Shortcut) {
if let handlers = keyUpHandlers[shortcut] {
for handler in handlers {
handler()
}
}
for (name, handlers) in userDefaultsKeyUpHandlers {
guard userDefaultsGet(name: name) == shortcut else {
continue
}
for handler in handlers {
handler()
}
}
}
/**
Listen to the keyboard shortcut with the given name being pressed.
You can register multiple listeners.
You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do.
```
import Cocoa
import KeyboardShortcuts
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
KeyboardShortcuts.onKeyDown(for: .toggleUnicornMode) {
self.isUnicornMode.toggle()
}
}
}
```
*/
public static func onKeyDown(for name: Name, action: @escaping KeyAction) {
if userDefaultsKeyDownHandlers[name] == nil {
userDefaultsKeyDownHandlers[name] = []
}
userDefaultsKeyDownHandlers[name]?.append(action)
// If the keyboard shortcut already exist, we register it.
if let shortcut = userDefaultsGet(name: name) {
register(shortcut)
}
}
/**
Listen to the keyboard shortcut with the given name being pressed.
You can register multiple listeners.
You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do.
```
import Cocoa
import KeyboardShortcuts
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) {
self.isUnicornMode.toggle()
}
}
}
```
*/
public static func onKeyUp(for name: Name, action: @escaping KeyAction) {
if userDefaultsKeyUpHandlers[name] == nil {
userDefaultsKeyUpHandlers[name] = []
}
userDefaultsKeyUpHandlers[name]?.append(action)
// If the keyboard shortcut already exist, we register it.
if let shortcut = userDefaultsGet(name: name) {
register(shortcut)
}
}
private static let userDefaultsPrefix = "KeyboardShortcuts_"
private static func userDefaultsKey(for shortcutName: Name) -> String { "\(userDefaultsPrefix)\(shortcutName.rawValue)"
}
// TODO: Should these be on `Shortcut` instead?
static func userDefaultsGet(name: Name) -> Shortcut? {
guard
let data = UserDefaults.standard.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Shortcut.self, from: data)
else {
return nil
}
return decoded
}
static func userDefaultsSet(name: Name, shortcut: Shortcut) {
guard let encoded = try? JSONEncoder().encode(shortcut).string else {
return
}
UserDefaults.standard.set(encoded, forKey: userDefaultsKey(for: name))
register(shortcut)
}
static func userDefaultsRemove(name: Name) {
guard let shortcut = userDefaultsGet(name: name) else {
return
}
UserDefaults.standard.removeObject(forKey: userDefaultsKey(for: name))
unregister(shortcut)
}
}

View File

@ -0,0 +1,33 @@
extension KeyboardShortcuts {
/**
The strongly-typed name of the keyboard shortcut.
After registering it, you can use it in, for example, `KeyboardShortcut.Recorder` and `KeyboardShortcut.onKeyUp()`.
```
import KeyboardShortcuts
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Name("toggleUnicornMode")
}
```
*/
public struct Name: Hashable {
// This is to allow `extension KeyboardShortcuts.Name { static let x = Name("x") }`.
/// :nodoc:
public typealias Name = KeyboardShortcuts.Name
public let rawValue: String
public init(_ name: String) {
self.rawValue = name
}
}
}
extension KeyboardShortcuts.Name: RawRepresentable {
/// :nodoc:
public init?(rawValue: String) {
self.init(rawValue)
}
}

View File

@ -0,0 +1,51 @@
import SwiftUI
@available(macOS 10.15, *)
extension KeyboardShortcuts {
/**
A SwiftUI `View` that lets the user record a keyboard shortcut.
You would usually put this in your preferences window.
It automatically prevents choosing a keyboard shortcut that is already taken by the system or by the app's main menu by showing a user-friendly alert to the user.
It takes care of storing the keyboard shortcut in `UserDefaults` for you.
```
import SwiftUI
import KeyboardShortcuts
struct PreferencesView: View {
var body: some View {
HStack {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
```
*/
public struct Recorder: NSViewRepresentable { // swiftlint:disable:this type_name
/// :nodoc:
public typealias NSViewType = RecorderCocoa
private let name: Name
public init(for name: Name) {
self.name = name
}
/// :nodoc:
public func makeNSView(context: Context) -> NSViewType { .init(for: name) }
/// :nodoc:
public func updateNSView(_ nsView: NSViewType, context: Context) {}
}
}
@available(macOS 10.15, *)
struct SwiftUI_Previews: PreviewProvider {
static var previews: some View {
KeyboardShortcuts.Recorder(for: .Name("xcodePreview"))
}
}

View File

@ -0,0 +1,168 @@
import Cocoa
import Carbon.HIToolbox
extension KeyboardShortcuts {
/**
A `NSView` that lets the user record a keyboard shortcut.
You would usually put this in your preferences window.
It automatically prevents choosing a keyboard shortcut that is already taken by the system or by the app's main menu by showing a user-friendly alert to the user.
It takes care of storing the keyboard shortcut in `UserDefaults` for you.
```
import Cocoa
import KeyboardShortcuts
final class PreferencesViewController: NSViewController {
override func loadView() {
view = NSView()
let recorder = KeyboardShortcuts.RecorderCocoa(for: .toggleUnicornMode)
view.addSubview(recorder)
}
}
```
*/
public final class RecorderCocoa: NSSearchField, NSSearchFieldDelegate {
private let minimumWidth: Double = 130
private var eventMonitor: LocalEventMonitor?
private let shortcutName: Name
/// :nodoc:
override public var canBecomeKeyView: Bool { false }
/// :nodoc:
override public var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width = CGFloat(minimumWidth)
return size
}
public required init(for name: Name) {
self.shortcutName = name
super.init(frame: .zero)
self.delegate = self
self.placeholderString = "Click to Record"
self.centersPlaceholder = true
self.alignment = .center
(self.cell as? NSSearchFieldCell)?.searchButtonCell = nil
if let shortcut = userDefaultsGet(name: shortcutName) {
self.stringValue = "\(shortcut)"
}
self.wantsLayer = true
self.translatesAutoresizingMaskIntoConstraints = false
self.setContentHuggingPriority(.defaultHigh, for: .vertical)
self.setContentHuggingPriority(.defaultHigh, for: .horizontal)
self.widthAnchor.constraint(greaterThanOrEqualToConstant: CGFloat(minimumWidth)).isActive = true
}
@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// :nodoc:
public func controlTextDidChange(_ object: Notification) {
if stringValue.isEmpty {
userDefaultsRemove(name: shortcutName)
}
}
/// :nodoc:
public func controlTextDidEndEditing(_ object: Notification) {
eventMonitor = nil
placeholderString = "Click to Record"
}
/// :nodoc:
override public func becomeFirstResponder() -> Bool {
let shouldBecomeFirstResponder = super.becomeFirstResponder()
guard shouldBecomeFirstResponder else {
return shouldBecomeFirstResponder
}
placeholderString = "Press Shortcut"
hideCaret()
eventMonitor = LocalEventMonitor(events: [.keyDown]) { [weak self] event in
guard let self = self else {
return nil
}
if
event.modifiers.isEmpty,
event.specialKey == .tab
{
return event
}
if
event.modifiers.isEmpty,
event.keyCode == kVK_Escape // TODO: Make this strongly typed.
{
self.blur()
return nil
}
if
event.modifiers.isEmpty &&
(
event.specialKey == .delete ||
event.specialKey == .deleteForward ||
event.specialKey == .backspace
)
{
self.clear()
return nil
}
guard
(
!event.modifiers.isEmpty ||
event.specialKey?.isFunctionKey == true
),
let shortcut = Shortcut(event: event)
else {
NSSound.beep()
return nil
}
if let menuItem = shortcut.takenByMainMenu {
NSAlert.showModal(
for: self.window,
message: "This keyboard shortcut cannot be used as it's already used by the “\(menuItem.title)” menu item."
)
return nil
}
guard !shortcut.isTakenBySystem else {
NSAlert.showModal(
for: self.window,
message: "This keyboard shortcut cannot be used as it's already a system-wide keyboard shortcut.",
// TODO: Add button to offer to open the relevant system preference pane for the user.
informativeText: "Most system-wide keyboard shortcuts can be changed in “System Preferences Keyboard Shortcuts“."
)
return nil
}
self.stringValue = "\(shortcut)"
userDefaultsSet(name: self.shortcutName, shortcut: shortcut)
self.blur()
return nil
}.start()
return shouldBecomeFirstResponder
}
private func blur() {
window?.makeFirstResponder(nil)
}
}
}

View File

@ -0,0 +1,190 @@
import Cocoa
import Carbon.HIToolbox
extension KeyboardShortcuts {
/// A keyboard shortcut.
public struct Shortcut: Hashable, Codable {
/// Carbon modifiers are not always stored as the same number.
/// For example, the system has `F2` stored with the modifiers number `135168`, but if you press the keyboard shortcut, you get `4096`.
private static func normalizeModifiers(_ carbonModifiers: Int) -> Int {
NSEvent.ModifierFlags(carbon: carbonModifiers).carbon
}
public let carbonKeyCode: Int
public let carbonModifiers: Int
public var key: Key? { Key(rawValue: carbonKeyCode) }
public var modifiers: NSEvent.ModifierFlags { NSEvent.ModifierFlags(carbon: carbonModifiers) }
/// Create a shortcut from a key code number and modifier code.
public init(carbonKeyCode: Int, carbonModifiers: Int = 0) {
self.carbonKeyCode = carbonKeyCode
self.carbonModifiers = Self.normalizeModifiers(carbonModifiers)
}
/// Create a keyboard shortcut from a strongly-typed key and modifiers.
public init(_ key: Key, modifiers: NSEvent.ModifierFlags = []) {
self.init(
carbonKeyCode: key.rawValue,
carbonModifiers: modifiers.carbon
)
}
/// Create a keyboard shortcut from a key event.
public init?(event: NSEvent) {
guard event.isKeyEvent else {
return nil
}
self.init(
carbonKeyCode: Int(event.keyCode),
carbonModifiers: event.modifierFlags.carbon
)
}
}
}
extension KeyboardShortcuts.Shortcut {
/// System-defined keyboard shortcuts.
static var system: [Self] { CarbonKeyboardShortcuts.system }
/// Check whether the keyboard shortcut is already taken by the system.
var isTakenBySystem: Bool { Self.system.contains(self) }
}
extension KeyboardShortcuts.Shortcut {
/// Recursively finds a menu item in the given menu that has a matching key equivalent and modifier.
func menuItemWithMatchingShortcut(in menu: NSMenu) -> NSMenuItem? {
for item in menu.items {
if
keyToCharacter() == item.keyEquivalent,
modifiers == item.keyEquivalentModifierMask
{
return item
}
if
let submenu = item.submenu,
let menuItem = menuItemWithMatchingShortcut(in: submenu)
{
return menuItem
}
}
return nil
}
/// Returns a menu item in the app's main menu that has a matching key equivalent and modifier.
var takenByMainMenu: NSMenuItem? {
guard let mainMenu = NSApp.mainMenu else {
return nil
}
return menuItemWithMatchingShortcut(in: mainMenu)
}
}
private var keyToCharacterMapping: [KeyboardShortcuts.Key: String] = [
.carriageReturn: "",
.delete: "",
.deleteForward: "",
.downArrow: "",
.end: "",
.escape: "",
.help: "?⃝",
.home: "",
.leftArrow: "",
.pageDown: "",
.pageUp: "",
.rightArrow: "",
.space: "Space",
.tab: "",
.upArrow: "",
.f1: "F1",
.f2: "F2",
.f3: "F3",
.f4: "F4",
.f5: "F5",
.f6: "F6",
.f7: "F7",
.f8: "F8",
.f9: "F9",
.f10: "F10",
.f11: "F11",
.f12: "F12",
.f13: "F13",
.f14: "F14",
.f15: "F15",
.f16: "F16",
.f17: "F17",
.f18: "F18",
.f19: "F19",
.f20: "F20",
.f21: "F21",
.f22: "F22",
.f23: "F23",
.f24: "F24",
.f25: "F25",
.f26: "F26",
.f27: "F27",
.f28: "F28",
.f29: "F29",
.f30: "F30",
.f31: "F31",
.f32: "F32",
.f33: "F33",
.f34: "F34",
.f35: "F35"
]
extension KeyboardShortcuts.Shortcut: CustomStringConvertible {
fileprivate func keyToCharacter() -> String? {
// Some characters cannot be automatically translated.
if
let key = key,
let character = keyToCharacterMapping[key]
{
return character
}
let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue()
let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
let dataRef = unsafeBitCast(layoutData, to: CFData.self)
let keyLayout = unsafeBitCast(CFDataGetBytePtr(dataRef), to: UnsafePointer<CoreServices.UCKeyboardLayout>.self)
var deadKeyState: UInt32 = 0
let maxCharacters = 256
var length = 0
var characters = [UniChar](repeating: 0, count: maxCharacters)
let error = CoreServices.UCKeyTranslate(
keyLayout,
UInt16(carbonKeyCode),
UInt16(CoreServices.kUCKeyActionDisplay),
UInt32(carbonModifiers),
UInt32(LMGetKbdType()),
OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit),
&deadKeyState,
maxCharacters,
&length,
&characters
)
guard error == noErr else {
return nil
}
return String(utf16CodeUnits: characters, count: length)
}
/**
The string representation of the keyboard shortcut.
```
print(Shortcut(.a, modifiers: [.command]))
//=> "A"
```
*/
public var description: String {
modifiers.description + (keyToCharacter()?.uppercased() ?? "<EFBFBD>")
}
}

View File

@ -0,0 +1,276 @@
import Cocoa
import Carbon.HIToolbox
extension Data {
var string: String? { String(data: self, encoding: .utf8) }
}
extension NSEvent {
var isKeyEvent: Bool { type == .keyDown || type == .keyUp }
}
extension NSTextField {
func hideCaret() {
(currentEditor() as? NSTextView)?.insertionPointColor = .clear
}
}
/**
Listen to local events.
- Important: Don't foret to call `.start()`.
```
eventMonitor = LocalEventMonitor(events: [.leftMouseDown, .rightMouseDown]) { event in
// Do something
return event
}.start()
```
*/
final class LocalEventMonitor {
private let events: NSEvent.EventTypeMask
private let callback: (NSEvent) -> NSEvent?
private weak var monitor: AnyObject?
init(events: NSEvent.EventTypeMask, callback: @escaping (NSEvent) -> NSEvent?) {
self.events = events
self.callback = callback
}
deinit {
stop()
}
@discardableResult
func start() -> Self {
monitor = NSEvent.addLocalMonitorForEvents(matching: events, handler: callback) as AnyObject
return self
}
func stop() {
guard let monitor = monitor else {
return
}
NSEvent.removeMonitor(monitor)
}
}
extension NSEvent {
static var modifiers: ModifierFlags {
modifierFlags
.intersection(.deviceIndependentFlagsMask)
// We remove `capsLock` as it shouldn't affect the modifiers.
// We remove `numericPad`/`function` as arrow keys trigger it, use `event.specialKeys` instead.
.subtracting([.capsLock, .numericPad, .function])
}
/**
Real modifiers.
- Note: Prefer this over `.modifierFlags`.
```
// Check if Command is one of possible more modifiers keys
event.modifiers.contains(.command)
// Check if Command is the only modifier key
event.modifiers == .command
// Check if Command and Shift are the only modifiers
event.modifiers == [.command, .shift]
```
*/
var modifiers: ModifierFlags {
modifierFlags
.intersection(.deviceIndependentFlagsMask)
// We remove `capsLock` as it shouldn't affect the modifiers.
// We remove `numericPad`/`function` as arrow keys trigger it, use `event.specialKeys` instead.
.subtracting([.capsLock, .numericPad, .function])
}
}
extension NSSearchField {
/// Clear the search field.
func clear() {
(cell as? NSSearchFieldCell)?.cancelButtonCell?.performClick(self)
}
}
extension NSAlert {
/// Show an alert as a window-modal sheet, or as an app-modal (window-independent) alert if the window is `nil` or not given.
@discardableResult
static func showModal(
for window: NSWindow? = nil,
message: String,
informativeText: String? = nil,
style: Style = .warning,
icon: NSImage? = nil
) -> NSApplication.ModalResponse {
NSAlert(
message: message,
informativeText: informativeText,
style: style,
icon: icon
).runModal(for: window)
}
convenience init(
message: String,
informativeText: String? = nil,
style: Style = .warning,
icon: NSImage? = nil
) {
self.init()
self.messageText = message
self.alertStyle = style
self.icon = icon
if let informativeText = informativeText {
self.informativeText = informativeText
}
}
/// Runs the alert as a window-modal sheet, or as an app-modal (window-independent) alert if the window is `nil` or not given.
@discardableResult
func runModal(for window: NSWindow? = nil) -> NSApplication.ModalResponse {
guard let window = window else {
return runModal()
}
beginSheetModal(for: window) { returnCode in
NSApp.stopModal(withCode: returnCode)
}
return NSApp.runModal(for: window)
}
}
extension NSEvent.ModifierFlags {
var carbon: Int {
var modifierFlags = 0
if contains(.control) {
modifierFlags |= controlKey
}
if contains(.option) {
modifierFlags |= optionKey
}
if contains(.shift) {
modifierFlags |= shiftKey
}
if contains(.command) {
modifierFlags |= cmdKey
}
return modifierFlags
}
init(carbon: Int) {
self.init()
if carbon & controlKey == controlKey {
insert(.control)
}
if carbon & optionKey == optionKey {
insert(.option)
}
if carbon & shiftKey == shiftKey {
insert(.shift)
}
if carbon & cmdKey == cmdKey {
insert(.command)
}
}
}
/// :nodoc:
extension NSEvent.ModifierFlags: CustomStringConvertible {
/**
The string representation of the modifier flags.
```
print(NSEvent.ModifierFlags([.command, .shift]))
//=> ""
```
*/
public var description: String {
var description = ""
if contains(.control) {
description += ""
}
if contains(.option) {
description += ""
}
if contains(.shift) {
description += ""
}
if contains(.command) {
description += ""
}
return description
}
}
extension NSEvent.SpecialKey {
static let functionKeys: Set<Self> = [
.f1,
.f2,
.f3,
.f4,
.f5,
.f6,
.f7,
.f8,
.f9,
.f10,
.f11,
.f12,
.f13,
.f14,
.f15,
.f16,
.f17,
.f18,
.f19,
.f20,
.f21,
.f22,
.f23,
.f24,
.f25,
.f26,
.f27,
.f28,
.f29,
.f30,
.f31,
.f32,
.f33,
.f34,
.f35
]
var isFunctionKey: Bool { Self.functionKeys.contains(self) }
}

9
license Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

156
readme.md Normal file
View File

@ -0,0 +1,156 @@
<div align="center">
<img width="900" src="logo.png" alt="KeyboardShortcuts">
<br>
</div>
This package lets you add support for user-customizable global keyboard shortcuts to your macOS app in minutes. It's fully sandbox and Mac App Store compatible. And it's used in production by [Dato](https://sindresorhus.com/dato), [Jiffy](https://sindresorhus.com/jiffy), and [Lungo](https://sindresorhus.com/lungo).
This package is still in its early days. I'm happy to accept more configurability and features. PR welcome! What you see here is just what I needed for my own apps.
<img src="screenshot.png" width="532">
## Requirements
macOS 10.11+
## Install
#### Swift Package Manager
```swift
.package(url: "https://github.com/sindresorhus/KeyboardShortcuts", from: "0.0.0")
```
You need to set the build setting “Other Linker Flags” to `-weak_framework Combine` to work around [this Xcode bug](https://github.com/feedback-assistant/reports/issues/44).
#### Carthage
```
github "sindresorhus/KeyboardShortcuts"
```
#### CocoaPods
```ruby
pod 'KeyboardShortcuts'
```
## Usage
First, register a name for the keyboard shortcut.
`Constants.swift`
```swift
import KeyboardShortcuts
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Name("toggleUnicornMode")
}
```
You can then refer to this strongly-typed name in other places.
You will want to make a view where the user can choose a keyboard shortcut.
`PreferencesView.swift`
```swift
import SwiftUI
import KeyboardShortcuts
struct PreferencesView: View {
var body: some View {
HStack {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
```
*There's also [support for Cocoa](#cocoa) instead of SwiftUI.*
`KeyboardShortcuts.Recorder` takes care of storing the keyboard shortcut in `UserDefaults` and also warning the user if the chosen keyboard shortcut is already used by the system or the app's main menu.
Add a listener for when the user presses their chosen keyboard shortcut.
`AppDelegate.swift`
```swift
import Cocoa
import KeyboardShortcuts
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) {
// The user pressed the keyboard shortcut for “unicorn mode”!
self.isUnicornMode.toggle()
}
}
}
```
*You can also listen to key down with `.onKeyDown()`*
**That's all! ✨**
You can find a complete example by opening `KeyboardShortcuts.xcodeproj` and then running the `KeyboardShortcutsExample` target.
#### Cocoa
Use [`KeyboardShortcuts.RecorderCocoa`](Sources/KeyboardShortcuts/RecorderCocoa.swift) instead of `KeyboardShortcuts.Recorder`.
```swift
import Cocoa
import KeyboardShortcuts
final class PreferencesViewController: NSViewController {
override func loadView() {
view = NSView()
let recorder = KeyboardShortcuts.RecorderCocoa(for: .toggleUnicornMode)
view.addSubview(recorder)
}
}
```
## API
[See the API docs.](https://sindresorhus.com/KeyboardShortcuts)
## FAQ
#### How is it different from [`MASShortcut`](https://github.com/shpakovski/MASShortcut)?
This package:
- Written in Swift with a swifty API.
- More native-looking UI component.
- SwiftUI component included.
- Support for listening to key down, not just key up.
- Swift Package Manager support.
`MASShortcut`:
- More mature.
- More features.
- Localized.
#### How is it different from [`HotKey`](https://github.com/soffes/HotKey)?
`HotKey` is good for adding hard-coded keyboard shortcuts, but it doesn't provide any UI component for the user to choose their own keyboard shortcuts.
#### Why is this package importing `Carbon`? Isn't that deprecated?
Most of the Carbon APIs were deprecated years ago, but there are some left that Apple never shipped modern replacements for. This includes registering global keyboard shortcuts. However, you should not need to worry about this. Apple will for sure ship new APIs before deprecating the Carbon APIs used here.
#### Does this package cause any permission dialogs?
No.
## Related
- [Preferences](https://github.com/sindresorhus/Preferences) - Add a preferences window to your macOS app in minutes
- [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB