Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hammerspoon config to send escape on short ctrl press
ctrl_table = {
sends_escape = true,
last_mods = {}
}
control_key_timer = hs.timer.delayed.new(0.15, function()
ctrl_table["send_escape"] = false
-- log.i("timer fired")
-- control_key_timer:stop()
end
)
last_mods = {}
control_handler = function(evt)
local new_mods = evt:getFlags()
if last_mods["ctrl"] == new_mods["ctrl"] then
return false
end
if not last_mods["ctrl"] then
-- log.i("control pressed")
last_mods = new_mods
ctrl_table["send_escape"] = true
-- log.i("starting timer")
control_key_timer:start()
else
-- log.i("contrtol released")
-- log.i(ctrl_table["send_escape"])
if ctrl_table["send_escape"] then
-- log.i("send escape key...")
hs.eventtap.keyStroke({}, "ESCAPE")
end
last_mods = new_mods
control_key_timer:stop()
end
return false
end
control_tap = hs.eventtap.new({12}, control_handler)
control_tap:start()
@alexlafroscia

This comment has been minimized.

Copy link

@alexlafroscia alexlafroscia commented Oct 27, 2016

Thanks so much for this! Works like a charm to get CAPSLOCK mapped to ESC and CTRL on Sierra!

@rjhilgefort

This comment has been minimized.

Copy link

@rjhilgefort rjhilgefort commented Oct 28, 2016

Thanks for this, now to get my hyper back...

@rjhilgefort

This comment has been minimized.

Copy link

@rjhilgefort rjhilgefort commented Oct 28, 2016

@arbelt I think it would be more clear if you change line 39 to be more explicit about the event flag you're targeting (it was the first thing I went digging for when reading through the code).

control_tap = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, control_handler)

Without logging out hs.eventtap.event.types, it's unclear what event code 12 is.

@rjhilgefort

This comment has been minimized.

Copy link

@rjhilgefort rjhilgefort commented Oct 28, 2016

Also, it doesn't appear to be breaking the logic since the priming on line 2 isn't necessary, but it's not named the same as the rest of the file. Line 2's sends_escape = true, has an "s" that the other references don't. It should be:

send_escape = true,
@rjhilgefort

This comment has been minimized.

Copy link

@rjhilgefort rjhilgefort commented Oct 28, 2016

I'm finding this solution doesn't work with my tmux prefix chord unfortunately. I'll hack on it a bit and report back. If anyone else is in the same boat, let me know, and let me know what you've tried.

To get ready to play with it, I did some cleanup and simplification refactoring (original logic is still in tact, but with fewer variables). Feel free to have a look if you're using it as a jumping off point as well: https://gist.github.com/rjhilgefort/07ce5cdd3832083d7e94113d54372b1c

@jasoncodes

This comment has been minimized.

Copy link

@jasoncodes jasoncodes commented Oct 29, 2016

I have expanded on this script to avoid sending Escape when other keys are tapped while Control is held down. I’ve been running this for a few weeks now. Works great.

https://github.com/jasoncodes/dotfiles/blob/master/hammerspoon/control_escape.lua

@joaalto

This comment has been minimized.

Copy link

@joaalto joaalto commented Nov 22, 2016

Thank you arbelt and jasoncodes! This saved me from a lot of pain!

@wezzynl

This comment has been minimized.

Copy link

@wezzynl wezzynl commented Jan 25, 2017

Instead of using hs.eventtap.keyStroke to emit an event, returning from the control_handler function with a table of events to post (KeyDown then KeyUp) makes the escape perform instantly on KeyUp. hs.eventtap.keyStroke feels slow for some reason, taking a fraction of a second before the actual stroke gets sent, drove me nuts.

control_handler = function(evt)
  local new_mods = evt:getFlags()
  if last_mods["ctrl"] == new_mods["ctrl"] then
    return false
  end
  if not last_mods["ctrl"] then
    last_mods = new_mods
    send_escape = true
    control_key_timer:start()
  else
    last_mods = new_mods
    control_key_timer:stop()
    if send_escape then
      return true, {
        hs.eventtap.event.newKeyEvent({}, 'escape', true),
        hs.eventtap.event.newKeyEvent({}, 'escape', false),
      }
    end
  end
  return false
end

Seems much faster!

@CestDiego

This comment has been minimized.

Copy link

@CestDiego CestDiego commented Mar 11, 2017

@wezzynl Thank you for that comment. I also felt it a little unbearable sometimes to wait a fraction of a second for it to work. Also for some reason the escape key event would not be sent to emacs until I tried your solution.

@zcmarine

This comment has been minimized.

Copy link

@zcmarine zcmarine commented Mar 28, 2017

In case anyone else comes upon this and finds the above code confusing or, like me, keeps getting a bug where you end up having to hit escape multiple times, I rewrote the above and added comments, clearer variable names, and fixed that bug. Gist located here: https://gist.github.com/zcmarine/f65182fe26b029900792fa0b59f09d7f

Definitely wouldn't have gotten that done without the above as an initial starting point though, so thanks a ton to arbelt and jasoncodes!

@safecancel

This comment has been minimized.

Copy link

@safecancel safecancel commented Jun 19, 2017

Hi everyone,
Is there a way to not have this run when a specific app is in focus? (for example: remote desktop, vmware, teamviewer etc.)
Many thanks!

@dguo

This comment has been minimized.

Copy link

@dguo dguo commented Feb 24, 2020

@safecancel you can grab the current active application and use that to determine if the logic should run.

Thanks everyone. I've been using Karabiner-Elements only to remap caps lock to escape on tap and control on hold. With your solutions, I can use Hammerspoon instead (after changing caps lock to control in System Preferences) and drop Karabiner as a dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment