├── .gitignore ├── .prettierrc ├── Brewfile ├── Brewfile.lock.json ├── LICENSE ├── README.md ├── hammerspoon ├── delete-words.lua ├── double_cmdq_to_quit.lua ├── hyper-apps-defaults.lua ├── hyper-apps.lua ├── hyper.lua ├── init.lua ├── markdown.lua ├── panes.lua ├── status-message.lua ├── windows-bindings-defaults.lua ├── windows-bindings.lua └── windows.lua ├── inputrc ├── karabiner └── karabiner.json ├── screenshots ├── accessibility-permissions-for-hammerspoon.png ├── karabiner-elements-system-extension-prompt-1.png └── karabiner-elements-system-extension-prompt-2.png └── script └── setup /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | karabiner/automatic_backups 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "jsxSingleQuote": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | cask 'karabiner-elements' 2 | cask 'hammerspoon' 3 | -------------------------------------------------------------------------------- /Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "cask": { 4 | "karabiner-elements": { 5 | "version": "14.11.0", 6 | "options": { 7 | "full_name": "karabiner-elements" 8 | } 9 | }, 10 | "hammerspoon": { 11 | "version": "0.9.97", 12 | "options": { 13 | "full_name": "hammerspoon" 14 | } 15 | } 16 | } 17 | }, 18 | "system": { 19 | "macos": { 20 | "catalina": { 21 | "HOMEBREW_VERSION": "2.5.2", 22 | "HOMEBREW_PREFIX": "/usr/local", 23 | "Homebrew/homebrew-core": "bac6904fef5f44b25d9bcdb3f0239db8548d8f09", 24 | "CLT": "12.0.0.0.1.1599194153", 25 | "Xcode": "12.0", 26 | "macOS": "10.15.7" 27 | }, 28 | "monterey": { 29 | "HOMEBREW_VERSION": "3.6.21", 30 | "HOMEBREW_PREFIX": "/opt/homebrew", 31 | "Homebrew/homebrew-core": "0c46c555dcb43e7ae3147392a71ebc2c1f5e0a26", 32 | "CLT": "14.2.0.0.1.1668646533", 33 | "Xcode": "14.1", 34 | "macOS": "12.6.3" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Jason Rudolph (http://jasonrudolph.com) 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 NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My custom settings for Hammerspoon and Karabiner (forked from [jasonrudolph](https://github.com/jasonrudolph/keyboard)) 2 | 3 | ### Toward a more useful keyboard 4 | 5 | Steve Losh's [Modern Space Cadet][modern-space-cadet] is an inspiration. 6 | It opened my eyes to the fact that there's a more useful keyboard hidden inside the vanilla QWERTY package that most of us have tolerated for all these years. 7 | This repo represents my nascent quest to unleash that more useful keyboard. 8 | 9 | At first, this might sound no different than the typical Emacs/Vim/\ tweakfest. 10 | But it is. 11 | I'm not talking about honing my editor-of-choice. 12 | I'm not talking about pimping out my shell. 13 | I want a more useful keyboard _everywhere_. 14 | Whether I'm in my editor, in the terminal, in the browser, or in Keynote, 15 | I want a more useful keyboard. 16 | 17 | And ideally, I want the _same_ (more useful) keyboard in every app. 18 | Ubiquitous keyboarding. 19 | Muscle memory. 20 | [Don't make me think][don't-make-me-think]. 21 | 22 | How do I go to the beginning of the line in this app? 23 | The same way I go to the beginning of the line in _every_ app! 24 | Don't make me think. 25 | 26 | How do I go to the top of the file/screen/page in this app? 27 | The same way I... 28 | Well, you get the point. 29 | 30 | ### More useful (for me) 31 | 32 | > **cus·tom·ize** (_verb_): to modify or build according to individual or personal specifications or preference [[dictionary.com][customize]] 33 | 34 | Any customization is, by definition, personal. 35 | While I find that these customizations yield a more-useful keyboard for me, they might not feel like a win for you. 36 | 37 | ### Features 38 | 39 | - [Access control and escape on the home row](#a-more-useful-caps-lock-key) 40 | - [Navigate (up/down/left/right) via the home row](#super-duper-mode) 41 | - [Navigate to previous/next word via the home row](#super-duper-mode) 42 | - [Arrange windows via the home row](#window-layout-mode) 43 | - [Enable other commonly-used actions on or near the home row](#miscellaneous-goodness) 44 | - [Format text as Markdown](#markdown-mode) 45 | - [Launch commonly-used apps via global keyboard shortcuts](#hyper-key-for-quickly-launching-apps) 46 | - [And more...](#miscellaneous-goodness) 47 | 48 | ### A more useful caps lock key 49 | 50 | By repurposing the anachronistic caps lock key, we can make control and escape accessible via the home row. 51 | 52 | - Tap caps lock for escape 53 | - Hold caps lock for control 54 | 55 | Left ctrl key also functions the same way! ;) 56 | 57 | ### (S)uper (D)uper Mode 58 | 59 | To activate, push the s and d keys simultaneously and hold them down. Now you're in (S)uper (D)uper Mode. It's like a secret keyboard _inside_ your keyboard. (Whoa.) It's optimized for keeping you on the home row, or very close to it. Now you can: 60 | 61 | - Use h / j / k / l for **left**/**down**/**up**/**right** respectively 62 | - Use a for option (AKA alt) 63 | - Use f for command 64 | - Use space for shift 65 | - Use a + j / k for page down / page up 66 | - Use i / o to move to the previous/next tab 67 | - Use u / p to go to the first/last tab (in most apps) 68 | - Use a + h / l to move to previous/next word (in most apps) 69 | 70 | [(S)uper (D)uper Mode Keybindings](https://cloud.githubusercontent.com/assets/2988/22397420/f2b3e346-e53e-11e6-97bb-9db71f86994b.png) 71 | 72 | 📣 Shout-out to [Karabiner's Simultaneous vi Mode](https://github.com/tekezo/Karabiner/blob/05ca98733f3e3501e0679814c3795d1cb57e177f/src/core/server/Resources/include/checkbox/simultaneouskeypresses_vi_mode.xml#L4-L10) for providing the inspiration for (S)uper (D)uper Mode. ⌨:neckbeard:✨ 73 | 74 | ### Window Layout Mode 75 | 76 | Quickly arrange and resize windows in common configurations, using keyboard shortcuts that are on or near the home row. (Or, feel free to [choose your own keyboard shortcuts](#choose-your-own-keybindings).) 77 | 78 | #### Default keybindings 79 | 80 | Use control + s to turn on Window Layout Mode. Then, use any shortcut below to make windows do your bidding. For example, to send the window left, hit control + s, and then hit h. 81 | 82 | - Use h to send window left (left half of screen) 83 | - Use j to send window down (bottom half of screen) 84 | - Use k to send window up (top half of screen) 85 | - Use l to send window right (right half of screen) 86 | - Use shift + h to send window to left 40% of screen 87 | - Use shift + l to send window to right 60% of screen 88 | - Use i to send window to upper left quarter of screen 89 | - Use o to send window to upper right quarter of screen 90 | - Use , to send window to lower left quarter of screen 91 | - Use . to send window to lower right quarter of screen 92 | - Use space to send window to center of screen with full height 93 | - Use shift + space to send window to center of screen but not full height 94 | - Use enter to resize window to fill the screen 95 | - Use n to send window to the next monitor 96 | - Use to send window to the monitor on the left (if there is one) 97 | - Use to send window to the monitor on the right (if there is one) 98 | - Use control + s to exit Window Layout Mode without moving any windows 99 | 100 | [Window Layout Mode Keybindings (1)](https://cloud.githubusercontent.com/assets/2988/22397114/715cc12e-e538-11e6-9dcd-b3447af0d9dd.png) [Window Layout Mode Keybindings (2)](https://cloud.githubusercontent.com/assets/2988/22397111/45672fe6-e538-11e6-905d-5b0234e290bb.png) 101 | 102 | #### Choose your own keybindings 103 | 104 | Window Layout Mode ships with the default keybindings above, but you're welcome to personalize this setup. See [`hammerspoon/windows-bindings-defaults.lua`](hammerspoon/windows-bindings-defaults.lua) for instructions on configuring shortcuts to your personal taste. 105 | 106 | ### Markdown Mode 107 | 108 | Perform common [Markdown](https://daringfireball.net/projects/markdown/syntax)-formatting tasks anywhere that you're editing text (e.g., in a GitHub comment, in your editor, in your email client). 109 | 110 | Use control + m to turn on Markdown Mode. Then, use any shortcut below to perform an action. For example, to format the selected text as bold in Markdown, hit control + m, and then b. 111 | 112 | - Use b to wrap the currently-selected text in double asterisks ("B" for "Bold") 113 | 114 | Example: `**selection**` 115 | 116 | - Use c to wrap the currently-selected text in backticks ("C" for "Code") 117 | 118 | Example: `` `selection` `` 119 | 120 | - Use i to wrap the currently-selected text in single asterisks ("I" for "Italic") 121 | 122 | Example: `*selection*` 123 | 124 | - Use s to wrap the currently-selected text in double tildes ("S" for "Strikethrough") 125 | 126 | Example: `~~selection~~` 127 | 128 | - Use l to convert the currently-selected text to an inline link, using a URL from the clipboard ("L" for "Link") 129 | 130 | Example: `[selection](clipboard)` 131 | 132 | - Use control + m to exit Markdown Mode without performing any actions 133 | 134 | ### Hyper key for quickly launching apps 135 | 136 | macOS doesn't have a native hyper key. But thanks to Karabiner-Elements, we can [create our own](karabiner/karabiner.json). In this setup, we'll use the right option key as our hyper key. 137 | 138 | With a new modifier key defined, we open a whole world of possibilities. I find it especially useful for providing global shortcuts for launching apps. 139 | 140 | #### Choose your own apps 141 | 142 | Hyper Mode ships with the default keybindings below, but you'll likely want to personalize this setup. See [`hammerspoon/hyper-apps-defaults.lua`](hammerspoon/hyper-apps-defaults.lua) for instructions on configuring shortcuts to launch *your* most commonly-used apps. 143 | 144 | #### Default app keybindings 145 | 146 | - hyper + a to open iTunes ("A" for "Apple Music") 147 | - hyper + b to open Google Chrome ("B" for "Browser") 148 | - hyper + c to open Slack ("C for "Chat") 149 | - hyper + d to open [Remember The Milk](https://www.rememberthemilk.com/) ("D" for "Do!" ... or "Done!") 150 | - hyper + e to open [Atom](https://atom.io) ("E" for "Editor") 151 | - hyper + f to open Finder ("F" for "Finder") 152 | - hyper + g to open [Mailplane](http://mailplaneapp.com/) ("G" for "Gmail") 153 | - hyper + s to open [Slack](https://slack.com/downloads/osx) ("S" for "Slack") 154 | - hyper + t to open [iTerm2](https://www.iterm2.com/) ("T" for "Terminal") 155 | 156 | ### Miscellaneous goodness 157 | 158 | - Use control + - (dash) to split iTerm2 panes horizontally 159 | - Use control + | (pipe) split iTerm2 panes vertically 160 | - Use control + h / j / k / l to move left/down/up/right by one pane in iTerm2 161 | - Use control + u to delete to the start of the line 162 | - Use control + ; to delete to the end of the line 163 | - Use option + h / l to delete the previous/next word 164 | 165 | ## Dependencies 166 | 167 | This setup is honed and tested with the following dependencies. 168 | 169 | - macOS High Sierra, 10.13 170 | - [Karabiner-Elements 12.2.0][karabiner] 171 | - [Hammerspoon 0.9.73][hammerspoon] 172 | 173 | ## Installation 174 | 175 | 1. Grab the bits 176 | 177 | ```sh 178 | git clone https://github.com/jasonrudolph/keyboard.git ~/.keyboard 179 | 180 | cd ~/.keyboard 181 | 182 | script/setup 183 | ``` 184 | 185 | 2. Enable accessibility to allow Hammerspoon to do its thing [[screenshot]](screenshots/accessibility-permissions-for-hammerspoon.png) 186 | 187 | 3. On macOS High Sierra or later, you'll be [prompted to allow Karabiner-Elements to load its kernel extension](https://pqrs.org/osx/karabiner/document.html#usage). Follow the prompts to upgrade your life: 188 | 1. Click "Open System Preferences" [[screenshot]](https://github.com/jasonrudolph/keyboard/blob/v5.0.0/screenshots/karabiner-elements-system-extension-prompt-1.png) 189 | 1. Click "Allow" [[screenshot]](https://github.com/jasonrudolph/keyboard/blob/v5.0.0/screenshots/karabiner-elements-system-extension-prompt-2.png) 190 | 191 | [customize]: http://dictionary.reference.com/browse/customize 192 | [don't-make-me-think]: http://en.wikipedia.org/wiki/Don't_Make_Me_Think 193 | [karabiner]: https://github.com/tekezo/Karabiner-Elements 194 | [hammerspoon]: http://www.hammerspoon.org 195 | [hammerspoon-releases]: https://github.com/Hammerspoon/hammerspoon/releases 196 | [modern-space-cadet]: http://stevelosh.com/blog/2012/10/a-modern-space-cadet 197 | [modern-space-cadet-key-repeat]: http://stevelosh.com/blog/2012/10/a-modern-space-cadet/#controlescape 198 | -------------------------------------------------------------------------------- /hammerspoon/delete-words.lua: -------------------------------------------------------------------------------- 1 | local log = hs.logger.new('delete-words.lua', 'debug') 2 | 3 | local isInTerminal = function() 4 | app = hs.application.frontmostApplication():name() 5 | return app == 'iTerm2' or app == 'Terminal' 6 | end 7 | 8 | -- Use option + h to delete previous word 9 | hs.hotkey.bind({'alt'}, 'h', function() 10 | if isInTerminal() then 11 | keyUpDown({'ctrl'}, 'w') 12 | else 13 | keyUpDown({'alt'}, 'delete') 14 | end 15 | end) 16 | 17 | -- Use option + l to delete next word 18 | hs.hotkey.bind({'alt'}, 'l', function() 19 | if isInTerminal() then 20 | keyUpDown({}, 'escape') 21 | keyUpDown({}, 'd') 22 | else 23 | keyUpDown({'alt'}, 'forwarddelete') 24 | end 25 | end) 26 | 27 | -- Use control + u to delete to beginning of line 28 | -- 29 | -- In bash, control + u automatically deletes to the beginning of the line, so 30 | -- we don't need (or want) this hotkey in the terminal. If this hotkey was 31 | -- enabled in the terminal, it would break the standard control + u behavior. 32 | -- Therefore, we only enable this hotkey for non-terminal apps. 33 | local wf = hs.window.filter.new():setFilters({iTerm2 = false, Terminal = false}) 34 | enableHotkeyForWindowsMatchingFilter(wf, hs.hotkey.new({'ctrl'}, 'u', function() 35 | keyUpDown({'cmd'}, 'delete') 36 | end)) 37 | 38 | -- Use control + ; to delete to end of line 39 | -- 40 | -- I prefer to use control+h/j/k/l to move left/down/up/right by one pane in all 41 | -- multi-pane apps (e.g., iTerm, various editors). That's convenient and 42 | -- consistent, but it conflicts with the default macOS binding for deleting to 43 | -- the end of the line (i.e., control+k). To maintain that very useful 44 | -- functionality, and to keep it on the home row, this hotkey binds control+; to 45 | -- delete to the end of the line. 46 | hs.hotkey.bind({'ctrl'}, ';', function() 47 | -- If we're in the terminal, then temporarily disable our custom control+k 48 | -- hotkey used for pane navigation, then fire control+k to delete to the end 49 | -- of the line, and then renable the control+k hotkey. 50 | -- 51 | -- If we're not in the terminal, then just select to the end of the line and 52 | -- then delete the selected text. 53 | if isInTerminal() then 54 | hotkeyForControlK = hs.fnutils.find(hs.hotkey.getHotkeys(), function(hotkey) 55 | return hotkey.idx == '⌃K' 56 | end) 57 | if hotkeyForControlK then hotkeyForControlK:disable() end 58 | 59 | keyUpDown({'ctrl'}, 'k') 60 | 61 | -- Allow some time for the control+k keystroke to fire asynchronously before 62 | -- we re-enable our custom control+k hotkey. 63 | hs.timer.doAfter(0.2, function() 64 | if hotkeyForControlK then hotkeyForControlK:enable() end 65 | end) 66 | else 67 | keyUpDown({'cmd', 'shift'}, 'right') 68 | keyUpDown({}, 'forwarddelete') 69 | end 70 | end) 71 | -------------------------------------------------------------------------------- /hammerspoon/double_cmdq_to_quit.lua: -------------------------------------------------------------------------------- 1 | -- Press Cmd+Q twice to quit 2 | 3 | local quitModal = hs.hotkey.modal.new('cmd','q') 4 | 5 | function quitModal:entered() 6 | hs.alert.show("Press Cmd+Q again to quit", 1) 7 | hs.timer.doAfter(1, function() quitModal:exit() end) 8 | end 9 | 10 | local function doQuit() 11 | local app = hs.application.frontmostApplication() 12 | app:kill() 13 | end 14 | 15 | quitModal:bind('cmd', 'q', doQuit) 16 | 17 | quitModal:bind('', 'escape', function() quitModal:exit() end) 18 | -------------------------------------------------------------------------------- /hammerspoon/hyper-apps-defaults.lua: -------------------------------------------------------------------------------- 1 | -- Default keybindings for launching apps in Hyper Mode 2 | -- 3 | -- To launch _your_ most commonly-used apps via Hyper Mode, create a copy of 4 | -- this file, save it as `hyper-apps.lua`, and edit the table below to configure 5 | -- your preferred shortcuts. 6 | return { 7 | --[[A for App]] { 'a', 'App Store'}, 8 | --[[B for Browser]] { 'b', 'Google Chrome'}, 9 | --[[C for ]] -- { 'c', ''}, 10 | --[[D for ]] -- { 'd', ''}, 11 | --[[E for ]] -- { 'e', ''}, 12 | --[[F for Finder]] { 'f', 'Finder'}, 13 | --[[G for ]] -- { 'g', ''}, 14 | --[[H for reload Hammerspoon config]] { 'h', function() hs.reload() end}, 15 | --[[I for ]] -- { 'i', ''}, 16 | --[[J for ]] -- { 'j', ''}, 17 | --[[K for ]] -- { 'k', ''}, 18 | --[[L for ]] -- { 'l', ''}, 19 | --[[M for ]] -- { 'm', ''}, 20 | --[[N for ]] -- { 'n', ''}, 21 | --[[O for ]] -- { 'o', ''}, 22 | --[[P for Preferences]] { 'p', 'System Preferences'}, 23 | --[[Q for ]] -- { 'q', ''}, 24 | --[[R for ]] -- { 'r', ''}, 25 | --[[S for ]] -- { 's', ''}, 26 | --[[T for Terminal]] { 't', 'Terminal'}, 27 | --[[U for ]] -- { 'u', ''}, 28 | --[[V for ]] -- { 'v', ''}, 29 | --[[W for Word]] { 'w', 'Microsoft Word'}, 30 | --[[X for Xcode]] { 'x', 'Xcode'}, 31 | --[[Y for ]] -- { 'y', ''}, 32 | --[[Z for ]] -- { 'z', ''}, 33 | } 34 | -------------------------------------------------------------------------------- /hammerspoon/hyper-apps.lua: -------------------------------------------------------------------------------- 1 | -- Keybindings for launching apps in Hyper Mode 2 | 3 | return { 4 | { 'a', 'Android Studio'}, --A for Android 5 | { 'b', 'Bitwarden'}, --B for Bitwarden 6 | { 'c', 'Google Chrome'}, --C for Chrome 7 | { 'd', 'Dash'}, --D for Dash 8 | { 'e', 'Microsoft Edge'}, --E for Edge 9 | { 'f', 'Messenger'}, --F for Facebook Messenger 10 | { 'g', 'Insomnia'}, --G for GraphQL 11 | { 'h', function() hs.reload() end}, --H for reload Hammerspoon config 12 | { 'i', 'System Information'}, --I for Information 13 | { 'j', 'IntelliJ'}, --J for IntelliJ 14 | { 'k', 'Keynote'}, --K for Keynote 15 | { 'l', 'Launchpad'}, --L for Launchpad 16 | { 'm', 'Spark'}, --M for Spark Mails 17 | { 'n', 'Notion'}, --N for Notion 18 | { 'o', 'Orion'}, --O for Orion 19 | { 'p', 'System Preferences'}, --P for Preferences 20 | { 'q', 'QuickTime Player'}, --Q for QuickTime 21 | { 'r', 'Spotify'}, --R for Radio 22 | { 's', 'Slack'}, --S for Slack 23 | -- 't' for iTerm slide down window (set in iTerm Settings) 24 | -- 'u' for Unclutter (set in Unclutter Settings) 25 | { 'v', 'Visual Studio Code'}, --V for VSCode 26 | { 'w', 'Microsoft Word'}, --W for Word 27 | { 'x', 'Excel'}, --X for Excel 28 | { 'y', 'YouTube Music'}, --Y for Youtube Music 29 | { 'z', 'Zoom.us'}, --Z for Zoom 30 | -- '1' for 1Password Quick Access (set in 1Password Settings) 31 | { '2', 'Visual Studio Code'}, 32 | { '3', 'iTerm'}, 33 | { '4', 'Insomnia'}, 34 | { '5', 'Slack'}, 35 | } 36 | -------------------------------------------------------------------------------- /hammerspoon/hyper.lua: -------------------------------------------------------------------------------- 1 | local status, hyperModeAppMappings = pcall(require, 'keyboard.hyper-apps') 2 | 3 | if not status then 4 | hyperModeAppMappings = require('keyboard.hyper-apps-defaults') 5 | end 6 | 7 | for i, mapping in ipairs(hyperModeAppMappings) do 8 | local key = mapping[1] 9 | local app = mapping[2] 10 | hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function() 11 | if (type(app) == 'string') then 12 | hs.application.open(app) 13 | elseif (type(app) == 'function') then 14 | app() 15 | else 16 | hs.logger.new('hyper'):e('Invalid mapping for Hyper +', key) 17 | end 18 | end) 19 | end 20 | -------------------------------------------------------------------------------- /hammerspoon/init.lua: -------------------------------------------------------------------------------- 1 | local log = hs.logger.new('init.lua', 'debug') 2 | 3 | keyUpDown = function(modifiers, key) 4 | -- Un-comment & reload config to log each keystroke that we're triggering 5 | -- log.d('Sending keystroke:', hs.inspect(modifiers), key) 6 | 7 | hs.eventtap.keyStroke(modifiers, key, 0) 8 | end 9 | 10 | -- Subscribe to the necessary events on the given window filter such that the 11 | -- given hotkey is enabled for windows that match the window filter and disabled 12 | -- for windows that don't match the window filter. 13 | -- 14 | -- windowFilter - An hs.window.filter object describing the windows for which 15 | -- the hotkey should be enabled. 16 | -- hotkey - The hs.hotkey object to enable/disable. 17 | -- 18 | -- Returns nothing. 19 | enableHotkeyForWindowsMatchingFilter = function(windowFilter, hotkey) 20 | windowFilter:subscribe(hs.window.filter.windowFocused, function() 21 | hotkey:enable() 22 | end) 23 | 24 | windowFilter:subscribe(hs.window.filter.windowUnfocused, function() 25 | hotkey:disable() 26 | end) 27 | end 28 | 29 | require('keyboard.delete-words') 30 | require('keyboard.double_cmdq_to_quit') 31 | require('keyboard.hyper') 32 | require('keyboard.markdown') 33 | require('keyboard.panes') 34 | require('keyboard.windows') 35 | 36 | hs.notify.new({title='Hammerspoon', informativeText='Ready to rock 🤘'}):send() 37 | -------------------------------------------------------------------------------- /hammerspoon/markdown.lua: -------------------------------------------------------------------------------- 1 | function wrapSelectedText(wrapCharacters) 2 | -- Preserve the current contents of the system clipboard 3 | local originalClipboardContents = hs.pasteboard.getContents() 4 | 5 | -- Copy the currently-selected text to the system clipboard 6 | keyUpDown('cmd', 'c') 7 | 8 | -- Allow some time for the command+c keystroke to fire asynchronously before 9 | -- we try to read from the clipboard 10 | hs.timer.doAfter(0.2, function() 11 | -- Construct the formatted output and paste it over top of the 12 | -- currently-selected text 13 | local selectedText = hs.pasteboard.getContents() 14 | local wrappedText = wrapCharacters .. selectedText .. wrapCharacters 15 | hs.pasteboard.setContents(wrappedText) 16 | keyUpDown('cmd', 'v') 17 | 18 | -- Allow some time for the command+v keystroke to fire asynchronously before 19 | -- we restore the original clipboard 20 | hs.timer.doAfter(0.2, function() 21 | hs.pasteboard.setContents(originalClipboardContents) 22 | end) 23 | end) 24 | end 25 | 26 | function inlineLink() 27 | -- Fetch URL from the system clipboard 28 | local linkUrl = hs.pasteboard.getContents() 29 | 30 | -- Copy the currently-selected text to use as the link text 31 | keyUpDown('cmd', 'c') 32 | 33 | -- Allow some time for the command+c keystroke to fire asynchronously before 34 | -- we try to read from the clipboard 35 | hs.timer.doAfter(0.2, function() 36 | -- Construct the formatted output and paste it over top of the 37 | -- currently-selected text 38 | local linkText = hs.pasteboard.getContents() 39 | local markdown = '[' .. linkText .. '](' .. linkUrl .. ')' 40 | hs.pasteboard.setContents(markdown) 41 | keyUpDown('cmd', 'v') 42 | 43 | -- Allow some time for the command+v keystroke to fire asynchronously before 44 | -- we restore the original clipboard 45 | hs.timer.doAfter(0.2, function() 46 | hs.pasteboard.setContents(linkUrl) 47 | end) 48 | end) 49 | end 50 | 51 | -------------------------------------------------------------------------------- 52 | -- Define Markdown Mode 53 | -- 54 | -- Markdown Mode allows you to perform common Markdown-formatting tasks anywhere 55 | -- that you're editing text. Use Control+m to turn on Markdown mode. Then, use 56 | -- any shortcut below to perform a formatting action. For example, to format the 57 | -- selected text as bold in Markdown, hit Control+m, and then b. 58 | -- 59 | -- b => wrap the selected text in double asterisks ("b" for "bold") 60 | -- c => wrap the selected text in backticks ("c" for "code") 61 | -- i => wrap the selected text in single asterisks ("i" for "italic") 62 | -- s => wrap the selected text in double tildes ("s" for "strikethrough") 63 | -- l => convert the currently-selected text to an inline link, using a URL 64 | -- from the clipboard ("l" for "link") 65 | -------------------------------------------------------------------------------- 66 | 67 | markdownMode = hs.hotkey.modal.new({}, 'F20') 68 | 69 | local message = require('keyboard.status-message') 70 | markdownMode.statusMessage = message.new('Markdown Mode (control-m)') 71 | markdownMode.entered = function() 72 | markdownMode.statusMessage:show() 73 | end 74 | markdownMode.exited = function() 75 | markdownMode.statusMessage:hide() 76 | end 77 | 78 | -- Bind the given key to call the given function and exit Markdown mode 79 | function markdownMode.bindWithAutomaticExit(mode, key, fn) 80 | mode:bind({}, key, function() 81 | mode:exit() 82 | fn() 83 | end) 84 | end 85 | 86 | markdownMode:bindWithAutomaticExit('b', function() 87 | wrapSelectedText('**') 88 | end) 89 | 90 | markdownMode:bindWithAutomaticExit('i', function() 91 | wrapSelectedText('*') 92 | end) 93 | 94 | markdownMode:bindWithAutomaticExit('s', function() 95 | wrapSelectedText('~~') 96 | end) 97 | 98 | markdownMode:bindWithAutomaticExit('l', function() 99 | inlineLink() 100 | end) 101 | 102 | markdownMode:bindWithAutomaticExit('c', function() 103 | wrapSelectedText('`') 104 | end) 105 | 106 | -- Use Control+m to toggle Markdown Mode 107 | hs.hotkey.bind({'ctrl'}, 'm', function() 108 | markdownMode:enter() 109 | end) 110 | markdownMode:bind({'ctrl'}, 'm', function() 111 | markdownMode:exit() 112 | end) 113 | -------------------------------------------------------------------------------- /hammerspoon/panes.lua: -------------------------------------------------------------------------------- 1 | local itermHotkeyMappings = { 2 | -- Use control + dash to split panes horizontally 3 | { 4 | from = {{'ctrl'}, '-'}, 5 | to = {{'cmd', 'shift'}, 'd'} 6 | }, 7 | 8 | -- Use control + pipe to split panes vertically 9 | { 10 | from = {{'ctrl', 'shift'}, '\\'}, 11 | to = {{'cmd'}, 'd'} 12 | }, 13 | 14 | -- Use control + h/j/k/l to move left/down/up/right by one pane 15 | { 16 | from = {{'ctrl'}, 'h'}, 17 | to = {{'cmd', 'alt'}, 'left'} 18 | }, 19 | { 20 | from = {{'ctrl'}, 'j'}, 21 | to = {{'cmd', 'alt'}, 'down'} 22 | }, 23 | { 24 | from = {{'ctrl'}, 'k'}, 25 | to = {{'cmd', 'alt'}, 'up'} 26 | }, 27 | { 28 | from = {{'ctrl'}, 'l'}, 29 | to = {{'cmd', 'alt'}, 'right'} 30 | }, 31 | } 32 | 33 | local terminalWindowFilter = hs.window.filter.new('iTerm2') 34 | local itermHotkeys = hs.fnutils.each(itermHotkeyMappings, function(mapping) 35 | local fromMods = mapping['from'][1] 36 | local fromKey = mapping['from'][2] 37 | local toMods = mapping['to'][1] 38 | local toKey = mapping['to'][2] 39 | 40 | local hotkey = hs.hotkey.new(fromMods, fromKey, function() 41 | keyUpDown(toMods, toKey) 42 | end) 43 | 44 | enableHotkeyForWindowsMatchingFilter(terminalWindowFilter, hotkey) 45 | end) 46 | -------------------------------------------------------------------------------- /hammerspoon/status-message.lua: -------------------------------------------------------------------------------- 1 | local drawing = require 'hs.drawing' 2 | local geometry = require 'hs.geometry' 3 | local screen = require 'hs.screen' 4 | local styledtext = require 'hs.styledtext' 5 | 6 | local statusmessage = {} 7 | statusmessage.new = function(messageText) 8 | local buildParts = function(messageText) 9 | local frame = screen.primaryScreen():frame() 10 | 11 | local styledTextAttributes = { font = { name = 'Menlo', size = 24 } } 12 | 13 | local styledText = styledtext.new('🔨 ' .. messageText, styledTextAttributes) 14 | 15 | local styledTextSize = drawing.getTextDrawingSize(styledText) 16 | local textRect = { 17 | -- x = frame.w - styledTextSize.w - 40, 18 | -- y = frame.h - styledTextSize.h, 19 | x = (frame.w - styledTextSize.w) / 2, 20 | y = (frame.h - styledTextSize.h) / 2, 21 | w = styledTextSize.w + 40, 22 | h = styledTextSize.h + 40, 23 | } 24 | local text = drawing.text(textRect, styledText):setAlpha(0.7) 25 | 26 | local background = drawing.rectangle( 27 | { 28 | -- x = frame.w - styledTextSize.w - 45, 29 | -- y = frame.h - styledTextSize.h - 3, 30 | x = (frame.w - styledTextSize.w) / 2, 31 | y = (frame.h - styledTextSize.h) / 2, 32 | w = styledTextSize.w + 15, 33 | h = styledTextSize.h + 6 34 | } 35 | ) 36 | background:setRoundedRectRadii(10, 10) 37 | background:setFillColor({ red = 100, green = 100, blue = 100, alpha=0.6 }) 38 | 39 | return background, text 40 | end 41 | 42 | return { 43 | _buildParts = buildParts, 44 | show = function(self) 45 | self:hide() 46 | 47 | self.background, self.text = self._buildParts(messageText) 48 | self.background:show() 49 | self.text:show() 50 | end, 51 | hide = function(self) 52 | if self.background then 53 | self.background:delete() 54 | self.background = nil 55 | end 56 | if self.text then 57 | self.text:delete() 58 | self.text = nil 59 | end 60 | end, 61 | notify = function(self, seconds) 62 | local seconds = seconds or 1 63 | self:show() 64 | hs.timer.delayed.new(seconds, function() self:hide() end):start() 65 | end 66 | } 67 | end 68 | 69 | return statusmessage 70 | -------------------------------------------------------------------------------- /hammerspoon/windows-bindings-defaults.lua: -------------------------------------------------------------------------------- 1 | -- Default keybindings for WindowLayout Mode 2 | -- 3 | -- To customize the key bindings for WindowLayout Mode, create a copy of this 4 | -- file, save it as `windows-bindings.lua`, and edit the table below to 5 | -- configure your preferred shortcuts. 6 | 7 | -------------------------------------------------------------------------------- 8 | -- Define WindowLayout Mode 9 | -- 10 | -- WindowLayout Mode allows you to manage window layout using keyboard shortcuts 11 | -- that are on the home row, or very close to it. Use Control+s to turn 12 | -- on WindowLayout mode. Then, use any shortcut below to perform a window layout 13 | -- action. For example, to send the window left, press and release 14 | -- Control+s, and then press h. 15 | -- 16 | -- h/j/k/l => send window to the left/bottom/top/right half of the screen 17 | -- i => send window to the upper left quarter of the screen 18 | -- o => send window to the upper right quarter of the screen 19 | -- , => send window to the lower left quarter of the screen 20 | -- . => send window to the lower right quarter of the screen 21 | -- return => make window full screen 22 | -- n => send window to the next monitor 23 | -- left => send window to the monitor on the left (if there is one) 24 | -- right => send window to the monitor on the right (if there is one) 25 | -------------------------------------------------------------------------------- 26 | 27 | return { 28 | modifiers = {'ctrl'}, 29 | showHelp = true, 30 | trigger = 's', 31 | mappings = { 32 | { {}, 'return', 'maximize' }, 33 | { {'shift'}, 'return', 'fullHeightCenter' }, 34 | { {}, 'space', 'halfHeightWideCenter' }, 35 | { {'shift'}, 'space', 'halfAndHalfCenter' }, 36 | { {}, 'h', 'left' }, 37 | { {}, 'l', 'right' }, 38 | { {}, 'k', 'up' }, 39 | { {}, 'j', 'down' }, 40 | { {}, 'i', 'upLeft' }, 41 | { {}, 'o', 'upRight' }, 42 | { {}, ',', 'downLeft' }, 43 | { {}, '.', 'downRight' }, 44 | { {'shift'}, 'h', 'left40' }, 45 | { {'shift'}, 'l', 'right60' }, 46 | { {}, 'n', 'nextScreen' }, -- not working currently 47 | { {}, 'right', 'moveOneScreenEast' }, -- not working currently 48 | { {}, 'left', 'moveOneScreenWest' }, -- not working currently 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /hammerspoon/windows-bindings.lua: -------------------------------------------------------------------------------- 1 | -- Default keybindings for WindowLayout Mode 2 | -- 3 | -- To customize the key bindings for WindowLayout Mode, create a copy of this 4 | -- file, save it as `windows-bindings.lua`, and edit the table below to 5 | -- configure your preferred shortcuts. 6 | 7 | -------------------------------------------------------------------------------- 8 | -- Define WindowLayout Mode 9 | -- 10 | -- WindowLayout Mode allows you to manage window layout using keyboard shortcuts 11 | -- that are on the home row, or very close to it. Use Control+s to turn 12 | -- on WindowLayout mode. Then, use any shortcut below to perform a window layout 13 | -- action. For example, to send the window left, press and release 14 | -- Control+s, and then press h. 15 | -- 16 | -- h/j/k/l => send window to the left/bottom/top/right half of the screen 17 | -- i => send window to the upper left quarter of the screen 18 | -- o => send window to the upper right quarter of the screen 19 | -- , => send window to the lower left quarter of the screen 20 | -- . => send window to the lower right quarter of the screen 21 | -- return => make window full screen 22 | -- n => send window to the next monitor 23 | -- left => send window to the monitor on the left (if there is one) 24 | -- right => send window to the monitor on the right (if there is one) 25 | -------------------------------------------------------------------------------- 26 | 27 | return { 28 | modifiers = {'ctrl'}, 29 | showHelp = true, 30 | trigger = 'w', 31 | mappings = { 32 | { {}, 'return', 'maximize' }, 33 | { {'shift'}, 'return', 'fullHeightCenter' }, 34 | { {}, 'space', 'halfHeightWideCenter' }, 35 | { {'shift'}, 'space', 'halfAndHalfCenter' }, 36 | { {}, 'h', 'left' }, 37 | { {}, 'l', 'right' }, 38 | { {}, 'k', 'up' }, 39 | { {}, 'up', 'up' }, 40 | { {}, 'j', 'down' }, 41 | { {}, 'down', 'down' }, 42 | { {}, 'i', 'upLeft' }, 43 | { {}, 'o', 'upRight' }, 44 | { {}, ',', 'downLeft' }, 45 | { {}, '.', 'downRight' }, 46 | { {'shift'}, 'h', 'left40' }, 47 | { {'shift'}, 'left', 'left40' }, 48 | { {'shift'}, 'l', 'right60' }, 49 | { {'shift'}, 'right', 'right60' }, 50 | { {}, 'n', 'nextScreen' }, 51 | { {}, 'right', 'moveOneScreenEast' }, 52 | { {}, 'left', 'moveOneScreenWest' }, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hammerspoon/windows.lua: -------------------------------------------------------------------------------- 1 | hs.window.animationDuration = 0.1 2 | 3 | -- +-----------------+ 4 | -- | | | 5 | -- | HERE | | 6 | -- | | | 7 | -- +-----------------+ 8 | function left(win) 9 | local f = win:frame() 10 | local screen = win:screen() 11 | local max = screen:frame() 12 | 13 | f.x = max.x 14 | f.y = max.y 15 | f.w = max.w / 2 16 | f.h = max.h 17 | win:setFrame(f) 18 | end 19 | 20 | -- +-----------------+ 21 | -- | | | 22 | -- | | HERE | 23 | -- | | | 24 | -- +-----------------+ 25 | function right(win) 26 | local f = win:frame() 27 | local screen = win:screen() 28 | local max = screen:frame() 29 | 30 | f.x = max.x + (max.w / 2) 31 | f.y = max.y 32 | f.w = max.w / 2 33 | f.h = max.h 34 | win:setFrame(f) 35 | end 36 | 37 | -- +-----------------+ 38 | -- | HERE | 39 | -- +-----------------+ 40 | -- | | 41 | -- +-----------------+ 42 | function up(win) 43 | local f = win:frame() 44 | local screen = win:screen() 45 | local max = screen:frame() 46 | 47 | f.x = max.x 48 | f.w = max.w 49 | f.y = max.y 50 | f.h = max.h / 2 51 | win:setFrame(f) 52 | end 53 | 54 | -- +-----------------+ 55 | -- | | 56 | -- +-----------------+ 57 | -- | HERE | 58 | -- +-----------------+ 59 | function down(win) 60 | local f = win:frame() 61 | local screen = win:screen() 62 | local max = screen:frame() 63 | 64 | f.x = max.x 65 | f.w = max.w 66 | f.y = max.y + (max.h / 2) 67 | f.h = max.h / 2 68 | win:setFrame(f) 69 | end 70 | 71 | -- +-----------------+ 72 | -- | HERE | | 73 | -- +--------+ | 74 | -- | | 75 | -- +-----------------+ 76 | function upLeft(win) 77 | local f = win:frame() 78 | local screen = win:screen() 79 | local max = screen:fullFrame() 80 | 81 | f.x = max.x 82 | f.y = max.y 83 | f.w = max.w/2 84 | f.h = max.h/2 85 | win:setFrame(f) 86 | end 87 | 88 | -- +-----------------+ 89 | -- | | 90 | -- +--------+ | 91 | -- | HERE | | 92 | -- +-----------------+ 93 | function downLeft(win) 94 | local f = win:frame() 95 | local screen = win:screen() 96 | local max = screen:fullFrame() 97 | 98 | f.x = max.x 99 | f.y = max.y + (max.h / 2) 100 | f.w = max.w/2 101 | f.h = max.h/2 102 | win:setFrame(f) 103 | end 104 | 105 | -- +-----------------+ 106 | -- | | 107 | -- | +--------| 108 | -- | | HERE | 109 | -- +-----------------+ 110 | function downRight(win) 111 | local f = win:frame() 112 | local screen = win:screen() 113 | local max = screen:fullFrame() 114 | 115 | f.x = max.x + (max.w / 2) 116 | f.y = max.y + (max.h / 2) 117 | f.w = max.w/2 118 | f.h = max.h/2 119 | 120 | win:setFrame(f) 121 | end 122 | 123 | -- +-----------------+ 124 | -- | | HERE | 125 | -- | +--------| 126 | -- | | 127 | -- +-----------------+ 128 | function upRight(win) 129 | local f = win:frame() 130 | local screen = win:screen() 131 | local max = screen:fullFrame() 132 | 133 | f.x = max.x + (max.w / 2) 134 | f.y = max.y 135 | f.w = max.w/2 136 | f.h = max.h/2 137 | win:setFrame(f) 138 | end 139 | 140 | -- +------------------+ 141 | -- | +--------+ | 142 | -- | | HERE | | 143 | -- | +--------+ | 144 | -- +------------------+ 145 | function halfAndHalfCenter(win) 146 | local f = win:frame() 147 | local screen = win:screen() 148 | local max = screen:fullFrame() 149 | 150 | f.x = max.x + (max.w / 5) 151 | f.w = max.w * 3/5 152 | f.y = max.y + (max.h / 10) 153 | f.h = max.h * 4/5 154 | win:setFrame(f) 155 | end 156 | 157 | -- +------------------+ 158 | -- | +------------+ | 159 | -- | | HERE | | 160 | -- | +------------+ | 161 | -- +------------------+ 162 | function halfHeightWideCenter(win) 163 | local f = win:frame() 164 | local screen = win:screen() 165 | local max = screen:fullFrame() 166 | 167 | f.x = max.x + (max.w / 10) 168 | f.w = max.w * 4/5 169 | f.y = max.y + (max.h / 10) 170 | f.h = max.h * 4/5 171 | win:setFrame(f) 172 | end 173 | 174 | -- +------------------+ 175 | -- | | | | 176 | -- | | HERE | | 177 | -- | | | | 178 | -- +------------------+ 179 | function fullHeightCenter(win) 180 | local f = win:frame() 181 | local screen = win:screen() 182 | local max = screen:fullFrame() 183 | 184 | f.x = max.x + (max.w / 5) 185 | f.w = max.w * 3/5 186 | f.y = max.y 187 | f.h = max.h 188 | win:setFrame(f) 189 | end 190 | 191 | -- +-----------------+ 192 | -- | | | 193 | -- | HERE | | 194 | -- | | | 195 | -- +-----------------+ 196 | function left40(win) 197 | local f = win:frame() 198 | local screen = win:screen() 199 | local max = screen:frame() 200 | 201 | f.x = max.x 202 | f.y = max.y 203 | f.w = max.w * 0.4 204 | f.h = max.h 205 | win:setFrame(f) 206 | end 207 | 208 | -- +-----------------+ 209 | -- | | | 210 | -- | | HERE | 211 | -- | | | 212 | -- +-----------------+ 213 | function right60(win) 214 | local f = win:frame() 215 | local screen = win:screen() 216 | local max = screen:frame() 217 | 218 | f.x = max.x + (max.w * 0.4) 219 | f.y = max.y 220 | f.w = max.w * 0.6 221 | f.h = max.h 222 | win:setFrame(f) 223 | end 224 | 225 | function nextScreen(win) 226 | local currentScreen = win:screen() 227 | local allScreens = hs.screen.allScreens() 228 | currentScreenIndex = hs.fnutils.indexOf(allScreens, currentScreen) 229 | nextScreenIndex = currentScreenIndex + 1 230 | 231 | if allScreens[nextScreenIndex] then 232 | win:moveToScreen(allScreens[nextScreenIndex]) 233 | else 234 | win:moveToScreen(allScreens[1]) 235 | end 236 | end 237 | 238 | function maximize(win) 239 | win:maximize() 240 | end 241 | 242 | function moveOneScreenWest(win) 243 | win:moveOneScreenWest() 244 | end 245 | 246 | function moveOneScreenEast(win) 247 | win:moveOneScreenEast() 248 | end 249 | 250 | local windowActions = { 251 | ["maximize"] = maximize, 252 | ["fullHeightCenter"] = fullHeightCenter, 253 | ["halfAndHalfCenter"] = halfAndHalfCenter, 254 | ["halfHeightWideCenter"] = halfHeightWideCenter, 255 | ["left"] = left, 256 | ["right"] = right, 257 | ["up"] = up, 258 | ["down"] = down, 259 | ["upLeft"] = upLeft, 260 | ["upRight"] = upRight, 261 | ["downLeft"] = downLeft, 262 | ["downRight"] = downRight, 263 | ["left40"] = left40, 264 | ["right60"] = right60, 265 | ["nextScreen"] = nextScreen, 266 | ["moveOneScreenWest"] = moveOneScreenWest, 267 | ["moveOneScreenEast"] = moveOneScreenEast, 268 | } 269 | 270 | windowLayoutMode = hs.hotkey.modal.new({}, 'F16') 271 | 272 | windowLayoutMode.entered = function() 273 | windowLayoutMode.statusMessage:show() 274 | end 275 | windowLayoutMode.exited = function() 276 | windowLayoutMode.statusMessage:hide() 277 | end 278 | 279 | -- Bind the given key to call the given function and exit WindowLayout mode 280 | function windowLayoutMode.bindWithAutomaticExit(mode, modifiers, key, fn) 281 | mode:bind(modifiers, key, function() 282 | mode:exit() 283 | fn() 284 | end) 285 | end 286 | 287 | local status, windowMappings = pcall(require, 'keyboard.windows-bindings') 288 | 289 | if not status then 290 | windowMappings = require('keyboard.windows-bindings-defaults') 291 | end 292 | 293 | local modifiers = windowMappings.modifiers 294 | local showHelp = windowMappings.showHelp 295 | local trigger = windowMappings.trigger 296 | local mappings = windowMappings.mappings 297 | 298 | function getModifiersStr(modifiers) 299 | local modMap = { shift = '⇧', ctrl = '⌃', alt = '⌥', cmd = '⌘' } 300 | local retVal = '' 301 | for i, v in ipairs(modifiers) do 302 | retVal = retVal .. modMap[v] 303 | end 304 | return retVal 305 | end 306 | 307 | local msgStr = getModifiersStr(modifiers) 308 | msgStr = 'Window Layout Mode (' .. msgStr .. (string.len(msgStr) > 0 and '+' or '') .. trigger .. ')' 309 | 310 | for i, mapping in ipairs(mappings) do 311 | local modifiers, trigger, winAction = table.unpack(mapping) 312 | local hotKeyStr = getModifiersStr(modifiers) 313 | 314 | if showHelp == true then 315 | if string.len(hotKeyStr) > 0 then 316 | msgStr = msgStr .. (string.format('\n%10s+%s => %s', hotKeyStr, trigger, winAction)) 317 | else 318 | msgStr = msgStr .. (string.format('\n%11s => %s', trigger, winAction)) 319 | end 320 | end 321 | 322 | windowLayoutMode:bindWithAutomaticExit(modifiers, trigger, function() 323 | --example: hs.window.focusedWindow():upRight() 324 | local focusedWin = hs.window.focusedWindow() 325 | if focusedWin == nil then 326 | return 327 | end 328 | windowActions[winAction](focusedWin) 329 | end) 330 | end 331 | 332 | local message = require('keyboard.status-message') 333 | windowLayoutMode.statusMessage = message.new(msgStr) 334 | 335 | -- Use modifiers+trigger to toggle WindowLayout Mode 336 | hs.hotkey.bind(modifiers, trigger, function() 337 | windowLayoutMode:enter() 338 | end) 339 | windowLayoutMode:bind(modifiers, trigger, function() 340 | windowLayoutMode:exit() 341 | end) 342 | -------------------------------------------------------------------------------- /inputrc: -------------------------------------------------------------------------------- 1 | # Configure option + right arrow to move forward one word in iTerm2 2 | # https://www.gnu.org/software/bash/manual/bashref.html#Commands-For-Moving 3 | "\e\e[C": forward-word 4 | 5 | # Configure option + left arrow to move backward one word in iTerm2 6 | # https://www.gnu.org/software/bash/manual/bashref.html#Commands-For-Moving 7 | "\e\e[D": backward-word 8 | -------------------------------------------------------------------------------- /karabiner/karabiner.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": [ 3 | { 4 | "complex_modifications": { 5 | "parameters": { "basic.simultaneous_threshold_milliseconds": 70 }, 6 | "rules": [ 7 | { 8 | "description": "S+D -> (S)uper(D)uper Mode", 9 | "manipulators": [ 10 | { 11 | "from": { 12 | "simultaneous": [ 13 | { "key_code": "s" }, 14 | { "key_code": "d" } 15 | ], 16 | "simultaneous_options": { 17 | "detect_key_down_uninterruptedly": true, 18 | "key_down_order": "insensitive", 19 | "key_up_order": "insensitive", 20 | "key_up_when": "any", 21 | "to_after_key_up": [ 22 | { 23 | "set_variable": { 24 | "name": "superduper-mode", 25 | "value": 0 26 | } 27 | } 28 | ] 29 | } 30 | }, 31 | "to": [ 32 | { 33 | "set_variable": { 34 | "name": "superduper-mode", 35 | "value": 1 36 | } 37 | } 38 | ], 39 | "type": "basic" 40 | } 41 | ] 42 | }, 43 | { 44 | "description": "(S)uper(D)uper + H -> Left", 45 | "manipulators": [ 46 | { 47 | "conditions": [ 48 | { 49 | "name": "superduper-mode", 50 | "type": "variable_if", 51 | "value": 1 52 | } 53 | ], 54 | "from": { 55 | "key_code": "h", 56 | "modifiers": { "optional": ["any"] } 57 | }, 58 | "to": [{ "key_code": "left_arrow" }], 59 | "type": "basic" 60 | } 61 | ] 62 | }, 63 | { 64 | "description": "(S)uper(D)uper + J -> Down", 65 | "manipulators": [ 66 | { 67 | "conditions": [ 68 | { 69 | "name": "superduper-mode", 70 | "type": "variable_if", 71 | "value": 1 72 | } 73 | ], 74 | "from": { 75 | "key_code": "j", 76 | "modifiers": { "optional": ["any"] } 77 | }, 78 | "to": [{ "key_code": "down_arrow" }], 79 | "type": "basic" 80 | } 81 | ] 82 | }, 83 | { 84 | "description": "(S)uper(D)uper + K -> Up", 85 | "manipulators": [ 86 | { 87 | "conditions": [ 88 | { 89 | "name": "superduper-mode", 90 | "type": "variable_if", 91 | "value": 1 92 | } 93 | ], 94 | "from": { 95 | "key_code": "k", 96 | "modifiers": { "optional": ["any"] } 97 | }, 98 | "to": [{ "key_code": "up_arrow" }], 99 | "type": "basic" 100 | } 101 | ] 102 | }, 103 | { 104 | "description": "(S)uper(D)uper + L -> Right", 105 | "manipulators": [ 106 | { 107 | "conditions": [ 108 | { 109 | "name": "superduper-mode", 110 | "type": "variable_if", 111 | "value": 1 112 | } 113 | ], 114 | "from": { 115 | "key_code": "l", 116 | "modifiers": { "optional": ["any"] } 117 | }, 118 | "to": [{ "key_code": "right_arrow" }], 119 | "type": "basic" 120 | } 121 | ] 122 | }, 123 | { 124 | "description": "(S)uper(D)uper + A -> Option", 125 | "manipulators": [ 126 | { 127 | "conditions": [ 128 | { 129 | "name": "superduper-mode", 130 | "type": "variable_if", 131 | "value": 1 132 | } 133 | ], 134 | "from": { 135 | "key_code": "a", 136 | "modifiers": { "optional": ["any"] } 137 | }, 138 | "to": [{ "key_code": "left_option" }], 139 | "type": "basic" 140 | } 141 | ] 142 | }, 143 | { 144 | "description": "(S)uper(D)uper + F -> Command", 145 | "manipulators": [ 146 | { 147 | "conditions": [ 148 | { 149 | "name": "superduper-mode", 150 | "type": "variable_if", 151 | "value": 1 152 | } 153 | ], 154 | "from": { 155 | "key_code": "f", 156 | "modifiers": { "optional": ["any"] } 157 | }, 158 | "to": [{ "key_code": "right_command" }], 159 | "type": "basic" 160 | } 161 | ] 162 | }, 163 | { 164 | "description": "(S)uper(D)uper + Space -> Shift", 165 | "manipulators": [ 166 | { 167 | "conditions": [ 168 | { 169 | "name": "superduper-mode", 170 | "type": "variable_if", 171 | "value": 1 172 | } 173 | ], 174 | "from": { 175 | "key_code": "spacebar", 176 | "modifiers": { "optional": ["any"] } 177 | }, 178 | "to": [{ "key_code": "right_shift" }], 179 | "type": "basic" 180 | } 181 | ] 182 | }, 183 | { 184 | "description": "(S)uper(D)uper + U -> First Tab", 185 | "manipulators": [ 186 | { 187 | "conditions": [ 188 | { 189 | "name": "superduper-mode", 190 | "type": "variable_if", 191 | "value": 1 192 | } 193 | ], 194 | "from": { 195 | "key_code": "u", 196 | "modifiers": { "optional": ["any"] } 197 | }, 198 | "to": [ 199 | { 200 | "key_code": "1", 201 | "modifiers": ["left_command"] 202 | } 203 | ], 204 | "type": "basic" 205 | } 206 | ] 207 | }, 208 | { 209 | "description": "(S)uper(D)uper + I -> Prev Tab", 210 | "manipulators": [ 211 | { 212 | "conditions": [ 213 | { 214 | "name": "superduper-mode", 215 | "type": "variable_if", 216 | "value": 1 217 | } 218 | ], 219 | "from": { 220 | "key_code": "i", 221 | "modifiers": { "optional": ["any"] } 222 | }, 223 | "to": [ 224 | { 225 | "key_code": "open_bracket", 226 | "modifiers": ["left_command", "left_shift"] 227 | } 228 | ], 229 | "type": "basic" 230 | } 231 | ] 232 | }, 233 | { 234 | "description": "(S)uper(D)uper + O -> Next Tab", 235 | "manipulators": [ 236 | { 237 | "conditions": [ 238 | { 239 | "name": "superduper-mode", 240 | "type": "variable_if", 241 | "value": 1 242 | } 243 | ], 244 | "from": { 245 | "key_code": "o", 246 | "modifiers": { "optional": ["any"] } 247 | }, 248 | "to": [ 249 | { 250 | "key_code": "close_bracket", 251 | "modifiers": ["left_command", "left_shift"] 252 | } 253 | ], 254 | "type": "basic" 255 | } 256 | ] 257 | }, 258 | { 259 | "description": "(S)uper(D)uper + P -> Last Tab", 260 | "manipulators": [ 261 | { 262 | "conditions": [ 263 | { 264 | "name": "superduper-mode", 265 | "type": "variable_if", 266 | "value": 1 267 | } 268 | ], 269 | "from": { 270 | "key_code": "p", 271 | "modifiers": { "optional": ["any"] } 272 | }, 273 | "to": [ 274 | { 275 | "key_code": "9", 276 | "modifiers": ["left_command"] 277 | } 278 | ], 279 | "type": "basic" 280 | } 281 | ] 282 | }, 283 | { 284 | "description": "Better Shifting: Parentheses on shift keys", 285 | "manipulators": [ 286 | { 287 | "from": { "key_code": "left_shift" }, 288 | "to": [{ "key_code": "left_shift" }], 289 | "to_if_alone": [ 290 | { 291 | "key_code": "9", 292 | "modifiers": ["left_shift"] 293 | } 294 | ], 295 | "type": "basic" 296 | }, 297 | { 298 | "from": { "key_code": "right_shift" }, 299 | "to": [{ "key_code": "right_shift" }], 300 | "to_if_alone": [ 301 | { 302 | "key_code": "0", 303 | "modifiers": ["right_shift"] 304 | } 305 | ], 306 | "type": "basic" 307 | } 308 | ] 309 | }, 310 | { 311 | "manipulators": [ 312 | { 313 | "conditions": [ 314 | { 315 | "bundle_identifiers": [ 316 | "^org\\.mozilla\\.firefox$" 317 | ], 318 | "type": "frontmost_application_if" 319 | } 320 | ], 321 | "description": "[Firefox] Use cmd+shift+n to open new private browsing window", 322 | "from": { 323 | "key_code": "n", 324 | "modifiers": { "mandatory": ["command", "shift"] } 325 | }, 326 | "to": [ 327 | { 328 | "key_code": "p", 329 | "modifiers": ["left_command", "left_shift"] 330 | } 331 | ], 332 | "type": "basic" 333 | } 334 | ] 335 | }, 336 | { 337 | "description": "Toggle caps_lock by pressing left_shift + right_shift at the same time", 338 | "manipulators": [ 339 | { 340 | "from": { 341 | "key_code": "left_shift", 342 | "modifiers": { 343 | "mandatory": ["right_shift"], 344 | "optional": ["caps_lock"] 345 | } 346 | }, 347 | "to": [{ "key_code": "caps_lock" }], 348 | "to_if_alone": [{ "key_code": "left_shift" }], 349 | "type": "basic" 350 | }, 351 | { 352 | "from": { 353 | "key_code": "right_shift", 354 | "modifiers": { 355 | "mandatory": ["left_shift"], 356 | "optional": ["caps_lock"] 357 | } 358 | }, 359 | "to": [{ "key_code": "caps_lock" }], 360 | "to_if_alone": [{ "key_code": "right_shift" }], 361 | "type": "basic" 362 | } 363 | ] 364 | }, 365 | { 366 | "description": "Change caps_lock to escape on single press, to ctrl on press and hold", 367 | "manipulators": [ 368 | { 369 | "from": { 370 | "key_code": "caps_lock", 371 | "modifiers": { "optional": ["any"] } 372 | }, 373 | "to": [{ "key_code": "left_control" }], 374 | "to_if_alone": [{ "key_code": "escape" }], 375 | "type": "basic" 376 | } 377 | ] 378 | }, 379 | { 380 | "description": "Change left_ctrl to escape on single press", 381 | "enabled": false, 382 | "manipulators": [ 383 | { 384 | "from": { 385 | "key_code": "left_control", 386 | "modifiers": { "optional": ["any"] } 387 | }, 388 | "to": [{ "key_code": "left_control" }], 389 | "to_if_alone": [{ "key_code": "escape" }], 390 | "type": "basic" 391 | } 392 | ] 393 | }, 394 | { 395 | "description": "Change esc to cmd+ctrl+opt+shift on press and hold", 396 | "manipulators": [ 397 | { 398 | "from": { 399 | "key_code": "escape", 400 | "modifiers": { "optional": ["any"] } 401 | }, 402 | "to": [ 403 | { 404 | "key_code": "left_shift", 405 | "modifiers": ["left_control", "left_option", "left_command"] 406 | } 407 | ], 408 | "to_if_alone": [{ "key_code": "escape" }], 409 | "type": "basic" 410 | } 411 | ] 412 | }, 413 | { 414 | "description": "Change spacebar+cmd to cmd+ctrl+opt+shift+\\ to trigger Raycast", 415 | "enabled": false, 416 | "manipulators": [ 417 | { 418 | "from": { 419 | "key_code": "spacebar", 420 | "modifiers": { "mandatory": ["command"] } 421 | }, 422 | "to": { 423 | "key_code": "backslash", 424 | "modifiers": ["left_shift", "left_control", "left_option", "left_command"] 425 | }, 426 | "type": "basic" 427 | } 428 | ] 429 | } 430 | ] 431 | }, 432 | "devices": [ 433 | { 434 | "identifiers": { 435 | "is_keyboard": true, 436 | "product_id": 49970, 437 | "vendor_id": 1133 438 | }, 439 | "ignore": true, 440 | "manipulate_caps_lock_led": false 441 | } 442 | ], 443 | "fn_function_keys": [ 444 | { 445 | "from": { "key_code": "f6" }, 446 | "to": [{ "key_code": "f19" }] 447 | } 448 | ], 449 | "name": "Default", 450 | "selected": true, 451 | "simple_modifications": [ 452 | { 453 | "from": { "consumer_key_code": "eject" }, 454 | "to": [{ "key_code": "vk_none" }] 455 | } 456 | ], 457 | "virtual_hid_keyboard": { 458 | "caps_lock_delay_milliseconds": 0, 459 | "country_code": 0, 460 | "keyboard_type": "ansi", 461 | "keyboard_type_v2": "ansi" 462 | } 463 | } 464 | ] 465 | } -------------------------------------------------------------------------------- /screenshots/accessibility-permissions-for-hammerspoon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanakritBenz/keyboard/bb9fd425d3a88edf527e409e398e2c9d1d16f443/screenshots/accessibility-permissions-for-hammerspoon.png -------------------------------------------------------------------------------- /screenshots/karabiner-elements-system-extension-prompt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanakritBenz/keyboard/bb9fd425d3a88edf527e409e398e2c9d1d16f443/screenshots/karabiner-elements-system-extension-prompt-1.png -------------------------------------------------------------------------------- /screenshots/karabiner-elements-system-extension-prompt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanakritBenz/keyboard/bb9fd425d3a88edf527e409e398e2c9d1d16f443/screenshots/karabiner-elements-system-extension-prompt-2.png -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | which -s brew || (echo "Homebrew is required: http://brew.sh/" && exit 1) 6 | 7 | brew bundle check || brew bundle 8 | 9 | # Prepare custom settings for Karabiner-Elements 10 | # https://github.com/tekezo/Karabiner-Elements/issues/597#issuecomment-282760186 11 | mkdir -p ~/.config 12 | ln -sfn $PWD/karabiner ~/.config/ 13 | 14 | # Prepare custom settings for Hammerspoon 15 | mkdir -p ~/.hammerspoon 16 | if ! grep -sq "require('keyboard')" ~/.hammerspoon/init.lua; then 17 | echo "require('keyboard') -- Load Hammerspoon bits from https://github.com/jasonrudolph/keyboard" >> ~/.hammerspoon/init.lua 18 | fi 19 | ln -sfn $PWD/hammerspoon ~/.hammerspoon/keyboard 20 | 21 | # Prepare custom settings for navigating between words in iTerm2 22 | grep -sq forward-word ~/.inputrc || cat $PWD/inputrc >> ~/.inputrc 23 | 24 | # Disable Dock icon for Hammerspoon 25 | defaults write org.hammerspoon.Hammerspoon MJShowDockIconKey -bool FALSE 26 | 27 | # If Hammerspoon is already running, kill it so we can pick up the new config 28 | # when opening Hammerspoon below 29 | killall Hammerspoon || true 30 | 31 | # Open Apps 32 | open /Applications/Hammerspoon.app 33 | open /Applications/Karabiner-Elements.app 34 | 35 | # Enable apps at startup 36 | osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/Hammerspoon.app", hidden:true}' > /dev/null 37 | osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/Karabiner-Elements.app", hidden:true}' > /dev/null 38 | 39 | echo "Done! Remember to enable Accessibility for Hammerspoon." 40 | --------------------------------------------------------------------------------