File size: 3,226 Bytes
064bfd6 | 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 | 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<string, () => 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<ExitState>({
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
}
|