├── .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 | ![CI](https://github.com/Clipy/Sauce/workflows/Xcode-Build/badge.svg) 3 | [![Release version](https://img.shields.io/github/release/Clipy/Sauce.svg)](https://github.com/Clipy/Sauce/releases/latest) 4 | [![License: MIT](https://img.shields.io/github/license/Clipy/Sauce.svg)](https://github.com/Clipy/Sauce/blob/master/LICENSE) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![Version](https://img.shields.io/cocoapods/v/Sauce.svg)](https://cocoapods.org/pods/Sauce) 7 | [![Platform](https://img.shields.io/cocoapods/p/Sauce.svg)](https://cocoapods.org/pods/Sauce) 8 | [![SPM supported](https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat)](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 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Default 530 | 531 | 532 | 533 | 534 | 535 | 536 | Left to Right 537 | 538 | 539 | 540 | 541 | 542 | 543 | Right to Left 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 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 | --------------------------------------------------------------------------------