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["ArrowUp"] = { code: "ArrowUp" };
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> = {};
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
@@ -170,6 +181,15 @@ export function chordStringToShortcutKeys(s: string): ShortcutKey[][] {
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 {
if (MODIFIER_CODES.has(event.code)) {
return null;
@@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { View, Text, Platform } from "react-native";
import { useIsFocused } from "@react-navigation/native";
import { StyleSheet } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
import { Button } from "@/components/ui/button";
@@ -13,18 +14,30 @@ import {
import {
chordStringToShortcutKeys,
comboStringToShortcutKeys,
heldModifiersFromEvent,
keyboardEventToComboString,
} from "@/keyboard/shortcut-string";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsElectronRuntime } from "@/constants/layout";
function ShortcutSequence({ chord }: { chord: string[] | null }) {
if (!chord || chord.length === 0) {
function ShortcutSequence({
chord,
heldModifiers,
}: {
chord: string[] | null;
heldModifiers: string | null;
}) {
if ((!chord || chord.length === 0) && !heldModifiers) {
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({
@@ -33,6 +46,7 @@ function ShortcutRow({
overrideCombo,
isCapturing,
capturedCombos,
heldModifiers,
onRebind,
onDone,
onCancel,
@@ -43,6 +57,7 @@ function ShortcutRow({
overrideCombo: string | undefined;
isCapturing: boolean;
capturedCombos: string[];
heldModifiers: string | null;
onRebind: () => void;
onDone: () => void;
onCancel: () => void;
@@ -55,7 +70,7 @@ function ShortcutRow({
<Text style={styles.rowLabel}>{row.label}</Text>
<View style={styles.rowActions}>
{isCapturing ? (
<ShortcutSequence chord={capturedCombos} />
<ShortcutSequence chord={capturedCombos} heldModifiers={heldModifiers} />
) : (
<Shortcut chord={displayChord} />
)}
@@ -88,22 +103,32 @@ function ShortcutRow({
export function KeyboardShortcutsSection() {
const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null);
const [capturedCombos, setCapturedCombos] = useState<string[]>([]);
const [heldModifiers, setHeldModifiers] = useState<string | null>(null);
const { overrides, hasOverrides, setOverride, removeOverride, resetAll } =
useKeyboardShortcutOverrides();
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
const isFocused = useIsFocused();
const isMac = getShortcutOs() === "mac";
const isDesktopApp = getIsElectronRuntime();
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
useEffect(() => {
if (!isFocused && capturingBindingId !== null) {
cancelCapture();
}
}, [isFocused]);
function cancelCapture() {
setCapturedCombos([]);
setHeldModifiers(null);
setCapturingBindingId(null);
setCapturingShortcut(false);
}
function startCapture(bindingId: string) {
setCapturedCombos([]);
setHeldModifiers(null);
setCapturingBindingId(bindingId);
setCapturingShortcut(true);
}
@@ -132,9 +157,11 @@ export function KeyboardShortcutsSection() {
const comboString = keyboardEventToComboString(event);
if (comboString === null) {
setHeldModifiers(heldModifiersFromEvent(event));
return;
}
setHeldModifiers(null);
setCapturedCombos((current) => [...current, comboString]);
}
@@ -194,6 +221,7 @@ export function KeyboardShortcutsSection() {
overrideCombo={overrideCombo}
isCapturing={capturingBindingId === bindingId}
capturedCombos={capturingBindingId === bindingId ? capturedCombos : []}
heldModifiers={capturingBindingId === bindingId ? heldModifiers : null}
onRebind={() => {
if (bindingId) {
startCapture(bindingId);