@ -31,18 +31,32 @@ var kBufSize = 10 * 1024;
var util = require ( 'util' ) ;
var inherits = require ( 'util' ) . inherits ;
var EventEmitter = require ( 'events' ) . EventEmitter ;
var tty = require ( 'tty' ) ;
exports . createInterface = function ( input , output , completer ) {
return new Interface ( input , output , completer ) ;
exports . createInterface = function ( input , output , completer , terminal ) {
var rl ;
if ( arguments . length === 1 ) {
rl = new Interface ( input ) ;
} else {
rl = new Interface ( input , output , completer , terminal ) ;
}
return rl ;
} ;
function Interface ( input , output , completer ) {
function Interface ( input , output , completer , terminal ) {
if ( ! ( this instanceof Interface ) ) {
return new Interface ( input , output , completer ) ;
return new Interface ( input , output , completer , terminal ) ;
}
if ( arguments . length === 1 ) {
// an options object was given
output = input . output ;
completer = input . completer ;
terminal = input . terminal ;
input = input . input ;
}
EventEmitter . call ( this ) ;
completer = completer || function ( ) { return [ ] ; } ;
@ -51,6 +65,12 @@ function Interface(input, output, completer) {
throw new TypeError ( 'Argument \'completer\' must be a function' ) ;
}
// backwards compat; check the isTTY prop of the output stream
// when `terminal` was not specified
if ( typeof terminal == 'undefined' ) {
terminal = ! ! output . isTTY ;
}
var self = this ;
this . output = output ;
@ -64,19 +84,17 @@ function Interface(input, output, completer) {
this . setPrompt ( '> ' ) ;
this . enabled = output . isTTY ;
if ( parseInt ( process . env [ 'NODE_NO_READLINE' ] , 10 ) ) {
this . enabled = false ;
}
this . terminal = ! ! terminal ;
if ( ! this . enab led ) {
if ( ! this . terminal ) {
input . on ( 'data' , function ( data ) {
self . _ normalWrite ( data ) ;
} ) ;
} else {
exports . emitKeypressEvents ( input ) ;
// input usually refers to stdin
input . on ( 'keypress' , function ( s , key ) {
self . _ ttyWrite ( s , key ) ;
@ -85,9 +103,10 @@ function Interface(input, output, completer) {
// Current line
this . line = '' ;
// Check process.env.TERM ?
tty . setRawMode ( true ) ;
this . enabled = true ;
if ( typeof input . setRawMode === 'function' ) {
input . setRawMode ( true ) ;
}
this . terminal = true ;
// Cursor position on the line.
this . cursor = 0 ;
@ -95,26 +114,16 @@ function Interface(input, output, completer) {
this . history = [ ] ;
this . historyIndex = - 1 ;
var winSize = output . getWindowSize ( ) ;
exports . columns = winSize [ 0 ] ;
if ( process . listeners ( 'SIGWINCH' ) . length === 0 ) {
process . on ( 'SIGWINCH' , function ( ) {
var winSize = output . getWindowSize ( ) ;
exports . columns = winSize [ 0 ] ;
// FIXME: when #2922 will be approved, change this to
// output.on('resize', ...
self . _ refreshLine ( ) ;
} ) ;
}
output . on ( 'resize' , function ( ) {
self . _ refreshLine ( ) ;
} ) ;
}
}
inherits ( Interface , EventEmitter ) ;
Interface . prototype . __ defineGetter__ ( 'columns' , function ( ) {
return exports . columns ;
return this . output . columns || Infinity ;
} ) ;
Interface . prototype . setPrompt = function ( prompt , length ) {
@ -131,7 +140,7 @@ Interface.prototype.setPrompt = function(prompt, length) {
Interface . prototype . prompt = function ( preserveCursor ) {
if ( this . paused ) this . resume ( ) ;
if ( this . enab led ) {
if ( this . t ermi nal) {
if ( ! preserveCursor ) this . cursor = 0 ;
this . _ refreshLine ( ) ;
} else {
@ -194,13 +203,13 @@ Interface.prototype._refreshLine = function() {
// first move to the bottom of the current line, based on cursor pos
var prevRows = this . prevRows || 0 ;
if ( prevRows > 0 ) {
this . output . moveCursor ( 0 , - prevRows ) ;
exports . moveCursor ( this . output , 0 , - prevRows ) ;
}
// Cursor to left edge.
this . output . cursorTo ( 0 ) ;
exports . cursorTo ( this . output , 0 ) ;
// erase data
this . output . clearScreenDown ( ) ;
exports . clearScreenDown ( this . output ) ;
// Write the prompt and the current buffer content.
this . output . write ( line ) ;
@ -211,11 +220,11 @@ Interface.prototype._refreshLine = function() {
}
// Move cursor to original position.
this . output . cursorTo ( cursorPos . cols ) ;
exports . cursorTo ( this . output , cursorPos . cols ) ;
var diff = lineRows - cursorPos . rows ;
if ( diff > 0 ) {
this . output . moveCursor ( 0 , - diff ) ;
exports . moveCursor ( this . output , 0 , - diff ) ;
}
this . prevRows = cursorPos . rows ;
@ -224,8 +233,10 @@ Interface.prototype._refreshLine = function() {
Interface . prototype . pause = function ( ) {
if ( this . paused ) return ;
if ( this . enabled ) {
tty . setRawMode ( false ) ;
if ( this . terminal ) {
if ( typeof this . input . setRawMode === 'function' ) {
this . input . setRawMode ( true ) ;
}
}
this . input . pause ( ) ;
this . paused = true ;
@ -235,8 +246,10 @@ Interface.prototype.pause = function() {
Interface . prototype . resume = function ( ) {
this . input . resume ( ) ;
if ( this . enabled ) {
tty . setRawMode ( true ) ;
if ( this . terminal ) {
if ( typeof this . input . setRawMode === 'function' ) {
this . input . setRawMode ( true ) ;
}
}
this . paused = false ;
this . emit ( 'resume' ) ;
@ -245,7 +258,7 @@ Interface.prototype.resume = function() {
Interface . prototype . write = function ( d , key ) {
if ( this . paused ) this . resume ( ) ;
this . enab led ? this . _ ttyWrite ( d , key ) : this . _ normalWrite ( d , key ) ;
this . t ermi nal ? this . _ ttyWrite ( d , key ) : this . _ normalWrite ( d , key ) ;
} ;
@ -514,7 +527,7 @@ Interface.prototype._moveCursor = function(dx) {
// check if cursors are in the same line
if ( oldPos . rows === newPos . rows ) {
this . output . moveCursor ( this . cursor - oldcursor , 0 ) ;
exports . moveCursor ( this . output , this . cursor - oldcursor , 0 ) ;
this . prevRows = newPos . rows ;
} else {
this . _ refreshLine ( ) ;
@ -728,3 +741,344 @@ Interface.prototype._ttyWrite = function(s, key) {
exports . Interface = Interface ;
/ * *
* accepts a readable Stream instance and makes it emit "keypress" events
* /
function emitKeypressEvents ( stream ) {
if ( stream . _ emitKeypress ) return ;
stream . _ emitKeypress = true ;
var keypressListeners = stream . listeners ( 'keypress' ) ;
function onData ( b ) {
if ( keypressListeners . length ) {
emitKey ( stream , b ) ;
} else {
// Nobody's watching anyway
stream . removeListener ( 'data' , onData ) ;
stream . on ( 'newListener' , onNewListener ) ;
}
}
function onNewListener ( event ) {
if ( event == 'keypress' ) {
stream . on ( 'data' , onData ) ;
stream . removeListener ( 'newListener' , onNewListener ) ;
}
}
if ( keypressListeners . length ) {
stream . on ( 'data' , onData ) ;
} else {
stream . on ( 'newListener' , onNewListener ) ;
}
}
exports . emitKeypressEvents = emitKeypressEvents ;
/ *
Some patterns seen in terminal key escape codes , derived from combos seen
at http : //www.midnight-commander.org/browser/lib/tty/key.c
ESC letter
ESC [ letter
ESC [ modifier letter
ESC [ 1 ; modifier letter
ESC [ num char
ESC [ num ; modifier char
ESC O letter
ESC O modifier letter
ESC O 1 ; modifier letter
ESC N letter
ESC [ [ num ; modifier char
ESC [ [ 1 ; modifier letter
ESC ESC [ num char
ESC ESC O letter
- char is usually ~ but $ and ^ also happen with rxvt
- modifier is 1 +
( shift * 1 ) +
( left_alt * 2 ) +
( ctrl * 4 ) +
( right_alt * 8 )
- two leading ESCs apparently mean the same as one leading ESC
* /
// Regexes used for ansi escape code splitting
var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/ ;
var functionKeyCodeRe =
/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/ ;
function emitKey ( stream , s ) {
var char ,
key = {
name : undefined ,
ctrl : false ,
meta : false ,
shift : false
} ,
parts ;
if ( Buffer . isBuffer ( s ) ) {
if ( s [ 0 ] > 127 && s [ 1 ] === undefined ) {
s [ 0 ] -= 128 ;
s = '\x1b' + s . toString ( stream . encoding || 'utf-8' ) ;
} else {
s = s . toString ( stream . encoding || 'utf-8' ) ;
}
}
key . sequence = s ;
if ( s === '\r' || s === '\n' ) {
// enter
key . name = 'enter' ;
} else if ( s === '\t' ) {
// tab
key . name = 'tab' ;
} else if ( s === '\b' || s === '\x7f' ||
s === '\x1b\x7f' || s === '\x1b\b' ) {
// backspace or ctrl+h
key . name = 'backspace' ;
key . meta = ( s . charAt ( 0 ) === '\x1b' ) ;
} else if ( s === '\x1b' || s === '\x1b\x1b' ) {
// escape key
key . name = 'escape' ;
key . meta = ( s . length === 2 ) ;
} else if ( s === ' ' || s === '\x1b ' ) {
key . name = 'space' ;
key . meta = ( s . length === 2 ) ;
} else if ( s <= '\x1a' ) {
// ctrl+letter
key . name = String . fromCharCode ( s . charCodeAt ( 0 ) + 'a' . charCodeAt ( 0 ) - 1 ) ;
key . ctrl = true ;
} else if ( s . length === 1 && s >= 'a' && s <= 'z' ) {
// lowercase letter
key . name = s ;
} else if ( s . length === 1 && s >= 'A' && s <= 'Z' ) {
// shift+letter
key . name = s . toLowerCase ( ) ;
key . shift = true ;
} else if ( parts = metaKeyCodeRe . exec ( s ) ) {
// meta+character key
key . name = parts [ 1 ] . toLowerCase ( ) ;
key . meta = true ;
key . shift = /^[A-Z]$/ . test ( parts [ 1 ] ) ;
} else if ( parts = functionKeyCodeRe . exec ( s ) ) {
// ansi escape sequence
// reassemble the key code leaving out leading \x1b's,
// the modifier key bitflag and any meaningless "1;" sequence
var code = ( parts [ 1 ] || '' ) + ( parts [ 2 ] || '' ) +
( parts [ 4 ] || '' ) + ( parts [ 6 ] || '' ) ,
modifier = ( parts [ 3 ] || parts [ 5 ] || 1 ) - 1 ;
// Parse the key modifier
key . ctrl = ! ! ( modifier & 4 ) ;
key . meta = ! ! ( modifier & 10 ) ;
key . shift = ! ! ( modifier & 1 ) ;
key . code = code ;
// Parse the key itself
switch ( code ) {
/* xterm/gnome ESC O letter */
case 'OP' : key . name = 'f1' ; break ;
case 'OQ' : key . name = 'f2' ; break ;
case 'OR' : key . name = 'f3' ; break ;
case 'OS' : key . name = 'f4' ; break ;
/* xterm/rxvt ESC [ number ~ */
case '[11~' : key . name = 'f1' ; break ;
case '[12~' : key . name = 'f2' ; break ;
case '[13~' : key . name = 'f3' ; break ;
case '[14~' : key . name = 'f4' ; break ;
/* from Cygwin and used in libuv */
case '[[A' : key . name = 'f1' ; break ;
case '[[B' : key . name = 'f2' ; break ;
case '[[C' : key . name = 'f3' ; break ;
case '[[D' : key . name = 'f4' ; break ;
case '[[E' : key . name = 'f5' ; break ;
/* common */
case '[15~' : key . name = 'f5' ; break ;
case '[17~' : key . name = 'f6' ; break ;
case '[18~' : key . name = 'f7' ; break ;
case '[19~' : key . name = 'f8' ; break ;
case '[20~' : key . name = 'f9' ; break ;
case '[21~' : key . name = 'f10' ; break ;
case '[23~' : key . name = 'f11' ; break ;
case '[24~' : key . name = 'f12' ; break ;
/* xterm ESC [ letter */
case '[A' : key . name = 'up' ; break ;
case '[B' : key . name = 'down' ; break ;
case '[C' : key . name = 'right' ; break ;
case '[D' : key . name = 'left' ; break ;
case '[E' : key . name = 'clear' ; break ;
case '[F' : key . name = 'end' ; break ;
case '[H' : key . name = 'home' ; break ;
/* xterm/gnome ESC O letter */
case 'OA' : key . name = 'up' ; break ;
case 'OB' : key . name = 'down' ; break ;
case 'OC' : key . name = 'right' ; break ;
case 'OD' : key . name = 'left' ; break ;
case 'OE' : key . name = 'clear' ; break ;
case 'OF' : key . name = 'end' ; break ;
case 'OH' : key . name = 'home' ; break ;
/* xterm/rxvt ESC [ number ~ */
case '[1~' : key . name = 'home' ; break ;
case '[2~' : key . name = 'insert' ; break ;
case '[3~' : key . name = 'delete' ; break ;
case '[4~' : key . name = 'end' ; break ;
case '[5~' : key . name = 'pageup' ; break ;
case '[6~' : key . name = 'pagedown' ; break ;
/* putty */
case '[[5~' : key . name = 'pageup' ; break ;
case '[[6~' : key . name = 'pagedown' ; break ;
/* rxvt */
case '[7~' : key . name = 'home' ; break ;
case '[8~' : key . name = 'end' ; break ;
/* rxvt keys with modifiers */
case '[a' : key . name = 'up' ; key . shift = true ; break ;
case '[b' : key . name = 'down' ; key . shift = true ; break ;
case '[c' : key . name = 'right' ; key . shift = true ; break ;
case '[d' : key . name = 'left' ; key . shift = true ; break ;
case '[e' : key . name = 'clear' ; key . shift = true ; break ;
case '[2$' : key . name = 'insert' ; key . shift = true ; break ;
case '[3$' : key . name = 'delete' ; key . shift = true ; break ;
case '[5$' : key . name = 'pageup' ; key . shift = true ; break ;
case '[6$' : key . name = 'pagedown' ; key . shift = true ; break ;
case '[7$' : key . name = 'home' ; key . shift = true ; break ;
case '[8$' : key . name = 'end' ; key . shift = true ; break ;
case 'Oa' : key . name = 'up' ; key . ctrl = true ; break ;
case 'Ob' : key . name = 'down' ; key . ctrl = true ; break ;
case 'Oc' : key . name = 'right' ; key . ctrl = true ; break ;
case 'Od' : key . name = 'left' ; key . ctrl = true ; break ;
case 'Oe' : key . name = 'clear' ; key . ctrl = true ; break ;
case '[2^' : key . name = 'insert' ; key . ctrl = true ; break ;
case '[3^' : key . name = 'delete' ; key . ctrl = true ; break ;
case '[5^' : key . name = 'pageup' ; key . ctrl = true ; break ;
case '[6^' : key . name = 'pagedown' ; key . ctrl = true ; break ;
case '[7^' : key . name = 'home' ; key . ctrl = true ; break ;
case '[8^' : key . name = 'end' ; key . ctrl = true ; break ;
/* misc. */
case '[Z' : key . name = 'tab' ; key . shift = true ; break ;
default : key . name = 'undefined' ; break ;
}
} else if ( s . length > 1 && s [ 0 ] !== '\x1b' ) {
// Got a longer-than-one string of characters.
// Probably a paste, since it wasn't a control sequence.
Array . prototype . forEach . call ( s , function ( c ) {
emitKey ( stream , c ) ;
} ) ;
return ;
}
// Don't emit a key if no name was found
if ( key . name === undefined ) {
key = undefined ;
}
if ( s . length === 1 ) {
char = s ;
}
if ( key || char ) {
stream . emit ( 'keypress' , char , key ) ;
}
}
/ * *
* moves the cursor to the x and y coordinate on the given stream
* /
function cursorTo ( stream , x , y ) {
if ( typeof x !== 'number' && typeof y !== 'number' )
return ;
if ( typeof x !== 'number' )
throw new Error ( "Can't set cursor row without also setting it's column" ) ;
if ( typeof y !== 'number' ) {
stream . write ( '\x1b[' + ( x + 1 ) + 'G' ) ;
} else {
stream . write ( '\x1b[' + ( y + 1 ) + ';' + ( x + 1 ) + 'H' ) ;
}
}
exports . cursorTo = cursorTo ;
/ * *
* moves the cursor relative to its current location
* /
function moveCursor ( stream , dx , dy ) {
if ( dx < 0 ) {
stream . write ( '\x1b[' + ( - dx ) + 'D' ) ;
} else if ( dx > 0 ) {
stream . write ( '\x1b[' + dx + 'C' ) ;
}
if ( dy < 0 ) {
stream . write ( '\x1b[' + ( - dy ) + 'A' ) ;
} else if ( dy > 0 ) {
stream . write ( '\x1b[' + dy + 'B' ) ;
}
}
exports . moveCursor = moveCursor ;
/ * *
* clears the current line the cursor is on :
* - 1 for left of the cursor
* + 1 for right of the cursor
* 0 for the entire line
* /
function clearLine ( stream , dir ) {
if ( dir < 0 ) {
// to the beginning
stream . write ( '\x1b[1K' ) ;
} else if ( dir > 0 ) {
// to the end
stream . write ( '\x1b[0K' ) ;
} else {
// entire line
stream . write ( '\x1b[2K' ) ;
}
}
exports . clearLine = clearLine ;
/ * *
* clears the screen from the current position of the cursor down
* /
function clearScreenDown ( stream ) {
stream . write ( '\x1b[0J' ) ;
}
exports . clearScreenDown = clearScreenDown ;