让 Caps Lock 成为最有价值的键:基于 hidutil + Hammerspoon 的零污染 Hyper Key 方案

💡 把键盘上利用率最低的 Caps Lock 改造成全局 “Hyper 键”,承载所有自定义快捷键。不依赖第三方驱动,不污染任何现有快捷键

一、为什么是 Caps Lock?

键盘上有这么一个键:

  • 占据黄金位置(左手小指自然落点)
  • 几乎从不使用(用过 Shift 切大小写的人都知道)
  • 误触代价高(写代码时不小心按一下:HELLOhello

它是被设计史抛弃却始终留在键盘上的化石。把它收回来,能做的事情非常多。

经典的 Vim/Emacs 玩家会把它改成 CtrlEsc,但这只是单一功能。本文介绍的玩法是把它改成 Hyper 键 —— 一个实际不存在但功能强大的复合修饰键,作为所有自定义全局快捷键的入口。

最终效果:

操作 行为
单击 Caps Lock Esc(Vim 用户福音)
按住 Caps Lock + T 切换 iTerm
按住 Caps Lock + L 切换 Feishu
按住 Caps Lock + C 切换 Cursor
按住 Caps Lock + G 切换 Chrome
按住 Caps Lock + W 切换 WeChat
按住 Caps Lock + U 呼出剪贴板面板

想加新的快捷键?只改一行配置就行


二、为什么不用 Karabiner-Elements?

社区最流行的方案是 Karabiner-Elements + Hammerspoon。Karabiner 是 macOS 上最强的键盘改键工具,能做到极其精细的键位重映射。

但它有几个实践中的痛点

  1. 需要内核级 DriverKit 扩展。Karabiner 通过虚拟 HID 设备截获并重发所有键盘事件。每次 macOS 升级、Karabiner 升级,都可能需要重新授权 / 重启 / 处理驱动兼容问题。
  2. 兼容性偶发翻车。我在 macOS 15 + Karabiner 16.0.0 上踩了一个坑:只要 Karabiner 在跑(哪怕规则全空),系统级的 Cmd+H 隐藏应用就失效。”Temporarily disable all modifications” 后立刻恢复。这种黑盒问题排查起来非常痛苦。
  3. 重量级:6+ 个常驻进程,菜单栏 1 个图标,系统扩展 1 个。

🎁 我们其实不需要这么强的能力。需求只是 “Caps Lock 按下时进入一个特殊模态,按住时拦截后续键”。这件事完全可以用 macOS 自带的 hidutil + Hammerspoon 完成,且完全没有以上痛点


三、方案架构

事件流自下而上:

  1. 硬件层:按下 Caps Lock,键盘通过 HID 协议上报 usage code 0x39
  2. 内核层hidutil 用 macOS 自带的 UserKeyMapping 机制把 0x39 在最底层重映射成 0x6D(F18)。此后整个系统都看不到 Caps Lock 了,只能看到 F18
  3. 用户空间:Hammerspoon 监听 F18 的按下/松开,实现一个”模态状态机”:
    • 单击 F18 → 发送 Escape
    • 按住 F18 + 任意键 → 执行对应绑定的动作

方案架构:硬件层 → hidutil 内核映射 → Hammerspoon 用户空间

为什么是 F18?

F18 是一个物理键盘上不存在的虚拟按键。这意味着:

  • 不会和任何 App 的现有快捷键冲突
  • 不会被用户误触(你按不出 F18)
  • 在 macOS 系统快捷键里也没人占用

它就是一张完美的”空白支票”,专门给 Hammerspoon 监听。

为什么不直接发 Cmd+Ctrl+Opt+Shift(真 Hyper)?

很多教程把 Caps Lock 映射成 ⌘⌃⌥⇧(4 个修饰键的组合),称为”真 Hyper”。这样做的好处是:其他 App(如 Raycast、Alfred)可以直接绑定 Hyper+X 快捷键。

但坏处是:

  • 一旦你按下 Caps Lock,等于同时按下了 4 个修饰键,会触发任何监听这 4 个修饰键组合的快捷键
  • macOS 系统、第三方 App 偶尔会用到 ⌘⌥⇧ 这种 3 键组合(比如 ⌘⌥⇧V 是”无格式粘贴”)。当你按 Caps Lock + V 时会意外触发它。
  • 修饰键残留 bug:如果发送 4 个修饰键的实现稍微有点问题,松开 Caps Lock 时其中一个修饰键状态没清干净,系统就处于”Shift 一直按着”的状态,需要重启或重新按 Shift 才能恢复。Karabiner 在我这台机器上的 Cmd+H 问题大概率就是这个。

F18 方案完全避开了这两个问题:F18 是单独的一个键,单纯、干净、没有副作用。

Hyper 模态的状态机

Hammerspoon 用一个简单的状态机区分”单击”和”按住”:

  • F18 按下 → 进入 Hyper Active 状态,triggered 标志位置 false,开始监听后续键。
  • 在 Active 期间按了 T / L / W 等绑定过的键 → 执行动作,triggered = true,进入 Triggered 状态。
  • F18 松开:
    • 如果 triggered = false(没按其他键)→ 这是一次”单击”,发送 Escape。
    • 如果 triggered = true(已经触发了动作)→ 静默退出,不发 Escape。

Hyper 模态状态机:Idle → Hyper Active → Triggered

这套机制让”单击 = Esc”和”按住 = 修饰键”两种语义优雅地共存。


四、为什么这套方案不会污染快捷键?

这是核心卖点,分三层看。

1. hidutil 是 macOS 内置机制

不需要驱动注入、不需要内核扩展,是 Apple 官方 IOHIDFamily 提供的标准 API。和系统自带的”Caps Lock → Esc”功能(系统设置 → 键盘 → 修饰键…)是同一个底层机制,只不过 GUI 只能映射成几个固定的键,而 hidutil 能映射成任意 HID 键码。

2. F18 没有任何冲突源

  • 物理键盘没有 F18 键,所以你绝对不会因为别的操作意外触发它。
  • macOS 系统快捷键、Apple 自家 App、绝大多数第三方 App 都不会用到 F18。
  • 即使有 App 用了,也不会和我们冲突 —— 因为我们用的是 “F18 + 其他键” 的组合,单独的 F18 单击只会被 Hammerspoon 捕获并转成 Esc。

3. Hammerspoon 是用户空间的常规 App

  • 不需要驱动权限,只需要标准的”辅助功能”权限。
  • 它通过 hs.hotkey.bindhs.hotkey.modal 注册全局快捷键,只接管自己显式绑定的那些组合
  • 没绑定的键(比如 Caps Lock+X 没绑 → 实际按了 F18+X)会原样传给系统,不会拦截。

对比一下传统快捷键工具的污染问题:

工具 是否需要驱动 是否拦截所有键 卸载难度
Karabiner 是(DriverKit 扩展) 是(虚拟键盘全量重发) 需要 sudo 卸载驱动
系统设置改键 否(hidutil 底层 API) 否(只重映射指定键) 一行命令撤销
本方案 否(只接管 F18) 删两个文件就行

五、完整实现步骤

5.1 安装 Hammerspoon

1
brew install --cask hammerspoon

首次运行后到 系统设置 → 隐私与安全性 → 辅助功能 勾选 Hammerspoon。

5.2 用 hidutil 把 Caps Lock 映射成 F18

直接执行下面这条命令立即生效(一次性,重启失效):

1
2
3
4
hidutil property --set '{"UserKeyMapping":[
{"HIDKeyboardModifierMappingSrc":0x700000039,
"HIDKeyboardModifierMappingDst":0x70000006D}
]}'

💡 HID Usage Code 速查

  • 0x700000039 = Caps Lock
  • 0x70000006D = F18

为了开机自动应用,写一个 LaunchAgent。新建 ~/Library/LaunchAgents/com.local.KeyRemapping.plist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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":0x70000006D}]}</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

加载:

1
launchctl load ~/Library/LaunchAgents/com.local.KeyRemapping.plist

验证生效:

1
hidutil property --get UserKeyMapping

应该看到 30064771129 -> 30064771181 的映射。

🎁 hidutil 的映射对每个键盘单独生效。换一把新键盘上来会需要重新执行(LaunchAgent 重启时也会重跑,所以平时不用管)。

5.3 配置 Hammerspoon

编辑 ~/.hammerspoon/init.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
require("hs.ipc")

-- ============================================================
-- Hyper Key 模态
-- Caps Lock 已经被 hidutil 映射成了 F18
-- 单击 F18 -> Escape,按住 F18 + X -> 触发绑定动作
-- ============================================================

local hyperMode = hs.hotkey.modal.new()

local function enterHyper()
hyperMode.triggered = false
hyperMode:enter()
end

local function exitHyper()
hyperMode:exit()
if not hyperMode.triggered then
hs.eventtap.keyStroke({}, "escape", 0)
end
end

hs.hotkey.bind({}, "F18", enterHyper, exitHyper)

-- ============================================================
-- 工具函数:toggle App(前台时隐藏,否则启动/聚焦)
-- ============================================================
local function toggleApp(bundleID)
return function()
hyperMode.triggered = true
local front = hs.application.frontmostApplication()
if front and front:bundleID() == bundleID then
front:hide()
else
hs.application.launchOrFocusByBundleID(bundleID)
end
end
end

-- ============================================================
-- Hyper 快捷键绑定(在这里加新的就行)
-- ============================================================

hyperMode:bind({}, "T", nil, toggleApp("com.googlecode.iterm2")) -- iTerm
hyperMode:bind({}, "L", nil, toggleApp("com.electron.lark")) -- Feishu
hyperMode:bind({}, "W", nil, toggleApp("com.tencent.xinWeChat")) -- WeChat
hyperMode:bind({}, "C", nil, toggleApp("com.todesktop.230313mzl4w4u92")) -- Cursor
hyperMode:bind({}, "G", nil, toggleApp("com.google.Chrome")) -- Chrome

-- 转发到 App 自带的快捷键(Oka Paste 用 Cmd+Shift+B 呼出面板)
hyperMode:bind({}, "U", nil, function()
hyperMode.triggered = true
hs.eventtap.keyStroke({"cmd", "shift"}, "B", 0)
end)

-- ============================================================
-- 编辑 init.lua 时自动 reload
-- ============================================================
hs.pathwatcher.new(hs.configdir, function(files)
for _, f in ipairs(files) do
if f:sub(-4) == ".lua" then
hs.reload()
return
end
end
end):start()

hs.alert.show("Hammerspoon config loaded")

保存后 Hammerspoon 会自动 reload。屏幕上看到 “Hammerspoon config loaded” 弹窗就说明成功。


六、添加新快捷键

1
2
3
4
5
6
7
8
9
10
11
hyperMode:bind({}, "S", nil, toggleApp("com.tinyspeck.slackmacgap"))

hyperMode:bind({}, "V", nil, function()
hyperMode.triggered = true
hs.eventtap.keyStroke({"cmd", "shift"}, "V", 0)
end)

hyperMode:bind({}, "R", nil, function()
hyperMode.triggered = true
hs.reload()
end)

查某个 App 的 bundle ID:

1
osascript -e 'id of app "应用名"'

七、卸载 / 撤销

完整退出方案非常干净:

1
2
3
4
5
6
7
launchctl unload ~/Library/LaunchAgents/com.local.KeyRemapping.plist
rm ~/Library/LaunchAgents/com.local.KeyRemapping.plist

hidutil property --set '{"UserKeyMapping":[]}'

brew uninstall --cask hammerspoon
rm -rf ~/.hammerspoon

无残留、无驱动、无后台服务


八、进阶玩法

基础架构搭好后,Hammerspoon 还能干很多事,都可以接到 Hyper 模态下:

  • 窗口管理hyperMode:bind({}, "H", nil, function() hs.window.focusedWindow():moveToUnit({0, 0, 0.5, 1}) end) —— 左半屏。
  • 快速切应用 + 自动布局:进入”工作模式”时一键打开 iTerm + Cursor + 浏览器,按 4:3:3 比例排好。
  • 剪贴板历史 / URL 处理 / 系统音量 / 鼠标定位 —— 都是社区成熟方案。

推荐看:


九、总结

关键点 做法 为什么
重映射 Caps Lock hidutil macOS 原生 API,无驱动、无副作用
选择中转键 F18 物理不存在,无冲突,无误触
区分单击/按住 Hammerspoon 模态 + triggered 标志 用一个键承担 Esc + 修饰键两种语义
添加快捷键 hyperMode:bind 改一行 Lua,自动 reload
卸载 删 2 个文件 没有任何残留

如果你之前被 Karabiner 的兼容性折磨过,或者干脆没装过任何改键工具,这套方案值得一试。用 macOS 自己的能力 + 一个轻量 Lua 脚本,把利用率最低的键变成你最高效的快捷键入口。

让 Caps Lock 成为最有价值的键:基于 hidutil + Hammerspoon 的零污染 Hyper Key 方案

https://simonsu.net/2026/06/04/capslock妙用/

Author

simonisacoder

Posted on

2026-06-04

Licensed under

Comments