Programmatically Toggle the Caps Lock Key

Hey Folks,

Someone posted this and then withdrew it.

I tested the code, and it works perfectly on my macOS 10.12.6 Sierra system, so I'm posting it for posterity.

Our own @CJK gets the credit.

-Chris


// --------------------------------------------------------
// @CJK
// 2019.06.02 · 15:49
// Toggle caps lock programmatically using AppleScript's JavaScript for Automation (JXA)
// https://apple.stackexchange.com/questions/361373/toggle-caps-lock-programmatically-using-applescript
// --------------------------------------------------------

ObjC.import("IOKit");
ObjC.import("CoreServices");


(() => {
    var ioConnect = Ref();
    var state = Ref();

    $.IOServiceOpen(
        $.IOServiceGetMatchingService(
            $.kIOMasterPortDefault,
            $.IOServiceMatching(
                $.kIOHIDSystemClass
            )
        ),
        $.mach_task_self_,
        $.kIOHIDParamConnectType,
        ioConnect
    );
    $.IOHIDGetModifierLockState(ioConnect, $.kIOHIDCapsLockState, state);
    $.IOHIDSetModifierLockState(ioConnect, $.kIOHIDCapsLockState, !state[0]);
    $.IOServiceClose(ioConnect);
})();
2 Likes

On Catalina and Big Sur it only activates the CAPSLOCK, but doesn't deactivate it.

1 Like

As @ALYB above and others (KMF, SE, GH, …) have mentioned, the CapsLock state detection of the script by @CJK (see OP) stopped working with more recent macOS releases, probably since Catalina.

I fiddled around a bit with that, and was unable to find a really proper solution.

However, I have two approaches that work sufficiently well (for me). For setting the CapsLock state, both are using the setter part of CJK's script. The difference is in the method how to decide if CapsLock should be activated or deactivated:


Approach 1: "Brutal Workaround"

Keyboard: Toggle CapsLock State (remember state via KM var).kmmacros (6.1 KB)

Overview

Main Script
ObjC.import("IOKit");

const state = kmvar.keyboard_CapsLock_State;
const toState = !Number(state);

const ioConnect = Ref();

$.IOServiceOpen(
	$.IOServiceGetMatchingService(
		$.kIOMasterPortDefault,
		$.IOServiceMatching(
			$.kIOHIDSystemClass
		)
	),
	$.mach_task_self_,
	$.kIOHIDParamConnectType,
	ioConnect
);
$.IOHIDSetModifierLockState(ioConnect, $.kIOHIDCapsLockState, toState);
$.IOServiceClose(ioConnect);

No effort is made to detect the actual CapsLock state. Instead, the macro sets a global variable at each run; the variable changes between 1 and 0 and represents the assumed CapsLock state. Depending on this variable, the macro either sets or unsets CapsLock.

If the macro is your only CapsLock toggle, this works perfectly fine, since the variable changes every time you change the CapsLock state.

If you toggled CapsLock by other means (e.g. you disabled CapsLock via the little typing popup), the variable will be out of sync with the true CapsLock state. But this is not really a game stopper, because after one extra macro run, the variable will be in sync again with the state.


Approach 2: "Better – probably"

Keyboard: Toggle CapsLock State (detect state via script).kmmacros (5.7 KB)

Overview

Main Script
ObjC.import("AppKit")
ObjC.import("IOKit");

const theMask = $.NSAlphaShiftKeyMask; // 65536
const theFlag = $.NSEvent.modifierFlags;
const state = (theFlag / theMask) % 2;
const toState = !state;

const ioConnect = Ref();

$.IOServiceOpen(
	$.IOServiceGetMatchingService(
		$.kIOMasterPortDefault,
		$.IOServiceMatching(
			$.kIOHIDSystemClass
		)
	),
	$.mach_task_self_,
	$.kIOHIDParamConnectType,
	ioConnect
);
$.IOHIDSetModifierLockState(ioConnect, $.kIOHIDCapsLockState, toState);
$.IOServiceClose(ioConnect);

This one uses a true state detection (see script), based on a snippet by Shane Stanley.

A small catch here is that it only detects the CapsLock state correctly if any key is pressed, or was pressed since the last macro run.

But, if you use the macro with a trigger similar to the posted one (Long Press Shift), or even a simple hotkey trigger, everything should work flawlessly.

Other, possible, downsides of this approach:

  • Since it runs code for the detection (using an additional framework), it may be slower. (Noticeable?)
  • I noticed a one-time glitch in one particular application (Script Editor). But this could also be a glitch in the setter part (i.e. it would have happened with the other approach as well). I have not had a chance to verify this yet.

If anyone who is more familiar with these things knows a way to detect the state independently of other keys (like the original script – probably – did it before Catalina), I would be grateful for a hint.

So far, I tried a couple of variations, also as pure Swift scripts, but anything that involves NSEvent.modifierFlags seems to expose the same behavior.

Does this checker work for you?

Caps Lock Detection.kmmacros (4.0 KB)

Image

I won't pretend to know what's happening, I just ripped bits out of this, which I suspect is an update of the script you've referenced.

1 Like

That's exactly the checker from my "Approach 2" (before I integrated it in the JXA, I used it in ASOC form too).

So, yes it works, with the mentioned caveat (needs other modifier in order to detect the Caps state).

I did say I didn't understand it!

Except I only posted it because I'm not seeing that behaviour. Caps Lock up, click macro's run button, reports Caps Lock up. Press Caps Lock down, click the run button again -- so no modifiers involved -- reports Caps Lock down.

macOS Sonoma 14.4.1 on this tester, if that makes a difference.

I downloaded and tried your macro. It's the same for me like with mine: If run it from the KM Editor's Run button, it reports the wrong state. If I hold down a modifier (eg Option) when clicking the Run button, it works.

Sonoma 14.5

Weird! I'm testing on an M1 Air with built-in k/b -- I'll try an Intel iMac and Magic later in case it's hardware dependent.

I'm on Intel, forgot to mention it.

1 Like

I just found out that my "Approach 2" also works when launched from an F-Key (without modifier). This would mean that it doesn't need another modifier, but just another key press (any).

Edit: But it definitely doesn't work when launched via click on the Try button, or from the Status Menu (except I hold down Shift or Cmd while doing so – or inbetween two clicks).

In fact, I can also press any key in-between two clicks on the Run/Try Button to make it work. So what I called caveat isn't actually much of a caveat :slight_smile:

It seems the API just needs any key event to update the state internally, so that it can report back the correct state when the macro is run.

Edit:

For testing, here is your version with the ASOC script, plus the setter JXA action (using the return value from the ASOC script):

Caps Lock Detection + Change [test].kmmacros (3.3 KB)

1 Like

Both yours and my "detectors" work fine on a 2020 Intel iMac running 14.5, so it isn't just the platform that's the issue. And the Caps Lock setter is working too.

I can try the AppleScripts on some other devices, if you want. Just a thought -- are you using anything that grants "hyperkey" functionality on the Caps Lock?

No. My CapsLock key is set to Ctrl via System Settings, and no Karabiner installed currently. If I set it back to CapsLock, it's the same.

But the CapsLock key shouldn't be a part of a test anyway (the point of the macro is to reduplicate CapsLock key functionality when the CapsLock key is mapped to something else), the green lamp is the only interesting thing of that key ;-).

So, with "work fine", do you definitely mean this(?):

  • If you run my "Approach 2" macro from the KM Editor Run button or from the KM Status Menu via mouse click repeatedly and without pressing any key in-between or while launching the macro, it reliably toggles CapsLock On/Off/On/Off/On/… etc., it does not "hang" at one of the two states?

  • The same is true for the "Caps Lock Detection + Change [test]" macro I uploaded yesterday in the post above? (Just for curiosity, as it should behave exactly the same.)


Since I've discovered (see my posts above) that any keystroke ensures correct state detection, this is more of an academic interest, but I'm still curious :wink:

Ah, I see where you're coming from now. My tests involved manually toggling the Caps Lock key (detection) or typing characters (did the change happen?). I wasn't just mashing the "Run" button and watching the Caps Lock light... Apologies for any false hope I might have given.

IMO -- and remember, total noob offering his opinion here -- this is because NSEvent only updates after an "actual" input action, which doesn't include your software set of Caps Lock state. You're effectively working with stale data wrt Caps Lock state -- it appears that a mouse-click event doesn't update recorded key state either.

Unless can you force an NSEvent change/update as part of your JXA you're left tracking it yourself. I don't know enough about this to know if there's some NSEvent property you can store to then compare against on the next run.

1 Like

Ah, I see where you're coming from now. My tests involved manually toggling the Caps Lock key

Ah, OK. Yes, the not-touching-any-key part was important.

IMO -- and remember, total noob offering his opinion here -- this is because NSEvent only updates after an "actual" input action

Yes, as mentioned here, this was exactly my (also noob ;-)) conclusion yesterday, after I’ve discovered that any keystroke is sufficient.


When I said it is more of academic interest, it wasn’t entirely true. A while ago I’ve written a little command line tool to detect modifier states. Apparently I have never thoroughly tested the CapsLock part, and only discovered in the context of my macro essays here that the CapsLock detection there has the same "flaw".

While the needs-a-keystroke flaw is pretty much meaningless for a macro that most likely is launched via keystroke, it isn’t entirely meaningless for a command line tool that may be run via script automatically.

Btw, the same behavior is shown by the 12-year old Carbon/Obj-C tool that gave me the idea for the command line tool.

One could also interpret this behavior as ‘correct’, in the sense that the CapsLock key is 100% detected while it is actually pressed (just like it is the case with any "normal" modifier key). But this means an inconsistency then, because the state is also detected when the CapsLock key is no longer pressed and any other keystroke was performed while the state is still active.


Unless can you force an NSEvent change/update as part of your JXA you're left tracking it yourself

In this context, an observation I find interesting:

With the little blue typing popup enabled that indicates an active CapsLock state at the cursor position, I see this:

  1. CapsLock state active: The popup appears as soon as I place the cursor into a text input area.
  2. Now I click outside of the text input, and deactivate CapsLock
  3. I place the cursor again in the text input area.
    • Now, the popup appears for approx. 0.2s, and then disappears correctly.

This means IMO that the update of the state on API side is in fact strictly event-bound, but in this case it is not a keystroke, but a mouse click event, or an enter-text-input-area event (in case this exists).

This event though does not update the NSEvent.modifierFlags as used in my macro and command line tool, so there must be another API or method that allows for correct detection, … I think.


Many thanks for your testing!