用 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 就是它。
  • 监听 Capshidutil 把 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/GOption+←/→(单词跳转)
  • Caps + A/S/D/WShift+←↓↑→(方向选中)
  • Caps + Q/EShift+Option+←/→(选中单词)
  • Caps + ' → 真实 CapsLock(兜底)

踩过的三个坑

  1. hs.eventtap.event:setFlags() 要传字符串表{"shift"}),传 raw 掩码数字会报 table expected, got number
  2. 带修饰键的按键必须发真实事件序列:只给合成事件设 flag,浏览器这类 App 不认,表现为"⌘+→ 只移动了一个字符"。正确做法是发 修饰键 keyDown → 按键 → 修饰键 keyUp 完整序列。
  3. 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 让"自己写个程序切输入法"变得非常简单——如果你的工具和系统在某处闹矛盾,亲手写个几十行的专用小程序,往往比调试那个"大而全"的工具更省心。