Skip to content

Commit 8afeb96

Browse files
committed
fix(shortcuts): harden Windows shortcut key tracking
Ignore the helper's own injected events: tag the Ctrl+V paste chord and modifier-masking releases with a sentinel dwExtraInfo and skip tagged events in HookCallback, so our synthetic input never feeds back into shortcut matching or pressed-key tracking. Mirrors the macOS helper's SELF_GENERATED_EVENT_TAG. Forward shortcut-modifier events on the dedup path: when the helper still thinks a modifier is down (e.g. after a stale-key recheck cleared the app's copy), a genuine re-press looks like a duplicate here; emit it anyway for shortcut keys so the app can rebuild its active-key set on the first retry. desktop: only re-run shortcut matching when the active-key set actually changes. addActiveKey/removeActiveKey now report whether the set changed and checkShortcuts is gated on it; redundant key events just refresh the recheck timestamp. Avoids the per-event matching/emit churn from forwarded auto-repeats.
1 parent a135117 commit 8afeb96

4 files changed

Lines changed: 42 additions & 11 deletions

File tree

apps/desktop/src/main/managers/shortcut-manager.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,27 +226,36 @@ export class ShortcutManager extends EventEmitter {
226226
if (!this.isKnownKeycode(keyCode)) {
227227
return;
228228
}
229-
this.addActiveKey(keyCode);
230-
this.checkShortcuts();
229+
if (this.addActiveKey(keyCode)) {
230+
this.checkShortcuts();
231+
}
231232
}
232233

233234
private handleKeyUp(payload: KeyEventPayload) {
234235
const keyCode = this.getKeycodeFromPayload(payload);
235236
if (!this.isKnownKeycode(keyCode)) {
236237
return;
237238
}
238-
this.removeActiveKey(keyCode);
239-
this.checkShortcuts();
239+
if (this.removeActiveKey(keyCode)) {
240+
this.checkShortcuts();
241+
}
240242
}
241243

242-
private addActiveKey(keyCode: number) {
244+
private addActiveKey(keyCode: number): boolean {
245+
const wasActive = this.activeKeys.has(keyCode);
243246
this.activeKeys.set(keyCode, { keyCode, timestamp: Date.now() });
244-
this.emitActiveKeysChanged();
247+
if (!wasActive) {
248+
this.emitActiveKeysChanged();
249+
}
250+
return !wasActive;
245251
}
246252

247-
private removeActiveKey(keyCode: number) {
248-
this.activeKeys.delete(keyCode);
249-
this.emitActiveKeysChanged();
253+
private removeActiveKey(keyCode: number): boolean {
254+
const changed = this.activeKeys.delete(keyCode);
255+
if (changed) {
256+
this.emitActiveKeysChanged();
257+
}
258+
return changed;
250259
}
251260

252261
private removeActiveKeys(keyCodes: number[]) {

packages/native-helpers/windows-helper/src/KeycodeConstants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23

34
namespace WindowsHelper
@@ -27,5 +28,14 @@ internal static class KeycodeConstants
2728
};
2829

2930
internal static readonly HashSet<int> ModifierKeyCodeSet = new(ModifierKeyCodes);
31+
32+
/// <summary>
33+
/// Sentinel stamped into dwExtraInfo on keyboard events the helper injects itself
34+
/// (the Ctrl+V paste chord and modifier-masking releases in AccessibilityService).
35+
/// The low-level keyboard hook skips any event carrying this tag, so our own
36+
/// synthetic input never feeds back into shortcut matching or pressed-key tracking.
37+
/// Mirrors the macOS helper's SELF_GENERATED_EVENT_TAG (0x414D4943414C5048 = "AMICALPH").
38+
/// </summary>
39+
internal static readonly IntPtr SelfInjectedEventTag = unchecked((IntPtr)0x414D4943414C5048L);
3040
}
3141
}

packages/native-helpers/windows-helper/src/Services/AccessibilityService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ private static INPUT CreateKeyboardInput(ushort virtualKey, uint flags = 0)
149149
wScan = 0,
150150
dwFlags = flags,
151151
time = 0,
152-
dwExtraInfo = IntPtr.Zero,
152+
dwExtraInfo = KeycodeConstants.SelfInjectedEventTag,
153153
}
154154
}
155155
};

packages/native-helpers/windows-helper/src/ShortcutMonitor.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,32 @@ private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
135135
{
136136
var kbStruct = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
137137

138+
// Skip our own injected events (tagged dwExtraInfo) to avoid feedback loops.
139+
if (kbStruct.dwExtraInfo == KeycodeConstants.SelfInjectedEventTag)
140+
{
141+
return CallNextHookEx(hookId, nCode, wParam, lParam);
142+
}
143+
138144
var vkCode = (int)kbStruct.vkCode;
139145
var isModifier = IsModifierKey(kbStruct.vkCode);
140146

141147
if (isModifier)
142148
{
143149
var wasDown = ShortcutManager.Instance.IsModifierPressed(vkCode);
144150
var isDown = isKeyDown;
151+
var isShortcutKey = ShortcutManager.Instance.IsShortcutKey(vkCode);
145152

146153
if (wasDown == isDown)
147154
{
155+
if (isShortcutKey)
156+
{
157+
EmitKeyEvent(isDown ? HelperEventType.KeyDown : HelperEventType.KeyUp, vkCode);
158+
}
159+
148160
return CallNextHookEx(hookId, nCode, wParam, lParam);
149161
}
150162

151-
if (ShortcutManager.Instance.IsShortcutKey(vkCode))
163+
if (isShortcutKey)
152164
{
153165
var resyncResult = ShortcutManager.Instance.ValidateAndResyncKeyState(vkCode);
154166
EmitResyncKeyEvents(resyncResult, vkCode);

0 commit comments

Comments
 (0)