From c818f12c24cd299dcb00efdf3ed9b2cd0a37dadc Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:22:36 +0100 Subject: [PATCH] fix(hotkeys): release our combos while capturing a new one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A RegisterHotKey combo is delivered to the owning window, never to the WebView. So a combo one of OUR slots holds could not be captured at all — pressing it fired that hotkey instead (Ctrl+Shift+L ran an autofill in the middle of the capture), and the "already used by another hotkey" collision check was unreachable dead code. Swapping two combos was impossible. The capture now suspends all five hotkeys (enabled=0 + vk=0 for quick-search and new-entry, which unregisters without re-registering and is excluded from the failure report) and restores them on finish — silently when cancelled or timed out, with the usual toast on a successful change. Combos held by ANOTHER app still can't be captured — that one is out of our hands, and the 10 s timeout explains it. Co-Authored-By: Claude Opus 4.8 --- js/app.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/js/app.js b/js/app.js index 00bb14d..28a98cb 100644 --- a/js/app.js +++ b/js/app.js @@ -207,6 +207,7 @@ const Bridge = (() => { } let tp = ''; if (combos.totp) { + // (see suspendHotkeys below for why vk=0 matters) const t = autofillComboToWin32(combos.totp); tp = '&totp_mods=' + t.mods + '&totp_vk=' + t.vk; } @@ -215,9 +216,18 @@ const Bridge = (() => { '&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne + tp); }, - // Called by Delphi after a setAutofillHotkeys request, with true if - // BOTH combos registered successfully, false otherwise (clash with - // another app holding a global hotkey). Surfaces a toast. + // Release ALL five global hotkeys while the Settings capture is armed. + // A RegisterHotKey combo never reaches the WebView — Windows delivers + // it to the owning window — so a combo we hold can't be captured, and + // pressing it would FIRE that hotkey mid-capture. vk=0 unregisters a + // slot without re-registering it (and is excluded from the failure + // report). autofillPushHotkeys() restores everything afterwards. + suspendHotkeys() { + if (!active) return; + cmd('cmd://autofill/hotkeys?enabled=0' + + '&qs_mods=0&qs_vk=0&ne_mods=0&ne_vk=0'); + }, + // `failed` = comma-separated SLOT IDS Windows refused (another app // already owns them); empty string = all registered. A clashing hotkey // is otherwise indistinguishable from a broken feature — it silently @@ -7707,6 +7717,10 @@ async function init() { btn.classList.add('is-capturing'); const original = btn.textContent; btn.textContent = 'Press combo… (Esc to cancel)'; + // Free our own combos so they reach this listener instead of + // firing their hotkey. Restored by finish() / the success path. + hotkeyPushSilent = true; + Bridge.suspendHotkeys(); // A combo already owned by another app via RegisterHotKey is // swallowed by Windows — it is delivered to that app, never to us, @@ -7725,7 +7739,12 @@ async function init() { btn.dataset.capturing = ''; btn.classList.remove('is-capturing'); document.removeEventListener('keydown', onKey, true); - if (restore) btn.textContent = original; + if (restore) { + btn.textContent = original; + // Cancelled / timed out: nothing changed, so re-register the + // combos we suspended — quietly, it's not a user action. + autofillPushHotkeys(true); + } } function onKey(e) {