# Capslock 如何工作：F18、事件钩子，以及只归还自己改动的映射

> macOS 交付的 Caps Lock 并不适合当修饰键。Capslock 把它映射成 F18，用主动事件钩子读取， 并且只改动系统键位映射中属于自己的那一项。

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

Caps Lock 占着键盘上最好的位置，干着最没用的活。Capslock 给它换了一份工作：轻按是
Escape，按住是 Hyper，再与四个修饰键组合，得到十六个控制平面。本文解释一个原生应用
如何在 macOS 上做到这一点，而不需要内核扩展、驱动或特权 Helper。

## Caps Lock 不是修饰键 {#not-a-modifier}

macOS 并不把 Caps Lock 报告为「按下、抬起」的一对事件，而是报告一个来回翻转的锁定
状态，还加了一小段延迟，防止误触切换大小写。这对输入大写字母没问题，对一个需要按住
的修饰键却毫无用处。

所以 Capslock 先做一次映射：启用时通过 `hidutil` 让物理 Caps Lock 发送 **F18**——
一个苹果键盘上根本不存在的键。F18 以普通的按下、抬起事件到达，应用可以据此判断
「按住」和「松开」。

F18 还有第二个好处：它不是系统修饰键。按住 Caps 和 Shift 再按 H，到达的是「F18 按住、
Shift 按住、H」。macOS 不会把 F18 与 Shift 合成别的东西，于是四个修饰键都可以零成本
地叠加在 Caps 之上。

## 用事件钩子读取按键 {#event-tap}

Capslock 在 HID 层级、队列最前端安装一个主动的 `CGEventTap`。「主动」意味着它可以修改
或丢弃事件，而不只是旁观。macOS 用**辅助功能**权限管控这一能力；它不需要输入监控权限，
那只适用于只读的监听钩子。

如果启动时还没有授权，应用会持续重试；你一授权，钩子立即安装，无需重启应用。钩子运行
在独立线程上，回调里几乎只做整数比较，不会拖慢打字。

## 解析组合键 {#chords}

F18 按下时，应用记录「Caps 已按住」并吞掉这个事件；F18 抬起时，它判断这次按键是哪一种：

- **轻按**：期间没有按其他键，执行轻按动作。默认配置里是 Escape。
- **组合键**：Caps 加另一个键，执行该键在当前平面上映射的动作，原始按键不会到达前台应用。
- **双击**：如果某个映射定义了双击，就执行双击动作。单击会等过双击窗口，两者不会同时触发。

平面由你与 Caps 一起按住的修饰键决定。每个修饰键是一位：Command 1、Option 2、
Control 4、Shift 8，它们的和对应十六个平面之一，从单独按 Caps 的 0 号到四键全按的
15 号。匹配是精确的：多按了一个修饰键的组合，绝不会退回到更宽泛的平面。因此给导航键
加上 Shift 可以表示「选择」，而不会在别处带来意外。

Capslock 合成的每个事件都带有标记，钩子会跳过带标记的事件。否则 Caps+H 发出的方向键
会立刻又被钩子截回来。

## 两种 Hyper {#hyper-modes}

早期的 Capslock 布局通过按住右侧四个修饰键来实现 Hyper。Alfred、Moom 等启动器和窗口
管理工具会监听这个组合，很多人的快捷键也建立在它之上。

Capslock 把它做成一个选项，而不是实现细节：

- **经典兼容**：Caps 按住期间持续按住右侧 Command、Option、Control 与 Shift，其他
  应用照旧能看到 Hyper。
- **原生纯净**：Hyper 只在 Capslock 内部生效。按住 Caps 时其他应用什么也看不到，只能
  看到它产生的动作。

两种模式下，平面都由你实际按下的修饰键计算；Capslock 为兼容而按住的右侧修饰键会被
排除，否则每个组合键都会落到 15 号平面。

## 只拥有自己改动的部分 {#ownership}

`hidutil property --set` 会整体替换用户键位映射。最直接的实现是启动时写入自己的列表、
退出时清空，这会删掉其他工具或用户自己设置的映射。Capslock 把已有映射当作别人的东西：

1. 读取当前映射；如果已有条目重映射了 Caps Lock，就拒绝启动。
2. 改动之前，先写一份恢复日志，记录原来的映射和即将添加的那一个条目。
3. 只合并自己的条目，并回读结果确认写入成功。
4. 暂停或退出时，只移除这一条并恢复原映射——除非期间有别人改过它。

如果运行期间有其他程序修改了映射，Capslock 会停止并报告冲突，而不是去抢最后一次写入。
睡眠唤醒、插入键盘、切换用户会话之后，它都会重新核对。这也是为什么「暂停」会还给你
一个真正的 Caps Lock，而不是一个被透传的 F18。

## 应用没有正常退出时 {#crash}

崩溃或强制退出会跳过第 4 步的清理。有两道保障：应用内嵌一个小型看门狗，它读取恢复
日志并盯住父进程，如果应用退出时没有释放映射，就恢复原来的映射；下次启动时，应用也会
在安装钩子之前先核对遗留的日志。

如果需要手动清除，请使用应用自带的命令，它只移除自己添加的 Caps Lock 条目：

```bash
/Applications/Capslock.app/Contents/MacOS/Capslock --restore-original-caps-lock
```

不要用空列表重置映射：那是全局操作，会把其他工具的映射一并删掉。何况这份映射本来
就不会在重启后保留。

## 一切留在本地 {#local}

以上所有处理都在你的 Mac 上完成。Capslock 不含分析或广告 SDK，不会把按键、映射、剪贴板
内容或使用数据发送到任何地方。诊断日志不记录输入的文字、剪贴板内容、Shell 输出和应用
历史。存储了什么、何时联网，见[隐私说明](/zh/docs/reference/privacy/)。

---

反链：

- [Capslock 原生版](/zh/blog/post/capslock-goes-native/)
