| 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 | |