From 39406d712ed27ad9b6c32d3ad74dc97ca28d72ea Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Thu, 11 Jun 2026 14:31:12 +0100 Subject: [PATCH] feat: Ctrl+Shift+Q quick-search-fill + Edge browser directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl+Shift+Q quick-search + autofill - New global hotkey: capture the foreground HWND, restore the window if hidden, pop the quick-search modal in "fill mode". On pick, the password is SendInput'd into the saved HWND — no clipboard touch. - hide_after flag added to cmd://autofill/execute: when set (tray-mode hotkey), Delphi MinimizeToTray's *after* SendInput completes. Hiding before SendInput would trip Win10/11 anti-focus-stealing rules and block focus handoff to the target. - Quick-search modal hint text adapts to fill vs copy mode. - Esc / close in fill mode sends cmd://autofill/cancel so a stale HWND doesn't get reused by an unrelated Ctrl+Shift+L later. Compile-time browser engine switch - {.$DEFINE USE_EDGE_BROWSER} in UMainForm.pas selects between TTMSFNCWebBrowser (default, cross-platform abstraction) and TTMSFNCEdgeWebBrowser (Windows-only WebView2 wrapper). Both inherit from TTMSFNCCustomWebBrowser so the bridge cmd:// glue is unchanged; the field type is a conditional alias TWebBrowserClass. - WebBrowser is created dynamically in FormCreate so neither variant needs a second .fmx. Events are wired BEFORE Parent assignment so OnInitialized doesn't race the WebView2 async init on fast/pre-warmed Edge installs (was silently missing the disable-context-menu / disable-accelerator-keys calls). - Native context menu disabled by assigning an empty PopupMenu1 (works for both backends, unlike OnGetContextMenu which is publish-gated via {$IFNDEF FNCLIB} on TTMSFNCWebBrowser). Co-Authored-By: Claude Opus 4.7 --- delphi-backend/Source/PM.Bridge.pas | 33 ++++++ delphi-backend/UMainForm.fmx | 10 +- delphi-backend/UMainForm.pas | 168 ++++++++++++++++++++++++++-- delphi-backend/assets/assets.res | Bin 373436 -> 376060 bytes js/app.js | 63 +++++++++-- 5 files changed, 252 insertions(+), 22 deletions(-) diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 8668693..973202f 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -109,6 +109,12 @@ type FOnDebugHotkey: TProc; FNewEntryHotkeyRegistered: Boolean; FOnNewEntryHotkey: TNewEntryHotkeyEvent; + // Ctrl+Shift+Q — "Quick search + autofill": open the JS quick-search + // modal, the user picks an entry, the password is SendInput'd into the + // window that had focus at hotkey time (captured into FAutofillTargetHWND + // by the host). + FQuickSearchHotkeyRegistered: Boolean; + FOnQuickSearchHotkey: TAutofillRequestEvent; procedure MsgWindowHandler(var AMsg: TMessage); procedure PrepareNid; procedure ShowTrayMenu; @@ -183,6 +189,11 @@ type // browser suffix by the JS layer before pre-fill). property OnNewEntryHotkey: TNewEntryHotkeyEvent read FOnNewEntryHotkey write FOnNewEntryHotkey; + // Fires on Ctrl+Shift+Q — quick-search-and-fill. Args mirror the + // regular autofill hotkey so the host can save the target HWND and + // pop the JS modal. Kind is always akPasswordOnly (no Tab). + property OnQuickSearchHotkey: TAutofillRequestEvent + read FOnQuickSearchHotkey write FOnQuickSearchHotkey; end; implementation @@ -227,6 +238,7 @@ const AUTOFILL_HOTKEY_ID_PWDONLY = 43; // Ctrl+Shift+P → password only DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title + QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT @@ -422,6 +434,8 @@ begin AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D')); FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A')); + FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow, + QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q')); end; destructor TPMBridge.Destroy; @@ -432,6 +446,9 @@ begin if FNewEntryHotkeyRegistered then UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID); + if FQuickSearchHotkeyRegistered then + UnregisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID); + if (FPowerNotify <> 0) and Assigned(_PowerUnregister) then _PowerUnregister(FPowerNotify); @@ -739,6 +756,22 @@ begin end; end + else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = QUICK_SEARCH_HOTKEY_ID) then + begin + // Quick-search + autofill: capture the foreground HWND BEFORE the JS + // modal steals focus, hand it to the host so it can stash it in + // FAutofillTargetHWND (consumed by cmd://autofill/execute later). + if Assigned(FOnQuickSearchHotkey) then + begin + var LTarget := GetForegroundWindow; + var LTitle: string; + SetLength(LTitle, 512); + var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512); + SetLength(LTitle, LLen); + FOnQuickSearchHotkey(akPasswordOnly, LTarget, LTitle); + end; + end + else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and ((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or (AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then diff --git a/delphi-backend/UMainForm.fmx b/delphi-backend/UMainForm.fmx index ab6677c..e92c557 100644 --- a/delphi-backend/UMainForm.fmx +++ b/delphi-backend/UMainForm.fmx @@ -123,12 +123,8 @@ object MainForm: TMainForm Size.Height = 6.000000000000000000 Size.PlatformDefault = False end - object WebBrowser: TTMSFNCWebBrowser - Align = Client - Size.Width = 1100.000000000000000000 - Size.Height = 502.000000000000000000 - Size.PlatformDefault = False - TabOrder = 3 - DesigntimeEnabled = False + object PopupMenu1: TPopupMenu + Left = 424 + Top = 240 end end diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 2d9e83d..5cbb906 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1,19 +1,48 @@ unit UMainForm; +// ---------------------------------------------------------------------- +// WebBrowser engine switch — compile-time directive. +// +// Uncomment USE_EDGE_BROWSER to build against TTMSFNCEdgeWebBrowser +// (Windows-only, direct WebView2 wrapper). Default = TTMSFNCWebBrowser +// (cross-platform abstraction also on WebView2 under Windows). Both +// expose the same TTMSFNCCustomWebBrowser API for OnBeforeNavigate / +// ExecuteJavaScript / Navigate, so the bridge cmd:// glue is unchanged. +// ---------------------------------------------------------------------- +{$DEFINE USE_EDGE_BROWSER} + interface uses System.SysUtils, System.Classes, System.UITypes, System.NetEncoding, - System.StrUtils, + System.StrUtils, System.Generics.Collections, Winapi.Windows, FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types, FMX.Dialogs, FMX.DialogService, FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser, + // ICoreWebView2 / ICoreWebView2Settings3 COM interface declarations — + // used to disable browser accelerator keys (Ctrl+P, Ctrl+J, F12, etc.) + // via the AreBrowserAcceleratorKeysEnabled property that Settings.* doesn't + // expose at the TMS wrapper level. + FMX.TMSFNCWebBrowser.Win, + {$IFDEF USE_EDGE_BROWSER} + FMX.TMSFNCEdgeWebBrowser, + {$ENDIF} PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart, PM.Favicon, - FMX.Platform.Win; // WindowHandleToPlatform → HWND for visibility check + FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check + +type + // Concrete class chosen at compile time. Both inherit from + // TTMSFNCCustomWebBrowser so we use that as the field type — events + // and ExecuteJavaScript live on the base class. + {$IFDEF USE_EDGE_BROWSER} + TWebBrowserClass = TTMSFNCEdgeWebBrowser; + {$ELSE} + TWebBrowserClass = TTMSFNCWebBrowser; + {$ENDIF} type TMainForm = class(TForm) @@ -28,7 +57,7 @@ type PanelLog: TPanel; Memo: TMemo; Splitter: TSplitter; - WebBrowser: TTMSFNCWebBrowser; + PopupMenu1: TPopupMenu; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); @@ -51,6 +80,17 @@ type FAutofillPendingHWND: HWND; FAutofillPendingUser: string; FAutofillPendingPass: string; + // True when the JS layer asked us to hide back to the tray AFTER the + // autofill SendInput completes. Used by the Ctrl+Shift+Q hotkey path + // when invoked while the app was in the tray — we can't hide before + // SendInput because Win10/11 anti-focus-stealing rules then refuse to + // hand focus to the target window. + FAutofillPendingHide: Boolean; + // Created dynamically in FormCreate so the directive can pick either + // TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without + // needing two .fmx variants. Aligned to Client to fill the remaining + // space between PanelTop (top) and PanelLog/Splitter (bottom). + WebBrowser: TWebBrowserClass; procedure AutofillTimerTick(Sender: TObject); procedure LogLine(const AMsg: string); procedure UpdateButtons; @@ -59,6 +99,9 @@ type // JS↔Delphi bridge procedure WebBrowserBeforeNavigate(Sender: TObject; var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams); + procedure WebBrowserGetContextMenu(Sender: TObject; + ATarget: TTMSFNCWebBrowserTargetItem; + AContextMenu: TObjectList); procedure HandleBridgeCommand(const ACmd, AParams: string); procedure BridgeSystemLock; procedure BridgeTrayRestore; @@ -69,6 +112,8 @@ type procedure BridgeDebugHotkey; procedure BridgeNewEntryHotkey(const AWindowTitle: string); procedure BridgeQuickSearchRequest; + procedure BridgeQuickSearchHotkey(AKind: TAutofillKind; + ATargetHWND: HWND; const ATitle: string); procedure WebBrowserInitialized(Sender: TObject); end; @@ -85,6 +130,30 @@ function MaskAccessToken(const AUrl: string): string; forward; procedure TMainForm.FormCreate(Sender: TObject); begin + // Create the WebBrowser control programmatically so the {$IFDEF + // USE_EDGE_BROWSER} directive can swap the concrete class without a + // second .fmx variant. Align Client to fill the area below PanelTop + // and above the (collapsible) PanelLog + Splitter. + WebBrowser := TWebBrowserClass.Create(Self); + // Wire events BEFORE Parent assignment: setting Parent triggers the + // TMS browser's async WebView2 init, which fires OnInitialized when + // Edge Chromium is ready. If we assigned the handlers afterwards we'd + // miss the event on fast / pre-warmed Edge installs (and the + // Settings.EnableContextMenu / SetAcceleratorKeys calls inside would + // never run, leaving the native context menu and Ctrl+P/J live). + WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate; + WebBrowser.OnInitialized := WebBrowserInitialized; + {$IFDEF USE_EDGE_BROWSER} + // On TTMSFNCEdgeWebBrowser the event is published. + // On TTMSFNCWebBrowser the same property is only re-published inside a + // conditional ($IFNDEF FNCLIB) block — depending on the FNC build, it + // may not be accessible. Restricting the assignment to the Edge build + // keeps the unconditional path TTMSFNCWebBrowser-safe. + WebBrowser.OnGetContextMenu := WebBrowserGetContextMenu; + {$ENDIF} + WebBrowser.Parent := Self; + WebBrowser.Align := TAlignLayout.Client; + FServer := TPMHTTPServer.Create; FServer.OnLog := LogLine; @@ -97,13 +166,10 @@ begin FBridge.OnDebugHotkey := BridgeDebugHotkey; FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey; FBridge.OnQuickSearchRequest := BridgeQuickSearchRequest; + FBridge.OnQuickSearchHotkey := BridgeQuickSearchHotkey; FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later FAutofillTargetHWND := 0; - - WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate; - WebBrowser.OnInitialized := WebBrowserInitialized; - // Delayed-Navigate timer: TTMSFNCWebBrowser (WebView2 backend) ignores // Navigate() calls until Edge Chromium finishes its async init (~1-2s). // We wait 1.5 s after Start, then issue a SINGLE Navigate — no retry loop @@ -115,7 +181,7 @@ begin FNavTimer.OnTimer := NavTimerTick; UpdateButtons; - LogLine('Password Manager - Delphi backend ready.'); + LogLine('Password Manager - d backend ready.'); LogLine('Click Start to launch server + embedded web vault.'); PanelTop.Visible := False; FRequireAccessToken := True; @@ -162,9 +228,47 @@ begin end; procedure TMainForm.WebBrowserInitialized(Sender: TObject); +var + LUnk: IUnknown; + LCtrl: ICoreWebView2Controller; + LWv2: ICoreWebView2; + LSettings: ICoreWebView2Settings; + LSettings3: ICoreWebView2Settings3; + LPtr: Pointer; begin + // Disable native Edge context menu + DevTools via the TMS-wrapped settings. + // Disable browser-level accelerator keys (Ctrl+P / Ctrl+J / Ctrl+H / + // Ctrl+S / F12 / etc.). TMS's NativeBrowser returns the + // ICoreWebView2Controller, not the ICoreWebView2 — so we walk the chain: + // Controller → get_CoreWebView2 → get_Settings → QI(Settings3) → set_. + // Pointer→Interface cast must go through an explicit AddRef to keep the + // refcount balanced (otherwise the auto-Release at scope exit frees a + // reference TMS is still holding → AV at next use). + LPtr := WebBrowser.NativeBrowser; + if LPtr <> nil then + begin + Pointer(LUnk) := LPtr; + LUnk._AddRef; + if Supports(LUnk, ICoreWebView2Controller, LCtrl) then + begin + LCtrl.get_CoreWebView2(LWv2); + if Assigned(LWv2) then + begin + LWv2.get_Settings(LSettings); + if Assigned(LSettings) and + Supports(LSettings, ICoreWebView2Settings3, LSettings3) then + LSettings3.set_AreBrowserAcceleratorKeysEnabled(False); + end; + end; + end; + {$IFDEF USE_EDGE_BROWSER} + WebBrowser.Settings.EnableContextMenu := False; + WebBrowser.Settings.EnableShowDebugConsole := False; + {$ELSE} WebBrowser.EnableContextMenu := False; WebBrowser.EnableShowDebugConsole := False; + WebBrowser.PopupMenu := PopupMenu1; + {$ENDIF} // Race-safe navigation fallback: the 1.5 s timer in NavigateToVault // assumes WebView2 finishes its async init within that window. On slow @@ -183,6 +287,29 @@ begin end; end; +procedure TMainForm.BridgeQuickSearchHotkey(AKind: TAutofillKind; + ATargetHWND: HWND; const ATitle: string); +var + LWasHidden: Boolean; + LWasHiddenJs: string; +begin + // Ctrl+Shift+Q anywhere — save the foreground HWND so the JS layer's + // cmd://autofill/execute (fired after the user picks an entry) sends + // the password into it. Then pop the modal in fill mode. + if not FServer.Active then Exit; + FAutofillTargetHWND := ATargetHWND; + LWasHidden := (not Self.Visible) or + IsIconic(WindowHandleToPlatform(Self.Handle).Wnd); + FBridge.RestoreFromTray; + LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower; + // Second arg forFill=true tells JS to SendInput on pick instead of copy. + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&typeof Bridge.openQuickSearch==="function")' + + 'Bridge.openQuickSearch(' + LWasHiddenJs + ',true)'); + LogLine(Format('Quick-search hotkey — target=%s, title="%s"', + [IntToHex(ATargetHWND, 8), ATitle])); +end; + procedure TMainForm.BridgeQuickSearchRequest; var LWasHidden: Boolean; @@ -303,6 +430,8 @@ procedure TMainForm.btnStartClick(Sender: TObject); var LPort: Integer; begin +// WebBrowser.Navigate('about:blank'); +// exit; LPort := StrToIntDef(edtPort.Text, 8765); try FServer.Start(LPort, True, FRequireAccessToken, FRequireProcessCheck); @@ -347,6 +476,16 @@ end; // JS↔Delphi bridge // --------------------------------------------------------------------------- +procedure TMainForm.WebBrowserGetContextMenu(Sender: TObject; + ATarget: TTMSFNCWebBrowserTargetItem; + AContextMenu: TObjectList); +begin + // Clearing the items leaves WebView2 with nothing to show → the native + // right-click menu is suppressed entirely. The JS layer renders a + // custom Cut/Copy/Paste menu on text inputs via installCustomContextMenu(). + AContextMenu.Clear; +end; + procedure TMainForm.WebBrowserBeforeNavigate(Sender: TObject; var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams); var @@ -544,6 +683,7 @@ begin FAutofillPendingUser := GetParam('username'); FAutofillPendingPass := GetParam('password'); FAutofillPendingHWND := FAutofillTargetHWND; + FAutofillPendingHide := GetParam('hide_after') = '1'; FAutofillTargetHWND := 0; // Small timer so SetForegroundWindow has time to take effect before @@ -756,14 +896,17 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject); var TargetHwnd: HWND; PendingUser, PendingPass: string; + HideAfter: Boolean; ForegroundAfter: HWND; begin TargetHwnd := FAutofillPendingHWND; PendingUser := FAutofillPendingUser; PendingPass := FAutofillPendingPass; + HideAfter := FAutofillPendingHide; FAutofillPendingHWND := 0; FAutofillPendingUser := ''; FAutofillPendingPass := ''; + FAutofillPendingHide := False; TTimer(Sender).Enabled := False; TTimer(Sender).Free; @@ -773,6 +916,15 @@ begin LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s', [IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8), BoolToStr(ForegroundAfter = TargetHwnd, True)])); + + // Hide-after (Ctrl+Shift+Q from tray): SendInput is done, the target + // already has focus — now we can safely tray ourselves without + // confusing the Win10/11 foreground-stealing watchdog. + if HideAfter then + begin + FBridge.MinimizeToTray; + LogLine('Hidden back to tray (post-autofill).'); + end; end; procedure TMainForm.BridgeAutofillRequest(AKind: TAutofillKind; diff --git a/delphi-backend/assets/assets.res b/delphi-backend/assets/assets.res index 644564f14c37ddde6621f043154a0d1b85a357d3..caddabfbc0e4a1c7809824a1d94400e5b9437472 100644 GIT binary patch delta 2120 zcmaJ?U1%It6n2uS+9)k*jI?4sNwK?YcQ=g>77{|;CL1fZ6`M#N6!q@Rot+_f@15Rz zC)-uZ>XRTSr7ImDL@W4cp(PJ_Dbk-e6)99HL8vJBR`t2ygXhlAW`m^*`!KWTp8K8e zeBZa1{~Y`G?%2h*t`GhG?AqJcFYWAtAGoyUX)0&6j3^MbqF|n~&<{Y19GdT$j;;fhw?NiW_bfC7dR6hZWl)JC+Fw)?NR>-KP3jX-uGvL&rN)g!sPe^kp z>D4v;u-TlJZ2zJkczXZq6;Fy*C28p~(S~B1f@!dAkc6_S0!%}pQ0mow+`abj&*Qtcm(6$!0b{+3w?@yoLl4H^JZC?) zw)EF|t5^SJXDOYI4lFVNvp^l-d2mW@DNogW@US#cjxbqA1T9l zUY@`|^Uv&&2Cib+ryN8`IRa{QwrmD`p;u_9oD0M!3KUtzLc^umN>hr6yCc{pnT2o{ z0<{HxFT6R{t-@;@!T^_meDoD#j+sUwQK~H@9_40nE(j0B6n4zGncy52gbhKYTOC1( z!N~}FEm9&K-*A6SxD;R(Ji-*685!{+?&v`9yl(yWLF>EiUA)dzL(7yG2PK#dTAJPj zw-uQiNa|?TL5Nx;>dUYvV`u(Jsc9QN%~&aU;NelX3dh9@(4PXwiX6Y1GzvT@-fJFQ z>3Dp;X6=bjJJx*s=A8A$xt+syeBh4tR2-GWGosZp3o!1>5V_Q& zQYC2mteYjjcmP_-YhE6-5<(didl`-eJZJ^)(Iv5jjxVUp^JY5Kh0;=VSI}P!uT6)^ z?0qC~&GP=|+_s1qiYEvYNcVH@Agt8HhIx?Gi&Uly-wgY7V)$GpiMw?cUe);OoV7Ri zOYVqbp-_gf?Ur+K7!e`fG!#oS@vHOJ!RHm_x=@-KL~&SXA3R2y3Yq3AYM~8~>iDG_ z_Sg)xBBjAZmZTVEK@(lP$;iFd+m~zn{ma&5T)Sb9$Dfw%9r2Z4t!?XH)U4Uz&2Wh~ z=B { }, // Tell Delphi to simulate keystrokes. Empty username = password only - // (no Tab is sent). - executeAutofill(username, password) { + // (no Tab is sent). hideAfter=true asks Delphi to hide our window + // back to the tray AFTER SendInput completes — necessary for the + // Ctrl+Shift+Q-from-tray flow (we cannot hide before SendInput or + // Win10/11 anti-focus-stealing rules block the target). + executeAutofill(username, password, hideAfter) { if (!active) return; cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) + - '&password=' + encodeURIComponent(password)); + '&password=' + encodeURIComponent(password) + + (hideAfter ? '&hide_after=1' : '')); }, // Ask Delphi to bring the main window to front (used when the @@ -247,7 +251,7 @@ const Bridge = (() => { // hide the window again so the paste workflow is one keystroke // (Ctrl+V in the target app). // Locked vault → fall through to the master-password screen. - openQuickSearch(wasHidden) { + openQuickSearch(wasHidden, forFill) { if (state.locked || !state.cryptoKey || !state.token) { const pwd = document.getElementById('loginPassword'); if (pwd && !document.getElementById('authScreen').classList.contains('is-hidden')) { @@ -255,10 +259,13 @@ const Bridge = (() => { } if (typeof toast === 'function') toast('Vault is locked — unlock to search', 'warning'); + // Tell Delphi we cancelled so the captured HWND doesn't + // linger waiting for a never-coming /execute. + if (forFill && Bridge.cancelAutofill) Bridge.cancelAutofill(); return; } if (typeof openQuickSearchModal === 'function') - openQuickSearchModal(!!wasHidden); + openQuickSearchModal(!!wasHidden, !!forFill); }, // Hide the window back to the tray icon. Used by Quick search to @@ -688,6 +695,10 @@ let quickSearchSelected = 0; // picks an entry — so the previously-foreground app comes back and // Ctrl+V drops the password in. let quickSearchHideAfter = false; +// When opened by Ctrl+Shift+Q hotkey, Delphi has saved the foreground +// HWND and is waiting for cmd://autofill/execute. On pick we SendInput +// the password instead of copying to the clipboard. +let quickSearchFillMode = false; function quickSearchScoreEntry(e, q) { if (!q) return 1; // empty query → all entries pass, ordering preserved @@ -755,6 +766,28 @@ function quickSearchRender() { } async function quickSearchPickEntry(entry, copyUsername) { + // Fill mode (Ctrl+Shift+Q hotkey): SendInput the password directly into + // the HWND Delphi saved when the hotkey fired. No clipboard touch. + if (quickSearchFillMode && !copyUsername) { + const pwd = await decryptPwd(entry.encrypted_password, entry.iv); + if (pwd === '[ERROR]') { + toast('Decryption error', 'error'); + if (Bridge.active) Bridge.cancelAutofill(); + return; + } + // Single command — Delphi defers the SendInput by 60 ms then, + // if hide_after=1, MinimizeToTray's AFTER the keystrokes land. + // Hiding before SendInput would tip the Win10/11 anti-focus-stealing + // rules into refusing to hand focus to the target window. + if (Bridge.active) Bridge.executeAutofill('', pwd, quickSearchHideAfter); + toast(entryDisplayName(entry) + ' · password sent'); + // Both flags consumed — closeQuickSearchModal must not re-trigger. + quickSearchFillMode = false; + quickSearchHideAfter = false; + closeQuickSearchModal(); + return; + } + if (copyUsername) { const u = entry.username || ''; if (!u) { @@ -777,19 +810,35 @@ async function quickSearchPickEntry(entry, copyUsername) { closeQuickSearchModal(); } -function openQuickSearchModal(hideAfter) { +function openQuickSearchModal(hideAfter, forFill) { const modal = document.getElementById('quickSearchModal'); const input = document.getElementById('quickSearchInput'); modal.classList.remove('is-hidden'); input.value = ''; - quickSearchSelected = 0; + quickSearchSelected = 0; quickSearchHideAfter = !!hideAfter; + quickSearchFillMode = !!forFill; + // Subtle hint to the user about what Enter will do. + const hintEl = modal.querySelector('.quick-search-hint'); + if (hintEl) { + hintEl.textContent = forFill + ? 'Enter = type password into the active window · Esc = cancel' + : 'Enter = copy password · Shift+Enter = copy username · Esc = close'; + } quickSearchRender(); setTimeout(() => input.focus(), 50); } function closeQuickSearchModal() { document.getElementById('quickSearchModal').classList.add('is-hidden'); + // Fill-mode cancel: tell Delphi to drop the saved HWND so the next + // /execute (e.g. an unrelated Ctrl+Shift+L) doesn't accidentally + // target the stale window. + if (quickSearchFillMode) { + if (Bridge.active && typeof Bridge.cancelAutofill === 'function') + Bridge.cancelAutofill(); + quickSearchFillMode = false; + } // If the modal was opened from the tray (window was hidden), restore // the previous "in tray" state so the user can paste straight into // the target app. Cancel (Esc / close X) also triggers this — they