import { useEffect, useLayoutEffect } from 'react' import { useEventCallback } from 'usehooks-ts' import type { InputEvent, Key } from '../events/input-event.js' import useStdin from './use-stdin.js' type Handler = (input: string, key: Key, event: InputEvent) => void type Options = { /** * Enable or disable capturing of user input. * Useful when there are multiple useInput hooks used at once to avoid handling the same input several times. * * @default true */ isActive?: boolean } /** * This hook is used for handling user input. * It's a more convenient alternative to using `StdinContext` and listening to `data` events. * The callback you pass to `useInput` is called for each character when user enters any input. * However, if user pastes text and it's more than one character, the callback will be called only once and the whole string will be passed as `input`. * * ``` * import {useInput} from 'ink'; * * const UserInput = () => { * useInput((input, key) => { * if (input === 'q') { * // Exit program * } * * if (key.leftArrow) { * // Left arrow key pressed * } * }); * * return … * }; * ``` */ const useInput = (inputHandler: Handler, options: Options = {}) => { const { setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdin() // useLayoutEffect (not useEffect) so that raw mode is enabled synchronously // during React's commit phase, before render() returns. With useEffect, raw // mode setup is deferred to the next event loop tick via React's scheduler, // leaving the terminal in cooked mode — keystrokes echo and the cursor is // visible until the effect fires. useLayoutEffect(() => { if (options.isActive === false) { return } setRawMode(true) return () => { setRawMode(false) } }, [options.isActive, setRawMode]) // Register the listener once on mount so its slot in the EventEmitter's // listener array is stable. If isActive were in the effect's deps, the // listener would re-append on false→true, moving it behind listeners // that registered while it was inactive — breaking // stopImmediatePropagation() ordering. useEventCallback keeps the // reference stable while reading latest isActive/inputHandler from // closure (it syncs via useLayoutEffect, so it's compiler-safe). const handleData = useEventCallback((event: InputEvent) => { if (options.isActive === false) { return } const { input, key } = event // If app is not supposed to exit on Ctrl+C, then let input listener handle it // Note: discreteUpdates is called at the App level when emitting events, // so all listeners are already within a high-priority update context. if (!(input === 'c' && key.ctrl) || !internal_exitOnCtrlC) { inputHandler(input, key, event) } }) useEffect(() => { internal_eventEmitter?.on('input', handleData) return () => { internal_eventEmitter?.removeListener('input', handleData) } }, [internal_eventEmitter, handleData]) } export default useInput