修复远程桌面软件按 modifier key 时产生幽灵 'a' 按键的问题#1116
Conversation
Some remote desktop software (e.g. Parsec, older Deskflow) sends flagsChanged events via IOHIDPostEvent without setting event.key.keyCode for modifier keys, causing it to default to 0 (kVK_ANSI_A). The osxKeycodeToRime function then maps keycode 0 to XK_a via additionalCodeMappings, producing a ghost 'a' keypress whenever Shift, CapsLock, or other modifier keys are pressed. This fix validates that event.keyCode corresponds to a known modifier virtual keycode for flagsChanged events. When an invalid keycode is detected (such as 0), the correct modifier keycode is inferred from the changed modifier flags instead. Fixes rime#825 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
本 PR 旨在修复部分远程桌面软件发送 .flagsChanged 事件时 event.keyCode 异常(默认为 0 / kVK_ANSI_A)导致按下 modifier key 产生“幽灵 a”输入的问题(#825)。
Changes:
- 在
.flagsChanged分支中校验event.keyCode是否为合法的 modifier 虚拟键码集合 - 当
keyCode非法时,基于modifierFlags的变化(changes)推断正确的 modifier keyCode,再转换为 Rime keycode
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // For flags-changed event, keyCode is available since macOS 10.15 (#715) | ||
| // Some remote desktop software (e.g. Parsec) sends flagsChanged events with | ||
| // keyCode defaulting to 0 (kVK_ANSI_A) instead of the actual modifier keycode, | ||
| // causing a ghost 'a' keypress. Validate and infer the correct keycode from | ||
| // the changed modifier flags when necessary. (#825) | ||
| let modifierKeycodes: Set<UInt16> = [ | ||
| UInt16(kVK_Shift), UInt16(kVK_RightShift), | ||
| UInt16(kVK_CapsLock), | ||
| UInt16(kVK_Control), UInt16(kVK_RightControl), | ||
| UInt16(kVK_Option), UInt16(kVK_RightOption), | ||
| UInt16(kVK_Command), UInt16(kVK_RightCommand), | ||
| UInt16(kVK_Function) |
There was a problem hiding this comment.
This code references kVK_* virtual keycode constants, which are defined in the Carbon module. SquirrelInputController.swift currently only imports InputMethodKit, so this may fail to compile unless Carbon is explicitly imported here. Consider adding import Carbon (or import Carbon.HIToolbox) in this file, or otherwise ensuring the constants are available without relying on transitive imports.
| keyCode = UInt16(kVK_Option) | ||
| } else if changes.contains(.command) { | ||
| keyCode = UInt16(kVK_Command) | ||
| } else { |
There was a problem hiding this comment.
In the fallback else branch, handled = true then break exits the .flagsChanged case before lastModifiers = modifiers is executed. If this branch is hit (e.g. an invalid keyCode with an unhandled modifier flag change), lastModifiers remains stale and future changes calculations can be incorrect/repeated. Consider still updating lastModifiers (and performing any needed rimeUpdate()) before breaking, or restructuring to skip processing without skipping state updates.
| } else { | |
| } else { | |
| // Fallback: we could not infer a specific modifier keycode. | |
| // Still update modifier state to avoid stale `lastModifiers` | |
| // and inconsistent `changes` computation on subsequent events. | |
| lastModifiers = modifiers | |
| rimeUpdate() |
| let modifierKeycodes: Set<UInt16> = [ | ||
| UInt16(kVK_Shift), UInt16(kVK_RightShift), | ||
| UInt16(kVK_CapsLock), | ||
| UInt16(kVK_Control), UInt16(kVK_RightControl), | ||
| UInt16(kVK_Option), UInt16(kVK_RightOption), | ||
| UInt16(kVK_Command), UInt16(kVK_RightCommand), | ||
| UInt16(kVK_Function) | ||
| ] |
There was a problem hiding this comment.
modifierKeycodes is re-created as a Set on every .flagsChanged event. Since this handler can be called frequently, consider moving this to a static let (or another cached constant) to avoid repeated allocations and hashing work.
概述
修复 #825
部分远程桌面软件(如 Parsec、旧版 Deskflow)通过
IOHIDPostEvent发送flagsChanged事件时,未正确设置event.key.keyCode,导致其默认为0(即 macOS 上的kVK_ANSI_A)。自 PR #936 新增additionalCodeMappings后,osxKeycodeToRime()会将 keycode 0 映射为XK_a,导致通过远程桌面按下 Shift、CapsLock 等 modifier key 时产生幽灵 'a' 按键。根因分析
NX_FLAGSCHANGED事件时keyCode = 0(默认值),而非实际的 modifier key virtual keycodeosxKeycodeToRime(keycode: 0, keychar: nil, ...)→keycodeMappings中无 0 的映射 →keychar为nil跳过 ASCII 路径 →additionalCodeMappings[0]返回XK_a(因为kVK_ANSI_A == 0)修复方案
在
.flagsChanged处理逻辑中,校验event.keyCode是否对应已知的 modifier key virtual keycode(kVK_Shift、kVK_CapsLock等)。当检测到无效的 keycode(如 0)时,从变化的 modifier flags(NSEvent.ModifierFlags)推断正确的 modifier keycode。这是 Squirrel 侧的防御性修复——虽然 bug 源自远程桌面软件未正确设置 keyCode(Deskflow 已在 deskflow/deskflow#9435 修复),但 Parsec 等闭源软件可能不会及时修复。让 Squirrel 对
flagsChanged事件中的无效 keycode 具备容错能力是更合理的做法。局限性
从 flags 推断 keycode 时,无法区分左右 modifier(如
Shift_LvsShift_R),因此默认使用左侧。但这仅影响event.keyCode本身已经无效的情况。测试计划
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com