Skip to content

Commit

Permalink
Implement Hyper Mode exclusively with Hammerspoon
Browse files Browse the repository at this point in the history
- Hyper Mode is now activated by simultaneously pressing the 'k' and 'l'
  keys and holding them down. This is similar to activating (S)uper
  (D)uper Mode by simultaneously pressing the 's' and 'd' keys and
  holding them down.
- Hyper Mode is no longer activated by pressing the right option key.
- The format of the Hyper Mode key-to-app mappings has changed.
  See hammerspoon/hyper-apps-defaults.lua.
  • Loading branch information
jasonrudolph committed May 5, 2018
1 parent 8dc9812 commit b6e7d24
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 44 deletions.
18 changes: 9 additions & 9 deletions hammerspoon/hyper-apps-defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
-- this file, save it as `hyper-apps.lua`, and edit the table below to configure
-- your preferred shortcuts.
return {
{ 'a', 'iTunes' }, -- "A" for "Apple Music"
{ 'b', 'Google Chrome' }, -- "B" for "Browser"
{ 'c', 'Slack' }, -- "C for "Chat"
{ 'd', 'Remember The Milk' }, -- "D" for "Do!" ... or "Done!"
{ 'e', 'Atom' }, -- "E" for "Editor"
{ 'f', 'Finder' }, -- "F" for "Finder"
{ 'g', 'Mailplane 3' }, -- "G" for "Gmail"
{ 's', 'Slack' }, -- "S" for "Slack"
{ 't', 'iTerm' }, -- "T" for "Terminal"
a = 'iTunes', -- "A" for "Apple Music"
b = 'Google Chrome', -- "B" for "Browser"
c = 'Slack', -- "C for "Chat"
d = 'Remember The Milk', -- "D" for "Do!" ... or "Done!"
e = 'Atom', -- "E" for "Editor"
f = 'Finder', -- "F" for "Finder"
g = 'Mailplane 3', -- "G" for "Gmail"
s = 'Slack', -- "S" for "Slack"
t = 'iTerm', -- "T" for "Terminal"
}
35 changes: 28 additions & 7 deletions hammerspoon/hyper.lua
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
local status, hyperModeAppMappings = pcall(require, 'keyboard.hyper-apps')
local eventtap = hs.eventtap
local eventTypes = hs.eventtap.event.types
local simultaneousKeypressModal = require('keyboard.simultaneous-keypress-modal')

-- 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

for i, mapping in ipairs(hyperModeAppMappings) do
local key = mapping[1]
local app = mapping[2]
hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function()
-- Create a hotkey that will enter Hyper Mode when 'k' and 'l' are pressed
-- simultaneously.
hyperMode = simultaneousKeypressModal.new('Hyper Mode', 'k', 'l')

--------------------------------------------------------------------------------
-- Watch for key-down events in Hyper Mode, and trigger app associated with the
-- given key.
--------------------------------------------------------------------------------
hyperModeKeyListener = eventtap.new({ eventTypes.keyDown }, function(event)
if not hyperMode:isActive() then
return false
end

local app = hyperModeAppMappings[event:getCharacters(true):lower()]

if app then
if (type(app) == 'string') then
hs.application.open(app)
elseif (type(app) == 'function') then
app()
else
hs.logger.new('hyper'):e('Invalid mapping for Hyper +', key)
end
end)
end
return true
end
end):start()

--- We're all set. Now we just enable Hyper Mode and get to work. 👔
hyperMode:enable()
151 changes: 151 additions & 0 deletions hammerspoon/simultaneous-keypress-modal.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
local eventtap = hs.eventtap
local eventTypes = hs.eventtap.event.types
local message = require('keyboard.status-message')

local modal={}

modal.new = function(name, key1, key2)
local instance = {
-- The two keys that must be pressed simultaneously to enter the modal state
key1 = key1,
key2 = key2,

-- If key1 and key2 are *both* pressed within this time period, consider
-- this to mean that they've been pressed simultaneously, and therefore we
-- should enter the modal state.
maxTimeBetweenSimultaneousKeypresses = 0.04, -- 40 milliseconds

-- The status message to display when the modal state is active
statusMessage = message.new(name),

-- Resets object to initial state
reset = function(self)
self.active = false
self.isKey1Down = false
self.isKey2Down = false
self.ignoreNextKey1 = false
self.ignoreNextKey2 = false
self.statusMessage:hide()

return self
end,

-- Are we in the modal state?
isActive = function(self)
return self.active
end,

-- Enters the modal state
--
-- Mimics hs.hotkey.modal:enter()
enter = function(self)
if self.active then return end

self.statusMessage:show()
self.active = true
self:entered()
end,

-- Exits the modal state
--
-- Mimics hs.hotkey.modal:exit()
exit = function(self)
if not self.active then return end

self:reset()
self:exited()
end,

-- Optional callback for when a modal state is entered
--
-- Mimics hs.hotkey.modal:entered()
entered = function(self) end,

-- Optional callback for when a modal state is exited
--
-- Mimics hs.hotkey.modal:exited()
exited = function(self) end,

-- Enable the simultaneous keypress hotkey
--
-- Mimics hs.hotkey:enable()
enable = function(self)
self.activationListener:start()
self.deactivationListener:start()
end,

-- Diable the simultaneous keypress hotkey
--
-- Mimics hs.hotkey:disable()
disable = function(self)
self:exit()
self.activationListener:stop()
self.deactivationListener:stop()
end,
}

instance.activationListener = eventtap.new({ eventTypes.keyDown }, function(event)
-- If key1 or key2 is pressed in conjuction with any modifier keys
-- (e.g., command + key1), then we're not activating the modal state.
if not (next(event:getFlags()) == nil) then
return false
end

local characters = event:getCharacters()

if characters == instance.key1 then
if instance.ignoreNextKey1 then
instance.ignoreNextKey1 = false
return false
end
-- Temporarily suppress this key1 keystroke. At this point, we're not sure
-- if the user intends to type key1, or if the user is attempting to
-- activate the modal state. If key2 is pressed by the time the following
-- function executes, then activate the modal state. Otherwise, trigger an
-- ordinary key1 keystroke.
instance.isKey1Down = true
hs.timer.doAfter(instance.maxTimeBetweenSimultaneousKeypresses, function()
if instance.isKey2Down then
instance:enter()
else
instance.ignoreNextKey1 = true
keyUpDown({}, instance.key1)
return false
end
end)
return true
elseif characters == instance.key2 then
if instance.ignoreNextKey2 then
instance.ignoreNextKey2 = false
return false
end
-- Temporarily suppress this key2 keystroke. At this point, we're not sure
-- if the user intends to type key2, or if the user is attempting to
-- activate the modal state. If key1 is pressed by the time the following
-- function executes, then activate the modal state. Otherwise, trigger an
-- ordinary key2 keystroke.
instance.isKey2Down = true
hs.timer.doAfter(instance.maxTimeBetweenSimultaneousKeypresses, function()
if instance.isKey1Down then
instance:enter()
else
instance.ignoreNextKey2 = true
keyUpDown({}, instance.key2)
return false
end
end)
return true
end
end)

instance.deactivationListener = eventtap.new({ eventTypes.keyUp }, function(event)
local characters = event:getCharacters()
if characters == instance.key1 or characters == instance.key2 then
instance:reset()
end
end)

return instance:reset()
end

return modal
29 changes: 1 addition & 28 deletions karabiner/karabiner.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,7 @@
"basic.to_if_alone_timeout_milliseconds": 1000,
"basic.to_if_held_down_threshold_milliseconds": 500
},
"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"
}
]
}
]
"rules": []
},
"devices": [
{
Expand Down

0 comments on commit b6e7d24

Please sign in to comment.