fix(app): require a number when recording index shortcuts (#7365)

Tab and workspace navigation need a final 1–9 index. Prevent saving an incomplete recorded sequence and explain the requirement while preserving arbitrary chord prefixes.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-19 11:55:10 +02:00
committed by GitHub
parent b6dde61a7f
commit e760764b37
13 changed files with 81 additions and 2 deletions
+1
View File
@@ -2333,6 +2333,7 @@ export const ar: TranslationResources = {
searchPlaceholder: "البحث في الاختصارات",
unavailableOnMobile: "اختصارات لوحة المفاتيح متاحة فقط على سطح المكتب",
capturePrompt: "اضغط على الاختصار...",
captureIndexPrompt: "اختم برقم من 1 إلى 9.",
unassigned: "غير معين",
actions: {
menu: "إجراءات {{name}}",
+1
View File
@@ -2456,6 +2456,7 @@ export const en = {
searchPlaceholder: "Search shortcuts",
unavailableOnMobile: "Keyboard shortcuts are only available on desktop",
capturePrompt: "Press shortcut...",
captureIndexPrompt: "End with a number from 1 to 9.",
unassigned: "Not set",
actions: {
menu: "Actions for {{name}}",
+1
View File
@@ -2387,6 +2387,7 @@ export const es: TranslationResources = {
searchPlaceholder: "Buscar atajos",
unavailableOnMobile: "Los atajos de teclado solo están disponibles en el escritorio",
capturePrompt: "Presione el acceso directo...",
captureIndexPrompt: "Termina con un número del 1 al 9.",
unassigned: "Sin asignar",
actions: {
menu: "Acciones para {{name}}",
+1
View File
@@ -2387,6 +2387,7 @@ export const fr: TranslationResources = {
searchPlaceholder: "Rechercher des raccourcis",
unavailableOnMobile: "Les raccourcis clavier ne sont disponibles que sur l’app de bureau",
capturePrompt: "Appuyez sur le raccourci…",
captureIndexPrompt: "Terminez par un chiffre de 1 à 9.",
unassigned: "Non attribué",
actions: {
menu: "Actions pour {{name}}",
+1
View File
@@ -2354,6 +2354,7 @@ export const ja: TranslationResources = {
searchPlaceholder: "ショートカットを検索",
unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます",
capturePrompt: "ショートカットを押してください...",
captureIndexPrompt: "最後に1〜9の数字を入力してください。",
unassigned: "未設定",
actions: {
menu: "{{name}} のアクション",
+1
View File
@@ -2343,6 +2343,7 @@ export const ko: TranslationResources = {
searchPlaceholder: "검색 단축키",
unavailableOnMobile: "키보드 단축키는 데스크톱에서만 사용할 수 있습니다",
capturePrompt: "단축키를 누르세요...",
captureIndexPrompt: "마지막에 1부터 9까지의 숫자를 입력하세요.",
unassigned: "설정되지 않음",
actions: {
menu: "{{name}} 작업",
+1
View File
@@ -2368,6 +2368,7 @@ export const ptBR: TranslationResources = {
searchPlaceholder: "Pesquisar atalhos",
unavailableOnMobile: "Atalhos de teclado estão disponíveis apenas no desktop",
capturePrompt: "Pressione o atalho...",
captureIndexPrompt: "Termine com um número de 1 a 9.",
unassigned: "Não definido",
actions: {
menu: "Ações para {{name}}",
+1
View File
@@ -2373,6 +2373,7 @@ export const ru: TranslationResources = {
searchPlaceholder: "Поиск сочетаний клавиш",
unavailableOnMobile: "Сочетания клавиш доступны только на рабочем столе.",
capturePrompt: "Нажмите сочетание клавиш...",
captureIndexPrompt: "Завершите цифрой от 1 до 9.",
unassigned: "Не задано",
actions: {
menu: "Действия для {{name}}",
+1
View File
@@ -2305,6 +2305,7 @@ export const zhCN: TranslationResources = {
searchPlaceholder: "搜索快捷键",
unavailableOnMobile: "键盘快捷键仅在桌面端可用",
capturePrompt: "按下快捷键...",
captureIndexPrompt: "请以 1 到 9 的数字结尾。",
unassigned: "未设置",
actions: {
menu: "{{name}} 的操作",
@@ -4,7 +4,9 @@ import { keyboardEventToComboString } from "./shortcut-string";
import {
buildKeyboardShortcutHelpSections,
buildEffectiveBindings,
DEFAULT_BINDINGS,
normalizeCapturedShortcutCombo,
isShortcutCaptureComplete,
getBindingIdForAction,
getDefaultKeysForAction,
getWorkspaceIndexJumpModifierKey,
@@ -1390,3 +1392,22 @@ describe("number-group shortcut capture", () => {
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+9")).toBe("Ctrl+Digit");
});
});
describe("shortcut capture completion", () => {
const indexBindings = DEFAULT_BINDINGS.filter((binding) => binding.payload?.type === "index");
it.each(indexBindings)("requires a final 1–9 number for $id", ({ id }) => {
for (const combos of [[], ["Ctrl+K"], ["Ctrl+0"], ["Ctrl+Digit", "Ctrl+K"]]) {
expect(isShortcutCaptureComplete(id, combos)).toBe(false);
}
for (let index = 1; index <= 9; index++) {
const combo = normalizeCapturedShortcutCombo(id, `Ctrl+${index}`);
expect(isShortcutCaptureComplete(id, [combo])).toBe(true);
expect(isShortcutCaptureComplete(id, ["Ctrl+K", combo])).toBe(true);
}
});
it("keeps ordinary shortcuts available without a number", () => {
expect(isShortcutCaptureComplete("show-shortcuts", [])).toBe(false);
expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+K"])).toBe(true);
expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+0"])).toBe(true);
});
});
@@ -1217,6 +1217,14 @@ export function normalizeCapturedShortcutCombo(bindingId: string, combo: string)
return combo.replace(/(^|\+)[1-9]$/, "$1Digit");
}
/** Index navigation needs a final number; earlier combos may be any chord prefix. */
export function isShortcutCaptureComplete(bindingId: string, combos: readonly string[]): boolean {
const lastCombo = combos.at(-1);
if (!lastCombo) return false;
const binding = DEFAULT_BINDINGS.find((candidate) => candidate.id === bindingId);
return binding?.payload?.type !== "index" || /(^|\+)(Digit|[1-9])$/.test(lastCombo);
}
export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShortcutBinding[] {
return DEFAULT_BINDINGS.map(function (binding) {
const override = overrides[binding.id];
@@ -20,6 +20,7 @@ import {
buildKeyboardShortcutHelpSections,
getBindingIdForAction,
normalizeCapturedShortcutCombo,
isShortcutCaptureComplete,
getDefaultKeysForAction,
resolveShortcutKeysForAction,
type KeyboardShortcutHelpRow,
@@ -269,6 +270,8 @@ function ShortcutRow({
const isBindable = bindingId !== null;
const showDone = isCapturing && capturedCombos.length > 0;
const captureComplete =
bindingId !== null && isShortcutCaptureComplete(bindingId, capturedCombos);
const showClear = displayChord !== null;
// Reset restores the default, so it is only meaningful when there is a
// default to restore. A binding that ships without one would otherwise show a
@@ -288,11 +291,14 @@ function ShortcutRow({
capturedCombos={capturedCombos}
heldModifiers={heldModifiers}
/>
{showDone && !captureComplete && (
<Text style={styles.capturingText}>{t("settings.shortcuts.captureIndexPrompt")}</Text>
)}
</View>
{isCapturing ? (
<>
{showDone && (
<Button variant="ghost" size="sm" onPress={onDone}>
<Button variant="ghost" size="sm" disabled={!captureComplete} onPress={onDone}>
{t("settings.shortcuts.actions.done")}
</Button>
)}
@@ -357,7 +363,10 @@ export function KeyboardShortcutsSection() {
);
const saveCapture = useCallback(() => {
if (capturingBindingId === null || capturedCombos.length === 0) {
if (
capturingBindingId === null ||
!isShortcutCaptureComplete(capturingBindingId, capturedCombos)
) {
return;
}
void setOverride(capturingBindingId, capturedCombos.join(" "));
@@ -261,3 +261,35 @@ test("rebinding Jump to tab preserves the number group after reload", async ({
await openSettingsSection(page, "shortcuts");
await expectTabNumberRange(page);
});
async function expectNumberRequiredBeforeSaving(page: Page, label: string) {
const row = page.getByText(label, { exact: true }).locator("..");
await expect(row.getByRole("button", { name: "Done", exact: true })).toBeDisabled();
await expect(row.getByText("End with a number from 1 to 9.", { exact: true })).toBeVisible();
}
async function completeNumberShortcut(page: Page, label: string) {
const row = page.getByText(label, { exact: true }).locator("..");
await page.keyboard.press("Backspace");
await page.keyboard.press("Control+2");
await expect(row.getByRole("button", { name: "Done", exact: true })).toBeEnabled();
await row.getByRole("button", { name: "Done", exact: true }).click();
await expect(row.getByText("⌃K", { exact: true })).toBeVisible();
await expect(row.getByText("⌃1-9", { exact: true })).toBeVisible();
}
for (const { label, action } of [
{ label: "Jump to tab", action: "workspace-tab-jump-index" },
{ label: "Jump to workspace", action: "workspace-jump-index" },
]) {
test(`${label} requires a final number before a recorded shortcut can be saved`, async ({
page,
}) => {
await openShortcutsSettings(page);
await captureShortcut(page, label, action, "Control+K");
await expectNumberRequiredBeforeSaving(page, label);
await page.keyboard.press("Control+0");
await expectNumberRequiredBeforeSaving(page, label);
await completeNumberShortcut(page, label);
});
}