├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── keyboard.xcodeproj
└── project.pbxproj
└── keyboard
├── AppComponent.swift
├── AppDelegate.swift
├── Assets.xcassets
└── AppIcon.appiconset
│ └── Contents.json
├── Base.lproj
└── MainMenu.xib
├── Core
├── Emitter.swift
├── EventManager.swift
├── Handler.swift
├── KeyCode.swift
├── RepeatedKey.swift
└── SuperKey.swift
├── Extensions
├── AXAttribute.swift
├── AXError.swift
├── AXUIElement.swift
├── DispatchTime.swift
├── NSScreen.swift
└── TISInputSource.swift
├── Handlers
├── AppQuitHandler.swift
├── AppSwitchHandler.swift
├── EmacsHandler.swift
├── EscapeHandler.swift
├── InputSourceHandler.swift
├── MouseHandler.swift
├── NavigationHandler.swift
├── WindowResizeHandler.swift
└── WordMotionHandler.swift
├── HighlighterView.swift
├── Info.plist
└── MainMenu.xib
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build-and-publish
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: macos-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Show Xcode version
11 | run: xcodebuild -version
12 | - name: Build
13 | run: make build
14 | - name: Create distribution file
15 | run: make dist
16 | - name: Release
17 | uses: softprops/action-gh-release@v1
18 | if: startsWith(github.ref, 'refs/tags/v')
19 | with:
20 | prerelease: ${{ endsWith(github.ref, '-pre') }}
21 | files: build/*.zip
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | *.xcworkspace
4 | *.pbxuser
5 | *.perspectivev3
6 | xcuserdata
7 | *.xccheckout
8 | build
9 |
10 | # CocoaPods
11 | Pods/
12 | /.bundle
13 | /vendor/bundle
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017- Yuki Iwanaga
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash -eu -o pipefail
2 |
3 | NAME := keyboard
4 |
5 | .PHONY: build
6 | build:
7 | @xcodebuild -project keyboard.xcodeproj -target keyboard -configuration Release build
8 |
9 | .PHONY: dist
10 | dist:
11 | @cd build/Release \
12 | && zip -r "$(NAME).zip" Keyboard.app \
13 | && mv "$(NAME).zip" .. \
14 | && cd ../..
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Keyboard
2 | ========
3 |
4 | [](https://github.com/creasty/Keyboard/actions/workflows/build.yml)
5 | [](https://github.com/creasty/Keyboard/releases)
6 | [](./LICENSE)
7 |
8 | Master of keyboard is master of automation.
9 |
10 |
11 | Installation
12 | ------------
13 |
14 | ```
15 | $ brew cask install creasty/tools/keyboard
16 | ```
17 |
18 |
19 | List of actions
20 | ---------------
21 |
22 | `+` denotes a key sequence in the 'super key' mode which is activated by pressing and holding the first letter with no modifier keys.
23 |
24 | ### Window/space navigation
25 |
26 | | Key | Description |
27 | |:---|:---|
28 | | S+H | Move to left space |
29 | | S+L | Move to right space |
30 | | S+J | Switch to next application |
31 | | S+K | Switch to previous application |
32 | | S+N | Switch to next window |
33 | | S+B | Switch to previous window |
34 | | S+M | Mission control |
35 |
36 | Requirements
37 |
38 | Open "System Preferences" and set the following shortcuts.
39 |
40 | - `Mission Control` > `Move left a space` Ctrl-LeftArrow
41 | - `Mission Control` > `Move right a space` Ctrl-RightArrow
42 | - `Keyboard` > `Move focus to next window` Cmd-F1
43 |
44 | | 1 | 2 |
45 | |---|---|
46 | |  |  |
47 |
48 |
49 |
50 | ### Window resizing/positioning
51 |
52 | | Key | Description |
53 | |:---|:---|
54 | | S+D+F | Full screen |
55 | | S+D+H | Left half |
56 | | S+D+J | Bottom half |
57 | | S+D+K | Top half |
58 | | S+D+L | Right half |
59 |
60 | ### Emacs mode
61 |
62 | | Key | Description | Shift allowed |
63 | |:---|:---|:---|
64 | | Ctrl-C | Escape | NO |
65 | | Ctrl-D | Forward delete | NO |
66 | | Ctrl-H | Backspace | NO |
67 | | Ctrl-J | Enter | NO |
68 | | Ctrl-P | :arrow_up: | YES |
69 | | Ctrl-N | :arrow_down: | YES |
70 | | Ctrl-B | :arrow_left: | YES |
71 | | Ctrl-F | :arrow_right: | YES |
72 | | Ctrl-A | Beginning of line | YES |
73 | | Ctrl-E | End of line | YES |
74 |
75 | ### Word motions
76 |
77 | | Key | Description |
78 | |:---|:---|
79 | | A+D | Delete word after cursor |
80 | | A+H | Delete word before cursor |
81 | | A+B | Move cursor backward by word |
82 | | A+F | Move cursor forward by word |
83 |
84 | ### Mouse keys
85 |
86 | Mouse button:
87 |
88 | | Key | Description |
89 | |:---|:---|
90 | | C+M | Left click |
91 | | C+, | Right click |
92 |
93 | Cursor pointer:
94 |
95 | | Key | Description |
96 | |:---|:---|
97 | | | **Parallel movements (10px)** |
98 | | C+H | :arrow_left: |
99 | | C+J | :arrow_down: |
100 | | C+K | :arrow_up: |
101 | | C+L | :arrow_right: |
102 | | | **Parallel movements (10%)** |
103 | | C+S+H | :arrow_left: |
104 | | C+S+J | :arrow_down: |
105 | | C+S+K | :arrow_up: |
106 | | C+S+L | :arrow_right: |
107 | | | **Diagonal movements (10px)** |
108 | | C+H+J | ↙ |
109 | | C+J+L | ↘ |
110 | | C+K+L | ↗ |
111 | | C+H+K | ↖️ |
112 | | | **Diagonal movements (10%)** |
113 | | C+S+H+J | ↙ |
114 | | C+S+J+L | ↘ |
115 | | C+S+K+L | ↗ |
116 | | C+S+H+K | ↖️ |
117 | | | **Quick jump actions** (Highlight enabled) |
118 | | C+Y | Top-left corner |
119 | | C+U | Bottom-left corner |
120 | | C+I | Top-right corner |
121 | | C+O | Bottom-right corner |
122 | | C+U+I | Center of screen |
123 |
124 | Scroll:
125 |
126 | | Key | Description |
127 | |:---|:---|
128 | | C+X+H | :arrow_left: |
129 | | C+X+J | :arrow_down: |
130 | | C+X+K | :arrow_up: |
131 | | C+X+L | :arrow_right: |
132 |
133 | Highlight:
134 |
135 | | Key | Description |
136 | |:---|:---|
137 | | C+Space | Highlight the location of the mouse pointer |
138 |
139 | ### Switch input source
140 |
141 | | Key | Description |
142 | |:---|:---|
143 | | Ctrl-; | Selects next source in the input menu |
144 |
145 | ### Switch input source with Escape key
146 |
147 | Change the input source to English as you leave 'insert mode' in Vim with Escape key so it can prevent IME from capturing key strokes in 'normal mode'.
148 |
149 | | Key | Description |
150 | |:---|:---|
151 | | Ctrl-C | Invokes EISUU, Ctrl-C |
152 | | Escape | Invokes EISUU, Escape |
153 |
154 | ### Switch between apps
155 |
156 | | Key | App | Bundle ID | URL |
157 | |:---|:---|:---|:---|
158 | | ;+F | Finder | `com.apple.finder` | N/A |
159 | | ;+M | Alacritty | `io.alacritty` | https://github.com/jwilm/alacritty |
160 | | ;+T | Things | `com.culturedcode.ThingsMac` | https://culturedcode.com/things |
161 | | ;+N | Bear | `net.shinyfrog.bear` | https://bear.app |
162 |
163 | ### Fool-safe "Quit Application"
164 |
165 | Prevents Cmd-Q from quiting applications.
166 |
167 | | Key | Description |
168 | |:---|:---|
169 | | Cmd-Q | No-op |
170 | | Cmd-Q, Cmd-Q | Invokes Cmd-Q. Quits application |
171 |
--------------------------------------------------------------------------------
/keyboard.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 31C7CD28248546F000A97A11 /* WordMotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C7CD27248546F000A97A11 /* WordMotionHandler.swift */; };
11 | 31C7CD2A24854BB700A97A11 /* MouseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C7CD2924854BB700A97A11 /* MouseHandler.swift */; };
12 | 31C7CD2C2485DBFB00A97A11 /* NSScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C7CD2B2485DBFB00A97A11 /* NSScreen.swift */; };
13 | 31E66C2B2485FD7B004BBEFD /* HighlighterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E66C2A2485FD7B004BBEFD /* HighlighterView.swift */; };
14 | AE38940621D8CB8C0097BE73 /* AXAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38940521D8CB8C0097BE73 /* AXAttribute.swift */; };
15 | AE38940821D8DF3A0097BE73 /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38940721D8DF3A0097BE73 /* AXUIElement.swift */; };
16 | AE38940A21D8DF9F0097BE73 /* AXError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38940921D8DF9F0097BE73 /* AXError.swift */; };
17 | AE38940E21D8E0940097BE73 /* DispatchTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38940D21D8E0940097BE73 /* DispatchTime.swift */; };
18 | AE38941021D8F74B0097BE73 /* Emitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38940F21D8F74B0097BE73 /* Emitter.swift */; };
19 | AE5E8CD02234D86D00A9BC49 /* AppQuitHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5E8CCF2234D86D00A9BC49 /* AppQuitHandler.swift */; };
20 | AE6A00D61ED6A25000A93C2E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6A00D51ED6A25000A93C2E /* AppDelegate.swift */; };
21 | AE6A00D81ED6A25000A93C2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE6A00D71ED6A25000A93C2E /* Assets.xcassets */; };
22 | AE6A00DB1ED6A25000A93C2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = AE6A00D91ED6A25000A93C2E /* MainMenu.xib */; };
23 | AE6A00E41ED88F3F00A93C2E /* EventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6A00E31ED88F3F00A93C2E /* EventManager.swift */; };
24 | AE6A00E61ED8962500A93C2E /* KeyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6A00E51ED8962500A93C2E /* KeyCode.swift */; };
25 | AE6A00E81ED8990E00A93C2E /* SuperKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6A00E71ED8990E00A93C2E /* SuperKey.swift */; };
26 | AE825DF31EDA85000021C6E3 /* RepeatedKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE825DF21EDA85000021C6E3 /* RepeatedKey.swift */; };
27 | AEC1049021FC733400B58342 /* InputSourceHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC1048F21FC733400B58342 /* InputSourceHandler.swift */; };
28 | AEC1049221FC7D5000B58342 /* TISInputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC1049121FC7D5000B58342 /* TISInputSource.swift */; };
29 | AECCF7CD21D8FA8C00B21C0A /* Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7CC21D8FA8C00B21C0A /* Handler.swift */; };
30 | AECCF7D121D8FCE800B21C0A /* NavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7D021D8FCE800B21C0A /* NavigationHandler.swift */; };
31 | AECCF7D321D8FF2800B21C0A /* EmacsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7D221D8FF2800B21C0A /* EmacsHandler.swift */; };
32 | AECCF7D521D8FFE400B21C0A /* EscapeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7D421D8FFE400B21C0A /* EscapeHandler.swift */; };
33 | AECCF7D721D9004E00B21C0A /* WindowResizeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7D621D9004E00B21C0A /* WindowResizeHandler.swift */; };
34 | AECCF7DB21D9072C00B21C0A /* AppComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7DA21D9072C00B21C0A /* AppComponent.swift */; };
35 | AECCF7DD21D90B5B00B21C0A /* AppSwitchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECCF7DC21D90B5B00B21C0A /* AppSwitchHandler.swift */; };
36 | /* End PBXBuildFile section */
37 |
38 | /* Begin PBXFileReference section */
39 | 31C7CD27248546F000A97A11 /* WordMotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordMotionHandler.swift; sourceTree = ""; };
40 | 31C7CD2924854BB700A97A11 /* MouseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseHandler.swift; sourceTree = ""; };
41 | 31C7CD2B2485DBFB00A97A11 /* NSScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreen.swift; sourceTree = ""; };
42 | 31E66C2A2485FD7B004BBEFD /* HighlighterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterView.swift; sourceTree = ""; };
43 | AE38940521D8CB8C0097BE73 /* AXAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AXAttribute.swift; sourceTree = ""; };
44 | AE38940721D8DF3A0097BE73 /* AXUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = ""; };
45 | AE38940921D8DF9F0097BE73 /* AXError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AXError.swift; sourceTree = ""; };
46 | AE38940D21D8E0940097BE73 /* DispatchTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTime.swift; sourceTree = ""; };
47 | AE38940F21D8F74B0097BE73 /* Emitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emitter.swift; sourceTree = ""; };
48 | AE5E8CCF2234D86D00A9BC49 /* AppQuitHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppQuitHandler.swift; sourceTree = ""; };
49 | AE6A00D21ED6A25000A93C2E /* Keyboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Keyboard.app; sourceTree = BUILT_PRODUCTS_DIR; };
50 | AE6A00D51ED6A25000A93C2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
51 | AE6A00D71ED6A25000A93C2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
52 | AE6A00DA1ED6A25000A93C2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
53 | AE6A00DC1ED6A25000A93C2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
54 | AE6A00E31ED88F3F00A93C2E /* EventManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventManager.swift; sourceTree = ""; };
55 | AE6A00E51ED8962500A93C2E /* KeyCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCode.swift; sourceTree = ""; };
56 | AE6A00E71ED8990E00A93C2E /* SuperKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuperKey.swift; sourceTree = ""; };
57 | AE825DF21EDA85000021C6E3 /* RepeatedKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepeatedKey.swift; sourceTree = ""; };
58 | AEC1048F21FC733400B58342 /* InputSourceHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceHandler.swift; sourceTree = ""; };
59 | AEC1049121FC7D5000B58342 /* TISInputSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TISInputSource.swift; sourceTree = ""; };
60 | AECCF7CC21D8FA8C00B21C0A /* Handler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Handler.swift; sourceTree = ""; };
61 | AECCF7D021D8FCE800B21C0A /* NavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationHandler.swift; sourceTree = ""; };
62 | AECCF7D221D8FF2800B21C0A /* EmacsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmacsHandler.swift; sourceTree = ""; };
63 | AECCF7D421D8FFE400B21C0A /* EscapeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapeHandler.swift; sourceTree = ""; };
64 | AECCF7D621D9004E00B21C0A /* WindowResizeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowResizeHandler.swift; sourceTree = ""; };
65 | AECCF7DA21D9072C00B21C0A /* AppComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppComponent.swift; sourceTree = ""; };
66 | AECCF7DC21D90B5B00B21C0A /* AppSwitchHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSwitchHandler.swift; sourceTree = ""; };
67 | /* End PBXFileReference section */
68 |
69 | /* Begin PBXFrameworksBuildPhase section */
70 | AE6A00CF1ED6A25000A93C2E /* Frameworks */ = {
71 | isa = PBXFrameworksBuildPhase;
72 | buildActionMask = 2147483647;
73 | files = (
74 | );
75 | runOnlyForDeploymentPostprocessing = 0;
76 | };
77 | /* End PBXFrameworksBuildPhase section */
78 |
79 | /* Begin PBXGroup section */
80 | AE38940421D8CB6D0097BE73 /* Extensions */ = {
81 | isa = PBXGroup;
82 | children = (
83 | AE38940521D8CB8C0097BE73 /* AXAttribute.swift */,
84 | AE38940921D8DF9F0097BE73 /* AXError.swift */,
85 | AE38940721D8DF3A0097BE73 /* AXUIElement.swift */,
86 | AE38940D21D8E0940097BE73 /* DispatchTime.swift */,
87 | 31C7CD2B2485DBFB00A97A11 /* NSScreen.swift */,
88 | AEC1049121FC7D5000B58342 /* TISInputSource.swift */,
89 | );
90 | path = Extensions;
91 | sourceTree = "";
92 | };
93 | AE6A00C91ED6A25000A93C2E = {
94 | isa = PBXGroup;
95 | children = (
96 | AE6A00D31ED6A25000A93C2E /* Products */,
97 | AE6A00D41ED6A25000A93C2E /* keyboard */,
98 | );
99 | sourceTree = "";
100 | };
101 | AE6A00D31ED6A25000A93C2E /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | AE6A00D21ED6A25000A93C2E /* Keyboard.app */,
105 | );
106 | name = Products;
107 | sourceTree = "";
108 | };
109 | AE6A00D41ED6A25000A93C2E /* keyboard */ = {
110 | isa = PBXGroup;
111 | children = (
112 | AE6A00E21ED88F0200A93C2E /* Core */,
113 | AE38940421D8CB6D0097BE73 /* Extensions */,
114 | AECCF7CB21D8FA6F00B21C0A /* Handlers */,
115 | AECCF7DA21D9072C00B21C0A /* AppComponent.swift */,
116 | AE6A00D51ED6A25000A93C2E /* AppDelegate.swift */,
117 | AE6A00D71ED6A25000A93C2E /* Assets.xcassets */,
118 | AE6A00DC1ED6A25000A93C2E /* Info.plist */,
119 | AE6A00D91ED6A25000A93C2E /* MainMenu.xib */,
120 | 31E66C2A2485FD7B004BBEFD /* HighlighterView.swift */,
121 | );
122 | path = keyboard;
123 | sourceTree = "";
124 | };
125 | AE6A00E21ED88F0200A93C2E /* Core */ = {
126 | isa = PBXGroup;
127 | children = (
128 | AE38940F21D8F74B0097BE73 /* Emitter.swift */,
129 | AE6A00E31ED88F3F00A93C2E /* EventManager.swift */,
130 | AECCF7CC21D8FA8C00B21C0A /* Handler.swift */,
131 | AE6A00E51ED8962500A93C2E /* KeyCode.swift */,
132 | AE825DF21EDA85000021C6E3 /* RepeatedKey.swift */,
133 | AE6A00E71ED8990E00A93C2E /* SuperKey.swift */,
134 | );
135 | path = Core;
136 | sourceTree = "";
137 | };
138 | AECCF7CB21D8FA6F00B21C0A /* Handlers */ = {
139 | isa = PBXGroup;
140 | children = (
141 | AE5E8CCF2234D86D00A9BC49 /* AppQuitHandler.swift */,
142 | AECCF7DC21D90B5B00B21C0A /* AppSwitchHandler.swift */,
143 | AECCF7D221D8FF2800B21C0A /* EmacsHandler.swift */,
144 | AECCF7D421D8FFE400B21C0A /* EscapeHandler.swift */,
145 | AEC1048F21FC733400B58342 /* InputSourceHandler.swift */,
146 | 31C7CD2924854BB700A97A11 /* MouseHandler.swift */,
147 | AECCF7D021D8FCE800B21C0A /* NavigationHandler.swift */,
148 | AECCF7D621D9004E00B21C0A /* WindowResizeHandler.swift */,
149 | 31C7CD27248546F000A97A11 /* WordMotionHandler.swift */,
150 | );
151 | path = Handlers;
152 | sourceTree = "";
153 | };
154 | /* End PBXGroup section */
155 |
156 | /* Begin PBXNativeTarget section */
157 | AE6A00D11ED6A25000A93C2E /* keyboard */ = {
158 | isa = PBXNativeTarget;
159 | buildConfigurationList = AE6A00DF1ED6A25000A93C2E /* Build configuration list for PBXNativeTarget "keyboard" */;
160 | buildPhases = (
161 | AE6A00CE1ED6A25000A93C2E /* Sources */,
162 | AE6A00CF1ED6A25000A93C2E /* Frameworks */,
163 | AE6A00D01ED6A25000A93C2E /* Resources */,
164 | );
165 | buildRules = (
166 | );
167 | dependencies = (
168 | );
169 | name = keyboard;
170 | productName = keyboard;
171 | productReference = AE6A00D21ED6A25000A93C2E /* Keyboard.app */;
172 | productType = "com.apple.product-type.application";
173 | };
174 | /* End PBXNativeTarget section */
175 |
176 | /* Begin PBXProject section */
177 | AE6A00CA1ED6A25000A93C2E /* Project object */ = {
178 | isa = PBXProject;
179 | attributes = {
180 | LastSwiftUpdateCheck = 0820;
181 | LastUpgradeCheck = 1000;
182 | ORGANIZATIONNAME = Creasty;
183 | TargetAttributes = {
184 | AE6A00D11ED6A25000A93C2E = {
185 | CreatedOnToolsVersion = 8.2.1;
186 | ProvisioningStyle = Automatic;
187 | };
188 | };
189 | };
190 | buildConfigurationList = AE6A00CD1ED6A25000A93C2E /* Build configuration list for PBXProject "keyboard" */;
191 | compatibilityVersion = "Xcode 3.2";
192 | developmentRegion = English;
193 | hasScannedForEncodings = 0;
194 | knownRegions = (
195 | English,
196 | en,
197 | Base,
198 | );
199 | mainGroup = AE6A00C91ED6A25000A93C2E;
200 | productRefGroup = AE6A00D31ED6A25000A93C2E /* Products */;
201 | projectDirPath = "";
202 | projectRoot = "";
203 | targets = (
204 | AE6A00D11ED6A25000A93C2E /* keyboard */,
205 | );
206 | };
207 | /* End PBXProject section */
208 |
209 | /* Begin PBXResourcesBuildPhase section */
210 | AE6A00D01ED6A25000A93C2E /* Resources */ = {
211 | isa = PBXResourcesBuildPhase;
212 | buildActionMask = 2147483647;
213 | files = (
214 | AE6A00D81ED6A25000A93C2E /* Assets.xcassets in Resources */,
215 | AE6A00DB1ED6A25000A93C2E /* MainMenu.xib in Resources */,
216 | );
217 | runOnlyForDeploymentPostprocessing = 0;
218 | };
219 | /* End PBXResourcesBuildPhase section */
220 |
221 | /* Begin PBXSourcesBuildPhase section */
222 | AE6A00CE1ED6A25000A93C2E /* Sources */ = {
223 | isa = PBXSourcesBuildPhase;
224 | buildActionMask = 2147483647;
225 | files = (
226 | AEC1049021FC733400B58342 /* InputSourceHandler.swift in Sources */,
227 | 31C7CD2C2485DBFB00A97A11 /* NSScreen.swift in Sources */,
228 | AE5E8CD02234D86D00A9BC49 /* AppQuitHandler.swift in Sources */,
229 | AECCF7D121D8FCE800B21C0A /* NavigationHandler.swift in Sources */,
230 | AECCF7D321D8FF2800B21C0A /* EmacsHandler.swift in Sources */,
231 | AECCF7CD21D8FA8C00B21C0A /* Handler.swift in Sources */,
232 | AE6A00E41ED88F3F00A93C2E /* EventManager.swift in Sources */,
233 | 31C7CD2A24854BB700A97A11 /* MouseHandler.swift in Sources */,
234 | AECCF7DB21D9072C00B21C0A /* AppComponent.swift in Sources */,
235 | AE6A00E61ED8962500A93C2E /* KeyCode.swift in Sources */,
236 | AE38940621D8CB8C0097BE73 /* AXAttribute.swift in Sources */,
237 | AE6A00D61ED6A25000A93C2E /* AppDelegate.swift in Sources */,
238 | AECCF7DD21D90B5B00B21C0A /* AppSwitchHandler.swift in Sources */,
239 | 31E66C2B2485FD7B004BBEFD /* HighlighterView.swift in Sources */,
240 | AE38940821D8DF3A0097BE73 /* AXUIElement.swift in Sources */,
241 | AE38940E21D8E0940097BE73 /* DispatchTime.swift in Sources */,
242 | 31C7CD28248546F000A97A11 /* WordMotionHandler.swift in Sources */,
243 | AE38940A21D8DF9F0097BE73 /* AXError.swift in Sources */,
244 | AEC1049221FC7D5000B58342 /* TISInputSource.swift in Sources */,
245 | AE825DF31EDA85000021C6E3 /* RepeatedKey.swift in Sources */,
246 | AE38941021D8F74B0097BE73 /* Emitter.swift in Sources */,
247 | AECCF7D521D8FFE400B21C0A /* EscapeHandler.swift in Sources */,
248 | AE6A00E81ED8990E00A93C2E /* SuperKey.swift in Sources */,
249 | AECCF7D721D9004E00B21C0A /* WindowResizeHandler.swift in Sources */,
250 | );
251 | runOnlyForDeploymentPostprocessing = 0;
252 | };
253 | /* End PBXSourcesBuildPhase section */
254 |
255 | /* Begin PBXVariantGroup section */
256 | AE6A00D91ED6A25000A93C2E /* MainMenu.xib */ = {
257 | isa = PBXVariantGroup;
258 | children = (
259 | AE6A00DA1ED6A25000A93C2E /* Base */,
260 | );
261 | name = MainMenu.xib;
262 | path = .;
263 | sourceTree = "";
264 | };
265 | /* End PBXVariantGroup section */
266 |
267 | /* Begin XCBuildConfiguration section */
268 | AE6A00DD1ED6A25000A93C2E /* Debug */ = {
269 | isa = XCBuildConfiguration;
270 | buildSettings = {
271 | ALWAYS_SEARCH_USER_PATHS = NO;
272 | CLANG_ANALYZER_NONNULL = YES;
273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
274 | CLANG_CXX_LIBRARY = "libc++";
275 | CLANG_ENABLE_MODULES = YES;
276 | CLANG_ENABLE_OBJC_ARC = YES;
277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
278 | CLANG_WARN_BOOL_CONVERSION = YES;
279 | CLANG_WARN_COMMA = YES;
280 | CLANG_WARN_CONSTANT_CONVERSION = YES;
281 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
282 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
283 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
284 | CLANG_WARN_EMPTY_BODY = YES;
285 | CLANG_WARN_ENUM_CONVERSION = YES;
286 | CLANG_WARN_INFINITE_RECURSION = YES;
287 | CLANG_WARN_INT_CONVERSION = YES;
288 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
290 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
291 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
292 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
293 | CLANG_WARN_STRICT_PROTOTYPES = YES;
294 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
295 | CLANG_WARN_UNREACHABLE_CODE = YES;
296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
297 | CODE_SIGN_IDENTITY = "-";
298 | COPY_PHASE_STRIP = NO;
299 | DEBUG_INFORMATION_FORMAT = dwarf;
300 | ENABLE_STRICT_OBJC_MSGSEND = YES;
301 | ENABLE_TESTABILITY = YES;
302 | GCC_C_LANGUAGE_STANDARD = gnu99;
303 | GCC_DYNAMIC_NO_PIC = NO;
304 | GCC_NO_COMMON_BLOCKS = YES;
305 | GCC_OPTIMIZATION_LEVEL = 0;
306 | GCC_PREPROCESSOR_DEFINITIONS = (
307 | "DEBUG=1",
308 | "$(inherited)",
309 | );
310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
312 | GCC_WARN_UNDECLARED_SELECTOR = YES;
313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
314 | GCC_WARN_UNUSED_FUNCTION = YES;
315 | GCC_WARN_UNUSED_VARIABLE = YES;
316 | MACOSX_DEPLOYMENT_TARGET = 10.15;
317 | MTL_ENABLE_DEBUG_INFO = YES;
318 | ONLY_ACTIVE_ARCH = YES;
319 | SDKROOT = macosx;
320 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
322 | };
323 | name = Debug;
324 | };
325 | AE6A00DE1ED6A25000A93C2E /* Release */ = {
326 | isa = XCBuildConfiguration;
327 | buildSettings = {
328 | ALWAYS_SEARCH_USER_PATHS = NO;
329 | CLANG_ANALYZER_NONNULL = YES;
330 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
331 | CLANG_CXX_LIBRARY = "libc++";
332 | CLANG_ENABLE_MODULES = YES;
333 | CLANG_ENABLE_OBJC_ARC = YES;
334 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
335 | CLANG_WARN_BOOL_CONVERSION = YES;
336 | CLANG_WARN_COMMA = YES;
337 | CLANG_WARN_CONSTANT_CONVERSION = YES;
338 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
339 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
340 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
341 | CLANG_WARN_EMPTY_BODY = YES;
342 | CLANG_WARN_ENUM_CONVERSION = YES;
343 | CLANG_WARN_INFINITE_RECURSION = YES;
344 | CLANG_WARN_INT_CONVERSION = YES;
345 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
347 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
349 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
350 | CLANG_WARN_STRICT_PROTOTYPES = YES;
351 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
352 | CLANG_WARN_UNREACHABLE_CODE = YES;
353 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
354 | CODE_SIGN_IDENTITY = "-";
355 | COPY_PHASE_STRIP = NO;
356 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
357 | ENABLE_NS_ASSERTIONS = NO;
358 | ENABLE_STRICT_OBJC_MSGSEND = YES;
359 | GCC_C_LANGUAGE_STANDARD = gnu99;
360 | GCC_NO_COMMON_BLOCKS = YES;
361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
363 | GCC_WARN_UNDECLARED_SELECTOR = YES;
364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
365 | GCC_WARN_UNUSED_FUNCTION = YES;
366 | GCC_WARN_UNUSED_VARIABLE = YES;
367 | MACOSX_DEPLOYMENT_TARGET = 10.15;
368 | MTL_ENABLE_DEBUG_INFO = NO;
369 | SDKROOT = macosx;
370 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
371 | };
372 | name = Release;
373 | };
374 | AE6A00E01ED6A25000A93C2E /* Debug */ = {
375 | isa = XCBuildConfiguration;
376 | buildSettings = {
377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
378 | COMBINE_HIDPI_IMAGES = YES;
379 | INFOPLIST_FILE = keyboard/Info.plist;
380 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
381 | PRODUCT_BUNDLE_IDENTIFIER = "com.creasty.keyboard-dev";
382 | PRODUCT_DISPLAY_NAME = "Keyboard Dev";
383 | PRODUCT_NAME = Keyboard;
384 | SWIFT_VERSION = 4.2;
385 | };
386 | name = Debug;
387 | };
388 | AE6A00E11ED6A25000A93C2E /* Release */ = {
389 | isa = XCBuildConfiguration;
390 | buildSettings = {
391 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
392 | COMBINE_HIDPI_IMAGES = YES;
393 | INFOPLIST_FILE = keyboard/Info.plist;
394 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
395 | PRODUCT_BUNDLE_IDENTIFIER = com.creasty.keyboard;
396 | PRODUCT_DISPLAY_NAME = Keyboard;
397 | PRODUCT_NAME = Keyboard;
398 | SWIFT_VERSION = 4.2;
399 | };
400 | name = Release;
401 | };
402 | /* End XCBuildConfiguration section */
403 |
404 | /* Begin XCConfigurationList section */
405 | AE6A00CD1ED6A25000A93C2E /* Build configuration list for PBXProject "keyboard" */ = {
406 | isa = XCConfigurationList;
407 | buildConfigurations = (
408 | AE6A00DD1ED6A25000A93C2E /* Debug */,
409 | AE6A00DE1ED6A25000A93C2E /* Release */,
410 | );
411 | defaultConfigurationIsVisible = 0;
412 | defaultConfigurationName = Release;
413 | };
414 | AE6A00DF1ED6A25000A93C2E /* Build configuration list for PBXNativeTarget "keyboard" */ = {
415 | isa = XCConfigurationList;
416 | buildConfigurations = (
417 | AE6A00E01ED6A25000A93C2E /* Debug */,
418 | AE6A00E11ED6A25000A93C2E /* Release */,
419 | );
420 | defaultConfigurationIsVisible = 0;
421 | defaultConfigurationName = Release;
422 | };
423 | /* End XCConfigurationList section */
424 | };
425 | rootObject = AE6A00CA1ED6A25000A93C2E /* Project object */;
426 | }
427 |
--------------------------------------------------------------------------------
/keyboard/AppComponent.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | // Needs to be globally accesible
4 | var _eventManager: EventManagerType?
5 | var _eventTap: CFMachPort?
6 |
7 | final class AppComponent {
8 | let showHighlightCallback: () -> Void
9 |
10 | let nsWorkspace = NSWorkspace.shared
11 | let fileManager = FileManager.default
12 |
13 | let eventTapCallback: CGEventTapCallBack = { (proxy, type, event, _) in
14 | switch type {
15 | case .tapDisabledByTimeout:
16 | if let tap = _eventTap {
17 | CGEvent.tapEnable(tap: tap, enable: true) // Re-enable
18 | }
19 | case .keyUp, .keyDown:
20 | if let manager = _eventManager {
21 | return manager.handle(proxy: proxy, cgEvent: event)
22 | }
23 | default:
24 | break
25 | }
26 | return Unmanaged.passRetained(event)
27 | }
28 |
29 | private(set) var emitter: EmitterType = Emitter()
30 |
31 | init(showHighlightCallback: @escaping () -> Void) {
32 | self.showHighlightCallback = showHighlightCallback
33 | }
34 |
35 | func navigationHandler() -> Handler {
36 | return NavigationHandler(
37 | workspace: nsWorkspace,
38 | fileManager: fileManager,
39 | emitter: emitter
40 | )
41 | }
42 |
43 | func emacsHandler() -> Handler {
44 | return EmacsHandler(workspace: nsWorkspace, emitter: emitter)
45 | }
46 |
47 | func wordMotionHandler() -> Handler {
48 | return WordMotionHandler(workspace: nsWorkspace, emitter: emitter)
49 | }
50 |
51 | func escapeHandler() -> Handler {
52 | return EscapeHandler(emitter: emitter)
53 | }
54 |
55 | func windowResizeHandler() -> Handler {
56 | return WindowResizeHandler(workspace: nsWorkspace)
57 | }
58 |
59 | func mouseHandler() -> Handler {
60 | return MouseHandler(emitter: emitter, showHighlight: showHighlightCallback)
61 | }
62 |
63 | func appSwitchHandler() -> Handler {
64 | return AppSwitchHandler(workspace: nsWorkspace)
65 | }
66 |
67 | func inputMethodHandler() -> Handler {
68 | return InputSourceHandler()
69 | }
70 |
71 | func appQuithHandler() -> Handler {
72 | return AppQuithHandler()
73 | }
74 |
75 | func eventManager() -> EventManagerType {
76 | let eventManager: EventManagerType = EventManager(emitter: emitter)
77 | eventManager.register(handler: emacsHandler())
78 | eventManager.register(handler: escapeHandler())
79 | eventManager.register(handler: navigationHandler())
80 | eventManager.register(handler: mouseHandler())
81 | eventManager.register(handler: wordMotionHandler())
82 | eventManager.register(handler: windowResizeHandler())
83 | eventManager.register(handler: appSwitchHandler())
84 | eventManager.register(handler: inputMethodHandler())
85 | eventManager.register(handler: appQuithHandler())
86 | return eventManager
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/keyboard/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | @NSApplicationMain
4 | class AppDelegate: NSObject, NSApplicationDelegate {
5 | private lazy var statusItem: NSStatusItem = {
6 | return NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
7 | }()
8 |
9 | private lazy var appComponent: AppComponent = {
10 | return AppComponent(showHighlightCallback: { [weak self] in
11 | self?.showHighlight()
12 | })
13 | }()
14 |
15 | private var window: NSWindow?
16 | private var highlighterWork: DispatchWorkItem?
17 |
18 | func applicationDidFinishLaunching(_ aNotification: Notification) {
19 | guard isProcessTrusted() else {
20 | exit(1)
21 | }
22 |
23 | setupWindow()
24 | setupStatusItem()
25 | setupEventManager()
26 | setupEventTap()
27 | }
28 |
29 | func applicationWillTerminate(_ aNotification: Notification) {
30 | }
31 |
32 | private func isProcessTrusted() -> Bool {
33 | let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
34 | let opts = [promptKey: true] as CFDictionary
35 | return AXIsProcessTrustedWithOptions(opts)
36 | }
37 |
38 | @objc
39 | private func handleQuit() {
40 | NSApplication.shared.terminate(nil)
41 | }
42 | }
43 |
44 | private extension AppDelegate {
45 | func setupWindow() {
46 | let window = NSWindow(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: true)
47 | window.isOpaque = false
48 | window.makeKeyAndOrderFront(nil)
49 | window.backgroundColor = .clear
50 | window.level = .floating
51 | self.window = window
52 | }
53 |
54 | func setupStatusItem() {
55 | if let button = statusItem.button {
56 | button.title = "K"
57 | }
58 |
59 | statusItem.menu = {
60 | let menu = NSMenu()
61 | menu.addItem(NSMenuItem(title: "Creasty's Keyboard", action: nil, keyEquivalent: ""))
62 | menu.addItem(NSMenuItem.separator())
63 | menu.addItem(NSMenuItem(title: "Quit", action: #selector(handleQuit), keyEquivalent: "q"))
64 | return menu
65 | }()
66 | }
67 |
68 | func setupEventManager() {
69 | _eventManager = appComponent.eventManager()
70 | }
71 |
72 | func setupEventTap() {
73 | let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
74 |
75 | guard let eventTap = CGEvent.tapCreate(
76 | tap: .cghidEventTap,
77 | place: .headInsertEventTap,
78 | options: .defaultTap,
79 | eventsOfInterest: CGEventMask(eventMask),
80 | callback: appComponent.eventTapCallback,
81 | userInfo: nil
82 | ) else {
83 | fatalError("Failed to create event tap")
84 | }
85 | _eventTap = eventTap
86 |
87 | let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
88 | CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
89 | CGEvent.tapEnable(tap: eventTap, enable: true)
90 | CFRunLoopRun()
91 | }
92 |
93 | func showHighlight() {
94 | guard let screen = NSScreen.currentScreen else { return }
95 | guard let window = window else { return }
96 |
97 | let highlighterView = HighlighterView(frame: screen.frame)
98 | highlighterView.location = {
99 | var mouseLocation = NSEvent.mouseLocation
100 | mouseLocation.x -= screen.frame.origin.x
101 | mouseLocation.y -= screen.frame.origin.y
102 | return mouseLocation
103 | }()
104 |
105 | highlighterWork?.cancel()
106 | let work = DispatchWorkItem() { self.hideHighlight() }
107 | highlighterWork = work
108 |
109 | window.contentView = highlighterView
110 | window.setFrame(screen.frame, display: true)
111 |
112 | let dispatchTime = DispatchTime.now() + DispatchTimeInterval.seconds(1)
113 | DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: work)
114 | }
115 |
116 | func hideHighlight() {
117 | window?.contentView = nil
118 | window?.setFrame(.zero, display: false)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/keyboard/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 | }
--------------------------------------------------------------------------------
/keyboard/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/keyboard/Core/Emitter.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | enum EmitterKeyAction {
4 | case down
5 | case up
6 | case both
7 |
8 | fileprivate var keyDowns: [Bool] {
9 | switch self {
10 | case .down:
11 | return [true]
12 | case .up:
13 | return [false]
14 | case .both:
15 | return [true, false]
16 | }
17 | }
18 | }
19 |
20 | enum EmitterMouseButton {
21 | case left
22 | case right
23 |
24 | fileprivate var eventParams: (CGEventType, CGEventType, CGMouseButton) {
25 | switch self {
26 | case .left:
27 | return (.leftMouseDown, .leftMouseUp, .left)
28 | case .right:
29 | return (.rightMouseDown, .rightMouseUp, .right)
30 | }
31 | }
32 | }
33 |
34 | protocol EmitterType {
35 | func setProxy(_ proxy: CGEventTapProxy?)
36 |
37 | func emit(keyCode: KeyCode, flags: CGEventFlags, action: EmitterKeyAction)
38 | func emit(mouseMoveTo location: CGPoint)
39 | func emit(mouseClick button: EmitterMouseButton)
40 | func emit(mouseScroll point: CGPoint)
41 | }
42 |
43 | class Emitter: EmitterType {
44 | struct Const {
45 | static let pauseInterval: UInt32 = 1000
46 | }
47 |
48 | private var proxy: CGEventTapProxy?
49 |
50 | func setProxy(_ proxy: CGEventTapProxy?) {
51 | self.proxy = proxy
52 | }
53 |
54 | func emit(keyCode: KeyCode, flags: CGEventFlags, action: EmitterKeyAction) {
55 | var shouldPause = false
56 |
57 | action.keyDowns.forEach {
58 | if shouldPause {
59 | pause()
60 | }
61 | shouldPause = true
62 |
63 | let e = CGEvent(
64 | keyboardEventSource: nil,
65 | virtualKey: keyCode.rawValue,
66 | keyDown: $0
67 | )
68 | e?.flags = flags
69 | e?.tapPostEvent(proxy)
70 | }
71 | }
72 |
73 | func emit(mouseMoveTo location: CGPoint) {
74 | CGEvent(
75 | mouseEventSource: nil,
76 | mouseType: .mouseMoved,
77 | mouseCursorPosition: location,
78 | mouseButton: .right
79 | )?.post(tap: .cghidEventTap)
80 | }
81 |
82 | func emit(mouseClick button: EmitterMouseButton) {
83 | guard let voidEvent = CGEvent(source: nil) else { return }
84 |
85 | let (downEventType, upEventType, cgMouseButton) = button.eventParams
86 |
87 | CGEvent(
88 | mouseEventSource: nil,
89 | mouseType: downEventType,
90 | mouseCursorPosition: voidEvent.location,
91 | mouseButton: cgMouseButton
92 | )?.post(tap: .cghidEventTap)
93 |
94 | pause()
95 |
96 | CGEvent(
97 | mouseEventSource: nil,
98 | mouseType: upEventType,
99 | mouseCursorPosition: voidEvent.location,
100 | mouseButton: cgMouseButton
101 | )?.post(tap: .cghidEventTap)
102 | }
103 |
104 | func emit(mouseScroll point: CGPoint) {
105 | CGEvent(
106 | scrollWheelEvent2Source: nil,
107 | units: .pixel,
108 | wheelCount: 2,
109 | wheel1: Int32(point.y),
110 | wheel2: Int32(point.x),
111 | wheel3: 0
112 | )?.post(tap: .cghidEventTap)
113 | }
114 |
115 | func pause() {
116 | // NOTE: it's not possible to post consecutive events
117 | usleep(Const.pauseInterval)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/keyboard/Core/EventManager.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | protocol EventManagerType {
4 | func register(handler: Handler)
5 | func handle(proxy: CGEventTapProxy, cgEvent: CGEvent) -> Unmanaged?
6 | }
7 |
8 | final class EventManager: EventManagerType {
9 | private let emitter: EmitterType
10 |
11 | private var handlers = [Handler]()
12 | private let superKey = SuperKey()
13 | private var superKeyPrefixes = Set()
14 |
15 | init(emitter: EmitterType) {
16 | self.emitter = emitter
17 | }
18 |
19 | func register(handler: Handler) {
20 | handlers.append(handler)
21 |
22 | handler.activateSuperKeys().forEach {
23 | superKeyPrefixes.insert($0)
24 | }
25 | }
26 |
27 | func handle(proxy: CGEventTapProxy, cgEvent: CGEvent) -> Unmanaged? {
28 | emitter.setProxy(proxy)
29 |
30 | guard let event = NSEvent(cgEvent: cgEvent) else {
31 | return Unmanaged.passUnretained(cgEvent)
32 | }
33 | guard let keyEvent = KeyEvent(nsEvent: event) else {
34 | return Unmanaged.passUnretained(cgEvent)
35 | }
36 |
37 | let action = updateSuperKey(keyEvent: keyEvent)
38 | ?? handleSuperKey(keyEvent: keyEvent)
39 | ?? handle(keyEvent: keyEvent)
40 | ?? .passThrough
41 |
42 | switch action {
43 | case .prevent:
44 | return nil
45 | case .passThrough:
46 | return Unmanaged.passUnretained(cgEvent)
47 | }
48 | }
49 | }
50 |
51 | extension EventManager: Handler {
52 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
53 | for handler in handlers {
54 | if let action = handler.handle(keyEvent: keyEvent) {
55 | return action
56 | }
57 | }
58 | return nil
59 | }
60 |
61 | @discardableResult
62 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
63 | for handler in handlers {
64 | if handler.handleSuperKey(prefix: prefix, keys: keys) {
65 | return true
66 | }
67 | }
68 | return false
69 | }
70 | }
71 |
72 | private extension EventManager {
73 | func updateSuperKey(keyEvent: KeyEvent) -> HandlerAction? {
74 | guard keyEvent.match() else {
75 | superKey.inactivate()
76 | return nil
77 | }
78 |
79 | if superKeyPrefixes.contains(keyEvent.code) {
80 | // Activte the mode
81 | if keyEvent.isDown, superKey.activate(prefixKey: keyEvent.code) {
82 | return .prevent
83 | }
84 |
85 | if let prefixKey = superKey.prefixKey, prefixKey == keyEvent.code {
86 | // Cancel on the final keyup
87 | if !keyEvent.isDown && !keyEvent.isARepeat {
88 | switch superKey.state {
89 | case .activated:
90 | emitter.emit(keyCode: prefixKey, flags: [], action: .both)
91 | case .used, .enabled:
92 | // Abort a pending operation if any
93 | if let pendingKey = superKey.cancel() {
94 | emitter.emit(keyCode: prefixKey, flags: [], action: .both)
95 | emitter.emit(keyCode: pendingKey, flags: [], action: .both)
96 | } else {
97 | // Trigger any key events to clean up
98 | emitter.emit(keyCode: .command, flags: [], action: .both)
99 | }
100 | default: break
101 | }
102 |
103 | // Restore the state
104 | superKey.inactivate()
105 | }
106 |
107 | // Always ignore the prefix key
108 | return .prevent
109 | }
110 | }
111 |
112 | // Disable when another key was pressed immediately after the activation
113 | if keyEvent.isDown, !superKey.enable() {
114 | if let prefixKey = superKey.prefixKey {
115 | emitter.emit(keyCode: prefixKey, flags: [], action: .both)
116 | }
117 | emitter.emit(keyCode: keyEvent.code, flags: [], action: .down)
118 |
119 | return .prevent
120 | }
121 |
122 | return nil
123 | }
124 |
125 | func handleSuperKey(keyEvent: KeyEvent) -> HandlerAction? {
126 | guard superKey.isEnabled, let prefixKey = superKey.prefixKey else {
127 | return nil
128 | }
129 | guard keyEvent.match() else {
130 | return nil
131 | }
132 |
133 | superKey.perform(key: keyEvent.code, isKeyDown: keyEvent.isDown) { [weak self] (keys) in
134 | self?.handleSuperKey(prefix: prefixKey, keys: keys)
135 | }
136 |
137 | return .prevent
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/keyboard/Core/Handler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | enum HandlerAction {
4 | case prevent
5 | case passThrough
6 | }
7 |
8 | protocol Handler {
9 | func activateSuperKeys() -> [KeyCode]
10 | func handle(keyEvent: KeyEvent) -> HandlerAction?
11 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool
12 | }
13 |
14 | extension Handler {
15 | func activateSuperKeys() -> [KeyCode] {
16 | return []
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/keyboard/Core/KeyCode.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | enum KeyCode: UInt16 {
4 | case a = 0x00
5 | case s = 0x01
6 | case d = 0x02
7 | case f = 0x03
8 | case h = 0x04
9 | case g = 0x05
10 | case z = 0x06
11 | case x = 0x07
12 | case c = 0x08
13 | case v = 0x09
14 | case b = 0x0b
15 | case q = 0x0c
16 | case w = 0x0d
17 | case e = 0x0e
18 | case r = 0x0f
19 | case y = 0x10
20 | case t = 0x11
21 | case i1 = 0x12 // exclamation "!"
22 | case i2 = 0x13 // atmark "@"
23 | case i3 = 0x14 // hash "#"
24 | case i4 = 0x15 // dollar "$"
25 | case i5 = 0x17 // percent "%"
26 | case i6 = 0x16 // hat "^"
27 | case equal = 0x18 // plus "+"
28 | case i9 = 0x19 // leftParen "("
29 | case i7 = 0x1a // ampersand "&"
30 | case minus = 0x1b
31 | case i8 = 0x1c // asterisk "*"
32 | case i0 = 0x1d // rightParen ")"
33 | case rightBracket = 0x1e // "]" / rightBrace "}"
34 | case o = 0x1f
35 | case u = 0x20
36 | case leftBracket = 0x21 // "[" / leftBrace "{"
37 | case i = 0x22
38 | case p = 0x23
39 | case l = 0x25
40 | case j = 0x26
41 | case doubleQuote = 0x27 // """ / singleQuote "'"
42 | case k = 0x28
43 | case semicolon = 0x29 // ";" / colon ":"
44 | case backslash = 0x2a // "\" / pipe "|"
45 | case comma = 0x2b // "," / leftAngledBracket "<"
46 | case slash = 0x2c // "/" / question "?"
47 | case n = 0x2d
48 | case m = 0x2e
49 | case period = 0x2f // "." / rightAngledBracket ">"
50 | case backtick = 0x32 // "`" / tilde "~"
51 | case keypadDecimal = 0x41
52 | case keypadMultiply = 0x43
53 | case keypadPlus = 0x45
54 | case keypadClear = 0x47
55 | case keypadDivide = 0x4b
56 | case keypadEnter = 0x4c
57 | case keypadMinus = 0x4e
58 | case keypadEquals = 0x51
59 | case keypad0 = 0x52
60 | case keypad1 = 0x53
61 | case keypad2 = 0x54
62 | case keypad3 = 0x55
63 | case keypad4 = 0x56
64 | case keypad5 = 0x57
65 | case keypad6 = 0x58
66 | case keypad7 = 0x59
67 | case keypad8 = 0x5b
68 | case keypad9 = 0x5c
69 | case backspace = 0x33
70 | case enter = 0x24
71 |
72 | // Independent of keyboard layout
73 | case tab = 0x30
74 | case space = 0x31
75 | case escape = 0x35
76 | case command = 0x37
77 | case shift = 0x38
78 | case capsLock = 0x39
79 | case option = 0x3a
80 | case control = 0x3b
81 | case rightShift = 0x3c
82 | case rightOption = 0x3d
83 | case rightControl = 0x3e
84 | case function = 0x3f
85 | case f17 = 0x40
86 | case volumeUp = 0x48
87 | case volumeDown = 0x49
88 | case mute = 0x4a
89 | case f18 = 0x4f
90 | case f19 = 0x50
91 | case f20 = 0x5a
92 | case f5 = 0x60
93 | case f6 = 0x61
94 | case f7 = 0x62
95 | case f3 = 0x63
96 | case f8 = 0x64
97 | case f9 = 0x65
98 | case f11 = 0x67
99 | case f13 = 0x69
100 | case f16 = 0x6a
101 | case f14 = 0x6b
102 | case f10 = 0x6d
103 | case f12 = 0x6f
104 | case f15 = 0x71
105 | case help = 0x72
106 | case home = 0x73
107 | case pageUp = 0x74
108 | case forwardDelete = 0x75
109 | case f4 = 0x76
110 | case end = 0x77
111 | case f2 = 0x78
112 | case pageDown = 0x79
113 | case f1 = 0x7a
114 | case leftArrow = 0x7b
115 | case rightArrow = 0x7c
116 | case downArrow = 0x7d
117 | case upArrow = 0x7e
118 |
119 | // ISO keyboard
120 | case isoSection = 0x0a
121 |
122 | // JIS keyboard
123 | case jisYen = 0x5d
124 | // case jisXXX = 0x5e
125 | case jisKeypadComma = 0x5f
126 | case jisEisu = 0x66
127 | case jisKana = 0x68
128 | }
129 |
130 | struct KeyEvent {
131 | let code: KeyCode
132 | let shift: Bool
133 | let control: Bool
134 | let option: Bool
135 | let command: Bool
136 |
137 | let isDown: Bool
138 | let isARepeat: Bool
139 |
140 | init?(nsEvent: NSEvent) {
141 | guard let code = KeyCode(rawValue: nsEvent.keyCode) else {
142 | return nil
143 | }
144 |
145 | self.code = code
146 | shift = nsEvent.modifierFlags.contains(.shift)
147 | control = nsEvent.modifierFlags.contains(.control)
148 | option = nsEvent.modifierFlags.contains(.option)
149 | command = nsEvent.modifierFlags.contains(.command)
150 |
151 | isDown = (nsEvent.type == .keyDown)
152 | isARepeat = nsEvent.isARepeat
153 | }
154 |
155 | func match(
156 | code: KeyCode? = nil,
157 | shift: Bool? = false,
158 | control: Bool? = false,
159 | option: Bool? = false,
160 | command: Bool? = false
161 | ) -> Bool {
162 | if let code = code, self.code != code {
163 | return false
164 | }
165 | if let shift = shift, self.shift != shift {
166 | return false
167 | }
168 | if let control = control, self.control != control {
169 | return false
170 | }
171 | if let option = option, self.option != option {
172 | return false
173 | }
174 | if let command = command, self.command != command {
175 | return false
176 | }
177 | return true
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/keyboard/Core/RepeatedKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | final class RepeatedKey {
4 | private let threshold: Double = 300 * 1e6
5 |
6 | private var state: (count: Int, timestamp: Double)?
7 |
8 | func count() -> Int {
9 | guard let state = state else {
10 | return 0
11 | }
12 |
13 | guard now() - state.timestamp < threshold else {
14 | self.state = nil
15 | return 0
16 | }
17 |
18 | return state.count
19 | }
20 |
21 | func record() -> Int {
22 | let n = count() + 1
23 | self.state = (count: n, timestamp: now())
24 | return n
25 | }
26 |
27 | func reset() {
28 | state = nil
29 | }
30 |
31 | func match(at exactCount: Int) -> Bool {
32 | if record() == exactCount {
33 | reset()
34 | return true
35 | }
36 | return false
37 | }
38 |
39 | private func now() -> Double {
40 | return DispatchTime.uptimeNanoseconds()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/keyboard/Core/SuperKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | final class SuperKey {
4 | struct Const {
5 | static let downThresholdMs: Double = 50
6 | static let dispatchDelayMs: Int = 150
7 | }
8 |
9 | enum State {
10 | case inactive
11 | case activated
12 | case enabled
13 | case used
14 | case disabled
15 | }
16 |
17 | private var activatedAt: Double = 0
18 | private var current: (key: KeyCode, time: DispatchTime, work: DispatchWorkItem?)?
19 |
20 | private(set) var prefixKey: KeyCode?
21 | private var pressedKeys: Set = []
22 |
23 | private(set) var state: State = .inactive {
24 | didSet {
25 | guard state != oldValue else {
26 | return
27 | }
28 |
29 | if state == .activated {
30 | activatedAt = DispatchTime.uptimeNanoseconds()
31 | }
32 | }
33 | }
34 |
35 | var isEnabled: Bool {
36 | return [.enabled, .used].contains(state)
37 | }
38 |
39 | func activate(prefixKey: KeyCode) -> Bool {
40 | guard state == .inactive else { return false }
41 |
42 | self.prefixKey = prefixKey
43 | state = .activated
44 | return true
45 | }
46 |
47 | func inactivate() {
48 | state = .inactive
49 | }
50 |
51 | func enable() -> Bool {
52 | guard state == .activated else {
53 | return true
54 | }
55 | guard DispatchTime.uptimeNanoseconds() - activatedAt > Const.downThresholdMs * 1e6 else {
56 | state = .disabled
57 | return false
58 | }
59 | state = .enabled
60 | return true
61 | }
62 |
63 | func perform(key: KeyCode, isKeyDown: Bool, block: @escaping (Set) -> Void) {
64 | guard isKeyDown else {
65 | pressedKeys.remove(key)
66 | return
67 | }
68 | pressedKeys.insert(key)
69 | let keys = pressedKeys
70 |
71 | guard state != .used else {
72 | current = (key: key, time: DispatchTime.now(), work: nil)
73 | block(keys)
74 | return
75 | }
76 | state = .used
77 |
78 | let work = DispatchWorkItem() {
79 | block(keys)
80 | }
81 | let dispatchTime = DispatchTime.now() + DispatchTimeInterval.milliseconds(Const.dispatchDelayMs)
82 | current = (key: key, time: dispatchTime, work: work)
83 | DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: work)
84 | }
85 |
86 | func cancel() -> KeyCode? {
87 | guard let current = current else { return nil }
88 | self.current = nil
89 |
90 | prefixKey = nil
91 | pressedKeys = []
92 |
93 | guard current.time > DispatchTime.now() else { return nil }
94 | current.work?.cancel()
95 |
96 | return current.key
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/keyboard/Extensions/AXAttribute.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | struct AXAttribute {
4 | let key: String
5 |
6 | init(_ key: String) {
7 | self.key = key
8 | }
9 | }
10 |
11 | struct AXAttributes {
12 | static let frame = AXAttribute("AXFrame")
13 | static let position = AXAttribute(kAXPositionAttribute)
14 | static let size = AXAttribute(kAXSizeAttribute)
15 | static let windows = AXAttribute<[AXUIElement]>(kAXWindowsAttribute)
16 | static let focusedWindow = AXAttribute(kAXFocusedWindowAttribute)
17 | }
18 |
--------------------------------------------------------------------------------
/keyboard/Extensions/AXError.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension AXError: Swift.Error {}
4 |
5 | extension AXError: CustomStringConvertible {
6 | private var valueAsString: String {
7 | switch self {
8 | case .success:
9 | return "success"
10 | case .failure:
11 | return "failure"
12 | case .illegalArgument:
13 | return "illegalArgument"
14 | case .invalidUIElement:
15 | return "invalidUIElement"
16 | case .invalidUIElementObserver:
17 | return "invalidUIElementObserver"
18 | case .cannotComplete:
19 | return "cannotComplete"
20 | case .attributeUnsupported:
21 | return "attributeUnsupported"
22 | case .actionUnsupported:
23 | return "actionUnsupported"
24 | case .notificationUnsupported:
25 | return "notificationUnsupported"
26 | case .notImplemented:
27 | return "notImplemented"
28 | case .notificationAlreadyRegistered:
29 | return "notificationAlreadyRegistered"
30 | case .notificationNotRegistered:
31 | return "notificationNotRegistered"
32 | case .apiDisabled:
33 | return "apiDisabled"
34 | case .noValue:
35 | return "noValue"
36 | case .parameterizedAttributeUnsupported:
37 | return "parameterizedAttributeUnsupported"
38 | case .notEnoughPrecision:
39 | return "notEnoughPrecision"
40 | }
41 | }
42 |
43 | public var description: String {
44 | return "AXError.\(valueAsString)"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/keyboard/Extensions/AXUIElement.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension AXUIElement {
4 | func getAttribute(_ attribute: AXAttribute) throws -> T? {
5 | return try getAttribute(attribute.key)
6 | }
7 |
8 | func getAttribute(_ attribute: String) throws -> T? {
9 | var value: AnyObject?
10 | let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &value)
11 |
12 | if error == .noValue || error == .attributeUnsupported {
13 | return nil
14 | }
15 | guard error == .success else {
16 | throw error
17 | }
18 |
19 | return unpackAXValue(value!) as? T
20 | }
21 |
22 | func setAttribute(_ attribute: AXAttribute, value: T) throws {
23 | try setAttribute(attribute.key, value: value)
24 | }
25 |
26 | func setAttribute(_ attribute: String, value: Any) throws {
27 | let error = AXUIElementSetAttributeValue(self, attribute as CFString, packAXValue(value))
28 |
29 | guard error == .success else {
30 | throw error
31 | }
32 | }
33 |
34 | private func unpackAXValue(_ value: AnyObject) -> Any {
35 | switch CFGetTypeID(value) {
36 | case AXUIElementGetTypeID():
37 | return value
38 | case AXValueGetTypeID():
39 | let type = AXValueGetType(value as! AXValue)
40 | switch type {
41 | case .axError:
42 | var result: AXError = .success
43 | let success = AXValueGetValue(value as! AXValue, type, &result)
44 | assert(success)
45 | return result
46 | case .cfRange:
47 | var result: CFRange = CFRange()
48 | let success = AXValueGetValue(value as! AXValue, type, &result)
49 | assert(success)
50 | return result
51 | case .cgPoint:
52 | var result: CGPoint = CGPoint.zero
53 | let success = AXValueGetValue(value as! AXValue, type, &result)
54 | assert(success)
55 | return result
56 | case .cgRect:
57 | var result: CGRect = CGRect.zero
58 | let success = AXValueGetValue(value as! AXValue, type, &result)
59 | assert(success)
60 | return result
61 | case .cgSize:
62 | var result: CGSize = CGSize.zero
63 | let success = AXValueGetValue(value as! AXValue, type, &result)
64 | assert(success)
65 | return result
66 | case .illegal:
67 | return value
68 | }
69 | default:
70 | return value
71 | }
72 | }
73 |
74 | private func packAXValue(_ value: Any) -> AnyObject {
75 | switch value {
76 | case var val as CFRange:
77 | return AXValueCreate(AXValueType(rawValue: kAXValueCFRangeType)!, &val)!
78 | case var val as CGPoint:
79 | return AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &val)!
80 | case var val as CGRect:
81 | return AXValueCreate(AXValueType(rawValue: kAXValueCGRectType)!, &val)!
82 | case var val as CGSize:
83 | return AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &val)!
84 | default:
85 | return value as AnyObject
86 | }
87 | }
88 | }
89 |
90 | extension NSRunningApplication {
91 | func axUIElement() -> AXUIElement? {
92 | if isTerminated {
93 | return nil
94 | }
95 | if processIdentifier < 0 {
96 | return nil
97 | }
98 | return AXUIElementCreateApplication(processIdentifier)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/keyboard/Extensions/DispatchTime.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension DispatchTime {
4 | static func uptimeNanoseconds() -> Double {
5 | return Double(now().uptimeNanoseconds)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/keyboard/Extensions/NSScreen.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSScreen {
4 | static var currentScreen: NSScreen? {
5 | let mouseLocation = NSEvent.mouseLocation
6 | return NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) })
7 | }
8 |
9 | static var currentScreenRect: CGRect? {
10 | guard let mainScreen = NSScreen.main else { return nil }
11 | guard let currentScreen = currentScreen else { return nil }
12 |
13 | // Convert the coordinate system
14 | var rect = currentScreen.frame
15 | rect.origin.y = (mainScreen.frame.origin.y + mainScreen.frame.size.height) - (currentScreen.frame.origin.y + currentScreen.frame.size.height)
16 |
17 | return rect
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/keyboard/Extensions/TISInputSource.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import InputMethodKit
3 |
4 | extension TISInputSource {
5 | private func getProperty(_ key: CFString) -> AnyObject? {
6 | guard let cfType = TISGetInputSourceProperty(self, key) else { return nil }
7 | return Unmanaged.fromOpaque(cfType).takeUnretainedValue()
8 | }
9 |
10 | var id: String {
11 | return getProperty(kTISPropertyInputSourceID) as! String
12 | }
13 |
14 | var category: String {
15 | return getProperty(kTISPropertyInputSourceCategory) as! String
16 | }
17 |
18 | var isKeyboardInputSource: Bool {
19 | return category == (kTISCategoryKeyboardInputSource as String)
20 | }
21 |
22 | var isSelectable: Bool {
23 | return getProperty(kTISPropertyInputSourceIsSelectCapable) as! Bool
24 | }
25 |
26 | var isSelected: Bool {
27 | return getProperty(kTISPropertyInputSourceIsSelected) as! Bool
28 | }
29 |
30 | var sourceLanguages: [String] {
31 | return getProperty(kTISPropertyInputSourceLanguages) as! [String]
32 | }
33 |
34 | var isCJKV: Bool {
35 | if let lang = sourceLanguages.first {
36 | return ["ko", "ja", "vi"].contains(lang) || lang.hasPrefix("zh")
37 | }
38 | return false
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/keyboard/Handlers/AppQuitHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | final class AppQuithHandler: Handler {
4 | private let repeatedKey = RepeatedKey()
5 |
6 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
7 | guard keyEvent.isDown else {
8 | return nil
9 | }
10 | guard keyEvent.match(code: .q, command: true) else {
11 | return nil
12 | }
13 |
14 | if repeatedKey.match(at: 2) {
15 | return .passThrough
16 | }
17 |
18 | return .prevent
19 | }
20 |
21 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
22 | return false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/keyboard/Handlers/AppSwitchHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | protocol ApplicationLaunchable {
4 | var workspace: NSWorkspace { get }
5 |
6 | func showOrHideApplication(_ id: String)
7 | }
8 |
9 | extension ApplicationLaunchable {
10 | func showOrHideApplication(_ id: String) {
11 | if let app = workspace.runningApplications.first(where: { $0.bundleIdentifier == id }) {
12 | if app.isActive {
13 | app.hide()
14 | } else {
15 | app.unhide()
16 | app.activate(options: [.activateIgnoringOtherApps])
17 | }
18 | return
19 | }
20 |
21 | workspace.launchApplication(
22 | withBundleIdentifier: id,
23 | options: [],
24 | additionalEventParamDescriptor: nil,
25 | launchIdentifier: nil
26 | )
27 | }
28 | }
29 |
30 | // Swtich between apps:
31 | //
32 | // ;+F Finder
33 | // ;+M Terminal
34 | // ;+T Things
35 | // ;+B Bear
36 | //
37 | final class AppSwitchHandler: Handler, ApplicationLaunchable {
38 | struct Const {
39 | static let superKey: KeyCode = .semicolon
40 | }
41 |
42 | let workspace: NSWorkspace
43 |
44 | init(workspace: NSWorkspace) {
45 | self.workspace = workspace
46 | }
47 |
48 | func activateSuperKeys() -> [KeyCode] {
49 | return [Const.superKey]
50 | }
51 |
52 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
53 | return nil
54 | }
55 |
56 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
57 | guard prefix == Const.superKey else { return false }
58 |
59 | switch keys {
60 | case [.f]:
61 | showOrHideApplication("com.apple.finder")
62 | return true
63 | case [.m]:
64 | showOrHideApplication("org.alacritty")
65 | return true
66 | case [.t]:
67 | showOrHideApplication("com.culturedcode.ThingsMac")
68 | return true
69 | case [.n]:
70 | showOrHideApplication("net.shinyfrog.bear")
71 | return true
72 | default:
73 | return false
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/keyboard/Handlers/EmacsHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | private let escapeKeyDisabledApps: Set = [
4 | "com.apple.Terminal",
5 | "net.sourceforge.iTerm",
6 | "com.googlecode.iterm2",
7 | "co.zeit.hyperterm",
8 | "co.zeit.hyper",
9 | "io.alacritty",
10 | "org.alacritty",
11 | "net.kovidgoyal.kitty",
12 | "com.ident.goneovim",
13 | "com.qvacua.VimR",
14 | ]
15 |
16 | private let advancedCursorKeysDisabledApps: Set = [
17 | "com.microsoft.VSCode",
18 | ]
19 |
20 | private let allCursorKeysDisabledApps: Set = [
21 | // eclipse
22 | "org.eclipse.eclipse",
23 | "org.eclipse.platform.ide",
24 | "org.eclipse.sdk.ide",
25 | "com.springsource.sts",
26 | "org.springsource.sts.ide",
27 |
28 | // emacs
29 | "org.gnu.Emacs",
30 | "org.gnu.AquamacsEmacs",
31 | "org.gnu.Aquamacs",
32 | "org.pqrs.unknownapp.conkeror",
33 |
34 | // remote desktop connection
35 | "com.microsoft.rdc",
36 | "com.microsoft.rdc.mac",
37 | "com.microsoft.rdc.osx.beta",
38 | "net.sf.cord",
39 | "com.thinomenon.RemoteDesktopConnection",
40 | "com.itap-mobile.qmote",
41 | "com.nulana.remotixmac",
42 | "com.p5sys.jump.mac.viewer",
43 | "com.p5sys.jump.mac.viewer.web",
44 | "com.vmware.horizon",
45 | "com.2X.Client.Mac",
46 | "karabiner.remotedesktop.microsoft",
47 | "karabiner.remotedesktop",
48 |
49 | // TERMINAL
50 | "com.apple.Terminal",
51 | "iTerm",
52 | "net.sourceforge.iTerm",
53 | "com.googlecode.iterm2",
54 | "co.zeit.hyperterm",
55 | "co.zeit.hyper",
56 | "io.alacritty",
57 | "org.alacritty",
58 | "net.kovidgoyal.kitty",
59 |
60 | // vi
61 | "org.vim.MacVim",
62 | "com.ident.goneovim",
63 | "com.qvacua.VimR",
64 |
65 | // virtualmachine
66 | "com.vmware.fusion",
67 | "com.vmware.horizon",
68 | "com.vmware.view",
69 | "com.parallels.desktop",
70 | "com.parallels.vm",
71 | "com.parallels.desktop.console",
72 | "org.virtualbox.app.VirtualBoxVM",
73 |
74 | // x11
75 | "org.x.X11",
76 | "com.apple.x11",
77 | "org.macosforge.xquartz.X11",
78 | "org.macports.X11",
79 | ]
80 |
81 | // Emacs mode:
82 | //
83 | // Ctrl-C Escape
84 | // Ctrl-D Forward delete Advanced
85 | // Ctrl-H Backspace Advanced
86 | // Ctrl-J Enter
87 | // Ctrl-P ↑
88 | // Ctrl-N ↓
89 | // Ctrl-B ←
90 | // Ctrl-F →
91 | // Ctrl-A Beginning of line (Shift allowed) Advanced
92 | // Ctrl-E End of line (Shift allowed) Advanced
93 | //
94 | final class EmacsHandler: Handler {
95 | private let workspace: NSWorkspace
96 | private let emitter: EmitterType
97 |
98 | init(workspace: NSWorkspace, emitter: EmitterType) {
99 | self.workspace = workspace
100 | self.emitter = emitter
101 | }
102 |
103 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
104 | guard let bundleId = workspace.frontmostApplication?.bundleIdentifier else {
105 | return nil
106 | }
107 |
108 | let escapeKeyEnabled = !escapeKeyDisabledApps.contains(bundleId)
109 | let cursorKeysEnabled = !allCursorKeysDisabledApps.contains(bundleId)
110 | let advancedCursorKeysEnabled = !advancedCursorKeysDisabledApps.contains(bundleId)
111 |
112 | if escapeKeyEnabled {
113 | if keyEvent.match(code: .c, control: true) {
114 | if keyEvent.isDown {
115 | emitter.emit(keyCode: .jisEisu, flags: [], action: .both)
116 | }
117 | emitter.emit(keyCode: .escape, flags: [], action: (keyEvent.isDown ? .down : .up))
118 | return .prevent
119 | }
120 | }
121 |
122 | if cursorKeysEnabled {
123 | var remap: (KeyCode, CGEventFlags)? = nil
124 |
125 | if keyEvent.match(control: true) {
126 | switch keyEvent.code {
127 | case .d:
128 | if advancedCursorKeysEnabled {
129 | remap = (.forwardDelete, [])
130 | }
131 | case .h:
132 | if advancedCursorKeysEnabled {
133 | remap = (.backspace, [])
134 | }
135 | case .j:
136 | remap = (.enter, [])
137 | default:
138 | break
139 | }
140 | }
141 | if keyEvent.match(shift: nil, control: true) {
142 | switch keyEvent.code {
143 | case .p:
144 | remap = (.upArrow, [])
145 | case .n:
146 | remap = (.downArrow, [])
147 | case .b:
148 | remap = (.leftArrow, [])
149 | case .f:
150 | remap = (.rightArrow, [])
151 | case .a:
152 | if advancedCursorKeysEnabled {
153 | remap = (.leftArrow, [.maskCommand])
154 | }
155 | case .e:
156 | if advancedCursorKeysEnabled {
157 | remap = (.rightArrow, [.maskCommand])
158 | }
159 | default:
160 | break
161 | }
162 | }
163 |
164 | if let remap = remap {
165 | let remapFlags = keyEvent.shift
166 | ? remap.1.union(.maskShift)
167 | : remap.1
168 |
169 | emitter.emit(keyCode: remap.0, flags: remapFlags, action: (keyEvent.isDown ? .down : .up))
170 | return .prevent
171 | }
172 | }
173 |
174 | return nil
175 | }
176 |
177 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
178 | return false
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/keyboard/Handlers/EscapeHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | // Switch to EISUU with Escape key
4 | final class EscapeHandler: Handler {
5 | private let emitter: EmitterType
6 |
7 | init(emitter: EmitterType) {
8 | self.emitter = emitter
9 | }
10 |
11 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
12 | guard keyEvent.isDown else {
13 | return nil
14 | }
15 | guard isEscapeKey(keyEvent: keyEvent) else {
16 | return nil
17 | }
18 |
19 | emitter.emit(keyCode: .jisEisu, flags: [], action: .both)
20 |
21 | return .passThrough
22 | }
23 |
24 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
25 | return false
26 | }
27 |
28 | private func isEscapeKey(keyEvent: KeyEvent) -> Bool {
29 | if keyEvent.match(code: .escape) {
30 | return true
31 | }
32 | if keyEvent.match(code: .c, shift: false, control: true, option: false, command: false) {
33 | return true
34 | }
35 | return false
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/keyboard/Handlers/InputSourceHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import InputMethodKit
3 |
4 | // Input source switching
5 | //
6 | // Ctrl-; Select next source in the input menu
7 | //
8 | final class InputSourceHandler: Handler {
9 | private let inputSources: [TISInputSource]
10 |
11 | init() {
12 | let inputSourceNSArray = TISCreateInputSourceList(nil, false).takeRetainedValue() as NSArray
13 | let inputSourceList = inputSourceNSArray as! [TISInputSource]
14 | self.inputSources = inputSourceList.filter { $0.isKeyboardInputSource && $0.isSelectable }
15 | }
16 |
17 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
18 | guard keyEvent.isDown else { return nil }
19 | guard !keyEvent.isARepeat else { return nil }
20 | guard keyEvent.match(code: .semicolon, control: true) else { return nil }
21 |
22 | changeSource()
23 | return .prevent
24 | }
25 |
26 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
27 | return false
28 | }
29 |
30 | private func changeSource() {
31 | guard let i = inputSources.firstIndex(where: { $0.isSelected }) else { return }
32 |
33 | let current = inputSources[i]
34 | let next = inputSources[(i + 1) % inputSources.count]
35 |
36 | if !current.isCJKV && next.isCJKV, let nonCJKV = inputSources.first(where: { !$0.isCJKV }) {
37 | // Workaround for TIS CJKV layout bug:
38 | // when it's CJKV, select nonCJKV input first and then return
39 | TISSelectInputSource(nonCJKV)
40 | }
41 |
42 | TISSelectInputSource(next)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/keyboard/Handlers/MouseHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | final class MouseHandler: Handler {
4 | struct Const {
5 | static let superKey: KeyCode = .c
6 | static let speedKey: KeyCode = .s
7 | static let scrollKey: KeyCode = .x
8 | static let pauseInterval: UInt32 = 1000
9 | }
10 |
11 | enum Movement {
12 | case translate(x: CGFloat, y: CGFloat)
13 | case translatePropotionally(rx: CGFloat, ry: CGFloat)
14 | case moveTo(x: CGFloat, y: CGFloat)
15 | case movePropotionallyTo(rx: CGFloat, ry: CGFloat)
16 | }
17 |
18 | private let emitter: EmitterType
19 | private let showHighlight: () -> Void
20 |
21 | init(emitter: EmitterType, showHighlight: @escaping () -> Void) {
22 | self.emitter = emitter
23 | self.showHighlight = showHighlight
24 | }
25 |
26 | func activateSuperKeys() -> [KeyCode] {
27 | return [Const.superKey]
28 | }
29 |
30 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
31 | return nil
32 | }
33 |
34 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
35 | guard prefix == Const.superKey else { return false }
36 |
37 | switch keys {
38 | case [.h]:
39 | moveCursor(.translate(x: -10, y: 0))
40 | return true
41 | case [.j]:
42 | moveCursor(.translate(x: 0, y: 10))
43 | return true
44 | case [.k]:
45 | moveCursor(.translate(x: 0, y: -10))
46 | return true
47 | case [.l]:
48 | moveCursor(.translate(x: 10, y: 0))
49 | return true
50 |
51 | case [.h, .j]:
52 | moveCursor(.translate(x: -10, y: 10))
53 | return true
54 | case [.j, .l]:
55 | moveCursor(.translate(x: 10, y: 10))
56 | return true
57 | case [.k, .l]:
58 | moveCursor(.translate(x: 10, y: -10))
59 | return true
60 | case [.h, .k]:
61 | moveCursor(.translate(x: -10, y: -10))
62 | return true
63 |
64 | case [Const.speedKey, .h]:
65 | moveCursor(.translatePropotionally(rx: -0.1, ry: 0))
66 | return true
67 | case [Const.speedKey, .j]:
68 | moveCursor(.translatePropotionally(rx: 0, ry: 0.1))
69 | return true
70 | case [Const.speedKey, .k]:
71 | moveCursor(.translatePropotionally(rx: 0, ry: -0.1))
72 | return true
73 | case [Const.speedKey, .l]:
74 | moveCursor(.translatePropotionally(rx: 0.1, ry: 0))
75 | return true
76 |
77 | case [Const.speedKey, .h, .j]:
78 | moveCursor(.translatePropotionally(rx: -0.1, ry: 0.1))
79 | return true
80 | case [Const.speedKey, .j, .l]:
81 | moveCursor(.translatePropotionally(rx: 0.1, ry: 0.1))
82 | return true
83 | case [Const.speedKey, .k, .l]:
84 | moveCursor(.translatePropotionally(rx: 0.1, ry: -0.1))
85 | return true
86 | case [Const.speedKey, .h, .k]:
87 | moveCursor(.translatePropotionally(rx: -0.1, ry: -0.1))
88 | return true
89 |
90 | case [.y]:
91 | moveCursor(.movePropotionallyTo(rx: 0.1, ry: 0.1), highlight: true)
92 | return true
93 | case [.u]:
94 | moveCursor(.movePropotionallyTo(rx: 0.1, ry: 0.9), highlight: true)
95 | return true
96 | case [.i]:
97 | moveCursor(.movePropotionallyTo(rx: 0.9, ry: 0.1), highlight: true)
98 | return true
99 | case [.o]:
100 | moveCursor(.movePropotionallyTo(rx: 0.9, ry: 0.9), highlight: true)
101 | return true
102 | case [.u, .i]:
103 | moveCursor(.movePropotionallyTo(rx: 0.5, ry: 0.5), highlight: true)
104 | return true
105 |
106 | case [Const.scrollKey, .h]:
107 | emitter.emit(mouseScroll: .init(x: 50, y: 0))
108 | return true
109 | case [Const.scrollKey, .j]:
110 | emitter.emit(mouseScroll: .init(x: 0, y: -50))
111 | return true
112 | case [Const.scrollKey, .k]:
113 | emitter.emit(mouseScroll: .init(x: 0, y: 50))
114 | return true
115 | case [Const.scrollKey, .l]:
116 | emitter.emit(mouseScroll: .init(x: -50, y: 0))
117 | return true
118 |
119 | case [.space]:
120 | showHighlight()
121 | return true
122 |
123 | case [.m]:
124 | emitter.emit(mouseClick: .left)
125 | return true
126 | case [.comma]:
127 | emitter.emit(mouseClick: .right)
128 | return true
129 | default:
130 | break
131 | }
132 |
133 | return false
134 | }
135 |
136 | func moveCursor(_ movement: Movement, highlight: Bool = false) {
137 | guard let screenRect = NSScreen.currentScreenRect else { return }
138 | guard let voidEvent = CGEvent(source: nil) else { return }
139 |
140 | var location = voidEvent.location
141 |
142 | switch movement {
143 | case let .translate(x, y):
144 | location.x += x
145 | location.y += y
146 | case let .translatePropotionally(rx, ry):
147 | location.x += screenRect.width * rx
148 | location.y += screenRect.height * ry
149 | case let .moveTo(x, y):
150 | location.x = x
151 | location.y = y
152 | case let .movePropotionallyTo(rx, ry):
153 | location.x = screenRect.minX + screenRect.width * rx
154 | location.y = screenRect.minY + screenRect.height * ry
155 | }
156 |
157 | // NOTE: -1 to workaround a problem of losing NSScreen.currentScreen.
158 | location.x = max(screenRect.minX, min(location.x, screenRect.maxX - 1))
159 | location.y = max(screenRect.minY, min(location.y, screenRect.maxY - 1))
160 |
161 | emitter.emit(mouseMoveTo: location)
162 |
163 | if highlight {
164 | DispatchQueue.main.async {
165 | self.showHighlight()
166 | }
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/keyboard/Handlers/NavigationHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | private let witch3BundleId = "com.manytricks.WitchWrapper"
4 | private let witch4PrefPanePath = "Library/PreferencePanes/Witch.prefPane"
5 |
6 | // Window/Space navigations:
7 | //
8 | // S+H Move to left space
9 | // S+L Move to right space
10 | // S+J Switch to next application
11 | // S+K Switch to previous application
12 | // S+N Switch to next window
13 | // S+B Switch to previous window
14 | // S+M Mission Control
15 | //
16 | final class NavigationHandler: Handler, ApplicationLaunchable {
17 | struct Const {
18 | static let superKey: KeyCode = .s
19 | }
20 |
21 | let workspace: NSWorkspace
22 | private let fileManager: FileManager
23 | private let emitter: EmitterType
24 |
25 | private var homeDirectory: URL {
26 | if #available(OSX 10.12, *) {
27 | return fileManager.homeDirectoryForCurrentUser
28 | } else {
29 | return URL(fileURLWithPath: NSHomeDirectory())
30 | }
31 | }
32 |
33 | private lazy var hasWitch3: Bool = {
34 | return workspace.absolutePathForApplication(withBundleIdentifier: witch3BundleId) != nil
35 | }()
36 | private lazy var hasWitch4: Bool = {
37 | let prefPath = homeDirectory.appendingPathComponent(witch4PrefPanePath, isDirectory: false).path
38 | return fileManager.fileExists(atPath: prefPath)
39 | }()
40 | private lazy var hasWitch: Bool = {
41 | return hasWitch4 || hasWitch3
42 | }()
43 |
44 | init(workspace: NSWorkspace, fileManager: FileManager, emitter: EmitterType) {
45 | self.workspace = workspace
46 | self.fileManager = fileManager
47 | self.emitter = emitter
48 | }
49 |
50 | func activateSuperKeys() -> [KeyCode] {
51 | return [Const.superKey]
52 | }
53 |
54 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
55 | return nil
56 | }
57 |
58 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
59 | guard prefix == Const.superKey else { return false }
60 |
61 | switch keys {
62 | case [.h]:
63 | emitter.emit(keyCode: .leftArrow, flags: [.maskControl, .maskSecondaryFn], action: .both)
64 | return true
65 | case [.j]:
66 | if hasWitch {
67 | emitter.emit(keyCode: .tab, flags: [.maskAlternate], action: .both)
68 | } else {
69 | emitter.emit(keyCode: .tab, flags: [.maskCommand], action: .both)
70 | }
71 | return true
72 | case [.k]:
73 | if hasWitch {
74 | emitter.emit(keyCode: .tab, flags: [.maskAlternate, .maskShift], action: .both)
75 | } else {
76 | emitter.emit(keyCode: .tab, flags: [.maskCommand, .maskShift], action: .both)
77 | }
78 | return true
79 | case [.l]:
80 | emitter.emit(keyCode: .rightArrow, flags: [.maskControl, .maskSecondaryFn], action: .both)
81 | return true
82 | case [.n]:
83 | if hasWitch {
84 | emitter.emit(keyCode: .tab, flags: [.maskControl, .maskAlternate], action: .both)
85 | } else {
86 | emitter.emit(keyCode: .f1, flags: [.maskCommand, .maskSecondaryFn], action: .both)
87 | }
88 | return true
89 | case [.b]:
90 | if hasWitch {
91 | emitter.emit(keyCode: .tab, flags: [.maskControl, .maskAlternate, .maskShift], action: .both)
92 | } else {
93 | emitter.emit(keyCode: .f1, flags: [.maskCommand, .maskShift, .maskSecondaryFn], action: .both)
94 | }
95 | return true
96 | case [.m]:
97 | showOrHideApplication("com.apple.exposelauncher")
98 | return true
99 | default:
100 | return false
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/keyboard/Handlers/WindowResizeHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | private enum WindowSize {
4 | case full
5 | case left
6 | case top
7 | case right
8 | case bottom
9 | case topLeft
10 | case topRight
11 | case bottomRight
12 | case bottomLeft
13 |
14 | func rect(screenFrame: CGRect) -> CGRect {
15 | var frame = screenFrame
16 |
17 | switch self {
18 | case .full:
19 | break
20 | case .left:
21 | frame.size.width = screenFrame.width / 2
22 | case .top:
23 | frame.size.height = screenFrame.height / 2
24 | case .right:
25 | frame.origin.x += screenFrame.width / 2
26 | frame.size.width = screenFrame.width / 2
27 | case .bottom:
28 | frame.origin.y += screenFrame.height / 2
29 | frame.size.height = screenFrame.height / 2
30 | case .topLeft:
31 | frame.size.height = screenFrame.height / 2
32 | frame.size.width = screenFrame.width / 2
33 | case .topRight:
34 | frame.origin.x += screenFrame.width / 2
35 | frame.size.height = screenFrame.height / 2
36 | frame.size.width = screenFrame.width / 2
37 | case .bottomLeft:
38 | frame.origin.y += screenFrame.height / 2
39 | frame.size.height = screenFrame.height / 2
40 | frame.size.width = screenFrame.width / 2
41 | case .bottomRight:
42 | frame.origin.y += screenFrame.height / 2
43 | frame.origin.x += screenFrame.width / 2
44 | frame.size.height = screenFrame.height / 2
45 | frame.size.width = screenFrame.width / 2
46 | }
47 |
48 | return frame
49 | }
50 | }
51 |
52 | // Window resizer:
53 | //
54 | // S+D+F Full
55 | // S+D+H Left
56 | // S+D+J Bottom
57 | // S+D+K Top
58 | // S+D+L Right
59 | //
60 | // Cmd-Alt-/ Full
61 | // Cmd-Alt-Left Left
62 | // Cmd-Alt-Up Top
63 | // Cmd-Alt-Right Right
64 | // Cmd-Alt-Down Bottom
65 | // Shift-Cmd-Alt-Left Top-left
66 | // Shift-Cmd-Alt-Up Top-right
67 | // Shift-Cmd-Alt-Right Bottom-right
68 | // Shift-Cmd-Alt-Down Bottom-left
69 | //
70 | final class WindowResizeHandler: Handler {
71 | struct Const {
72 | static let superKey: KeyCode = .s
73 | }
74 |
75 | private let workspace: NSWorkspace
76 |
77 | init(workspace: NSWorkspace) {
78 | self.workspace = workspace
79 | }
80 |
81 | func activateSuperKeys() -> [KeyCode] {
82 | return [Const.superKey]
83 | }
84 |
85 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
86 | // guard keyEvent.isDown else {
87 | // return nil
88 | // }
89 | // guard keyEvent.match(shift: nil, option: true, command: true) else {
90 | // return nil
91 | // }
92 | //
93 | // var windowSize: WindowSize?
94 | //
95 | // if keyEvent.shift {
96 | // switch keyEvent.code {
97 | // case .leftArrow: windowSize = .topLeft
98 | // case .upArrow: windowSize = .topRight
99 | // case .rightArrow: windowSize = .bottomRight
100 | // case .downArrow: windowSize = .bottomLeft
101 | // default: break
102 | // }
103 | // } else {
104 | // switch keyEvent.code {
105 | // case .slash: windowSize = .full
106 | // case .leftArrow: windowSize = .left
107 | // case .upArrow: windowSize = .top
108 | // case .rightArrow: windowSize = .right
109 | // case .downArrow: windowSize = .bottom
110 | // default: break
111 | // }
112 | // }
113 | //
114 | // if let windowSize = windowSize {
115 | // do {
116 | // try resizeWindow(windowSize: windowSize)
117 | // } catch {
118 | // print(error)
119 | // }
120 | //
121 | // return .prevent
122 | // }
123 |
124 | return nil
125 | }
126 |
127 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
128 | guard prefix == Const.superKey else { return false }
129 |
130 | var windowSize: WindowSize?
131 | switch keys {
132 | case [.d, .f]: windowSize = .full
133 | case [.d, .h]: windowSize = .left
134 | case [.d, .k]: windowSize = .top
135 | case [.d, .l]: windowSize = .right
136 | case [.d, .j]: windowSize = .bottom
137 | default: break
138 | }
139 |
140 | if let windowSize = windowSize {
141 | do {
142 | try resizeWindow(windowSize: windowSize)
143 | } catch {
144 | print(error)
145 | }
146 |
147 | return true
148 | }
149 |
150 | return false
151 | }
152 |
153 | private func resizeWindow(windowSize: WindowSize) throws {
154 | guard let app = workspace.frontmostApplication?.axUIElement() else { return }
155 | guard let window = try app.getAttribute(AXAttributes.focusedWindow) else { return }
156 | guard let frame = try window.getAttribute(AXAttributes.frame) else { return }
157 |
158 | guard let screen = (NSScreen.screens
159 | .map { screen in (screen, screen.frame.intersection(frame)) }
160 | .filter { _, intersect in !intersect.isNull }
161 | .map { screen, intersect in (screen, intersect.size.width * intersect.size.height) }
162 | .max { lhs, rhs in lhs.1 < rhs.1 }?
163 | .0
164 | ) else {
165 | return
166 | }
167 |
168 | let newFrame = windowSize.rect(screenFrame: screen.frame)
169 |
170 | try window.setAttribute(AXAttributes.position, value: newFrame.origin)
171 | try window.setAttribute(AXAttributes.size, value: newFrame.size)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/keyboard/Handlers/WordMotionHandler.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | // Word motions:
4 | //
5 | // A+D Delete word after cursor
6 | // A+H Delete word before cursor
7 | // A+B Move cursor backward by word
8 | // A+F Move cursor forward by word
9 | //
10 | final class WordMotionHandler: Handler {
11 | struct Const {
12 | static let superKey: KeyCode = .a
13 | }
14 |
15 | private let workspace: NSWorkspace
16 | private let emitter: EmitterType
17 |
18 | init(workspace: NSWorkspace, emitter: EmitterType) {
19 | self.workspace = workspace
20 | self.emitter = emitter
21 | }
22 |
23 | func activateSuperKeys() -> [KeyCode] {
24 | return [Const.superKey]
25 | }
26 |
27 | func handle(keyEvent: KeyEvent) -> HandlerAction? {
28 | return nil
29 | }
30 |
31 | func handleSuperKey(prefix: KeyCode, keys: Set) -> Bool {
32 | guard prefix == Const.superKey else { return false }
33 |
34 | switch keys {
35 | case [.d]:
36 | emitter.emit(keyCode: .forwardDelete, flags: .maskAlternate, action: .both)
37 | return true
38 | case [.h]:
39 | emitter.emit(keyCode: .backspace, flags: .maskAlternate, action: .both)
40 | return true
41 | case [.b]:
42 | emitter.emit(keyCode: .leftArrow, flags: .maskAlternate, action: .both)
43 | return true
44 | case [.f]:
45 | emitter.emit(keyCode: .rightArrow, flags: .maskAlternate, action: .both)
46 | return true
47 | default:
48 | break
49 | }
50 |
51 | return false
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/keyboard/HighlighterView.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class HighlighterView: NSView {
4 | struct Const {
5 | static let size: CGFloat = 32
6 | static let color = NSColor(calibratedRed: 0, green: 0.7, blue: 1.0, alpha: 0.7)
7 | }
8 |
9 | var location: CGPoint?
10 |
11 | override func draw(_ dirtyRect: NSRect) {
12 | guard let location = location else { return }
13 |
14 | let rect = NSMakeRect(
15 | location.x - Const.size / 2,
16 | location.y - Const.size / 2,
17 | Const.size,
18 | Const.size
19 | )
20 | let path = NSBezierPath(roundedRect: rect, xRadius: Const.size, yRadius: Const.size)
21 | Const.color.set()
22 | path.fill()
23 | path.appendRect(dirtyRect)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/keyboard/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 © 2017 Creasty. All rights reserved.
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 | LSUIElement
32 |
33 | CFBundleDisplayName
34 | ${PRODUCT_DISPLAY_NAME}
35 |
36 |
37 |
--------------------------------------------------------------------------------
/keyboard/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
672 |
673 |
674 |
--------------------------------------------------------------------------------