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:
@@ -148,6 +148,12 @@ Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
|
||||
|
||||
- `Ctrl+Shift+L` : autofill complet (user + Tab + password)
|
||||
- `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+D` : toggle debug panel (uniquement si `config.txt` présent)
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@ type
|
||||
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
|
||||
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type
|
||||
// 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
|
||||
// window HWND and its title (captured before any focus change), plus
|
||||
@@ -117,6 +120,7 @@ type
|
||||
FAutofillRegistered: Boolean;
|
||||
FAutofillFullActive: Boolean;
|
||||
FAutofillPwdActive: Boolean;
|
||||
FAutofillTotpActive: Boolean;
|
||||
FOnAutofillRequest: TAutofillRequestEvent;
|
||||
FDebugHotkeyRegistered: Boolean;
|
||||
FOnDebugHotkey: TProc;
|
||||
@@ -162,8 +166,10 @@ type
|
||||
// Returns True if both hotkeys registered successfully. If one or both
|
||||
// failed (clash with another app), best-effort: whichever succeeded
|
||||
// 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,
|
||||
APwdMods, APwdVk: Word): Boolean;
|
||||
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||||
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
|
||||
// user-chosen combo. True on success.
|
||||
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||||
@@ -287,6 +293,7 @@ const
|
||||
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
|
||||
AUTOFILL_HOTKEY_ID_TOTP = 47; // Ctrl+Shift+T → type the 2FA code
|
||||
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
|
||||
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
|
||||
|
||||
@@ -895,13 +902,16 @@ begin
|
||||
|
||||
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
|
||||
((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
|
||||
// Capture the foreground window BEFORE any focus change, then fire the
|
||||
// callback so the host can match the title against vault entries.
|
||||
var LKind: TAutofillKind;
|
||||
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
|
||||
LKind := akPasswordOnly
|
||||
else if AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP then
|
||||
LKind := akTotp
|
||||
else
|
||||
LKind := akFull;
|
||||
var LTarget := GetForegroundWindow;
|
||||
@@ -920,7 +930,7 @@ end;
|
||||
// =============================================================================
|
||||
|
||||
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
|
||||
APwdMods, APwdVk: Word): Boolean;
|
||||
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||||
begin
|
||||
// Tear down whatever is currently registered before installing the new
|
||||
// combos. RegisterHotKey would fail if the same ID is already taken.
|
||||
@@ -934,6 +944,11 @@ begin
|
||||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||||
FAutofillPwdActive := False;
|
||||
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
|
||||
// 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
|
||||
FAutofillPwdActive := RegisterHotKey(FMsgWindow,
|
||||
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;
|
||||
Result := FAutofillFullActive and FAutofillPwdActive;
|
||||
FAutofillRegistered := FAutofillFullActive or 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;
|
||||
|
||||
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||||
@@ -1041,10 +1062,12 @@ end;
|
||||
|
||||
procedure TPMBridge.RegisterAutofillHotkey;
|
||||
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.
|
||||
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;
|
||||
|
||||
procedure TPMBridge.UnregisterAutofillHotkey;
|
||||
@@ -1059,6 +1082,11 @@ begin
|
||||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||||
FAutofillPwdActive := False;
|
||||
end;
|
||||
if FAutofillTotpActive then
|
||||
begin
|
||||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
|
||||
FAutofillTotpActive := False;
|
||||
end;
|
||||
FAutofillRegistered := False;
|
||||
end;
|
||||
|
||||
|
||||
@@ -895,10 +895,14 @@ begin
|
||||
var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L')));
|
||||
var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6));
|
||||
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,
|
||||
LPwdMods, LPwdVk);
|
||||
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d (all_ok=%s)',
|
||||
[LFullMods, LFullVk, LPwdMods, LPwdVk, BoolToStr(LAllOk, True)]));
|
||||
LPwdMods, LPwdVk,
|
||||
LTotpMods, LTotpVk);
|
||||
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
|
||||
// (typically a clash with another app's global hotkey).
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
@@ -1542,7 +1546,11 @@ begin
|
||||
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(
|
||||
'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' +
|
||||
|
||||
@@ -675,6 +675,12 @@
|
||||
</span>
|
||||
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button>
|
||||
</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">
|
||||
<span style="font-size:12px;color:var(--text-dim)">
|
||||
Quick search picker (find entry, fill from anywhere)
|
||||
|
||||
+43
-4
@@ -81,12 +81,14 @@ function autofillCaptureFromEvent(e) {
|
||||
function autofillPushHotkeys() {
|
||||
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
|
||||
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
|
||||
localStorage.setItem('autofillHotkeyTotp', JSON.stringify(state.autofillHotkeyTotp));
|
||||
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
|
||||
localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey));
|
||||
if (Bridge.active) {
|
||||
Bridge.setAutofillHotkeys(state.autofillEnabled, {
|
||||
full: state.autofillHotkeyFull,
|
||||
password: state.autofillHotkeyPwd,
|
||||
totp: state.autofillHotkeyTotp,
|
||||
quickSearch: state.quickSearchHotkey,
|
||||
newEntry: state.newEntryHotkey,
|
||||
});
|
||||
@@ -209,13 +211,22 @@ async function autofillHandleRequest(windowTitle, kind) {
|
||||
}
|
||||
|
||||
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) }))
|
||||
.filter(x => x.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -230,8 +241,34 @@ async function autofillHandleRequest(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) {
|
||||
// 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);
|
||||
if (password === '[ERROR]') {
|
||||
toast('Autofill: decryption error', 'error');
|
||||
@@ -285,7 +322,9 @@ function openAutofillPicker(entries, windowTitle, kind) {
|
||||
}
|
||||
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) + '…"';
|
||||
$('#autofillPickerTitle').textContent = head;
|
||||
$('#autofillPickerModal').classList.remove('is-hidden');
|
||||
|
||||
@@ -205,9 +205,14 @@ const Bridge = (() => {
|
||||
const n = autofillComboToWin32(combos.newEntry);
|
||||
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') +
|
||||
'&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
|
||||
@@ -673,6 +678,8 @@ const state = {
|
||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'),
|
||||
autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') ||
|
||||
'{"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') ||
|
||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'),
|
||||
newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') ||
|
||||
@@ -6302,6 +6309,7 @@ function openSettings() {
|
||||
// Hotkey capture buttons — labels reflect current combos.
|
||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
|
||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
||||
@@ -6690,7 +6698,8 @@ const SYNCED_SETTING_KEYS = [
|
||||
// Hotkey combos are user preferences — values are portable. The
|
||||
// registration itself is Windows-only, so non-Windows clients just
|
||||
// ignore them.
|
||||
'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', 'newEntryHotkey',
|
||||
'autofillHotkeyFull', 'autofillHotkeyPwd', 'autofillHotkeyTotp',
|
||||
'quickSearchHotkey', 'newEntryHotkey',
|
||||
// Sidebar section collapsed state. Object of { folders, tags, tools }
|
||||
// booleans. Synced so the user gets the same fold state across devices.
|
||||
'sidebarCollapsed',
|
||||
@@ -6764,6 +6773,7 @@ async function loadServerSettings() {
|
||||
case 'pageSize': localStorage.setItem('pageSize', String(v)); break;
|
||||
case 'autofillHotkeyFull':
|
||||
case 'autofillHotkeyPwd':
|
||||
case 'autofillHotkeyTotp':
|
||||
case 'quickSearchHotkey':
|
||||
case 'newEntryHotkey':
|
||||
case 'sidebarCollapsed':
|
||||
@@ -7702,6 +7712,7 @@ async function init() {
|
||||
const others = [
|
||||
kind !== 'full' ? state.autofillHotkeyFull : null,
|
||||
kind !== 'password' ? state.autofillHotkeyPwd : null,
|
||||
kind !== 'totp' ? state.autofillHotkeyTotp : null,
|
||||
kind !== 'quickSearch' ? state.quickSearchHotkey : null,
|
||||
kind !== 'newEntry' ? state.newEntryHotkey : null,
|
||||
].filter(Boolean);
|
||||
@@ -7714,6 +7725,7 @@ async function init() {
|
||||
// Commit.
|
||||
if (kind === 'full') state.autofillHotkeyFull = captured;
|
||||
else if (kind === 'password') state.autofillHotkeyPwd = captured;
|
||||
else if (kind === 'totp') state.autofillHotkeyTotp = captured;
|
||||
else if (kind === 'quickSearch') state.quickSearchHotkey = captured;
|
||||
else /* newEntry */ state.newEntryHotkey = captured;
|
||||
btn.textContent = autofillComboLabel(captured);
|
||||
@@ -7726,16 +7738,19 @@ async function init() {
|
||||
}
|
||||
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
||||
bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
|
||||
bindHotkeyCapture('#settingAutofillTotpCombo', 'totp');
|
||||
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
|
||||
bindHotkeyCapture('#settingNewEntryCombo', 'newEntry');
|
||||
|
||||
$('#settingAutofillResetHotkeys').addEventListener('click', () => {
|
||||
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.autofillHotkeyTotp = { ctrl: true, shift: true, alt: false, win: false, key: 'T' };
|
||||
state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' };
|
||||
state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' };
|
||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
|
||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||
autofillPushHotkeys();
|
||||
|
||||
@@ -187,6 +187,7 @@ const CHEATSHEET_GROUPS = [
|
||||
items: [
|
||||
{ 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', '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', 'A'], desc: 'Quick-add a new entry pre-filled with the foreground window title' },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user