Skip to content

Commit

Permalink
Merge pull request jasonrudolph#38 from jasonrudolph/🔥-karabiner
Browse files Browse the repository at this point in the history
Remove dependency on Karabiner-Elements
  • Loading branch information
jasonrudolph authored Jun 22, 2018
2 parents 3601e5e + 1799676 commit bd2f8ff
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 254 deletions.
1 change: 0 additions & 1 deletion Brewfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
tap 'caskroom/cask'

cask 'karabiner-elements'
cask 'hammerspoon'
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -128,27 +128,30 @@ Use <kbd>control</kbd> + <kbd>m</kbd> to turn on Markdown Mode. Then, use any sh

- Use <kbd>control</kbd> + <kbd>m</kbd> 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 <kbd>hyper</kbd> key. But thanks to Karabiner-Elements, we can [create our own](karabiner/karabiner.json). In this setup, we'll use the <kbd>right option</kbd> key as our <kbd>hyper</kbd> 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 <kbd>option</kbd> (AKA <kbd>alt</kbd>) 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 <kbd>option</kbd> key (i.e., press and then release it in quick succession) to enter Hyper Mode
2. Then, press <kbd>f</kbd> for Finder

#### Choose your own apps

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.

#### Default app keybindings

- <kbd>hyper</kbd> + <kbd>a</kbd> to open iTunes ("A" for "Apple Music")
- <kbd>hyper</kbd> + <kbd>b</kbd> to open Google Chrome ("B" for "Browser")
- <kbd>hyper</kbd> + <kbd>c</kbd> to open Slack ("C for "Chat")
- <kbd>hyper</kbd> + <kbd>d</kbd> to open [Remember The Milk](https://www.rememberthemilk.com/) ("D" for "Do!" ... or "Done!")
- <kbd>hyper</kbd> + <kbd>e</kbd> to open [Atom](https://atom.io) ("E" for "Editor")
- <kbd>hyper</kbd> + <kbd>f</kbd> to open Finder ("F" for "Finder")
- <kbd>hyper</kbd> + <kbd>g</kbd> to open [Mailplane](http://mailplaneapp.com/) ("G" for "Gmail")
- <kbd>hyper</kbd> + <kbd>s</kbd> to open [Slack](https://slack.com/downloads/osx) ("S" for "Slack")
- <kbd>hyper</kbd> + <kbd>t</kbd> to open [iTerm2](https://www.iterm2.com/) ("T" for "Terminal")
- <kbd>a</kbd> to open iTunes ("A" for "Apple Music")
- <kbd>b</kbd> to open Google Chrome ("B" for "Browser")
- <kbd>c</kbd> to open Slack ("C for "Chat")
- <kbd>d</kbd> to open [Remember The Milk](https://www.rememberthemilk.com/) ("D" for "Do!" ... or "Done!")
- <kbd>e</kbd> to open [Atom](https://atom.io) ("E" for "Editor")
- <kbd>f</kbd> to open Finder ("F" for "Finder")
- <kbd>g</kbd> to open [Mailplane](http://mailplaneapp.com/) ("G" for "Gmail")
- <kbd>s</kbd> to open [Slack](https://slack.com/downloads/osx) ("S" for "Slack")
- <kbd>t</kbd> to open [iTerm2](https://www.iterm2.com/) ("T" for "Terminal")

### Miscellaneous goodness

Expand All @@ -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

Expand All @@ -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 <kbd>caps lock</kbd> key](#a-more-useful-caps-lock-key): Open *System Preferences*, navigate to *Keyboard > Modifier Keys*, and set the <kbd>caps lock</kbd> key to <kbd>control</kbd> [[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):
Expand All @@ -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
Expand Down
24 changes: 22 additions & 2 deletions hammerspoon/hyper.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
180 changes: 180 additions & 0 deletions hammerspoon/tap-modifier-for-hotkey.lua
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit bd2f8ff

Please sign in to comment.