1+ import { useState , useCallback , useRef } from "react" ;
2+ import {
3+ deleteCharBefore ,
4+ deleteCharAfter ,
5+ deleteWordBefore ,
6+ deleteWordAfter ,
7+ insertText ,
8+ moveToLineStart ,
9+ moveToLineEnd ,
10+ moveToPreviousWord ,
11+ moveToNextWord ,
12+ } from "../utils/text-utils" ;
13+ import { useInputHistory } from "./use-input-history" ;
14+
15+ export interface Key {
16+ name ?: string ;
17+ ctrl ?: boolean ;
18+ meta ?: boolean ;
19+ shift ?: boolean ;
20+ paste ?: boolean ;
21+ sequence ?: string ;
22+ upArrow ?: boolean ;
23+ downArrow ?: boolean ;
24+ leftArrow ?: boolean ;
25+ rightArrow ?: boolean ;
26+ return ?: boolean ;
27+ escape ?: boolean ;
28+ tab ?: boolean ;
29+ backspace ?: boolean ;
30+ delete ?: boolean ;
31+ }
32+
33+ export interface EnhancedInputHook {
34+ input : string ;
35+ cursorPosition : number ;
36+ isMultiline : boolean ;
37+ setInput : ( text : string ) => void ;
38+ setCursorPosition : ( position : number ) => void ;
39+ clearInput : ( ) => void ;
40+ insertAtCursor : ( text : string ) => void ;
41+ resetHistory : ( ) => void ;
42+ handleInput : ( inputChar : string , key : Key ) => void ;
43+ }
44+
45+ interface UseEnhancedInputProps {
46+ onSubmit ?: ( text : string ) => void ;
47+ onEscape ?: ( ) => void ;
48+ onSpecialKey ?: ( key : Key ) => boolean ; // Return true to prevent default handling
49+ disabled ?: boolean ;
50+ multiline ?: boolean ;
51+ }
52+
53+ export function useEnhancedInput ( {
54+ onSubmit,
55+ onEscape,
56+ onSpecialKey,
57+ disabled = false ,
58+ multiline = false ,
59+ } : UseEnhancedInputProps = { } ) : EnhancedInputHook {
60+ const [ input , setInputState ] = useState ( "" ) ;
61+ const [ cursorPosition , setCursorPositionState ] = useState ( 0 ) ;
62+ const isMultilineRef = useRef ( multiline ) ;
63+
64+ const {
65+ addToHistory,
66+ navigateHistory,
67+ resetHistory,
68+ setOriginalInput,
69+ isNavigatingHistory,
70+ } = useInputHistory ( ) ;
71+
72+ const setInput = useCallback ( ( text : string ) => {
73+ setInputState ( text ) ;
74+ setCursorPositionState ( Math . min ( text . length , cursorPosition ) ) ;
75+ if ( ! isNavigatingHistory ( ) ) {
76+ setOriginalInput ( text ) ;
77+ }
78+ } , [ cursorPosition , isNavigatingHistory , setOriginalInput ] ) ;
79+
80+ const setCursorPosition = useCallback ( ( position : number ) => {
81+ setCursorPositionState ( Math . max ( 0 , Math . min ( input . length , position ) ) ) ;
82+ } , [ input . length ] ) ;
83+
84+ const clearInput = useCallback ( ( ) => {
85+ setInputState ( "" ) ;
86+ setCursorPositionState ( 0 ) ;
87+ setOriginalInput ( "" ) ;
88+ } , [ setOriginalInput ] ) ;
89+
90+ const insertAtCursor = useCallback ( ( text : string ) => {
91+ const result = insertText ( input , cursorPosition , text ) ;
92+ setInputState ( result . text ) ;
93+ setCursorPositionState ( result . position ) ;
94+ setOriginalInput ( result . text ) ;
95+ } , [ input , cursorPosition , setOriginalInput ] ) ;
96+
97+ const handleSubmit = useCallback ( ( ) => {
98+ if ( input . trim ( ) ) {
99+ addToHistory ( input ) ;
100+ onSubmit ?.( input ) ;
101+ clearInput ( ) ;
102+ }
103+ } , [ input , addToHistory , onSubmit , clearInput ] ) ;
104+
105+ const handleInput = useCallback ( ( inputChar : string , key : Key ) => {
106+ if ( disabled ) return ;
107+
108+ // Handle Ctrl+C - check multiple ways it could be detected
109+ if ( ( key . ctrl && inputChar === "c" ) || inputChar === "\x03" ) {
110+ setInputState ( "" ) ;
111+ setCursorPositionState ( 0 ) ;
112+ setOriginalInput ( "" ) ;
113+ return ;
114+ }
115+
116+ // Allow special key handler to override default behavior
117+ if ( onSpecialKey ?.( key ) ) {
118+ return ;
119+ }
120+
121+ // Handle Escape
122+ if ( key . escape ) {
123+ onEscape ?.( ) ;
124+ return ;
125+ }
126+
127+ // Handle Enter/Return
128+ if ( key . return ) {
129+ if ( multiline && key . shift ) {
130+ // Shift+Enter in multiline mode inserts newline
131+ const result = insertText ( input , cursorPosition , "\n" ) ;
132+ setInputState ( result . text ) ;
133+ setCursorPositionState ( result . position ) ;
134+ setOriginalInput ( result . text ) ;
135+ } else {
136+ handleSubmit ( ) ;
137+ }
138+ return ;
139+ }
140+
141+ // Handle history navigation
142+ if ( ( key . upArrow || key . name === 'up' ) && ! key . ctrl && ! key . meta ) {
143+ const historyInput = navigateHistory ( "up" ) ;
144+ if ( historyInput !== null ) {
145+ setInputState ( historyInput ) ;
146+ setCursorPositionState ( historyInput . length ) ;
147+ }
148+ return ;
149+ }
150+
151+ if ( ( key . downArrow || key . name === 'down' ) && ! key . ctrl && ! key . meta ) {
152+ const historyInput = navigateHistory ( "down" ) ;
153+ if ( historyInput !== null ) {
154+ setInputState ( historyInput ) ;
155+ setCursorPositionState ( historyInput . length ) ;
156+ }
157+ return ;
158+ }
159+
160+ // Handle cursor movement - ignore meta flag for arrows as it's unreliable in terminals
161+ // Only do word movement if ctrl is pressed AND no arrow escape sequence is in inputChar
162+ if ( ( key . leftArrow || key . name === 'left' ) && key . ctrl && ! inputChar . includes ( '[' ) ) {
163+ const newPos = moveToPreviousWord ( input , cursorPosition ) ;
164+ setCursorPositionState ( newPos ) ;
165+ return ;
166+ }
167+
168+ if ( ( key . rightArrow || key . name === 'right' ) && key . ctrl && ! inputChar . includes ( '[' ) ) {
169+ const newPos = moveToNextWord ( input , cursorPosition ) ;
170+ setCursorPositionState ( newPos ) ;
171+ return ;
172+ }
173+
174+ // Handle regular cursor movement - single character (ignore meta flag)
175+ if ( key . leftArrow || key . name === 'left' ) {
176+ const newPos = Math . max ( 0 , cursorPosition - 1 ) ;
177+ setCursorPositionState ( newPos ) ;
178+ return ;
179+ }
180+
181+ if ( key . rightArrow || key . name === 'right' ) {
182+ const newPos = Math . min ( input . length , cursorPosition + 1 ) ;
183+ setCursorPositionState ( newPos ) ;
184+ return ;
185+ }
186+
187+ // Handle Home/End keys or Ctrl+A/E
188+ if ( ( key . ctrl && inputChar === "a" ) || key . name === "home" ) {
189+ setCursorPositionState ( 0 ) ; // Simple start of input
190+ return ;
191+ }
192+
193+ if ( ( key . ctrl && inputChar === "e" ) || key . name === "end" ) {
194+ setCursorPositionState ( input . length ) ; // Simple end of input
195+ return ;
196+ }
197+
198+ // Handle deletion - check multiple ways backspace might be detected
199+ // Backspace can be detected in different ways depending on terminal
200+ // In some terminals, backspace shows up as delete:true with empty inputChar
201+ const isBackspace = key . backspace ||
202+ key . name === 'backspace' ||
203+ inputChar === '\b' ||
204+ inputChar === '\x7f' ||
205+ ( key . delete && inputChar === '' && ! key . shift ) ;
206+
207+ if ( isBackspace ) {
208+ if ( key . ctrl || key . meta ) {
209+ // Ctrl/Cmd + Backspace: Delete word before cursor
210+ const result = deleteWordBefore ( input , cursorPosition ) ;
211+ setInputState ( result . text ) ;
212+ setCursorPositionState ( result . position ) ;
213+ setOriginalInput ( result . text ) ;
214+ } else {
215+ // Regular backspace
216+ const result = deleteCharBefore ( input , cursorPosition ) ;
217+ setInputState ( result . text ) ;
218+ setCursorPositionState ( result . position ) ;
219+ setOriginalInput ( result . text ) ;
220+ }
221+ return ;
222+ }
223+
224+ // Handle forward delete (Del key) - but not if it was already handled as backspace above
225+ if ( ( key . delete && inputChar !== '' ) || ( key . ctrl && inputChar === "d" ) ) {
226+ if ( key . ctrl || key . meta ) {
227+ // Ctrl/Cmd + Delete: Delete word after cursor
228+ const result = deleteWordAfter ( input , cursorPosition ) ;
229+ setInputState ( result . text ) ;
230+ setCursorPositionState ( result . position ) ;
231+ setOriginalInput ( result . text ) ;
232+ } else {
233+ // Regular delete
234+ const result = deleteCharAfter ( input , cursorPosition ) ;
235+ setInputState ( result . text ) ;
236+ setCursorPositionState ( result . position ) ;
237+ setOriginalInput ( result . text ) ;
238+ }
239+ return ;
240+ }
241+
242+ // Handle Ctrl+K: Delete from cursor to end of line
243+ if ( key . ctrl && inputChar === "k" ) {
244+ const lineEnd = moveToLineEnd ( input , cursorPosition ) ;
245+ const newText = input . slice ( 0 , cursorPosition ) + input . slice ( lineEnd ) ;
246+ setInputState ( newText ) ;
247+ setOriginalInput ( newText ) ;
248+ return ;
249+ }
250+
251+ // Handle Ctrl+U: Delete from cursor to start of line
252+ if ( key . ctrl && inputChar === "u" ) {
253+ const lineStart = moveToLineStart ( input , cursorPosition ) ;
254+ const newText = input . slice ( 0 , lineStart ) + input . slice ( cursorPosition ) ;
255+ setInputState ( newText ) ;
256+ setCursorPositionState ( lineStart ) ;
257+ setOriginalInput ( newText ) ;
258+ return ;
259+ }
260+
261+ // Handle Ctrl+W: Delete word before cursor
262+ if ( key . ctrl && inputChar === "w" ) {
263+ const result = deleteWordBefore ( input , cursorPosition ) ;
264+ setInputState ( result . text ) ;
265+ setCursorPositionState ( result . position ) ;
266+ setOriginalInput ( result . text ) ;
267+ return ;
268+ }
269+
270+ // Handle Ctrl+X: Clear entire input
271+ if ( key . ctrl && inputChar === "x" ) {
272+ setInputState ( "" ) ;
273+ setCursorPositionState ( 0 ) ;
274+ setOriginalInput ( "" ) ;
275+ return ;
276+ }
277+
278+ // Handle regular character input
279+ if ( inputChar && ! key . ctrl && ! key . meta ) {
280+ const result = insertText ( input , cursorPosition , inputChar ) ;
281+ setInputState ( result . text ) ;
282+ setCursorPositionState ( result . position ) ;
283+ setOriginalInput ( result . text ) ;
284+ }
285+ } , [ disabled , onSpecialKey , input , cursorPosition , multiline , handleSubmit , navigateHistory , setOriginalInput ] ) ;
286+
287+ return {
288+ input,
289+ cursorPosition,
290+ isMultiline : isMultilineRef . current ,
291+ setInput,
292+ setCursorPosition,
293+ clearInput,
294+ insertAtCursor,
295+ resetHistory,
296+ handleInput,
297+ } ;
298+ }
0 commit comments