fix: use listenOnly event tap to prevent keyboard lag#25
Conversation
CGEvent.tapCreate with .defaultTap creates an active filter that serialises every keystroke through our callback. Any main-thread latency (DispatchQueue.main.async, lock contention) stalls all keyboard and mouse input system-wide. Switch to .listenOnly — we only observe the trigger key, never need to block or modify events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With .listenOnly, the callback return value is ignored by the system — returning nil no longer swallows events. Remove the two return-nil branches and the now-unnecessary isModifierKey check so the code faithfully reflects that we never intercept events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
代码审查 — PR #25
评分:5/10 ❌ REQUEST_CHANGES
| 维度 | 得分 | 说明 |
|---|---|---|
| 正确性 | 1/3 | .listenOnly 下 return nil 不再吞事件,两处功能回退 |
| 简洁性 | 2/2 | 改动精简,只改了该改的地方 |
| 可读性 | 2/2 | 注释清楚解释了 why |
| 可维护性 | 1/2 | 留下了死代码路径(return nil 永远不会生效) |
| 安全性 | 1/1 | 无问题 |
总结
方向是对的 —— .defaultTap 阻塞事件管线确实是系统卡顿的根因,改 .listenOnly 解决了这个问题。
但 .listenOnly 模式下,回调的返回值被系统完全忽略。return nil 不再有「吞掉事件」的效果。这导致两个功能回退:
- 录制模式(第 198 行):录制触发键时,被录制的按键会穿透到当前活跃的应用中。用户按
F5录制,F5同时会被输入到编辑器里。 - 非修饰键触发(第 206-209 行):如果用户配置了普通键(非修饰键)作为触发键,按下时触发键的字符会被输入到目标应用中。之前
.defaultTap会吞掉这个事件,现在不会了。
如果默认触发键是 Fn(修饰键),那第 2 点不会被立即发现(修饰键本身不产生字符输入)。但代码明确支持非修饰键作为触发键,这是一个真实的回退。
建议方案
既然你不需要修改事件,只是在两个场景下需要吞事件,可以考虑:
- 录制模式用
CGEvent.tapEnable(tap:enable:)临时切回 active filter - 或者接受非修饰键触发时的穿透行为,但至少要把
return nil的注释和死代码清理掉,别让后来的人以为这行代码还在工作
There was a problem hiding this comment.
代码审查 — PR #25(第二轮)
评分:9/10 ✅ APPROVE
| 维度 | 得分 | 说明 |
|---|---|---|
| 正确性 | 3/3 | .listenOnly 解决了根因,死代码已清理 |
| 简洁性 | 2/2 | 只改了该改的,干净利落 |
| 可读性 | 2/2 | 注释准确解释了 why |
| 可维护性 | 1/2 | 见下方说明 |
| 安全性 | 1/1 | 无问题 |
总结
上轮 review 提出的问题已全部解决。第二个 commit 把 .listenOnly 下的两处 return nil 死代码清理干净了,现在代码和实际行为完全一致 —— 所有路径都 return Unmanaged.passRetained(event),语义清晰。
根因修复是正确的。.defaultTap 创建 active filter 把每个键盘事件串行化到回调,主线程有任何抖动就会阻塞系统全局输入。这个 app 只需要观察触发键,从来不需要修改或拦截事件,用 .listenOnly 是唯一正确的做法。
扣 1 分的原因:非修饰键作为触发键时,按键会穿透到前台应用(.listenOnly 无法吞事件)。默认触发键是 Fn 所以用户几乎不会遇到,但代码层面确实支持配置普通键为触发键。建议后续在 UI 层面限制只允许修饰键作为触发键,或者在录制模式中过滤掉普通键 —— 不阻塞本次合并。
Summary
将
CGEvent.tapCreate的 options 从.defaultTap改为.listenOnly。Root Cause
.defaultTap创建 active filter,每一个键盘事件都必须经过回调处理后才能继续传递。主线程有任何延迟时,所有键盘和鼠标输入都会被阻塞,导致系统级卡顿(打字无响应、光标移动卡顿)。CPU 和内存占用正常,因为这不是性能问题,而是事件传递被串行化阻塞。
Fix
改为
.listenOnly(被动监听),事件直接传递到目标应用,回调仍会触发用于检测触发键。同一问题已在 PetTypeless 中修复:ZhaoChaoqun/pet-typeless#19
Test plan
🤖 Generated with Claude Code