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