All files / src/hooks useAppKeyboard.ts

95.45% Statements 42/44
96.55% Branches 28/29
84.61% Functions 11/13
94.44% Lines 34/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98                                          2x     2x 2x     2x             17x       17x 15x 15x 7x 7x 7x       10x 10x 9x 9x 8x 7x 7x 7x             49x 196x 2x 2x     49x             1x             49x   19x 2x 2x 2x   17x 10x     49x 49x      
import { useEffect } from 'react'
import type { ViewMode } from './useViewMode'
 
interface KeyboardActions {
  onQuickOpen: () => void
  onCommandPalette: () => void
  onSearch: () => void
  onCreateNote: () => void
  onSave: () => void
  onOpenSettings: () => void
  onTrashNote: (path: string) => void
  onArchiveNote: (path: string) => void
  onSetViewMode: (mode: ViewMode) => void
  onGoBack?: () => void
  onGoForward?: () => void
  activeTabPathRef: React.MutableRefObject<string | null>
  handleCloseTabRef: React.MutableRefObject<(path: string) => void>
}
 
type ShortcutHandler = () => void
 
const TEXT_EDITING_KEYS = new Set(['Backspace', 'Delete'])
 
function isTextInputFocused(): boolean {
  const tag = document.activeElement?.tagName
  return tag === 'INPUT' || tag === 'TEXTAREA'
}
 
const VIEW_MODE_KEYS: Record<string, ViewMode> = {
  '1': 'editor-only',
  '2': 'editor-list',
  '3': 'all',
}
 
function isCmdOnly(e: KeyboardEvent): boolean {
  return (e.metaKey || e.ctrlKey) && !e.altKey
}
 
function handleViewModeKey(e: KeyboardEvent, onSetViewMode: (m: ViewMode) => void): boolean {
  if (!isCmdOnly(e)) return false
  const mode = VIEW_MODE_KEYS[e.key]
  if (!mode) return false
  e.preventDefault()
  onSetViewMode(mode)
  return true
}
 
function handleCmdKey(e: KeyboardEvent, keyMap: Record<string, ShortcutHandler>): boolean {
  const mod = e.metaKey || e.ctrlKey
  if (!mod) return false
  const handler = keyMap[e.key]
  if (!handler) return false
  if (TEXT_EDITING_KEYS.has(e.key) && isTextInputFocused()) return false
  e.preventDefault()
  handler()
  return true
}
 
export function useAppKeyboard({
  onQuickOpen, onCommandPalette, onSearch, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote,
  onSetViewMode, onGoBack, onGoForward, activeTabPathRef, handleCloseTabRef,
}: KeyboardActions) {
  useEffect(() => {
    const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
      const path = activeTabPathRef.current
      Eif (path) fn(path)
    }
 
    const cmdKeyMap: Record<string, ShortcutHandler> = {
      k: onCommandPalette,
      p: onQuickOpen,
      n: onCreateNote,
      s: onSave,
      ',': onOpenSettings,
      e: withActiveTab(onArchiveNote),
      w: withActiveTab((path) => handleCloseTabRef.current(path)),
      Backspace: withActiveTab(onTrashNote),
      Delete: withActiveTab(onTrashNote),
      '[': () => onGoBack?.(),
      ']': () => onGoForward?.(),
    }
 
    const handleKeyDown = (e: KeyboardEvent) => {
      // Cmd+Shift+F: full-text search (distinct from Cmd+F browser find)
      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
        e.preventDefault()
        onSearch()
        return
      }
      if (!handleViewModeKey(e, onSetViewMode)) {
        handleCmdKey(e, cmdKeyMap)
      }
    }
    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onGoBack, onGoForward])
}