Hammerspoon bind "cmd" & "shift" without any other key

228 views Asked by At

I want to use hammerspoon to replicate Windows' language switching using Alt and Shift. For this I want to add a hotkey bind for command and shift, but all my attempts fail.

I tried:

hs.hotkey.bind({"cmd", "shift"}, "", function()
    performAction()
end)

and

hs.hotkey.bind({"cmd"}, "shift", function()
    performAction()
end)

But nothing works.

2

There are 2 answers

0
AlonMln On BEST ANSWER

philsnow's answer gave me most of the building blocks I needed, here's the code I used to map cmd -> cmd + shift -> shift -> nothing to switch between languages like in Windows. For anyone needing something similar, this code worked for me to switch toggle languages.

function same_keys(t1, t2)
    return _same_keys_oneway(t1, t2) and _same_keys_oneway(t2, t1)
    end
    
    function _same_keys_oneway(t1, t2)
    for k, _ in pairs(t1) do
        local found = false
        for j, _ in pairs(t2) do
            if k == j then found = true end
        end
        if found == false then return false end
    end
    return true
end

nMinus1 = {}
nMinus2 = {}
nMinus3 = {}

function tablelength(T)
    local count = 0
    for _ in pairs(T) do count = count + 1 end
    return count
end

function switchLanguage()
    local lay = hs.keycodes.currentLayout()
    print(lay)
    if lay == "ABC" then
      hs.keycodes.setLayout("Hebrew – PC")
    else
      hs.keycodes.setLayout("ABC")
    end
end

etap = hs.eventtap.new(
   {
      hs.eventtap.event.types.flagsChanged,
   },
   function(ev)
      local flags = ev:getFlags()

      if tablelength(flags) == 0 then
         if (same_keys(nMinus1, {shift = true}) 
                and same_keys(nMinus2, {cmd = true, shift = true})
                and same_keys(nMinus3, {cmd = true})) then
            switchLanguage()
         end
      end
      nMinus3 = nMinus2
      nMinus2 = nMinus1
      nMinus1 = flags
   end
):start()
1
philsnow On

hs.hotkey is keystroke-specific and doesn't seem to consider mods going up or down as keystrokes. hs.hotkey.bind eventually [0] calls hotkey._new from libhotkey.m (meaning this implementation is baked into the Hammerspoon binary, so changing it would be harder).

On the other hand, hs.eventtap lets you see all the modifier changes and react to them.

hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, function callback(e) ... end)

... will call your callback every time the modifiers change.

I assume you don't want to performAction when the modifier sequence goes none -> ctrl -> ctrl+shift -> ctrl+shift+cmd -> shift+cmd, that is, you only want to performAction when going from shift -> shift+cmd or cmd -> shift+cmd. So, you need to remember what the previous set of modifiers was and compare it to the one you see in your callback.

etap_last_flags = {}
etap = hs.eventtap.new(
   {
      hs.eventtap.event.types.flagsChanged,
   },
   function(ev)
      local flags = ev:getFlags()

      if same_keys(flags, {cmd = true, shift = true}) then
         if (same_keys(etap_last_flags, {cmd = true}) or
             same_keys(etap_last_flags, {shift = true})) then
            print("do the thing")
            -- performAction()
         end
      end

      etap_last_flags = flags
   end
):start()

note that this does performAction() right when you hit/keyDown the second modifier key, not when you release/keyUp it. You could do the latter if you want, by keeping track of more history in etap_last_flags.

ev:getFlags() returns a table with all the modifiers that are currently set as keys, all with value true, so like {cmd = true, shift = true}. You can't just compare those with == in lua, so I wrote a janky compare for just this one case:

-- This is not a robust table comparison, see
-- http://lua-users.org/wiki/CompareTables if you want that.  This is
-- fine for checking whether two tables of the form {foo = true} and
-- {bar = true} have the same keys.

function same_keys(t1, t2)
   return _same_keys_oneway(t1, t2) and _same_keys_oneway(t2, t1)
end

function _same_keys_oneway(t1, t2)
   for k, _ in pairs(t1) do
      local found = false
      for j, _ in pairs(t2) do
         if k == j then found = true end
      end
      if found == false then return false end
   end
   return true
end

Of course another possibility is to use Karabiner-Elements or similar to synthetically emit a plain keystroke like f18 when shift and cmd are tapped together, and then just hs.hotkey.bind({{}, 'f18'}, ...).

[0] https://github.com/Hammerspoon/hammerspoon/blob/0ccc9d07641a660140d1d2f05b76f682b501a0e8/extensions/hotkey/hotkey.lua#L217