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