244 lines
7.0 KiB
JavaScript
244 lines
7.0 KiB
JavaScript
'use strict'
|
|
|
|
var fs = require('fs');
|
|
var stripAnsi = require('strip-ansi');
|
|
var term = 13; // carriage return
|
|
|
|
/**
|
|
* create -- sync function for reading user input from stdin
|
|
* @param {Object} config {
|
|
* sigint: {Boolean} exit on ^C
|
|
* autocomplete: {StringArray} function({String})
|
|
* history: {String} a history control object (see `prompt-sync-history`)
|
|
* }
|
|
* @returns {Function} prompt function
|
|
*/
|
|
|
|
// for ANSI escape codes reference see https://en.wikipedia.org/wiki/ANSI_escape_code
|
|
|
|
function create(config) {
|
|
|
|
config = config || {};
|
|
var sigint = config.sigint;
|
|
var eot = config.eot;
|
|
var autocomplete = config.autocomplete =
|
|
config.autocomplete || function(){return []};
|
|
var history = config.history;
|
|
prompt.history = history || {save: function(){}};
|
|
prompt.hide = function (ask) { return prompt(ask, {echo: ''}) };
|
|
|
|
return prompt;
|
|
|
|
|
|
/**
|
|
* prompt -- sync function for reading user input from stdin
|
|
* @param {String} ask opening question/statement to prompt for
|
|
* @param {String} value initial value for the prompt
|
|
* @param {Object} opts {
|
|
* echo: set to a character to be echoed, default is '*'. Use '' for no echo
|
|
* value: {String} initial value for the prompt
|
|
* ask: {String} opening question/statement to prompt for, does not override ask param
|
|
* autocomplete: {StringArray} function({String})
|
|
* }
|
|
*
|
|
* @returns {string} Returns the string input or (if sigint === false)
|
|
* null if user terminates with a ^C
|
|
*/
|
|
|
|
|
|
function prompt(ask, value, opts) {
|
|
var insert = 0, savedinsert = 0, res, i, savedstr;
|
|
opts = opts || {};
|
|
|
|
if (Object(ask) === ask) {
|
|
opts = ask;
|
|
ask = opts.ask;
|
|
} else if (Object(value) === value) {
|
|
opts = value;
|
|
value = opts.value;
|
|
}
|
|
ask = ask || '';
|
|
var echo = opts.echo;
|
|
var masked = 'echo' in opts;
|
|
autocomplete = opts.autocomplete || autocomplete;
|
|
|
|
var fd = (process.platform === 'win32') ?
|
|
process.stdin.fd :
|
|
fs.openSync('/dev/tty', 'rs');
|
|
|
|
var wasRaw = process.stdin.isRaw;
|
|
if (!wasRaw) { process.stdin.setRawMode && process.stdin.setRawMode(true); }
|
|
|
|
var buf = Buffer.alloc(3);
|
|
var str = '', character, read;
|
|
|
|
savedstr = '';
|
|
|
|
if (ask) {
|
|
process.stdout.write(ask);
|
|
}
|
|
|
|
var cycle = 0;
|
|
var prevComplete;
|
|
|
|
while (true) {
|
|
read = fs.readSync(fd, buf, 0, 3);
|
|
if (read > 1) { // received a control sequence
|
|
switch(buf.toString()) {
|
|
case '\u001b[A': //up arrow
|
|
if (masked) break;
|
|
if (!history) break;
|
|
if (history.atStart()) break;
|
|
|
|
if (history.atEnd()) {
|
|
savedstr = str;
|
|
savedinsert = insert;
|
|
}
|
|
str = history.prev();
|
|
insert = str.length;
|
|
process.stdout.write('\u001b[2K\u001b[0G' + ask + str);
|
|
break;
|
|
case '\u001b[B': //down arrow
|
|
if (masked) break;
|
|
if (!history) break;
|
|
if (history.pastEnd()) break;
|
|
|
|
if (history.atPenultimate()) {
|
|
str = savedstr;
|
|
insert = savedinsert;
|
|
history.next();
|
|
} else {
|
|
str = history.next();
|
|
insert = str.length;
|
|
}
|
|
process.stdout.write('\u001b[2K\u001b[0G'+ ask + str + '\u001b['+(insert+ask.length+1)+'G');
|
|
break;
|
|
case '\u001b[D': //left arrow
|
|
if (masked) break;
|
|
var before = insert;
|
|
insert = (--insert < 0) ? 0 : insert;
|
|
if (before - insert)
|
|
process.stdout.write('\u001b[1D');
|
|
break;
|
|
case '\u001b[C': //right arrow
|
|
if (masked) break;
|
|
insert = (++insert > str.length) ? str.length : insert;
|
|
process.stdout.write('\u001b[' + (insert+ask.length+1) + 'G');
|
|
break;
|
|
default:
|
|
if (buf.toString()) {
|
|
str = str + buf.toString();
|
|
str = str.replace(/\0/g, '');
|
|
insert = str.length;
|
|
promptPrint(masked, ask, echo, str, insert);
|
|
process.stdout.write('\u001b[' + (insert+ask.length+1) + 'G');
|
|
buf = Buffer.alloc(3);
|
|
}
|
|
}
|
|
continue; // any other 3 character sequence is ignored
|
|
}
|
|
|
|
// if it is not a control character seq, assume only one character is read
|
|
character = buf[read-1];
|
|
|
|
// catch a ^C and return null
|
|
if (character == 3){
|
|
process.stdout.write('^C\n');
|
|
fs.closeSync(fd);
|
|
|
|
if (sigint) process.exit(130);
|
|
|
|
process.stdin.setRawMode && process.stdin.setRawMode(wasRaw);
|
|
|
|
return null;
|
|
}
|
|
|
|
// catch a ^D and exit
|
|
if (character == 4) {
|
|
if (str.length == 0 && eot) {
|
|
process.stdout.write('exit\n');
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// catch the terminating character
|
|
if (character == term) {
|
|
fs.closeSync(fd);
|
|
if (!history) break;
|
|
if (!masked && str.length) history.push(str);
|
|
history.reset();
|
|
break;
|
|
}
|
|
|
|
// catch a TAB and implement autocomplete
|
|
if (character == 9) { // TAB
|
|
res = autocomplete(str);
|
|
|
|
if (str == res[0]) {
|
|
res = autocomplete('');
|
|
} else {
|
|
prevComplete = res.length;
|
|
}
|
|
|
|
if (res.length == 0) {
|
|
process.stdout.write('\t');
|
|
continue;
|
|
}
|
|
|
|
var item = res[cycle++] || res[cycle = 0, cycle++];
|
|
|
|
if (item) {
|
|
process.stdout.write('\r\u001b[K' + ask + item);
|
|
str = item;
|
|
insert = item.length;
|
|
}
|
|
}
|
|
|
|
if (character == 127 || (process.platform == 'win32' && character == 8)) { //backspace
|
|
if (!insert) continue;
|
|
str = str.slice(0, insert-1) + str.slice(insert);
|
|
insert--;
|
|
process.stdout.write('\u001b[2D');
|
|
} else {
|
|
if ((character < 32 ) || (character > 126))
|
|
continue;
|
|
str = str.slice(0, insert) + String.fromCharCode(character) + str.slice(insert);
|
|
insert++;
|
|
};
|
|
|
|
promptPrint(masked, ask, echo, str, insert);
|
|
|
|
}
|
|
|
|
process.stdout.write('\n')
|
|
|
|
process.stdin.setRawMode && process.stdin.setRawMode(wasRaw);
|
|
|
|
return str || value || '';
|
|
};
|
|
|
|
|
|
function promptPrint(masked, ask, echo, str, insert) {
|
|
if (masked) {
|
|
process.stdout.write('\u001b[2K\u001b[0G' + ask + Array(str.length+1).join(echo));
|
|
} else {
|
|
process.stdout.write('\u001b[s');
|
|
if (insert == str.length) {
|
|
process.stdout.write('\u001b[2K\u001b[0G'+ ask + str);
|
|
} else {
|
|
if (ask) {
|
|
process.stdout.write('\u001b[2K\u001b[0G'+ ask + str);
|
|
} else {
|
|
process.stdout.write('\u001b[2K\u001b[0G'+ str + '\u001b[' + (str.length - insert) + 'D');
|
|
}
|
|
}
|
|
|
|
// Reposition the cursor to the right of the insertion point
|
|
var askLength = stripAnsi(ask).length;
|
|
process.stdout.write(`\u001b[${askLength+1+(echo==''? 0:insert)}G`);
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = create;
|