Skip to content

Commit bd2f8ff

Browse files
authored
Merge pull request #38 from jasonrudolph/🔥-karabiner
Remove dependency on Karabiner-Elements
2 parents 3601e5e + 1799676 commit bd2f8ff

File tree

6 files changed

+228
-254
lines changed

6 files changed

+228
-254
lines changed

‎Brewfile‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
tap 'caskroom/cask'
22

3-
cask 'karabiner-elements'
43
cask 'hammerspoon'

‎README.md‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ While I find that these customizations yield a more-useful keyboard for me, they
4040
- [Arrange windows via the home row](#window-layout-mode)
4141
- [Enable other commonly-used actions on or near the home row](#miscellaneous-goodness)
4242
- [Format text as Markdown](#markdown-mode)
43-
- [Launch commonly-used apps via global keyboard shortcuts](#hyper-key-for-quickly-launching-apps)
43+
- [Launch commonly-used apps via global keyboard shortcuts](#hyper-mode-for-quickly-launching-apps)
4444
- [And more...](#miscellaneous-goodness)
4545

4646
### A more useful caps lock key
@@ -128,27 +128,30 @@ Use <kbd>control</kbd> + <kbd>m</kbd> to turn on Markdown Mode. Then, use any sh
128128

129129
- Use <kbd>control</kbd> + <kbd>m</kbd> to exit Markdown Mode without performing any actions
130130

131-
### Hyper key for quickly launching apps
131+
### Hyper Mode for quickly launching apps
132132

133-
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.
133+
Launch your favorite apps with global shortcuts, and do so without interfering with existing macOS shortcuts or application-specific shortcuts. 😅
134134

135-
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.
135+
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:
136+
137+
1. Tap the <kbd>option</kbd> key (i.e., press and then release it in quick succession) to enter Hyper Mode
138+
2. Then, press <kbd>f</kbd> for Finder
136139

137140
#### Choose your own apps
138141

139142
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.
140143

141144
#### Default app keybindings
142145

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

153156
### Miscellaneous goodness
154157

@@ -164,8 +167,7 @@ Hyper Mode ships with the default keybindings below, but you'll likely want to p
164167
This setup is honed and tested with the following dependencies.
165168

166169
- macOS High Sierra, 10.13
167-
- [Karabiner-Elements 11.4.0][karabiner]
168-
- [Hammerspoon 0.9.57][hammerspoon]
170+
- [Hammerspoon 0.9.66][hammerspoon]
169171

170172
## Installation
171173

@@ -181,6 +183,8 @@ This setup is honed and tested with the following dependencies.
181183

182184
2. Enable accessibility to allow Hammerspoon to do its thing [[screenshot]](screenshots/accessibility-permissions-for-hammerspoon.png)
183185

186+
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)
187+
184188
## TODO
185189

186190
- 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.
189193

190194
[customize]: http://dictionary.reference.com/browse/customize
191195
[don't-make-me-think]: http://en.wikipedia.org/wiki/Don't_Make_Me_Think
192-
[karabiner]: https://github.com/tekezo/Karabiner-Elements
193196
[hammerspoon]: http://www.hammerspoon.org
194197
[hammerspoon-releases]: https://github.com/Hammerspoon/hammerspoon/releases
195198
[modern-space-cadet]: http://stevelosh.com/blog/2012/10/a-modern-space-cadet

‎hammerspoon/hyper.lua‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
-- Look for custom Hyper Mode app mappings. If there are none, then use the
2+
-- default mappings.
13
local status, hyperModeAppMappings = pcall(require, 'keyboard.hyper-apps')
2-
34
if not status then
45
hyperModeAppMappings = require('keyboard.hyper-apps-defaults')
56
end
67

8+
-- Create a hotkey that will enter Hyper Mode when 'alt' is tapped (i.e.,
9+
-- when 'alt' is pressed and then released in quick succession).
10+
local hotkey = require('keyboard.tap-modifier-for-hotkey')
11+
hyperMode = hotkey.new('alt')
12+
13+
-- Bind the hotkeys that will be active when we're in Hyper Mode
714
for i, mapping in ipairs(hyperModeAppMappings) do
815
local key = mapping[1]
916
local app = mapping[2]
10-
hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function()
17+
hyperMode:bind(key, function()
1118
if (type(app) == 'string') then
1219
hs.application.open(app)
1320
elseif (type(app) == 'function') then
@@ -17,3 +24,16 @@ for i, mapping in ipairs(hyperModeAppMappings) do
1724
end
1825
end)
1926
end
27+
28+
-- Show a status message when we're in Hyper Mode
29+
local message = require('keyboard.status-message')
30+
hyperMode.statusMessage = message.new('Hyper Mode')
31+
hyperMode.entered = function()
32+
hyperMode.statusMessage:show()
33+
end
34+
hyperMode.exited = function()
35+
hyperMode.statusMessage:hide()
36+
end
37+
38+
-- We're all set. Now we just enable Hyper Mode and get to work. 👔
39+
hyperMode:enable()
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
local eventtap = require('hs.eventtap')
2+
local events = eventtap.event.types
3+
4+
local modal={}
5+
6+
-- Return an object whose behavior is inspired by hs.hotkey.modal. In this case,
7+
-- the modal state is entered when the specified modifier key is tapped (i.e.,
8+
-- pressed and then released in quick succession).
9+
modal.new = function(modifier)
10+
local instance = {
11+
modifier = modifier,
12+
13+
modalStateTimeoutInSeconds = 1.0,
14+
15+
modalKeybindings = {},
16+
17+
inModalState = false,
18+
19+
reset = function(self)
20+
-- Keep track of the three most recent events.
21+
self.eventHistory = {
22+
-- Serialize the event and push it into the history
23+
push = function(self, event)
24+
self[3] = self[2]
25+
self[2] = self[1]
26+
self[1] = event:asData()
27+
end,
28+
29+
-- Fetch the event (if any) at the given index
30+
fetch = function(self, index)
31+
if self[index] then
32+
return eventtap.event.newEventFromData(self[index])
33+
end
34+
end
35+
}
36+
37+
return self
38+
end,
39+
40+
-- Enable the modal
41+
--
42+
-- Mimics hs.modal:enable()
43+
enable = function(self)
44+
self.watcher:start()
45+
end,
46+
47+
-- Disable the modal
48+
--
49+
-- Mimics hs.modal:disable()
50+
disable = function(self)
51+
self.watcher:stop()
52+
self.watcher:reset()
53+
end,
54+
55+
-- Temporarily enter the modal state in which the modal's hotkeys are
56+
-- active. The modal state will terminate after `modalStateTimeoutInSeconds`
57+
-- or after the first keydown event, whichever comes first.
58+
--
59+
-- Mimics hs.modal.modal:enter()
60+
enter = function(self)
61+
self.inModalState = true
62+
self:entered()
63+
self.autoExitTimer:setNextTrigger(self.modalStateTimeoutInSeconds)
64+
end,
65+
66+
-- Exit the modal state in which the modal's hotkey are active
67+
--
68+
-- Mimics hs.modal.modal:exit()
69+
exit = function(self)
70+
if not self.inModalState then return end
71+
72+
self.autoExitTimer:stop()
73+
self.inModalState = false
74+
self:reset()
75+
self:exited()
76+
end,
77+
78+
-- Optional callback for when modal state is entered
79+
--
80+
-- Mimics hs.modal.modal:entered()
81+
entered = function(self) end,
82+
83+
-- Optional callback for when modal state is exited
84+
--
85+
-- Mimics hs.modal.modal:exited()
86+
exited = function(self) end,
87+
88+
-- Bind hotkey that will be enabled/disabled as modal state is
89+
-- entered/exited
90+
bind = function(self, key, fn)
91+
self.modalKeybindings[key] = fn
92+
end,
93+
}
94+
95+
isNoModifiers = function(flags)
96+
local isFalsey = function(value)
97+
return not value
98+
end
99+
100+
return hs.fnutils.every(flags, isFalsey)
101+
end
102+
103+
isOnlyModifier = function(flags)
104+
isPrimaryModiferDown = flags[modifier]
105+
areOtherModifiersDown = hs.fnutils.some(flags, function(isDown, modifierName)
106+
local isPrimaryModifier = modifierName == modifier
107+
return isDown and not isPrimaryModifier
108+
end)
109+
110+
return isPrimaryModiferDown and not areOtherModifiersDown
111+
end
112+
113+
isFlagsChangedEvent = function(event)
114+
return event and event:getType() == events.flagsChanged
115+
end
116+
117+
isFlagsChangedEventWithNoModifiers = function(event)
118+
return isFlagsChangedEvent(event) and isNoModifiers(event:getFlags())
119+
end
120+
121+
isFlagsChangedEventWithOnlyModifier = function(event)
122+
return isFlagsChangedEvent(event) and isOnlyModifier(event:getFlags())
123+
end
124+
125+
instance.autoExitTimer = hs.timer.new(0, function() instance:exit() end)
126+
127+
instance.watcher = eventtap.new({events.flagsChanged, events.keyDown},
128+
function(event)
129+
-- If we're in the modal state, and we got a keydown event, then trigger
130+
-- the function associated with the key.
131+
if (event:getType() == events.keyDown and instance.inModalState) then
132+
local fn = instance.modalKeybindings[event:getCharacters():lower()]
133+
134+
-- Some actions may take a while to perform (e.g., opening Slack when
135+
-- it's not yet running). We don't want to keep the modal state active
136+
-- while we wait for a long-running action to complete. So, we schedule
137+
-- the action to run in the background so that we can exit the modal
138+
-- state and let the user go on about their business.
139+
local delayInSeconds = 0.001 -- 1 millisecond
140+
hs.timer.doAfter(delayInSeconds, function()
141+
if fn then fn() end
142+
end)
143+
144+
instance:exit()
145+
146+
-- Delete the event so that we're the sole consumer of it
147+
return true
148+
end
149+
150+
-- Otherwise, determine if this event should cause us to enter the modal
151+
-- state.
152+
153+
local currentEvent = event
154+
local lastEvent = instance.eventHistory:fetch(1)
155+
local secondToLastEvent = instance.eventHistory:fetch(2)
156+
157+
instance.eventHistory:push(currentEvent)
158+
159+
-- If we've observed the following sequence of events, then enter the
160+
-- modal state:
161+
--
162+
-- 1. No modifiers are down
163+
-- 2. Modifiers changed, and now only the primary modifier is down
164+
-- 3. Modifiers changed, and now no modifiers are down
165+
if (secondToLastEvent == nil or isNoModifiers(secondToLastEvent:getFlags())) and
166+
isFlagsChangedEventWithOnlyModifier(lastEvent) and
167+
isFlagsChangedEventWithNoModifiers(currentEvent) then
168+
169+
instance:enter()
170+
end
171+
172+
-- Let the event propagate
173+
return false
174+
end
175+
)
176+
177+
return instance:reset()
178+
end
179+
180+
return modal

0 commit comments

Comments
 (0)