fix: show modifier keys and missing keys during shortcut rebinding

Add Tab, F1-F12, Delete, Home, End, PageUp, PageDown, Insert to the
key map so they can be captured during rebinding. Show held modifier
keys (Ctrl, Alt, Shift, Cmd) as live feedback matching VS Code's
keydown-only approach — modifiers persist after release until the
next keypress. Cancel capture when navigating away from settings.
This commit is contained in:
Mohamed Boudra
2026-04-01 18:32:31 +07:00
parent c13972c835
commit ade1e338ea
2 changed files with 52 additions and 4 deletions
@@ -43,6 +43,17 @@ KEY_MAP["ArrowLeft"] = { code: "ArrowLeft" };
KEY_MAP["ArrowRight"] = { code: "ArrowRight" }; KEY_MAP["ArrowRight"] = { code: "ArrowRight" };
KEY_MAP["ArrowUp"] = { code: "ArrowUp" }; KEY_MAP["ArrowUp"] = { code: "ArrowUp" };
KEY_MAP["ArrowDown"] = { code: "ArrowDown" }; KEY_MAP["ArrowDown"] = { code: "ArrowDown" };
KEY_MAP["Tab"] = { code: "Tab" };
KEY_MAP["Delete"] = { code: "Delete" };
KEY_MAP["Home"] = { code: "Home" };
KEY_MAP["End"] = { code: "End" };
KEY_MAP["PageUp"] = { code: "PageUp" };
KEY_MAP["PageDown"] = { code: "PageDown" };
KEY_MAP["Insert"] = { code: "Insert" };
for (let i = 1; i <= 12; i++) {
KEY_MAP[`F${i}`] = { code: `F${i}` };
}
const CODE_TO_KEY: Record<string, string> = {}; const CODE_TO_KEY: Record<string, string> = {};
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) { for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
@@ -170,6 +181,15 @@ export function chordStringToShortcutKeys(s: string): ShortcutKey[][] {
return s.split(" ").map(comboStringToShortcutKeys); return s.split(" ").map(comboStringToShortcutKeys);
} }
export function heldModifiersFromEvent(event: KeyboardEvent): string | null {
const parts: string[] = [];
if (event.ctrlKey) parts.push("Ctrl");
if (event.altKey) parts.push("Alt");
if (event.shiftKey) parts.push("Shift");
if (event.metaKey) parts.push("Cmd");
return parts.length > 0 ? parts.join("+") : null;
}
export function keyboardEventToComboString(event: KeyboardEvent): string | null { export function keyboardEventToComboString(event: KeyboardEvent): string | null {
if (MODIFIER_CODES.has(event.code)) { if (MODIFIER_CODES.has(event.code)) {
return null; return null;
@@ -1,5 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { View, Text, Platform } from "react-native"; import { View, Text, Platform } from "react-native";
import { useIsFocused } from "@react-navigation/native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings"; import { settingsStyles } from "@/styles/settings";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -13,18 +14,30 @@ import {
import { import {
chordStringToShortcutKeys, chordStringToShortcutKeys,
comboStringToShortcutKeys, comboStringToShortcutKeys,
heldModifiersFromEvent,
keyboardEventToComboString, keyboardEventToComboString,
} from "@/keyboard/shortcut-string"; } from "@/keyboard/shortcut-string";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsElectronRuntime } from "@/constants/layout"; import { getIsElectronRuntime } from "@/constants/layout";
function ShortcutSequence({ chord }: { chord: string[] | null }) { function ShortcutSequence({
if (!chord || chord.length === 0) { chord,
heldModifiers,
}: {
chord: string[] | null;
heldModifiers: string | null;
}) {
if ((!chord || chord.length === 0) && !heldModifiers) {
return <Text style={styles.capturingText}>Press shortcut...</Text>; return <Text style={styles.capturingText}>Press shortcut...</Text>;
} }
return <Shortcut chord={chord.map(comboStringToShortcutKeys)} />; const displayCombos = [...(chord ?? [])];
if (heldModifiers) {
displayCombos.push(heldModifiers);
}
return <Shortcut chord={displayCombos.map(comboStringToShortcutKeys)} />;
} }
function ShortcutRow({ function ShortcutRow({
@@ -33,6 +46,7 @@ function ShortcutRow({
overrideCombo, overrideCombo,
isCapturing, isCapturing,
capturedCombos, capturedCombos,
heldModifiers,
onRebind, onRebind,
onDone, onDone,
onCancel, onCancel,
@@ -43,6 +57,7 @@ function ShortcutRow({
overrideCombo: string | undefined; overrideCombo: string | undefined;
isCapturing: boolean; isCapturing: boolean;
capturedCombos: string[]; capturedCombos: string[];
heldModifiers: string | null;
onRebind: () => void; onRebind: () => void;
onDone: () => void; onDone: () => void;
onCancel: () => void; onCancel: () => void;
@@ -55,7 +70,7 @@ function ShortcutRow({
<Text style={styles.rowLabel}>{row.label}</Text> <Text style={styles.rowLabel}>{row.label}</Text>
<View style={styles.rowActions}> <View style={styles.rowActions}>
{isCapturing ? ( {isCapturing ? (
<ShortcutSequence chord={capturedCombos} /> <ShortcutSequence chord={capturedCombos} heldModifiers={heldModifiers} />
) : ( ) : (
<Shortcut chord={displayChord} /> <Shortcut chord={displayChord} />
)} )}
@@ -88,22 +103,32 @@ function ShortcutRow({
export function KeyboardShortcutsSection() { export function KeyboardShortcutsSection() {
const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null); const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null);
const [capturedCombos, setCapturedCombos] = useState<string[]>([]); const [capturedCombos, setCapturedCombos] = useState<string[]>([]);
const [heldModifiers, setHeldModifiers] = useState<string | null>(null);
const { overrides, hasOverrides, setOverride, removeOverride, resetAll } = const { overrides, hasOverrides, setOverride, removeOverride, resetAll } =
useKeyboardShortcutOverrides(); useKeyboardShortcutOverrides();
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut); const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
const isFocused = useIsFocused();
const isMac = getShortcutOs() === "mac"; const isMac = getShortcutOs() === "mac";
const isDesktopApp = getIsElectronRuntime(); const isDesktopApp = getIsElectronRuntime();
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }); const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
useEffect(() => {
if (!isFocused && capturingBindingId !== null) {
cancelCapture();
}
}, [isFocused]);
function cancelCapture() { function cancelCapture() {
setCapturedCombos([]); setCapturedCombos([]);
setHeldModifiers(null);
setCapturingBindingId(null); setCapturingBindingId(null);
setCapturingShortcut(false); setCapturingShortcut(false);
} }
function startCapture(bindingId: string) { function startCapture(bindingId: string) {
setCapturedCombos([]); setCapturedCombos([]);
setHeldModifiers(null);
setCapturingBindingId(bindingId); setCapturingBindingId(bindingId);
setCapturingShortcut(true); setCapturingShortcut(true);
} }
@@ -132,9 +157,11 @@ export function KeyboardShortcutsSection() {
const comboString = keyboardEventToComboString(event); const comboString = keyboardEventToComboString(event);
if (comboString === null) { if (comboString === null) {
setHeldModifiers(heldModifiersFromEvent(event));
return; return;
} }
setHeldModifiers(null);
setCapturedCombos((current) => [...current, comboString]); setCapturedCombos((current) => [...current, comboString]);
} }
@@ -194,6 +221,7 @@ export function KeyboardShortcutsSection() {
overrideCombo={overrideCombo} overrideCombo={overrideCombo}
isCapturing={capturingBindingId === bindingId} isCapturing={capturingBindingId === bindingId}
capturedCombos={capturingBindingId === bindingId ? capturedCombos : []} capturedCombos={capturingBindingId === bindingId ? capturedCombos : []}
heldModifiers={capturingBindingId === bindingId ? heldModifiers : null}
onRebind={() => { onRebind={() => {
if (bindingId) { if (bindingId) {
startCapture(bindingId); startCapture(bindingId);