UNPKG

58.2 kBJavaScriptView Raw
1/*
2 Terminal Kit
3
4 Copyright (c) 2009 - 2020 Cédric Ronvel
5
6 The MIT License (MIT)
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all
16 copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25*/
26
27"use strict" ;
28
29
30
31const tree = require( 'tree-kit' ) ;
32const string = require( 'string-kit' ) ;
33const NextGenEvents = require( 'nextgen-events' ) ;
34const Promise = require( 'seventh' ) ;
35
36const termkit = require( './termkit.js' ) ;
37//function noop() {}
38
39
40
41/*
42 Since there is a lot of hack with the Terminal instance creation, we can't use the 'new' operator at all...
43*/
44function Terminal( ... args ) { return Terminal.create( ... args ) ; }
45
46Terminal.prototype = Object.create( NextGenEvents.prototype ) ;
47Terminal.prototype.constructor = Terminal ;
48module.exports = Terminal ;
49
50
51
52Terminal.create = function( createOptions ) {
53 // Default options...
54 if ( ! createOptions || typeof createOptions !== 'object' ) { createOptions = {} ; }
55 if ( ! createOptions.stdin ) { createOptions.stdin = process.stdin ; }
56 if ( ! createOptions.stdout ) { createOptions.stdout = process.stdout ; }
57 if ( ! createOptions.stderr ) { createOptions.stderr = process.stderr ; }
58 if ( typeof createOptions.generic !== 'string' ) { createOptions.generic = 'xterm' ; }
59
60 var k ;
61
62 var termconfig ;
63 var chainable = Object.create( notChainable ) ;
64 var options = {
65 on: '' , off: '' , params: 0 , out: createOptions.stdout
66 } ;
67
68 var term = applyEscape.bind( undefined , options ) ;
69
70 // Yay, this is a nasty hack...
71 Object.setPrototypeOf( term , chainable ) ;
72 term.apply = Function.prototype.apply ;
73 term.call = Function.prototype.call ;
74
75 // Fix the root
76 options.root = term ;
77 term.root = term ;
78
79 term.options = options ;
80 term.stdin = createOptions.stdin ;
81 term.stdout = createOptions.stdout ;
82 term.stderr = createOptions.stderr ;
83 term.generic = createOptions.generic ;
84 term.appId = createOptions.appId ;
85 term.appName = createOptions.appName ;
86 term.isTTY = createOptions.isTTY === undefined ? true : !! createOptions.isTTY ;
87 term.isSSH = !! createOptions.isSSH ;
88 term.pid = createOptions.pid ;
89 term.grabbing = false ;
90 term.mouseGrabbing = false ;
91 term.focusGrabbing = false ;
92 term.timeout = term.isSSH ? 500 : 200 ; // The value is a bit high, to prevent lag due to huge terminal load
93 term.shutdown = false ;
94 term.raw = term.stdout.write.bind( term.stdout ) ; // Used by ScreenBuffer, for optimization
95
96 term.onStdin = onStdin.bind( term ) ; // bindings...
97 term.prependStdinChunk = null ;
98
99 term.lock = {} ;
100
101 term.wrapOptions = {
102 x: 1 ,
103 width: null ,
104 continue: false ,
105 offset: 0
106 } ;
107
108 // Screen size
109 term.width = undefined ;
110 term.height = undefined ;
111 onResize.call( term ) ;
112
113 // Resizing event, by order of preference
114 if ( createOptions.preferProcessSigwinch ) { process.on( 'SIGWINCH' , onResize.bind( term ) ) ; }
115 else if ( term.stdout.isTTY ) { term.stdout.on( 'resize' , onResize.bind( term ) ) ; }
116 else if ( createOptions.processSigwinch ) { process.on( 'SIGWINCH' , onResize.bind( term ) ) ; }
117
118 // States
119 term.state = {
120 fullscreen: false ,
121 button: {
122 left: null ,
123 middle: null ,
124 right: null ,
125 other: null
126 }
127 } ;
128
129 if ( term.appId ) {
130 // We have got the real terminal app
131 try {
132 term.termconfigFile = term.appId + '.js' ;
133 termconfig = require( './termconfig/' + term.termconfigFile ) ;
134 }
135 catch ( error ) {} // Do nothing, let the next if block handle the case
136 }
137
138 if ( ! termconfig ) {
139 // The real terminal app is not known, or we fail to load it...
140 // Fallback to the terminal generic (most of time, got from the $TERM env variable).
141 try {
142 // If a .generic.js file exists, this is a widely used terminal generic, 'xterm' for example.
143 // We should use this generic files because despite advertising them as 'xterm',
144 // most terminal sucks at being truly 'xterm' compatible (only 33% to 50% of xterm capabilities
145 // are supported, even gnome-terminal and Konsole are bad).
146 // So we will try to maintain a fail-safe xterm generic config.
147 term.termconfigFile = term.generic + '.generic.js' ;
148 termconfig = require( './termconfig/' + term.termconfigFile ) ;
149 }
150 catch ( error ) {
151 try {
152 // No generic config exists, try a specific config
153 term.termconfigFile = term.generic + '.js' ;
154 termconfig = require( './termconfig/' + term.termconfigFile ) ;
155 }
156 catch ( error_ ) {
157 // Nothing found, fallback to the most common terminal generic
158 term.termconfigFile = 'xterm.generic.js' ;
159 termconfig = require( './termconfig/' + term.termconfigFile ) ;
160 }
161 }
162 }
163
164 //console.log( term.termconfigFile ) ;
165
166 // if needed, this should be replaced by some tput commands?
167
168 term.esc = tree.extend( { deep: true } , {} , termconfig.esc ) ;
169 term.support = tree.extend( { deep: true } , {} , termconfig.support ) ;
170
171 tree.extend(
172 null , // Do not use deep:true here
173 term.esc ,
174 pseudoEsc ,
175 {
176 // Aliases
177 gray: term.esc.brightBlack ,
178 grey: term.esc.brightBlack ,
179 bgGray: term.esc.bgBrightBlack ,
180 bgGrey: term.esc.bgBrightBlack
181 }
182 ) ;
183
184 term.handler = tree.extend( null , {} , termconfig.handler ) ;
185 term.keymap = tree.extend( { deep: true } , {} , termconfig.keymap ) ;
186 term.colorRegister = tree.extend( { deep: true } , [] , defaultColorRegister , termconfig.colorRegister ) ;
187
188 term.escHandler = { root: term } ;
189 term.escOffHandler = { root: term } ;
190
191 // reverse keymap
192 term.rKeymap = [] ;
193 term.rKeymapMaxSize = -1 ;
194 term.rKeymapStarter = [] ;
195 term.rKeymapStarterMaxSize = -1 ;
196
197 Object.keys( term.keymap ).forEach( ( key ) => {
198
199 var i , j , keymapObject , code , codeList = term.keymap[ key ] ;
200
201 if ( ! Array.isArray( codeList ) ) { codeList = [ codeList ] ; term.keymap[ key ] = codeList ; }
202
203 for ( j = 0 ; j < codeList.length ; j ++ ) {
204 code = codeList[ j ] ;
205
206 if ( typeof code === 'object' ) {
207 keymapObject = code ;
208 keymapObject.name = key ;
209 code = keymapObject.code ;
210 }
211 else {
212 keymapObject = {
213 code: code ,
214 name: key ,
215 matches: [ key ]
216 } ;
217
218 term.keymap[ key ][ j ] = { code: code } ;
219 }
220
221 // keymap handler
222 if ( keymapObject.handler && typeof keymapObject.handler !== 'function' ) {
223 term.keymap[ key ][ j ].handler = term.handler[ keymapObject.handler ] ;
224 }
225
226 if ( code ) {
227 if ( code.length > term.rKeymapMaxSize ) {
228 for ( i = term.rKeymapMaxSize + 1 ; i <= code.length ; i ++ ) { term.rKeymap[ i ] = {} ; }
229 term.rKeymapMaxSize = code.length ;
230 }
231
232 if ( term.rKeymap[ code.length ][ code ] ) {
233 term.rKeymap[ code.length ][ code ].matches.push( key ) ;
234 }
235 else {
236 term.rKeymap[ code.length ][ code ] = keymapObject ;
237 term.rKeymap[ code.length ][ code ].matches = [ key ] ;
238 }
239 }
240 else {
241 if ( ! keymapObject.starter || ! keymapObject.ender || ! keymapObject.handler ) { continue ; }
242
243 if ( keymapObject.starter.length > term.rKeymapStarterMaxSize ) {
244 for ( i = term.rKeymapStarterMaxSize + 1 ; i <= keymapObject.starter.length ; i ++ ) { term.rKeymapStarter[ i ] = {} ; }
245 term.rKeymapStarterMaxSize = keymapObject.starter.length ;
246 }
247
248 if ( term.rKeymapStarter[ keymapObject.starter.length ][ keymapObject.starter ] ) {
249 term.rKeymapStarter[ keymapObject.starter.length ][ keymapObject.starter ].push( key ) ;
250 }
251 else {
252 term.rKeymapStarter[ keymapObject.starter.length ][ keymapObject.starter ] = [ keymapObject ] ;
253 }
254 }
255 }
256 } ) ;
257
258
259 // Create methods for the 'chainable' prototype
260
261 Object.keys( term.esc ).forEach( ( key ) => {
262
263 if ( ! term.esc[ key ] || typeof term.esc[ key ] !== 'object' ) {
264 console.error( "Bad escape sequence entry '" + key + "' using termconfig: '" + term.termconfigFile + "'." ) ;
265 return ;
266 }
267
268 // build-time resolution
269 if ( typeof term.esc[ key ].on === 'function' ) { term.esc[ key ].on = term.esc[ key ].on.call( term ) ; }
270 if ( typeof term.esc[ key ].off === 'function' ) { term.esc[ key ].off = term.esc[ key ].off.call( term ) ; }
271
272 // dynamic handler
273 if ( term.esc[ key ].handler ) {
274 if ( typeof term.esc[ key ].handler === 'function' ) { term.escHandler[ key ] = term.esc[ key ].handler.bind( term ) ; }
275 else { term.escHandler[ key ] = term.handler[ term.esc[ key ].handler ] ; }
276 }
277
278 // dynamic off handler
279 if ( term.esc[ key ].offHandler ) {
280 if ( typeof term.esc[ key ].offHandler === 'function' ) { term.escOffHandler[ key ] = term.esc[ key ].offHandler.bind( term ) ; }
281 else { term.escOffHandler[ key ] = term.handler[ term.esc[ key ].offHandler ] ; }
282 }
283
284 Object.defineProperty( chainable , key , {
285 configurable: true ,
286 get: function() {
287 var fn , chainOptions ;
288
289 chainOptions = Object.assign( {} , this.options ) ;
290
291 chainOptions.on += this.root.esc[ key ].on || '' ;
292 chainOptions.off = ( this.root.esc[ key ].off || '' ) + chainOptions.off ;
293 chainOptions.params += string.format.count( this.root.esc[ key ].on ) ;
294
295 if ( ! chainOptions.onHasFormatting &&
296 ( chainOptions.params ||
297 ( typeof this.root.esc[ key ].on === 'string' &&
298 string.format.hasFormatting( this.root.esc[ key ].on ) ) ) ) {
299 chainOptions.onHasFormatting = true ;
300 }
301
302 if ( ! chainOptions.offHasFormatting &&
303 ( typeof this.root.esc[ key ].off === 'string' &&
304 string.format.hasFormatting( this.root.esc[ key ].off ) ) ) {
305 chainOptions.offHasFormatting = true ;
306 }
307
308 if ( this.root.esc[ key ].err ) { chainOptions.err = true ; chainOptions.out = this.root.stderr ; }
309 if ( this.root.esc[ key ].str ) { chainOptions.str = true ; }
310 if ( this.root.esc[ key ].bind ) { chainOptions.bind = true ; }
311 if ( this.root.esc[ key ].forceStyleOnReset ) { chainOptions.forceStyleOnReset = true ; }
312 if ( this.root.esc[ key ].noFormat ) { chainOptions.noFormat = true ; }
313 if ( this.root.esc[ key ].markupOnly ) { chainOptions.markupOnly = true ; }
314 if ( this.root.esc[ key ].wrap ) { chainOptions.wrap = true ; }
315
316 fn = applyEscape.bind( undefined , chainOptions ) ;
317
318 // Yay, this is a nasty hack...
319 Object.setPrototypeOf( fn , chainable ) ;
320 fn.apply = Function.prototype.apply ;
321 fn.root = this.root || this ;
322 fn.options = chainOptions ;
323
324 // Replace the getter by the newly created function, to speed up further call
325 Object.defineProperty( this , key , { value: fn , configurable: true } ) ;
326
327 //console.log( 'Create function:' , key ) ;
328
329 return fn ;
330 }
331 } ) ;
332 } ) ;
333
334 // Format and markup config
335 term.resetString = '' ;
336 term.setResetString = function( str ) {
337 term.resetString = string.markupMethod.call( term.formatConfig.rawMarkupConfig , str ) ;
338 } ;
339
340 var resetFn = ( extra ) => term.str.styleReset() + term.resetString + extra ;
341
342 term.formatConfig = {
343 fn: {} ,
344 endingMarkupReset: true ,
345 markupReset: resetFn.bind( undefined , '' ) ,
346 //markupReset: term.str.styleReset() ,
347 shiftMarkup: {
348 '#': 'background'
349 } ,
350 markup: {
351 ":": resetFn.bind( undefined , '' ) ,
352 " ": resetFn.bind( undefined , ' ' ) ,
353 //":": term.str.styleReset() ,
354 //" ": term.str.styleReset() + ' ' ,
355
356 "-": term.str.dim() ,
357 "+": term.str.bold() ,
358 "_": term.str.underline() ,
359 "/": term.str.italic() ,
360 "!": term.str.inverse() ,
361
362 "b": term.str.blue() ,
363 "B": term.str.brightBlue() ,
364 "c": term.str.cyan() ,
365 "C": term.str.brightCyan() ,
366 "g": term.str.green() ,
367 "G": term.str.brightGreen() ,
368 "k": term.str.black() ,
369 "K": term.str.brightBlack() ,
370 "m": term.str.magenta() ,
371 "M": term.str.brightMagenta() ,
372 "r": term.str.red() ,
373 "R": term.str.brightRed() ,
374 "w": term.str.white() ,
375 "W": term.str.brightWhite() ,
376 "y": term.str.yellow() ,
377 "Y": term.str.brightYellow()
378 } ,
379 shiftedMarkup: {
380 background: {
381 ":": resetFn.bind( undefined , '' ) ,
382 " ": resetFn.bind( undefined , ' ' ) ,
383 //":": term.str.styleReset() ,
384 //" ": term.str.styleReset() + ' ' ,
385
386 "b": term.str.bgBlue() ,
387 "B": term.str.bgBrightBlue() ,
388 "c": term.str.bgCyan() ,
389 "C": term.str.bgBrightCyan() ,
390 "g": term.str.bgGreen() ,
391 "G": term.str.bgBrightGreen() ,
392 "k": term.str.bgBlack() ,
393 "K": term.str.bgBrightBlack() ,
394 "m": term.str.bgMagenta() ,
395 "M": term.str.bgBrightMagenta() ,
396 "r": term.str.bgRed() ,
397 "R": term.str.bgBrightRed() ,
398 "w": term.str.bgWhite() ,
399 "W": term.str.bgBrightWhite() ,
400 "y": term.str.bgYellow() ,
401 "Y": term.str.bgBrightYellow()
402 }
403 }
404 } ;
405
406 term.formatConfig.rawMarkupConfig = Object.create( term.formatConfig ) ;
407 term.formatConfig.rawMarkupConfig.startingMarkupReset = false ;
408 term.formatConfig.rawMarkupConfig.endingMarkupReset = false ;
409
410 for ( k in term.escHandler ) { term.formatConfig.fn[ k ] = term.escHandler[ k ] ; }
411 for ( k in term.escOffHandler ) { term.formatConfig.fn[ k + '_off' ] = term.escOffHandler[ k ] ; }
412
413 term.format = string.createFormatter( term.formatConfig ) ;
414 term.markup = string.createMarkup( term.formatConfig ) ;
415 term.options = options ;
416
417 // Should come after any escape sequence definitions
418 createOptimized( term ) ;
419
420 // Register various exit handler
421 // Fix the issue #3, turn grabInput off on exit
422 // Disable input grabbing at exit.
423 // Note: the terminal can still send some garbage if it was about to do it when exit kickin.
424
425 process.on( 'exit' , () => {
426 //console.log( '>>> exit' ) ;
427 // Cleanup was done from elsewhere, probably by .asyncCleanup()
428 if ( term.shutdown ) { return ; }
429 term.shutdown = true ;
430 term.styleReset() ;
431 term.grabInput( false ) ;
432 } ) ;
433
434 // Promise.asyncExit() produce this:
435 process.on( 'asyncExit' , ( code , timeout , done ) => {
436 //console.log( '>>> asyncExit' ) ;
437 term.asyncCleanup().then( done ) ;
438 } ) ;
439
440 // The event loop is empty, we have more time to clean up things:
441 // We keep the process running for a little bit of time, to prevent the terminal from displaying garbage.
442 process.once( 'beforeExit' , () => {
443 //console.log( '>>> beforeExit' ) ;
444 term.asyncCleanup() ;
445 } ) ;
446
447 // Should be done at the end, once everything is working: it needs a configured terminal to generate escape sequences
448 term.palette = new termkit.Palette( { system: true , term: term } ) ;
449
450 return term ;
451} ;
452
453
454
455
456
457/* Optimized */
458
459
460
461function createOptimized( term ) {
462 // This is a subset of the terminal capability, mainly used to speed up ScreenBuffer and ScreenBufferHD
463 var i ;
464
465 term.optimized = {} ;
466
467 // reset
468 term.optimized.styleReset = term.str.styleReset() ;
469
470 // Styles
471 term.optimized.bold = term.str.bold() ;
472 term.optimized.dim = term.str.dim() ;
473 term.optimized.italic = term.str.italic() ;
474 term.optimized.underline = term.str.underline() ;
475 term.optimized.blink = term.str.blink() ;
476 term.optimized.inverse = term.str.inverse() ;
477 term.optimized.hidden = term.str.hidden() ;
478 term.optimized.strike = term.str.strike() ;
479
480 term.optimized.noBold = term.str.bold( false ) ;
481 term.optimized.noDim = term.str.dim( false ) ;
482 term.optimized.noItalic = term.str.italic( false ) ;
483 term.optimized.noUnderline = term.str.underline( false ) ;
484 term.optimized.noBlink = term.str.blink( false ) ;
485 term.optimized.noInverse = term.str.inverse( false ) ;
486 term.optimized.noHidden = term.str.hidden( false ) ;
487 term.optimized.noStrike = term.str.strike( false ) ;
488
489 // Colors
490 term.optimized.color256 = [] ;
491 term.optimized.bgColor256 = [] ;
492
493 for ( i = 0 ; i <= 255 ; i ++ ) {
494 term.optimized.color256[ i ] = term.str.color256( i ) ;
495 term.optimized.bgColor256[ i ] = term.str.bgColor256( i ) ;
496 }
497
498 term.optimized.defaultColor = term.str.defaultColor() ;
499 term.optimized.bgDefaultColor = term.str.bgDefaultColor() ;
500
501 // Move To
502 term.optimized.moveTo = term.esc.moveTo.optimized || term.str.moveTo ;
503
504 // Move 1 char to the right
505 term.optimized.right = term.str.right( 1 ) ;
506
507 // 24 bits colors
508 term.optimized.color24bits = term.esc.color24bits.optimized || term.str.color24bits ;
509 term.optimized.bgColor24bits = term.esc.bgColor24bits.optimized || term.str.bgColor24bits ;
510}
511
512
513
514
515
516/* Apply */
517
518
519
520// CAUTION: 'options' MUST NOT BE OVERWRITTEN!
521// It is binded at the function creation and contains function specificities!
522function applyEscape( options , ... args ) {
523 var fn , newOptions , wrapOptions ;
524
525 // Cause trouble because the shutting down process itself needs to send escape sequences asynchronously
526 //if ( options.root.shutdown && ! options.str ) { return options.root ; }
527
528 if ( options.bounded ) { args = options.bounded.concat( args ) ; }
529
530 //console.error( args ) ;
531 if ( options.bind ) {
532 newOptions = Object.assign( {} , options , { bind: false , bounded: args } ) ;
533 fn = applyEscape.bind( this , newOptions ) ;
534
535 // Still a nasty hack...
536 Object.setPrototypeOf( fn , Object.getPrototypeOf( options.root ) ) ;
537 fn.apply = Function.prototype.apply ;
538 fn.root = options.root ;
539 fn.options = newOptions ;
540
541 return fn ;
542 }
543
544 var onFormat = [ options.on ] , output , on , off ;
545 var action = args[ options.params ] ;
546
547 // If not enough arguments, return right now
548 // Well... what about term.up(), term.previousLine(), and so on?
549 //if ( arguments.length < 1 + options.params && ( action === null || action === false ) ) { return options.root ; }
550
551 if ( options.params ) {
552 onFormat = onFormat.concat( args.slice( 0 , options.params ) ) ;
553 }
554
555 //console.log( '\n>>> Action:' , action , '<<<\n' ) ;
556 //console.log( 'Attributes:' , attributes ) ;
557 if ( action === undefined || action === true ) {
558 on = options.onHasFormatting ? options.root.format( ... onFormat ) : options.on ;
559 if ( options.str ) { return on ; }
560 options.out.write( on ) ;
561 return options.root ;
562 }
563
564 if ( action === null || action === false ) {
565 off = options.offHasFormatting ? options.root.format( options.off ) : options.off ;
566 if ( options.str ) { return off ; }
567 options.out.write( off ) ;
568 return options.root ;
569 }
570
571 if ( typeof action !== 'string' ) {
572 if ( typeof action.toString === 'function' ) { action = action.toString() ; }
573 else { action = '' ; }
574 }
575
576 // So we have got a string
577
578 on = options.onHasFormatting ? options.root.format( ... onFormat ) : options.on ;
579
580 if ( options.markupOnly ) {
581 action = options.root.markup( ... args.slice( options.params ) ) ;
582 }
583 else if ( ! options.noFormat ) {
584 action = options.root.format( ... args.slice( options.params ) ) ;
585 }
586
587 if ( options.wrap ) {
588 if ( options.root.wrapOptions.x && options.root.wrapOptions.x > 1 ) {
589 wrapOptions = {
590 width: options.root.wrapOptions.width || options.root.width - options.root.wrapOptions.x + 1 ,
591 glue: '\n' + options.root.str.column( options.root.wrapOptions.x ) ,
592 offset: options.root.wrapOptions.offset ,
593 updateOffset: true ,
594 skipFn: termkit.escapeSequenceSkipFn
595 } ;
596
597 action = string.wordwrap( action , wrapOptions ) ;
598
599 if ( ! options.root.wrapOptions.continue ) {
600 action = options.root.str.column( options.root.wrapOptions.x ) + action ;
601 }
602
603 options.root.wrapOptions.continue = true ;
604 options.root.wrapOptions.offset = wrapOptions.offset ;
605 }
606 else {
607 wrapOptions = {
608 width: options.root.wrapOptions.width || options.root.width ,
609 glue: '\n' ,
610 offset: options.root.wrapOptions.offset ,
611 updateOffset: true ,
612 skipFn: termkit.escapeSequenceSkipFn
613 } ;
614
615 action = string.wordwrap( action , wrapOptions ) ;
616 options.root.wrapOptions.continue = true ;
617 options.root.wrapOptions.offset = wrapOptions.offset ;
618 }
619 }
620 else {
621 // All non-wrapped string display reset the offset
622 options.root.wrapOptions.continue = false ;
623 options.root.wrapOptions.offset = 0 ;
624 }
625
626 off = options.offHasFormatting ? options.root.format( options.off ) : options.off ;
627
628 if ( options.forceStyleOnReset ) {
629 action = action.replace( new RegExp( string.escape.regExp( options.root.optimized.styleReset ) , 'g' ) , options.root.optimized.styleReset + on ) ;
630 }
631
632 if ( options.root.resetString ) {
633 output = options.root.resetString + on + action + off + options.root.resetString ;
634 }
635 else {
636 output = on + action + off ;
637 }
638
639 // tmp hack?
640 if ( options.crlf ) { output = output.replace( /\n/g , '\r\n' ) ; }
641
642 if ( options.str ) { return output ; }
643
644 options.out.write( output ) ;
645
646 return options.root ;
647}
648
649
650
651
652
653/* Pseudo esc */
654
655
656
657var pseudoEsc = {
658 // It just set error:true so it will write to STDERR instead of STDOUT
659 error: { err: true } ,
660
661 // It just set str:true so it will not write anything, but return the value in a string
662 str: { str: true } ,
663
664 // It just set attr:true so it will not write anything, but return an attribute object
665 attr: { attr: true } ,
666
667 // It just set bind to an empty array so it will not do anything except returning a wrapper
668 bindArgs: { bind: true } ,
669
670 // It just set forceStyleOnReset:true so it will find style reset and recall the full chain
671 forceStyleOnReset: { forceStyleOnReset: true } ,
672
673 // It just set noFormat:true so it will not call string.format() on user input,
674 // only useful for ScreenBuffer, so blit-like redraw() can perform slightly faster
675 noFormat: { noFormat: true } ,
676
677 // It just set markupOnly:true so it will not use format string but allow caret ^ markup
678 markupOnly: { markupOnly: true } ,
679
680 // It just set wrap:true so it will wrap words on different lines
681 wrap: { wrap: true } ,
682
683 move: {
684 on: '%[move:%a%a]F' ,
685 handler: function move( x , y ) {
686
687 var sequence = '' ;
688
689 if ( x ) {
690 if ( x > 0 ) { sequence += this.root.format( this.root.esc.right.on , x ) ; }
691 else { sequence += this.root.format( this.root.esc.left.on , -x ) ; }
692 }
693
694 if ( y ) {
695 if ( y > 0 ) { sequence += this.root.format( this.root.esc.down.on , y ) ; }
696 else { sequence += this.root.format( this.root.esc.up.on , -y ) ; }
697 }
698
699 return sequence ;
700 }
701 } ,
702
703 color: {
704 on: '%[color:%a]F' ,
705 off: function() { return this.root.esc.defaultColor.on ; } ,
706 handler: function color( c ) {
707 if ( typeof c === 'string' ) { c = termkit.colorNameToIndex( c ) ; }
708 if ( typeof c !== 'number' ) { return '' ; }
709
710 c = Math.floor( c ) ;
711
712 if ( c < 0 || c > 15 ) { return '' ; }
713
714 if ( c <= 7 ) { return this.root.format( this.root.esc.darkColor.on , c ) ; }
715 return this.root.format( this.root.esc.brightColor.on , c - 8 ) ;
716 }
717 } ,
718
719 bgColor: {
720 on: '%[bgColor:%a]F' ,
721 off: function() { return this.root.esc.bgDefaultColor.on ; } ,
722 handler: function bgColor( c ) {
723 if ( typeof c === 'string' ) { c = termkit.colorNameToIndex( c ) ; }
724 if ( typeof c !== 'number' ) { return '' ; }
725
726 c = Math.floor( c ) ;
727
728 if ( c < 0 || c > 15 ) { return '' ; }
729
730 if ( c <= 7 ) { return this.root.format( this.root.esc.bgDarkColor.on , c ) ; }
731 return this.root.format( this.root.esc.bgBrightColor.on , c - 8 ) ;
732 }
733 } ,
734
735 colorRgb: {
736 on: '%[colorRgb:%a%a%a]F' ,
737 off: function() { return this.root.esc.defaultColor.on ; } ,
738 handler: colorRgbHandler
739
740 } ,
741
742 bgColorRgb: {
743 on: '%[bgColorRgb:%a%a%a]F' ,
744 off: function() { return this.root.esc.bgDefaultColor.on ; } ,
745 handler: bgColorRgbHandler
746 } ,
747
748 colorRgbHex: {
749 on: '%[colorRgbHex:%a]F' ,
750 off: function() { return this.root.esc.defaultColor.on ; } ,
751 handler: colorRgbHandler
752
753 } ,
754
755 bgColorRgbHex: {
756 on: '%[bgColorRgbHex:%a]F' ,
757 off: function() { return this.root.esc.bgDefaultColor.on ; } ,
758 handler: bgColorRgbHandler
759 } ,
760
761 colorGrayscale: {
762 on: '%[colorGrayscale:%a]F' ,
763 off: function() { return this.root.esc.defaultColor.on ; } ,
764 handler: function colorGrayscale( g ) {
765 var c ;
766
767 if ( typeof g !== 'number' ) { return '' ; }
768 if ( g < 0 || g > 255 ) { return '' ; }
769
770 if ( ! this.root.esc.color24bits.na && ! this.root.esc.color24bits.fb ) {
771 // The terminal supports 24bits! Yeah!
772 return this.root.format( this.root.esc.color24bits.on , g , g , g ) ;
773 }
774
775 if ( ! this.root.esc.color256.na && ! this.root.esc.color256.fb ) {
776 // The terminal supports 256 colors
777
778 // Convert to 0..25 range
779 g = Math.round( g * 25 / 255 ) ;
780
781 if ( g < 0 || g > 25 ) { return '' ; }
782
783 if ( g === 0 ) { c = 16 ; }
784 else if ( g === 25 ) { c = 231 ; }
785 else { c = g + 231 ; }
786
787 return this.root.format( this.root.esc.color256.on , c ) ;
788 }
789
790 // The terminal does not support 256 colors, fallback
791 c = this.root.registerForRgb( g , g , g , 0 , 15 ) ;
792 return this.root.format( this.root.esc.color.on , c ) ;
793 }
794 } ,
795
796 bgColorGrayscale: {
797 on: '%[bgColorGrayscale:%a]F' ,
798 off: function() { return this.root.esc.bgDefaultColor.on ; } ,
799 handler: function bgColorGrayscale( g ) {
800 var c ;
801
802 if ( typeof g !== 'number' ) { return '' ; }
803 if ( g < 0 || g > 255 ) { return '' ; }
804
805 if ( ! this.root.esc.bgColor24bits.na && ! this.root.esc.bgColor24bits.fb ) {
806 // The terminal supports 24bits! Yeah!
807 return this.root.format( this.root.esc.bgColor24bits.on , g , g , g ) ;
808 }
809
810 if ( ! this.root.esc.bgColor256.na && ! this.root.esc.bgColor256.fb ) {
811 // Convert to 0..25 range
812 g = Math.round( g * 25 / 255 ) ;
813
814 if ( g < 0 || g > 25 ) { return '' ; }
815
816 if ( g === 0 ) { c = 16 ; }
817 else if ( g === 25 ) { c = 231 ; }
818 else { c = g + 231 ; }
819
820 return this.root.format( this.root.esc.bgColor256.on , c ) ;
821 }
822
823 // The terminal does not support 256 colors, fallback
824 c = this.root.registerForRgb( g , g , g , 0 , 15 ) ;
825 return this.root.format( this.root.esc.bgColor.on , c ) ;
826 }
827 }
828} ;
829
830
831
832
833
834/* Internal/private functions */
835
836
837
838function colorRgbHandler( r , g , b ) {
839 var c , rgb ;
840
841 if ( typeof r === 'string' ) {
842 rgb = termkit.hexToRgba( r ) ;
843 r = rgb.r ; g = rgb.g ; b = rgb.b ;
844 }
845
846 if (
847 typeof r !== 'number' || isNaN( r ) ||
848 typeof g !== 'number' || isNaN( g ) ||
849 typeof b !== 'number' || isNaN( b ) ||
850 r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255
851 ) {
852 return '' ;
853 }
854
855 if ( ! this.root.esc.color24bits.na && ! this.root.esc.color24bits.fb ) {
856 // The terminal supports 24bits! Yeah!
857 return this.root.format( this.root.esc.color24bits.on , r , g , b ) ;
858 }
859
860 if ( ! this.root.esc.color256.na && ! this.root.esc.color256.fb ) {
861 // The terminal supports 256 colors
862
863 // Convert to 0..5 range
864 r = Math.round( r * 5 / 255 ) ;
865 g = Math.round( g * 5 / 255 ) ;
866 b = Math.round( b * 5 / 255 ) ;
867
868 c = 16 + r * 36 + g * 6 + b ;
869
870 // min:16 max:231
871 //if ( c < 16 || c > 231 ) { return '' ; }
872
873 return this.root.format( this.root.esc.color256.on , c ) ;
874 }
875
876 // The terminal does not support 256 colors, fallback
877 c = this.root.registerForRgb( r , g , b , 0 , 15 ) ;
878 return this.root.format( this.root.esc.color.on , c ) ;
879}
880
881
882
883function bgColorRgbHandler( r , g , b ) {
884 var c , rgb ;
885
886 if ( typeof r === 'string' ) {
887 rgb = termkit.hexToRgba( r ) ;
888 r = rgb.r ; g = rgb.g ; b = rgb.b ;
889 }
890
891 if (
892 typeof r !== 'number' || isNaN( r ) ||
893 typeof g !== 'number' || isNaN( g ) ||
894 typeof b !== 'number' || isNaN( b ) ||
895 r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255
896 ) {
897 return '' ;
898 }
899
900 if ( ! this.root.esc.bgColor24bits.na && ! this.root.esc.bgColor24bits.fb ) {
901 // The terminal supports 24bits! Yeah!
902 return this.root.format( this.root.esc.bgColor24bits.on , r , g , b ) ;
903 }
904
905 if ( ! this.root.esc.bgColor256.na && ! this.root.esc.bgColor256.fb ) {
906 // The terminal supports 256 colors
907
908 // Convert to 0..5 range
909 r = Math.round( r * 5 / 255 ) ;
910 g = Math.round( g * 5 / 255 ) ;
911 b = Math.round( b * 5 / 255 ) ;
912
913 c = 16 + r * 36 + g * 6 + b ;
914
915 // min:16 max:231
916 //if ( c < 16 || c > 231 ) { return '' ; }
917
918 return this.root.format( this.root.esc.bgColor256.on , c ) ;
919 }
920
921 // The terminal does not support 256 colors, fallback
922 c = this.root.registerForRgb( r , g , b , 0 , 15 ) ;
923 return this.root.format( this.root.esc.bgColor.on , c ) ;
924}
925
926
927
928// Called by either SIGWINCH signal or stdout's 'resize' event.
929// It is not meant to be used by end-user.
930function onResize() {
931 if ( this.stdout.columns && this.stdout.rows ) {
932 this.width = this.stdout.columns ;
933 this.height = this.stdout.rows ;
934 }
935
936 this.emit( 'resize' , this.width , this.height ) ;
937}
938
939
940
941
942
943/* Advanced methods */
944
945
946
947// Complexes functions that cannot be chained.
948// It is the ancestors of the terminal object, so it should inherit from async.EventEmitter.
949var notChainable = Object.create( Terminal.prototype ) ;
950
951
952
953// Complexes high-level features have their own file
954notChainable.yesOrNo = require( './yesOrNo.js' ) ;
955notChainable.inputField = require( './inputField.js' ) ;
956notChainable.fileInput = require( './fileInput.js' ) ;
957notChainable.singleRowMenu = notChainable.singleLineMenu = require( './singleLineMenu.js' ) ;
958notChainable.singleColumnMenu = require( './singleColumnMenu.js' ) ;
959notChainable.gridMenu = require( './gridMenu.js' ) ;
960notChainable.progressBar = require( './progressBar.js' ) ;
961notChainable.bar = require( './bar.js' ) ;
962notChainable.slowTyping = require( './slowTyping.js' ) ;
963
964
965
966notChainable.createDocument = function( options ) {
967 if ( ! options || typeof options !== 'object' ) { options = {} ; }
968 options.outputDst = this ;
969 options.eventSource = this ;
970 return new termkit.Document( options ) ;
971} ;
972
973
974
975notChainable.createInlineElement = function( Type , options ) {
976 return termkit.Element.createInline( this , Type , options ) ;
977} ;
978
979
980
981notChainable.table = function( table , options = {} ) {
982 return termkit.Element.createInline( this , termkit.TextTable ,
983 Object.assign( {} , options , {
984 cellContents: table ,
985 fit: options.fit !== undefined ? !! options.fit : true
986 } )
987 ) ;
988} ;
989
990
991
992/*
993 .wrapColumn()
994 .wrapColumn( width )
995 .wrapColumn( x , width )
996 .wrapColumn( namedParameters )
997
998 width: the width for word-wrapping, null for terminal width
999 x: x position (for column wrapping)
1000 continue: true if we are continuing
1001 offset: offset of the first line (default: 0)
1002*/
1003notChainable.wrapColumn = function( ... args ) {
1004 this.wrapOptions.continue = false ;
1005 this.wrapOptions.offset = 0 ;
1006
1007 if ( ! args.length ) { return ; }
1008
1009 if ( args[ 0 ] && typeof args[ 0 ] === 'object' ) {
1010 Object.assign( this.wrapOptions , args[ 0 ] ) ;
1011 return this.wrap ;
1012 }
1013
1014 if ( args.length === 1 ) {
1015 this.wrapOptions.x = 1 ;
1016 this.wrapOptions.width = args[ 0 ] ;
1017 return this.wrap ;
1018 }
1019
1020 this.wrapOptions.x = args[ 0 ] ;
1021 this.wrapOptions.width = args[ 1 ] ;
1022 return this.wrap ;
1023} ;
1024
1025
1026
1027// Fail-safe alternate screen buffer
1028notChainable.fullscreen = function( options ) {
1029 if ( options === false ) {
1030 if ( ! this.state.fullscreen ) { return this ; }
1031
1032 // Disable fullscreen mode
1033 this.state.fullscreen = false ;
1034 this.moveTo( 1 , this.height , '\n' ) ;
1035 this.alternateScreenBuffer( false ) ;
1036 return this ;
1037 }
1038
1039 if ( ! options ) { options = {} ; }
1040
1041 this.state.fullscreen = true ;
1042 if ( ! options.noAlternate ) { this.alternateScreenBuffer( true ) ; }
1043 this.clear() ;
1044} ;
1045
1046
1047
1048
1049
1050/* Input management */
1051
1052
1053
1054function onStdin( chunk ) {
1055 var i , j , buffer , startBuffer , char , codepoint ,
1056 keymapCode , keymapStartCode , keymap , keymapList ,
1057 regexp , matches , bytes , found , handlerResult ,
1058 accumulate = false ,
1059 index = 0 , length = chunk.length ;
1060
1061 if ( this.shutdown ) { return ; }
1062
1063 if ( this.prependStdinChunk ) {
1064 chunk = Buffer.concat( [ this.prependStdinChunk , chunk ] ) ;
1065 }
1066
1067 while ( index < length ) {
1068 found = false ;
1069 bytes = 1 ;
1070
1071 if ( chunk[ index ] <= 0x1f || chunk[ index ] === 0x7f ) {
1072 // Those are ASCII control character and DEL key
1073
1074 for ( i = Math.min( length , Math.max( this.rKeymapMaxSize , this.rKeymapStarterMaxSize ) ) ; i > 0 ; i -- ) {
1075 buffer = chunk.slice( index ) ;
1076 keymapCode = buffer.toString() ;
1077 startBuffer = chunk.slice( index , index + i ) ;
1078 keymapStartCode = startBuffer.toString() ;
1079
1080
1081 if ( this.rKeymap[ i ] && this.rKeymap[ i ][ keymapStartCode ] ) {
1082 // First test fixed sequences
1083
1084 keymap = this.rKeymap[ i ][ keymapStartCode ] ;
1085 found = true ;
1086
1087 if ( keymap.handler ) {
1088 handlerResult = keymap.handler.call( this , keymap.name , chunk.slice( index + i ) ) ;
1089 bytes = i + handlerResult.eaten ;
1090
1091 if ( ! handlerResult.disable ) {
1092 this.emit( keymap.event , handlerResult.name , handlerResult.data ) ;
1093 }
1094 }
1095 else if ( keymap.event ) {
1096 bytes = i ;
1097 this.emit( keymap.event , keymap.name , keymap.data , { code: startBuffer } ) ;
1098 }
1099 else {
1100 bytes = i ;
1101 this.emit( 'key' , keymap.name , keymap.matches , { isCharacter: false , code: startBuffer } ) ;
1102 }
1103
1104 break ;
1105 }
1106 else if ( this.rKeymapStarter[ i ] && this.rKeymapStarter[ i ][ keymapStartCode ] ) {
1107 // Then test pattern sequences
1108
1109 keymapList = this.rKeymapStarter[ i ][ keymapStartCode ] ;
1110
1111 //console.log( 'for i:' , keymapList ) ;
1112
1113 for ( j = 0 ; j < keymapList.length ; j ++ ) {
1114 keymap = keymapList[ j ] ;
1115
1116 if ( keymap.altEnder ) {
1117 regexp = '^' +
1118 string.escape.regExp( keymap.starter ) +
1119 '([ -~]*?)' + // [ -~] match only all ASCII non-control character
1120 '(' + string.escape.regExp( keymap.ender ) + '|' + string.escape.regExp( keymap.altEnder ) + ')' ;
1121 }
1122 else {
1123 regexp = '^' +
1124 string.escape.regExp( keymap.starter ) +
1125 '([ -~]*?)' + // [ -~] match only all ASCII non-control character
1126 string.escape.regExp( keymap.ender ) ;
1127 }
1128
1129 matches = keymapCode.match( new RegExp( regexp ) , 'g' ) ;
1130
1131 //console.log( 'for j:' , keymap , regexp , matches ) ;
1132
1133 if ( matches ) {
1134 found = true ;
1135
1136 handlerResult = keymap.handler.call( this , keymap.name , matches[ 1 ] ) ;
1137 bytes = matches[ 0 ].length ;
1138 this.emit( keymap.event , handlerResult.name , handlerResult.data ) ;
1139
1140 break ;
1141 }
1142 else if ( keymap.accumulate ) {
1143 found = true ;
1144 accumulate = true ;
1145 break ;
1146 }
1147 }
1148
1149 if ( found ) { break ; }
1150 }
1151 }
1152
1153 // Nothing was found, so to not emit trash, we just abort the current buffer processing
1154 if ( ! found ) { this.emit( 'unknown' , chunk ) ; break ; }
1155 }
1156 else if ( chunk[ index ] >= 0x80 ) {
1157 // Unicode bytes per char guessing
1158 if ( chunk[ index ] < 0xc0 ) { continue ; } // We are in a middle of an unicode multibyte sequence... Something fails somewhere, we will just continue for now...
1159 else if ( chunk[ index ] < 0xe0 ) { bytes = 2 ; }
1160 else if ( chunk[ index ] < 0xf0 ) { bytes = 3 ; }
1161 else if ( chunk[ index ] < 0xf8 ) { bytes = 4 ; }
1162 else if ( chunk[ index ] < 0xfc ) { bytes = 5 ; }
1163 else { bytes = 6 ; }
1164
1165 buffer = chunk.slice( index , index + bytes ) ;
1166 char = buffer.toString( 'utf8' ) ;
1167
1168 //if ( bytes > 2 ) { codepoint = punycode.ucs2.decode( char )[ 0 ] ; }
1169 if ( bytes > 2 ) { codepoint = string.unicode.firstCodePoint( char ) ; }
1170 else { codepoint = char.charCodeAt( 0 ) ; }
1171
1172 this.emit( 'key' , char , [ char ] , { isCharacter: true , codepoint: codepoint , code: buffer } ) ;
1173 }
1174 else {
1175 // Standard ASCII
1176 char = String.fromCharCode( chunk[ index ] ) ;
1177 this.emit( 'key' , char , [ char ] , { isCharacter: true , codepoint: chunk[ index ] , code: chunk[ index ] } ) ;
1178 }
1179
1180 index += bytes ;
1181 }
1182
1183 if ( accumulate ) {
1184 this.prependStdinChunk = chunk ;
1185 }
1186 else {
1187 this.prependStdinChunk = null ;
1188 }
1189}
1190
1191
1192
1193/*
1194 * options `false` or `Object` where:
1195 * mouse `string` one of:
1196 * 'button'
1197 * 'drag'
1198 * 'motion'
1199 * focus `boolean`
1200 * safe `boolean` (optional), when set and when *options* is set to `false`, it turns *.grabInput()*
1201 it returns a promise that resolve when input grabbing is safely turned off, avoiding extra junks to
1202 be echoed when the terminal left the raw mode. It is mostly useful after grabbing mouse motion.
1203*/
1204notChainable.grabInput = function( options , safe ) {
1205 // RESET
1206 this.mouseButton( false ) ;
1207 this.mouseDrag( false ) ;
1208 this.mouseMotion( false ) ;
1209 //this.mouseSGR( false ) ;
1210 this.focusEvent( false ) ;
1211 this.stdin.removeListener( 'data' , this.onStdin ) ;
1212
1213 // Start disabling everything
1214 this.grabbing = false ;
1215 this.mouseGrabbing = false ;
1216 this.focusGrabbing = false ;
1217
1218 // Disable grabInput mode
1219 var disable = () => {
1220 // Very important: removing all listeners don't switch back to pause mode.
1221 // This is some nasty Node.js quirks (the documentation pleads for backward compatibility).
1222 this.stdin.pause() ;
1223
1224 try {
1225 this.stdin.setRawMode( false ) ;
1226 }
1227 catch ( error ) {
1228 // That's not critical in any way and thus can be ignored: we are probably reading from a non-TTY
1229 }
1230 } ;
1231
1232 if ( options === false ) {
1233 if ( safe ) {
1234 return Promise.resolveSafeTimeout( this.timeout / 2 ).then( disable ) ;
1235 }
1236
1237 disable() ;
1238 return Promise.resolved ;
1239 }
1240
1241 // Should not be moved before, because shutdown typically needs .grabInput( false )
1242 if ( this.shutdown ) { return Promise.resolved ; }
1243
1244 this.grabbing = true ;
1245
1246 if ( ! options ) { options = {} ; }
1247
1248 // SET
1249 try {
1250 this.stdin.setRawMode( true ) ;
1251 }
1252 catch ( error ) {
1253 // Same here, that's not critical in any way and thus can be ignored: we are probably reading from a non-TTY
1254 }
1255
1256 this.stdin.on( 'data' , this.onStdin ) ;
1257
1258 // Very important: after the first this.stdin.pause(), listening for data seems to not switch back to flowing mode.
1259 // Again, a nasty Node.js quirk.
1260 this.stdin.resume() ;
1261
1262 if ( options.mouse ) {
1263 this.mouseGrabbing = true ;
1264
1265 switch ( options.mouse ) {
1266 case 'button' : this.mouseButton.mouseSGR() ; break ;
1267 case 'drag' : this.mouseDrag.mouseSGR() ; break ;
1268 case 'motion' : this.mouseMotion.mouseSGR() ; break ;
1269 }
1270 }
1271
1272 if ( options.focus ) {
1273 this.focusEvent() ;
1274 this.focusGrabbing = true ;
1275 }
1276
1277 return Promise.resolved ;
1278} ;
1279
1280
1281
1282// Like process.exit(), but perform cleanup of the terminal first.
1283// It is asynchronous, so it should be followed by a 'return' if needed.
1284// A better way to handle that is to use Promise.asyncExit(), that is detected by the Terminal instance.
1285notChainable.processExit = function( code ) {
1286 this( '\n' ) ;
1287 this.asyncCleanup().then( () => process.exit( code ) ) ;
1288} ;
1289
1290
1291
1292notChainable.asyncCleanup = async function() {
1293 if ( this.shutdown ) { return ; }
1294 this.shutdown = true ;
1295
1296 this.styleReset() ;
1297
1298 var wasGrabbing = this.grabbing ;
1299
1300 await this.waitStreamDone( this.stdout ) ;
1301
1302 if ( ! this.isTTY || ! wasGrabbing ) { return ; }
1303
1304 await Promise.resolveSafeTimeout( this.timeout / 4 ) ;
1305 return this.grabInput( false , true ) ;
1306} ;
1307
1308
1309
1310notChainable.waitStreamDone = function( stream ) {
1311 if ( ! stream._writableState.needDrain ) { return Promise.resolved ; }
1312 return Promise.onceEvent( stream , 'drain' ) ;
1313} ;
1314
1315
1316
1317notChainable.object2attr = function( object ) {
1318 var attr = this.esc.styleReset.on ;
1319
1320 if ( ! object || typeof object !== 'object' ) { object = {} ; }
1321
1322 // Color part
1323 if ( typeof object.color === 'string' ) { object.color = termkit.colorNameToIndex( object.color ) ; }
1324 if ( typeof object.color !== 'number' || object.color < 0 || object.color > 255 ) { object.color = 7 ; }
1325 else { object.color = Math.floor( object.color ) ; }
1326
1327 attr += this.str.color( object.color ) ;
1328
1329 // Background color part
1330 if ( typeof object.bgColor === 'string' ) { object.bgColor = termkit.colorNameToIndex( object.bgColor ) ; }
1331 if ( typeof object.bgColor !== 'number' || object.bgColor < 0 || object.bgColor > 255 ) { object.bgColor = 0 ; }
1332 else { object.bgColor = Math.floor( object.bgColor ) ; }
1333
1334 attr += this.str.bgColor( object.bgColor ) ;
1335
1336 // Style part
1337 if ( object.bold ) { attr += this.esc.bold.on ; }
1338 if ( object.dim ) { attr += this.esc.dim.on ; }
1339 if ( object.italic ) { attr += this.esc.italic.on ; }
1340 if ( object.underline ) { attr += this.esc.underline.on ; }
1341 if ( object.blink ) { attr += this.esc.blink.on ; }
1342 if ( object.inverse ) { attr += this.esc.inverse.on ; }
1343 if ( object.hidden ) { attr += this.esc.hidden.on ; }
1344 if ( object.strike ) { attr += this.esc.strike.on ; }
1345
1346 return attr ;
1347} ;
1348
1349
1350
1351// Erase a whole rectangular area
1352// .eraseArea( x , y , [width] , [height] )
1353notChainable.eraseArea = function( xMin , yMin , width = 1 , height = 1 ) {
1354 xMin = Math.min( xMin , this.width ) ;
1355 yMin = Math.min( yMin , this.height ) ;
1356
1357 var y ,
1358 xMax = Math.min( xMin + width , this.width + 1 ) ,
1359 yMax = Math.min( yMin + height , this.height + 1 ) ,
1360 str = ' '.repeat( xMax - xMin ) ;
1361
1362 for ( y = yMin ; y < yMax ; y ++ ) {
1363 this.moveTo( xMin , y , str ) ;
1364 }
1365} ;
1366
1367
1368
1369// A facility for those who don't want to deal with requestCursorLocation() and events...
1370notChainable.getCursorLocation = function( callback ) {
1371 var wasGrabbing = this.grabbing , alreadyCleanedUp = false , error ;
1372
1373 if ( this.shutdown ) { return Promise.resolved ; }
1374
1375 // First, check capabilities:
1376 if ( this.esc.requestCursorLocation.na ) {
1377 error = new Error( 'Terminal is not capable' ) ;
1378 if ( callback ) {
1379 callback( error ) ;
1380 return Promise.resolved ;
1381 }
1382
1383 return Promise.reject( error ) ;
1384 }
1385
1386 var promise = new Promise() ;
1387
1388 // Now .getCursorLocation() cannot run in concurrency anymore
1389 if ( this.lock.getCursorLocation ) {
1390 this.once( 'unlock_getCursorLocation' , () => {
1391 this.getCursorLocation().then(
1392 data => {
1393 if ( callback ) { callback( undefined , data.x , data.y ) ; }
1394 else { promise.resolve( data ) ; }
1395 } ,
1396 error_ => {
1397 if ( callback ) { callback( error_ ) ; }
1398 else { promise.reject( error_ ) ; }
1399 }
1400 ) ;
1401 } ) ;
1402
1403 return promise ;
1404 }
1405
1406 this.lock.getCursorLocation = true ;
1407
1408 var cleanup = ( error_ , x , y ) => {
1409 if ( alreadyCleanedUp ) { return ; }
1410 alreadyCleanedUp = true ;
1411
1412 this.removeListener( 'terminal' , onTerminal ) ;
1413 if ( ! wasGrabbing ) { this.grabInput( false ) ; }
1414
1415 if ( error_ ) {
1416 if ( this.shutdown ) { error_.code = 'shutdown' ; }
1417
1418 if ( callback ) { callback( error_ ) ; }
1419 else { promise.reject( error_ ) ; }
1420 return ;
1421 }
1422
1423 if ( callback ) { callback( undefined , x , y ) ; }
1424 else { promise.resolve( { x , y } ) ; }
1425 } ;
1426
1427 var onTerminal = ( name , data ) => {
1428 if ( name !== 'CURSOR_LOCATION' ) { return ; }
1429 this.lock.getCursorLocation = false ;
1430 this.emit( 'unlock_getCursorLocation' ) ;
1431 cleanup( undefined , data.x , data.y ) ;
1432 } ;
1433
1434 if ( ! wasGrabbing ) { this.grabInput() ; }
1435
1436 this.on( 'terminal' , onTerminal ) ;
1437 this.requestCursorLocation() ;
1438
1439 Promise.resolveSafeTimeout( this.timeout ).then( () => {
1440 if ( alreadyCleanedUp ) { return ; }
1441 var error_ = new Error( '.getCursorLocation() timed out' ) ;
1442 error_.code = 'timeout' ;
1443 cleanup( error_ ) ;
1444 } ) ;
1445
1446 return promise ;
1447} ;
1448
1449
1450
1451// Get the RGB value for a color register
1452notChainable.getColor = function( register , callback ) {
1453 var wasGrabbing = this.grabbing , alreadyCleanedUp = false , error ;
1454
1455 if ( this.shutdown ) { return Promise.resolved ; }
1456
1457 // First, check capabilities:
1458 if ( this.esc.requestColor.na ) {
1459 error = new Error( 'Terminal is not capable' ) ;
1460
1461 if ( callback ) {
1462 callback( error ) ;
1463 return Promise.resolved ;
1464 }
1465
1466 return Promise.reject( error ) ;
1467 }
1468
1469 var promise = new Promise() ;
1470
1471 var cleanup = ( error_ , data ) => {
1472 if ( alreadyCleanedUp ) { return ; }
1473 alreadyCleanedUp = true ;
1474
1475 this.removeListener( 'terminal' , onTerminal ) ;
1476 if ( ! wasGrabbing ) { this.grabInput( false ) ; }
1477
1478 if ( error_ ) {
1479 if ( this.shutdown ) { error_.code = 'shutdown' ; }
1480
1481 if ( callback ) { callback( error_ ) ; }
1482 else { promise.reject( error_ ) ; }
1483 return ;
1484 }
1485
1486 if ( callback ) { callback( undefined , data ) ; }
1487 else { promise.resolve( data ) ; }
1488 } ;
1489
1490 var onTerminal = ( name , data ) => {
1491
1492 if ( name !== 'COLOR_REGISTER' ) { return ; }
1493
1494 // We have got a color definition, but this is not for our register, so this is not our response
1495 if ( data.register !== register ) { return ; }
1496
1497 // This is a good opportunity to update the color register
1498 if ( register < 16 ) { this.colorRegister[ register ] = { r: data.r , g: data.g , b: data.b } ; }
1499
1500 // Everything is fine...
1501 cleanup( undefined , data ) ;
1502 } ;
1503
1504 if ( ! wasGrabbing ) { this.grabInput() ; }
1505
1506 this.requestColor( register ) ;
1507 this.on( 'terminal' , onTerminal ) ;
1508
1509 Promise.resolveSafeTimeout( this.timeout ).then( () => {
1510 if ( alreadyCleanedUp ) { return ; }
1511 var error_ = new Error( '.getColor() timed out' ) ;
1512 error_.code = 'timeout' ;
1513 cleanup( error_ ) ;
1514 } ) ;
1515
1516 return promise ;
1517} ;
1518
1519
1520
1521// Get the current 16 colors palette of the terminal, if possible
1522notChainable.getPalette = function( callback ) {
1523 var defaultPalette ,
1524 wasGrabbing = this.grabbing ;
1525
1526 if ( this.shutdown ) { return Promise.resolved ; }
1527
1528 if ( ! wasGrabbing ) { this.grabInput() ; }
1529
1530 // First, check capabilities, if not capable, return the default palette
1531 if ( this.esc.requestColor.na ) {
1532 defaultPalette = this.colorRegister.slice( 0 , 16 ) ;
1533
1534 if ( callback ) {
1535 callback( undefined , defaultPalette ) ;
1536 return Promise.resolved ;
1537 }
1538
1539 return Promise.resolve( defaultPalette ) ;
1540 }
1541
1542 return Promise.concurrent(
1543 4 ,
1544 [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 ] ,
1545 register => this.getColor( register )
1546 )
1547 .then(
1548 palette => {
1549 if ( ! wasGrabbing ) { this.grabInput( false ) ; }
1550 if ( callback ) { callback( undefined , palette ) ; }
1551 else { return palette ; }
1552 } ,
1553 error => {
1554 if ( ! wasGrabbing ) { this.grabInput( false ) ; }
1555
1556 if ( callback ) { callback( error ) ; }
1557 else { throw error ; } // re-throw if not using callback
1558 }
1559 ) ;
1560} ;
1561
1562
1563
1564// Set the color for a register
1565notChainable.setColor = function( register , r , g , b , names ) {
1566 if ( r && typeof r === 'object' ) {
1567 b = r.b ;
1568 g = r.g ;
1569 r = r.r ;
1570 names = g ;
1571 }
1572
1573 // Allow modification of register > 15 ? It looks like terminals don't allow it... (at least not gnome)
1574 if ( typeof register !== 'number' || register < 0 || register > 15 ) { throw new Error( 'Bad register value' ) ; }
1575
1576 if ( ! Array.isArray( names ) ) { names = [] ; }
1577
1578 if (
1579 typeof r !== 'number' || r < 0 || r > 255 ||
1580 typeof g !== 'number' || g < 0 || g > 255 ||
1581 typeof b !== 'number' || b < 0 || b > 255
1582 ) {
1583 throw new Error( 'Bad RGB value' ) ;
1584 }
1585
1586 // Issue an error, or not?
1587 if ( this.setColorLL.na ) { return ; }
1588
1589 // This is a good opportunity to update the color register
1590 this.colorRegister[ register ] = {
1591 r: r , g: g , b: b , names: names
1592 } ;
1593
1594 // Call the Low Level set color
1595 this.setColorLL( register , r , g , b ) ;
1596} ;
1597
1598
1599
1600// Set the current 16 colors palette of the terminal, if possible
1601notChainable.setPalette = function( palette ) {
1602 var i ;
1603
1604 if ( typeof palette === 'string' ) {
1605 try {
1606 palette = require( './colorScheme/' + palette + '.json' ) ;
1607 }
1608 catch( error ) {
1609 throw new Error( '[terminal] .setPalette(): color scheme not found: ' + palette ) ;
1610 }
1611 }
1612
1613 if ( ! Array.isArray( palette ) ) { throw new Error( '[terminal] .setPalette(): argument #0 should be an Array of RGB Object or a built-in color scheme' ) ; }
1614
1615 // Issue an error, or not?
1616 if ( this.setColorLL.na ) { return ; }
1617
1618 for ( i = 0 ; i <= 15 ; i ++ ) {
1619 if ( ! palette[ i ] || typeof palette[ i ] !== 'object' ) { continue ; }
1620 this.setColor( i , palette[ i ] ) ;
1621 }
1622} ;
1623
1624
1625
1626notChainable.getClipboard = function( source = 'c' ) {
1627 var wasGrabbing = this.grabbing , alreadyCleanedUp = false , extClipboard ;
1628
1629 if ( this.shutdown ) { return Promise.resolved ; }
1630
1631 // First, check capabilities:
1632 if ( this.esc.requestClipboard.na ) {
1633 extClipboard = require( './extClipboard.js' ) ;
1634 return extClipboard.getClipboard( source ).catch( () => '' ) ;
1635 }
1636
1637 var promise = new Promise() ;
1638
1639 var cleanup = ( error_ , data ) => {
1640 if ( alreadyCleanedUp ) { return ; }
1641 alreadyCleanedUp = true ;
1642
1643 this.removeListener( 'terminal' , onTerminal ) ;
1644 if ( ! wasGrabbing ) { this.grabInput( false ) ; }
1645
1646 if ( error_ ) {
1647 if ( this.shutdown ) { error_.code = 'shutdown' ; }
1648 promise.reject( error_ ) ;
1649 return ;
1650 }
1651
1652 promise.resolve( data ) ;
1653 } ;
1654
1655 var onTerminal = ( name , data ) => {
1656 //console.log( "EVENT: " , name , data ) ;
1657 if ( name !== 'CLIPBOARD' ) { return ; }
1658
1659 // We have got some content, but this is not for our source, so this is not our response
1660 //if ( data.source !== source ) { return ; }
1661
1662 // Everything is fine...
1663 cleanup( undefined , data.content ) ;
1664 } ;
1665
1666 if ( ! wasGrabbing ) { this.grabInput() ; }
1667
1668 this.requestClipboard( source[ 0 ] ) ;
1669 this.on( 'terminal' , onTerminal ) ;
1670
1671 Promise.resolveSafeTimeout( this.timeout ).then( () => {
1672 if ( alreadyCleanedUp ) { return ; }
1673 var error_ = new Error( '.getClipboard() timed out' ) ;
1674 error_.code = 'timeout' ;
1675 cleanup( error_ ) ;
1676 } ) ;
1677
1678 return promise ;
1679} ;
1680
1681
1682
1683// Set the color for a register
1684notChainable.setClipboard = async function( str , source = 'c' ) {
1685 var extClipboard ;
1686
1687 if ( this.esc.setClipboardLL.na ) {
1688 extClipboard = require( './extClipboard.js' ) ;
1689 return extClipboard.setClipboard( str , source ).catch( () => undefined ) ;
1690 }
1691
1692 var base64 = Buffer.from( str ).toString( 'base64' ) ;
1693
1694 //console.log( "base64:" , base64 , "retro:" , Buffer.from( base64 , 'base64' ).toString() ) ;
1695
1696 // Call the Low Level set clipboard
1697 this.setClipboardLL( source[ 0 ] , base64 ) ;
1698 return Promise.resolved ;
1699} ;
1700
1701
1702
1703
1704
1705/* Utilities */
1706
1707
1708
1709// Default colors, used for guessing
1710var defaultColorRegister = require( './colorScheme/default.json' ) ;
1711
1712( function buildDefaultColorRegister() {
1713 var register , offset , factor , l ;
1714
1715 for ( register = 16 ; register < 232 ; register ++ ) {
1716 // RGB 6x6x6
1717 offset = register - 16 ;
1718 factor = 255 / 5 ;
1719 defaultColorRegister[ register ] = {
1720 r: Math.round( ( Math.floor( offset / 36 ) % 6 ) * factor ) ,
1721 g: Math.round( ( Math.floor( offset / 6 ) % 6 ) * factor ) ,
1722 b: Math.round( ( offset % 6 ) * factor ) ,
1723 names: []
1724 } ;
1725 }
1726
1727 for ( register = 232 ; register <= 255 ; register ++ ) {
1728 // Grayscale 0..23
1729 offset = register - 231 ; // not 232, because the first of them is not a #000000 black
1730 factor = 255 / 25 ; // not 23, because the last is not a #ffffff white
1731 l = Math.round( offset * factor ) ;
1732 defaultColorRegister[ register ] = {
1733 r: l , g: l , b: l , names: []
1734 } ;
1735 }
1736} )() ;
1737
1738
1739
1740// If register hasn't changed, this is used to get the RGB value for them
1741notChainable.rgbForRegister = function( register ) {
1742 if ( register < 0 || register > 255 ) { throw new Error( 'Bad register value' ) ; }
1743
1744 // Simply clone it
1745 return {
1746 r: this.colorRegister[ register ].r ,
1747 g: this.colorRegister[ register ].g ,
1748 b: this.colorRegister[ register ].b
1749 } ;
1750} ;
1751
1752
1753
1754// If register hasn't changed, this is used to get it for an RGB
1755// .registerForRgb( r , g , b , [minRegister] , [maxRegister] )
1756// .registerForRgb( rgbObject , [minRegister] , [maxRegister] )
1757
1758// Lab HCL cylinder coordinate distance
1759notChainable.registerForRgb = function( r , g , b , minRegister , maxRegister ) {
1760 // Manage function arguments
1761 if ( r && typeof r === 'object' ) {
1762 // Manage the .registerForRgb( rgbObject , [minRegister] , [maxRegister] ) variante
1763 maxRegister = b ;
1764 minRegister = g ;
1765 b = r.b ;
1766 g = r.g ;
1767 r = r.r ;
1768 }
1769
1770 if (
1771 typeof r !== 'number' || r < 0 || r > 255 ||
1772 typeof g !== 'number' || g < 0 || g > 255 ||
1773 typeof b !== 'number' || b < 0 || b > 255
1774 ) {
1775 throw new Error( 'Bad RGB value' ) ;
1776 }
1777
1778 if ( typeof maxRegister !== 'number' || maxRegister < 0 || maxRegister > 255 ) { maxRegister = 15 ; }
1779 if ( typeof minRegister !== 'number' || minRegister < 0 || minRegister > 255 ) { minRegister = 0 ; }
1780
1781 if ( minRegister > maxRegister ) {
1782 var tmp ;
1783 tmp = maxRegister ;
1784 maxRegister = minRegister ;
1785 minRegister = tmp ;
1786 }
1787
1788 return this._registerForRgb( r , g , b , minRegister , maxRegister ) ;
1789} ;
1790
1791
1792
1793notChainable._registerForRgb = function( r , g , b , minRegister , maxRegister ) {
1794 // Search for the best match
1795 var register , delta ,
1796 minDelta = Infinity ,
1797 rgb = [ r , g , b ] ;
1798
1799 for ( register = minRegister ; register <= maxRegister ; register ++ ) {
1800 delta = termkit.chroma.distance( rgb , this.colorRegister[ register ] , 'hcl' ) ;
1801 if ( delta < minDelta ) {
1802 minDelta = delta ;
1803 minRegister = register ;
1804 }
1805 }
1806
1807 return minRegister ;
1808} ;
1809
1810
1811
1812notChainable.colorNameForRgb = function( r , g , b ) {
1813 return termkit.indexToColorName( this.registerForRgb( r , g , b , 0 , 15 ) ) ;
1814} ;
1815
1816
1817
1818notChainable.colorNameForHex = function( hex ) {
1819 var rgba = termkit.hexToRgba( hex ) ;
1820 return this.colorNameForRgb( rgba.r , rgba.g , rgba.b ) ;
1821} ;
1822
1823
1824
1825notChainable.registerForRgbCache = function( cache , r , g , b , minRegister , maxRegister ) {
1826 var key = r + '-' + g + '-' + b ;
1827 if ( cache[ key ] ) { return cache[ key ] ; }
1828 return ( cache[ key ] = this._registerForRgb( r , g , b , minRegister , maxRegister ) ) ;
1829} ;
1830
1831
1832
1833
1834
1835/* ScreenBuffer compatible methods */
1836
1837
1838
1839// Cursor is always drawn so there is nothing to do here
1840notChainable.drawCursor = function() {} ;
1841
1842// /!\ Missing: markup, attr return
1843notChainable.put = function( options , str , ... args ) {
1844 var i , x , y , dx , dy , attr , wrap , characters , len , moveToNeeded , inline ;
1845
1846 // Manage options
1847 if ( ! options ) { options = {} ; }
1848
1849 wrap = options.wrap === undefined ? true : options.wrap ;
1850
1851 x = options.x || 0 ;
1852 y = options.y || 0 ;
1853
1854 if ( typeof x !== 'number' || x < 1 ) { x = 1 ; }
1855 else if ( x > this.width ) { x = this.width ; }
1856 else { x = Math.floor( x ) ; }
1857
1858 if ( typeof y !== 'number' || y < 1 ) { y = 1 ; }
1859 else if ( y > this.height ) { y = this.height ; }
1860 else { y = Math.floor( y ) ; }
1861
1862
1863 // Process directions/increments
1864 dx = 1 ;
1865 dy = 0 ;
1866
1867 switch ( options.direction ) {
1868 //case 'right' : // not needed, use the default dx & dy
1869 case 'left' :
1870 dx = -1 ;
1871 break ;
1872 case 'up' :
1873 dx = 0 ;
1874 dy = -1 ;
1875 break ;
1876 case 'down' :
1877 dx = 0 ;
1878 dy = 1 ;
1879 break ;
1880 case null :
1881 case 'none' :
1882 dx = 0 ;
1883 dy = 0 ;
1884 break ;
1885 }
1886
1887 if ( typeof options.dx === 'number' ) { dx = options.dx ; }
1888 if ( typeof options.dy === 'number' ) { dy = options.dy ; }
1889
1890 inline = ( dx === 1 && dy === 0 ) ;
1891
1892
1893 // Process attributes
1894 attr = options.attr || this.esc.styleReset.on ;
1895
1896 if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; }
1897 if ( typeof attr !== 'string' ) { attr = this.esc.styleReset.on ; }
1898
1899
1900 // Process the input string
1901 if ( typeof str !== 'string' ) {
1902 if ( str.toString ) { str = str.toString() ; }
1903 else { return ; }
1904 }
1905
1906 if ( args.length ) { str = string.format( str , ... args ) ; }
1907 str = termkit.stripControlChars( str ) ;
1908
1909 //characters = punycode.ucs2.decode( str ) ;
1910 characters = string.unicode.toArray( str ) ;
1911 len = characters.length ;
1912
1913 moveToNeeded = true ;
1914 this.stdout.write( attr ) ;
1915
1916 for ( i = 0 ; i < len ; i ++ ) {
1917 if ( moveToNeeded ) { this.moveTo( x , y ) ; }
1918 this( characters[ i ] ) ;
1919
1920 x += dx ;
1921 y += dy ;
1922
1923 moveToNeeded = ! inline ;
1924
1925 if ( x < 0 ) {
1926 if ( ! wrap ) { break ; }
1927 x = this.width - 1 ;
1928 y -- ;
1929 moveToNeeded = true ;
1930 }
1931 else if ( x >= this.width ) {
1932 if ( ! wrap ) { break ; }
1933 x = 0 ;
1934 y ++ ;
1935 moveToNeeded = true ;
1936 }
1937
1938 if ( y < 0 ) { break ; }
1939 else if ( y >= this.height ) { break ; }
1940 }
1941} ;
1942
1943
1944
1945notChainable.drawNdarrayImage = function( pixels /* , options */ ) {
1946 var x , xMax = Math.min( pixels.shape[ 0 ] , this.width ) ,
1947 y , yMax = Math.ceil( pixels.shape[ 1 ] / 2 ) ,
1948 hasAlpha = pixels.shape[ 2 ] === 4 ,
1949 maxRegister = this.support['256colors'] ? 255 : 15 ,
1950 fgColor , bgColor , fgAlpha , bgAlpha , cache = {} ;
1951
1952
1953 for ( y = 0 ; y < yMax ; y ++ ) {
1954 for ( x = 0 ; x < xMax ; x ++ ) {
1955 if ( this.support.trueColor ) {
1956 fgAlpha = hasAlpha ? pixels.get( x , y * 2 , 3 ) / 255 : 1 ;
1957
1958 if ( y * 2 + 1 < pixels.shape[ 1 ] ) {
1959 bgAlpha = hasAlpha ? pixels.get( x , y * 2 + 1 , 3 ) / 255 : 1 ;
1960
1961 this.noFormat(
1962 this.optimized.color24bits(
1963 Math.round( fgAlpha * pixels.get( x , y * 2 , 0 ) ) ,
1964 Math.round( fgAlpha * pixels.get( x , y * 2 , 1 ) ) ,
1965 Math.round( fgAlpha * pixels.get( x , y * 2 , 2 ) )
1966 ) +
1967 this.optimized.bgColor24bits(
1968 Math.round( bgAlpha * pixels.get( x , y * 2 + 1 , 0 ) ) ,
1969 Math.round( bgAlpha * pixels.get( x , y * 2 + 1 , 1 ) ) ,
1970 Math.round( bgAlpha * pixels.get( x , y * 2 + 1 , 2 ) )
1971 ) +
1972 '▀'
1973 ) ;
1974 }
1975 else {
1976 this.noFormat(
1977 this.optimized.color24bits(
1978 Math.round( fgAlpha * pixels.get( x , y * 2 , 0 ) ) ,
1979 Math.round( fgAlpha * pixels.get( x , y * 2 , 1 ) ) ,
1980 Math.round( fgAlpha * pixels.get( x , y * 2 , 2 ) )
1981 ) +
1982 this.optimized.bgColor24bits( 0 , 0 , 0 ) +
1983 '▀'
1984 ) ;
1985 }
1986 }
1987 else {
1988 fgColor = hasAlpha && pixels.get( x , y * 2 , 3 ) < 127 ?
1989 0 :
1990 this.registerForRgbCache(
1991 cache ,
1992 pixels.get( x , y * 2 , 0 ) ,
1993 pixels.get( x , y * 2 , 1 ) ,
1994 pixels.get( x , y * 2 , 2 ) ,
1995 0 , maxRegister
1996 ) ;
1997
1998 if ( y * 2 + 1 < pixels.shape[ 1 ] ) {
1999 bgColor = hasAlpha && pixels.get( x , y * 2 + 1 , 3 ) < 127 ?
2000 0 :
2001 this.registerForRgbCache(
2002 cache ,
2003 pixels.get( x , y * 2 + 1 , 0 ) ,
2004 pixels.get( x , y * 2 + 1 , 1 ) ,
2005 pixels.get( x , y * 2 + 1 , 2 ) ,
2006 0 , maxRegister
2007 ) ;
2008
2009 this.noFormat( this.optimized.color256[ fgColor ] + this.optimized.bgColor256[ bgColor ] + '▀' ) ;
2010 }
2011 else {
2012 this.noFormat( this.optimized.color256[ fgColor ] + this.optimized.bgColor256[ 0 ] + '▀' ) ;
2013 }
2014 }
2015 }
2016
2017 this.styleReset()( '\n' ) ;
2018 }
2019} ;
2020
2021
2022
2023notChainable.drawImage = function( filepath , options , callback ) {
2024 return termkit.image.load.call( this , notChainable.drawNdarrayImage.bind( this ) , filepath , options , callback ) ;
2025} ;
2026