├── .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 |
--------------------------------------------------------------------------------