source dump of claude code
at main 95 lines 3.2 kB view raw
1import { useCallback, useMemo, useState } from 'react' 2import useApp from '../ink/hooks/use-app.js' 3import type { KeybindingContextName } from '../keybindings/types.js' 4import { useDoublePress } from './useDoublePress.js' 5 6export type ExitState = { 7 pending: boolean 8 keyName: 'Ctrl-C' | 'Ctrl-D' | null 9} 10 11type KeybindingOptions = { 12 context?: KeybindingContextName 13 isActive?: boolean 14} 15 16type UseKeybindingsHook = ( 17 handlers: Record<string, () => void>, 18 options?: KeybindingOptions, 19) => void 20 21/** 22 * Handle ctrl+c and ctrl+d for exiting the application. 23 * 24 * Uses a time-based double-press mechanism: 25 * - First press: Shows "Press X again to exit" message 26 * - Second press within timeout: Exits the application 27 * 28 * Note: We use time-based double-press rather than the chord system because 29 * we want the first ctrl+c to also trigger interrupt (handled elsewhere). 30 * The chord system would prevent the first press from firing any action. 31 * 32 * These keys are hardcoded and cannot be rebound via keybindings.json. 33 * 34 * @param useKeybindingsHook - The useKeybindings hook to use for registering handlers 35 * (dependency injection to avoid import cycles) 36 * @param onInterrupt - Optional callback for features to handle interrupt (ctrl+c). 37 * Return true if handled, false to fall through to double-press exit. 38 * @param onExit - Optional custom exit handler 39 * @param isActive - Whether the keybinding is active (default true). Set false 40 * while an embedded TextInput is focused — TextInput's own 41 * ctrl+c/d handlers will manage cancel/exit, and Dialog's 42 * handler would otherwise double-fire (child useInput runs 43 * before parent useKeybindings, so both see every keypress). 44 */ 45export function useExitOnCtrlCD( 46 useKeybindingsHook: UseKeybindingsHook, 47 onInterrupt?: () => boolean, 48 onExit?: () => void, 49 isActive = true, 50): ExitState { 51 const { exit } = useApp() 52 const [exitState, setExitState] = useState<ExitState>({ 53 pending: false, 54 keyName: null, 55 }) 56 57 const exitFn = useMemo(() => onExit ?? exit, [onExit, exit]) 58 59 // Double-press handler for ctrl+c 60 const handleCtrlCDoublePress = useDoublePress( 61 pending => setExitState({ pending, keyName: 'Ctrl-C' }), 62 exitFn, 63 ) 64 65 // Double-press handler for ctrl+d 66 const handleCtrlDDoublePress = useDoublePress( 67 pending => setExitState({ pending, keyName: 'Ctrl-D' }), 68 exitFn, 69 ) 70 71 // Handler for app:interrupt (ctrl+c by default) 72 // Let features handle interrupt first via callback 73 const handleInterrupt = useCallback(() => { 74 if (onInterrupt?.()) return // Feature handled it 75 handleCtrlCDoublePress() 76 }, [handleCtrlCDoublePress, onInterrupt]) 77 78 // Handler for app:exit (ctrl+d by default) 79 // This also uses double-press to confirm exit 80 const handleExit = useCallback(() => { 81 handleCtrlDDoublePress() 82 }, [handleCtrlDDoublePress]) 83 84 const handlers = useMemo( 85 () => ({ 86 'app:interrupt': handleInterrupt, 87 'app:exit': handleExit, 88 }), 89 [handleInterrupt, handleExit], 90 ) 91 92 useKeybindingsHook(handlers, { context: 'Global', isActive }) 93 94 return exitState 95}