import { useState, useEffect } from "react"; import { View, Text } from "react-native"; import { useIsFocused } from "@react-navigation/native"; import { StyleSheet } from "react-native-unistyles"; import { settingsStyles } from "@/styles/settings"; import { SettingsSection } from "@/screens/settings/settings-section"; import { Button } from "@/components/ui/button"; import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { buildKeyboardShortcutHelpSections, getBindingIdForAction, type KeyboardShortcutHelpRow, } from "@/keyboard/keyboard-shortcuts"; 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"; import { isNative } from "@/constants/platform"; function ShortcutSequence({ chord, heldModifiers, }: { chord: string[] | null; heldModifiers: string | null; }) { if ((!chord || chord.length === 0) && !heldModifiers) { return Press shortcut...; } const displayCombos = [...(chord ?? [])]; if (heldModifiers) { displayCombos.push(heldModifiers); } return ; } function ShortcutRow({ row, bindingId, overrideCombo, isCapturing, capturedCombos, heldModifiers, onRebind, onDone, onCancel, onReset, }: { row: KeyboardShortcutHelpRow; bindingId: string | null; overrideCombo: string | undefined; isCapturing: boolean; capturedCombos: string[]; heldModifiers: string | null; onRebind: () => void; onDone: () => void; onCancel: () => void; onReset: () => void; }) { const displayChord = overrideCombo ? chordStringToShortcutKeys(overrideCombo) : [row.keys]; return ( {row.label} {isCapturing ? ( ) : ( )} {bindingId !== null && ( <> {isCapturing && capturedCombos.length > 0 ? ( ) : null} )} {overrideCombo !== undefined && !isCapturing && ( )} ); } export function KeyboardShortcutsSection() { const [capturingBindingId, setCapturingBindingId] = useState(null); const [capturedCombos, setCapturedCombos] = useState([]); const [heldModifiers, setHeldModifiers] = useState(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); } function saveCapture() { if (capturingBindingId === null || capturedCombos.length === 0) { return; } void setOverride(capturingBindingId, capturedCombos.join(" ")); cancelCapture(); } useEffect(() => { if (isNative) return; if (capturingBindingId === null) return; function handleKeyDown(event: KeyboardEvent) { event.preventDefault(); event.stopPropagation(); const key = event.key ?? ""; if (key === "Backspace") { setCapturedCombos((current) => (current.length > 0 ? current.slice(0, -1) : current)); return; } const comboString = keyboardEventToComboString(event); if (comboString === null) { setHeldModifiers(heldModifiersFromEvent(event)); return; } setHeldModifiers(null); setCapturedCombos((current) => [...current, comboString]); } window.addEventListener("keydown", handleKeyDown, true); return () => { window.removeEventListener("keydown", handleKeyDown, true); }; }, [capturingBindingId]); useEffect(() => { return () => { setCapturingShortcut(false); }; }, [setCapturingShortcut]); if (isNative) { return ( Keyboard shortcuts are only available on desktop. ); } const resetAllButton = hasOverrides ? ( ) : undefined; return ( <> {sections.map(function (section, sectionIndex) { return ( {section.rows.map(function (row, index) { const bindingId = getBindingIdForAction(row.id, { isMac, isDesktop: isDesktopApp, }); const overrideCombo = bindingId ? overrides[bindingId] : undefined; return ( { if (bindingId) { startCapture(bindingId); } }} onDone={saveCapture} onCancel={cancelCapture} onReset={() => { if (bindingId) void removeOverride(bindingId); }} /> {index < section.rows.length - 1 && } ); })} ); })} ); } const styles = StyleSheet.create((theme) => ({ row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[3], }, rowCapturing: { backgroundColor: theme.colors.surface2, }, rowLabel: { fontSize: theme.fontSize.sm, color: theme.colors.foreground, flexShrink: 1, }, rowActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, capturingText: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, resetText: { color: theme.colors.foregroundMuted, }, separator: { height: 1, backgroundColor: theme.colors.border, }, mobileCard: { padding: theme.spacing[4], }, mobileText: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, }));