用 Hammerspoon 替代 Karabiner:macOS Tahoe 上 Caps Lock 切换输入法修复记
2026年8月1日 · 1121 字
从"轻按 Caps Lock 切换中英文失灵"一路排查,最后写了个 80 行的 Hammerspoon 脚本把 Karabiner 给替了。
症状
环境:macOS 26.5.2 (Tahoe)、Karabiner-Elements 16.1.0。
按一下 Caps Lock,键盘直接"死掉"——字母、方向键全部无响应;再按一下,输入法切换成功;再按一下,键盘又死了。形成严格的"死 → 切 → 死 → 切"交替,像有个修饰键被卡住了。
排查:两条线索
1. 一条被"记住"的 HID 映射
用 hidutil property --get UserKeyMapping 查到一个映射:Caps Lock (0x700000039) → F18 (0x70000006D),内容和 GitHub gist bennlee/remapping-keys-via-hidutil 的示例一字不差。
关键点:这个映射按键盘设备持久化,重启后 macOS 会在设备层自动恢复。我清掉后重启,它又回来了。
2. Karabiner 在 Tahoe 上的 IPC 故障
Karabiner 的架构是:grabber(HID 层抓走键盘)→ core-service(处理规则)→ VirtualHIDDevice(虚拟键盘)→ 系统,组件间靠 UNIX socket 通信。
console_user_server.log 里反复出现:
core_service_daemon_client connect_failed: Permission denied
core_service_daemon_client error: Operation timed out
这正是 GitHub issue #4371、#4314 报告的 Tahoe 兼容问题:Tahoe 收紧了 socket 沙盒权限,Karabiner 的内部连接被拒。连接一断,键盘被 grabber 抓走却不转发,按键被吞;而 Caps Lock 在 Karabiner 的虚拟键盘里是当作修饰键处理的,连接抖动时"按下/抬起"对不齐,虚拟键盘以为 Caps 一直按着 → 之后的每个键都带着一个卡住的修饰键 → 再按一次 Caps 才复位。这正好解释了交替现象。
sudo kill -9 也没用:这些守护进程由 SMAppService + KeepAlive 托管,杀掉立刻复活;而且 SIGKILL 是在状态转换中途硬打断,复活后状态更乱。
顺带确认了 /System/Library/CoreServices/TextInputSwitcher.app 是苹果官方的输入源切换器,行为正常,跟这事无关。
方案:自己写一个轻量程序
既然 Karabiner 的问题出在"整个键盘抓走 + 内部 socket",那我只需要它做的那一件事——监听 Caps + 切输入法。社区早就拆好了轮子:
- 切输入法:macOS 的 TIS API(
TISSelectInputSource),命令行工具im-select就是它。 - 监听 Caps:
hidutil把 Caps 重映射成一个不存在的键 F18,再用程序监听 F18——这样系统完全看不见 Caps,不会抢、也没有原生防误触延时。
实现选型上我用了 Hammerspoon(Lua 脚本,改配置即热加载),总代码约 80 行。
1. Caps → F18 映射(IOKit 层)
hidutil property --set '{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":0x700000039,"HIDKeyboardModifierMappingDst":0x70000006D}]}'
配一个 LaunchAgent 在开机时执行,保证持久化。
2. Hammerspoon 监听 F18
hs.eventtap 监听 F18 的 keyDown/keyUp,逻辑:
- 轻按 < 0.35s → 切中英输入法(
hs.keycodes.currentSourceID(id)直接调 TIS,记住最近用过的中文输入法,换豆包/简体拼音都无所谓) - 长按 ≥ 0.35s → 切换大写锁定(合成一次真实 CapsLock 键)
- Caps + H/J/K/L → 方向键
- Caps + I/O →
⌘+←/→(行首/行尾) - Caps + F/G →
Option+←/→(单词跳转) - Caps + A/S/D/W →
Shift+←↓↑→(方向选中) - Caps + Q/E →
Shift+Option+←/→(选中单词) - Caps + ' → 真实 CapsLock(兜底)
踩过的三个坑
hs.eventtap.event:setFlags()要传字符串表({"shift"}),传 raw 掩码数字会报table expected, got number。- 带修饰键的按键必须发真实事件序列:只给合成事件设 flag,浏览器这类 App 不认,表现为"⌘+→ 只移动了一个字符"。正确做法是发
修饰键 keyDown → 按键 → 修饰键 keyUp完整序列。 - macOS 的行尾不是裸 End 键:keycode 119(End)在浏览器里是"滚到页面底部",文本域的行尾/行首其实是
⌘+→ / ⌘+←。
效果与文件
文件都在我的 dotfiles 仓库 hammerspoon/ 目录:init.lua + com.local.capslock-f18.plist,同步到 ~/.hammerspoon/init.lua 后 Reload 即生效。
实际体验:Caps 轻按切换输入法无延迟(去掉了系统的防误触延时),方向键、行首行尾、选中在浏览器输入框里都正常工作。而且因为只监听一个 F18、不抓整个键盘,没有 root 守护进程,天然免疫 Karabiner 在 Tahoe 上的那类问题。
一点总结
macOS 上要合成一个"Windows 式"的按键,最可靠的还是发真实的事件序列(修饰键 + 主键 + 抬起),别指望 flag 能传达给所有 App。TIS API 让"自己写个程序切输入法"变得非常简单——如果你的工具和系统在某处闹矛盾,亲手写个几十行的专用小程序,往往比调试那个"大而全"的工具更省心。