import { useEffect, useMemo, useRef } from 'react' import { isTauri } from '../mock-tauri' import { filterEntries, sortByModified, buildRelationshipGroups } from '../utils/noteListHelpers' import type { VaultEntry, SidebarSelection } from '../types' interface Tab { entry: VaultEntry content: string } interface KeyboardNavigationOptions { tabs: Tab[] activeTabPath: string | null entries: VaultEntry[] selection: SidebarSelection allContent: Record onSwitchTab: (path: string) => void onReplaceActiveTab: (entry: VaultEntry) => void onSelectNote: (entry: VaultEntry) => void } function computeVisibleNotes( entries: VaultEntry[], selection: SidebarSelection, allContent: Record, ): VaultEntry[] { if (selection.kind === 'entity') { return buildRelationshipGroups(selection.entry, entries, allContent) .flatMap((g) => g.entries) } return [...filterEntries(entries, selection)].sort(sortByModified) } function navigateTab( tabsRef: React.RefObject, activeTabPathRef: React.RefObject, onSwitchTab: React.RefObject<(path: string) => void>, direction: 1 | -1, ) { const currentTabs = tabsRef.current! if (currentTabs.length === 0) return const currentPath = activeTabPathRef.current const currentIndex = currentTabs.findIndex((t) => t.entry.path === currentPath) const nextIndex = (currentIndex + direction + currentTabs.length) % currentTabs.length onSwitchTab.current!(currentTabs[nextIndex].entry.path) } function navigateNote( visibleNotesRef: React.RefObject, activeTabPathRef: React.RefObject, onReplace: React.RefObject<(entry: VaultEntry) => void>, onSelect: React.RefObject<(entry: VaultEntry) => void>, direction: 1 | -1, ) { const notes = visibleNotesRef.current! if (notes.length === 0) return const currentPath = activeTabPathRef.current const currentIndex = notes.findIndex((n) => n.path === currentPath) const nextIndex = currentIndex === -1 ? (direction === 1 ? 0 : notes.length - 1) : (currentIndex + direction + notes.length) % notes.length const nextNote = notes[nextIndex] if (currentPath) { onReplace.current!(nextNote) } else { onSelect.current!(nextNote) } } type ShortcutKind = 'tab' | 'note' | null function classifyShortcut(e: KeyboardEvent, inTauri: boolean): ShortcutKind { const mod = e.metaKey || e.ctrlKey if (!mod) return null const isTabShortcut = inTauri ? (e.altKey && !e.shiftKey) : (e.shiftKey && !e.altKey) if (isTabShortcut && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return 'tab' if (e.altKey && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) return 'note' return null } function arrowDirection(key: string): 1 | -1 { return (key === 'ArrowRight' || key === 'ArrowDown') ? 1 : -1 } function useLatestRef(value: T): React.RefObject { const ref = useRef(value) useEffect(() => { ref.current = value }) return ref } export function useKeyboardNavigation({ tabs, activeTabPath, entries, selection, allContent, onSwitchTab, onReplaceActiveTab, onSelectNote, }: KeyboardNavigationOptions) { const visibleNotes = useMemo( () => computeVisibleNotes(entries, selection, allContent), [entries, selection, allContent], ) const tabsRef = useLatestRef(tabs) const activeTabPathRef = useLatestRef(activeTabPath) const visibleNotesRef = useLatestRef(visibleNotes) const onSwitchTabRef = useLatestRef(onSwitchTab) const onReplaceRef = useLatestRef(onReplaceActiveTab) const onSelectNoteRef = useLatestRef(onSelectNote) useEffect(() => { const inTauri = isTauri() const handleKeyDown = (e: KeyboardEvent) => { const kind = classifyShortcut(e, inTauri) if (!kind) return e.preventDefault() if (kind === 'tab') navigateTab(tabsRef, activeTabPathRef, onSwitchTabRef, arrowDirection(e.key)) else navigateNote(visibleNotesRef, activeTabPathRef, onReplaceRef, onSelectNoteRef, arrowDirection(e.key)) } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [tabsRef, activeTabPathRef, visibleNotesRef, onSwitchTabRef, onReplaceRef, onSelectNoteRef]) }