UNPKG

ink

Version:
126 lines (122 loc) 4.96 kB
import { useEffect, useEffectEvent } from 'react'; import parseKeypress, { nonAlphanumericKeys } from '../parse-keypress.js'; import reconciler from '../reconciler.js'; import { useStdinContext } from './use-stdin.js'; /** A React hook that returns `void` and handles user input. It's a more convenient alternative to using `StdinContext` and listening for `data` events. The callback you pass to `useInput` is called for each character when the user enters any input. However, if the 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, options = {}) => { // eslint-disable-next-line @typescript-eslint/naming-convention const { setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdinContext(); useEffect(() => { if (options.isActive === false) { return; } setRawMode(true); return () => { setRawMode(false); }; }, [options.isActive, setRawMode]); const handleData = useEffectEvent((data) => { const keypress = parseKeypress(data); const key = { upArrow: keypress.name === 'up', downArrow: keypress.name === 'down', leftArrow: keypress.name === 'left', rightArrow: keypress.name === 'right', pageDown: keypress.name === 'pagedown', pageUp: keypress.name === 'pageup', home: keypress.name === 'home', end: keypress.name === 'end', return: keypress.name === 'return', escape: keypress.name === 'escape', ctrl: keypress.ctrl, shift: keypress.shift, tab: keypress.name === 'tab', backspace: keypress.name === 'backspace', delete: keypress.name === 'delete', meta: keypress.meta, // Kitty keyboard protocol modifiers super: keypress.super ?? false, hyper: keypress.hyper ?? false, capsLock: keypress.capsLock ?? false, numLock: keypress.numLock ?? false, eventType: keypress.eventType, }; let input; if (keypress.isKittyProtocol) { // Use text-as-codepoints field for printable keys (needed when // reportAllKeysAsEscapeCodes flag is enabled), suppress non-printable if (keypress.isPrintable) { input = keypress.text ?? keypress.name; } else if (keypress.ctrl && keypress.name.length === 1) { // Ctrl+letter via codepoint 1-26 form: not printable text, but // the letter name must flow through so handlers (e.g. exitOnCtrlC // checking `input === 'c' && key.ctrl`) still work. input = keypress.name; } else { input = ''; } } else if (keypress.ctrl) { // Keypress.name is guaranteed non-undefined by parseKeypress, // but guard defensively since a TypeError here would crash the // entire Ink app (see https://github.com/vadimdemedes/ink/issues/901). input = keypress.name ?? ''; } else { input = keypress.sequence; } if (!keypress.isKittyProtocol && nonAlphanumericKeys.includes(keypress.name)) { input = ''; } // Strip escape prefix from broken/incomplete sequences that // parseKeypress did not fully resolve (e.g. a flushed "\u001B["). if (input.startsWith('\u001B')) { input = input.slice(1); } if (input.length === 1 && /[A-Z]/.test(input)) { key.shift = true; } // If app is supposed to exit on Ctrl+C, skip input listeners. if (input === 'c' && key.ctrl && internal_exitOnCtrlC) { return; } // Use discreteUpdates to assign DiscreteEventPriority to state // updates from keyboard input, ensuring they are processed at the // highest priority in concurrent mode. // @ts-expect-error Types require 5 arguments (fn, a, b, c, d) but only fn is needed at runtime. reconciler.discreteUpdates(() => { inputHandler(input, key); }); }); useEffect(() => { if (options.isActive === false) { return; } internal_eventEmitter.on('input', handleData); return () => { internal_eventEmitter.removeListener('input', handleData); }; }, [options.isActive, internal_eventEmitter]); }; export default useInput; //# sourceMappingURL=use-input.js.map