import { useCallback, useMemo, useState } from 'react' import useApp from '../ink/hooks/use-app.js' import type { KeybindingContextName } from '../keybindings/types.js' import { useDoublePress } from './useDoublePress.js' export type ExitState = { pending: boolean keyName: 'Ctrl-C' | 'Ctrl-D' | null } type KeybindingOptions = { context?: KeybindingContextName isActive?: boolean } type UseKeybindingsHook = ( handlers: Record void>, options?: KeybindingOptions, ) => void /** * Handle ctrl+c and ctrl+d for exiting the application. * * Uses a time-based double-press mechanism: * - First press: Shows "Press X again to exit" message * - Second press within timeout: Exits the application * * Note: We use time-based double-press rather than the chord system because * we want the first ctrl+c to also trigger interrupt (handled elsewhere). * The chord system would prevent the first press from firing any action. * * These keys are hardcoded and cannot be rebound via keybindings.json. * * @param useKeybindingsHook - The useKeybindings hook to use for registering handlers * (dependency injection to avoid import cycles) * @param onInterrupt - Optional callback for features to handle interrupt (ctrl+c). * Return true if handled, false to fall through to double-press exit. * @param onExit - Optional custom exit handler * @param isActive - Whether the keybinding is active (default true). Set false * while an embedded TextInput is focused — TextInput's own * ctrl+c/d handlers will manage cancel/exit, and Dialog's * handler would otherwise double-fire (child useInput runs * before parent useKeybindings, so both see every keypress). */ export function useExitOnCtrlCD( useKeybindingsHook: UseKeybindingsHook, onInterrupt?: () => boolean, onExit?: () => void, isActive = true, ): ExitState { const { exit } = useApp() const [exitState, setExitState] = useState({ pending: false, keyName: null, }) const exitFn = useMemo(() => onExit ?? exit, [onExit, exit]) // Double-press handler for ctrl+c const handleCtrlCDoublePress = useDoublePress( pending => setExitState({ pending, keyName: 'Ctrl-C' }), exitFn, ) // Double-press handler for ctrl+d const handleCtrlDDoublePress = useDoublePress( pending => setExitState({ pending, keyName: 'Ctrl-D' }), exitFn, ) // Handler for app:interrupt (ctrl+c by default) // Let features handle interrupt first via callback const handleInterrupt = useCallback(() => { if (onInterrupt?.()) return // Feature handled it handleCtrlCDoublePress() }, [handleCtrlCDoublePress, onInterrupt]) // Handler for app:exit (ctrl+d by default) // This also uses double-press to confirm exit const handleExit = useCallback(() => { handleCtrlDDoublePress() }, [handleCtrlDDoublePress]) const handlers = useMemo( () => ({ 'app:interrupt': handleInterrupt, 'app:exit': handleExit, }), [handleInterrupt, handleExit], ) useKeybindingsHook(handlers, { context: 'Global', isActive }) return exitState }