feat(autofill): Ctrl+Shift+T types the 2FA code

Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.

Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.

- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
  two autofill combos; an unconfigured combo (vk=0) doesn't count as a
  registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
  never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
  against the other four slots, covered by Reset to defaults and the cheatsheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 16:53:00 +01:00
parent 2d6ba98073
commit 7c86f35fce
7 changed files with 121 additions and 18 deletions
+6
View File
@@ -148,6 +148,12 @@ Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
- `Ctrl+Shift+L` : autofill complet (user + Tab + password) - `Ctrl+Shift+L` : autofill complet (user + Tab + password)
- `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens) - `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens)
- `Ctrl+Shift+T` : autofill du **code 2FA** (cliquer le champ 2FA d'abord).
ID hotkey 47 / `akTotp`. Ne matche que les entrées avec `totp_secret` (filtre
appliqué **avant** le scoring → le picker ne propose jamais un candidat sans
TOTP). JS génère le code et l'envoie **dans le slot password avec un username
vide** → réutilise le chemin password-only de `ExecuteAutofill`, aucun code
natif en plus.
- `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground - `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground
- `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent) - `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent)
+36 -8
View File
@@ -62,7 +62,10 @@ type
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the // Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type // "password only" hotkey (Ctrl+Shift+P). The host decides what to type
// based on this kind. // based on this kind.
TAutofillKind = (akFull, akPasswordOnly); // akTotp reuses the password-only typing path: JS sends the generated
// 6-digit code in the password slot with an empty username, so Delphi
// types just the code into the focused 2FA field.
TAutofillKind = (akFull, akPasswordOnly, akTotp);
// Fired when an autofill hotkey is pressed. Args are the foreground // Fired when an autofill hotkey is pressed. Args are the foreground
// window HWND and its title (captured before any focus change), plus // window HWND and its title (captured before any focus change), plus
@@ -117,6 +120,7 @@ type
FAutofillRegistered: Boolean; FAutofillRegistered: Boolean;
FAutofillFullActive: Boolean; FAutofillFullActive: Boolean;
FAutofillPwdActive: Boolean; FAutofillPwdActive: Boolean;
FAutofillTotpActive: Boolean;
FOnAutofillRequest: TAutofillRequestEvent; FOnAutofillRequest: TAutofillRequestEvent;
FDebugHotkeyRegistered: Boolean; FDebugHotkeyRegistered: Boolean;
FOnDebugHotkey: TProc; FOnDebugHotkey: TProc;
@@ -162,8 +166,10 @@ type
// Returns True if both hotkeys registered successfully. If one or both // Returns True if both hotkeys registered successfully. If one or both
// failed (clash with another app), best-effort: whichever succeeded // failed (clash with another app), best-effort: whichever succeeded
// stays active. // stays active.
// ATotpVk = 0 means "no 2FA hotkey configured" — it is then skipped and
// does NOT count against the returned all-registered flag.
function SetAutofillHotkeys(AFullMods, AFullVk, function SetAutofillHotkeys(AFullMods, AFullVk,
APwdMods, APwdVk: Word): Boolean; APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a // Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
// user-chosen combo. True on success. // user-chosen combo. True on success.
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean; function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
@@ -287,6 +293,7 @@ const
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title 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 QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill
AUTOFILL_HOTKEY_ID_TOTP = 47; // Ctrl+Shift+T → type the 2FA code
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
@@ -895,13 +902,16 @@ begin
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or ((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then (AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY) or
(AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP)) then
begin begin
// Capture the foreground window BEFORE any focus change, then fire the // Capture the foreground window BEFORE any focus change, then fire the
// callback so the host can match the title against vault entries. // callback so the host can match the title against vault entries.
var LKind: TAutofillKind; var LKind: TAutofillKind;
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
LKind := akPasswordOnly LKind := akPasswordOnly
else if AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP then
LKind := akTotp
else else
LKind := akFull; LKind := akFull;
var LTarget := GetForegroundWindow; var LTarget := GetForegroundWindow;
@@ -920,7 +930,7 @@ end;
// ============================================================================= // =============================================================================
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk, function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
APwdMods, APwdVk: Word): Boolean; APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
begin begin
// Tear down whatever is currently registered before installing the new // Tear down whatever is currently registered before installing the new
// combos. RegisterHotKey would fail if the same ID is already taken. // combos. RegisterHotKey would fail if the same ID is already taken.
@@ -934,6 +944,11 @@ begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY); UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
FAutofillPwdActive := False; FAutofillPwdActive := False;
end; end;
if FAutofillTotpActive then
begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
FAutofillTotpActive := False;
end;
// Best-effort registration. A failure (typically MOD_x clash with another // Best-effort registration. A failure (typically MOD_x clash with another
// app's global hotkey) is silent: the other slot can still be live. // app's global hotkey) is silent: the other slot can still be live.
@@ -943,9 +958,15 @@ begin
if (APwdVk <> 0) and (APwdMods <> 0) then if (APwdVk <> 0) and (APwdMods <> 0) then
FAutofillPwdActive := RegisterHotKey(FMsgWindow, FAutofillPwdActive := RegisterHotKey(FMsgWindow,
AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk); AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk);
if (ATotpVk <> 0) and (ATotpMods <> 0) then
FAutofillTotpActive := RegisterHotKey(FMsgWindow,
AUTOFILL_HOTKEY_ID_TOTP, ATotpMods, ATotpVk);
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive; FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive
Result := FAutofillFullActive and FAutofillPwdActive; or FAutofillTotpActive;
// An unconfigured 2FA combo (vk=0) must not report a registration failure.
Result := FAutofillFullActive and FAutofillPwdActive
and (FAutofillTotpActive or (ATotpVk = 0));
end; end;
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean; function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
@@ -1041,10 +1062,12 @@ end;
procedure TPMBridge.RegisterAutofillHotkey; procedure TPMBridge.RegisterAutofillHotkey;
begin begin
// Convenience default — Ctrl+Shift+L (full) + Ctrl+Shift+P (password). // Convenience defaults — Ctrl+Shift+L (full), Ctrl+Shift+P (password),
// Ctrl+Shift+T (2FA code).
// Idempotent: calling twice with the same combos is harmless. // Idempotent: calling twice with the same combos is harmless.
SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'), SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'),
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P')); AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'),
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('T'));
end; end;
procedure TPMBridge.UnregisterAutofillHotkey; procedure TPMBridge.UnregisterAutofillHotkey;
@@ -1059,6 +1082,11 @@ begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY); UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
FAutofillPwdActive := False; FAutofillPwdActive := False;
end; end;
if FAutofillTotpActive then
begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
FAutofillTotpActive := False;
end;
FAutofillRegistered := False; FAutofillRegistered := False;
end; end;
+12 -4
View File
@@ -895,10 +895,14 @@ begin
var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L'))); var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L')));
var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6)); var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6));
var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P'))); var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P')));
// 2FA combo — vk 0 when the user cleared it, which skips registration.
var LTotpMods := Word(StrToIntDef(GetParam('totp_mods'), 6));
var LTotpVk := Word(StrToIntDef(GetParam('totp_vk'), Ord('T')));
var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk, var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk,
LPwdMods, LPwdVk); LPwdMods, LPwdVk,
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d (all_ok=%s)', LTotpMods, LTotpVk);
[LFullMods, LFullVk, LPwdMods, LPwdVk, BoolToStr(LAllOk, True)])); LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)',
[LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)]));
// Notify JS of the result so the UI can flag a failed-to-register combo // Notify JS of the result so the UI can flag a failed-to-register combo
// (typically a clash with another app's global hotkey). // (typically a clash with another app's global hotkey).
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
@@ -1542,7 +1546,11 @@ begin
LTitle := LTitle.Replace('\', '\\'); LTitle := LTitle.Replace('\', '\\');
LTitle := LTitle.Replace('"', '\"'); LTitle := LTitle.Replace('"', '\"');
if AKind = akPasswordOnly then LKind := 'password' else LKind := 'full'; case AKind of
akPasswordOnly: LKind := 'password';
akTotp: LKind := 'totp';
else LKind := 'full';
end;
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' + 'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' +
+6
View File
@@ -675,6 +675,12 @@
</span> </span>
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button> <button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button>
</div> </div>
<div class="setting-row">
<span style="font-size:12px;color:var(--text-dim)">
2FA code only (click the 2FA field first)
</span>
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillTotpCombo">Ctrl+Shift+T</button>
</div>
<div class="setting-row"> <div class="setting-row">
<span style="font-size:12px;color:var(--text-dim)"> <span style="font-size:12px;color:var(--text-dim)">
Quick search picker (find entry, fill from anywhere) Quick search picker (find entry, fill from anywhere)
+43 -4
View File
@@ -81,12 +81,14 @@ function autofillCaptureFromEvent(e) {
function autofillPushHotkeys() { function autofillPushHotkeys() {
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull)); localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd)); localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
localStorage.setItem('autofillHotkeyTotp', JSON.stringify(state.autofillHotkeyTotp));
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey)); localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey)); localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey));
if (Bridge.active) { if (Bridge.active) {
Bridge.setAutofillHotkeys(state.autofillEnabled, { Bridge.setAutofillHotkeys(state.autofillEnabled, {
full: state.autofillHotkeyFull, full: state.autofillHotkeyFull,
password: state.autofillHotkeyPwd, password: state.autofillHotkeyPwd,
totp: state.autofillHotkeyTotp,
quickSearch: state.quickSearchHotkey, quickSearch: state.quickSearchHotkey,
newEntry: state.newEntryHotkey, newEntry: state.newEntryHotkey,
}); });
@@ -209,13 +211,22 @@ async function autofillHandleRequest(windowTitle, kind) {
} }
const titleLower = autofillStripBrowserSuffix(windowTitle).toLowerCase(); const titleLower = autofillStripBrowserSuffix(windowTitle).toLowerCase();
const scored = state.entries // The 2FA hotkey can only serve entries that actually carry a TOTP secret
// — filtering here (not after scoring) also means the picker never lists a
// candidate that would fail once chosen.
const pool = (kind === 'totp')
? state.entries.filter(e => e.totp_secret && e.totp_iv)
: state.entries;
const scored = pool
.map(e => ({ entry: e, score: autofillScore(e, titleLower) })) .map(e => ({ entry: e, score: autofillScore(e, titleLower) }))
.filter(x => x.score > 0) .filter(x => x.score > 0)
.sort((a, b) => b.score - a.score); .sort((a, b) => b.score - a.score);
if (scored.length === 0) { if (scored.length === 0) {
toast('Autofill: no match for "' + windowTitle.slice(0, 40) + '"', 'warning'); toast(kind === 'totp'
? 'No 2FA code stored for "' + windowTitle.slice(0, 40) + '"'
: 'Autofill: no match for "' + windowTitle.slice(0, 40) + '"',
'warning');
Bridge.cancelAutofill(); Bridge.cancelAutofill();
return; return;
} }
@@ -230,8 +241,34 @@ async function autofillHandleRequest(windowTitle, kind) {
openAutofillPicker(scored.map(x => x.entry), windowTitle, kind); openAutofillPicker(scored.map(x => x.entry), windowTitle, kind);
} }
// Decrypt and type an entry. kind = 'full' or 'password'. // Decrypt and type an entry. kind = 'full' | 'password' | 'totp'.
async function autofillFillEntry(entry, kind) { async function autofillFillEntry(entry, kind) {
// 2FA: type the current 6-digit code instead of the password. Sent in the
// password slot with an empty username, so Delphi's password-only path
// types it as-is — no new native code path.
if (kind === 'totp') {
const secret = await decryptTotpSecret(entry.totp_secret, entry.totp_iv);
if (!secret || secret === '[ERROR]') {
toast('2FA: cannot decrypt the secret', 'error');
Bridge.cancelAutofill();
return;
}
let code;
try { code = (await generateTOTP(secret)).code; }
catch (e) {
toast('2FA: invalid secret for ' + entryDisplayName(entry), 'error');
Bridge.cancelAutofill();
return;
}
autofillPendingToast = '2FA code filled: ' + entryDisplayName(entry);
Bridge.executeAutofill('', code);
fetch('' + '/audit', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ action: 'autofill_totp', site: entry.site }),
}).catch(() => {});
return;
}
const password = await decryptPwd(entry.encrypted_password, entry.iv); const password = await decryptPwd(entry.encrypted_password, entry.iv);
if (password === '[ERROR]') { if (password === '[ERROR]') {
toast('Autofill: decryption error', 'error'); toast('Autofill: decryption error', 'error');
@@ -285,7 +322,9 @@ function openAutofillPicker(entries, windowTitle, kind) {
} }
list.appendChild(btn); list.appendChild(btn);
}); });
const head = (kind === 'password' ? 'Pick entry (password only) — ' : 'Pick entry — ') const head = (kind === 'totp' ? 'Pick entry (2FA code) — '
: kind === 'password' ? 'Pick entry (password only) — '
: 'Pick entry — ')
+ entries.length + ' match "' + windowTitle.slice(0, 30) + '…"'; + entries.length + ' match "' + windowTitle.slice(0, 30) + '…"';
$('#autofillPickerTitle').textContent = head; $('#autofillPickerTitle').textContent = head;
$('#autofillPickerModal').classList.remove('is-hidden'); $('#autofillPickerModal').classList.remove('is-hidden');
+17 -2
View File
@@ -205,9 +205,14 @@ const Bridge = (() => {
const n = autofillComboToWin32(combos.newEntry); const n = autofillComboToWin32(combos.newEntry);
ne = '&ne_mods=' + n.mods + '&ne_vk=' + n.vk; ne = '&ne_mods=' + n.mods + '&ne_vk=' + n.vk;
} }
let tp = '';
if (combos.totp) {
const t = autofillComboToWin32(combos.totp);
tp = '&totp_mods=' + t.mods + '&totp_vk=' + t.vk;
}
cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') + cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') +
'&full_mods=' + f.mods + '&full_vk=' + f.vk + '&full_mods=' + f.mods + '&full_vk=' + f.vk +
'&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne); '&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne + tp);
}, },
// Called by Delphi after a setAutofillHotkeys request, with true if // Called by Delphi after a setAutofillHotkeys request, with true if
@@ -673,6 +678,8 @@ const state = {
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'), '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'),
autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') || autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') ||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'), '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'),
autofillHotkeyTotp: JSON.parse(localStorage.getItem('autofillHotkeyTotp') ||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"T"}'),
quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') || quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') ||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'), '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'),
newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') || newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') ||
@@ -6302,6 +6309,7 @@ function openSettings() {
// Hotkey capture buttons — labels reflect current combos. // Hotkey capture buttons — labels reflect current combos.
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey); $('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey); $('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none'; $('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
@@ -6690,7 +6698,8 @@ const SYNCED_SETTING_KEYS = [
// Hotkey combos are user preferences — values are portable. The // Hotkey combos are user preferences — values are portable. The
// registration itself is Windows-only, so non-Windows clients just // registration itself is Windows-only, so non-Windows clients just
// ignore them. // ignore them.
'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', 'newEntryHotkey', 'autofillHotkeyFull', 'autofillHotkeyPwd', 'autofillHotkeyTotp',
'quickSearchHotkey', 'newEntryHotkey',
// Sidebar section collapsed state. Object of { folders, tags, tools } // Sidebar section collapsed state. Object of { folders, tags, tools }
// booleans. Synced so the user gets the same fold state across devices. // booleans. Synced so the user gets the same fold state across devices.
'sidebarCollapsed', 'sidebarCollapsed',
@@ -6764,6 +6773,7 @@ async function loadServerSettings() {
case 'pageSize': localStorage.setItem('pageSize', String(v)); break; case 'pageSize': localStorage.setItem('pageSize', String(v)); break;
case 'autofillHotkeyFull': case 'autofillHotkeyFull':
case 'autofillHotkeyPwd': case 'autofillHotkeyPwd':
case 'autofillHotkeyTotp':
case 'quickSearchHotkey': case 'quickSearchHotkey':
case 'newEntryHotkey': case 'newEntryHotkey':
case 'sidebarCollapsed': case 'sidebarCollapsed':
@@ -7702,6 +7712,7 @@ async function init() {
const others = [ const others = [
kind !== 'full' ? state.autofillHotkeyFull : null, kind !== 'full' ? state.autofillHotkeyFull : null,
kind !== 'password' ? state.autofillHotkeyPwd : null, kind !== 'password' ? state.autofillHotkeyPwd : null,
kind !== 'totp' ? state.autofillHotkeyTotp : null,
kind !== 'quickSearch' ? state.quickSearchHotkey : null, kind !== 'quickSearch' ? state.quickSearchHotkey : null,
kind !== 'newEntry' ? state.newEntryHotkey : null, kind !== 'newEntry' ? state.newEntryHotkey : null,
].filter(Boolean); ].filter(Boolean);
@@ -7714,6 +7725,7 @@ async function init() {
// Commit. // Commit.
if (kind === 'full') state.autofillHotkeyFull = captured; if (kind === 'full') state.autofillHotkeyFull = captured;
else if (kind === 'password') state.autofillHotkeyPwd = captured; else if (kind === 'password') state.autofillHotkeyPwd = captured;
else if (kind === 'totp') state.autofillHotkeyTotp = captured;
else if (kind === 'quickSearch') state.quickSearchHotkey = captured; else if (kind === 'quickSearch') state.quickSearchHotkey = captured;
else /* newEntry */ state.newEntryHotkey = captured; else /* newEntry */ state.newEntryHotkey = captured;
btn.textContent = autofillComboLabel(captured); btn.textContent = autofillComboLabel(captured);
@@ -7726,16 +7738,19 @@ async function init() {
} }
bindHotkeyCapture('#settingAutofillFullCombo', 'full'); bindHotkeyCapture('#settingAutofillFullCombo', 'full');
bindHotkeyCapture('#settingAutofillPwdCombo', 'password'); bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
bindHotkeyCapture('#settingAutofillTotpCombo', 'totp');
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch'); bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
bindHotkeyCapture('#settingNewEntryCombo', 'newEntry'); bindHotkeyCapture('#settingNewEntryCombo', 'newEntry');
$('#settingAutofillResetHotkeys').addEventListener('click', () => { $('#settingAutofillResetHotkeys').addEventListener('click', () => {
state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' }; state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' };
state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' }; state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' };
state.autofillHotkeyTotp = { ctrl: true, shift: true, alt: false, win: false, key: 'T' };
state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' }; state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' };
state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' }; state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' };
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey); $('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey); $('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
autofillPushHotkeys(); autofillPushHotkeys();
+1
View File
@@ -187,6 +187,7 @@ const CHEATSHEET_GROUPS = [
items: [ items: [
{ keys: ['Ctrl', 'Shift', 'L'], desc: 'Autofill username + password into the active window' }, { keys: ['Ctrl', 'Shift', 'L'], desc: 'Autofill username + password into the active window' },
{ keys: ['Ctrl', 'Shift', 'P'], desc: 'Autofill password only (step-2 forms, unlock screens)' }, { keys: ['Ctrl', 'Shift', 'P'], desc: 'Autofill password only (step-2 forms, unlock screens)' },
{ keys: ['Ctrl', 'Shift', 'T'], desc: 'Autofill the 2FA code (click the 2FA field first)' },
{ keys: ['Ctrl', 'Shift', 'Q'], desc: 'Quick search → SendInput password into the active window' }, { keys: ['Ctrl', 'Shift', 'Q'], desc: 'Quick search → SendInput password into the active window' },
{ keys: ['Ctrl', 'Shift', 'A'], desc: 'Quick-add a new entry pre-filled with the foreground window title' }, { keys: ['Ctrl', 'Shift', 'A'], desc: 'Quick-add a new entry pre-filled with the foreground window title' },
], ],