feat(autostart): "Start with Windows" toggle
- PM.AutoStart wraps HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
Value "PMServer" = "<exe>" -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 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ plus courant après modif frontend.
|
|||||||
| Quick unlock DPAPI (device-bound) | `delphi-backend/Source/PM.QuickUnlock.pas` |
|
| Quick unlock DPAPI (device-bound) | `delphi-backend/Source/PM.QuickUnlock.pas` |
|
||||||
| Prefs key/value DPAPI (`prefs.bin`) | `delphi-backend/Source/PM.UserPrefs.pas` |
|
| Prefs key/value DPAPI (`prefs.bin`) | `delphi-backend/Source/PM.UserPrefs.pas` |
|
||||||
| Single-instance mutex + broadcast | `delphi-backend/Source/PM.SingleInstance.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` |
|
| Handlers REST | `delphi-backend/Handlers/PM.Handler.*.pas` |
|
||||||
| Frontend complet | `js/app.js` |
|
| Frontend complet | `js/app.js` |
|
||||||
| HTML racine | `index.html` |
|
| HTML racine | `index.html` |
|
||||||
@@ -51,6 +52,7 @@ Commandes connues :
|
|||||||
- `clipboard/copy`, `clipboard/clear`, `clipboard/read` (Paste custom menu)
|
- `clipboard/copy`, `clipboard/clear`, `clipboard/read` (Paste custom menu)
|
||||||
- `quickunlock/{store,get,clear,status}`
|
- `quickunlock/{store,get,clear,status}`
|
||||||
- `prefs/{get,set}?key=...` (device-bound DPAPI key/value, voir plus bas)
|
- `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}`
|
- `autofill/{configure,hotkeys,execute,cancel}`
|
||||||
- `app/focus` (ramène la fenêtre au premier plan, pour le picker)
|
- `app/focus` (ramène la fenêtre au premier plan, pour le picker)
|
||||||
- `app/ready` (page chargée → SetFocus WebBrowser + DOM focus auth input)
|
- `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
|
Menu : Open / Lock vault / Quit (via `TrackPopupMenu`, themé par
|
||||||
`SetPreferredAppMode` ci-dessus).
|
`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"` = `"<exe-path>" -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`)
|
## Device-bound prefs (`PM.UserPrefs`)
|
||||||
|
|
||||||
**Problème résolu** : le serveur HTTP bind un port éphémère aléatoire
|
**Problème résolu** : le serveur HTTP bind un port éphémère aléatoire
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ uses
|
|||||||
PM.Bridge in 'Source\PM.Bridge.pas',
|
PM.Bridge in 'Source\PM.Bridge.pas',
|
||||||
PM.QuickUnlock in 'Source\PM.QuickUnlock.pas',
|
PM.QuickUnlock in 'Source\PM.QuickUnlock.pas',
|
||||||
PM.UserPrefs in 'Source\PM.UserPrefs.pas',
|
PM.UserPrefs in 'Source\PM.UserPrefs.pas',
|
||||||
|
PM.AutoStart in 'Source\PM.AutoStart.pas',
|
||||||
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
|
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
|
||||||
PM.Handler.Ping in 'Handlers\PM.Handler.Ping.pas',
|
PM.Handler.Ping in 'Handlers\PM.Handler.Ping.pas',
|
||||||
PM.Handler.Auth in 'Handlers\PM.Handler.Auth.pas',
|
PM.Handler.Auth in 'Handlers\PM.Handler.Auth.pas',
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<DCCReference Include="Source\PM.QuickUnlock.pas"/>
|
<DCCReference Include="Source\PM.QuickUnlock.pas"/>
|
||||||
<DCCReference Include="Source\PM.UserPrefs.pas"/>
|
<DCCReference Include="Source\PM.UserPrefs.pas"/>
|
||||||
<DCCReference Include="Source\PM.SingleInstance.pas"/>
|
<DCCReference Include="Source\PM.SingleInstance.pas"/>
|
||||||
|
<DCCReference Include="Source\PM.AutoStart.pas"/>
|
||||||
<DCCReference Include="Handlers\PM.Handler.Ping.pas"/>
|
<DCCReference Include="Handlers\PM.Handler.Ping.pas"/>
|
||||||
<DCCReference Include="Handlers\PM.Handler.Auth.pas"/>
|
<DCCReference Include="Handlers\PM.Handler.Auth.pas"/>
|
||||||
<DCCReference Include="Handlers\PM.Handler.Folders.pas"/>
|
<DCCReference Include="Handlers\PM.Handler.Folders.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.
|
||||||
@@ -11,7 +11,7 @@ uses
|
|||||||
FMX.Dialogs, FMX.DialogService,
|
FMX.Dialogs, FMX.DialogService,
|
||||||
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
|
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
|
||||||
FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser,
|
FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser,
|
||||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs;
|
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart;
|
||||||
|
|
||||||
type
|
type
|
||||||
TMainForm = class(TForm)
|
TMainForm = class(TForm)
|
||||||
@@ -131,6 +131,20 @@ begin
|
|||||||
end;
|
end;
|
||||||
btnStartClick(Nil);
|
btnStartClick(Nil);
|
||||||
btnToggleLogClick(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;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.FormDestroy(Sender: TObject);
|
procedure TMainForm.FormDestroy(Sender: TObject);
|
||||||
@@ -552,6 +566,28 @@ begin
|
|||||||
PM.UserPrefs.SetPref(LKey, GetParam('value'));
|
PM.UserPrefs.SetPref(LKey, GetParam('value'));
|
||||||
end
|
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
|
else
|
||||||
LogLine('Bridge: unknown command "' + ACmd + '"');
|
LogLine('Bridge: unknown command "' + ACmd + '"');
|
||||||
end;
|
end;
|
||||||
|
|||||||
Binary file not shown.
+14
@@ -396,6 +396,20 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row" id="settingAutoStartRow">
|
||||||
|
<span>
|
||||||
|
Start with Windows
|
||||||
|
<small class="setting-hint">
|
||||||
|
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).
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="settingAutoStart">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="setting-row" id="settingAutofillRow">
|
<div class="setting-row" id="settingAutofillRow">
|
||||||
<span>
|
<span>
|
||||||
Autofill with global hotkey
|
Autofill with global hotkey
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0)
|
|||||||
// Active only when running inside the Delphi-hosted WebView2 (API === '').
|
// Active only when running inside the Delphi-hosted WebView2 (API === '').
|
||||||
// Falls back to navigator.clipboard for the standalone PHP frontend.
|
// Falls back to navigator.clipboard for the standalone PHP frontend.
|
||||||
const prefResolvers = {};
|
const prefResolvers = {};
|
||||||
|
let autoStartResolver = null;
|
||||||
const Bridge = (() => {
|
const Bridge = (() => {
|
||||||
const active = (API === '');
|
const active = (API === '');
|
||||||
|
|
||||||
@@ -188,6 +189,35 @@ const Bridge = (() => {
|
|||||||
r(value || '');
|
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);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||||
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
$('#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;
|
$('#settingUser').textContent = state.username;
|
||||||
// Async: query server for recovery key state and update the label
|
// Async: query server for recovery key state and update the label
|
||||||
refreshRecoveryStatus();
|
refreshRecoveryStatus();
|
||||||
@@ -5436,6 +5474,13 @@ async function init() {
|
|||||||
toast('Breach check disabled');
|
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 => {
|
$('#settingAutofill').addEventListener('change', e => {
|
||||||
state.autofillEnabled = e.target.checked;
|
state.autofillEnabled = e.target.checked;
|
||||||
localStorage.setItem('autofillEnabled', state.autofillEnabled ? '1' : '0');
|
localStorage.setItem('autofillEnabled', state.autofillEnabled ? '1' : '0');
|
||||||
|
|||||||
Reference in New Issue
Block a user