chd/.config/hammerspoon/modalHotKey.lua

149 lines
5.7 KiB
Lua

--[[ modalHotKey.lua
Adapted from https://github.com/asmagill/hammerspoon-config/blob/master/_scratch/modalSuppression.lua
see https://github.com/Hammerspoon/hammerspoon/issues/1505
save file somewhere then type `modalHotKey = dofile("modalHotKey.lua")` to load it
Here's an example based on my usage:
appCuts = {
e = 'Emacs',
f = 'Finder',
j = 'Cisco Jabber',
k = 'Keychain Access',
o = 'Microsoft Outlook',
s = 'Safari',
t = 'Terminal',
}
appActionTable = {}
for key, app in pairs(appCuts) do
appActionTable[key] = function() hs.application.launchOrFocus(app) end
end
modalHotKey = dofile("modalHotKey.lua")
appModal = modalHotKey.new(
hs.hotkey.modal.new({"cmd", "ctrl"}, "t"),
appActionTable,
appCuts,
"Special Application Switcher"
)
*only* the recognized key sequences will be allowed through -- this means you can't even quit hammerspoon with Cmd-Q
without tapping escape first.
--]]
-- suppressKeysOtherThanOurs(): Create custom eventtap to suppress unwanted keys and pass through the ones we do want
-- modal: An object created from module.new() below (not just a modal hotkey from hs.hotkey.modal.new(); requires our extension methods)
local suppressKeysOtherThanOurs = function(modal)
local passThroughKeys = {}
-- this is annoying because the event's raw flag bitmasks differ from the bitmasks used by hotkey, so
-- we have to convert here for the lookup
for i,v in ipairs(modal.keys) do
-- parse for flags, get keycode for each
local kc, mods = tostring(v._hk):match("keycode: (%d+), mods: (0x[^ ]+)")
local hkFlags = tonumber(mods)
local hkOriginal = hkFlags
local flags = 0
if (hkFlags & 256) == 256 then hkFlags, flags = hkFlags - 256, flags | hs.eventtap.event.rawFlagMasks.command end
if (hkFlags & 512) == 512 then hkFlags, flags = hkFlags - 512, flags | hs.eventtap.event.rawFlagMasks.shift end
if (hkFlags & 2048) == 2048 then hkFlags, flags = hkFlags - 2048, flags | hs.eventtap.event.rawFlagMasks.alternate end
if (hkFlags & 4096) == 4096 then hkFlags, flags = hkFlags - 4096, flags | hs.eventtap.event.rawFlagMasks.control end
if hkFlags ~= 0 then print("unexpected flag pattern detected for " .. tostring(v._hk)) end
passThroughKeys[tonumber(kc)] = flags
end
return hs.eventtap.new({
hs.eventtap.event.types.keyDown,
hs.eventtap.event.types.keyUp,
}, function(event)
-- check only the flags we care about and filter the rest
local flags = event:getRawEventData().CGEventData.flags & (
hs.eventtap.event.rawFlagMasks.command |
hs.eventtap.event.rawFlagMasks.control |
hs.eventtap.event.rawFlagMasks.alternate |
hs.eventtap.event.rawFlagMasks.shift
)
if passThroughKeys[event:getKeyCode()] == flags then
hs.printf("passing: %3d 0x%08x", event:getKeyCode(), flags)
return false -- pass it through so hotkey can catch it
else
hs.printf("suppressing: %3d 0x%08x", event:getKeyCode(), flags)
modal:exitWithMessage("Invalid modal key " .. event:getKeyCode() .. " exiting mode")
return true -- delete it if we got this far -- it's a key that we want suppressed
end
end)
end
local module = {}
-- new(): Create a new modal hotkey
-- triggerKey: A table created by invoking hs.hotkey.modal.new(), like hs.hotkey.modal.new({"cmd", "ctrl"}, "t")
-- actionTable: A table of {subKey = action} pairs
-- actionDescTable: A table of {subKey = description} pairs
-- modalMessagePrefix: A message prefix to display when communicating to the user about this hot key
module.new = function(triggerKey, actionTable, actionDescTable, modalMessagePrefix)
local modality = {}
modality.activeAlert = nil
modality.alertMessage = modalMessagePrefix .. "\n========\n"
-- Create a table of index->subKey mappings
-- This lets us alphabetize our hotkeys for display
alphaKeys = {}
for subKey, _ in pairs(actionDescTable) do
table.insert(alphaKeys, subKey)
end
table.sort(alphaKeys)
-- Show a menu of allowed subKey presses
doubleTab = " "
for idx, subKey in pairs(alphaKeys) do
desc = actionDescTable[subKey]
modality.alertMessage = modality.alertMessage .. "\n" .. subKey .. ":" .. doubleTab .. desc
end
triggerKey.exitWithMessage = function(self, message)
hs.alert(modalMessagePrefix .. "\n========\n" .. message)
self:exit()
end
triggerKey.entered = function(self)
self._eventtap = suppressKeysOtherThanOurs(self):start()
modality.activeAlert = hs.alert.show(modality.alertMessage, {}, hs.screen.mainScreen(), "forever")
end
triggerKey.exited = function(self)
self._eventtap:stop()
self._eventtap = nil
hs.alert.closeSpecific(modality.activeAlert)
end
-- define an explicit way out
triggerKey:bind({}, "escape", function() triggerKey:exit() end)
for subKey, action in pairs(actionTable) do
triggerKey:bind({}, subKey, function()
action()
triggerKey:exit()
end)
end
modality.triggerKey = triggerKey
modality.start = function(self)
if self.triggerKey._eventtap then
self.triggerKey:enter()
end
end
modality.stop = function(self)
if self.triggerKey._eventtap then
self.triggerKey:exit()
end
end
modality.started = function(self)
return not not self.triggerKey._eventtap
end
return modality
end
return module