@@ -46,6 +46,23 @@ const color = {
4646
4747class ExitOrbit extends Error { }
4848
49+ const NUMERIC_SHORTCUT_DELAY_MS = 700 ;
50+
51+ export function numericShortcutDecision ( buffer , optionCount ) {
52+ if ( ! / ^ [ 1 - 9 ] \d * $ / . test ( buffer ) || ! Number . isInteger ( optionCount ) || optionCount < 1 ) {
53+ return { state : 'invalid' , index : null } ;
54+ }
55+
56+ const matches = Array . from ( { length : optionCount } , ( _ , index ) => String ( index + 1 ) )
57+ . filter ( ( value ) => value . startsWith ( buffer ) ) ;
58+ if ( ! matches . length ) return { state : 'invalid' , index : null } ;
59+
60+ const exactIndex = matches . includes ( buffer ) ? Number ( buffer ) - 1 : null ;
61+ const hasLongerMatch = matches . some ( ( value ) => value . length > buffer . length ) ;
62+ if ( exactIndex !== null && ! hasLongerMatch ) return { state : 'complete' , index : exactIndex } ;
63+ return { state : exactIndex === null ? 'partial' : 'ambiguous' , index : exactIndex } ;
64+ }
65+
4966export class TerminalUI {
5067 constructor ( ) {
5168 this . interactive = Boolean ( process . stdin . isTTY && process . stdout . isTTY ) ;
@@ -97,34 +114,72 @@ export class TerminalUI {
97114
98115 readline . emitKeypressEvents ( process . stdin ) ;
99116 let selected = 0 ;
117+ let numericBuffer = '' ;
100118 const render = ( ) => {
101119 this . clear ( ) ;
102120 this . header ( title ) ;
103121 options . forEach ( ( option , index ) => {
104122 const active = index === selected ;
105123 process . stdout . write ( `${ active ? `${ color . violet } ›${ color . reset } ` : ' ' } ${ index + 1 } ${ active ? color . bold : '' } ${ option . label } ${ color . reset } \n` ) ;
106124 } ) ;
107- process . stdout . write ( `\n${ color . dim } ↑↓ gezin · Enter seç · numara hızlı seçim · Ctrl+C çıkış${ color . reset } \n` ) ;
125+ const numericStatus = numericBuffer ? ` · seçim: ${ numericBuffer } ` : '' ;
126+ process . stdout . write ( `\n${ color . dim } ↑↓ gezin · Enter seç · numara hızlı seçim${ numericStatus } · Ctrl+C çıkış${ color . reset } \n` ) ;
108127 } ;
109128
110129 return new Promise ( ( resolve , reject ) => {
111130 const previousRaw = process . stdin . isRaw ;
131+ let numericTimer = null ;
132+ let finished = false ;
112133 process . stdin . setRawMode ( true ) ;
113134 process . stdin . resume ( ) ;
135+ const clearNumericTimer = ( ) => {
136+ if ( numericTimer !== null ) clearTimeout ( numericTimer ) ;
137+ numericTimer = null ;
138+ } ;
114139 const finish = ( callback ) => {
140+ if ( finished ) return ;
141+ finished = true ;
142+ clearNumericTimer ( ) ;
115143 process . stdin . off ( 'keypress' , onKeypress ) ;
116144 process . stdin . setRawMode ( previousRaw ?? false ) ;
117145 process . stdin . pause ( ) ;
118146 process . stdout . write ( '\x1b[?25h' ) ;
119147 callback ( ) ;
120148 } ;
149+ const clearNumericInput = ( ) => {
150+ clearNumericTimer ( ) ;
151+ numericBuffer = '' ;
152+ } ;
153+ const resolveNumericInput = ( decision ) => finish ( ( ) => resolve ( options [ decision . index ] . value ) ) ;
154+ const armNumericFallback = ( decision ) => {
155+ clearNumericTimer ( ) ;
156+ if ( decision . index === null ) return ;
157+ numericTimer = setTimeout ( ( ) => resolveNumericInput ( decision ) , NUMERIC_SHORTCUT_DELAY_MS ) ;
158+ } ;
121159 const onKeypress = ( text , key = { } ) => {
122160 if ( key . ctrl && key . name === 'c' ) return finish ( ( ) => reject ( new ExitOrbit ( ) ) ) ;
123- if ( key . name === 'up' ) selected = ( selected - 1 + options . length ) % options . length ;
124- else if ( key . name === 'down' ) selected = ( selected + 1 ) % options . length ;
125- else if ( key . name === 'return' ) return finish ( ( ) => resolve ( options [ selected ] . value ) ) ;
126- else if ( / ^ [ 1 - 9 ] $ / . test ( text ) && options [ Number ( text ) - 1 ] ) {
127- return finish ( ( ) => resolve ( options [ Number ( text ) - 1 ] . value ) ) ;
161+ if ( key . name === 'up' ) {
162+ clearNumericInput ( ) ;
163+ selected = ( selected - 1 + options . length ) % options . length ;
164+ } else if ( key . name === 'down' ) {
165+ clearNumericInput ( ) ;
166+ selected = ( selected + 1 ) % options . length ;
167+ } else if ( key . name === 'return' ) {
168+ const decision = numericShortcutDecision ( numericBuffer , options . length ) ;
169+ if ( decision . index !== null ) return resolveNumericInput ( decision ) ;
170+ return finish ( ( ) => resolve ( options [ selected ] . value ) ) ;
171+ } else if ( / ^ \d $ / . test ( text ) ) {
172+ clearNumericTimer ( ) ;
173+ const decision = numericShortcutDecision ( `${ numericBuffer } ${ text } ` , options . length ) ;
174+ if ( decision . state === 'invalid' ) {
175+ numericBuffer = '' ;
176+ process . stdout . write ( '\x07' ) ;
177+ } else {
178+ numericBuffer += text ;
179+ if ( decision . index !== null ) selected = decision . index ;
180+ if ( decision . state === 'complete' ) return resolveNumericInput ( decision ) ;
181+ armNumericFallback ( decision ) ;
182+ }
128183 } else return ;
129184 render ( ) ;
130185 } ;
0 commit comments