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