windows: the low-level hook's modifier snapshot describes the event, not the moment before it - #69
Conversation
converted() trusted layout_switcher.current() for the source layout. If the layout moved on since the word was typed — exactly what the next word does on auto-correction — the guess was wrong, transliterate_to's own guard refused it (no letter of that layout in the text), and the force-switch silently did nothing. Both directions are tried now, current first, falling back to the swap when the text does not belong to it. transliterate_to already refuses a source layout the text carries no letter of, so the wrong direction rejects itself and nothing is forced through blind. Measured on macOS 26 and Windows Server 2025 with the fix: repeated presses on one selection convert it back and forth, each from the first press, whichever layout is active at the time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ment before it GetAsyncKeyState inside WH_KEYBOARD_LL reports the keyboard as it was before the event being delivered has taken effect. A Ctrl release therefore arrived reading "Ctrl held", and with nothing typed afterwards that reading was the last one the engine got: it believed the force-switch chord was still down and waited for a release it had already been handed — CHORD_RELEASE_WAIT is five seconds, and the wait ended only when the next keystroke refreshed the snapshot. Measured on Windows Server 2025 (over RDP, 0.34.0): six manual switches took 1.65–4.69 s from `applying correction` to the layout change, each exactly as long as the user took to press the hotkey again; automatic corrections on the same path took 0 ms. The second press then landed inside the re-arm window and was swallowed, or undid the first, or started a fresh correction on top of it. convert_selection runs the same wait first, so selection conversion never got as far as a log line. With this change the same presses take 0.13–0.26 s and selection conversion works. For a modifier the event is about, the event is the truth: the platform applies the delivered key's own press/release to the snapshot, reading the other side of the same modifier live so that releasing one Shift while the other is held keeps Shift. The transition is a method on Modifiers in poltertype-types, platform-free and unit-tested; the VK mapping and the live reads sit in a modifiers.rs sibling of the listener, since `xtask style` refuses a seventh free function beside the type. Nothing changes for macOS, whose event tap already hands out post-event flags, nor for Linux. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
vstrelnikof
left a comment
There was a problem hiding this comment.
The Windows modifier-snapshot fix is correct and well-structured. GetAsyncKeyState inside WH_KEYBOARD_LL reports the keyboard state before the event has taken effect — so a Ctrl release still reads "Ctrl held", and the engine's chord-release wait stalls until the next keystroke.
The fix applies the event's own transition via Modifiers::after_transition(): a clean, platform-free method in poltertype-types with 4 unit tests covering release-clears, press-sets, other-side-held, and isolation from unrelated modifiers. The Windows-specific modifiers_for_event() in a new modifiers.rs sibling reads the other physical key of the same modifier live. Nothing changes for macOS or Linux — correct.
407 core tests + 6 types tests pass, clippy clean on the host.
Stacked on #68: this branch carries that PR's commit until it lands. The two changes touch different crates and do not depend on each other; they share a branch only so both Windows measurements below come from one build.
On Windows the manual switch-last hotkey has been stalling since 0.30, and it is not the RDP transport and not
hold_keys— it is where the hook reads the modifier state from.GetAsyncKeyStateinsideWH_KEYBOARD_LLreports the keyboard as it was before the event being delivered has taken effect. A Ctrl release therefore arrives reading "Ctrl held". With nothing typed afterwards that snapshot is the last one the engine gets,trigger_key_down()'s fallback sees a held set matching the chord's modifiers, andwait_for_trigger_release(the #51 pass) waits for a release it has already been handed — up toCHORD_RELEASE_WAIT, five seconds, or until the next keystroke refreshes the snapshot.Measured on Windows Server 2025 over RDP, stock behaviour on a 0.34.0 build (debug log):
applying correction→WM_INPUTLANGCHANGEREQUESTEach manual delay is exactly as long as the user took to press the hotkey again: the second press is the keystroke that refreshes the snapshot and releases the first. That second press then lands inside
FORCE_SWITCH_REARMand is swallowed, or arrives as an undo of the first correction, or starts a fresh one on top — the "works on the second press, or eats the word" a user sees.convert_selectionruns the same wait first, so on Windows selection conversion never got as far as a log line.The fix is platform code, in
poltertype-input. For a modifier the event is about, the event is the truth: the listener applies the delivered key's own press/release to the snapshot, reading the other side of the same modifier live so that releasing one Shift while the other is held keeps Shift. The transition itself —Modifiers::after_transition(key, pressed, other_side_down)with a smallModifierKeyenum — lives inpoltertype-types, platform-free and unit-tested (four tests); the VK mapping and the live reads sit inwindows/modifiers.rs, a sibling of the listener, sincextask stylerefuses a seventh free function besideWindowsListener. Nothing changes for macOS, whose event tap hands out post-event flags already, nor for Linux, whose listeners track their own state.Measured with the fix, same machine, same scenario: the manual switches take 0.135 · 0.262 · 0.224 · 0.218 s, none is swallowed by the re-arm window, none undoes its predecessor, and selection conversion converts — both directions, first press.
cargo xtask styleclean;cargo test -p poltertype-types6 passed;cargo clippy -D warningsclean on the host and forx86_64-pc-windows-gnu(poltertype-input,poltertype-types); the Windows release built and installed via the project's own MSI.