summaryrefslogtreecommitdiff
path: root/config/hammerspoon/init.lua
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-01 18:05:25 +0300
committerIgor Soarez <igor@soarez.org>2026-10-01 18:05:25 +0300
commit6c7eb1b380cec100663c7c11d0252f9e6b46d34a (patch)
tree572c254c008df0dad812476850711e3861349c31 /config/hammerspoon/init.lua
parent7fc9494ac2b7d905cebff10ff5b8be5a3de80403 (diff)
hammerspoon: Caps Lock sends F20, and stays that way
hidutil remaps inside the IOKit HID driver, below the window server, so the key stops being Caps Lock before anything above the driver sees it: no toggle to suppress, and F20 is a real key to other apps, to System Settings shortcuts, to a password field. An eventtap cannot do this -- Caps Lock is not reachable from there, which is why hs.hid exists as its own IOKit module. What hidutil will not do is cover a keyboard attached after it ran, so dotlib re-applies on USB attach, on wake, and on a Caps Lock event. That last one is the general answer: a mapped keyboard cannot produce a Caps Lock event, so an event arriving at all is proof of a keyboard the remap missed, and the fault triggers its own repair. The watchers are there to get the common cases before a key is pressed, since the repairing press is spent repairing. No launchd job and no link/ part. The remap lives and dies with Hammerspoon, which already owns the keyboard on these machines, and a plist would only have bought independence from it for the second or two before it starts -- a user agent does not run at the login window either. Nothing is bound to f20 here; bind it like any other key. A machine gives the key back with capslock_off() from its local.lua, which loads last: shadowing cannot undo a side effect that has already run. hs.ipc is required so capslock_status() answers from a shell.
Diffstat (limited to 'config/hammerspoon/init.lua')
-rw-r--r--config/hammerspoon/init.lua17
1 files changed, 17 insertions, 0 deletions
diff --git a/config/hammerspoon/init.lua b/config/hammerspoon/init.lua
index cb1462a..74ae68f 100644
--- a/config/hammerspoon/init.lua
+++ b/config/hammerspoon/init.lua
@@ -35,6 +35,23 @@ local type_random_ids = lib.type_random_ids
local cs, ca, csa = lib.mods.cs, lib.mods.ca, lib.mods.csa
local cag, hyp, spc = lib.mods.cag, lib.mods.hyp, lib.mods.spc
+-- Caps Lock sends F20 from here on: dotlib does the HID-level remap and keeps
+-- re-applying it, including for keyboards attached later. Nothing is bound to
+-- f20 by default -- bind it below or in a machine's local.lua, the same as any
+-- other key. A machine that wants its Caps Lock back calls
+-- require("dotlib").capslock_off() from its local.lua, which loads last.
+--
+-- Early, so the keyboard behaves the same for the rest of this file's startup,
+-- but deliberately after the dotlib guard above: a config that failed to load
+-- has no bindings either, and a remapped key with nothing bound to it is worse
+-- than a working Caps Lock.
+lib.capslock_f20()
+
+-- The message port behind the `hs` CLI, so this config can be queried and
+-- driven from a shell -- `hs -c 'hs.hid.capslock.get()'`. Without it there is
+-- no way to check what Hammerspoon thinks from outside Hammerspoon.
+require("hs.ipc")
+
local function toggle(appName)
return function()
local currentSpace = hs.spaces.focusedSpace() -- Get current space