hammerspoon
hammerspoon copied to clipboard
Bind capslock to hotkey
I want to bind capslock along with J,K,L,I keys to trigger arrow keys respectively. From my understanding, this might not be possible according to this issue Is this still the case with hammerspoon?
Yes and no... as a toggle, yes, we can detect the caps-lock and optionally enable a set of hotkeys. But we can't prevent it from being used by the OS to actually engage the caps lock, so no release event is detected by us. Hitting the caps-lock again will issue another flag change event so we can un-enable the hotkeys, but it has to be as a toggle, not as a true modifier like key. For example:
local eventtap = require("hs.eventtap")
local event = eventtap.event
-- get initial capslock state
local clFlagState = (eventtap.checkKeyboardModifiers(true)._raw & event.rawFlagMasks.alphaShift) ~= 0
local capsLockOnFn = function()
-- enable your hot keys here
print("caps lock down")
end
local capsLockOffFn = function()
-- disable your hot keys here
print("caps lock not down")
end
local eventtapFn = function(e)
-- this is so we can use it to set initial state when started up
if type(e) == "boolean" then
if e then
capsLockOnFn()
else
capsLockOffFn()
end
else
local rf = e:rawFlags()
local state = (rf & event.rawFlagMasks.alphaShift) ~= 0
if state and not clFlagState then
clFlagState = true
capsLockOnFn()
-- in theory, the following should "throw away" this event, but as you'll see
-- if you try this code, it doesn't affect the caps-lock event -- the system has
-- already done its job and set the flag internally and lit the caps-lock led
--
-- in my code, I'd just remove it -- leaving it in only prevents other eventtaps
-- (possibly in other apps) from detecting this flag change if they registered
-- after we did
return true
elseif not state and clFlagState then
clFlagState = false
capsLockOffFn()
end
end
end
-- set initial state
eventtapFn(clFlagState)
tap = eventtap.new({ event.types.flagsChanged }, eventtapFn):start()
edited to remove some unnecessary code to flags -- the inequality check already sets true/false
Thinking about it a little more, it sounds like you're doing something similar to my viKeys.lua, which uses the fn
key as the modifier -- unlike the caps lock, it has to be held down, but it has worked for me for quite a while now.
Using the above as a template, I would do something like this -- it has the advantage that if you hold the shift/option/cmd keys while the toggle is in effect that you get the shift-arrow/option-arrow/cmd-arrow behavior and auto-repeats as well...
-- this is minimally tested, but initial tests show that it seems to work fine, as long as you
-- remember that caps-lock is a toggle and it's only changing JKLI and no other keys
-- (though disabling those would just require the key-handler returning true instead of
-- false in the else clause)
local eventtap = require("hs.eventtap")
local event = eventtap.event
-- get initial capslock state
local clFlagState = (eventtap.checkKeyboardModifiers(true)._raw & event.rawFlagMasks.alphaShift) ~= 0
-- key handling function
local keyHandler = function(e)
-- local watchFor = { h = "left", j = "down", k = "up", l = "right" } -- vi style
-- I remember using this on an Apple ][, but forget the program, so no fancy style name :-)
local watchFor = { j = "left", k = "down", i = "up", l = "right" }
local actualKey = e:getCharacters(true)
local replacement = watchFor[actualKey:lower()]
-- if replacement has a value, then one of our keys was pressed
if replacement then
-- duplicate the event, keeping all traditional modifiers, but with our (arrow) key instead
local isDown = e:getType() == event.types.keyDown
local flags = {}
for k, v in pairs(e:getFlags()) do
if v then
table.insert(flags, k)
end
end
local replacementEvent = event.newKeyEvent(flags, replacement, isDown)
if isDown then
-- if a key is held down, it actually sends a second down event, but with
-- the autorepeat property set, so duplicate that as well if this is a
-- key-down event:
replacementEvent:setProperty(event.properties.keyboardEventAutorepeat, e:getProperty(event.properties.keyboardEventAutorepeat))
end
-- throw out the original event and replace it with ours
return true, { replacementEvent }
else
-- otherwise, do nothing to the event, just pass it along
return false
end
end
-- set up, but don't start, keyup/keydown eventtap
local keyListener = eventtap.new({ event.types.keyDown, event.types.keyUp }, keyHandler)
-- this is the eventtap function that checks for the caps lock key change
local eventtapFn = function(e)
-- this is so we can use it to set initial state when started up
local state
if type(e) == "boolean" then
state = e
clFlagState = not clFlagState
else
state = (e:rawFlags() & event.rawFlagMasks.alphaShift) ~= 0
end
if state and not clFlagState then
clFlagState = true
keyListener:start()
elseif not state and clFlagState then
clFlagState = false
keyListener:stop()
end
end
-- set initial state
eventtapFn(clFlagState)
-- start watching flags
tap = eventtap.new({ event.types.flagsChanged }, eventtapFn):start()
@asmagill Thank you very much for your detailed response! So it's unfortunately not really possible to do what i wanted, since i want to hold down the capslock, not toggle it. Maybe i should give toggling a try, but i think it might be slower than holding down.
My plans were to hotkey capslock (while held down) with other keys to get shift-arrow/option-arrow/cmd-arrow behavior like you said, however instead of pressing down the option/shift/cmd keys, it would be S,D,F keys.
So for example:
- capslock + j = left
- capslock + s + j = option + left
- capslock + d + j = option + shift + left
- and so on...
And ofcourse i can create any combination of s,d,f being pressed down simultaneously for a total of 2^3 = 8 different options (or even use other keys to increase it exponentially)
From some reading around i see that people use karabiner-elements to map the capslock key to a "hyper" key (from my understanding its a combination of keys). I will hopefully find a way to achieve binding capslock when being pressed down, since its a very under utilized key that is very comfortable to use
May macOS' key remapping help you? It doesn't turn capslock into a new modifier key but I believe you could do this within Hammerspoon.
I remapped caps lock to F13 to toggle my terminal window by creating the file ~/Library/LaunchAgents/com.local.KeyRemapping.plist
with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.KeyRemapping</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/hidutil</string>
<string>property</string>
<string>--set</string>
<string>{"UserKeyMapping":[
{
"HIDKeyboardModifierMappingSrc": 0x700000039,
"HIDKeyboardModifierMappingDst": 0x700000068
}
]}</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Run launchctl load ~/Library/LaunchAgents/com.local.KeyRemapping.plist
or log off and back in.
For reference: Apple HIDUTIL.
You can generate a property list with any remappings here.
Pros:
- no extra software
- works reliably
- works persistently after reboots and OS updates
- you can do whatever you want with that new key in Hammerspoon
Cons:
- no caps lock key anymore (for me, that's a pro too)
EDIT:
When thinking about it, it's much easier to put this into ~/.hammerspoon/init.lua
(no need for a LauchAgent):
function key_remapping()
-- remap capslock to F13:
status = os.execute("hidutil property --set '{\"UserKeyMapping\":[{\"HIDKeyboardModifierMappingSrc\": 0x700000039, \"HIDKeyboardModifierMappingDst\": 0x700000068}]}'")
if not status then
hs.dialog.blockAlert("Key remapping failed", "Check with:\nhidutil property --get UserKeyMapping")
end
end
key_remapping()
Yes and no... as a toggle, yes, we can detect the caps-lock and optionally enable a set of hotkeys. But we can't prevent it from being used by the OS to actually engage the caps lock, so no release event is detected by us. Hitting the caps-lock again will issue another flag change event so we can un-enable the hotkeys, but it has to be as a toggle, not as a true modifier like key. For example:
local eventtap = require("hs.eventtap") local event = eventtap.event -- get initial capslock state local clFlagState = (eventtap.checkKeyboardModifiers(true)._raw & event.rawFlagMasks.alphaShift) ~= 0 local capsLockOnFn = function() -- enable your hot keys here print("caps lock down") end local capsLockOffFn = function() -- disable your hot keys here print("caps lock not down") end local eventtapFn = function(e) -- this is so we can use it to set initial state when started up if type(e) == "boolean" then if e then capsLockOnFn() else capsLockOffFn() end else local rf = e:rawFlags() local state = (rf & event.rawFlagMasks.alphaShift) ~= 0 if state and not clFlagState then clFlagState = true capsLockOnFn() -- in theory, the following should "throw away" this event, but as you'll see -- if you try this code, it doesn't affect the caps-lock event -- the system has -- already done its job and set the flag internally and lit the caps-lock led -- -- in my code, I'd just remove it -- leaving it in only prevents other eventtaps -- (possibly in other apps) from detecting this flag change if they registered -- after we did return true elseif not state and clFlagState then clFlagState = false capsLockOffFn() end end end -- set initial state eventtapFn(clFlagState) tap = eventtap.new({ event.types.flagsChanged }, eventtapFn):start()
edited to remove some unnecessary code to flags -- the inequality check already sets true/false
Hi, I'm trying to set up a hotkey where CAPSLOCK-<NUMBER_KEY> executes a command, i.e. I'd like to use capslock as a modifier key. Would your code here be appropriate for such use? Ideally, I'd like to also be able to reset the capslock key back to it's previous state after pressing the hotkey so e.g. if capslock is off then after hitting CAPSLOCK-1 the capslock key should reset back to off. Hope that makes sense.
@alimbada Google Up on using Caps-Lock as a Hyper key, that should do the trick for you.
Then here's how to do it without Karabiner-Elements.
Hi @piechologist,
I recently started using Universal Control to operate two Macs and encountered a challenging issue with keyboard events not transmitting properly between the machines with karabiner-elements. I've been struggling with this for quite a while. Fortunately, I came across your solution in this issue, and it perfectly solved my problem!
I followed your method to remap the capslock key within Hammerspoon, and it worked flawlessly. It's a relief to have this issue resolved, thanks to your guidance.
I just wanted to express my sincere gratitude for your help. Your solution not only helped me immensely but will undoubtedly assist others facing similar challenges.
Thank you again for sharing your knowledge and expertise!