From 33e4b4b6145a64fecf8f530cd5d5b24d226da3ca Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Mon, 8 Jun 2026 21:39:37 +0100 Subject: [PATCH] feat(autostart): "Start with Windows" toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PM.AutoStart wraps HKCU\Software\Microsoft\Windows\CurrentVersion\Run. Value "PMServer" = "" -tray. Per-user, no admin required, shows up in Task Manager → Startup so the user can override from there. - UMainForm honours the -tray CLI flag (set by the registry entry): after the server starts, MinimizeToTray via TThread.ForceQueue so the app comes up directly in the tray with no visible window flash. - Bridge cmd://autostart/{get,set} + Bridge.getAutoStart() / setAutoStart() / onAutoStartStatus(). Settings exposes a toggle in the Security section, visible only when Bridge.active (the PHP frontend can't touch the registry). - Toggle reads "on" only when the registered command matches the current exe path, so a stale entry from a moved exe lets the user re-enable to refresh. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 17 +++++ delphi-backend/PMServer.dpr | 1 + delphi-backend/PMServer.dproj | 1 + delphi-backend/Source/PM.AutoStart.pas | 98 +++++++++++++++++++++++++ delphi-backend/UMainForm.pas | 38 +++++++++- delphi-backend/assets/assets.res | Bin 325848 -> 328316 bytes index.html | 14 ++++ js/app.js | 45 ++++++++++++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 delphi-backend/Source/PM.AutoStart.pas diff --git a/CLAUDE.md b/CLAUDE.md index 8aaa0f3..9938d26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ plus courant après modif frontend. | Quick unlock DPAPI (device-bound) | `delphi-backend/Source/PM.QuickUnlock.pas` | | Prefs key/value DPAPI (`prefs.bin`) | `delphi-backend/Source/PM.UserPrefs.pas` | | Single-instance mutex + broadcast | `delphi-backend/Source/PM.SingleInstance.pas` | +| Start with Windows (HKCU Run) | `delphi-backend/Source/PM.AutoStart.pas` | | Handlers REST | `delphi-backend/Handlers/PM.Handler.*.pas` | | Frontend complet | `js/app.js` | | HTML racine | `index.html` | @@ -51,6 +52,7 @@ Commandes connues : - `clipboard/copy`, `clipboard/clear`, `clipboard/read` (Paste custom menu) - `quickunlock/{store,get,clear,status}` - `prefs/{get,set}?key=...` (device-bound DPAPI key/value, voir plus bas) +- `autostart/{get,set}?enabled=1|0` (HKCU Run registry, "Start with Windows") - `autofill/{configure,hotkeys,execute,cancel}` - `app/focus` (ramène la fenêtre au premier plan, pour le picker) - `app/ready` (page chargée → SetFocus WebBrowser + DOM focus auth input) @@ -156,6 +158,21 @@ la fenêtre + clipboard clear + balloon first-time. Menu : Open / Lock vault / Quit (via `TrackPopupMenu`, themé par `SetPreferredAppMode` ci-dessus). +## Start with Windows (`PM.AutoStart`) + +- Toggle dans Settings → "Start with Windows" (visible seulement quand + `Bridge.active`) +- Mécanisme : `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, + valeur `"PMServer"` = `"" -tray` (single dash — `--tray` + ne match pas `FindCmdLineSwitch`) +- Per-user, pas d'admin requis. Visible dans Task Manager → Startup tab +- Toggle "on" = registered AND la valeur pointe vers notre exe courant + (un old entry d'un exe déplacé lit comme "off" → user peut re-enable + pour rafraîchir le path) +- CLI flag `-tray` (`FindCmdLineSwitch('tray', True)`) dans FormCreate + → `FBridge.MinimizeToTray` via `TThread.ForceQueue` (defer après la + show initiale FMX pour minimiser le flash) + ## Device-bound prefs (`PM.UserPrefs`) **Problème résolu** : le serveur HTTP bind un port éphémère aléatoire diff --git a/delphi-backend/PMServer.dpr b/delphi-backend/PMServer.dpr index 53fef5f..f15e66e 100644 --- a/delphi-backend/PMServer.dpr +++ b/delphi-backend/PMServer.dpr @@ -18,6 +18,7 @@ uses PM.Bridge in 'Source\PM.Bridge.pas', PM.QuickUnlock in 'Source\PM.QuickUnlock.pas', PM.UserPrefs in 'Source\PM.UserPrefs.pas', + PM.AutoStart in 'Source\PM.AutoStart.pas', PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas', PM.Handler.Ping in 'Handlers\PM.Handler.Ping.pas', PM.Handler.Auth in 'Handlers\PM.Handler.Auth.pas', diff --git a/delphi-backend/PMServer.dproj b/delphi-backend/PMServer.dproj index 3b37da9..8cbf281 100644 --- a/delphi-backend/PMServer.dproj +++ b/delphi-backend/PMServer.dproj @@ -220,6 +220,7 @@ $(PreBuildEvent)]]> + diff --git a/delphi-backend/Source/PM.AutoStart.pas b/delphi-backend/Source/PM.AutoStart.pas new file mode 100644 index 0000000..280a378 --- /dev/null +++ b/delphi-backend/Source/PM.AutoStart.pas @@ -0,0 +1,98 @@ +unit PM.AutoStart; + +{ + Start-with-Windows toggle. + + Mechanism: HKCU\Software\Microsoft\Windows\CurrentVersion\Run + - Per-user (no admin needed) + - Visible in Task Manager → Startup tab (user can disable from there) + - Standard pattern used by 1Password, Bitwarden, Slack, Discord, etc. + + Command line: launched with --tray so PMServer starts minimized to the + tray instead of popping a window during Windows login. + + Value name "PMServer" — fixed string, not exe path-derived, so moving + the exe + flipping the toggle off/on cleans up the old entry. +} + +interface + +function IsAutoStartEnabled: Boolean; + +// Returns True on success. Failures (registry locked, etc.) are silent — +// caller surfaces the resulting status via IsAutoStartEnabled. +function SetAutoStart(AEnabled: Boolean): Boolean; + +implementation + +uses + System.SysUtils, System.Win.Registry, Winapi.Windows; + +const + RUN_KEY = 'Software\Microsoft\Windows\CurrentVersion\Run'; + VALUE_NAME = 'PMServer'; + TRAY_ARG = '-tray'; // single dash so Delphi's FindCmdLineSwitch picks it up + +function BuildAutoStartCommand: string; +begin + // Quote the exe path (may contain spaces) and append the silent-start + // arg. Single string written to a REG_SZ value — Windows splits args + // the same way CommandLineToArgvW does. + Result := '"' + ParamStr(0) + '" ' + TRAY_ARG; +end; + +function IsAutoStartEnabled: Boolean; +var + R: TRegistry; + LCurrent: string; +begin + Result := False; + R := TRegistry.Create(KEY_READ); + try + R.RootKey := HKEY_CURRENT_USER; + if not R.OpenKeyReadOnly(RUN_KEY) then Exit; + try + if not R.ValueExists(VALUE_NAME) then Exit; + LCurrent := R.ReadString(VALUE_NAME); + // Consider the toggle "on" only if the registered command points + // to OUR exe — a stale entry from a moved exe should read as off + // so the user can re-enable to refresh the path. + Result := SameText(LCurrent, BuildAutoStartCommand); + finally + R.CloseKey; + end; + finally + R.Free; + end; +end; + +function SetAutoStart(AEnabled: Boolean): Boolean; +var + R: TRegistry; +begin + Result := False; + R := TRegistry.Create(KEY_READ or KEY_WRITE); + try + R.RootKey := HKEY_CURRENT_USER; + if not R.OpenKey(RUN_KEY, True) then Exit; + try + if AEnabled then + begin + R.WriteString(VALUE_NAME, BuildAutoStartCommand); + Result := True; + end + else + begin + if R.ValueExists(VALUE_NAME) then + R.DeleteValue(VALUE_NAME); + Result := True; + end; + finally + R.CloseKey; + end; + finally + R.Free; + end; +end; + +end. diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 7da371d..0ecf167 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -11,7 +11,7 @@ uses FMX.Dialogs, FMX.DialogService, FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser, - PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs; + PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart; type TMainForm = class(TForm) @@ -131,6 +131,20 @@ begin end; btnStartClick(Nil); btnToggleLogClick(nil); + + // CLI flag --tray (set by the "Start with Windows" registry entry): + // launch directly into the tray instead of popping a window during + // the Windows login. Defer to ForceQueue so the form has finished its + // initial Show before we hide it — minimises the visible flash. + if FindCmdLineSwitch('tray', True) then + begin + TThread.ForceQueue(nil, + procedure + begin + FBridge.MinimizeToTray; + LogLine('Launched with --tray, minimised at startup'); + end); + end; end; procedure TMainForm.FormDestroy(Sender: TObject); @@ -552,6 +566,28 @@ begin PM.UserPrefs.SetPref(LKey, GetParam('value')); end + // ---- Start with Windows (HKCU Run registry) -------------------------- + else if ACmd = 'autostart/get' then + begin + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onAutoStartStatus)' + + 'Bridge.onAutoStartStatus(' + + BoolToStr(PM.AutoStart.IsAutoStartEnabled, True).ToLower + ')'); + end + + else if ACmd = 'autostart/set' then + begin + var LOk := PM.AutoStart.SetAutoStart(GetParam('enabled') = '1'); + LogLine(Format('Autostart set to %s (ok=%s)', + [GetParam('enabled'), BoolToStr(LOk, True)])); + // Echo back the resulting state so the UI re-syncs (covers the + // case where the registry write was blocked silently). + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onAutoStartStatus)' + + 'Bridge.onAutoStartStatus(' + + BoolToStr(PM.AutoStart.IsAutoStartEnabled, True).ToLower + ')'); + end + else LogLine('Bridge: unknown command "' + ACmd + '"'); end; diff --git a/delphi-backend/assets/assets.res b/delphi-backend/assets/assets.res index cc0266471e534b858180833d19c1915f7d872fbf..20e8cee1f8ee153ac36d26afce74de450f5903d2 100644 GIT binary patch delta 1281 zcmZuxUu+ab9Pa+9?zK{Bd(^hvnP2&Lcciz4_+Th8ilP=V0g8kN6Vkog>F&UG_S~6m zIgEisj4w)vqfXQp2~Q+6wl<*xkrEPOQcKg)7oUvsV&V(dXJee*yIb1SecGM*ecyb) z@AvKevfBC2#q?!Rcix!ZHs=_}nH8{Me}XR`>Hf@pz9asI3CvtLiHYq`P~Prs<-13E zIdQWCZ(AWLyTDvb!KzdM6ZwvS1y&U%TtuMihd{AI;vJt`@AmK|Md!tevGdV|&w=`J zy%jQl6nh@HK0AY|k}E(EAj_^c5HH}dF=S(lA+Kt}K{SrbsOS@~3KTJhi39lx3K|+A zq$nte)$2+x@5Yy!<1S=N&+)s%&3x)~Gk^BfxtV3>kHcEip--;fn$a>IVi3uw8Gxwo zjZ40lfP|hm^TQ1#%b%HR-?h;W$PXQU?F0;mL<)8=WkIzdtP^_omKLZ#4q^uy%>2kt zJH$5+)U5b!Q8oCtH=pKTFBMYB$>Pe-YDT=jpr!b~_ggnf7>y<}AUSg4SijOK9>1lw zi&t)GE#h*Qu77!5F?i*+zJ}qzqMGNwnZ}x>_wK4a;!Z|4qIJHsY>2CiYFdo+D~8yd z(L1>Ia8K%q#=Y}HsoJs9T)uB%8^1Q+*3)RmCrPEWgN2muozJW}Y;{3WZ(2}O;;)?c zR9bqNbT?f0Rs6oHbs7y%L?W*>$oKUjVwF5(i|N0W>~<-GAu}O(y|yPgle+!9?_OFd zi9SQgY_5kP5h$jQtLd%fQUS^(v+Sbs8Dy^me>l1F@#Km!JGr8gU(XK=fNUvX#VGK_ zdfEO!C2O*Z11e zh#q+)piMW$o_<{yA3adJ_~ZL6V)rGj?R;!t9+|R6k~B=OGejpa-=b_oL83A$c^Psi zqw7FUKVa)_AIL{njJ#e7xpjy^iSWsz3tw$v|AWej_7=TYd^)8WvwwEzr*~;hO--}! J?9(sK{Rh-C!jk|1 delta 63 zcmV-F0KotJgcR8E6ObSRVzaR#zh|?gYS&8wY=xtKwga=Ax&Iu4&)SF2+5xxE+5%$U Vx2C!S%V4*M90YDAxA;N?gufLlAaVcz diff --git a/index.html b/index.html index 38b7121..a55dda2 100644 --- a/index.html +++ b/index.html @@ -396,6 +396,20 @@ +
+ + Start with Windows + + Launch this app in the tray when you sign in to + Windows — so the autofill hotkeys are ready + immediately. Device-only setting (per Windows user). + + + +
Autofill with global hotkey diff --git a/js/app.js b/js/app.js index fba7b17..fb0e095 100644 --- a/js/app.js +++ b/js/app.js @@ -14,6 +14,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0) // Active only when running inside the Delphi-hosted WebView2 (API === ''). // Falls back to navigator.clipboard for the standalone PHP frontend. const prefResolvers = {}; +let autoStartResolver = null; const Bridge = (() => { const active = (API === ''); @@ -188,6 +189,35 @@ const Bridge = (() => { r(value || ''); } }, + + // ---- Start with Windows (HKCU Run registry) ----------------------- + getAutoStart() { + if (!active) return Promise.resolve(false); + return new Promise(resolve => { + autoStartResolver = resolve; + cmd('cmd://autostart/get'); + setTimeout(() => { + if (autoStartResolver === resolve) { + autoStartResolver = null; + resolve(false); + } + }, 2000); + }); + }, + setAutoStart(enabled) { + if (!active) return; + cmd('cmd://autostart/set?enabled=' + (enabled ? '1' : '0')); + }, + onAutoStartStatus(enabled) { + if (autoStartResolver) { + const r = autoStartResolver; + autoStartResolver = null; + r(!!enabled); + } + state.autoStartEnabled = !!enabled; + const cb = document.getElementById('settingAutoStart'); + if (cb) cb.checked = !!enabled; + }, }; })(); @@ -4851,6 +4881,14 @@ function openSettings() { $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); $('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none'; + // Start-with-Windows toggle: only meaningful inside the Delphi host + // (registry access). Hide for the PHP frontend. + $('#settingAutoStartRow').style.display = Bridge.active ? '' : 'none'; + if (Bridge.active) { + // Fire-and-forget: the bridge response updates the checkbox via + // Bridge.onAutoStartStatus. + Bridge.getAutoStart(); + } $('#settingUser').textContent = state.username; // Async: query server for recovery key state and update the label refreshRecoveryStatus(); @@ -5436,6 +5474,13 @@ async function init() { toast('Breach check disabled'); } }); + $('#settingAutoStart').addEventListener('change', e => { + if (!Bridge.active) return; + Bridge.setAutoStart(e.target.checked); + toast(e.target.checked + ? 'Will start with Windows (in tray)' + : 'Won’t start with Windows'); + }); $('#settingAutofill').addEventListener('change', e => { state.autofillEnabled = e.target.checked; localStorage.setItem('autofillEnabled', state.autofillEnabled ? '1' : '0');