Skip to content

修复远程桌面软件按 modifier key 时产生幽灵 'a' 按键的问题#1116

Open
HunterTom94 wants to merge 1 commit intorime:masterfrom
HunterTom94:fix/ghost-keypress-remote-desktop
Open

修复远程桌面软件按 modifier key 时产生幽灵 'a' 按键的问题#1116
HunterTom94 wants to merge 1 commit intorime:masterfrom
HunterTom94:fix/ghost-keypress-remote-desktop

Conversation

@HunterTom94
Copy link
Copy Markdown

@HunterTom94 HunterTom94 commented Mar 30, 2026

概述

修复 #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' 按键

根因分析

  1. 远程桌面软件发送 NX_FLAGSCHANGED 事件时 keyCode = 0(默认值),而非实际的 modifier key virtual keycode
  2. osxKeycodeToRime(keycode: 0, keychar: nil, ...)keycodeMappings 中无 0 的映射 → keycharnil 跳过 ASCII 路径 → additionalCodeMappings[0] 返回 XK_a(因为 kVK_ANSI_A == 0

修复方案

.flagsChanged 处理逻辑中,校验 event.keyCode 是否对应已知的 modifier key virtual keycode(kVK_ShiftkVK_CapsLock 等)。当检测到无效的 keycode(如 0)时,从变化的 modifier flags(NSEvent.ModifierFlags)推断正确的 modifier keycode。

这是 Squirrel 侧的防御性修复——虽然 bug 源自远程桌面软件未正确设置 keyCode(Deskflow 已在 deskflow/deskflow#9435 修复),但 Parsec 等闭源软件可能不会及时修复。让 Squirrel 对 flagsChanged 事件中的无效 keycode 具备容错能力是更合理的做法。

局限性

从 flags 推断 keycode 时,无法区分左右 modifier(如 Shift_L vs Shift_R),因此默认使用左侧。但这仅影响 event.keyCode 本身已经无效的情况。

测试计划

  • 验证本地键盘的 Shift/CapsLock/Control/Option/Command 中英切换功能正常
  • 验证通过 Parsec(或类似远程桌面)按 Shift/CapsLock 不再产生幽灵 'a' 按键
  • 验证 modifier key 的释放事件处理正常

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

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>
@HunterTom94 HunterTom94 changed the title Fix ghost 'a' keypress from remote desktop modifier key events 修复远程桌面软件按 modifier key 时产生幽灵 'a' 按键的问题 Mar 30, 2026
@LEOYoon-Tsaw LEOYoon-Tsaw requested a review from Copilot March 31, 2026 13:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +65 to +76
// 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)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
keyCode = UInt16(kVK_Option)
} else if changes.contains(.command) {
keyCode = UInt16(kVK_Command)
} else {
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
} 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()

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +77
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)
]
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.

2 participants