diff --git a/Brewfile b/Brewfile index a3d7d608..e130bbc6 100644 --- a/Brewfile +++ b/Brewfile @@ -1,4 +1,3 @@ tap 'caskroom/cask' -cask 'karabiner-elements' cask 'hammerspoon' diff --git a/README.md b/README.md index f5e7ead3..cf084be2 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ While I find that these customizations yield a more-useful keyboard for me, they - [Arrange windows via the home row](#window-layout-mode) - [Enable other commonly-used actions on or near the home row](#miscellaneous-goodness) - [Format text as Markdown](#markdown-mode) -- [Launch commonly-used apps via global keyboard shortcuts](#hyper-key-for-quickly-launching-apps) +- [Launch commonly-used apps via global keyboard shortcuts](#hyper-mode-for-quickly-launching-apps) - [And more...](#miscellaneous-goodness) ### A more useful caps lock key @@ -128,11 +128,14 @@ Use control + m to turn on Markdown Mode. Then, use any sh - Use control + m to exit Markdown Mode without performing any actions -### Hyper key for quickly launching apps +### Hyper Mode for quickly launching apps -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. +Launch your favorite apps with global shortcuts, and do so without interfering with existing macOS shortcuts or application-specific shortcuts. 😅 -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. +Tap option (AKA alt) to enter Hyper Mode and then press any shortcut to focus the associated application. For example, if you're using the default keybindings shown below to open the Finder, you would: + +1. Tap the option key (i.e., press and then release it in quick succession) to enter Hyper Mode +2. Then, press f for Finder #### Choose your own apps @@ -140,15 +143,15 @@ Hyper Mode ships with the default keybindings below, but you'll likely want to p #### Default app keybindings -- hyper + a to open iTunes ("A" for "Apple Music") -- hyper + b to open Google Chrome ("B" for "Browser") -- hyper + c to open Slack ("C for "Chat") -- hyper + d to open [Remember The Milk](https://www.rememberthemilk.com/) ("D" for "Do!" ... or "Done!") -- hyper + e to open [Atom](https://atom.io) ("E" for "Editor") -- hyper + f to open Finder ("F" for "Finder") -- hyper + g to open [Mailplane](http://mailplaneapp.com/) ("G" for "Gmail") -- hyper + s to open [Slack](https://slack.com/downloads/osx) ("S" for "Slack") -- hyper + t to open [iTerm2](https://www.iterm2.com/) ("T" for "Terminal") +- a to open iTunes ("A" for "Apple Music") +- b to open Google Chrome ("B" for "Browser") +- c to open Slack ("C for "Chat") +- d to open [Remember The Milk](https://www.rememberthemilk.com/) ("D" for "Do!" ... or "Done!") +- e to open [Atom](https://atom.io) ("E" for "Editor") +- f to open Finder ("F" for "Finder") +- g to open [Mailplane](http://mailplaneapp.com/) ("G" for "Gmail") +- s to open [Slack](https://slack.com/downloads/osx) ("S" for "Slack") +- t to open [iTerm2](https://www.iterm2.com/) ("T" for "Terminal") ### Miscellaneous goodness @@ -164,8 +167,7 @@ Hyper Mode ships with the default keybindings below, but you'll likely want to p This setup is honed and tested with the following dependencies. - macOS High Sierra, 10.13 -- [Karabiner-Elements 11.4.0][karabiner] -- [Hammerspoon 0.9.57][hammerspoon] +- [Hammerspoon 0.9.66][hammerspoon] ## Installation @@ -181,6 +183,8 @@ This setup is honed and tested with the following dependencies. 2. Enable accessibility to allow Hammerspoon to do its thing [[screenshot]](screenshots/accessibility-permissions-for-hammerspoon.png) +3. Give yourself a [more useful caps lock key](#a-more-useful-caps-lock-key): Open *System Preferences*, navigate to *Keyboard > Modifier Keys*, and set the caps lock key to control [[screenshot]](https://user-images.githubusercontent.com/2988/27111039-7f620442-507b-11e7-9bcf-93d46e14af13.png) + ## TODO - Add [#13](https://github.com/jasonrudolph/keyboard/pull/13) to [features](#features): @@ -189,7 +193,6 @@ This setup is honed and tested with the following dependencies. [customize]: http://dictionary.reference.com/browse/customize [don't-make-me-think]: http://en.wikipedia.org/wiki/Don't_Make_Me_Think -[karabiner]: https://github.com/tekezo/Karabiner-Elements [hammerspoon]: http://www.hammerspoon.org [hammerspoon-releases]: https://github.com/Hammerspoon/hammerspoon/releases [modern-space-cadet]: http://stevelosh.com/blog/2012/10/a-modern-space-cadet diff --git a/hammerspoon/hyper.lua b/hammerspoon/hyper.lua index 7f99fe8c..4b2a1b7b 100644 --- a/hammerspoon/hyper.lua +++ b/hammerspoon/hyper.lua @@ -1,13 +1,20 @@ +-- Look for custom Hyper Mode app mappings. If there are none, then use the +-- default mappings. local status, hyperModeAppMappings = pcall(require, 'keyboard.hyper-apps') - if not status then hyperModeAppMappings = require('keyboard.hyper-apps-defaults') end +-- Create a hotkey that will enter Hyper Mode when 'alt' is tapped (i.e., +-- when 'alt' is pressed and then released in quick succession). +local hotkey = require('keyboard.tap-modifier-for-hotkey') +hyperMode = hotkey.new('alt') + +-- Bind the hotkeys that will be active when we're in Hyper Mode for i, mapping in ipairs(hyperModeAppMappings) do local key = mapping[1] local app = mapping[2] - hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function() + hyperMode:bind(key, function() if (type(app) == 'string') then hs.application.open(app) elseif (type(app) == 'function') then @@ -17,3 +24,16 @@ for i, mapping in ipairs(hyperModeAppMappings) do end end) end + +-- Show a status message when we're in Hyper Mode +local message = require('keyboard.status-message') +hyperMode.statusMessage = message.new('Hyper Mode') +hyperMode.entered = function() + hyperMode.statusMessage:show() +end +hyperMode.exited = function() + hyperMode.statusMessage:hide() +end + +-- We're all set. Now we just enable Hyper Mode and get to work. 👔 +hyperMode:enable() diff --git a/hammerspoon/tap-modifier-for-hotkey.lua b/hammerspoon/tap-modifier-for-hotkey.lua new file mode 100644 index 00000000..7dbff408 --- /dev/null +++ b/hammerspoon/tap-modifier-for-hotkey.lua @@ -0,0 +1,180 @@ +local eventtap = require('hs.eventtap') +local events = eventtap.event.types + +local modal={} + +-- Return an object whose behavior is inspired by hs.hotkey.modal. In this case, +-- the modal state is entered when the specified modifier key is tapped (i.e., +-- pressed and then released in quick succession). +modal.new = function(modifier) + local instance = { + modifier = modifier, + + modalStateTimeoutInSeconds = 1.0, + + modalKeybindings = {}, + + inModalState = false, + + reset = function(self) + -- Keep track of the three most recent events. + self.eventHistory = { + -- Serialize the event and push it into the history + push = function(self, event) + self[3] = self[2] + self[2] = self[1] + self[1] = event:asData() + end, + + -- Fetch the event (if any) at the given index + fetch = function(self, index) + if self[index] then + return eventtap.event.newEventFromData(self[index]) + end + end + } + + return self + end, + + -- Enable the modal + -- + -- Mimics hs.modal:enable() + enable = function(self) + self.watcher:start() + end, + + -- Disable the modal + -- + -- Mimics hs.modal:disable() + disable = function(self) + self.watcher:stop() + self.watcher:reset() + end, + + -- Temporarily enter the modal state in which the modal's hotkeys are + -- active. The modal state will terminate after `modalStateTimeoutInSeconds` + -- or after the first keydown event, whichever comes first. + -- + -- Mimics hs.modal.modal:enter() + enter = function(self) + self.inModalState = true + self:entered() + self.autoExitTimer:setNextTrigger(self.modalStateTimeoutInSeconds) + end, + + -- Exit the modal state in which the modal's hotkey are active + -- + -- Mimics hs.modal.modal:exit() + exit = function(self) + if not self.inModalState then return end + + self.autoExitTimer:stop() + self.inModalState = false + self:reset() + self:exited() + end, + + -- Optional callback for when modal state is entered + -- + -- Mimics hs.modal.modal:entered() + entered = function(self) end, + + -- Optional callback for when modal state is exited + -- + -- Mimics hs.modal.modal:exited() + exited = function(self) end, + + -- Bind hotkey that will be enabled/disabled as modal state is + -- entered/exited + bind = function(self, key, fn) + self.modalKeybindings[key] = fn + end, + } + + isNoModifiers = function(flags) + local isFalsey = function(value) + return not value + end + + return hs.fnutils.every(flags, isFalsey) + end + + isOnlyModifier = function(flags) + isPrimaryModiferDown = flags[modifier] + areOtherModifiersDown = hs.fnutils.some(flags, function(isDown, modifierName) + local isPrimaryModifier = modifierName == modifier + return isDown and not isPrimaryModifier + end) + + return isPrimaryModiferDown and not areOtherModifiersDown + end + + isFlagsChangedEvent = function(event) + return event and event:getType() == events.flagsChanged + end + + isFlagsChangedEventWithNoModifiers = function(event) + return isFlagsChangedEvent(event) and isNoModifiers(event:getFlags()) + end + + isFlagsChangedEventWithOnlyModifier = function(event) + return isFlagsChangedEvent(event) and isOnlyModifier(event:getFlags()) + end + + instance.autoExitTimer = hs.timer.new(0, function() instance:exit() end) + + instance.watcher = eventtap.new({events.flagsChanged, events.keyDown}, + function(event) + -- If we're in the modal state, and we got a keydown event, then trigger + -- the function associated with the key. + if (event:getType() == events.keyDown and instance.inModalState) then + local fn = instance.modalKeybindings[event:getCharacters():lower()] + + -- Some actions may take a while to perform (e.g., opening Slack when + -- it's not yet running). We don't want to keep the modal state active + -- while we wait for a long-running action to complete. So, we schedule + -- the action to run in the background so that we can exit the modal + -- state and let the user go on about their business. + local delayInSeconds = 0.001 -- 1 millisecond + hs.timer.doAfter(delayInSeconds, function() + if fn then fn() end + end) + + instance:exit() + + -- Delete the event so that we're the sole consumer of it + return true + end + + -- Otherwise, determine if this event should cause us to enter the modal + -- state. + + local currentEvent = event + local lastEvent = instance.eventHistory:fetch(1) + local secondToLastEvent = instance.eventHistory:fetch(2) + + instance.eventHistory:push(currentEvent) + + -- If we've observed the following sequence of events, then enter the + -- modal state: + -- + -- 1. No modifiers are down + -- 2. Modifiers changed, and now only the primary modifier is down + -- 3. Modifiers changed, and now no modifiers are down + if (secondToLastEvent == nil or isNoModifiers(secondToLastEvent:getFlags())) and + isFlagsChangedEventWithOnlyModifier(lastEvent) and + isFlagsChangedEventWithNoModifiers(currentEvent) then + + instance:enter() + end + + -- Let the event propagate + return false + end + ) + + return instance:reset() +end + +return modal diff --git a/karabiner/karabiner.json b/karabiner/karabiner.json deleted file mode 100644 index 5ee60c33..00000000 --- a/karabiner/karabiner.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "global": { - "check_for_updates_on_startup": true, - "show_in_menu_bar": false, - "show_profile_name_in_menu_bar": false - }, - "profiles": [ - { - "complex_modifications": { - "parameters": { - "basic.to_delayed_action_delay_milliseconds": 500, - "basic.to_if_alone_timeout_milliseconds": 1000 - }, - "rules": [ - { - "manipulators": [ - { - "description": "Change right option to Hyper (i.e., command+control+option+shift)", - "from": { - "key_code": "right_option", - "modifiers": { - "optional": [ - "any" - ] - } - }, - "to": [ - { - "key_code": "left_shift", - "modifiers": [ - "left_control", - "left_option", - "left_command" - ] - } - ], - "type": "basic" - } - ] - } - ] - }, - "devices": [ - { - "disable_built_in_keyboard_if_exists": false, - "fn_function_keys": [], - "identifiers": { - "is_keyboard": true, - "is_pointing_device": false, - "product_id": 276, - "vendor_id": 4176 - }, - "ignore": true, - "manipulate_caps_lock_led": false, - "simple_modifications": [] - }, - { - "disable_built_in_keyboard_if_exists": false, - "fn_function_keys": [], - "identifiers": { - "is_keyboard": true, - "is_pointing_device": false, - "product_id": 1031, - "vendor_id": 4176 - }, - "ignore": true, - "manipulate_caps_lock_led": false, - "simple_modifications": [] - }, - { - "disable_built_in_keyboard_if_exists": false, - "fn_function_keys": [], - "identifiers": { - "is_keyboard": true, - "is_pointing_device": false, - "product_id": 631, - "vendor_id": 1452 - }, - "ignore": false, - "manipulate_caps_lock_led": false, - "simple_modifications": [] - }, - { - "disable_built_in_keyboard_if_exists": false, - "fn_function_keys": [], - "identifiers": { - "is_keyboard": true, - "is_pointing_device": false, - "product_id": 34304, - "vendor_id": 1452 - }, - "ignore": true, - "manipulate_caps_lock_led": false, - "simple_modifications": [] - }, - { - "disable_built_in_keyboard_if_exists": false, - "fn_function_keys": [], - "identifiers": { - "is_keyboard": true, - "is_pointing_device": false, - "product_id": 0, - "vendor_id": 0 - }, - "ignore": false, - "manipulate_caps_lock_led": false, - "simple_modifications": [] - } - ], - "fn_function_keys": [ - { - "from": { - "key_code": "f1" - }, - "to": { - "key_code": "vk_consumer_brightness_down" - } - }, - { - "from": { - "key_code": "f2" - }, - "to": { - "key_code": "vk_consumer_brightness_up" - } - }, - { - "from": { - "key_code": "f3" - }, - "to": { - "key_code": "vk_mission_control" - } - }, - { - "from": { - "key_code": "f4" - }, - "to": { - "key_code": "vk_launchpad" - } - }, - { - "from": { - "key_code": "f5" - }, - "to": { - "key_code": "vk_consumer_illumination_down" - } - }, - { - "from": { - "key_code": "f6" - }, - "to": { - "key_code": "vk_consumer_illumination_up" - } - }, - { - "from": { - "key_code": "f7" - }, - "to": { - "key_code": "vk_consumer_previous" - } - }, - { - "from": { - "key_code": "f8" - }, - "to": { - "key_code": "vk_consumer_play" - } - }, - { - "from": { - "key_code": "f9" - }, - "to": { - "key_code": "vk_consumer_next" - } - }, - { - "from": { - "key_code": "f10" - }, - "to": { - "key_code": "mute" - } - }, - { - "from": { - "key_code": "f11" - }, - "to": { - "key_code": "volume_down" - } - }, - { - "from": { - "key_code": "f12" - }, - "to": { - "key_code": "volume_up" - } - } - ], - "name": "Default profile", - "selected": true, - "simple_modifications": [ - { - "from": { - "key_code": "caps_lock" - }, - "to": { - "key_code": "left_control" - } - } - ], - "virtual_hid_keyboard": { - "caps_lock_delay_milliseconds": 0, - "keyboard_type": "ansi" - } - } - ] -} diff --git a/script/setup b/script/setup index 276d6402..8c1596c1 100755 --- a/script/setup +++ b/script/setup @@ -6,10 +6,6 @@ which -s brew || (echo "Homebrew is required: http://brew.sh/" && exit 1) brew bundle check || brew bundle -# Prepare custom settings for Karabiner-Elements -# https://github.com/tekezo/Karabiner-Elements/issues/597#issuecomment-282760186 -ln -sfn $PWD/karabiner ~/.config/ - # Prepare custom settings for Hammerspoon mkdir -p ~/.hammerspoon if ! grep -sq "require('keyboard')" ~/.hammerspoon/init.lua; then @@ -27,12 +23,14 @@ defaults write org.hammerspoon.Hammerspoon MJShowDockIconKey -bool FALSE # when opening Hammerspoon below killall Hammerspoon || true -# Open Apps +# Open Hammerspoon open /Applications/Hammerspoon.app -open /Applications/Karabiner-Elements.app -# Enable apps at startup +# Enable Hammerspoon at startup osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/Hammerspoon.app", hidden:true}' > /dev/null -osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/Karabiner-Elements.app", hidden:true}' > /dev/null -echo "Done! Remember to enable Accessibility for Hammerspoon." +echo "Done!" +echo "" +echo "🎗 Remember to enable Accessibility for Hammerspoon." +echo "🎗 Remember to remap Caps Lock to Control." +echo "🎗 See details at https://github.com/jasonrudolph/keyboard#installation"