├── .github
├── CODEOWNERS
└── workflows
│ ├── Swift-Build.yml
│ ├── Xcode-Build.yml
│ └── Danger.yml
├── Gemfile
├── Example
├── Example
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Example.entitlements
│ ├── AppDelegate.swift
│ ├── ViewController.swift
│ ├── Info.plist
│ └── Base.lproj
│ │ └── Main.storyboard
├── Example.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ └── IDETemplateMacros.plist
│ └── project.pbxproj
└── ExampleTests
│ ├── Info.plist
│ └── ExampleTests.swift
├── Dangerfile
├── Lib
├── Sauce.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ ├── IDETemplateMacros.plist
│ │ └── xcschemes
│ │ │ └── Sauce.xcscheme
│ └── project.pbxproj
├── Sauce
│ ├── Sauce.h
│ ├── TISInputSource+Property.swift
│ ├── NSMenuItem+Key.swift
│ ├── Info.plist
│ ├── ModifierTransformer.swift
│ ├── InputSource.swift
│ ├── Sauce.swift
│ ├── SpecialKeyCode.swift
│ ├── KeyboardLayout.swift
│ └── Key.swift
└── SauceTests
│ ├── Info.plist
│ ├── NSMenuItem+KeyTests.swift
│ └── KeyboardLayoutTests.swift
├── Sauce.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sauce.podspec
├── Package.swift
├── .swiftlint.yml
├── LICENSE
├── README.md
├── .gitignore
└── Gemfile.lock
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @Econa77
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'cocoapods'
4 | gem "danger"
5 | gem "danger-swiftlint"
6 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Dangerfile:
--------------------------------------------------------------------------------
1 | github.dismiss_out_of_range_messages
2 | swiftlint.config_file = '.swiftlint.yml'
3 | swiftlint.binary_path = '.mint/bin/swiftlint'
4 | swiftlint.lint_files(inline_mode: true)
5 |
--------------------------------------------------------------------------------
/Lib/Sauce.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sauce.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sauce.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Lib/Sauce.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/Example.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/Swift-Build.yml:
--------------------------------------------------------------------------------
1 | name: Swift-Build
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | runs-on: ${{ matrix.os }}
6 | strategy:
7 | matrix:
8 | os: [macos-12, macos-13]
9 | xcode: ['14.2', '15.2']
10 | exclude:
11 | - os: macos-12
12 | xcode: '15.2'
13 | env:
14 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Build and Test
18 | run: swift test
19 |
--------------------------------------------------------------------------------
/Example/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | //
4 | // Example
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | import Cocoa
12 |
13 | @NSApplicationMain
14 | final class AppDelegate: NSObject, NSApplicationDelegate {
15 |
16 | // MARK: - NSApplication Delegate
17 | func applicationDidFinishLaunching(_ aNotification: Notification) {}
18 |
19 | func applicationWillTerminate(_ aNotification: Notification) {}
20 |
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Lib/Sauce/Sauce.h:
--------------------------------------------------------------------------------
1 | //
2 | // Sauce.h
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #import
12 |
13 | //! Project version number for Sauce.
14 | FOUNDATION_EXPORT double SauceVersionNumber;
15 |
16 | //! Project version string for Sauce.
17 | FOUNDATION_EXPORT const unsigned char SauceVersionString[];
18 |
19 | // In this header, you should import all the public headers of your framework using statements like #import
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Lib/Sauce/TISInputSource+Property.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TISInputSource+Property.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 |
15 | extension TISInputSource {
16 | func value(forProperty propertyKey: CFString, type: T.Type) -> T? {
17 | guard let value = TISGetInputSourceProperty(self, propertyKey) else { return nil }
18 | return Unmanaged.fromOpaque(value).takeUnretainedValue() as? T
19 | }
20 | }
21 | #endif
22 |
--------------------------------------------------------------------------------
/Sauce.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "Sauce"
3 | s.version = "2.4.1"
4 | s.summary = "Mapping various keyboard layout sources and key codes in macOS. (e.g.: QWERTY, Dvorak)"
5 | s.license = { :type => 'MIT', :file => 'LICENSE' }
6 | s.homepage = "https://github.com/Clipy/Sauce"
7 | s.author = { "Econa77" => "s.f.1992.ip@gmail.com" }
8 | s.source = { :git => "https://github.com/Clipy/Sauce.git", :tag => "v#{s.version}" }
9 | s.platform = :osx, '10.13'
10 | s.source_files = 'Lib/Sauce/**/*.swift'
11 | s.swift_version = '5.0'
12 | s.frameworks = 'Carbon', 'Cocoa'
13 | end
14 |
--------------------------------------------------------------------------------
/Lib/Sauce.xcodeproj/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___FILENAME___
8 | //
9 | // ___PACKAGENAME___
10 | // GitHub: ___REPOSITORYURL___
11 | // HP: ___PRODUCTURL___
12 | //
13 | // Copyright © 2015-___YEAR___ Clipy Project.
14 | //
15 | REPOSITORYURL
16 | https://github.com/clipy
17 | PRODUCTURL
18 | https://clipy-app.com
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___FILENAME___
8 | //
9 | // ___PACKAGENAME___
10 | // GitHub: ___REPOSITORYURL___
11 | // HP: ___PRODUCTURL___
12 | //
13 | // Copyright © 2015-___YEAR___ Clipy Project.
14 | //
15 | REPOSITORYURL
16 | https://github.com/clipy
17 | PRODUCTURL
18 | https://clipy-app.com
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Sauce",
7 | platforms: [
8 | .macOS(.v10_13)
9 | ],
10 | products: [
11 | .library(
12 | name: "Sauce",
13 | targets: ["Sauce"]),
14 | ],
15 | targets: [
16 | .target(
17 | name: "Sauce",
18 | dependencies: [],
19 | path: "Lib/Sauce"),
20 | .testTarget(
21 | name: "SauceTests",
22 | dependencies: ["Sauce"],
23 | path: "Lib/SauceTests"),
24 | ],
25 | swiftLanguageVersions: [.v5]
26 | )
27 |
--------------------------------------------------------------------------------
/Lib/Sauce/NSMenuItem+Key.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sauce.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import AppKit
13 |
14 | extension NSMenuItem {
15 | /// A `Key` instance that corresponds to the menu item's shortcut.
16 | public var key: Key? {
17 | // Prefer the shortcut overrides by the user (from "System Preferences" > "Keyboard")
18 | // to the developer's default.
19 | let keyEquivalent = !self.userKeyEquivalent.isEmpty
20 | ? self.userKeyEquivalent
21 | : self.keyEquivalent
22 | return Key(character: keyEquivalent, virtualKeyCode: nil)
23 | }
24 | }
25 | #endif
26 |
--------------------------------------------------------------------------------
/Lib/SauceTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/ExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Lib/SauceTests/NSMenuItem+KeyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSMenuItem+KeyTests.swift
3 | //
4 | // SauceTests
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2021 Clipy Project.
9 | //
10 |
11 | import XCTest
12 | import AppKit
13 | @testable import Sauce
14 |
15 | class NSMenuItem_KeyTests: XCTestCase {
16 | func testKeyConversionIgnoresCharacterCase() {
17 | let menuItem = NSMenuItem()
18 | menuItem.keyEquivalentModifierMask = .command
19 |
20 | menuItem.keyEquivalent = "b"
21 | XCTAssertEqual(menuItem.key, .b)
22 |
23 | menuItem.keyEquivalent = "B"
24 | XCTAssertEqual(menuItem.key, .b)
25 |
26 | menuItem.keyEquivalentModifierMask = .shift
27 | XCTAssertEqual(menuItem.key, .b)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/Example/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | //
4 | // Example
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | import Cocoa
12 | import Sauce
13 |
14 | class ViewController: NSViewController {
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | _ = Sauce.shared
19 | }
20 |
21 | @IBAction func buttonTapped(_ sender: Any) {
22 | guard let keyCode = Sauce.shared.currentKeyCode(for: .v) else { return }
23 | print(keyCode)
24 | print(Sauce.shared.character(for: Int(keyCode), cocoaModifiers: []) as Any)
25 | print(Sauce.shared.character(for: Int(keyCode), cocoaModifiers: .shift) as Any)
26 | print(Sauce.shared.key(for: Int(keyCode)) as Any)
27 | }
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/Xcode-Build.yml:
--------------------------------------------------------------------------------
1 | name: Xcode-Build
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | runs-on: ${{ matrix.os }}
6 | strategy:
7 | matrix:
8 | os: [macos-12, macos-13]
9 | xcode: ['14.2', '15.2']
10 | exclude:
11 | - os: macos-12
12 | xcode: '15.2'
13 | env:
14 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Build and Test
18 | run: |
19 | set -o pipefail
20 | xcodebuild build-for-testing test-without-building \
21 | -workspace "$PROJECT" \
22 | -scheme "$SCHEME" \
23 | -sdk "$SDK" \
24 | -configuration Debug \
25 | ENABLE_TESTABILITY=YES | xcpretty -c;
26 | env:
27 | PROJECT: Sauce.xcworkspace
28 | SCHEME: Sauce
29 | SDK: macosx
30 |
--------------------------------------------------------------------------------
/Lib/Sauce/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2018年 Clipy Project. All rights reserved.
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included:
2 | - Lib/Sauce
3 | - Lib/SauceTests
4 | opt_in_rules:
5 | - empty_count
6 | - explicit_init
7 | - closure_spacing
8 | - overridden_super_call
9 | - redundant_nil_coalescing
10 | - private_outlet
11 | - nimble_operator
12 | - operator_usage_whitespace
13 | - closure_end_indentation
14 | - first_where
15 | - prohibited_super_call
16 | - vertical_parameter_alignment_on_call
17 | - unneeded_parentheses_in_closure_argument
18 | - extension_access_modifier
19 | - literal_expression_end_indentation
20 | - joined_default_parameter
21 | - contains_over_first_not_nil
22 | - override_in_extension
23 | - private_action
24 | - quick_discouraged_call
25 | - quick_discouraged_focused_test
26 | - quick_discouraged_pending_test
27 | - single_test_class
28 | - sorted_first_last
29 | - yoda_condition
30 | disabled_rules:
31 | - nesting
32 | - function_parameter_count
33 | - cyclomatic_complexity
34 | identifier_name:
35 | excluded:
36 | - id
37 | - i
38 | line_length: 300
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015-2020 Clipy Project
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Example/ExampleTests/ExampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleTests.swift
3 | //
4 | // ExampleTests
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | import XCTest
12 | @testable import Example
13 |
14 | class ExampleTests: XCTestCase {
15 |
16 | override func setUp() {
17 | super.setUp()
18 | // Put setup code here. This method is called before the invocation of each test method in the class.
19 | }
20 |
21 | override func tearDown() {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | super.tearDown()
24 | }
25 |
26 | func testExample() {
27 | // This is an example of a functional test case.
28 | // Use XCTAssert and related functions to verify your tests produce the correct results.
29 | }
30 |
31 | func testPerformanceExample() {
32 | // This is an example of a performance test case.
33 | self.measure {
34 | // Put the code you want to measure the time of here.
35 | }
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2018年 Clipy Project. All rights reserved.
27 | NSMainStoryboardFile
28 | Main
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/.github/workflows/Danger.yml:
--------------------------------------------------------------------------------
1 | name: Danger
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | danger:
9 | runs-on: macos-13
10 | # https://github.com/danger/danger/issues/1103
11 | if: (github.event.pull_request.head.repo.fork == false)
12 | steps:
13 | - uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | - name: Setup ruby
17 | uses: ruby/setup-ruby@v1
18 | with:
19 | ruby-version: '3.2'
20 | bundler-cache: true
21 | - name: Cache Mint
22 | uses: actions/cache@v3
23 | with:
24 | path: .mint
25 | key: ${{ runner.os }}-mint-${{ env.SWIFTLINT_VERSION }}
26 | restore-keys: |
27 | ${{ runner.os }}-mint-
28 | - name: Install Mint and Packages
29 | run: |
30 | brew install mint
31 | mint install realm/SwiftLint@${{ env.SWIFTLINT_VERSION }}
32 | - name: Run Danger
33 | run: bundle exec danger
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | env:
37 | MINT_PATH: .mint/lib
38 | MINT_LINK_PATH: .mint/bin
39 | SWIFTLINT_VERSION: 0.53.0
40 | DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
41 |
--------------------------------------------------------------------------------
/Lib/Sauce/ModifierTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModifierTransformer.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 | import AppKit
15 |
16 | open class ModifierTransformer {}
17 |
18 | // MARK: - Cocoa & Carbon
19 | extension ModifierTransformer {
20 | public func carbonFlags(from cocoaFlags: NSEvent.ModifierFlags) -> Int {
21 | var carbonFlags: Int = 0
22 | if cocoaFlags.contains(.command) {
23 | carbonFlags |= cmdKey
24 | }
25 | if cocoaFlags.contains(.option) {
26 | carbonFlags |= optionKey
27 | }
28 | if cocoaFlags.contains(.control) {
29 | carbonFlags |= controlKey
30 | }
31 | if cocoaFlags.contains(.shift) {
32 | carbonFlags |= shiftKey
33 | }
34 | return carbonFlags
35 | }
36 |
37 | public func convertCharactorSupportCarbonModifiers(from carbonModifiers: Int) -> Int {
38 | var convertedCarbonModifiers: Int = 0
39 | if (carbonModifiers & optionKey) != 0 {
40 | convertedCarbonModifiers |= optionKey
41 | }
42 | if (carbonModifiers & shiftKey) != 0 {
43 | convertedCarbonModifiers |= shiftKey
44 | }
45 | return convertedCarbonModifiers
46 | }
47 | }
48 | #endif
49 |
--------------------------------------------------------------------------------
/Lib/Sauce/InputSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputSource.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 |
15 | open class InputSource {
16 |
17 | // MARK: - Properties
18 | public let id: String
19 | public let modeID: String?
20 | public let isASCIICapable: Bool
21 | public let isEnableCapable: Bool
22 | public let isSelectCapable: Bool
23 | public let isEnabled: Bool
24 | public let isSelected: Bool
25 | public let localizedName: String?
26 | public let source: TISInputSource
27 |
28 | // MARK: - Initialize
29 | init(source: TISInputSource) {
30 | // There are some custom input sources that cannot get a SourceID, so if cannot get an SourceID, use a UUID.
31 | // ref: https://github.com/Clipy/Sauce/pull/40
32 | self.id = source.value(forProperty: kTISPropertyInputSourceID, type: String.self) ?? UUID().uuidString
33 | self.modeID = source.value(forProperty: kTISPropertyInputModeID, type: String.self)
34 | self.isASCIICapable = source.value(forProperty: kTISPropertyInputSourceIsASCIICapable, type: Bool.self) ?? false
35 | self.isEnableCapable = source.value(forProperty: kTISPropertyInputSourceIsEnableCapable, type: Bool.self) ?? false
36 | self.isSelectCapable = source.value(forProperty: kTISPropertyInputSourceIsSelectCapable, type: Bool.self) ?? false
37 | self.isEnabled = source.value(forProperty: kTISPropertyInputSourceIsEnabled, type: Bool.self) ?? false
38 | self.isSelected = source.value(forProperty: kTISPropertyInputSourceIsSelected, type: Bool.self) ?? false
39 | self.localizedName = source.value(forProperty: kTISPropertyLocalizedName, type: String.self)
40 | self.source = source
41 | }
42 |
43 | }
44 |
45 | // MARK: - Hashable
46 | extension InputSource: Hashable {
47 | public func hash(into hasher: inout Hasher) {
48 | hasher.combine(id)
49 | hasher.combine(modeID)
50 | }
51 | }
52 |
53 | // MARK: - Equatable
54 | extension InputSource: Equatable {
55 | public static func == (lhs: InputSource, rhs: InputSource) -> Bool {
56 | return lhs.id == rhs.id &&
57 | lhs.modeID == rhs.modeID
58 | }
59 | }
60 | #endif
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sauce
2 | 
3 | [](https://github.com/Clipy/Sauce/releases/latest)
4 | [](https://github.com/Clipy/Sauce/blob/master/LICENSE)
5 | [](https://github.com/Carthage/Carthage)
6 | [](https://cocoapods.org/pods/Sauce)
7 | [](https://cocoapods.org/pods/Sauce)
8 | [](https://swift.org/package-manager)
9 |
10 | Mapping various keyboard layout sources and key codes in macOS. (e.g.: QWERTY, Dvorak)
11 |
12 | ## Motivation
13 | Only the ANSI-standard US keyboard is defined for the key code defined in Carbon.framework. Therefore, we can obtain only the key code of the QWERTY keyboard layout. (e.g.: `kVK_ANSI_V`)
14 | In layout other than QWERTY, (e.g. Dvorak) the virtual key code is different.
15 |
16 | | Keyboard Layout | Key | Key Code |
17 | | :---------------: | :---: | :--------: |
18 | | QWERTY | v | 9 |
19 | | Dvorak | v | 47 |
20 |
21 | This library is created with the purpose of mapping the key code of the input sources and making it possible to obtain the correct key code in various keyboard layouts.
22 |
23 | ## Usage
24 | ### CocoaPods
25 | ```
26 | pod 'Sauce'
27 | ```
28 |
29 | ### Carthage
30 | ```
31 | github "Clipy/Sauce"
32 | ```
33 |
34 | ## Example
35 | ### Key codes
36 | Get the key code of the current input source.
37 |
38 | ```swift
39 | let keyCode = Sauce.shared.keyCode(for: .v)
40 | ```
41 |
42 | ### Key
43 | Get the `Key` of the current input source.
44 |
45 | ```swift
46 | let key = Sauce.shared.key(for: keyCode)
47 | ```
48 |
49 | ### Character
50 | Get the character of the current input source.
51 |
52 | ```swift
53 | let character = Sauce.shared.character(for: keyCode, carbonModifiers: shiftKey)
54 | let character = Sauce.shared.character(for: keyCode, cocoaModifiers: [.shift])
55 | ```
56 |
57 | ## Notification
58 | ### `NSNotification.Name.SauceEnabledKeyboardInputSourcesChanged`
59 | `SauceEnabledKeyboardInputSourcesChanged` is the same as `kTISNotifyEnabledKeyboardInputSourcesChanged` in Carbon.framework
60 |
61 | ### `NSNotification.Name.SauceSelectedKeyboardInputSourceChanged`
62 | `SauceSelectedKeyboardInputSourceChanged` is different from `kTISNotifySelectedKeyboardInputSourceChanged` and is notified only when the input source id has changed.
63 | Since it is filtered and notified, please do not use it for the same purpose as normal `kTISNotifySelectedKeyboardInputSourceChanged`.
64 |
65 | ### `NSNotification.Name.SauceSelectedKeyboardKeyCodesChanged`
66 | By using this Notification, can detect when the setting changes with the same keyboard layout, when the input source changes from QWERTY to Dvorak, and so on.
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/c68eb91e0a72ca1efba128c33de9caea41281f19/Global/Xcode.gitignore
2 |
3 | # Xcode
4 | #
5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
6 |
7 | ## Build generated
8 | build/
9 | DerivedData/
10 | Sauce.xcodeproj/
11 |
12 | ## Various settings
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata/
22 |
23 | ## Other
24 | *.moved-aside
25 | *.xccheckout
26 | *.xcscmblueprint
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 | *.ipa
31 | *.dSYM.zip
32 | *.dSYM
33 |
34 | ## Playgrounds
35 | timeline.xctimeline
36 | playground.xcworkspace
37 |
38 | # Swift Package Manager
39 | #
40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41 | # Packages/
42 | # Package.pins
43 | # Package.resolved
44 | .build/
45 |
46 | # CocoaPods
47 | #
48 | # We recommend against adding the Pods directory to your .gitignore. However
49 | # you should judge for yourself, the pros and cons are mentioned at:
50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
51 | #
52 | # Pods/
53 | #
54 | # Add this line if you want to avoid checking in source code from the Xcode workspace
55 | # *.xcworkspace
56 |
57 | # Carthage
58 | #
59 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
60 | # Carthage/Checkouts
61 |
62 | Carthage/Build
63 |
64 | # fastlane
65 | #
66 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
67 | # screenshots whenever they are needed.
68 | # For more information about the recommended setup visit:
69 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
70 |
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots/**/*.png
74 | fastlane/test_output
75 |
76 | # Code Injection
77 | #
78 | # After new code Injection tools there's a generated folder /iOSInjectionProject
79 | # https://github.com/johnno1962/injectionforxcode
80 |
81 | iOSInjectionProject/
82 |
83 |
84 |
85 | ### https://raw.github.com/github/gitignore/c68eb91e0a72ca1efba128c33de9caea41281f19/Ruby.gitignore
86 |
87 | *.gem
88 | *.rbc
89 | /.config
90 | /coverage/
91 | /InstalledFiles
92 | /pkg/
93 | /spec/reports/
94 | /spec/examples.txt
95 | /test/tmp/
96 | /test/version_tmp/
97 | /tmp/
98 |
99 | # Used by dotenv library to load environment variables.
100 | # .env
101 |
102 | ## Specific to RubyMotion:
103 | .dat*
104 | .repl_history
105 | build/
106 | *.bridgesupport
107 | build-iPhoneOS/
108 | build-iPhoneSimulator/
109 |
110 | ## Specific to RubyMotion (use of CocoaPods):
111 | #
112 | # We recommend against adding the Pods directory to your .gitignore. However
113 | # you should judge for yourself, the pros and cons are mentioned at:
114 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
115 | #
116 | # vendor/Pods/
117 |
118 | ## Documentation cache and generated files:
119 | /.yardoc/
120 | /_yardoc/
121 | /doc/
122 | /rdoc/
123 |
124 | ## Environment normalization:
125 | /.bundle/
126 | /vendor/bundle
127 | /lib/bundler/man/
128 |
129 | # for a library or gem, you might want to ignore these files since the code is
130 | # intended to run in multiple environments; otherwise, check them in:
131 | # Gemfile.lock
132 | # .ruby-version
133 | # .ruby-gemset
134 |
135 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
136 | .rvmrc
137 |
--------------------------------------------------------------------------------
/Lib/Sauce.xcodeproj/xcshareddata/xcschemes/Sauce.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
51 |
52 |
62 |
63 |
69 |
70 |
71 |
72 |
78 |
79 |
85 |
86 |
87 |
88 |
90 |
91 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/Lib/Sauce/Sauce.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sauce.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import AppKit
14 |
15 | extension NSNotification.Name {
16 | static let SauceSelectedKeyboardInputSourceChanged = Notification.Name("SauceSelectedKeyboardInputSourceChanged")
17 | static let SauceEnabledKeyboardInputSourcesChanged = Notification.Name("SauceEnabledKeyboardInputSourcesChanged")
18 | static let SauceSelectedKeyboardKeyCodesChanged = Notification.Name("SauceSelectedKeyboardKeyCodesChanged")
19 | }
20 |
21 | open class Sauce {
22 |
23 | // MARK: - Properties
24 | public static let shared = Sauce()
25 |
26 | private let layout: KeyboardLayout
27 | private let modifierTransformar: ModifierTransformer
28 |
29 | // MARK: - Initialize
30 | init(layout: KeyboardLayout = KeyboardLayout(), modifierTransformar: ModifierTransformer = ModifierTransformer()) {
31 | self.layout = layout
32 | self.modifierTransformar = modifierTransformar
33 | }
34 |
35 | }
36 |
37 | // MARK: - Input Sources
38 | extension Sauce {
39 | public func currentInputSources() -> [InputSource] {
40 | return layout.inputSources
41 | }
42 | }
43 |
44 | // MARK: - KeyCodes
45 | extension Sauce {
46 | public func keyCode(for key: Key) -> CGKeyCode {
47 | return currentKeyCode(for: key) ?? key.QWERTYKeyCode
48 | }
49 |
50 | public func currentKeyCode(for key: Key) -> CGKeyCode? {
51 | return layout.currentKeyCode(for: key)
52 | }
53 |
54 | public func currentKeyCodes() -> [Key: CGKeyCode]? {
55 | return layout.currentKeyCodes()
56 | }
57 |
58 | public func keyCode(with source: InputSource, key: Key) -> CGKeyCode? {
59 | return layout.keyCode(with: source, key: key)
60 | }
61 |
62 | public func keyCodes(with source: InputSource) -> [Key: CGKeyCode]? {
63 | return layout.keyCodes(with: source)
64 | }
65 | }
66 |
67 | // MARK: - Key
68 | extension Sauce {
69 | public func key(for keyCode: Int) -> Key? {
70 | return currentKey(for: keyCode) ?? Key(QWERTYKeyCode: keyCode)
71 | }
72 |
73 | public func currentKey(for keyCode: Int) -> Key? {
74 | return layout.currentKey(for: keyCode)
75 | }
76 |
77 | public func key(with source: InputSource, keyCode: Int) -> Key? {
78 | return layout.key(with: source, keyCode: keyCode)
79 | }
80 | }
81 |
82 | // MARK: - Characters
83 | extension Sauce {
84 | public func character(for keyCode: Int, carbonModifiers: Int) -> String? {
85 | return currentCharacter(for: keyCode, carbonModifiers: carbonModifiers) ?? currentASCIICapableCharacter(for: keyCode, carbonModifiers: carbonModifiers)
86 | }
87 |
88 | public func character(for keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String? {
89 | return character(for: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers))
90 | }
91 |
92 | public func currentCharacter(for keyCode: Int, carbonModifiers: Int) -> String? {
93 | return layout.currentCharacter(for: keyCode, carbonModifiers: carbonModifiers)
94 | }
95 |
96 | public func currentCharacter(for keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String? {
97 | return currentCharacter(for: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers))
98 | }
99 |
100 | public func currentASCIICapableCharacter(for keyCode: Int, carbonModifiers: Int) -> String? {
101 | return layout.currentASCIICapableCharacter(for: keyCode, carbonModifiers: carbonModifiers)
102 | }
103 |
104 | public func currentASCIICapableCharacter(for keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String? {
105 | return currentASCIICapableCharacter(for: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers))
106 | }
107 |
108 | public func character(with source: InputSource, keyCode: Int, carbonModifiers: Int) -> String? {
109 | return layout.character(with: source, keyCode: keyCode, carbonModifiers: carbonModifiers)
110 | }
111 |
112 | public func character(with source: InputSource, keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String? {
113 | return character(with: source, keyCode: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers))
114 | }
115 | }
116 | #endif
117 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | activesupport (7.1.3.2)
9 | base64
10 | bigdecimal
11 | concurrent-ruby (~> 1.0, >= 1.0.2)
12 | connection_pool (>= 2.2.5)
13 | drb
14 | i18n (>= 1.6, < 2)
15 | minitest (>= 5.1)
16 | mutex_m
17 | tzinfo (~> 2.0)
18 | addressable (2.8.6)
19 | public_suffix (>= 2.0.2, < 6.0)
20 | algoliasearch (1.27.5)
21 | httpclient (~> 2.8, >= 2.8.3)
22 | json (>= 1.5.1)
23 | atomos (0.1.3)
24 | base64 (0.2.0)
25 | bigdecimal (3.1.7)
26 | claide (1.1.0)
27 | claide-plugins (0.9.2)
28 | cork
29 | nap
30 | open4 (~> 1.3)
31 | cocoapods (1.15.2)
32 | addressable (~> 2.8)
33 | claide (>= 1.0.2, < 2.0)
34 | cocoapods-core (= 1.15.2)
35 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
36 | cocoapods-downloader (>= 2.1, < 3.0)
37 | cocoapods-plugins (>= 1.0.0, < 2.0)
38 | cocoapods-search (>= 1.0.0, < 2.0)
39 | cocoapods-trunk (>= 1.6.0, < 2.0)
40 | cocoapods-try (>= 1.1.0, < 2.0)
41 | colored2 (~> 3.1)
42 | escape (~> 0.0.4)
43 | fourflusher (>= 2.3.0, < 3.0)
44 | gh_inspector (~> 1.0)
45 | molinillo (~> 0.8.0)
46 | nap (~> 1.0)
47 | ruby-macho (>= 2.3.0, < 3.0)
48 | xcodeproj (>= 1.23.0, < 2.0)
49 | cocoapods-core (1.15.2)
50 | activesupport (>= 5.0, < 8)
51 | addressable (~> 2.8)
52 | algoliasearch (~> 1.0)
53 | concurrent-ruby (~> 1.1)
54 | fuzzy_match (~> 2.0.4)
55 | nap (~> 1.0)
56 | netrc (~> 0.11)
57 | public_suffix (~> 4.0)
58 | typhoeus (~> 1.0)
59 | cocoapods-deintegrate (1.0.5)
60 | cocoapods-downloader (2.1)
61 | cocoapods-plugins (1.0.0)
62 | nap
63 | cocoapods-search (1.0.1)
64 | cocoapods-trunk (1.6.0)
65 | nap (>= 0.8, < 2.0)
66 | netrc (~> 0.11)
67 | cocoapods-try (1.2.0)
68 | colored2 (3.1.2)
69 | concurrent-ruby (1.2.3)
70 | connection_pool (2.4.1)
71 | cork (0.3.0)
72 | colored2 (~> 3.1)
73 | danger (9.4.3)
74 | claide (~> 1.0)
75 | claide-plugins (>= 0.9.2)
76 | colored2 (~> 3.1)
77 | cork (~> 0.1)
78 | faraday (>= 0.9.0, < 3.0)
79 | faraday-http-cache (~> 2.0)
80 | git (~> 1.13)
81 | kramdown (~> 2.3)
82 | kramdown-parser-gfm (~> 1.0)
83 | no_proxy_fix
84 | octokit (>= 4.0)
85 | terminal-table (>= 1, < 4)
86 | danger-swiftlint (0.35.0)
87 | danger
88 | rake (> 10)
89 | thor (~> 1.0.0)
90 | drb (2.2.1)
91 | escape (0.0.4)
92 | ethon (0.16.0)
93 | ffi (>= 1.15.0)
94 | faraday (2.9.0)
95 | faraday-net_http (>= 2.0, < 3.2)
96 | faraday-http-cache (2.5.1)
97 | faraday (>= 0.8)
98 | faraday-net_http (3.1.0)
99 | net-http
100 | ffi (1.16.3)
101 | fourflusher (2.3.1)
102 | fuzzy_match (2.0.4)
103 | gh_inspector (1.1.3)
104 | git (1.19.1)
105 | addressable (~> 2.8)
106 | rchardet (~> 1.8)
107 | httpclient (2.8.3)
108 | i18n (1.14.4)
109 | concurrent-ruby (~> 1.0)
110 | json (2.7.1)
111 | kramdown (2.4.0)
112 | rexml
113 | kramdown-parser-gfm (1.1.0)
114 | kramdown (~> 2.0)
115 | minitest (5.22.3)
116 | molinillo (0.8.0)
117 | mutex_m (0.2.0)
118 | nanaimo (0.3.0)
119 | nap (1.1.0)
120 | net-http (0.4.1)
121 | uri
122 | netrc (0.11.0)
123 | nkf (0.2.0)
124 | no_proxy_fix (0.1.2)
125 | octokit (8.1.0)
126 | base64
127 | faraday (>= 1, < 3)
128 | sawyer (~> 0.9)
129 | open4 (1.3.4)
130 | public_suffix (4.0.7)
131 | rake (13.1.0)
132 | rchardet (1.8.0)
133 | rexml (3.3.9)
134 | ruby-macho (2.5.1)
135 | sawyer (0.9.2)
136 | addressable (>= 2.3.5)
137 | faraday (>= 0.17.3, < 3)
138 | terminal-table (3.0.2)
139 | unicode-display_width (>= 1.1.1, < 3)
140 | thor (1.0.1)
141 | typhoeus (1.4.1)
142 | ethon (>= 0.9.0)
143 | tzinfo (2.0.6)
144 | concurrent-ruby (~> 1.0)
145 | unicode-display_width (2.5.0)
146 | uri (0.13.0)
147 | xcodeproj (1.25.1)
148 | CFPropertyList (>= 2.3.3, < 4.0)
149 | atomos (~> 0.1.3)
150 | claide (>= 1.0.2, < 2.0)
151 | colored2 (~> 3.1)
152 | nanaimo (~> 0.3.0)
153 | rexml (>= 3.3.6, < 4.0)
154 |
155 | PLATFORMS
156 | ruby
157 |
158 | DEPENDENCIES
159 | cocoapods
160 | danger
161 | danger-swiftlint
162 |
163 | BUNDLED WITH
164 | 2.1.4
165 |
--------------------------------------------------------------------------------
/Lib/Sauce/SpecialKeyCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpecialKeyCode.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 |
15 | // swiftlint:disable identifier_name function_body_length
16 |
17 | /**
18 | * keycodes for keys that are independent of keyboard layout
19 | * ref: Carbon.framework
20 | *
21 | * UCKeyTranslate can not convert a layout-independent keycode to string.
22 | **/
23 | enum SpecialKeyCode {
24 | case `return`
25 | case tab
26 | case space
27 | case delete
28 | case escape
29 | case f17
30 | case f18
31 | case f19
32 | case f20
33 | case f5
34 | case f6
35 | case f7
36 | case f3
37 | case f8
38 | case f9
39 | case f11
40 | case f13
41 | case f16
42 | case f14
43 | case f10
44 | case f12
45 | case f15
46 | case help
47 | case home
48 | case pageUp
49 | case forwardDelete
50 | case f4
51 | case end
52 | case f2
53 | case pageDown
54 | case f1
55 | case leftArrow
56 | case rightArrow
57 | case downArrow
58 | case upArrow
59 | case eisu
60 | case kana
61 | case keypadClear
62 | case keypadEnter
63 |
64 | // MARK: - Initialize
65 | init?(keyCode: Int) {
66 | switch keyCode {
67 | case kVK_Return: self = .return
68 | case kVK_Tab: self = .tab
69 | case kVK_Space: self = .space
70 | case kVK_Delete: self = .delete
71 | case kVK_Escape: self = .escape
72 | case kVK_F17: self = .f17
73 | case kVK_F18: self = .f18
74 | case kVK_F19: self = .f19
75 | case kVK_F20: self = .f20
76 | case kVK_F5: self = .f5
77 | case kVK_F6: self = .f6
78 | case kVK_F7: self = .f7
79 | case kVK_F3: self = .f3
80 | case kVK_F8: self = .f8
81 | case kVK_F9: self = .f9
82 | case kVK_F11: self = .f11
83 | case kVK_F13: self = .f13
84 | case kVK_F16: self = .f16
85 | case kVK_F14: self = .f14
86 | case kVK_F10: self = .f10
87 | case kVK_F12: self = .f12
88 | case kVK_F15: self = .f15
89 | case kVK_Help: self = .help
90 | case kVK_Home: self = .home
91 | case kVK_PageUp: self = .pageUp
92 | case kVK_ForwardDelete: self = .forwardDelete
93 | case kVK_F4: self = .f4
94 | case kVK_End: self = .end
95 | case kVK_F2: self = .f2
96 | case kVK_PageDown: self = .pageDown
97 | case kVK_F1: self = .f1
98 | case kVK_LeftArrow: self = .leftArrow
99 | case kVK_RightArrow: self = .rightArrow
100 | case kVK_DownArrow: self = .downArrow
101 | case kVK_UpArrow: self = .upArrow
102 | case kVK_JIS_Eisu: self = .eisu
103 | case kVK_JIS_Kana: self = .kana
104 | case kVK_ANSI_KeypadClear: self = .keypadClear
105 | case kVK_ANSI_KeypadEnter: self = .keypadEnter
106 | default: return nil
107 | }
108 | }
109 |
110 | // MARK: - Properties
111 | var character: String {
112 | switch self {
113 | case .return: return 0x21A9.string // ↩
114 | case .tab: return 0x21E5.string // ⇥
115 | case .space: return "Space"
116 | case .delete: return 0x232B.string // ⌫
117 | case .escape: return 0x238B.string // ⎋
118 | case .f17: return "F17"
119 | case .f18: return "F18"
120 | case .f19: return "F19"
121 | case .f20: return "F20"
122 | case .f5: return "F5"
123 | case .f6: return "F6"
124 | case .f7: return "F7"
125 | case .f3: return "F3"
126 | case .f8: return "F8"
127 | case .f9: return "F9"
128 | case .f11: return "F11"
129 | case .f13: return "F13"
130 | case .f16: return "F16"
131 | case .f14: return "F14"
132 | case .f10: return "F10"
133 | case .f12: return "F12"
134 | case .f15: return "F15"
135 | case .help: return "?⃝"
136 | case .home: return 0x2196.string // ↖
137 | case .pageUp: return 0x21DE.string // ⇞
138 | case .forwardDelete: return 0x2326.string // ⌦
139 | case .f4: return "F4"
140 | case .end: return 0x2198.string // ↘
141 | case .f2: return "F2"
142 | case .pageDown: return 0x21DF.string // ⇟
143 | case .f1: return "F1"
144 | case .leftArrow: return 0x2190.string // ←
145 | case .rightArrow: return 0x2192.string // →
146 | case .downArrow: return 0x2193.string // ↓
147 | case .upArrow: return 0x2191.string // ↑
148 | case .eisu: return "英数"
149 | case .kana: return "かな"
150 | case .keypadClear: return 0x2327.string // ⌧
151 | case .keypadEnter: return 0x2305.string // ⌅
152 | }
153 | }
154 | }
155 |
156 | private extension Int {
157 | var string: String {
158 | return String(format: "%C", self)
159 | }
160 | }
161 | #endif
162 |
--------------------------------------------------------------------------------
/Lib/Sauce/KeyboardLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayout.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 |
15 | final class KeyboardLayout {
16 |
17 | // MARK: - Properties
18 | private var currentKeyboardLayoutInputSource: InputSource
19 | private var currentASCIICapableInputSource: InputSource
20 | private var mappedKeyCodes = [InputSource: [Key: CGKeyCode]]()
21 | private(set) var inputSources = [InputSource]()
22 |
23 | private let distributedNotificationCenter: DistributedNotificationCenter
24 | private let notificationCenter: NotificationCenter
25 | private let modifierTransformer: ModifierTransformer
26 |
27 | // MARK: - Initialize
28 | init(distributedNotificationCenter: DistributedNotificationCenter = .default(), notificationCenter: NotificationCenter = .default, modifierTransformer: ModifierTransformer = ModifierTransformer()) {
29 | self.distributedNotificationCenter = distributedNotificationCenter
30 | self.notificationCenter = notificationCenter
31 | self.modifierTransformer = modifierTransformer
32 | self.currentKeyboardLayoutInputSource = InputSource(source: TISCopyCurrentKeyboardLayoutInputSource().takeUnretainedValue())
33 | self.currentASCIICapableInputSource = InputSource(source: TISCopyCurrentASCIICapableKeyboardInputSource().takeUnretainedValue())
34 | mappingInputSources()
35 | mappingKeyCodes(with: currentKeyboardLayoutInputSource)
36 | observeNotifications()
37 | }
38 |
39 | deinit {
40 | distributedNotificationCenter.removeObserver(self)
41 | notificationCenter.removeObserver(self)
42 | }
43 |
44 | }
45 |
46 | // MARK: - KeyCodes
47 | extension KeyboardLayout {
48 | func currentKeyCodes() -> [Key: CGKeyCode]? {
49 | return keyCodes(with: currentKeyboardLayoutInputSource)
50 | }
51 |
52 | func currentKeyCode(for key: Key) -> CGKeyCode? {
53 | return keyCode(with: currentKeyboardLayoutInputSource, key: key)
54 | }
55 |
56 | func keyCodes(with source: InputSource) -> [Key: CGKeyCode]? {
57 | return mappedKeyCodes[source]
58 | }
59 |
60 | func keyCode(with source: InputSource, key: Key) -> CGKeyCode? {
61 | return mappedKeyCodes[source]?[key]
62 | }
63 | }
64 |
65 | // MARK: - Key
66 | extension KeyboardLayout {
67 | func currentKey(for keyCode: Int) -> Key? {
68 | return key(with: currentKeyboardLayoutInputSource, keyCode: keyCode)
69 | }
70 |
71 | func key(with source: InputSource, keyCode: Int) -> Key? {
72 | return mappedKeyCodes[source]?.first(where: { $0.value == CGKeyCode(keyCode) })?.key
73 | }
74 | }
75 |
76 | // MARK: - Characters
77 | extension KeyboardLayout {
78 | func currentCharacter(for keyCode: Int, carbonModifiers: Int) -> String? {
79 | return character(with: currentKeyboardLayoutInputSource, keyCode: keyCode, carbonModifiers: carbonModifiers)
80 | }
81 |
82 | func currentASCIICapableCharacter(for keyCode: Int, carbonModifiers: Int) -> String? {
83 | return character(with: currentASCIICapableInputSource, keyCode: keyCode, carbonModifiers: carbonModifiers)
84 | }
85 |
86 | func character(with source: InputSource, keyCode: Int, carbonModifiers: Int) -> String? {
87 | return character(with: source.source, keyCode: keyCode, carbonModifiers: carbonModifiers)
88 | }
89 | }
90 |
91 | // MARK: - Notifications
92 | extension KeyboardLayout {
93 | private func observeNotifications() {
94 | distributedNotificationCenter.addObserver(self,
95 | selector: #selector(selectedKeyboardInputSourceChanged),
96 | name: NSNotification.Name(kTISNotifySelectedKeyboardInputSourceChanged as String),
97 | object: nil,
98 | suspensionBehavior: .deliverImmediately)
99 | distributedNotificationCenter.addObserver(self,
100 | selector: #selector(enabledKeyboardInputSourcesChanged),
101 | name: Notification.Name(kTISNotifyEnabledKeyboardInputSourcesChanged as String),
102 | object: nil,
103 | suspensionBehavior: .deliverImmediately)
104 | }
105 |
106 | @objc func selectedKeyboardInputSourceChanged() {
107 | let source = InputSource(source: TISCopyCurrentKeyboardLayoutInputSource().takeUnretainedValue())
108 | self.currentASCIICapableInputSource = InputSource(source: TISCopyCurrentASCIICapableKeyboardInputSource().takeUnretainedValue())
109 | guard source != currentKeyboardLayoutInputSource else { return }
110 | let previousKeyboardLayoutInputSource = currentKeyboardLayoutInputSource
111 | self.currentKeyboardLayoutInputSource = source
112 | guard mappedKeyCodes[source] == nil else {
113 | notificationCenter.post(name: .SauceSelectedKeyboardInputSourceChanged, object: nil)
114 | notifyKeyCodesChangedIfNeeded(previous: previousKeyboardLayoutInputSource, current: source)
115 | return
116 | }
117 | mappingKeyCodes(with: source)
118 | notificationCenter.post(name: .SauceSelectedKeyboardInputSourceChanged, object: nil)
119 | notifyKeyCodesChangedIfNeeded(previous: previousKeyboardLayoutInputSource, current: source)
120 | }
121 |
122 | @objc func enabledKeyboardInputSourcesChanged() {
123 | mappedKeyCodes.removeAll()
124 | mappingInputSources()
125 | mappingKeyCodes(with: currentKeyboardLayoutInputSource)
126 | notificationCenter.post(name: .SauceEnabledKeyboardInputSourcesChanged, object: nil)
127 | }
128 |
129 | private func notifyKeyCodesChangedIfNeeded(previous: InputSource, current: InputSource) {
130 | guard let previousKeyCodes = mappedKeyCodes[previous] else { return }
131 | guard let currentKeyCodes = mappedKeyCodes[current] else { return }
132 | guard previousKeyCodes != currentKeyCodes else { return }
133 | notificationCenter.post(name: .SauceSelectedKeyboardKeyCodesChanged, object: nil)
134 | }
135 | }
136 |
137 | // MARK: - Layouts
138 | private extension KeyboardLayout {
139 | func mappingInputSources() {
140 | guard let sources = TISCreateInputSourceList([:] as CFDictionary, false).takeUnretainedValue() as? [TISInputSource] else { return }
141 | inputSources = sources.map { InputSource(source: $0) }
142 | inputSources.forEach { mappingKeyCodes(with: $0) }
143 | }
144 |
145 | func mappingKeyCodes(with source: InputSource) {
146 | guard let layoutData = TISGetInputSourceProperty(source.source, kTISPropertyUnicodeKeyLayoutData) else { return }
147 | let data = Unmanaged.fromOpaque(layoutData).takeUnretainedValue() as Data
148 | var keyCodes = [Key: CGKeyCode]()
149 | for i in 0..<128 {
150 | guard let character = character(with: data, keyCode: i, carbonModifiers: 0) else { continue }
151 | guard let key = Key(character: character, virtualKeyCode: i) else { continue }
152 | guard keyCodes[key] == nil else { continue }
153 | keyCodes[key] = CGKeyCode(i)
154 | }
155 | mappedKeyCodes[source] = keyCodes
156 | }
157 |
158 | func character(with source: TISInputSource, keyCode: Int, carbonModifiers: Int) -> String? {
159 | guard let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { return nil }
160 | let data = Unmanaged.fromOpaque(layoutData).takeUnretainedValue() as Data
161 | return character(with: data, keyCode: keyCode, carbonModifiers: carbonModifiers)
162 | }
163 |
164 | func character(with layoutData: Data, keyCode: Int, carbonModifiers: Int) -> String? {
165 | // In the case of the special key code, it does not depend on the keyboard layout
166 | if let specialKeyCode = SpecialKeyCode(keyCode: keyCode) { return specialKeyCode.character }
167 |
168 | let modifierKeyState = (modifierTransformer.convertCharactorSupportCarbonModifiers(from: carbonModifiers) >> 8) & 0xff
169 | var deadKeyState: UInt32 = 0
170 | let maxChars = 256
171 | var chars = [UniChar](repeating: 0, count: maxChars)
172 | var length = 0
173 | let error = layoutData.withUnsafeBytes { pointer -> OSStatus in
174 | guard let keyboardLayoutPointer = pointer.bindMemory(to: UCKeyboardLayout.self).baseAddress else { return errSecAllocate }
175 | return CoreServices.UCKeyTranslate(keyboardLayoutPointer,
176 | UInt16(keyCode),
177 | UInt16(CoreServices.kUCKeyActionDisplay),
178 | UInt32(modifierKeyState),
179 | UInt32(LMGetKbdType()),
180 | OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit),
181 | &deadKeyState,
182 | maxChars,
183 | &length,
184 | &chars)
185 | }
186 | guard error == noErr else { return nil }
187 | return NSString(characters: &chars, length: length) as String
188 | }
189 | }
190 | #endif
191 |
--------------------------------------------------------------------------------
/Lib/SauceTests/KeyboardLayoutTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutTests.swift
3 | //
4 | // SauceTests
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | import XCTest
12 | import Carbon
13 | @testable import Sauce
14 |
15 | // swiftlint:disable discarded_notification_center_observer
16 | final class KeyboardLayoutTests: XCTestCase {
17 |
18 | // MARK: - Properties
19 | private let ABCKeyboardID = "com.apple.keylayout.ABC"
20 | private let dvorakKeyboardID = "com.apple.keylayout.Dvorak"
21 | private var japaneseKeyboardID: String {
22 | if #available(macOS 11.0, *) {
23 | return "com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese"
24 | } else {
25 | return "com.apple.inputmethod.Kotoeri.Japanese"
26 | }
27 | }
28 | private var kotoeriKeyboardID: String {
29 | if #available(macOS 11.0, *) {
30 | return "com.apple.inputmethod.Kotoeri.RomajiTyping"
31 | } else {
32 | return "com.apple.inputmethod.Kotoeri"
33 | }
34 | }
35 | private let modifierTransformer = ModifierTransformer()
36 | private let QWERTYVKeyCode = 9
37 | private let DvorakVKeyCode = 47 // swiftlint:disable:this identifier_name
38 |
39 | // MARK: - Tests
40 | func testKeyCodesForABCKeyboard() {
41 | let isInstalledABCKeyboard = isInstalledInputSource(id: ABCKeyboardID)
42 | XCTAssertTrue(installInputSource(id: ABCKeyboardID))
43 | XCTAssertTrue(selectInputSource(id: ABCKeyboardID))
44 | let notificationCenter = NotificationCenter()
45 | let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter)
46 | let vKeyCode = keyboardLayout.currentKeyCode(for: .v)
47 | XCTAssertEqual(vKeyCode, CGKeyCode(QWERTYVKeyCode))
48 | let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode)
49 | XCTAssertEqual(vKey, .v)
50 | let vCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: 0)
51 | XCTAssertEqual(vCharacter, "v")
52 | let vShiftCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: .shift))
53 | XCTAssertEqual(vShiftCharacter, "V")
54 | let vOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.option]))
55 | XCTAssertEqual(vOptionCharacter, "√")
56 | let vShiftOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .option]))
57 | XCTAssertEqual(vShiftOptionCharacter, "◊")
58 | guard !isInstalledABCKeyboard else { return }
59 | uninstallInputSource(id: ABCKeyboardID)
60 | }
61 |
62 | func testKeyCodesForDvorakKeyboard() {
63 | let isInstalledDvorakKeyboard = isInstalledInputSource(id: dvorakKeyboardID)
64 | XCTAssertTrue(installInputSource(id: dvorakKeyboardID))
65 | XCTAssertTrue(selectInputSource(id: dvorakKeyboardID))
66 | let notificationCenter = NotificationCenter()
67 | let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter)
68 | let vKeyCode = keyboardLayout.currentKeyCode(for: .v)
69 | XCTAssertEqual(vKeyCode, CGKeyCode(DvorakVKeyCode))
70 | let vKey = keyboardLayout.currentKey(for: DvorakVKeyCode)
71 | XCTAssertEqual(vKey, .v)
72 | let vCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: 0)
73 | XCTAssertEqual(vCharacter, "v")
74 | let vShiftCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: .shift))
75 | XCTAssertEqual(vShiftCharacter, "V")
76 | let vOptionCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.option]))
77 | XCTAssertEqual(vOptionCharacter, "√")
78 | let vShiftOptionCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .option]))
79 | XCTAssertEqual(vShiftOptionCharacter, "◊")
80 | guard !isInstalledDvorakKeyboard else { return }
81 | uninstallInputSource(id: dvorakKeyboardID)
82 | }
83 |
84 | func testKeyCodesJapanesesAndDvorakOnlyKeyboard() {
85 | let installedInputSources = fetchInputSource(includeAllInstalled: false)
86 | let isInstalledJapaneseKeyboard = isInstalledInputSource(id: japaneseKeyboardID)
87 | let isInstalledKotoeriKeyboard = isInstalledInputSource(id: kotoeriKeyboardID)
88 | let isInstalledDvorakKeyboard = isInstalledInputSource(id: dvorakKeyboardID)
89 | XCTAssertTrue(installInputSource(id: ABCKeyboardID))
90 | XCTAssertTrue(installInputSource(id: dvorakKeyboardID))
91 | XCTAssertTrue(installInputSource(id: kotoeriKeyboardID))
92 | XCTAssertTrue(installInputSource(id: japaneseKeyboardID))
93 | XCTAssertTrue(selectInputSource(id: ABCKeyboardID))
94 | XCTAssertTrue(selectInputSource(id: japaneseKeyboardID))
95 | XCTAssertTrue(uninstallInputSource(id: ABCKeyboardID))
96 | installedInputSources.filter { $0.id != japaneseKeyboardID && $0.id != dvorakKeyboardID && !$0.id.contains("Japanese") && !$0.id.contains("Kotoeri") }
97 | .forEach { uninstallInputSource(id: $0.id) }
98 | let notificationCenter = NotificationCenter()
99 | let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter)
100 | let vKeyCode = keyboardLayout.currentKeyCode(for: .v)
101 | XCTAssertEqual(vKeyCode, CGKeyCode(QWERTYVKeyCode))
102 | let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode)
103 | XCTAssertEqual(vKey, .v)
104 | let vCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: 0)
105 | XCTAssertEqual(vCharacter, "v")
106 | let vShiftCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: .shift))
107 | XCTAssertEqual(vShiftCharacter, "V")
108 | let vOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.option]))
109 | XCTAssertEqual(vOptionCharacter, "√")
110 | let vShiftOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .option]))
111 | XCTAssertEqual(vShiftOptionCharacter, "◊")
112 | installedInputSources.forEach { installInputSource(id: $0.id) }
113 | if !isInstalledJapaneseKeyboard {
114 | uninstallInputSource(id: japaneseKeyboardID)
115 | }
116 | if !isInstalledDvorakKeyboard {
117 | uninstallInputSource(id: dvorakKeyboardID)
118 | }
119 | if !isInstalledKotoeriKeyboard {
120 | uninstallInputSource(id: kotoeriKeyboardID)
121 | }
122 | }
123 |
124 | func testInputSourceChangedNotification() {
125 | let isInstalledABCKeyboard = isInstalledInputSource(id: ABCKeyboardID)
126 | let isInstalledDvorakKeyboard = isInstalledInputSource(id: dvorakKeyboardID)
127 | XCTAssertTrue(installInputSource(id: ABCKeyboardID))
128 | XCTAssertTrue(uninstallInputSource(id: dvorakKeyboardID))
129 | XCTAssertTrue(selectInputSource(id: ABCKeyboardID))
130 | let notificationCenter = NotificationCenter()
131 | let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter)
132 | let selectedExpectation = XCTestExpectation(description: "Selected Keycobard Input Source Changed")
133 | selectedExpectation.expectedFulfillmentCount = 2
134 | selectedExpectation.assertForOverFulfill = true
135 | notificationCenter.addObserver(forName: .SauceSelectedKeyboardInputSourceChanged, object: nil, queue: nil) { _ in
136 | selectedExpectation.fulfill()
137 | }
138 | let enabledExpectation = XCTestExpectation(description: "Enabled Keycobard Input Source Changed")
139 | notificationCenter.addObserver(forName: .SauceEnabledKeyboardInputSourcesChanged, object: nil, queue: nil) { _ in
140 | enabledExpectation.fulfill()
141 | }
142 | XCTAssertTrue(installInputSource(id: dvorakKeyboardID))
143 | XCTAssertTrue(selectInputSource(id: dvorakKeyboardID))
144 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
145 | XCTAssertTrue(self.selectInputSource(id: self.ABCKeyboardID))
146 | }
147 | wait(for: [selectedExpectation, enabledExpectation], timeout: 2)
148 | if !isInstalledABCKeyboard {
149 | uninstallInputSource(id: ABCKeyboardID)
150 | }
151 | if !isInstalledDvorakKeyboard {
152 | uninstallInputSource(id: dvorakKeyboardID)
153 | }
154 | }
155 |
156 | func testKeyCodesChangedNotification() {
157 | let isInstalledABCKeyboard = isInstalledInputSource(id: ABCKeyboardID)
158 | let isInstalledDvorakKeyboard = isInstalledInputSource(id: dvorakKeyboardID)
159 | XCTAssertTrue(installInputSource(id: ABCKeyboardID))
160 | XCTAssertTrue(installInputSource(id: dvorakKeyboardID))
161 | XCTAssertTrue(selectInputSource(id: ABCKeyboardID))
162 | let notificationCenter = NotificationCenter()
163 | let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter)
164 | let expectation = XCTestExpectation(description: "Selected Keycobard Key Codes Changed")
165 | expectation.expectedFulfillmentCount = 2
166 | expectation.assertForOverFulfill = true
167 | notificationCenter.addObserver(forName: .SauceSelectedKeyboardKeyCodesChanged, object: nil, queue: nil) { _ in
168 | expectation.fulfill()
169 | }
170 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
171 | XCTAssertTrue(self.selectInputSource(id: self.dvorakKeyboardID))
172 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
173 | XCTAssertTrue(self.selectInputSource(id: self.ABCKeyboardID))
174 | }
175 | }
176 | wait(for: [expectation], timeout: 2)
177 | if !isInstalledABCKeyboard {
178 | uninstallInputSource(id: ABCKeyboardID)
179 | }
180 | if !isInstalledDvorakKeyboard {
181 | uninstallInputSource(id: dvorakKeyboardID)
182 | }
183 | }
184 |
185 | // MARK: - Util
186 | private func fetchInputSource(includeAllInstalled: Bool) -> [InputSource] {
187 | guard let sources = TISCreateInputSourceList([:] as CFDictionary, includeAllInstalled).takeUnretainedValue() as? [TISInputSource] else { return [] }
188 | return sources.map { InputSource(source: $0) }
189 | }
190 |
191 | private func isInstalledInputSource(id: String) -> Bool {
192 | return fetchInputSource(includeAllInstalled: false).contains(where: { $0.id == id })
193 | }
194 |
195 | @discardableResult
196 | private func installInputSource(id: String) -> Bool {
197 | let allInputSources = fetchInputSource(includeAllInstalled: true)
198 | guard let targetInputSource = allInputSources.first(where: { $0.id == id }) else { return false }
199 | return TISEnableInputSource(targetInputSource.source) == noErr
200 | }
201 |
202 | @discardableResult
203 | private func uninstallInputSource(id: String) -> Bool {
204 | let installedInputSources = fetchInputSource(includeAllInstalled: false)
205 | guard let targetInputSource = installedInputSources.first(where: { $0.id == id }) else { return true }
206 | return TISDisableInputSource(targetInputSource.source) == noErr
207 | }
208 |
209 | @discardableResult
210 | private func selectInputSource(id: String) -> Bool {
211 | let installedInputSources = self.fetchInputSource(includeAllInstalled: false)
212 | guard let targetInputSource = installedInputSources.first(where: { $0.id == id }) else { return false }
213 | return TISSelectInputSource(targetInputSource.source) == noErr
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/Lib/Sauce.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0909EBD0245D5C9E00ABA339 /* KeyboardLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0909EBCF245D5C9E00ABA339 /* KeyboardLayoutTests.swift */; };
11 | 095EF0012456ED9A00174829 /* ModifierTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095EF0002456ED9A00174829 /* ModifierTransformer.swift */; };
12 | 09CDC68A210F044A007DDFE4 /* TISInputSource+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */; };
13 | 50582477261C6F1F00AD2DD8 /* NSMenuItem+Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */; };
14 | 5058247B261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5058247A261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift */; };
15 | FA1E407621107B0A0016D710 /* SpecialKeyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */; };
16 | FAA4132A210E2C730097D522 /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAA41320210E2C730097D522 /* Sauce.framework */; };
17 | FAA41331210E2C730097D522 /* Sauce.h in Headers */ = {isa = PBXBuildFile; fileRef = FAA41323210E2C730097D522 /* Sauce.h */; settings = {ATTRIBUTES = (Public, ); }; };
18 | FAA41365210E2D9B0097D522 /* Sauce.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41364210E2D9B0097D522 /* Sauce.swift */; };
19 | FAA41367210E2EFB0097D522 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41366210E2EFB0097D522 /* Key.swift */; };
20 | FAA41369210E33060097D522 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41368210E33060097D522 /* KeyboardLayout.swift */; };
21 | FAA4136B210E33CC0097D522 /* InputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA4136A210E33CB0097D522 /* InputSource.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXContainerItemProxy section */
25 | FAA4132B210E2C730097D522 /* PBXContainerItemProxy */ = {
26 | isa = PBXContainerItemProxy;
27 | containerPortal = FAA41317210E2C730097D522 /* Project object */;
28 | proxyType = 1;
29 | remoteGlobalIDString = FAA4131F210E2C730097D522;
30 | remoteInfo = Sauce;
31 | };
32 | /* End PBXContainerItemProxy section */
33 |
34 | /* Begin PBXFileReference section */
35 | 0909EBCF245D5C9E00ABA339 /* KeyboardLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayoutTests.swift; sourceTree = ""; };
36 | 095EF0002456ED9A00174829 /* ModifierTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierTransformer.swift; sourceTree = ""; };
37 | 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TISInputSource+Property.swift"; sourceTree = ""; };
38 | 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Key.swift"; sourceTree = ""; };
39 | 5058247A261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+KeyTests.swift"; sourceTree = ""; };
40 | FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialKeyCode.swift; sourceTree = ""; };
41 | FAA41320210E2C730097D522 /* Sauce.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sauce.framework; sourceTree = BUILT_PRODUCTS_DIR; };
42 | FAA41323210E2C730097D522 /* Sauce.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Sauce.h; sourceTree = ""; };
43 | FAA41324210E2C730097D522 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
44 | FAA41329210E2C730097D522 /* SauceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SauceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
45 | FAA41330210E2C730097D522 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
46 | FAA41364210E2D9B0097D522 /* Sauce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sauce.swift; sourceTree = ""; };
47 | FAA41366210E2EFB0097D522 /* Key.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = ""; };
48 | FAA41368210E33060097D522 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; };
49 | FAA4136A210E33CB0097D522 /* InputSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSource.swift; sourceTree = ""; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | FAA4131C210E2C730097D522 /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | );
58 | runOnlyForDeploymentPostprocessing = 0;
59 | };
60 | FAA41326210E2C730097D522 /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | FAA4132A210E2C730097D522 /* Sauce.framework in Frameworks */,
65 | );
66 | runOnlyForDeploymentPostprocessing = 0;
67 | };
68 | /* End PBXFrameworksBuildPhase section */
69 |
70 | /* Begin PBXGroup section */
71 | FAA41316210E2C730097D522 = {
72 | isa = PBXGroup;
73 | children = (
74 | FAA41322210E2C730097D522 /* Sauce */,
75 | FAA4132D210E2C730097D522 /* SauceTests */,
76 | FAA41321210E2C730097D522 /* Products */,
77 | );
78 | sourceTree = "";
79 | };
80 | FAA41321210E2C730097D522 /* Products */ = {
81 | isa = PBXGroup;
82 | children = (
83 | FAA41320210E2C730097D522 /* Sauce.framework */,
84 | FAA41329210E2C730097D522 /* SauceTests.xctest */,
85 | );
86 | name = Products;
87 | sourceTree = "";
88 | };
89 | FAA41322210E2C730097D522 /* Sauce */ = {
90 | isa = PBXGroup;
91 | children = (
92 | FAA41323210E2C730097D522 /* Sauce.h */,
93 | FAA41324210E2C730097D522 /* Info.plist */,
94 | FAA41364210E2D9B0097D522 /* Sauce.swift */,
95 | FAA4136A210E33CB0097D522 /* InputSource.swift */,
96 | FAA41368210E33060097D522 /* KeyboardLayout.swift */,
97 | 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */,
98 | FAA41366210E2EFB0097D522 /* Key.swift */,
99 | FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */,
100 | 095EF0002456ED9A00174829 /* ModifierTransformer.swift */,
101 | 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */,
102 | );
103 | path = Sauce;
104 | sourceTree = "";
105 | };
106 | FAA4132D210E2C730097D522 /* SauceTests */ = {
107 | isa = PBXGroup;
108 | children = (
109 | 0909EBCF245D5C9E00ABA339 /* KeyboardLayoutTests.swift */,
110 | 5058247A261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift */,
111 | FAA41330210E2C730097D522 /* Info.plist */,
112 | );
113 | path = SauceTests;
114 | sourceTree = "";
115 | };
116 | /* End PBXGroup section */
117 |
118 | /* Begin PBXHeadersBuildPhase section */
119 | FAA4131D210E2C730097D522 /* Headers */ = {
120 | isa = PBXHeadersBuildPhase;
121 | buildActionMask = 2147483647;
122 | files = (
123 | FAA41331210E2C730097D522 /* Sauce.h in Headers */,
124 | );
125 | runOnlyForDeploymentPostprocessing = 0;
126 | };
127 | /* End PBXHeadersBuildPhase section */
128 |
129 | /* Begin PBXNativeTarget section */
130 | FAA4131F210E2C730097D522 /* Sauce */ = {
131 | isa = PBXNativeTarget;
132 | buildConfigurationList = FAA41334210E2C730097D522 /* Build configuration list for PBXNativeTarget "Sauce" */;
133 | buildPhases = (
134 | FAA4131B210E2C730097D522 /* Sources */,
135 | FAA4131C210E2C730097D522 /* Frameworks */,
136 | FAA4131D210E2C730097D522 /* Headers */,
137 | FAA4131E210E2C730097D522 /* Resources */,
138 | );
139 | buildRules = (
140 | );
141 | dependencies = (
142 | );
143 | name = Sauce;
144 | productName = Sauce;
145 | productReference = FAA41320210E2C730097D522 /* Sauce.framework */;
146 | productType = "com.apple.product-type.framework";
147 | };
148 | FAA41328210E2C730097D522 /* SauceTests */ = {
149 | isa = PBXNativeTarget;
150 | buildConfigurationList = FAA41337210E2C730097D522 /* Build configuration list for PBXNativeTarget "SauceTests" */;
151 | buildPhases = (
152 | FAA41325210E2C730097D522 /* Sources */,
153 | FAA41326210E2C730097D522 /* Frameworks */,
154 | FAA41327210E2C730097D522 /* Resources */,
155 | );
156 | buildRules = (
157 | );
158 | dependencies = (
159 | FAA4132C210E2C730097D522 /* PBXTargetDependency */,
160 | );
161 | name = SauceTests;
162 | productName = SauceTests;
163 | productReference = FAA41329210E2C730097D522 /* SauceTests.xctest */;
164 | productType = "com.apple.product-type.bundle.unit-test";
165 | };
166 | /* End PBXNativeTarget section */
167 |
168 | /* Begin PBXProject section */
169 | FAA41317210E2C730097D522 /* Project object */ = {
170 | isa = PBXProject;
171 | attributes = {
172 | LastSwiftUpdateCheck = 0940;
173 | LastUpgradeCheck = 1320;
174 | ORGANIZATIONNAME = "Clipy Project";
175 | TargetAttributes = {
176 | FAA4131F210E2C730097D522 = {
177 | CreatedOnToolsVersion = 9.4.1;
178 | LastSwiftMigration = 1020;
179 | };
180 | FAA41328210E2C730097D522 = {
181 | CreatedOnToolsVersion = 9.4.1;
182 | };
183 | };
184 | };
185 | buildConfigurationList = FAA4131A210E2C730097D522 /* Build configuration list for PBXProject "Sauce" */;
186 | compatibilityVersion = "Xcode 9.3";
187 | developmentRegion = en;
188 | hasScannedForEncodings = 0;
189 | knownRegions = (
190 | en,
191 | Base,
192 | );
193 | mainGroup = FAA41316210E2C730097D522;
194 | productRefGroup = FAA41321210E2C730097D522 /* Products */;
195 | projectDirPath = "";
196 | projectRoot = "";
197 | targets = (
198 | FAA4131F210E2C730097D522 /* Sauce */,
199 | FAA41328210E2C730097D522 /* SauceTests */,
200 | );
201 | };
202 | /* End PBXProject section */
203 |
204 | /* Begin PBXResourcesBuildPhase section */
205 | FAA4131E210E2C730097D522 /* Resources */ = {
206 | isa = PBXResourcesBuildPhase;
207 | buildActionMask = 2147483647;
208 | files = (
209 | );
210 | runOnlyForDeploymentPostprocessing = 0;
211 | };
212 | FAA41327210E2C730097D522 /* Resources */ = {
213 | isa = PBXResourcesBuildPhase;
214 | buildActionMask = 2147483647;
215 | files = (
216 | );
217 | runOnlyForDeploymentPostprocessing = 0;
218 | };
219 | /* End PBXResourcesBuildPhase section */
220 |
221 | /* Begin PBXSourcesBuildPhase section */
222 | FAA4131B210E2C730097D522 /* Sources */ = {
223 | isa = PBXSourcesBuildPhase;
224 | buildActionMask = 2147483647;
225 | files = (
226 | 50582477261C6F1F00AD2DD8 /* NSMenuItem+Key.swift in Sources */,
227 | 09CDC68A210F044A007DDFE4 /* TISInputSource+Property.swift in Sources */,
228 | FAA41369210E33060097D522 /* KeyboardLayout.swift in Sources */,
229 | FAA4136B210E33CC0097D522 /* InputSource.swift in Sources */,
230 | FAA41365210E2D9B0097D522 /* Sauce.swift in Sources */,
231 | FA1E407621107B0A0016D710 /* SpecialKeyCode.swift in Sources */,
232 | FAA41367210E2EFB0097D522 /* Key.swift in Sources */,
233 | 095EF0012456ED9A00174829 /* ModifierTransformer.swift in Sources */,
234 | );
235 | runOnlyForDeploymentPostprocessing = 0;
236 | };
237 | FAA41325210E2C730097D522 /* Sources */ = {
238 | isa = PBXSourcesBuildPhase;
239 | buildActionMask = 2147483647;
240 | files = (
241 | 5058247B261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift in Sources */,
242 | 0909EBD0245D5C9E00ABA339 /* KeyboardLayoutTests.swift in Sources */,
243 | );
244 | runOnlyForDeploymentPostprocessing = 0;
245 | };
246 | /* End PBXSourcesBuildPhase section */
247 |
248 | /* Begin PBXTargetDependency section */
249 | FAA4132C210E2C730097D522 /* PBXTargetDependency */ = {
250 | isa = PBXTargetDependency;
251 | target = FAA4131F210E2C730097D522 /* Sauce */;
252 | targetProxy = FAA4132B210E2C730097D522 /* PBXContainerItemProxy */;
253 | };
254 | /* End PBXTargetDependency section */
255 |
256 | /* Begin XCBuildConfiguration section */
257 | FAA41332210E2C730097D522 /* Debug */ = {
258 | isa = XCBuildConfiguration;
259 | buildSettings = {
260 | ALWAYS_SEARCH_USER_PATHS = NO;
261 | CLANG_ANALYZER_NONNULL = YES;
262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
264 | CLANG_CXX_LIBRARY = "libc++";
265 | CLANG_ENABLE_MODULES = YES;
266 | CLANG_ENABLE_OBJC_ARC = YES;
267 | CLANG_ENABLE_OBJC_WEAK = YES;
268 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
269 | CLANG_WARN_BOOL_CONVERSION = YES;
270 | CLANG_WARN_COMMA = YES;
271 | CLANG_WARN_CONSTANT_CONVERSION = YES;
272 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
275 | CLANG_WARN_EMPTY_BODY = YES;
276 | CLANG_WARN_ENUM_CONVERSION = YES;
277 | CLANG_WARN_INFINITE_RECURSION = YES;
278 | CLANG_WARN_INT_CONVERSION = YES;
279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
280 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
283 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
285 | CLANG_WARN_STRICT_PROTOTYPES = YES;
286 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
288 | CLANG_WARN_UNREACHABLE_CODE = YES;
289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
290 | CODE_SIGN_IDENTITY = "-";
291 | COPY_PHASE_STRIP = NO;
292 | CURRENT_PROJECT_VERSION = 1;
293 | DEBUG_INFORMATION_FORMAT = dwarf;
294 | ENABLE_STRICT_OBJC_MSGSEND = YES;
295 | ENABLE_TESTABILITY = YES;
296 | GCC_C_LANGUAGE_STANDARD = gnu11;
297 | GCC_DYNAMIC_NO_PIC = NO;
298 | GCC_NO_COMMON_BLOCKS = YES;
299 | GCC_OPTIMIZATION_LEVEL = 0;
300 | GCC_PREPROCESSOR_DEFINITIONS = (
301 | "DEBUG=1",
302 | "$(inherited)",
303 | );
304 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
305 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
306 | GCC_WARN_UNDECLARED_SELECTOR = YES;
307 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
308 | GCC_WARN_UNUSED_FUNCTION = YES;
309 | GCC_WARN_UNUSED_VARIABLE = YES;
310 | MACOSX_DEPLOYMENT_TARGET = 10.13;
311 | MTL_ENABLE_DEBUG_INFO = YES;
312 | ONLY_ACTIVE_ARCH = YES;
313 | SDKROOT = macosx;
314 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
315 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
316 | VERSIONING_SYSTEM = "apple-generic";
317 | VERSION_INFO_PREFIX = "";
318 | };
319 | name = Debug;
320 | };
321 | FAA41333210E2C730097D522 /* Release */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ALWAYS_SEARCH_USER_PATHS = NO;
325 | CLANG_ANALYZER_NONNULL = YES;
326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
328 | CLANG_CXX_LIBRARY = "libc++";
329 | CLANG_ENABLE_MODULES = YES;
330 | CLANG_ENABLE_OBJC_ARC = YES;
331 | CLANG_ENABLE_OBJC_WEAK = YES;
332 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
333 | CLANG_WARN_BOOL_CONVERSION = YES;
334 | CLANG_WARN_COMMA = YES;
335 | CLANG_WARN_CONSTANT_CONVERSION = YES;
336 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
337 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
338 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
339 | CLANG_WARN_EMPTY_BODY = YES;
340 | CLANG_WARN_ENUM_CONVERSION = YES;
341 | CLANG_WARN_INFINITE_RECURSION = YES;
342 | CLANG_WARN_INT_CONVERSION = YES;
343 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
344 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
345 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
347 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
348 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
349 | CLANG_WARN_STRICT_PROTOTYPES = YES;
350 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
351 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
352 | CLANG_WARN_UNREACHABLE_CODE = YES;
353 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
354 | CODE_SIGN_IDENTITY = "-";
355 | COPY_PHASE_STRIP = NO;
356 | CURRENT_PROJECT_VERSION = 1;
357 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
358 | ENABLE_NS_ASSERTIONS = NO;
359 | ENABLE_STRICT_OBJC_MSGSEND = YES;
360 | GCC_C_LANGUAGE_STANDARD = gnu11;
361 | GCC_NO_COMMON_BLOCKS = YES;
362 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
363 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
364 | GCC_WARN_UNDECLARED_SELECTOR = YES;
365 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
366 | GCC_WARN_UNUSED_FUNCTION = YES;
367 | GCC_WARN_UNUSED_VARIABLE = YES;
368 | MACOSX_DEPLOYMENT_TARGET = 10.13;
369 | MTL_ENABLE_DEBUG_INFO = NO;
370 | SDKROOT = macosx;
371 | SWIFT_COMPILATION_MODE = wholemodule;
372 | SWIFT_OPTIMIZATION_LEVEL = "-O";
373 | VERSIONING_SYSTEM = "apple-generic";
374 | VERSION_INFO_PREFIX = "";
375 | };
376 | name = Release;
377 | };
378 | FAA41335210E2C730097D522 /* Debug */ = {
379 | isa = XCBuildConfiguration;
380 | buildSettings = {
381 | CLANG_ENABLE_MODULES = YES;
382 | CODE_SIGN_IDENTITY = "";
383 | CODE_SIGN_STYLE = Automatic;
384 | COMBINE_HIDPI_IMAGES = YES;
385 | DEFINES_MODULE = YES;
386 | DYLIB_COMPATIBILITY_VERSION = 1;
387 | DYLIB_CURRENT_VERSION = 1;
388 | DYLIB_INSTALL_NAME_BASE = "@rpath";
389 | FRAMEWORK_VERSION = A;
390 | INFOPLIST_FILE = Sauce/Info.plist;
391 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
392 | LD_RUNPATH_SEARCH_PATHS = (
393 | "$(inherited)",
394 | "@executable_path/../Frameworks",
395 | "@loader_path/Frameworks",
396 | );
397 | MARKETING_VERSION = 2.4.1;
398 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Sauce";
399 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
400 | SKIP_INSTALL = YES;
401 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
402 | SWIFT_VERSION = 5.0;
403 | };
404 | name = Debug;
405 | };
406 | FAA41336210E2C730097D522 /* Release */ = {
407 | isa = XCBuildConfiguration;
408 | buildSettings = {
409 | CLANG_ENABLE_MODULES = YES;
410 | CODE_SIGN_IDENTITY = "";
411 | CODE_SIGN_STYLE = Automatic;
412 | COMBINE_HIDPI_IMAGES = YES;
413 | DEFINES_MODULE = YES;
414 | DYLIB_COMPATIBILITY_VERSION = 1;
415 | DYLIB_CURRENT_VERSION = 1;
416 | DYLIB_INSTALL_NAME_BASE = "@rpath";
417 | FRAMEWORK_VERSION = A;
418 | INFOPLIST_FILE = Sauce/Info.plist;
419 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
420 | LD_RUNPATH_SEARCH_PATHS = (
421 | "$(inherited)",
422 | "@executable_path/../Frameworks",
423 | "@loader_path/Frameworks",
424 | );
425 | MARKETING_VERSION = 2.4.1;
426 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Sauce";
427 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
428 | SKIP_INSTALL = YES;
429 | SWIFT_VERSION = 5.0;
430 | };
431 | name = Release;
432 | };
433 | FAA41338210E2C730097D522 /* Debug */ = {
434 | isa = XCBuildConfiguration;
435 | buildSettings = {
436 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
437 | CODE_SIGN_STYLE = Automatic;
438 | COMBINE_HIDPI_IMAGES = YES;
439 | INFOPLIST_FILE = SauceTests/Info.plist;
440 | LD_RUNPATH_SEARCH_PATHS = (
441 | "$(inherited)",
442 | "@executable_path/../Frameworks",
443 | "@loader_path/../Frameworks",
444 | );
445 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.SauceTests";
446 | PRODUCT_NAME = "$(TARGET_NAME)";
447 | SWIFT_VERSION = 5.0;
448 | };
449 | name = Debug;
450 | };
451 | FAA41339210E2C730097D522 /* Release */ = {
452 | isa = XCBuildConfiguration;
453 | buildSettings = {
454 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
455 | CODE_SIGN_STYLE = Automatic;
456 | COMBINE_HIDPI_IMAGES = YES;
457 | INFOPLIST_FILE = SauceTests/Info.plist;
458 | LD_RUNPATH_SEARCH_PATHS = (
459 | "$(inherited)",
460 | "@executable_path/../Frameworks",
461 | "@loader_path/../Frameworks",
462 | );
463 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.SauceTests";
464 | PRODUCT_NAME = "$(TARGET_NAME)";
465 | SWIFT_VERSION = 5.0;
466 | };
467 | name = Release;
468 | };
469 | /* End XCBuildConfiguration section */
470 |
471 | /* Begin XCConfigurationList section */
472 | FAA4131A210E2C730097D522 /* Build configuration list for PBXProject "Sauce" */ = {
473 | isa = XCConfigurationList;
474 | buildConfigurations = (
475 | FAA41332210E2C730097D522 /* Debug */,
476 | FAA41333210E2C730097D522 /* Release */,
477 | );
478 | defaultConfigurationIsVisible = 0;
479 | defaultConfigurationName = Release;
480 | };
481 | FAA41334210E2C730097D522 /* Build configuration list for PBXNativeTarget "Sauce" */ = {
482 | isa = XCConfigurationList;
483 | buildConfigurations = (
484 | FAA41335210E2C730097D522 /* Debug */,
485 | FAA41336210E2C730097D522 /* Release */,
486 | );
487 | defaultConfigurationIsVisible = 0;
488 | defaultConfigurationName = Release;
489 | };
490 | FAA41337210E2C730097D522 /* Build configuration list for PBXNativeTarget "SauceTests" */ = {
491 | isa = XCConfigurationList;
492 | buildConfigurations = (
493 | FAA41338210E2C730097D522 /* Debug */,
494 | FAA41339210E2C730097D522 /* Release */,
495 | );
496 | defaultConfigurationIsVisible = 0;
497 | defaultConfigurationName = Release;
498 | };
499 | /* End XCConfigurationList section */
500 | };
501 | rootObject = FAA41317210E2C730097D522 /* Project object */;
502 | }
503 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C5F5702727AFCFE90079D5F0 /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5F5702427AFCFDB0079D5F0 /* Sauce.framework */; };
11 | C5F5702827AFCFE90079D5F0 /* Sauce.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C5F5702427AFCFDB0079D5F0 /* Sauce.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
12 | FAA41347210E2C960097D522 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41346210E2C960097D522 /* AppDelegate.swift */; };
13 | FAA41349210E2C960097D522 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41348210E2C960097D522 /* ViewController.swift */; };
14 | FAA4134B210E2C970097D522 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAA4134A210E2C970097D522 /* Assets.xcassets */; };
15 | FAA4134E210E2C970097D522 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAA4134C210E2C970097D522 /* Main.storyboard */; };
16 | FAA4135A210E2C970097D522 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA41359210E2C970097D522 /* ExampleTests.swift */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXContainerItemProxy section */
20 | C5F5702327AFCFDB0079D5F0 /* PBXContainerItemProxy */ = {
21 | isa = PBXContainerItemProxy;
22 | containerPortal = C5F5701E27AFCFDB0079D5F0 /* Sauce.xcodeproj */;
23 | proxyType = 2;
24 | remoteGlobalIDString = FAA41320210E2C730097D522;
25 | remoteInfo = Sauce;
26 | };
27 | C5F5702527AFCFDB0079D5F0 /* PBXContainerItemProxy */ = {
28 | isa = PBXContainerItemProxy;
29 | containerPortal = C5F5701E27AFCFDB0079D5F0 /* Sauce.xcodeproj */;
30 | proxyType = 2;
31 | remoteGlobalIDString = FAA41329210E2C730097D522;
32 | remoteInfo = SauceTests;
33 | };
34 | FAA41356210E2C970097D522 /* PBXContainerItemProxy */ = {
35 | isa = PBXContainerItemProxy;
36 | containerPortal = FAA4133B210E2C960097D522 /* Project object */;
37 | proxyType = 1;
38 | remoteGlobalIDString = FAA41342210E2C960097D522;
39 | remoteInfo = Example;
40 | };
41 | /* End PBXContainerItemProxy section */
42 |
43 | /* Begin PBXCopyFilesBuildPhase section */
44 | FAA4137A210E3CC20097D522 /* Embed Frameworks */ = {
45 | isa = PBXCopyFilesBuildPhase;
46 | buildActionMask = 2147483647;
47 | dstPath = "";
48 | dstSubfolderSpec = 10;
49 | files = (
50 | C5F5702827AFCFE90079D5F0 /* Sauce.framework in Embed Frameworks */,
51 | );
52 | name = "Embed Frameworks";
53 | runOnlyForDeploymentPostprocessing = 0;
54 | };
55 | /* End PBXCopyFilesBuildPhase section */
56 |
57 | /* Begin PBXFileReference section */
58 | C5F5701E27AFCFDB0079D5F0 /* Sauce.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sauce.xcodeproj; path = ../Lib/Sauce.xcodeproj; sourceTree = ""; };
59 | FAA41343210E2C960097D522 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
60 | FAA41346210E2C960097D522 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
61 | FAA41348210E2C960097D522 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
62 | FAA4134A210E2C970097D522 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
63 | FAA4134D210E2C970097D522 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
64 | FAA4134F210E2C970097D522 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
65 | FAA41350210E2C970097D522 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; };
66 | FAA41355210E2C970097D522 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
67 | FAA41359210E2C970097D522 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; };
68 | FAA4135B210E2C970097D522 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
69 | /* End PBXFileReference section */
70 |
71 | /* Begin PBXFrameworksBuildPhase section */
72 | FAA41340210E2C960097D522 /* Frameworks */ = {
73 | isa = PBXFrameworksBuildPhase;
74 | buildActionMask = 2147483647;
75 | files = (
76 | C5F5702727AFCFE90079D5F0 /* Sauce.framework in Frameworks */,
77 | );
78 | runOnlyForDeploymentPostprocessing = 0;
79 | };
80 | FAA41352210E2C970097D522 /* Frameworks */ = {
81 | isa = PBXFrameworksBuildPhase;
82 | buildActionMask = 2147483647;
83 | files = (
84 | );
85 | runOnlyForDeploymentPostprocessing = 0;
86 | };
87 | /* End PBXFrameworksBuildPhase section */
88 |
89 | /* Begin PBXGroup section */
90 | C5F5701D27AFCFC60079D5F0 /* Frameworks */ = {
91 | isa = PBXGroup;
92 | children = (
93 | C5F5701E27AFCFDB0079D5F0 /* Sauce.xcodeproj */,
94 | );
95 | name = Frameworks;
96 | sourceTree = "";
97 | };
98 | C5F5701F27AFCFDB0079D5F0 /* Products */ = {
99 | isa = PBXGroup;
100 | children = (
101 | C5F5702427AFCFDB0079D5F0 /* Sauce.framework */,
102 | C5F5702627AFCFDB0079D5F0 /* SauceTests.xctest */,
103 | );
104 | name = Products;
105 | sourceTree = "";
106 | };
107 | FAA4133A210E2C960097D522 = {
108 | isa = PBXGroup;
109 | children = (
110 | FAA41345210E2C960097D522 /* Example */,
111 | FAA41358210E2C970097D522 /* ExampleTests */,
112 | C5F5701D27AFCFC60079D5F0 /* Frameworks */,
113 | FAA41344210E2C960097D522 /* Products */,
114 | );
115 | sourceTree = "";
116 | };
117 | FAA41344210E2C960097D522 /* Products */ = {
118 | isa = PBXGroup;
119 | children = (
120 | FAA41343210E2C960097D522 /* Example.app */,
121 | FAA41355210E2C970097D522 /* ExampleTests.xctest */,
122 | );
123 | name = Products;
124 | sourceTree = "";
125 | };
126 | FAA41345210E2C960097D522 /* Example */ = {
127 | isa = PBXGroup;
128 | children = (
129 | FAA41346210E2C960097D522 /* AppDelegate.swift */,
130 | FAA41348210E2C960097D522 /* ViewController.swift */,
131 | FAA4134A210E2C970097D522 /* Assets.xcassets */,
132 | FAA4134C210E2C970097D522 /* Main.storyboard */,
133 | FAA4134F210E2C970097D522 /* Info.plist */,
134 | FAA41350210E2C970097D522 /* Example.entitlements */,
135 | );
136 | path = Example;
137 | sourceTree = "";
138 | };
139 | FAA41358210E2C970097D522 /* ExampleTests */ = {
140 | isa = PBXGroup;
141 | children = (
142 | FAA41359210E2C970097D522 /* ExampleTests.swift */,
143 | FAA4135B210E2C970097D522 /* Info.plist */,
144 | );
145 | path = ExampleTests;
146 | sourceTree = "";
147 | };
148 | /* End PBXGroup section */
149 |
150 | /* Begin PBXNativeTarget section */
151 | FAA41342210E2C960097D522 /* Example */ = {
152 | isa = PBXNativeTarget;
153 | buildConfigurationList = FAA4135E210E2C970097D522 /* Build configuration list for PBXNativeTarget "Example" */;
154 | buildPhases = (
155 | FAA4133F210E2C960097D522 /* Sources */,
156 | FAA41340210E2C960097D522 /* Frameworks */,
157 | FAA41341210E2C960097D522 /* Resources */,
158 | FAA4137A210E3CC20097D522 /* Embed Frameworks */,
159 | );
160 | buildRules = (
161 | );
162 | dependencies = (
163 | );
164 | name = Example;
165 | productName = Example;
166 | productReference = FAA41343210E2C960097D522 /* Example.app */;
167 | productType = "com.apple.product-type.application";
168 | };
169 | FAA41354210E2C970097D522 /* ExampleTests */ = {
170 | isa = PBXNativeTarget;
171 | buildConfigurationList = FAA41361210E2C970097D522 /* Build configuration list for PBXNativeTarget "ExampleTests" */;
172 | buildPhases = (
173 | FAA41351210E2C970097D522 /* Sources */,
174 | FAA41352210E2C970097D522 /* Frameworks */,
175 | FAA41353210E2C970097D522 /* Resources */,
176 | );
177 | buildRules = (
178 | );
179 | dependencies = (
180 | FAA41357210E2C970097D522 /* PBXTargetDependency */,
181 | );
182 | name = ExampleTests;
183 | productName = ExampleTests;
184 | productReference = FAA41355210E2C970097D522 /* ExampleTests.xctest */;
185 | productType = "com.apple.product-type.bundle.unit-test";
186 | };
187 | /* End PBXNativeTarget section */
188 |
189 | /* Begin PBXProject section */
190 | FAA4133B210E2C960097D522 /* Project object */ = {
191 | isa = PBXProject;
192 | attributes = {
193 | LastSwiftUpdateCheck = 0940;
194 | LastUpgradeCheck = 1320;
195 | ORGANIZATIONNAME = "Clipy Project";
196 | TargetAttributes = {
197 | FAA41342210E2C960097D522 = {
198 | CreatedOnToolsVersion = 9.4.1;
199 | LastSwiftMigration = 1020;
200 | };
201 | FAA41354210E2C970097D522 = {
202 | CreatedOnToolsVersion = 9.4.1;
203 | LastSwiftMigration = 1020;
204 | TestTargetID = FAA41342210E2C960097D522;
205 | };
206 | };
207 | };
208 | buildConfigurationList = FAA4133E210E2C960097D522 /* Build configuration list for PBXProject "Example" */;
209 | compatibilityVersion = "Xcode 9.3";
210 | developmentRegion = en;
211 | hasScannedForEncodings = 0;
212 | knownRegions = (
213 | en,
214 | Base,
215 | );
216 | mainGroup = FAA4133A210E2C960097D522;
217 | productRefGroup = FAA41344210E2C960097D522 /* Products */;
218 | projectDirPath = "";
219 | projectReferences = (
220 | {
221 | ProductGroup = C5F5701F27AFCFDB0079D5F0 /* Products */;
222 | ProjectRef = C5F5701E27AFCFDB0079D5F0 /* Sauce.xcodeproj */;
223 | },
224 | );
225 | projectRoot = "";
226 | targets = (
227 | FAA41342210E2C960097D522 /* Example */,
228 | FAA41354210E2C970097D522 /* ExampleTests */,
229 | );
230 | };
231 | /* End PBXProject section */
232 |
233 | /* Begin PBXReferenceProxy section */
234 | C5F5702427AFCFDB0079D5F0 /* Sauce.framework */ = {
235 | isa = PBXReferenceProxy;
236 | fileType = wrapper.framework;
237 | path = Sauce.framework;
238 | remoteRef = C5F5702327AFCFDB0079D5F0 /* PBXContainerItemProxy */;
239 | sourceTree = BUILT_PRODUCTS_DIR;
240 | };
241 | C5F5702627AFCFDB0079D5F0 /* SauceTests.xctest */ = {
242 | isa = PBXReferenceProxy;
243 | fileType = wrapper.cfbundle;
244 | path = SauceTests.xctest;
245 | remoteRef = C5F5702527AFCFDB0079D5F0 /* PBXContainerItemProxy */;
246 | sourceTree = BUILT_PRODUCTS_DIR;
247 | };
248 | /* End PBXReferenceProxy section */
249 |
250 | /* Begin PBXResourcesBuildPhase section */
251 | FAA41341210E2C960097D522 /* Resources */ = {
252 | isa = PBXResourcesBuildPhase;
253 | buildActionMask = 2147483647;
254 | files = (
255 | FAA4134B210E2C970097D522 /* Assets.xcassets in Resources */,
256 | FAA4134E210E2C970097D522 /* Main.storyboard in Resources */,
257 | );
258 | runOnlyForDeploymentPostprocessing = 0;
259 | };
260 | FAA41353210E2C970097D522 /* Resources */ = {
261 | isa = PBXResourcesBuildPhase;
262 | buildActionMask = 2147483647;
263 | files = (
264 | );
265 | runOnlyForDeploymentPostprocessing = 0;
266 | };
267 | /* End PBXResourcesBuildPhase section */
268 |
269 | /* Begin PBXSourcesBuildPhase section */
270 | FAA4133F210E2C960097D522 /* Sources */ = {
271 | isa = PBXSourcesBuildPhase;
272 | buildActionMask = 2147483647;
273 | files = (
274 | FAA41349210E2C960097D522 /* ViewController.swift in Sources */,
275 | FAA41347210E2C960097D522 /* AppDelegate.swift in Sources */,
276 | );
277 | runOnlyForDeploymentPostprocessing = 0;
278 | };
279 | FAA41351210E2C970097D522 /* Sources */ = {
280 | isa = PBXSourcesBuildPhase;
281 | buildActionMask = 2147483647;
282 | files = (
283 | FAA4135A210E2C970097D522 /* ExampleTests.swift in Sources */,
284 | );
285 | runOnlyForDeploymentPostprocessing = 0;
286 | };
287 | /* End PBXSourcesBuildPhase section */
288 |
289 | /* Begin PBXTargetDependency section */
290 | FAA41357210E2C970097D522 /* PBXTargetDependency */ = {
291 | isa = PBXTargetDependency;
292 | target = FAA41342210E2C960097D522 /* Example */;
293 | targetProxy = FAA41356210E2C970097D522 /* PBXContainerItemProxy */;
294 | };
295 | /* End PBXTargetDependency section */
296 |
297 | /* Begin PBXVariantGroup section */
298 | FAA4134C210E2C970097D522 /* Main.storyboard */ = {
299 | isa = PBXVariantGroup;
300 | children = (
301 | FAA4134D210E2C970097D522 /* Base */,
302 | );
303 | name = Main.storyboard;
304 | sourceTree = "";
305 | };
306 | /* End PBXVariantGroup section */
307 |
308 | /* Begin XCBuildConfiguration section */
309 | FAA4135C210E2C970097D522 /* Debug */ = {
310 | isa = XCBuildConfiguration;
311 | buildSettings = {
312 | ALWAYS_SEARCH_USER_PATHS = NO;
313 | CLANG_ANALYZER_NONNULL = YES;
314 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
316 | CLANG_CXX_LIBRARY = "libc++";
317 | CLANG_ENABLE_MODULES = YES;
318 | CLANG_ENABLE_OBJC_ARC = YES;
319 | CLANG_ENABLE_OBJC_WEAK = YES;
320 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
321 | CLANG_WARN_BOOL_CONVERSION = YES;
322 | CLANG_WARN_COMMA = YES;
323 | CLANG_WARN_CONSTANT_CONVERSION = YES;
324 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
325 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
326 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
327 | CLANG_WARN_EMPTY_BODY = YES;
328 | CLANG_WARN_ENUM_CONVERSION = YES;
329 | CLANG_WARN_INFINITE_RECURSION = YES;
330 | CLANG_WARN_INT_CONVERSION = YES;
331 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
332 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
333 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
334 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
335 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
336 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
337 | CLANG_WARN_STRICT_PROTOTYPES = YES;
338 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
339 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
340 | CLANG_WARN_UNREACHABLE_CODE = YES;
341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
342 | CODE_SIGN_IDENTITY = "-";
343 | COPY_PHASE_STRIP = NO;
344 | DEBUG_INFORMATION_FORMAT = dwarf;
345 | ENABLE_STRICT_OBJC_MSGSEND = YES;
346 | ENABLE_TESTABILITY = YES;
347 | GCC_C_LANGUAGE_STANDARD = gnu11;
348 | GCC_DYNAMIC_NO_PIC = NO;
349 | GCC_NO_COMMON_BLOCKS = YES;
350 | GCC_OPTIMIZATION_LEVEL = 0;
351 | GCC_PREPROCESSOR_DEFINITIONS = (
352 | "DEBUG=1",
353 | "$(inherited)",
354 | );
355 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
356 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
357 | GCC_WARN_UNDECLARED_SELECTOR = YES;
358 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
359 | GCC_WARN_UNUSED_FUNCTION = YES;
360 | GCC_WARN_UNUSED_VARIABLE = YES;
361 | MACOSX_DEPLOYMENT_TARGET = 10.13;
362 | MTL_ENABLE_DEBUG_INFO = YES;
363 | ONLY_ACTIVE_ARCH = YES;
364 | SDKROOT = macosx;
365 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
366 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
367 | };
368 | name = Debug;
369 | };
370 | FAA4135D210E2C970097D522 /* Release */ = {
371 | isa = XCBuildConfiguration;
372 | buildSettings = {
373 | ALWAYS_SEARCH_USER_PATHS = NO;
374 | CLANG_ANALYZER_NONNULL = YES;
375 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
376 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
377 | CLANG_CXX_LIBRARY = "libc++";
378 | CLANG_ENABLE_MODULES = YES;
379 | CLANG_ENABLE_OBJC_ARC = YES;
380 | CLANG_ENABLE_OBJC_WEAK = YES;
381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
382 | CLANG_WARN_BOOL_CONVERSION = YES;
383 | CLANG_WARN_COMMA = YES;
384 | CLANG_WARN_CONSTANT_CONVERSION = YES;
385 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
387 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
388 | CLANG_WARN_EMPTY_BODY = YES;
389 | CLANG_WARN_ENUM_CONVERSION = YES;
390 | CLANG_WARN_INFINITE_RECURSION = YES;
391 | CLANG_WARN_INT_CONVERSION = YES;
392 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
393 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
396 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
398 | CLANG_WARN_STRICT_PROTOTYPES = YES;
399 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
400 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
401 | CLANG_WARN_UNREACHABLE_CODE = YES;
402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
403 | CODE_SIGN_IDENTITY = "-";
404 | COPY_PHASE_STRIP = NO;
405 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
406 | ENABLE_NS_ASSERTIONS = NO;
407 | ENABLE_STRICT_OBJC_MSGSEND = YES;
408 | GCC_C_LANGUAGE_STANDARD = gnu11;
409 | GCC_NO_COMMON_BLOCKS = YES;
410 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
411 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
412 | GCC_WARN_UNDECLARED_SELECTOR = YES;
413 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
414 | GCC_WARN_UNUSED_FUNCTION = YES;
415 | GCC_WARN_UNUSED_VARIABLE = YES;
416 | MACOSX_DEPLOYMENT_TARGET = 10.13;
417 | MTL_ENABLE_DEBUG_INFO = NO;
418 | SDKROOT = macosx;
419 | SWIFT_COMPILATION_MODE = wholemodule;
420 | SWIFT_OPTIMIZATION_LEVEL = "-O";
421 | };
422 | name = Release;
423 | };
424 | FAA4135F210E2C970097D522 /* Debug */ = {
425 | isa = XCBuildConfiguration;
426 | buildSettings = {
427 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
429 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
430 | CODE_SIGN_IDENTITY = "-";
431 | CODE_SIGN_STYLE = Automatic;
432 | COMBINE_HIDPI_IMAGES = YES;
433 | INFOPLIST_FILE = Example/Info.plist;
434 | LD_RUNPATH_SEARCH_PATHS = (
435 | "$(inherited)",
436 | "@executable_path/../Frameworks",
437 | );
438 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Example";
439 | PRODUCT_NAME = "$(TARGET_NAME)";
440 | SWIFT_VERSION = 5.0;
441 | };
442 | name = Debug;
443 | };
444 | FAA41360210E2C970097D522 /* Release */ = {
445 | isa = XCBuildConfiguration;
446 | buildSettings = {
447 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
449 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
450 | CODE_SIGN_IDENTITY = "-";
451 | CODE_SIGN_STYLE = Automatic;
452 | COMBINE_HIDPI_IMAGES = YES;
453 | INFOPLIST_FILE = Example/Info.plist;
454 | LD_RUNPATH_SEARCH_PATHS = (
455 | "$(inherited)",
456 | "@executable_path/../Frameworks",
457 | );
458 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Example";
459 | PRODUCT_NAME = "$(TARGET_NAME)";
460 | SWIFT_VERSION = 5.0;
461 | };
462 | name = Release;
463 | };
464 | FAA41362210E2C970097D522 /* Debug */ = {
465 | isa = XCBuildConfiguration;
466 | buildSettings = {
467 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
468 | BUNDLE_LOADER = "$(TEST_HOST)";
469 | CODE_SIGN_STYLE = Automatic;
470 | COMBINE_HIDPI_IMAGES = YES;
471 | INFOPLIST_FILE = ExampleTests/Info.plist;
472 | LD_RUNPATH_SEARCH_PATHS = (
473 | "$(inherited)",
474 | "@executable_path/../Frameworks",
475 | "@loader_path/../Frameworks",
476 | );
477 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.ExampleTests";
478 | PRODUCT_NAME = "$(TARGET_NAME)";
479 | SWIFT_VERSION = 5.0;
480 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Contents/MacOS/Example";
481 | };
482 | name = Debug;
483 | };
484 | FAA41363210E2C970097D522 /* Release */ = {
485 | isa = XCBuildConfiguration;
486 | buildSettings = {
487 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
488 | BUNDLE_LOADER = "$(TEST_HOST)";
489 | CODE_SIGN_STYLE = Automatic;
490 | COMBINE_HIDPI_IMAGES = YES;
491 | INFOPLIST_FILE = ExampleTests/Info.plist;
492 | LD_RUNPATH_SEARCH_PATHS = (
493 | "$(inherited)",
494 | "@executable_path/../Frameworks",
495 | "@loader_path/../Frameworks",
496 | );
497 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.ExampleTests";
498 | PRODUCT_NAME = "$(TARGET_NAME)";
499 | SWIFT_VERSION = 5.0;
500 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Contents/MacOS/Example";
501 | };
502 | name = Release;
503 | };
504 | /* End XCBuildConfiguration section */
505 |
506 | /* Begin XCConfigurationList section */
507 | FAA4133E210E2C960097D522 /* Build configuration list for PBXProject "Example" */ = {
508 | isa = XCConfigurationList;
509 | buildConfigurations = (
510 | FAA4135C210E2C970097D522 /* Debug */,
511 | FAA4135D210E2C970097D522 /* Release */,
512 | );
513 | defaultConfigurationIsVisible = 0;
514 | defaultConfigurationName = Release;
515 | };
516 | FAA4135E210E2C970097D522 /* Build configuration list for PBXNativeTarget "Example" */ = {
517 | isa = XCConfigurationList;
518 | buildConfigurations = (
519 | FAA4135F210E2C970097D522 /* Debug */,
520 | FAA41360210E2C970097D522 /* Release */,
521 | );
522 | defaultConfigurationIsVisible = 0;
523 | defaultConfigurationName = Release;
524 | };
525 | FAA41361210E2C970097D522 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = {
526 | isa = XCConfigurationList;
527 | buildConfigurations = (
528 | FAA41362210E2C970097D522 /* Debug */,
529 | FAA41363210E2C970097D522 /* Release */,
530 | );
531 | defaultConfigurationIsVisible = 0;
532 | defaultConfigurationName = Release;
533 | };
534 | /* End XCConfigurationList section */
535 | };
536 | rootObject = FAA4133B210E2C960097D522 /* Project object */;
537 | }
538 |
--------------------------------------------------------------------------------
/Lib/Sauce/Key.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Key.swift
3 | //
4 | // Sauce
5 | // GitHub: https://github.com/clipy
6 | // HP: https://clipy-app.com
7 | //
8 | // Copyright © 2015-2020 Clipy Project.
9 | //
10 |
11 | #if os(macOS)
12 | import Foundation
13 | import Carbon
14 |
15 | // swiftlint:disable file_length function_body_length type_body_length identifier_name
16 | public enum Key: String, Codable, Equatable, Sendable {
17 | case a
18 | case s
19 | case d
20 | case f
21 | case h
22 | case g
23 | case z
24 | case x
25 | case c
26 | case v
27 | case b
28 | case q
29 | case w
30 | case e
31 | case r
32 | case y
33 | case t
34 | case one
35 | case two
36 | case three
37 | case four
38 | case six
39 | case five
40 | case equal
41 | case nine
42 | case seven
43 | case minus
44 | case eight
45 | case zero
46 | case rightBracket
47 | case o
48 | case u
49 | case leftBracket
50 | case i
51 | case p
52 | case l
53 | case j
54 | case quote
55 | case k
56 | case semicolon
57 | case backslash
58 | case comma
59 | case slash
60 | case n
61 | case m
62 | case period
63 | case grave
64 | case keypadDecimal
65 | case keypadMultiply
66 | case keypadPlus
67 | case keypadClear
68 | case keypadDivide
69 | case keypadEnter
70 | case keypadMinus
71 | case keypadEquals
72 | case keypadZero
73 | case keypadOne
74 | case keypadTwo
75 | case keypadThree
76 | case keypadFour
77 | case keypadFive
78 | case keypadSix
79 | case keypadSeven
80 | case keypadEight
81 | case keypadNine
82 | /* keycodes for keys that are independent of keyboard layout */
83 | case `return`
84 | case tab
85 | case space
86 | case delete
87 | case escape
88 | case f17
89 | case f18
90 | case f19
91 | case f20
92 | case f5
93 | case f6
94 | case f7
95 | case f3
96 | case f8
97 | case f9
98 | case f11
99 | case f13
100 | case f16
101 | case f14
102 | case f10
103 | case f12
104 | case f15
105 | case help
106 | case home
107 | case pageUp
108 | case forwardDelete
109 | case f4
110 | case end
111 | case f2
112 | case pageDown
113 | case f1
114 | case leftArrow
115 | case rightArrow
116 | case downArrow
117 | case upArrow
118 | /* keycodes for JIS keyboard only */
119 | case yen
120 | case underscore
121 | case keypadComma
122 | case eisu
123 | case kana
124 | case atSign
125 | case caret
126 | case colon
127 | /* keycodes for ISO keyboard only */
128 | case section
129 |
130 | // MARK: - Initiazlie
131 | public init?(character: String, virtualKeyCode: Int?) {
132 | let lowercasedString = character.lowercased()
133 | switch lowercasedString {
134 | case "a": self = .a
135 | case "s": self = .s
136 | case "d": self = .d
137 | case "f": self = .f
138 | case "h": self = .h
139 | case "g": self = .g
140 | case "z": self = .z
141 | case "x": self = .x
142 | case "c": self = .c
143 | case "v": self = .v
144 | case "b": self = .b
145 | case "q": self = .q
146 | case "w": self = .w
147 | case "e": self = .e
148 | case "r": self = .r
149 | case "y": self = .y
150 | case "t": self = .t
151 | case "1" where virtualKeyCode != kVK_ANSI_Keypad1,
152 | "one" where virtualKeyCode != kVK_ANSI_Keypad1: self = .one
153 | case "2" where virtualKeyCode != kVK_ANSI_Keypad2,
154 | "two" where virtualKeyCode != kVK_ANSI_Keypad2: self = .two
155 | case "3" where virtualKeyCode != kVK_ANSI_Keypad3,
156 | "three" where virtualKeyCode != kVK_ANSI_Keypad3: self = .three
157 | case "4" where virtualKeyCode != kVK_ANSI_Keypad4,
158 | "four" where virtualKeyCode != kVK_ANSI_Keypad4: self = .four
159 | case "6" where virtualKeyCode != kVK_ANSI_Keypad6,
160 | "six" where virtualKeyCode != kVK_ANSI_Keypad6: self = .six
161 | case "5" where virtualKeyCode != kVK_ANSI_Keypad5,
162 | "five" where virtualKeyCode != kVK_ANSI_Keypad5: self = .five
163 | case "equal" where virtualKeyCode != kVK_ANSI_KeypadEquals,
164 | "=" where virtualKeyCode != kVK_ANSI_KeypadEquals: self = .equal
165 | case "9" where virtualKeyCode != kVK_ANSI_Keypad9,
166 | "nine" where virtualKeyCode != kVK_ANSI_Keypad9: self = .nine
167 | case "7" where virtualKeyCode != kVK_ANSI_Keypad7,
168 | "seven" where virtualKeyCode != kVK_ANSI_Keypad7: self = .seven
169 | case "minus" where virtualKeyCode != kVK_ANSI_KeypadMinus,
170 | "-" where virtualKeyCode != kVK_ANSI_KeypadMinus: self = .minus
171 | case "8" where virtualKeyCode != kVK_ANSI_Keypad8,
172 | "eight" where virtualKeyCode != kVK_ANSI_Keypad8: self = .eight
173 | case "0" where virtualKeyCode != kVK_ANSI_Keypad0,
174 | "zero" where virtualKeyCode != kVK_ANSI_Keypad0: self = .zero
175 | case "rightbracket",
176 | "]": self = .rightBracket
177 | case "o": self = .o
178 | case "u": self = .u
179 | case "leftbracket",
180 | "[": self = .leftBracket
181 | case "i": self = .i
182 | case "p": self = .p
183 | case "l": self = .l
184 | case "j": self = .j
185 | case "quote",
186 | "'": self = .quote
187 | case "k": self = .k
188 | case "semicolon",
189 | ";": self = .semicolon
190 | case "backslash",
191 | "\\": self = .backslash
192 | case "comma",
193 | "," where virtualKeyCode != kVK_JIS_KeypadComma: self = .comma
194 | case "slash" where virtualKeyCode != kVK_ANSI_KeypadDivide,
195 | "/" where virtualKeyCode != kVK_ANSI_KeypadDivide: self = .slash
196 | case "n": self = .n
197 | case "m": self = .m
198 | case "period",
199 | "." where virtualKeyCode != kVK_ANSI_KeypadDecimal: self = .period
200 | case "grave",
201 | "`": self = .grave
202 | case "keypaddecimal",
203 | "." where virtualKeyCode == kVK_ANSI_KeypadDecimal: self = .keypadDecimal
204 | case "keypadmultiply",
205 | "*": self = .keypadMultiply
206 | case "keypadplus",
207 | "+": self = .keypadPlus
208 | case "keypadclear",
209 | SpecialKeyCode.keypadClear.character.lowercased(): self = .keypadClear
210 | case "keypaddivide",
211 | "/" where virtualKeyCode == kVK_ANSI_KeypadDivide: self = .keypadDivide
212 | case "keypadenter",
213 | SpecialKeyCode.keypadEnter.character.lowercased(): self = .keypadEnter
214 | case "keypadminus",
215 | "-" where virtualKeyCode == kVK_ANSI_KeypadMinus: self = .keypadMinus
216 | case "keypadequals",
217 | "=" where virtualKeyCode == kVK_ANSI_KeypadEquals: self = .keypadEquals
218 | case "keypad0",
219 | "keypadzero",
220 | "0" where virtualKeyCode == kVK_ANSI_Keypad0: self = .keypadZero
221 | case "keypad1",
222 | "keypadone",
223 | "1" where virtualKeyCode == kVK_ANSI_Keypad1: self = .keypadOne
224 | case "keypad2",
225 | "keypadtwo",
226 | "2" where virtualKeyCode == kVK_ANSI_Keypad2: self = .keypadTwo
227 | case "keypad3",
228 | "keypadthree",
229 | "3" where virtualKeyCode == kVK_ANSI_Keypad3: self = .keypadThree
230 | case "keypad4",
231 | "keypadfour",
232 | "4" where virtualKeyCode == kVK_ANSI_Keypad4: self = .keypadFour
233 | case "keypad5",
234 | "keypadfive",
235 | "5" where virtualKeyCode == kVK_ANSI_Keypad5: self = .keypadFive
236 | case "keypad6",
237 | "keypadsix",
238 | "6" where virtualKeyCode == kVK_ANSI_Keypad6: self = .keypadSix
239 | case "keypad7",
240 | "keypadseven",
241 | "7" where virtualKeyCode == kVK_ANSI_Keypad7: self = .keypadSeven
242 | case "keypad8",
243 | "keypadeight",
244 | "8" where virtualKeyCode == kVK_ANSI_Keypad8: self = .keypadEight
245 | case "keypad9",
246 | "keypadnine",
247 | "9" where virtualKeyCode == kVK_ANSI_Keypad9: self = .keypadNine
248 | case "return",
249 | "\r", // NSMenuItem.keyEquivalent for return key
250 | "\n",
251 | SpecialKeyCode.return.character.lowercased(): self = .return
252 | case "tab",
253 | SpecialKeyCode.tab.character.lowercased(): self = .tab
254 | case "space",
255 | SpecialKeyCode.space.character.lowercased(): self = .space
256 | case "delete",
257 | SpecialKeyCode.delete.character.lowercased(): self = .delete
258 | case "escape",
259 | SpecialKeyCode.escape.character.lowercased(): self = .escape
260 | case "f17",
261 | SpecialKeyCode.f17.character.lowercased(): self = .f17
262 | case "f18",
263 | SpecialKeyCode.f18.character.lowercased(): self = .f18
264 | case "f19",
265 | SpecialKeyCode.f19.character.lowercased(): self = .f19
266 | case "f20",
267 | SpecialKeyCode.f20.character.lowercased(): self = .f20
268 | case "f5",
269 | SpecialKeyCode.f5.character.lowercased(): self = .f5
270 | case "f6",
271 | SpecialKeyCode.f6.character.lowercased(): self = .f6
272 | case "f7",
273 | SpecialKeyCode.f7.character.lowercased(): self = .f7
274 | case "f3",
275 | SpecialKeyCode.f3.character.lowercased(): self = .f3
276 | case "f8",
277 | SpecialKeyCode.f8.character.lowercased(): self = .f8
278 | case "f9",
279 | SpecialKeyCode.f9.character.lowercased(): self = .f9
280 | case "f11",
281 | SpecialKeyCode.f11.character.lowercased(): self = .f11
282 | case "f13",
283 | SpecialKeyCode.f13.character.lowercased(): self = .f13
284 | case "f16",
285 | SpecialKeyCode.f16.character.lowercased(): self = .f16
286 | case "f14",
287 | SpecialKeyCode.f14.character.lowercased(): self = .f14
288 | case "f10",
289 | SpecialKeyCode.f10.character.lowercased(): self = .f10
290 | case "f12",
291 | SpecialKeyCode.f12.character.lowercased(): self = .f12
292 | case "f15",
293 | SpecialKeyCode.f15.character.lowercased(): self = .f15
294 | case "help",
295 | SpecialKeyCode.help.character.lowercased(): self = .help
296 | case "home",
297 | SpecialKeyCode.home.character.lowercased(): self = .home
298 | case "pageup",
299 | SpecialKeyCode.pageUp.character.lowercased(): self = .pageUp
300 | case "forwarddelete",
301 | SpecialKeyCode.forwardDelete.character.lowercased(): self = .forwardDelete
302 | case "f4",
303 | SpecialKeyCode.f4.character.lowercased(): self = .f4
304 | case "end",
305 | SpecialKeyCode.end.character.lowercased(): self = .end
306 | case "f2",
307 | SpecialKeyCode.f2.character.lowercased(): self = .f2
308 | case "pagedown",
309 | SpecialKeyCode.pageDown.character.lowercased(): self = .pageDown
310 | case "f1",
311 | SpecialKeyCode.f1.character.lowercased(): self = .f1
312 | case "leftarrow",
313 | SpecialKeyCode.leftArrow.character.lowercased(): self = .leftArrow
314 | case "rightarrow",
315 | SpecialKeyCode.rightArrow.character.lowercased(): self = .rightArrow
316 | case "downarrow",
317 | SpecialKeyCode.downArrow.character.lowercased(): self = .downArrow
318 | case "uparrow",
319 | SpecialKeyCode.upArrow.character.lowercased(): self = .upArrow
320 | case "¥": self = .yen
321 | case "_": self = .underscore
322 | case "," where virtualKeyCode == kVK_JIS_KeypadComma: self = .keypadComma
323 | case "英数",
324 | SpecialKeyCode.eisu.character.lowercased(): self = .eisu
325 | case "かな",
326 | SpecialKeyCode.kana.character.lowercased(): self = .kana
327 | case "@": self = .atSign
328 | case "^": self = .caret
329 | case ":": self = .colon
330 | case "§": self = .section
331 | default: return nil
332 | }
333 | }
334 |
335 | public init?(QWERTYKeyCode keyCode: Int) {
336 | switch keyCode {
337 | case kVK_ANSI_A: self = .a
338 | case kVK_ANSI_S: self = .s
339 | case kVK_ANSI_D: self = .d
340 | case kVK_ANSI_F: self = .f
341 | case kVK_ANSI_H: self = .h
342 | case kVK_ANSI_G: self = .g
343 | case kVK_ANSI_Z: self = .z
344 | case kVK_ANSI_X: self = .x
345 | case kVK_ANSI_C: self = .c
346 | case kVK_ANSI_V: self = .v
347 | case kVK_ANSI_B: self = .b
348 | case kVK_ANSI_Q: self = .q
349 | case kVK_ANSI_W: self = .w
350 | case kVK_ANSI_E: self = .e
351 | case kVK_ANSI_R: self = .r
352 | case kVK_ANSI_Y: self = .y
353 | case kVK_ANSI_T: self = .t
354 | case kVK_ANSI_1: self = .one
355 | case kVK_ANSI_2: self = .two
356 | case kVK_ANSI_3: self = .three
357 | case kVK_ANSI_4: self = .four
358 | case kVK_ANSI_6: self = .six
359 | case kVK_ANSI_5: self = .five
360 | case kVK_ANSI_Equal: self = .equal
361 | case kVK_ANSI_9: self = .nine
362 | case kVK_ANSI_7: self = .seven
363 | case kVK_ANSI_Minus: self = .minus
364 | case kVK_ANSI_8: self = .eight
365 | case kVK_ANSI_0: self = .zero
366 | case kVK_ANSI_RightBracket: self = .rightBracket
367 | case kVK_ANSI_O: self = .o
368 | case kVK_ANSI_U: self = .u
369 | case kVK_ANSI_LeftBracket: self = .leftBracket
370 | case kVK_ANSI_I: self = .i
371 | case kVK_ANSI_P: self = .p
372 | case kVK_ANSI_L: self = .l
373 | case kVK_ANSI_J: self = .j
374 | case kVK_ANSI_Quote: self = .quote
375 | case kVK_ANSI_K: self = .k
376 | case kVK_ANSI_Semicolon: self = .semicolon
377 | case kVK_ANSI_Backslash: self = .backslash
378 | case kVK_ANSI_Comma: self = .comma
379 | case kVK_ANSI_Slash: self = .slash
380 | case kVK_ANSI_N: self = .n
381 | case kVK_ANSI_M: self = .m
382 | case kVK_ANSI_Period: self = .period
383 | case kVK_ANSI_Grave: self = .grave
384 | case kVK_ANSI_KeypadDecimal: self = .keypadDecimal
385 | case kVK_ANSI_KeypadMultiply: self = .keypadMultiply
386 | case kVK_ANSI_KeypadPlus: self = .keypadPlus
387 | case kVK_ANSI_KeypadClear: self = .keypadClear
388 | case kVK_ANSI_KeypadDivide: self = .keypadDivide
389 | case kVK_ANSI_KeypadEnter: self = .keypadEnter
390 | case kVK_ANSI_KeypadMinus: self = .keypadMinus
391 | case kVK_ANSI_KeypadEquals: self = .keypadEquals
392 | case kVK_ANSI_Keypad0: self = .keypadZero
393 | case kVK_ANSI_Keypad1: self = .keypadOne
394 | case kVK_ANSI_Keypad2: self = .keypadTwo
395 | case kVK_ANSI_Keypad3: self = .keypadThree
396 | case kVK_ANSI_Keypad4: self = .keypadFour
397 | case kVK_ANSI_Keypad5: self = .keypadFive
398 | case kVK_ANSI_Keypad6: self = .keypadSix
399 | case kVK_ANSI_Keypad7: self = .keypadSeven
400 | case kVK_ANSI_Keypad8: self = .keypadEight
401 | case kVK_ANSI_Keypad9: self = .keypadNine
402 | case kVK_Return: self = .return
403 | case kVK_Tab: self = .tab
404 | case kVK_Space: self = .space
405 | case kVK_Delete: self = .delete
406 | case kVK_Escape: self = .escape
407 | case kVK_F17: self = .f17
408 | case kVK_F18: self = .f18
409 | case kVK_F19: self = .f19
410 | case kVK_F20: self = .f20
411 | case kVK_F5: self = .f5
412 | case kVK_F6: self = .f6
413 | case kVK_F7: self = .f7
414 | case kVK_F3: self = .f3
415 | case kVK_F8: self = .f8
416 | case kVK_F9: self = .f9
417 | case kVK_F11: self = .f11
418 | case kVK_F13: self = .f13
419 | case kVK_F16: self = .f16
420 | case kVK_F14: self = .f14
421 | case kVK_F10: self = .f10
422 | case kVK_F12: self = .f12
423 | case kVK_F15: self = .f15
424 | case kVK_Help: self = .help
425 | case kVK_Home: self = .home
426 | case kVK_PageUp: self = .pageUp
427 | case kVK_ForwardDelete: self = .forwardDelete
428 | case kVK_F4: self = .f4
429 | case kVK_End: self = .end
430 | case kVK_F2: self = .f2
431 | case kVK_PageDown: self = .pageDown
432 | case kVK_F1: self = .f1
433 | case kVK_LeftArrow: self = .leftArrow
434 | case kVK_RightArrow: self = .rightArrow
435 | case kVK_DownArrow: self = .downArrow
436 | case kVK_UpArrow: self = .upArrow
437 | case kVK_JIS_Yen: self = .yen
438 | case kVK_JIS_Underscore: self = .underscore
439 | case kVK_JIS_KeypadComma: self = .keypadComma
440 | case kVK_JIS_Eisu: self = .eisu
441 | case kVK_JIS_Kana: self = .kana
442 | // .atSign, .caret, .colon is excluded because it uses a duplicate keycode on JIS keyboard only.
443 | // For example, .atSign is applied to kVK_ANSI_LeftBracket on a JIS keyboard.
444 | case kVK_ISO_Section: self = .section
445 | default: return nil
446 | }
447 | }
448 |
449 | // MARK: - Properties
450 | public var QWERTYKeyCode: CGKeyCode {
451 | switch self {
452 | case .a: return CGKeyCode(kVK_ANSI_A)
453 | case .s: return CGKeyCode(kVK_ANSI_S)
454 | case .d: return CGKeyCode(kVK_ANSI_D)
455 | case .f: return CGKeyCode(kVK_ANSI_F)
456 | case .h: return CGKeyCode(kVK_ANSI_H)
457 | case .g: return CGKeyCode(kVK_ANSI_G)
458 | case .z: return CGKeyCode(kVK_ANSI_Z)
459 | case .x: return CGKeyCode(kVK_ANSI_X)
460 | case .c: return CGKeyCode(kVK_ANSI_C)
461 | case .v: return CGKeyCode(kVK_ANSI_V)
462 | case .b: return CGKeyCode(kVK_ANSI_B)
463 | case .q: return CGKeyCode(kVK_ANSI_Q)
464 | case .w: return CGKeyCode(kVK_ANSI_W)
465 | case .e: return CGKeyCode(kVK_ANSI_E)
466 | case .r: return CGKeyCode(kVK_ANSI_R)
467 | case .y: return CGKeyCode(kVK_ANSI_Y)
468 | case .t: return CGKeyCode(kVK_ANSI_T)
469 | case .one: return CGKeyCode(kVK_ANSI_1)
470 | case .two: return CGKeyCode(kVK_ANSI_2)
471 | case .three: return CGKeyCode(kVK_ANSI_3)
472 | case .four: return CGKeyCode(kVK_ANSI_4)
473 | case .six: return CGKeyCode(kVK_ANSI_6)
474 | case .five: return CGKeyCode(kVK_ANSI_5)
475 | case .equal: return CGKeyCode(kVK_ANSI_Equal)
476 | case .nine: return CGKeyCode(kVK_ANSI_9)
477 | case .seven: return CGKeyCode(kVK_ANSI_7)
478 | case .minus: return CGKeyCode(kVK_ANSI_Minus)
479 | case .eight: return CGKeyCode(kVK_ANSI_8)
480 | case .zero: return CGKeyCode(kVK_ANSI_0)
481 | case .rightBracket: return CGKeyCode(kVK_ANSI_RightBracket)
482 | case .o: return CGKeyCode(kVK_ANSI_O)
483 | case .u: return CGKeyCode(kVK_ANSI_U)
484 | case .leftBracket: return CGKeyCode(kVK_ANSI_LeftBracket)
485 | case .i: return CGKeyCode(kVK_ANSI_I)
486 | case .p: return CGKeyCode(kVK_ANSI_P)
487 | case .l: return CGKeyCode(kVK_ANSI_L)
488 | case .j: return CGKeyCode(kVK_ANSI_J)
489 | case .quote: return CGKeyCode(kVK_ANSI_Quote)
490 | case .k: return CGKeyCode(kVK_ANSI_K)
491 | case .semicolon: return CGKeyCode(kVK_ANSI_Semicolon)
492 | case .backslash: return CGKeyCode(kVK_ANSI_Backslash)
493 | case .comma: return CGKeyCode(kVK_ANSI_Comma)
494 | case .slash: return CGKeyCode(kVK_ANSI_Slash)
495 | case .n: return CGKeyCode(kVK_ANSI_N)
496 | case .m: return CGKeyCode(kVK_ANSI_M)
497 | case .period: return CGKeyCode(kVK_ANSI_Period)
498 | case .grave: return CGKeyCode(kVK_ANSI_Grave)
499 | case .keypadDecimal: return CGKeyCode(kVK_ANSI_KeypadDecimal)
500 | case .keypadMultiply: return CGKeyCode(kVK_ANSI_KeypadMultiply)
501 | case .keypadPlus: return CGKeyCode(kVK_ANSI_KeypadPlus)
502 | case .keypadClear: return CGKeyCode(kVK_ANSI_KeypadClear)
503 | case .keypadDivide: return CGKeyCode(kVK_ANSI_KeypadDivide)
504 | case .keypadEnter: return CGKeyCode(kVK_ANSI_KeypadEnter)
505 | case .keypadMinus: return CGKeyCode(kVK_ANSI_KeypadMinus)
506 | case .keypadEquals: return CGKeyCode(kVK_ANSI_KeypadEquals)
507 | case .keypadZero: return CGKeyCode(kVK_ANSI_Keypad0)
508 | case .keypadOne: return CGKeyCode(kVK_ANSI_Keypad1)
509 | case .keypadTwo: return CGKeyCode(kVK_ANSI_Keypad2)
510 | case .keypadThree: return CGKeyCode(kVK_ANSI_Keypad3)
511 | case .keypadFour: return CGKeyCode(kVK_ANSI_Keypad4)
512 | case .keypadFive: return CGKeyCode(kVK_ANSI_Keypad5)
513 | case .keypadSix: return CGKeyCode(kVK_ANSI_Keypad6)
514 | case .keypadSeven: return CGKeyCode(kVK_ANSI_Keypad7)
515 | case .keypadEight: return CGKeyCode(kVK_ANSI_Keypad8)
516 | case .keypadNine: return CGKeyCode(kVK_ANSI_Keypad9)
517 | case .return: return CGKeyCode(kVK_Return)
518 | case .tab: return CGKeyCode(kVK_Tab)
519 | case .space: return CGKeyCode(kVK_Space)
520 | case .delete: return CGKeyCode(kVK_Delete)
521 | case .escape: return CGKeyCode(kVK_Escape)
522 | case .f17: return CGKeyCode(kVK_F17)
523 | case .f18: return CGKeyCode(kVK_F18)
524 | case .f19: return CGKeyCode(kVK_F19)
525 | case .f20: return CGKeyCode(kVK_F20)
526 | case .f5: return CGKeyCode(kVK_F5)
527 | case .f6: return CGKeyCode(kVK_F6)
528 | case .f7: return CGKeyCode(kVK_F7)
529 | case .f3: return CGKeyCode(kVK_F3)
530 | case .f8: return CGKeyCode(kVK_F8)
531 | case .f9: return CGKeyCode(kVK_F9)
532 | case .f11: return CGKeyCode(kVK_F11)
533 | case .f13: return CGKeyCode(kVK_F13)
534 | case .f16: return CGKeyCode(kVK_F16)
535 | case .f14: return CGKeyCode(kVK_F14)
536 | case .f10: return CGKeyCode(kVK_F10)
537 | case .f12: return CGKeyCode(kVK_F12)
538 | case .f15: return CGKeyCode(kVK_F15)
539 | case .help: return CGKeyCode(kVK_Help)
540 | case .home: return CGKeyCode(kVK_Home)
541 | case .pageUp: return CGKeyCode(kVK_PageUp)
542 | case .forwardDelete: return CGKeyCode(kVK_ForwardDelete)
543 | case .f4: return CGKeyCode(kVK_F4)
544 | case .end: return CGKeyCode(kVK_End)
545 | case .f2: return CGKeyCode(kVK_F2)
546 | case .pageDown: return CGKeyCode(kVK_PageDown)
547 | case .f1: return CGKeyCode(kVK_F1)
548 | case .leftArrow: return CGKeyCode(kVK_LeftArrow)
549 | case .rightArrow: return CGKeyCode(kVK_RightArrow)
550 | case .downArrow: return CGKeyCode(kVK_DownArrow)
551 | case .upArrow: return CGKeyCode(kVK_UpArrow)
552 | case .yen: return CGKeyCode(kVK_JIS_Yen)
553 | case .underscore: return CGKeyCode(kVK_JIS_Underscore)
554 | case .keypadComma: return CGKeyCode(kVK_JIS_KeypadComma)
555 | case .eisu: return CGKeyCode(kVK_JIS_Eisu)
556 | case .kana: return CGKeyCode(kVK_JIS_Kana)
557 | case .atSign: return CGKeyCode(kVK_ANSI_LeftBracket)
558 | case .caret: return CGKeyCode(kVK_ANSI_Equal)
559 | case .colon: return CGKeyCode(kVK_ANSI_Quote)
560 | case .section: return CGKeyCode(kVK_ISO_Section)
561 | }
562 | }
563 |
564 | }
565 | #endif
566 |
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
--------------------------------------------------------------------------------