diff --git a/PLAN.md b/PLAN.md index cb765b5..c8e60ad 100644 --- a/PLAN.md +++ b/PLAN.md @@ -676,9 +676,19 @@ at the instant the handler runs the user is still holding `Win+Alt`. Synthesizin state delivers `Win+Alt+Ctrl+V`, which is a paste in no application on earth, and nothing happens. It presented as "unspooling advances the spool but never pastes", and it looked intermittent because a handler that happened to run after the keys came up worked perfectly — which is how it survived a -round of testing. **So the addon lifts every modifier that is currently down before pressing Ctrl+V**, -and does not restore them: the user's own keys are still physically held, their release is harmless, -and re-pressing `Win` would open the Start menu. +round of testing. **So the addon lifts every modifier that is currently down before pressing Ctrl+V** — and +then puts them back. + +Lifting them was only half the answer, and the half alone was worse than the disease. Releasing keys +the user is still physically holding leaves Windows believing they are up, so the *next* `U` arrives +as a bare `u` and is typed into their document: unspooling worked once and then printed `uuuu`. The +gesture this whole design rests on — hold `Win+Alt`, tap `U` as often as you like — was broken by the +fix for the bug above it. Restoring every modifier that was down is what makes the repeat survive. + +Re-pressing `Win` then threatens a Start menu, because Windows opens it on a `Win` release with no +other key in between, and a restored `Win` looks exactly like that. An unassigned virtual key sent +straight after marks the chord as used. Measured: `Win+Alt` held, `U` tapped three times, three +distinct clips pasted, no stray characters, no Start menu on release. Two consequences follow from taking the user's account seriously rather than the code's. Pasting the whole spool pastes too — one key that pastes and one that silently changes the clipboard is an diff --git a/native/clipboard/src/clipboard_win.cc b/native/clipboard/src/clipboard_win.cc index 305ff8a..d871a11 100644 --- a/native/clipboard/src/clipboard_win.cc +++ b/native/clipboard/src/clipboard_win.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -325,50 +326,56 @@ Napi::Value SendPaste(const Napi::CallbackInfo& info) { GetWindowThreadProcessId(foreground, &foreground_pid); if (foreground_pid == GetCurrentProcessId()) return Napi::Boolean::New(env, false); - // **Release whatever the user is still holding first.** + // **Lift whatever the user is holding, paste, then put it back.** // - // The hotkey that asked for this paste fires on the key *down*, so at this instant Win and Alt - // are almost certainly still held — the user has not let go of `Win+Alt+U` yet. Synthesizing - // Ctrl+V into that state delivers `Win+Alt+Ctrl+V`, which is not a paste in any application, and - // nothing happens. It cost a user their trust in the feature before it was understood, and it - // looked intermittent because a handler that happened to run after the keys came up worked fine. + // The hotkey fires on the key *down*, so at this instant Win and Alt are still held — the user + // has not let go of `Win+Alt+U`. Synthesizing Ctrl+V into that state delivers `Win+Alt+Ctrl+V`, + // which is a paste in no application, and nothing happens. // - // So: lift every modifier that is currently down, then press Ctrl+V cleanly. They are not - // restored afterwards. The user's own keys are still physically held and their next release is - // harmless, whereas re-pressing Win here would open the Start menu. + // Lifting them is only half the answer, and the half on its own is worse than the disease. + // Releasing keys the user is still physically holding leaves Windows believing they are up, so + // the *next* `U` arrives as a bare `u` and gets typed into their document. Unspooling worked + // once and then printed `uuuu`. **So every modifier that was down is pressed again afterwards**, + // and the repeat gesture survives: hold Win+Alt, tap U as often as you like. const WORD kModifiers[] = {VK_LWIN, VK_RWIN, VK_LMENU, VK_RMENU, VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL}; - std::vector inputs; + std::vector held; for (WORD vk : kModifiers) { - if ((GetAsyncKeyState(vk) & 0x8000) == 0) continue; - INPUT up = {}; - up.type = INPUT_KEYBOARD; - up.ki.wVk = vk; - up.ki.dwFlags = KEYEVENTF_KEYUP; - inputs.push_back(up); + if ((GetAsyncKeyState(vk) & 0x8000) != 0) held.push_back(vk); } - const size_t released = inputs.size(); - - INPUT press = {}; - press.type = INPUT_KEYBOARD; - press.ki.wVk = VK_CONTROL; - inputs.push_back(press); - - press.ki.wVk = 'V'; - inputs.push_back(press); - - INPUT release = {}; - release.type = INPUT_KEYBOARD; - release.ki.dwFlags = KEYEVENTF_KEYUP; - release.ki.wVk = 'V'; - inputs.push_back(release); - - release.ki.wVk = VK_CONTROL; - inputs.push_back(release); + std::vector inputs; + const auto key = [&inputs](WORD vk, bool down) { + INPUT input = {}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = vk; + input.ki.dwFlags = down ? 0 : KEYEVENTF_KEYUP; + inputs.push_back(input); + }; + + for (WORD vk : held) key(vk, false); + + key(VK_CONTROL, true); + key('V', true); + key('V', false); + key(VK_CONTROL, false); + + // Put them back in the order they were found, so the user's own eventual release is the one that + // ends them. + for (WORD vk : held) key(vk, true); + + // Re-pressing Win would otherwise open the Start menu the moment the user lets go: Windows opens + // it on a Win release that had no other key in between, and ours would look exactly like that. + // An unassigned virtual key marks the chord as used without doing anything else. + const bool restoredWin = std::find(held.begin(), held.end(), static_cast(VK_LWIN)) != held.end() || + std::find(held.begin(), held.end(), static_cast(VK_RWIN)) != held.end(); + if (restoredWin) { + key(0xE8, true); + key(0xE8, false); + } - const UINT expected = static_cast(released + 4); + const UINT expected = static_cast(inputs.size()); const UINT sent = SendInput(expected, inputs.data(), sizeof(INPUT)); return Napi::Boolean::New(env, sent == expected); }