├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .periphery.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── KeyCodes │ ├── Key.swift │ └── NSEvent+Key.swift └── Tests └── KeyCodesTests ├── KeyCodesTests.swift └── NSEventTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mattmassicotte] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - 'CODE_OF_CONDUCT.md' 10 | - '.editorconfig' 11 | - '.spi.yml' 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | jobs: 17 | test: 18 | name: Test 19 | runs-on: macOS-14 20 | env: 21 | DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer 22 | strategy: 23 | matrix: 24 | destination: 25 | - "platform=macOS" 26 | - "platform=macOS,variant=Mac Catalyst" 27 | - "platform=iOS Simulator,name=iPhone 12" 28 | - "platform=tvOS Simulator,name=Apple TV" 29 | - "platform=watchOS Simulator,name=Apple Watch Series 6 (40mm)" 30 | - "platform=visionOS Simulator,name=Apple Vision Pro" 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Test platform ${{ matrix.destination }} 35 | run: set -o pipefail && xcodebuild -scheme KeyCodes -destination "${{ matrix.destination }}" test | xcbeautify 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | retain_public: true 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | support@chimehq.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Chime 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "KeyCodes", 7 | products: [ 8 | .library(name: "KeyCodes", targets: ["KeyCodes"]), 9 | ], 10 | targets: [ 11 | .target(name: "KeyCodes"), 12 | .testTarget(name: "KeyCodesTests", dependencies: ["KeyCodes"]), 13 | ] 14 | ) 15 | 16 | let swiftSettings: [SwiftSetting] = [ 17 | .enableExperimentalFeature("StrictConcurrency"), 18 | ] 19 | 20 | for target in package.targets { 21 | var settings = target.swiftSettings ?? [] 22 | settings.append(contentsOf: swiftSettings) 23 | target.swiftSettings = settings 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Build Status][build status badge]][build status] 4 | [![Platforms][platforms badge]][platforms] 5 | [![Matrix][matrix badge]][matrix] 6 | 7 |
8 | 9 | # KeyCodes 10 | 11 | Versions of `UIKey`, `UIKeyboardHIDUsage`, and `UIKeyModifierFlags` that work with AppKit's `NSEvent`. No need for Carbon.HIToolbox. Aside from being a nicer API to work with, these versions should make it possible to more easily write source-compatible AppKit/UIKit keyboard handling code. Yes, this is basically a gigantic switch statement. 12 | 13 | These structures are particularly helpful for writing tests. Constructing `NSEvent` instances by hand is a pain. 14 | 15 | ## Usage 16 | 17 | ```swift 18 | import Carbon.HIToolbox 19 | 20 | func withoutKeyCodes(_ event: NSEvent) { 21 | let code = Int(event.keyCode) 22 | 23 | if code == kVK_Return { 24 | doThing() 25 | } 26 | 27 | if event.modifierFlags.deviceIndependentOnly.contains(.control) { 28 | controlKeyActive() 29 | } 30 | } 31 | 32 | import KeyCodes 33 | 34 | func withKeyCodes(_ event: NSEvent) { 35 | if event.keyboardHIDUsage == .keyboardReturn { 36 | doThing() 37 | } 38 | 39 | // UIKeyModifierFlags-compatible 40 | if event.keyModifierFlags.contains(.control) { 41 | controlKeyActive() 42 | } 43 | } 44 | ``` 45 | 46 | ## Integration 47 | 48 | ```swift 49 | dependencies: [ 50 | .package(url: "https://github.com/ChimeHQ/KeyCodes", from: "0.1.1") 51 | ] 52 | ``` 53 | 54 | ## Contributing and Collaboration 55 | 56 | I would love to hear from you! Issues or pull requests work great. A [Matrix space][matrix] is also available for live help, but I have a strong bias towards answering in the form of documenation. 57 | 58 | I prefer collaboration, and would love to find ways to work together if you have a similar project. 59 | 60 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace. 61 | 62 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md). 63 | 64 | [build status]: https://github.com/ChimeHQ/KeyCodes/actions 65 | [build status badge]: https://github.com/ChimeHQ/KeyCodes/workflows/CI/badge.svg 66 | [platforms]: https://swiftpackageindex.com/ChimeHQ/KeyCodes 67 | [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FKeyCodes%2Fbadge%3Ftype%3Dplatforms 68 | [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org 69 | [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix 70 | -------------------------------------------------------------------------------- /Sources/KeyCodes/Key.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(macOS) 3 | import Carbon.HIToolbox 4 | import AppKit 5 | #endif 6 | 7 | public enum KeyboardHIDUsage: Hashable, Sendable { 8 | case keyboardA 9 | case keyboardB 10 | case keyboardC 11 | case keyboardD 12 | case keyboardE 13 | case keyboardF 14 | case keyboardG 15 | case keyboardH 16 | case keyboardI 17 | case keyboardJ 18 | case keyboardK 19 | case keyboardL 20 | case keyboardM 21 | case keyboardN 22 | case keyboardO 23 | case keyboardP 24 | case keyboardQ 25 | case keyboardR 26 | case keyboardS 27 | case keyboardT 28 | case keyboardU 29 | case keyboardV 30 | case keyboardW 31 | case keyboardX 32 | case keyboardY 33 | case keyboardZ 34 | case keyboard0 35 | case keyboard1 36 | case keyboard2 37 | case keyboard3 38 | case keyboard4 39 | case keyboard5 40 | case keyboard6 41 | case keyboard7 42 | case keyboard8 43 | case keyboard9 44 | case keyboardBackslash 45 | case keyboardCloseBracket 46 | case keyboardComma 47 | case keyboardEqualSign 48 | case keyboardHyphen 49 | case keyboardNonUSBackslash 50 | case keyboardNonUSPound 51 | case keyboardOpenBracket 52 | case keyboardPeriod 53 | case keyboardQuote 54 | case keyboardSemicolon 55 | case keyboardSeparator 56 | case keyboardSlash 57 | case keyboardSpacebar 58 | case keyboardCapsLock 59 | case keyboardLeftAlt 60 | case keyboardLeftControl 61 | case keyboardLeftShift 62 | case keyboardLockingCapsLock 63 | case keyboardLockingNumLock 64 | case keyboardLockingScrollLock 65 | case keyboardRightAlt 66 | case keyboardRightControl 67 | case keyboardRightShift 68 | case keyboardScrollLock 69 | case keyboardLeftArrow 70 | case keyboardRightArrow 71 | case keyboardUpArrow 72 | case keyboardDownArrow 73 | case keyboardPageUp 74 | case keyboardPageDown 75 | case keyboardHome 76 | case keyboardEnd 77 | case keyboardDeleteForward 78 | case keyboardDeleteOrBackspace 79 | case keyboardEscape 80 | case keyboardInsert 81 | case keyboardReturn 82 | case keyboardTab 83 | case keyboardF1 84 | case keyboardF2 85 | case keyboardF3 86 | case keyboardF4 87 | case keyboardF5 88 | case keyboardF6 89 | case keyboardF7 90 | case keyboardF8 91 | case keyboardF9 92 | case keyboardF10 93 | case keyboardF11 94 | case keyboardF12 95 | case keyboardF13 96 | case keyboardF14 97 | case keyboardF15 98 | case keyboardF16 99 | case keyboardF17 100 | case keyboardF18 101 | case keyboardF19 102 | case keyboardF20 103 | case keyboardF21 104 | case keyboardF22 105 | case keyboardF23 106 | case keyboardF24 107 | case keypad0 108 | case keypad1 109 | case keypad2 110 | case keypad3 111 | case keypad4 112 | case keypad5 113 | case keypad6 114 | case keypad7 115 | case keypad8 116 | case keypad9 117 | case keypadAsterisk 118 | case keypadComma 119 | case keypadEnter 120 | case keypadEqualSign 121 | case keypadEqualSignAS400 122 | case keypadHyphen 123 | case keypadNumLock 124 | case keypadPeriod 125 | case keypadPlus 126 | case keypadSlash 127 | case keyboardPause 128 | case keyboardStop 129 | case keyboardMute 130 | case keyboardVolumeUp 131 | case keyboardVolumeDown 132 | case keyboardLANG1 133 | case keyboardLANG2 134 | case keyboardLANG3 135 | case keyboardLANG4 136 | case keyboardLANG5 137 | case keyboardLANG6 138 | case keyboardLANG7 139 | case keyboardLANG8 140 | case keyboardLANG9 141 | case keyboardInternational1 142 | case keyboardInternational2 143 | case keyboardInternational3 144 | case keyboardInternational4 145 | case keyboardInternational5 146 | case keyboardInternational6 147 | case keyboardInternational7 148 | case keyboardInternational8 149 | case keyboardInternational9 150 | case keyboardErrorRollOver 151 | case keyboardErrorUndefined 152 | case keyboardAgain 153 | case keyboardAlternateErase 154 | case keyboardApplication 155 | case keyboardCancel 156 | case keyboardClear 157 | case keyboardClearOrAgain 158 | case keyboardCopy 159 | case keyboardCrSelOrProps 160 | case keyboardCut 161 | case keyboardExSel 162 | case keyboardExecute 163 | case keyboardFind 164 | case keyboardGraveAccentAndTilde 165 | case keyboardHelp 166 | case keyboardLeftGUI 167 | case keyboardMenu 168 | case keyboardOper 169 | case keyboardOut 170 | case keyboardPOSTFail 171 | case keyboardPaste 172 | case keyboardPower 173 | case keyboardPrintScreen 174 | case keyboardPrior 175 | case keyboardReturnOrEnter 176 | case keyboardRightGUI 177 | case keyboardSelect 178 | case keyboardSysReqOrAttention 179 | case keyboardUndo 180 | case keyboard_Reserved 181 | } 182 | 183 | public struct KeyModifierFlags: OptionSet, Sendable, Hashable { 184 | public var rawValue: UInt 185 | 186 | public init(rawValue: UInt) { 187 | self.rawValue = rawValue 188 | } 189 | 190 | public static let alphaShift = KeyModifierFlags(rawValue: 1 << 16) 191 | public static let shift = KeyModifierFlags(rawValue: 1 << 17) 192 | public static let control = KeyModifierFlags(rawValue: 1 << 18) 193 | public static let alternate = KeyModifierFlags(rawValue: 1 << 19) 194 | public static let command = KeyModifierFlags(rawValue: 1 << 20) 195 | public static let numericPad = KeyModifierFlags(rawValue: 1 << 21) 196 | } 197 | 198 | public struct Key: Hashable, Sendable { 199 | public var keyCode: KeyboardHIDUsage 200 | public var modifierFlags: KeyModifierFlags 201 | public var characters: String 202 | public var charactersIgnoringModifiers: String 203 | 204 | public init(keyCode: KeyboardHIDUsage, characters: String, charactersIgnoringModifiers: String, modifierFlags: KeyModifierFlags = []) { 205 | self.keyCode = keyCode 206 | self.characters = characters 207 | self.charactersIgnoringModifiers = charactersIgnoringModifiers 208 | self.modifierFlags = modifierFlags 209 | } 210 | 211 | public init(keyCode: KeyboardHIDUsage, characters: String, modifierFlags: KeyModifierFlags = []) { 212 | self.keyCode = keyCode 213 | self.characters = characters 214 | self.charactersIgnoringModifiers = characters.lowercased() 215 | self.modifierFlags = modifierFlags 216 | } 217 | 218 | public init?(_ character: Character) { 219 | self.characters = String(character) 220 | self.charactersIgnoringModifiers = String(character).lowercased() 221 | self.modifierFlags = [] 222 | 223 | switch character { 224 | case "a": 225 | self.keyCode = .keyboardA 226 | case "A": 227 | self.keyCode = .keyboardA 228 | self.modifierFlags = [.shift] 229 | case "b": 230 | self.keyCode = .keyboardB 231 | case "B": 232 | self.keyCode = .keyboardB 233 | self.modifierFlags = [.shift] 234 | case "c": 235 | self.keyCode = .keyboardC 236 | case "C": 237 | self.keyCode = .keyboardC 238 | self.modifierFlags = [.shift] 239 | case "d": 240 | self.keyCode = .keyboardD 241 | case "D": 242 | self.keyCode = .keyboardD 243 | self.modifierFlags = [.shift] 244 | case "e": 245 | self.keyCode = .keyboardE 246 | case "E": 247 | self.keyCode = .keyboardE 248 | self.modifierFlags = [.shift] 249 | case "f": 250 | self.keyCode = .keyboardF 251 | case "F": 252 | self.keyCode = .keyboardF 253 | self.modifierFlags = [.shift] 254 | case "g": 255 | self.keyCode = .keyboardG 256 | case "G": 257 | self.keyCode = .keyboardG 258 | self.modifierFlags = [.shift] 259 | case "h": 260 | self.keyCode = .keyboardH 261 | case "H": 262 | self.keyCode = .keyboardH 263 | self.modifierFlags = [.shift] 264 | case "i": 265 | self.keyCode = .keyboardI 266 | case "I": 267 | self.keyCode = .keyboardI 268 | self.modifierFlags = [.shift] 269 | case "j": 270 | self.keyCode = .keyboardJ 271 | case "J": 272 | self.keyCode = .keyboardJ 273 | self.modifierFlags = [.shift] 274 | case "k": 275 | self.keyCode = .keyboardK 276 | case "K": 277 | self.keyCode = .keyboardK 278 | self.modifierFlags = [.shift] 279 | case "l": 280 | self.keyCode = .keyboardL 281 | case "L": 282 | self.keyCode = .keyboardL 283 | self.modifierFlags = [.shift] 284 | case "m": 285 | self.keyCode = .keyboardM 286 | case "M": 287 | self.keyCode = .keyboardM 288 | self.modifierFlags = [.shift] 289 | case "n": 290 | self.keyCode = .keyboardN 291 | case "N": 292 | self.keyCode = .keyboardN 293 | self.modifierFlags = [.shift] 294 | case "o": 295 | self.keyCode = .keyboardO 296 | case "O": 297 | self.keyCode = .keyboardO 298 | self.modifierFlags = [.shift] 299 | case "p": 300 | self.keyCode = .keyboardP 301 | case "P": 302 | self.keyCode = .keyboardP 303 | self.modifierFlags = [.shift] 304 | case "q": 305 | self.keyCode = .keyboardQ 306 | case "Q": 307 | self.keyCode = .keyboardQ 308 | self.modifierFlags = [.shift] 309 | case "r": 310 | self.keyCode = .keyboardR 311 | case "R": 312 | self.keyCode = .keyboardR 313 | self.modifierFlags = [.shift] 314 | case "s": 315 | self.keyCode = .keyboardS 316 | case "S": 317 | self.keyCode = .keyboardS 318 | self.modifierFlags = [.shift] 319 | case "t": 320 | self.keyCode = .keyboardT 321 | case "T": 322 | self.keyCode = .keyboardT 323 | self.modifierFlags = [.shift] 324 | case "u": 325 | self.keyCode = .keyboardU 326 | case "U": 327 | self.keyCode = .keyboardU 328 | self.modifierFlags = [.shift] 329 | case "v": 330 | self.keyCode = .keyboardV 331 | case "V": 332 | self.keyCode = .keyboardV 333 | self.modifierFlags = [.shift] 334 | case "w": 335 | self.keyCode = .keyboardW 336 | case "W": 337 | self.keyCode = .keyboardW 338 | self.modifierFlags = [.shift] 339 | case "x": 340 | self.keyCode = .keyboardX 341 | case "X": 342 | self.keyCode = .keyboardX 343 | self.modifierFlags = [.shift] 344 | case "y": 345 | self.keyCode = .keyboardY 346 | case "Y": 347 | self.keyCode = .keyboardY 348 | self.modifierFlags = [.shift] 349 | case "z": 350 | self.keyCode = .keyboardZ 351 | case "Z": 352 | self.keyCode = .keyboardZ 353 | self.modifierFlags = [.shift] 354 | 355 | case "1": 356 | self.keyCode = .keyboard1 357 | case "!": 358 | self.keyCode = .keyboard1 359 | self.modifierFlags = [.shift] 360 | case "2": 361 | self.keyCode = .keyboard2 362 | case "@": 363 | self.keyCode = .keyboard2 364 | self.modifierFlags = [.shift] 365 | case "3": 366 | self.keyCode = .keyboard3 367 | case "#": 368 | self.keyCode = .keyboard3 369 | self.modifierFlags = [.shift] 370 | case "4": 371 | self.keyCode = .keyboard4 372 | case "$": 373 | self.keyCode = .keyboard4 374 | self.modifierFlags = [.shift] 375 | case "5": 376 | self.keyCode = .keyboard5 377 | case "%": 378 | self.keyCode = .keyboard5 379 | self.modifierFlags = [.shift] 380 | case "6": 381 | self.keyCode = .keyboard6 382 | case "^": 383 | self.keyCode = .keyboard6 384 | self.modifierFlags = [.shift] 385 | case "7": 386 | self.keyCode = .keyboard7 387 | case "&": 388 | self.keyCode = .keyboard7 389 | self.modifierFlags = [.shift] 390 | case "8": 391 | self.keyCode = .keyboard8 392 | case "*": 393 | self.keyCode = .keyboard8 394 | self.modifierFlags = [.shift] 395 | case "9": 396 | self.keyCode = .keyboard9 397 | case "(": 398 | self.keyCode = .keyboard9 399 | self.modifierFlags = [.shift] 400 | case "0": 401 | self.keyCode = .keyboard0 402 | case ")": 403 | self.keyCode = .keyboard0 404 | self.modifierFlags = [.shift] 405 | 406 | case "-": 407 | self.keyCode = .keyboardHyphen 408 | case "_": 409 | self.keyCode = .keyboardHyphen 410 | self.modifierFlags = [.shift] 411 | case ",": 412 | self.keyCode = .keyboardComma 413 | case "<": 414 | self.keyCode = .keyboardComma 415 | self.modifierFlags = [.shift] 416 | case ".": 417 | self.keyCode = .keyboardPeriod 418 | case ">": 419 | self.keyCode = .keyboardPeriod 420 | self.modifierFlags = [.shift] 421 | case "`": 422 | self.keyCode = .keyboardGraveAccentAndTilde 423 | case "~": 424 | self.keyCode = .keyboardGraveAccentAndTilde 425 | self.modifierFlags = [.shift] 426 | case "=": 427 | self.keyCode = .keyboardEqualSign 428 | case "+": 429 | self.keyCode = .keyboardEqualSign 430 | self.modifierFlags = [.shift] 431 | case "[": 432 | self.keyCode = .keyboardOpenBracket 433 | case "{": 434 | self.keyCode = .keyboardOpenBracket 435 | self.modifierFlags = [.shift] 436 | case "]": 437 | self.keyCode = .keyboardCloseBracket 438 | case "}": 439 | self.keyCode = .keyboardCloseBracket 440 | self.modifierFlags = [.shift] 441 | case "\\": 442 | self.keyCode = .keyboardBackslash 443 | case "|": 444 | self.keyCode = .keyboardBackslash 445 | self.modifierFlags = [.shift] 446 | case ";": 447 | self.keyCode = .keyboardSemicolon 448 | case ":": 449 | self.keyCode = .keyboardSemicolon 450 | self.modifierFlags = [.shift] 451 | case "'": 452 | self.keyCode = .keyboardQuote 453 | case "\"": 454 | self.keyCode = .keyboardQuote 455 | self.modifierFlags = [.shift] 456 | case "/": 457 | self.keyCode = .keyboardSlash 458 | case "?": 459 | self.keyCode = .keyboardSlash 460 | self.modifierFlags = [.shift] 461 | 462 | default: 463 | return nil 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /Sources/KeyCodes/NSEvent+Key.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Carbon.HIToolbox 3 | import AppKit 4 | 5 | extension NSEvent { 6 | /// Return the set of flags representable by KeyModifierFlags 7 | /// 8 | /// This value can be a subset of the results of `deviceIndependentOnly`. There are flag values that `NSEvent` can include which are not representable by the `KeyModifierFlags` set. 9 | public var keyModifierFlags: KeyModifierFlags? { 10 | var flags = KeyModifierFlags(rawValue: 0) 11 | 12 | let deviceIndependentFlags = modifierFlags.deviceIndependentOnly 13 | 14 | if deviceIndependentFlags.contains(.control) { 15 | flags.insert(.control) 16 | } 17 | 18 | if deviceIndependentFlags.contains(.shift) { 19 | flags.insert(.shift) 20 | } 21 | 22 | if deviceIndependentFlags.contains(.command) { 23 | flags.insert(.command) 24 | } 25 | 26 | if deviceIndependentFlags.contains(.option) { 27 | flags.insert(.alternate) 28 | } 29 | 30 | if deviceIndependentFlags.contains(.capsLock) { 31 | flags.insert(.alphaShift) 32 | } 33 | 34 | if deviceIndependentFlags.contains(.numericPad) { 35 | flags.insert(.numericPad) 36 | } 37 | 38 | return flags 39 | } 40 | 41 | public var keyboardHIDUsage: KeyboardHIDUsage? { 42 | guard type == .keyDown || type == .keyUp else { 43 | return nil 44 | } 45 | 46 | switch Int(keyCode) { 47 | case kVK_ANSI_A: 48 | return .keyboardA 49 | case kVK_ANSI_B: 50 | return .keyboardB 51 | case kVK_ANSI_C: 52 | return .keyboardC 53 | case kVK_ANSI_D: 54 | return .keyboardD 55 | case kVK_ANSI_E: 56 | return .keyboardE 57 | case kVK_ANSI_F: 58 | return .keyboardF 59 | case kVK_ANSI_G: 60 | return .keyboardG 61 | case kVK_ANSI_H: 62 | return .keyboardH 63 | case kVK_ANSI_I: 64 | return .keyboardI 65 | case kVK_ANSI_J: 66 | return .keyboardJ 67 | case kVK_ANSI_K: 68 | return .keyboardK 69 | case kVK_ANSI_L: 70 | return .keyboardL 71 | case kVK_ANSI_M: 72 | return .keyboardM 73 | case kVK_ANSI_N: 74 | return .keyboardN 75 | case kVK_ANSI_O: 76 | return .keyboardO 77 | case kVK_ANSI_P: 78 | return .keyboardP 79 | case kVK_ANSI_Q: 80 | return .keyboardQ 81 | case kVK_ANSI_R: 82 | return .keyboardR 83 | case kVK_ANSI_S: 84 | return .keyboardS 85 | case kVK_ANSI_T: 86 | return .keyboardT 87 | case kVK_ANSI_U: 88 | return .keyboardU 89 | case kVK_ANSI_V: 90 | return .keyboardV 91 | case kVK_ANSI_W: 92 | return .keyboardW 93 | case kVK_ANSI_X: 94 | return .keyboardX 95 | case kVK_ANSI_Y: 96 | return .keyboardY 97 | case kVK_ANSI_Z: 98 | return .keyboardZ 99 | 100 | case kVK_ANSI_0: 101 | return .keyboard0 102 | case kVK_ANSI_1: 103 | return .keyboard1 104 | case kVK_ANSI_2: 105 | return .keyboard2 106 | case kVK_ANSI_3: 107 | return .keyboard3 108 | case kVK_ANSI_4: 109 | return .keyboard4 110 | case kVK_ANSI_5: 111 | return .keyboard5 112 | case kVK_ANSI_6: 113 | return .keyboard6 114 | case kVK_ANSI_7: 115 | return .keyboard7 116 | case kVK_ANSI_8: 117 | return .keyboard8 118 | case kVK_ANSI_9: 119 | return .keyboard9 120 | 121 | case kVK_LeftArrow: 122 | return .keyboardLeftArrow 123 | case kVK_RightArrow: 124 | return .keyboardRightArrow 125 | case kVK_UpArrow: 126 | return .keyboardUpArrow 127 | case kVK_DownArrow: 128 | return .keyboardDownArrow 129 | 130 | case kVK_ANSI_Equal: 131 | return .keyboardEqualSign 132 | case kVK_ANSI_Minus: 133 | return .keypadHyphen 134 | case kVK_ANSI_RightBracket: 135 | return .keyboardOpenBracket 136 | case kVK_ANSI_LeftBracket: 137 | return .keyboardCloseBracket 138 | case kVK_ANSI_Quote: 139 | return .keyboardQuote 140 | case kVK_ANSI_Semicolon: 141 | return .keyboardSemicolon 142 | case kVK_ANSI_Backslash: 143 | return .keyboardBackslash 144 | case kVK_ANSI_Comma: 145 | return .keyboardComma 146 | case kVK_ANSI_Slash: 147 | return .keyboardSlash 148 | case kVK_ANSI_Period: 149 | return .keyboardPeriod 150 | case kVK_ANSI_Grave: 151 | return .keyboardGraveAccentAndTilde 152 | 153 | case kVK_ANSI_KeypadDecimal: 154 | return .keypadPeriod 155 | case kVK_ANSI_KeypadPlus: 156 | return .keypadPlus 157 | case kVK_ANSI_KeypadMinus: 158 | return .keypadHyphen 159 | case kVK_ANSI_KeypadDivide: 160 | return .keypadSlash 161 | case kVK_ANSI_KeypadMultiply: 162 | return .keypadAsterisk 163 | case kVK_ANSI_Keypad0: 164 | return .keypad0 165 | case kVK_ANSI_Keypad1: 166 | return .keypad1 167 | case kVK_ANSI_Keypad2: 168 | return .keypad2 169 | case kVK_ANSI_Keypad3: 170 | return .keypad3 171 | case kVK_ANSI_Keypad4: 172 | return .keypad4 173 | case kVK_ANSI_Keypad5: 174 | return .keypad5 175 | case kVK_ANSI_Keypad6: 176 | return .keypad6 177 | case kVK_ANSI_Keypad7: 178 | return .keypad7 179 | case kVK_ANSI_Keypad8: 180 | return .keypad8 181 | case kVK_ANSI_Keypad9: 182 | return .keypad9 183 | 184 | case kVK_F1: 185 | return .keyboardF1 186 | case kVK_F2: 187 | return .keyboardF2 188 | case kVK_F3: 189 | return .keyboardF3 190 | case kVK_F4: 191 | return .keyboardF4 192 | case kVK_F5: 193 | return .keyboardF5 194 | case kVK_F6: 195 | return .keyboardF6 196 | case kVK_F7: 197 | return .keyboardF7 198 | case kVK_F8: 199 | return .keyboardF8 200 | case kVK_F9: 201 | return .keyboardF9 202 | case kVK_F10: 203 | return .keyboardF10 204 | case kVK_F11: 205 | return .keyboardF11 206 | case kVK_F12: 207 | return .keyboardF12 208 | case kVK_F13: 209 | return .keyboardF13 210 | case kVK_F14: 211 | return .keyboardF14 212 | case kVK_F15: 213 | return .keyboardF15 214 | case kVK_F16: 215 | return .keyboardF16 216 | case kVK_F17: 217 | return .keyboardF17 218 | case kVK_F18: 219 | return .keyboardF18 220 | case kVK_F19: 221 | return .keyboardF19 222 | case kVK_F20: 223 | return .keyboardF20 224 | 225 | case kVK_PageUp: 226 | return .keyboardPageUp 227 | case kVK_PageDown: 228 | return .keyboardPageDown 229 | case kVK_Home: 230 | return .keyboardHome 231 | case kVK_End: 232 | return .keyboardEnd 233 | case kVK_Help: 234 | return .keyboardHelp 235 | case kVK_Return: 236 | return .keyboardReturn 237 | case kVK_Tab: 238 | return .keyboardTab 239 | case kVK_Space: 240 | return .keyboardSpacebar 241 | case kVK_Delete: 242 | return .keyboardDeleteOrBackspace 243 | case kVK_ForwardDelete: 244 | return .keyboardDeleteForward 245 | case kVK_Escape: 246 | return .keyboardEscape 247 | case kVK_VolumeUp: 248 | return .keyboardVolumeUp 249 | case kVK_VolumeDown: 250 | return .keyboardVolumeDown 251 | 252 | default: 253 | return nil 254 | } 255 | } 256 | 257 | public var key: Key? { 258 | let chars = characters ?? "" 259 | let charsIgnoringModifiers = charactersIgnoringModifiers ?? "" 260 | 261 | switch (keyboardHIDUsage, keyModifierFlags) { 262 | case (let key?, let mods?): 263 | return Key(keyCode: key, 264 | characters: chars, 265 | charactersIgnoringModifiers: charsIgnoringModifiers, 266 | modifierFlags: mods) 267 | default: 268 | return nil 269 | } 270 | } 271 | } 272 | 273 | extension NSEvent.ModifierFlags { 274 | /// Returns the intersection with .deviceIndependentFlagsMask 275 | /// 276 | /// This value can include flags that are not representable by the `KeyModifierFlags` enum. 277 | public var deviceIndependentOnly: NSEvent.ModifierFlags { 278 | return intersection([.deviceIndependentFlagsMask]) 279 | } 280 | } 281 | 282 | #endif 283 | 284 | -------------------------------------------------------------------------------- /Tests/KeyCodesTests/KeyCodesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import KeyCodes 3 | 4 | final class KeyCodesTests: XCTestCase { 5 | func testKeysFromCharacters() throws { 6 | XCTAssertEqual(Key("a"), Key(keyCode: .keyboardA, characters: "a")) 7 | XCTAssertEqual(Key("A"), Key(keyCode: .keyboardA, characters: "A", modifierFlags: [.shift])) 8 | XCTAssertEqual(Key("b"), Key(keyCode: .keyboardB, characters: "b")) 9 | XCTAssertEqual(Key("B"), Key(keyCode: .keyboardB, characters: "B", modifierFlags: [.shift])) 10 | XCTAssertEqual(Key("c"), Key(keyCode: .keyboardC, characters: "c")) 11 | XCTAssertEqual(Key("C"), Key(keyCode: .keyboardC, characters: "C", modifierFlags: [.shift])) 12 | XCTAssertEqual(Key("d"), Key(keyCode: .keyboardD, characters: "d")) 13 | XCTAssertEqual(Key("D"), Key(keyCode: .keyboardD, characters: "D", modifierFlags: [.shift])) 14 | } 15 | 16 | func testNumericKeysFromCharacters() throws { 17 | XCTAssertEqual(Key("1"), Key(keyCode: .keyboard1, characters: "1")) 18 | XCTAssertEqual(Key("!"), Key(keyCode: .keyboard1, characters: "!", modifierFlags: [.shift])) 19 | XCTAssertEqual(Key("2"), Key(keyCode: .keyboard2, characters: "2")) 20 | XCTAssertEqual(Key("@"), Key(keyCode: .keyboard2, characters: "@", modifierFlags: [.shift])) 21 | XCTAssertEqual(Key("3"), Key(keyCode: .keyboard3, characters: "3")) 22 | XCTAssertEqual(Key("#"), Key(keyCode: .keyboard3, characters: "#", modifierFlags: [.shift])) 23 | XCTAssertEqual(Key("4"), Key(keyCode: .keyboard4, characters: "4")) 24 | XCTAssertEqual(Key("$"), Key(keyCode: .keyboard4, characters: "$", modifierFlags: [.shift])) 25 | XCTAssertEqual(Key("5"), Key(keyCode: .keyboard5, characters: "5")) 26 | XCTAssertEqual(Key("%"), Key(keyCode: .keyboard5, characters: "%", modifierFlags: [.shift])) 27 | XCTAssertEqual(Key("6"), Key(keyCode: .keyboard6, characters: "6")) 28 | XCTAssertEqual(Key("^"), Key(keyCode: .keyboard6, characters: "^", modifierFlags: [.shift])) 29 | XCTAssertEqual(Key("7"), Key(keyCode: .keyboard7, characters: "7")) 30 | XCTAssertEqual(Key("&"), Key(keyCode: .keyboard7, characters: "&", modifierFlags: [.shift])) 31 | XCTAssertEqual(Key("8"), Key(keyCode: .keyboard8, characters: "8")) 32 | XCTAssertEqual(Key("*"), Key(keyCode: .keyboard8, characters: "*", modifierFlags: [.shift])) 33 | XCTAssertEqual(Key("9"), Key(keyCode: .keyboard9, characters: "9")) 34 | XCTAssertEqual(Key("("), Key(keyCode: .keyboard9, characters: "(", modifierFlags: [.shift])) 35 | XCTAssertEqual(Key("0"), Key(keyCode: .keyboard0, characters: "0")) 36 | XCTAssertEqual(Key(")"), Key(keyCode: .keyboard0, characters: ")", modifierFlags: [.shift])) 37 | } 38 | 39 | func testSymbolKeysFromCharacters() throws { 40 | XCTAssertEqual(Key("`"), Key(keyCode: .keyboardGraveAccentAndTilde, characters: "`")) 41 | XCTAssertEqual(Key("~"), Key(keyCode: .keyboardGraveAccentAndTilde, characters: "~", modifierFlags: [.shift])) 42 | XCTAssertEqual(Key("-"), Key(keyCode: .keyboardHyphen, characters: "-")) 43 | XCTAssertEqual(Key("_"), Key(keyCode: .keyboardHyphen, characters: "_", modifierFlags: [.shift])) 44 | XCTAssertEqual(Key("="), Key(keyCode: .keyboardEqualSign, characters: "=")) 45 | XCTAssertEqual(Key("+"), Key(keyCode: .keyboardEqualSign, characters: "+", modifierFlags: [.shift])) 46 | XCTAssertEqual(Key("["), Key(keyCode: .keyboardOpenBracket, characters: "[")) 47 | XCTAssertEqual(Key("{"), Key(keyCode: .keyboardOpenBracket, characters: "{", modifierFlags: [.shift])) 48 | XCTAssertEqual(Key("]"), Key(keyCode: .keyboardCloseBracket, characters: "]")) 49 | XCTAssertEqual(Key("}"), Key(keyCode: .keyboardCloseBracket, characters: "}", modifierFlags: [.shift])) 50 | XCTAssertEqual(Key("\\"), Key(keyCode: .keyboardBackslash, characters: "\\")) 51 | XCTAssertEqual(Key("|"), Key(keyCode: .keyboardBackslash, characters: "|", modifierFlags: [.shift])) 52 | XCTAssertEqual(Key(";"), Key(keyCode: .keyboardSemicolon, characters: ";")) 53 | XCTAssertEqual(Key(":"), Key(keyCode: .keyboardSemicolon, characters: ":", modifierFlags: [.shift])) 54 | XCTAssertEqual(Key("'"), Key(keyCode: .keyboardQuote, characters: "'")) 55 | XCTAssertEqual(Key("\""), Key(keyCode: .keyboardQuote, characters: "\"", modifierFlags: [.shift])) 56 | XCTAssertEqual(Key(","), Key(keyCode: .keyboardComma, characters: ",")) 57 | XCTAssertEqual(Key("<"), Key(keyCode: .keyboardComma, characters: "<", modifierFlags: [.shift])) 58 | XCTAssertEqual(Key("."), Key(keyCode: .keyboardPeriod, characters: ".")) 59 | XCTAssertEqual(Key(">"), Key(keyCode: .keyboardPeriod, characters: ">", modifierFlags: [.shift])) 60 | XCTAssertEqual(Key("/"), Key(keyCode: .keyboardSlash, characters: "/")) 61 | XCTAssertEqual(Key("?"), Key(keyCode: .keyboardSlash, characters: "?", modifierFlags: [.shift])) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/KeyCodesTests/NSEventTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import KeyCodes 3 | 4 | #if os(macOS) 5 | import Carbon.HIToolbox 6 | 7 | final class NSEventTests: XCTestCase { 8 | func testDeviceIndependentOnly() throws { 9 | let eventRef = try XCTUnwrap(CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(kVK_ANSI_0), keyDown: true)) 10 | eventRef.flags = [.maskCommand, .maskNumericPad] 11 | 12 | let event = try XCTUnwrap(NSEvent(cgEvent: eventRef)) 13 | 14 | XCTAssertEqual(event.modifierFlags, [.command, .numericPad]) 15 | XCTAssertEqual(event.modifierFlags.deviceIndependentOnly, [.command, .numericPad]) 16 | 17 | XCTAssertEqual(event.keyModifierFlags, [.command, .numericPad]) 18 | } 19 | 20 | func testDeviceIndependentOnlyFromMouseDown() throws { 21 | let eventRef = try XCTUnwrap(CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: CGPoint(), mouseButton: .left)) 22 | eventRef.flags = [.maskCommand] 23 | 24 | let event = try XCTUnwrap(NSEvent(cgEvent: eventRef)) 25 | 26 | XCTAssertEqual(event.modifierFlags, [.command]) 27 | XCTAssertEqual(event.modifierFlags.deviceIndependentOnly, [.command]) 28 | 29 | XCTAssertEqual(event.keyModifierFlags, [.command]) 30 | } 31 | } 32 | #endif 33 | --------------------------------------------------------------------------------