import React from 'react'
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
import { usePasteHandler } from '../hooks/usePasteHandler.js'
import { useDeclaredCursor } from '@anthropic/ink'
import { Ansi, Box, Text, useInput } from '@anthropic/ink'
import type {
BaseInputState,
BaseTextInputProps,
} from '../types/textInputTypes.js'
import type { TextHighlight } from '../utils/textHighlighting.js'
import { HighlightedInput } from './PromptInput/ShimmeredInput.js'
type BaseTextInputComponentProps = BaseTextInputProps & {
inputState: BaseInputState
children?: React.ReactNode
terminalFocus: boolean
highlights?: TextHighlight[]
invert?: (text: string) => string
hidePlaceholderText?: boolean
}
/**
* A base component for text inputs that handles rendering and basic input
*/
export function BaseTextInput({
inputState,
children,
terminalFocus,
invert,
hidePlaceholderText,
...props
}: BaseTextInputComponentProps): React.ReactNode {
const { onInput, renderedValue, cursorLine, cursorColumn } = inputState
// Park the native terminal cursor at the input caret. Terminal emulators
// position IME preedit text at the physical cursor, and screen readers /
// screen magnifiers track it — so parking here makes CJK input appear
// inline and lets accessibility tools follow the input. The Box ref below
// is the yoga layout origin; (cursorLine, cursorColumn) is relative to it.
// Only active when the input is focused, showing its cursor, and the
// terminal itself has focus.
const cursorRef = useDeclaredCursor({
line: cursorLine,
column: cursorColumn,
active: Boolean(props.focus && props.showCursor && terminalFocus),
})
const { wrappedOnInput, isPasting } = usePasteHandler({
onPaste: props.onPaste,
onInput: (input, key) => {
// Prevent Enter key from triggering submission during paste
if (isPasting && key.return) {
return
}
onInput(input, key)
},
onImagePaste: props.onImagePaste,
})
// Notify parent when paste state changes
const { onIsPastingChange } = props
React.useEffect(() => {
if (onIsPastingChange) {
onIsPastingChange(isPasting)
}
}, [isPasting, onIsPastingChange])
const { showPlaceholder, renderedPlaceholder } = renderPlaceholder({
placeholder: props.placeholder,
value: props.value,
showCursor: props.showCursor,
focus: props.focus,
terminalFocus,
invert,
hidePlaceholderText,
})
useInput(wrappedOnInput, { isActive: props.focus })
// Show argument hint only when we have a value and the hint is provided
// Only show the argument hint when:
// 1. We have a hint to show
// 2. We have a command typed (value is not empty)
// 3. The command doesn't have arguments yet (no text after the space)
// 4. We're actually typing a command (the value starts with /)
const commandWithoutArgs =
(props.value && props.value.trim().indexOf(' ') === -1) ||
(props.value && props.value.endsWith(' '))
const showArgumentHint = Boolean(
props.argumentHint &&
props.value &&
commandWithoutArgs &&
props.value.startsWith('/'),
)
// Filter out highlights that contain the cursor position
const cursorFiltered =
props.showCursor && props.highlights
? props.highlights.filter(
h =>
h.dimColor ||
props.cursorOffset < h.start ||
props.cursorOffset >= h.end,
)
: props.highlights
// Adjust highlights for viewport windowing: highlight positions reference the
// full input text, but renderedValue only contains the windowed subset.
const { viewportCharOffset, viewportCharEnd } = inputState
const filteredHighlights =
cursorFiltered && viewportCharOffset > 0
? cursorFiltered
.filter(h => h.end > viewportCharOffset && h.start < viewportCharEnd)
.map(h => ({
...h,
start: Math.max(0, h.start - viewportCharOffset),
end: h.end - viewportCharOffset,
}))
: cursorFiltered
const hasHighlights = filteredHighlights && filteredHighlights.length > 0
if (hasHighlights) {
return (
{showArgumentHint && (
{props.value?.endsWith(' ') ? '' : ' '}
{props.argumentHint}
)}
{children}
)
}
return (
{showPlaceholder && props.placeholderElement ? (
props.placeholderElement
) : showPlaceholder && renderedPlaceholder ? (
{renderedPlaceholder}
) : (
{renderedValue}
)}
{showArgumentHint && (
{props.value?.endsWith(' ') ? '' : ' '}
{props.argumentHint}
)}
{children}
)
}