@@ -138,3 +138,172 @@ if (process.env.TERM === 'dumb') {
138138 } ) ) ;
139139 } ) ) ;
140140}
141+
142+ {
143+ class VirtualScreen {
144+ constructor ( ) {
145+ this . rows = [ [ ] ] ;
146+ this . row = 0 ;
147+ this . col = 0 ;
148+ }
149+
150+ ensureRow ( row ) {
151+ while ( this . rows . length <= row ) this . rows . push ( [ ] ) ;
152+ }
153+
154+ setChar ( row , col , ch ) {
155+ this . ensureRow ( row ) ;
156+ const target = this . rows [ row ] ;
157+ while ( target . length <= col ) target . push ( ' ' ) ;
158+ target [ col ] = ch ;
159+ }
160+
161+ clearLineRight ( ) {
162+ this . ensureRow ( this . row ) ;
163+ const target = this . rows [ this . row ] ;
164+ if ( this . col < target . length ) {
165+ target . length = this . col ;
166+ }
167+ }
168+
169+ clearFromCursor ( ) {
170+ this . clearLineRight ( ) ;
171+ if ( this . row + 1 < this . rows . length ) {
172+ this . rows . length = this . row + 1 ;
173+ }
174+ }
175+
176+ moveCursor ( dx , dy ) {
177+ this . row = Math . max ( 0 , this . row + dy ) ;
178+ this . ensureRow ( this . row ) ;
179+ this . col = Math . max ( 0 , this . col + dx ) ;
180+ }
181+
182+ handleEscape ( params , code ) {
183+ switch ( code ) {
184+ case 'A' : // Cursor Up
185+ this . moveCursor ( 0 , - ( Number ( params ) || 1 ) ) ;
186+ break ;
187+ case 'B' : // Cursor Down
188+ this . moveCursor ( 0 , Number ( params ) || 1 ) ;
189+ break ;
190+ case 'C' : // Cursor Forward
191+ this . moveCursor ( Number ( params ) || 1 , 0 ) ;
192+ break ;
193+ case 'D' : // Cursor Backward
194+ this . moveCursor ( - ( Number ( params ) || 1 ) , 0 ) ;
195+ break ;
196+ case 'G' : // Cursor Horizontal Absolute
197+ this . col = Math . max ( 0 , ( Number ( params ) || 1 ) - 1 ) ;
198+ break ;
199+ case 'H' :
200+ case 'f' : { // Cursor Position
201+ const [ row , col ] = params . split ( ';' ) . map ( ( n ) => Number ( n ) || 1 ) ;
202+ this . row = Math . max ( 0 , row - 1 ) ;
203+ this . col = Math . max ( 0 , ( col ?? 1 ) - 1 ) ;
204+ this . ensureRow ( this . row ) ;
205+ break ;
206+ }
207+ case 'J' :
208+ this . clearFromCursor ( ) ;
209+ break ;
210+ case 'K' :
211+ this . clearLineRight ( ) ;
212+ break ;
213+ default :
214+ break ;
215+ }
216+ }
217+
218+ write ( chunk ) {
219+ for ( let i = 0 ; i < chunk . length ; i ++ ) {
220+ const ch = chunk [ i ] ;
221+ if ( ch === '\r' ) {
222+ this . col = 0 ;
223+ continue ;
224+ }
225+ if ( ch === '\n' ) {
226+ this . row ++ ;
227+ this . col = 0 ;
228+ this . ensureRow ( this . row ) ;
229+ continue ;
230+ }
231+ if ( ch === '\u001b' && chunk [ i + 1 ] === '[' ) {
232+ const match = / ^ \u001b \[ ( [ 0 - 9 ; ] * ) ( [ A - Z a - z ] ) / . exec ( chunk . slice ( i ) ) ;
233+ if ( match ) {
234+ this . handleEscape ( match [ 1 ] , match [ 2 ] ) ;
235+ i += match [ 0 ] . length - 1 ;
236+ continue ;
237+ }
238+ }
239+ this . setChar ( this . row , this . col , ch ) ;
240+ this . col ++ ;
241+ }
242+ }
243+
244+ getLines ( ) {
245+ return this . rows . map ( ( row ) => row . join ( '' ) . trimEnd ( ) ) ;
246+ }
247+ }
248+
249+ class FakeTTY extends EventEmitter {
250+ columns = 80 ;
251+ rows = 24 ;
252+ isTTY = true ;
253+
254+ constructor ( screen ) {
255+ super ( ) ;
256+ this . screen = screen ;
257+ }
258+
259+ write ( data ) {
260+ this . screen . write ( data ) ;
261+ return true ;
262+ }
263+
264+ resume ( ) { }
265+
266+ pause ( ) { }
267+
268+ end ( ) { }
269+
270+ setRawMode ( mode ) {
271+ this . isRaw = mode ;
272+ }
273+ }
274+
275+ const screen = new VirtualScreen ( ) ;
276+ const fi = new FakeTTY ( screen ) ;
277+
278+ const rli = new readline . Interface ( {
279+ input : fi ,
280+ output : fi ,
281+ terminal : true ,
282+ completer : ( line ) => [ [ 'foobar' , 'foobaz' ] , line ] ,
283+ } ) ;
284+
285+ const promptLines = [ 'multiline' , 'prompt' , 'eats' , 'output' , '> ' ] ;
286+ rli . setPrompt ( promptLines . join ( '\n' ) ) ;
287+ rli . prompt ( ) ;
288+
289+ [ 'f' , 'o' , 'o' , '\t' , '\t' ] . forEach ( ( ch ) => fi . emit ( 'data' , ch ) ) ;
290+
291+ const display = screen . getLines ( ) ;
292+
293+ assert . strictEqual ( display [ 0 ] , 'multiline' ) ;
294+ assert . strictEqual ( display [ 1 ] , 'prompt' ) ;
295+ assert . strictEqual ( display [ 2 ] , 'eats' ) ;
296+ assert . strictEqual ( display [ 3 ] , 'output' ) ;
297+
298+ const inputLineIndex = 4 ;
299+ assert . ok (
300+ display [ inputLineIndex ] . includes ( '> fooba' ) ,
301+ 'prompt line should keep completed input' ,
302+ ) ;
303+
304+ const completionLineExists =
305+ display . some ( ( l ) => l . includes ( 'foobar' ) && l . includes ( 'foobaz' ) ) ;
306+ assert . ok ( completionLineExists , 'completion list should be visible' ) ;
307+
308+ rli . close ( ) ;
309+ }
0 commit comments