🎯 Use case
This file lives under “hooks/”, which covers reusable UI or integration hooks. On the API surface it exposes ExitState and useExitOnCtrlCD — mainly functions, hooks, or classes. Dependencies touch React UI. It composes internal code from ink, keybindings, and useDoublePress (relative imports).
Generated from folder role, exports, dependency roots, and inline comments — not hand-reviewed for every path.
🧠 Inline summary
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'
📤 Exports (heuristic)
ExitStateuseExitOnCtrlCD
📚 External import roots
Package roots from from "…" (relative paths omitted).
react
🖥️ Source preview
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
}