useExitOnCtrlCD.ts
hooks/useExitOnCtrlCD.ts
No strong subsystem tag
96
Lines
3226
Bytes
2
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 96 lines, 4 detected imports, and 2 detected exports.
Important relationships
Detected exports
ExitStateuseExitOnCtrlCD
Keywords
exitctrlpendingdouble-presshandlerexitstateusekeybindingshookhandlersinterruptusedoublepress
Detected imports
react../ink/hooks/use-app.js../keybindings/types.js./useDoublePress.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}