import React, { useState, useEffect, useRef } from 'react'; import { Text, useInput } from 'ink'; import chalk from 'chalk'; type Props = { readonly placeholder?: string; readonly focus?: boolean; readonly mask?: string; readonly showCursor?: boolean; readonly highlightPastedText?: boolean; readonly value: string; readonly onChange: (value: string) => void; readonly onSubmit?: (value: string) => void; }; export default function TextInput({ value: originalValue, placeholder = '', focus = true, mask, highlightPastedText = false, showCursor = true, onChange, onSubmit, }: Props) { const [state, setState] = useState({ cursorOffset: 0, cursorWidth: 0, }); const {cursorOffset, cursorWidth} = state; const valueRef = useRef(originalValue); const cursorOffsetRef = useRef(cursorOffset); const cursorWidthRef = useRef(cursorWidth); const renderCursorPosition = originalValue.length + cursorOffset; useEffect(() => { valueRef.current = originalValue; }, [originalValue]); useEffect(() => { cursorOffsetRef.current = cursorOffset; cursorWidthRef.current = cursorWidth; }, [cursorOffset, cursorWidth]); // Correct cursor position if dependencies change or text is shortened. useEffect(() => { setState(previousState => { if (!focus || !showCursor) { return previousState; } if (previousState.cursorOffset === 0) { return { cursorOffset: 0, cursorWidth: 0, }; } return previousState; }); }, [originalValue, focus, showCursor]); const cursorActualWidth = highlightPastedText ? cursorWidth : 0; const value = mask ? mask.repeat(originalValue.length) : originalValue; let renderedValue = value; let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined; // Fake mouse cursor, because it's too inconvenient to deal with actual cursor and ansi escapes if (showCursor && focus) { renderedPlaceholder = placeholder.length > 0 ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) : chalk.inverse(' '); renderedValue = value.length > 0 ? '' : chalk.inverse(' '); let i = 0; for (const char of value) { renderedValue += i >= renderCursorPosition - cursorActualWidth && i <= renderCursorPosition ? chalk.inverse(char) : char; i++; } if (value.length > 0 && renderCursorPosition === value.length) { renderedValue += chalk.inverse(' '); } } useInput( (input, key) => { const currentValue = valueRef.current; const previousCursorOffset = cursorOffsetRef.current; const previousCursorWidth = cursorWidthRef.current; let cursorPosition = currentValue.length + previousCursorOffset; if ( key.upArrow || key.downArrow || (key.ctrl && input === 'c') || key.tab || (key.shift && key.tab) ) { return; } if (key.return) { if (onSubmit) { onSubmit(valueRef.current); } return; } let nextCursorPosition = cursorPosition; let nextValue = currentValue; let nextCursorWidth = 0; if (key.ctrl && input === 'a') { nextCursorPosition = 0; } else if (key.ctrl && input === 'e') { nextCursorPosition = currentValue.length; } else if (key.ctrl && input === 'b') { if (showCursor && cursorPosition > 0) { nextCursorPosition = cursorPosition - 1; } } else if (key.ctrl && input === 'f') { if (showCursor && cursorPosition < currentValue.length) { nextCursorPosition = cursorPosition + 1; } } else if (key.meta && input === 'b') { if (showCursor && cursorPosition > 0) { let wordStart = cursorPosition; while (wordStart > 0 && /\s/.test(currentValue[wordStart - 1])) { wordStart--; } while (wordStart > 0 && !/\s/.test(currentValue[wordStart - 1])) { wordStart--; } nextCursorPosition = wordStart; } } else if (key.meta && input === 'f') { if (showCursor && cursorPosition < currentValue.length) { let wordEnd = cursorPosition; while (wordEnd < currentValue.length && /\s/.test(currentValue[wordEnd])) { wordEnd++; } while (wordEnd < currentValue.length && !/\s/.test(currentValue[wordEnd])) { wordEnd++; } nextCursorPosition = wordEnd; } } else if (key.ctrl && input === 'w') { if (cursorPosition > 0) { let wordStart = cursorPosition; while (wordStart > 0 && /\s/.test(currentValue[wordStart - 1])) { wordStart--; } while (wordStart > 0 && !/\s/.test(currentValue[wordStart - 1])) { wordStart--; } nextValue = currentValue.slice(0, wordStart) + currentValue.slice(cursorPosition); nextCursorPosition = wordStart; } } else if (key.ctrl && input === 'h') { if (cursorPosition > 0) { nextValue = currentValue.slice(0, cursorPosition - 1) + currentValue.slice(cursorPosition, currentValue.length); nextCursorPosition = cursorPosition - 1; } } else if (key.ctrl && input === 'd') { if (cursorPosition < currentValue.length) { nextValue = currentValue.slice(0, cursorPosition) + currentValue.slice(cursorPosition + 1, currentValue.length); } } else if (key.meta && input === 'd') { if (cursorPosition < currentValue.length) { let wordEnd = cursorPosition; while (wordEnd < currentValue.length && /\s/.test(currentValue[wordEnd])) { wordEnd++; } while (wordEnd < currentValue.length && !/\s/.test(currentValue[wordEnd])) { wordEnd++; } nextValue = currentValue.slice(0, cursorPosition) + currentValue.slice(wordEnd); } } else if (key.ctrl && input === 'k') { nextValue = currentValue.slice(0, cursorPosition); } else if (key.ctrl && input === 'u') { nextValue = currentValue.slice(cursorPosition); nextCursorPosition = 0; } else if (key.leftArrow) { if (showCursor) { nextCursorPosition--; } } else if (key.rightArrow) { if (showCursor) { nextCursorPosition++; } } else if (key.backspace || key.delete) { if (cursorPosition > 0) { nextValue = currentValue.slice(0, cursorPosition - 1) + currentValue.slice(cursorPosition, currentValue.length); nextCursorPosition--; } } else { nextValue = currentValue.slice(0, cursorPosition) + input + currentValue.slice(cursorPosition, currentValue.length); nextCursorPosition += input.length; if (input.length > 1) { nextCursorWidth = input.length; } } if (cursorPosition < 0 || nextCursorPosition < 0) { nextCursorPosition = 0; } if (nextCursorPosition > nextValue.length) { nextCursorPosition = nextValue.length; } const nextCursorOffset = nextCursorPosition - nextValue.length; if (nextCursorOffset !== previousCursorOffset || nextCursorWidth !== previousCursorWidth) { cursorOffsetRef.current = nextCursorOffset; cursorWidthRef.current = nextCursorWidth; setState({ cursorOffset: nextCursorOffset, cursorWidth: nextCursorWidth, }); } if (nextValue !== currentValue) { valueRef.current = nextValue; onChange(nextValue); } }, {isActive: focus}, ); return ( {placeholder ? value.length > 0 ? renderedValue : renderedPlaceholder : renderedValue} ); }