Skip to content

fix: use listenOnly event tap to prevent keyboard lag#25

Merged
ZhaoChaoqun merged 2 commits into
mainfrom
fix-keyboard-lag
Apr 4, 2026
Merged

fix: use listenOnly event tap to prevent keyboard lag#25
ZhaoChaoqun merged 2 commits into
mainfrom
fix-keyboard-lag

Conversation

@ZhaoChaoqun

Copy link
Copy Markdown
Owner

Summary

CGEvent.tapCreate 的 options 从 .defaultTap 改为 .listenOnly

Root Cause

.defaultTap 创建 active filter,每一个键盘事件都必须经过回调处理后才能继续传递。主线程有任何延迟时,所有键盘和鼠标输入都会被阻塞,导致系统级卡顿(打字无响应、光标移动卡顿)。

CPU 和内存占用正常,因为这不是性能问题,而是事件传递被串行化阻塞。

Fix

改为 .listenOnly(被动监听),事件直接传递到目标应用,回调仍会触发用于检测触发键。

同一问题已在 PetTypeless 中修复:ZhaoChaoqun/pet-typeless#19

Test plan

  • 启动 app 后正常打字和移动光标,不再有卡顿
  • Fn 键触发录音功能正常
  • 录制新触发键功能正常

🤖 Generated with Claude Code

ZhaoChaoqun and others added 2 commits April 3, 2026 22:31
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>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

代码审查 — PR #25

评分:5/10 ❌ REQUEST_CHANGES

维度 得分 说明
正确性 1/3 .listenOnlyreturn nil 不再吞事件,两处功能回退
简洁性 2/2 改动精简,只改了该改的地方
可读性 2/2 注释清楚解释了 why
可维护性 1/2 留下了死代码路径(return nil 永远不会生效)
安全性 1/1 无问题

总结

方向是对的 —— .defaultTap 阻塞事件管线确实是系统卡顿的根因,改 .listenOnly 解决了这个问题。

.listenOnly 模式下,回调的返回值被系统完全忽略return nil 不再有「吞掉事件」的效果。这导致两个功能回退:

  1. 录制模式(第 198 行):录制触发键时,被录制的按键会穿透到当前活跃的应用中。用户按 F5 录制,F5 同时会被输入到编辑器里。
  2. 非修饰键触发(第 206-209 行):如果用户配置了普通键(非修饰键)作为触发键,按下时触发键的字符会被输入到目标应用中。之前 .defaultTap 会吞掉这个事件,现在不会了。

如果默认触发键是 Fn(修饰键),那第 2 点不会被立即发现(修饰键本身不产生字符输入)。但代码明确支持非修饰键作为触发键,这是一个真实的回退。

建议方案

既然你不需要修改事件,只是在两个场景下需要吞事件,可以考虑:

  • 录制模式用 CGEvent.tapEnable(tap:enable:) 临时切回 active filter
  • 或者接受非修饰键触发时的穿透行为,但至少要把 return nil 的注释和死代码清理掉,别让后来的人以为这行代码还在工作

Comment thread Sources/KeyMonitor.swift

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

代码审查 — 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 层面限制只允许修饰键作为触发键,或者在录制模式中过滤掉普通键 —— 不阻塞本次合并。

@ZhaoChaoqun ZhaoChaoqun merged commit d89ec5a into main Apr 4, 2026
5 checks passed
@ZhaoChaoqun ZhaoChaoqun deleted the fix-keyboard-lag branch April 4, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant