96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
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
|
|
}
|