Skip to content

Instantly share code, notes, and snippets.

@arbelt
Created October 2, 2016 20:55
Show Gist options
  • Star 33 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save arbelt/b91e1f38a0880afb316dd5b5732759f1 to your computer and use it in GitHub Desktop.
Save arbelt/b91e1f38a0880afb316dd5b5732759f1 to your computer and use it in GitHub Desktop.
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
Copy link

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

@rjhilgefort
Copy link

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

@rjhilgefort
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

joaalto commented Nov 22, 2016

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

@wezzynl
Copy link

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
Copy link

@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
Copy link

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
Copy link

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
Copy link

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.

@chrisspiegl
Copy link

chrisspiegl commented Mar 19, 2021

This is a great and very helpful gist. I used it to get my application to more reliably trigger the ESCAPE key. Since sending it with the hs.eventtap.sendKey does not seem to reliably do so (especially in applications like Alfred, 1Password, and such.

But the tip by @wezzynl in their comment above got me thinking and it actually does work much faster and more reliable.

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