Hammerspoon: Remap key w/ all modifier combinations

Created on 4 Apr 2017  路  3Comments  路  Source: Hammerspoon/hammerspoon

I'm using the following in an attempt to emulate what I had w/ Karabiner to add a symbol layer to my keyboard. I use karabiner-elements to map Caps Lock to F18 and then use this to map tapping capslock to escape and holding and pressing another key (like Caps Lock+F -> ()

This works pretty well except for a couple exceptions. I don't want to list them all in one issue, so I'll list the first here:

If I press Ctrl+Caps Lock I end up with Ctrl+F18 when I really want Ctrl+ESC. I could remap that explicitly, but then I need to do the same for Cmd+Caps Lock and all combinations of modifiers.

Is there a way to sort of "pass through" the modifier keys for a binding? I'd also like this to apply to the symbols, so if I hit Ctrl+Caps Lock+F I'd get Ctrl+(

Most helpful comment

@rootscript thank you for the suggestion. This wasn't exactly what I needed, but it did point me in the right direction. I ended up using eventtap.new to create my bindings so I could listen to all modifiers and forward them on.

Here is is what I ended up with: https://github.com/aaronjensen/dotfiles/blob/9bcf07ebaf3ef7f597c37daefaae78d5492e13a0/hammerspoon/symbol_layer.lua


Symbol layer

-- The default delay is too slow, making key repeating very slow
fast_delay = 1000

function tableContainsValue(tbl, value)
  for _, v in ipairs(tbl) do
    if v == value then
      return true
    end
  end

  return false
end

function itableUnion(a, b)
  local ab = {}
  for _, v in ipairs(a) do
    table.insert(ab, v)
  end

  for _, v in ipairs(b) do
    if not tableContainsValue(ab, v) then
      table.insert(ab, v)
    end
  end

  return ab
end

function tableKeys(tbl)
  local ret = {}
  for key, _ in pairs(tbl) do
    table.insert(ret, key)
  end

  return ret
end

function enableSymbolLayer()
  local symbols = {
    o = {{}, '-'},            -- o -> -
    p = {{'shift'}, '='},     -- p -> +
    s = {{}, '['},            -- s -> [
    d = {{'shift'}, '['},     -- d -> {
    f = {{'shift'}, '9'},     -- f -> (
    g = {{}, '='},            -- g -> =
    h = {{'shift'}, '-'},     -- h -> _
    j = {{'shift'}, '0'},     -- j -> )
    k = {{'shift'}, ']'},     -- k -> }
    l = {{}, ']'},            -- l -> ]
    c = {{'shift'}, '`'},     -- c -> ~
    v = {{}, '`'},            -- v -> `
    m = {{'shift'}, '\\'},    -- m -> |
    [','] = {{'shift'}, ','}, -- , -> <
    ['.'] = {{'shift'}, '.'}, -- . -> >
  }

  -- If you hold capslock for this long and then let go, ESC will not be sent.
  local cancel_delay_seconds = 0.3
  local triggered = true
  local resetTimer = hs.timer.delayed.new(cancel_delay_seconds, function()
    triggered = true
  end)

  -- This tap handles remapping. It is only enabled while capslock is held
  local tap = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
    -- Check if this event is from the keyboard or from us. We need to ignore
    -- events from us.
    local processId = event:getProperty(hs.eventtap.event.properties.eventSourceUnixProcessID)
    if processId > 0 then
      return false
    end

    triggered = true

    local key = hs.keycodes.map[event:getKeyCode()]
    local remap = symbols[key]
    if remap then
      local mods = tableKeys(event:getFlags())
      local newMods = itableUnion(mods, remap[1])
      local newKey = remap[2]

      hs.eventtap.keyStroke(newMods, newKey, fast_delay)
    end

    -- Do not send event on
    return true
  end)

  -- This tap handles F18 (capslock) with any modifier and is enabled right away.
  hs.eventtap.new({hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, function(event)
    local key = hs.keycodes.map[event:getKeyCode()]
    -- Ignore all events but f18
    if key ~= 'f18' then
      return false
    end

    if event:getType() == hs.eventtap.event.types.keyDown then
      resetTimer:start()
      triggered = false
      tap:start()
    else
      resetTimer:stop()
      tap:stop()

      if not triggered then
        -- Strip the fn flag, which is set because the key is f18
        local flags = event:getFlags()
        flags['fn'] = nil
        local mods = tableKeys(flags)
        hs.eventtap.keyStroke(mods, 'ESCAPE', fast_delay)
      end
    end

    -- Do not send event on
    return true
  end):start()
end

enableSymbolLayer()

I consider this closed because I was able to do it.

All 3 comments

@aaronjensen
untested, and maybe reckless rubbish but maybe something along these lines:

hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, function(e)
    local mods = e:getFlags()
    local keyCode = e:getKeyCode()
    if ((hs.eventtap.checkKeyboardModifiers().capslock) and mods.ctrl and keyCode.f) then
        hs.eventtap.keyStroke({'ctrl'}, "(")
    end
end)

_apologies if this errors, not been coding for long_

@rootscript thank you for the suggestion. This wasn't exactly what I needed, but it did point me in the right direction. I ended up using eventtap.new to create my bindings so I could listen to all modifiers and forward them on.

Here is is what I ended up with: https://github.com/aaronjensen/dotfiles/blob/9bcf07ebaf3ef7f597c37daefaae78d5492e13a0/hammerspoon/symbol_layer.lua


Symbol layer

-- The default delay is too slow, making key repeating very slow
fast_delay = 1000

function tableContainsValue(tbl, value)
  for _, v in ipairs(tbl) do
    if v == value then
      return true
    end
  end

  return false
end

function itableUnion(a, b)
  local ab = {}
  for _, v in ipairs(a) do
    table.insert(ab, v)
  end

  for _, v in ipairs(b) do
    if not tableContainsValue(ab, v) then
      table.insert(ab, v)
    end
  end

  return ab
end

function tableKeys(tbl)
  local ret = {}
  for key, _ in pairs(tbl) do
    table.insert(ret, key)
  end

  return ret
end

function enableSymbolLayer()
  local symbols = {
    o = {{}, '-'},            -- o -> -
    p = {{'shift'}, '='},     -- p -> +
    s = {{}, '['},            -- s -> [
    d = {{'shift'}, '['},     -- d -> {
    f = {{'shift'}, '9'},     -- f -> (
    g = {{}, '='},            -- g -> =
    h = {{'shift'}, '-'},     -- h -> _
    j = {{'shift'}, '0'},     -- j -> )
    k = {{'shift'}, ']'},     -- k -> }
    l = {{}, ']'},            -- l -> ]
    c = {{'shift'}, '`'},     -- c -> ~
    v = {{}, '`'},            -- v -> `
    m = {{'shift'}, '\\'},    -- m -> |
    [','] = {{'shift'}, ','}, -- , -> <
    ['.'] = {{'shift'}, '.'}, -- . -> >
  }

  -- If you hold capslock for this long and then let go, ESC will not be sent.
  local cancel_delay_seconds = 0.3
  local triggered = true
  local resetTimer = hs.timer.delayed.new(cancel_delay_seconds, function()
    triggered = true
  end)

  -- This tap handles remapping. It is only enabled while capslock is held
  local tap = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
    -- Check if this event is from the keyboard or from us. We need to ignore
    -- events from us.
    local processId = event:getProperty(hs.eventtap.event.properties.eventSourceUnixProcessID)
    if processId > 0 then
      return false
    end

    triggered = true

    local key = hs.keycodes.map[event:getKeyCode()]
    local remap = symbols[key]
    if remap then
      local mods = tableKeys(event:getFlags())
      local newMods = itableUnion(mods, remap[1])
      local newKey = remap[2]

      hs.eventtap.keyStroke(newMods, newKey, fast_delay)
    end

    -- Do not send event on
    return true
  end)

  -- This tap handles F18 (capslock) with any modifier and is enabled right away.
  hs.eventtap.new({hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, function(event)
    local key = hs.keycodes.map[event:getKeyCode()]
    -- Ignore all events but f18
    if key ~= 'f18' then
      return false
    end

    if event:getType() == hs.eventtap.event.types.keyDown then
      resetTimer:start()
      triggered = false
      tap:start()
    else
      resetTimer:stop()
      tap:stop()

      if not triggered then
        -- Strip the fn flag, which is set because the key is f18
        local flags = event:getFlags()
        flags['fn'] = nil
        local mods = tableKeys(flags)
        hs.eventtap.keyStroke(mods, 'ESCAPE', fast_delay)
      end
    end

    -- Do not send event on
    return true
  end):start()
end

enableSymbolLayer()

I consider this closed because I was able to do it.

Hi, I don't think that the code is working anymore

Was this page helpful?
0 / 5 - 0 ratings

Related issues

latenitefilms picture latenitefilms  路  3Comments

agzam picture agzam  路  3Comments

iliyang picture iliyang  路  4Comments

fent picture fent  路  3Comments

BigSully picture BigSully  路  3Comments