/** * Minimal stub of useSearchInput for the standalone @anthropic/ink package. * * Provides the same interface as the full implementation but without * kill-ring / yank support. Suitable for FuzzyPicker and other theme * components that need text input handling. */ import { useCallback, useState } from 'react' import { KeyboardEvent } from '../core/events/keyboard-event.js' import type { Key, InputEvent } from '../core/events/input-event.js' import type { ParsedKey } from '../core/parse-keypress.js' import useInput from './use-input.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' type UseSearchInputOptions = { isActive: boolean onExit: () => void onCancel?: () => void onExitUp?: () => void columns?: number passthroughCtrlKeys?: string[] initialQuery?: string backspaceExitsOnEmpty?: boolean } type UseSearchInputReturn = { query: string setQuery: (q: string) => void cursorOffset: number handleKeyDown: (e: KeyboardEvent) => void } const UNHANDLED_SPECIAL_KEYS = new Set([ 'pageup', 'pagedown', 'insert', 'wheelup', 'wheeldown', 'mouse', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', ]) export function useSearchInput({ isActive, onExit, onCancel, onExitUp, columns, initialQuery = '', backspaceExitsOnEmpty = true, }: UseSearchInputOptions): UseSearchInputReturn { const { columns: terminalColumns } = useTerminalSize() const _effectiveColumns = columns ?? terminalColumns const [query, setQueryState] = useState(initialQuery) const [cursorOffset, setCursorOffset] = useState(initialQuery.length) const setQuery = useCallback((q: string) => { setQueryState(q) setCursorOffset(q.length) }, []) const handleKeyDown = (e: KeyboardEvent): void => { if (!isActive) return if (e.key === 'return' || e.key === 'down') { e.preventDefault() onExit() return } if (e.key === 'up') { e.preventDefault() onExitUp?.() return } if (e.key === 'escape') { e.preventDefault() if (onCancel) { onCancel() } else if (query.length > 0) { setQueryState('') setCursorOffset(0) } else { onExit() } return } if (e.key === 'backspace') { e.preventDefault() if (query.length === 0) { if (backspaceExitsOnEmpty) (onCancel ?? onExit)() return } const newOffset = Math.max(0, cursorOffset - 1) setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset)) setCursorOffset(newOffset) return } if (e.key === 'delete') { e.preventDefault() if (cursorOffset < query.length) { setQueryState( query.slice(0, cursorOffset) + query.slice(cursorOffset + 1), ) } return } if (e.key === 'left') { e.preventDefault() setCursorOffset(Math.max(0, cursorOffset - 1)) return } if (e.key === 'right') { e.preventDefault() setCursorOffset(Math.min(query.length, cursorOffset + 1)) return } if (e.key === 'home') { e.preventDefault() setCursorOffset(0) return } if (e.key === 'end') { e.preventDefault() setCursorOffset(query.length) return } if (e.ctrl) { switch (e.key.toLowerCase()) { case 'a': e.preventDefault() setCursorOffset(0) return case 'e': e.preventDefault() setCursorOffset(query.length) return case 'b': e.preventDefault() setCursorOffset(Math.max(0, cursorOffset - 1)) return case 'f': e.preventDefault() setCursorOffset(Math.min(query.length, cursorOffset + 1)) return case 'd': { e.preventDefault() if (query.length === 0) { ;(onCancel ?? onExit)() return } if (cursorOffset < query.length) { setQueryState( query.slice(0, cursorOffset) + query.slice(cursorOffset + 1), ) } return } case 'h': { e.preventDefault() if (query.length === 0) { if (backspaceExitsOnEmpty) (onCancel ?? onExit)() return } const newOffset = Math.max(0, cursorOffset - 1) setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset)) setCursorOffset(newOffset) return } case 'c': e.preventDefault() onCancel?.() return case 'u': e.preventDefault() setQueryState(query.slice(cursorOffset)) setCursorOffset(0) return case 'k': e.preventDefault() setQueryState(query.slice(0, cursorOffset)) return case 'w': { e.preventDefault() // Delete word before cursor const before = query.slice(0, cursorOffset) const after = query.slice(cursorOffset) const trimmed = before.replace(/\S+\s*$/, '') setQueryState(trimmed + after) setCursorOffset(trimmed.length) return } } return } if (e.key === 'tab') { return } // Regular character input if (e.key.length >= 1 && !UNHANDLED_SPECIAL_KEYS.has(e.key)) { e.preventDefault() setQueryState( query.slice(0, cursorOffset) + e.key + query.slice(cursorOffset), ) setCursorOffset(cursorOffset + 1) } } // Bridge: subscribe via useInput and adapt to KeyboardEvent useInput( (_input: string, _key: Key, event: InputEvent) => { handleKeyDown(new KeyboardEvent(event.keypress as ParsedKey)) }, { isActive }, ) return { query, setQuery, cursorOffset, handleKeyDown } }