1- import { Box , Text } from "ink" ;
2- import TextInput from "ink-text-input" ;
1+ import { Box , Text , useInput } from "ink" ;
32import type React from "react" ;
3+ import { useCallback , useMemo , useState } from "react" ;
44
55interface InputFieldProps {
66 value : string ;
@@ -15,16 +15,244 @@ export const InputField: React.FC<InputFieldProps> = ({
1515 onSubmit,
1616 showCursor,
1717} ) => {
18+ const [ cursorPosition , setCursorPosition ] = useState ( value . length ) ;
19+
20+ const lines = useMemo ( ( ) => value . split ( "\n" ) , [ value ] ) ;
21+ const { currentLineIndex, currentColumnIndex } = useMemo ( ( ) => {
22+ let pos = 0 ;
23+ for ( let i = 0 ; i < lines . length ; i ++ ) {
24+ if ( pos + ( lines [ i ] ?. length || 0 ) >= cursorPosition ) {
25+ return {
26+ currentLineIndex : i ,
27+ currentColumnIndex : cursorPosition - pos ,
28+ } ;
29+ }
30+ pos += ( lines [ i ] ?. length || 0 ) + 1 ; // +1 for newline
31+ }
32+ return {
33+ currentLineIndex : lines . length - 1 ,
34+ currentColumnIndex : lines [ lines . length - 1 ] ?. length || 0 ,
35+ } ;
36+ } , [ lines , cursorPosition ] ) ;
37+
38+ const updateValue = useCallback (
39+ ( newValue : string , newCursorPos ?: number ) => {
40+ onChange ( newValue ) ;
41+ if ( newCursorPos !== undefined ) {
42+ setCursorPosition ( Math . max ( 0 , Math . min ( newValue . length , newCursorPos ) ) ) ;
43+ }
44+ } ,
45+ [ onChange ] ,
46+ ) ;
47+
48+ useInput (
49+ useCallback (
50+ ( input , key ) => {
51+ if ( ! showCursor ) return ;
52+
53+ if ( key . return ) {
54+ onSubmit ( ) ;
55+ return ;
56+ }
57+
58+ if ( key . backspace ) {
59+ if ( cursorPosition > 0 ) {
60+ const newValue =
61+ value . slice ( 0 , cursorPosition - 1 ) + value . slice ( cursorPosition ) ;
62+ updateValue ( newValue , cursorPosition - 1 ) ;
63+ }
64+ return ;
65+ }
66+
67+ // Delete character at cursor position (Delete key and Ctrl+d)
68+ if ( key . delete || ( key . ctrl && input === "d" ) ) {
69+ if ( cursorPosition < value . length ) {
70+ const newValue =
71+ value . slice ( 0 , cursorPosition ) + value . slice ( cursorPosition + 1 ) ;
72+ updateValue ( newValue , cursorPosition ) ;
73+ }
74+ return ;
75+ }
76+
77+ // Cursor movement
78+ if ( key . leftArrow || ( key . ctrl && input === "b" ) ) {
79+ setCursorPosition ( Math . max ( 0 , cursorPosition - 1 ) ) ;
80+ return ;
81+ }
82+
83+ if ( key . rightArrow || ( key . ctrl && input === "f" ) ) {
84+ setCursorPosition ( Math . min ( value . length , cursorPosition + 1 ) ) ;
85+ return ;
86+ }
87+
88+ if ( key . upArrow || ( key . ctrl && input === "p" ) ) {
89+ if ( currentLineIndex > 0 ) {
90+ const prevLineLength = lines [ currentLineIndex - 1 ] ?. length || 0 ;
91+ const newColumnIndex = Math . min ( currentColumnIndex , prevLineLength ) ;
92+ let newPos = 0 ;
93+ for ( let i = 0 ; i < currentLineIndex - 1 ; i ++ ) {
94+ newPos += ( lines [ i ] ?. length || 0 ) + 1 ;
95+ }
96+ newPos += newColumnIndex ;
97+ setCursorPosition ( newPos ) ;
98+ }
99+ return ;
100+ }
101+
102+ if ( key . downArrow || ( key . ctrl && input === "n" ) ) {
103+ if ( currentLineIndex < lines . length - 1 ) {
104+ const nextLineLength = lines [ currentLineIndex + 1 ] ?. length || 0 ;
105+ const newColumnIndex = Math . min ( currentColumnIndex , nextLineLength ) ;
106+ let newPos = 0 ;
107+ for ( let i = 0 ; i <= currentLineIndex ; i ++ ) {
108+ newPos += ( lines [ i ] ?. length || 0 ) + 1 ;
109+ }
110+ newPos += newColumnIndex ;
111+ setCursorPosition ( newPos ) ;
112+ }
113+ return ;
114+ }
115+
116+ // Home/End
117+ if ( key . ctrl && input === "a" ) {
118+ let lineStartPos = 0 ;
119+ for ( let i = 0 ; i < currentLineIndex ; i ++ ) {
120+ lineStartPos += ( lines [ i ] ?. length || 0 ) + 1 ;
121+ }
122+ setCursorPosition ( lineStartPos ) ;
123+ return ;
124+ }
125+
126+ if ( key . ctrl && input === "e" ) {
127+ let lineEndPos = 0 ;
128+ for ( let i = 0 ; i <= currentLineIndex ; i ++ ) {
129+ lineEndPos += lines [ i ] ?. length || 0 ;
130+ if ( i < currentLineIndex ) lineEndPos += 1 ;
131+ }
132+ setCursorPosition ( lineEndPos ) ;
133+ return ;
134+ }
135+
136+ // Kill line from cursor to beginning
137+ if ( key . ctrl && input === "u" ) {
138+ let lineStartPos = 0 ;
139+ for ( let i = 0 ; i < currentLineIndex ; i ++ ) {
140+ lineStartPos += ( lines [ i ] ?. length || 0 ) + 1 ;
141+ }
142+ const newValue =
143+ value . slice ( 0 , lineStartPos ) + value . slice ( cursorPosition ) ;
144+ updateValue ( newValue , lineStartPos ) ;
145+ return ;
146+ }
147+
148+ // Kill word backward
149+ if ( key . ctrl && input === "w" ) {
150+ const beforeCursor = value . slice ( 0 , cursorPosition ) ;
151+ // Find the start of the word to delete
152+ let wordStart = cursorPosition ;
153+
154+ // Skip trailing whitespace
155+ while (
156+ wordStart > 0 &&
157+ / \s / . test ( beforeCursor [ wordStart - 1 ] || "" )
158+ ) {
159+ wordStart -- ;
160+ }
161+
162+ // Find the beginning of the word
163+ while (
164+ wordStart > 0 &&
165+ ! / \s / . test ( beforeCursor [ wordStart - 1 ] || "" )
166+ ) {
167+ wordStart -- ;
168+ }
169+
170+ const newValue =
171+ value . slice ( 0 , wordStart ) + value . slice ( cursorPosition ) ;
172+ updateValue ( newValue , wordStart ) ;
173+ return ;
174+ }
175+
176+ // Kill line from cursor to end
177+ if ( key . ctrl && input === "k" ) {
178+ let lineEndPos = 0 ;
179+ for ( let i = 0 ; i <= currentLineIndex ; i ++ ) {
180+ lineEndPos += lines [ i ] ?. length || 0 ;
181+ if ( i < currentLineIndex ) lineEndPos += 1 ;
182+ }
183+ const newValue =
184+ value . slice ( 0 , cursorPosition ) + value . slice ( lineEndPos ) ;
185+ updateValue ( newValue , cursorPosition ) ;
186+ return ;
187+ }
188+
189+ // Clear input (like screen clear in terminal)
190+ if ( key . ctrl && input === "l" ) {
191+ updateValue ( "" , 0 ) ;
192+ return ;
193+ }
194+
195+ // Regular character input
196+ if ( input && ! key . ctrl && ! key . meta ) {
197+ const newValue =
198+ value . slice ( 0 , cursorPosition ) +
199+ input +
200+ value . slice ( cursorPosition ) ;
201+ updateValue ( newValue , cursorPosition + input . length ) ;
202+ }
203+ } ,
204+ [
205+ value ,
206+ cursorPosition ,
207+ currentLineIndex ,
208+ currentColumnIndex ,
209+ lines ,
210+ showCursor ,
211+ onSubmit ,
212+ updateValue ,
213+ ] ,
214+ ) ,
215+ ) ;
216+
217+ const renderText = useMemo ( ( ) => {
218+ if ( ! showCursor ) {
219+ return value || < Text color = "gray" > Talk to the cat...</ Text > ;
220+ }
221+
222+ if ( value . length === 0 ) {
223+ return (
224+ < Text >
225+ < Text inverse > </ Text >
226+ </ Text >
227+ ) ;
228+ }
229+
230+ const beforeCursor = value . slice ( 0 , cursorPosition ) ;
231+ const cursorChar = value . slice ( cursorPosition , cursorPosition + 1 ) ;
232+ const afterCursor = value . slice ( cursorPosition + 1 ) ;
233+
234+ // Handle cursor display for newline characters and end of text
235+ let displayCursorChar = cursorChar ;
236+ if ( cursorChar === "\n" ) {
237+ displayCursorChar = " " ;
238+ } else if ( cursorChar === "" ) {
239+ displayCursorChar = " " ;
240+ }
241+
242+ return (
243+ < Text >
244+ { beforeCursor }
245+ < Text inverse > { displayCursorChar } </ Text >
246+ { cursorChar === "\n" ? "\n" : "" }
247+ { afterCursor }
248+ </ Text >
249+ ) ;
250+ } , [ value , cursorPosition , showCursor ] ) ;
251+
18252 return (
19253 < Box borderStyle = "single" borderColor = "gray" paddingX = { 1 } >
20254 < Text color = "yellow" > > </ Text >
21- < TextInput
22- value = { value }
23- onChange = { onChange }
24- onSubmit = { onSubmit }
25- showCursor = { showCursor }
26- placeholder = "Talk to the cat..."
27- />
255+ { renderText }
28256 </ Box >
29257 ) ;
30258} ;
0 commit comments