Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bind capslock to hotkey #3512

Open
Ron-Teller-DY opened this issue Jul 10, 2023 · 7 comments
Open

Bind capslock to hotkey #3512

Ron-Teller-DY opened this issue Jul 10, 2023 · 7 comments

Comments

@Ron-Teller-DY
Copy link

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?

@asmagill
Copy link
Member

asmagill commented Jul 10, 2023

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

@asmagill
Copy link
Member

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()

@Ron-Teller-DY
Copy link
Author

@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:

  1. capslock + j = left
  2. capslock + s + j = option + left
  3. capslock + d + j = option + shift + left
  4. 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

@piechologist
Copy link

piechologist commented Aug 2, 2023

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()

@alimbada
Copy link

alimbada commented Aug 4, 2023

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.

@jubr
Copy link

jubr commented Aug 29, 2023

@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.

@Drincann
Copy link

Drincann commented Nov 29, 2023

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants