Files
tolaria/src/hooks/appKeyboardShortcuts.ts
2026-04-08 21:05:16 +02:00

145 lines
4.4 KiB
TypeScript

import type { MutableRefObject } from 'react'
import type { ViewMode } from './useViewMode'
import { trackEvent } from '../lib/telemetry'
export interface KeyboardActions {
onQuickOpen: () => void
onCommandPalette: () => void
onSearch: () => void
onCreateNote: () => void
onOpenDailyNote: () => void
onSave: () => void
onOpenSettings: () => void
onDeleteNote: (path: string) => void
onArchiveNote: (path: string) => void
onSetViewMode: (mode: ViewMode) => void
onZoomIn: () => void
onZoomOut: () => void
onZoomReset: () => void
onGoBack?: () => void
onGoForward?: () => void
onToggleAIChat?: () => void
onToggleRawEditor?: () => void
onToggleInspector?: () => void
onToggleFavorite?: (path: string) => void
onOpenInNewWindow?: () => void
activeTabPathRef: MutableRefObject<string | null>
}
type ShortcutHandler = () => void
type ShortcutMap = Record<string, ShortcutHandler>
const TEXT_EDITING_KEYS = new Set(['Backspace', 'Delete'])
const VIEW_MODE_KEYS: Record<string, ViewMode> = {
'1': 'editor-only',
'2': 'editor-list',
'3': 'all',
}
function isTextInputFocused(): boolean {
const tag = document.activeElement?.tagName
return tag === 'INPUT' || tag === 'TEXTAREA'
}
function isCommandOrCtrlOnly(e: KeyboardEvent): boolean {
return (e.metaKey || e.ctrlKey) && e.altKey === false
}
function isCommandOrCtrlShiftOnly(e: KeyboardEvent): boolean {
return isCommandOrCtrlOnly(e) && e.shiftKey
}
function isCommandShiftOnly(e: KeyboardEvent): boolean {
return e.metaKey && e.ctrlKey === false && e.altKey === false && e.shiftKey
}
function withActiveTab(
activeTabPathRef: MutableRefObject<string | null>,
handler: (path: string) => void,
): ShortcutHandler {
return () => {
const path = activeTabPathRef.current
if (path) handler(path)
}
}
export function createCommandKeyMap(actions: KeyboardActions): ShortcutMap {
const { activeTabPathRef } = actions
return {
k: actions.onCommandPalette,
p: actions.onQuickOpen,
n: actions.onCreateNote,
j: actions.onOpenDailyNote,
s: actions.onSave,
',': actions.onOpenSettings,
d: withActiveTab(activeTabPathRef, (path) => actions.onToggleFavorite?.(path)),
e: withActiveTab(activeTabPathRef, actions.onArchiveNote),
Backspace: withActiveTab(activeTabPathRef, actions.onDeleteNote),
Delete: withActiveTab(activeTabPathRef, actions.onDeleteNote),
'[': () => actions.onGoBack?.(),
']': () => actions.onGoForward?.(),
'=': actions.onZoomIn,
'+': actions.onZoomIn,
'-': actions.onZoomOut,
'0': actions.onZoomReset,
'\\': () => actions.onToggleRawEditor?.(),
}
}
export function createShiftCommandKeyMap(actions: KeyboardActions): ShortcutMap {
return {
f: () => {
trackEvent('search_used')
actions.onSearch()
},
i: () => actions.onToggleInspector?.(),
o: () => actions.onOpenInNewWindow?.(),
}
}
export function handleViewModeKey(e: KeyboardEvent, onSetViewMode: (mode: ViewMode) => void): boolean {
if (isCommandOrCtrlOnly(e) === false || e.shiftKey) return false
const mode = VIEW_MODE_KEYS[e.key]
if (mode === undefined) return false
e.preventDefault()
onSetViewMode(mode)
return true
}
export function handleCommandKey(e: KeyboardEvent, keyMap: ShortcutMap): boolean {
if (isCommandOrCtrlOnly(e) === false || e.shiftKey) return false
const handler = keyMap[e.key]
if (handler === undefined) return false
if (TEXT_EDITING_KEYS.has(e.key) && isTextInputFocused()) return false
e.preventDefault()
handler()
return true
}
export function handleAiPanelKey(e: KeyboardEvent, onToggleAIChat?: () => void): boolean {
if (isCommandShiftOnly(e) === false || e.key.toLowerCase() !== 'l' || onToggleAIChat === undefined) return false
e.preventDefault()
onToggleAIChat()
return true
}
export function handleShiftCommandKey(e: KeyboardEvent, keyMap: ShortcutMap): boolean {
if (isCommandOrCtrlShiftOnly(e) === false) return false
const handler = keyMap[e.key.toLowerCase()]
if (handler === undefined) return false
e.preventDefault()
handler()
return true
}
export function handleAppKeyboardEvent(actions: KeyboardActions, event: KeyboardEvent) {
if (handleAiPanelKey(event, actions.onToggleAIChat)) return
const shiftKeyMap = createShiftCommandKeyMap(actions)
if (handleShiftCommandKey(event, shiftKeyMap)) return
if (handleViewModeKey(event, actions.onSetViewMode)) return
const cmdKeyMap = createCommandKeyMap(actions)
handleCommandKey(event, cmdKeyMap)
}