|
|
|
// Inspiration for this code comes from Salvatore Sanfilippo's linenoise.
|
|
|
|
// http://github.com/antirez/linenoise
|
|
|
|
// Reference:
|
|
|
|
// * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
|
|
// * http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
|
|
|
|
|
|
|
|
var kHistorySize = 30;
|
|
|
|
var kBufSize = 10*1024;
|
|
|
|
|
|
|
|
|
|
|
|
var Buffer = require('buffer').Buffer;
|
|
|
|
var sys = require('sys');
|
|
|
|
var inherits = require('sys').inherits;
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
|
|
|
var stdio = process.binding('stdio');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
exports.createInterface = function (output) {
|
|
|
|
return new Interface(output);
|
|
|
|
};
|
|
|
|
|
|
|
|
function Interface (output) {
|
|
|
|
this.output = output;
|
|
|
|
|
|
|
|
this.setPrompt("node> ");
|
|
|
|
|
|
|
|
this._tty = output.fd < 3;
|
|
|
|
|
|
|
|
if (parseInt(process.env['NODE_NO_READLINE'])) {
|
|
|
|
this._tty = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._tty) {
|
|
|
|
// input refers to stdin
|
|
|
|
|
|
|
|
// Current line
|
|
|
|
this.line = "";
|
|
|
|
|
|
|
|
// Check process.env.TERM ?
|
|
|
|
stdio.setRawMode(true);
|
|
|
|
this._tty = true;
|
|
|
|
this.columns = stdio.getColumns();
|
|
|
|
|
|
|
|
// Cursor position on the line.
|
|
|
|
this.cursor = 0;
|
|
|
|
|
|
|
|
this.history = [];
|
|
|
|
this.historyIndex = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inherits(Interface, EventEmitter);
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype.setPrompt = function (prompt, length) {
|
|
|
|
this._prompt = prompt;
|
|
|
|
this._promptLength = length ? length : Buffer.byteLength(prompt);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype.prompt = function () {
|
|
|
|
if (this._tty) {
|
|
|
|
this.cursor = 0;
|
|
|
|
this._refreshLine();
|
|
|
|
} else {
|
|
|
|
this.output.write(this._prompt);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype._addHistory = function () {
|
|
|
|
if (this.line.length === 0) return "";
|
|
|
|
|
|
|
|
this.history.unshift(this.line);
|
|
|
|
this.line = "";
|
|
|
|
this.historyIndex = -1;
|
|
|
|
|
|
|
|
this.cursor = 0;
|
|
|
|
|
|
|
|
// Only store so many
|
|
|
|
if (this.history.length > kHistorySize) this.history.pop();
|
|
|
|
|
|
|
|
return this.history[0];
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype._refreshLine = function () {
|
|
|
|
if (this._closed) return;
|
|
|
|
|
|
|
|
stdio.setRawMode(true);
|
|
|
|
|
|
|
|
// Cursor to left edge.
|
|
|
|
this.output.write('\x1b[0G');
|
|
|
|
|
|
|
|
// Write the prompt and the current buffer content.
|
|
|
|
this.output.write(this._prompt);
|
|
|
|
this.output.write(this.line);
|
|
|
|
|
|
|
|
// Erase to right.
|
|
|
|
this.output.write('\x1b[0K');
|
|
|
|
|
|
|
|
// Move cursor to original position.
|
|
|
|
this.output.write('\x1b[0G\x1b[' + (this._promptLength + this.cursor) + 'C');
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype.close = function (d) {
|
|
|
|
if (this._tty) {
|
|
|
|
stdio.setRawMode(false);
|
|
|
|
}
|
|
|
|
this.emit('close');
|
|
|
|
this._closed = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype.write = function (d) {
|
|
|
|
if (this._closed) return;
|
|
|
|
return this._tty ? this._ttyWrite(d) : this._normalWrite(d);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Interface.prototype._normalWrite = function (b) {
|
|
|
|
// Very simple implementation right now. Should try to break on
|
|
|
|
// new lines.
|
|
|
|
this.emit('line', b.toString());
|
|
|
|
};
|
|
|
|
|
|
|
|
Interface.prototype._historyNext = function () {
|
|
|
|
if (this.historyIndex > 0) {
|
|
|
|
this.historyIndex--;
|
|
|
|
this.line = this.history[this.historyIndex];
|
|
|
|
this.cursor = this.line.length; // set cursor to end of line.
|
|
|
|
this._refreshLine();
|
|
|
|
|
|
|
|
} else if (this.historyIndex === 0) {
|
|
|
|
this.historyIndex = -1;
|
|
|
|
this.cursor = 0;
|
|
|
|
this.line = '';
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Interface.prototype._historyPrev = function () {
|
|
|
|
if (this.historyIndex + 1 < this.history.length) {
|
|
|
|
this.historyIndex++;
|
|
|
|
this.line = this.history[this.historyIndex];
|
|
|
|
this.cursor = this.line.length; // set cursor to end of line.
|
|
|
|
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// handle a write from the tty
|
|
|
|
Interface.prototype._ttyWrite = function (b) {
|
|
|
|
switch (b[0]) {
|
|
|
|
/* ctrl+c */
|
|
|
|
case 3:
|
|
|
|
//process.kill(process.pid, "SIGINT");
|
|
|
|
this.close();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 4: // control-d, delete right or EOF
|
|
|
|
if (this.cursor === 0 && this.line.length === 0) {
|
|
|
|
this.close();
|
|
|
|
} else if (this.cursor < this.line.length) {
|
|
|
|
this.line = this.line.slice(0, this.cursor)
|
|
|
|
+ this.line.slice(this.cursor+1, this.line.length)
|
|
|
|
;
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 13: /* enter */
|
|
|
|
var line = this._addHistory();
|
|
|
|
this.output.write('\n\x1b[0G');
|
|
|
|
stdio.setRawMode(false);
|
|
|
|
this.emit('line', line);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 127: /* backspace */
|
|
|
|
case 8: /* ctrl+h */
|
|
|
|
if (this.cursor > 0 && this.line.length > 0) {
|
|
|
|
this.line = this.line.slice(0, this.cursor-1)
|
|
|
|
+ this.line.slice(this.cursor, this.line.length)
|
|
|
|
;
|
|
|
|
this.cursor--;
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 21: /* Ctrl+u, delete the whole line. */
|
|
|
|
this.cursor = 0;
|
|
|
|
this.line = '';
|
|
|
|
this._refreshLine();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 11: /* Ctrl+k, delete from current to end of line. */
|
|
|
|
this.line = this.line.slice(0, this.cursor);
|
|
|
|
this._refreshLine();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 1: /* Ctrl+a, go to the start of the line */
|
|
|
|
this.cursor = 0;
|
|
|
|
this._refreshLine();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 5: /* ctrl+e, go to the end of the line */
|
|
|
|
this.cursor = this.line.length;
|
|
|
|
this._refreshLine();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 2: // control-b, back one character
|
|
|
|
if (this.cursor > 0) {
|
|
|
|
this.cursor--;
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 6: // control-f, forward one character
|
|
|
|
if (this.cursor != this.line.length) {
|
|
|
|
this.cursor++;
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 14: // control-n, next history item
|
|
|
|
this._historyNext();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 16: // control-p, previous history item
|
|
|
|
this._historyPrev();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 26: /* ctrl+z */
|
|
|
|
process.kill(process.pid, "SIGTSTP");
|
|
|
|
return;
|
|
|
|
|
|
|
|
case 27: /* escape sequence */
|
|
|
|
if (b[1] === 98 && this.cursor > 0) { // meta-b - backward word
|
|
|
|
|
|
|
|
} else if (b[1] === 102 && this.cursor < this.line.length) { // meta-f - forward word
|
|
|
|
|
|
|
|
} else if (b[1] === 91 && b[2] === 68) { // left arrow
|
|
|
|
if (this.cursor > 0) {
|
|
|
|
this.cursor--;
|
|
|
|
this.output.write('\x1b[0D');
|
|
|
|
}
|
|
|
|
} else if (b[1] === 91 && b[2] === 67) { // right arrow
|
|
|
|
if (this.cursor != this.line.length) {
|
|
|
|
this.cursor++;
|
|
|
|
this.output.write('\x1b[0C');
|
|
|
|
}
|
|
|
|
} else if (b[1] === 91 && b[2] === 72) { // home
|
|
|
|
this.cursor = 0;
|
|
|
|
this._refreshLine();
|
|
|
|
} else if (b[1] === 91 && b[2] === 70) { // end
|
|
|
|
this.cursor = this.line.length;
|
|
|
|
this._refreshLine();
|
|
|
|
} else if (b[1] === 91 && b[2] === 65) { // up arrow
|
|
|
|
this._historyPrev();
|
|
|
|
} else if (b[1] === 91 && b[2] === 66) { // down arrow
|
|
|
|
this._historyNext();
|
|
|
|
} else if (b[1] === 91 && b[2] === 51 && this.cursor < this.line.length) { // delete right
|
|
|
|
this.line = this.line.slice(0, this.cursor) +
|
|
|
|
this.line.slice(this.cursor+1, this.line.length);
|
|
|
|
this._refreshLine();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
var c = b.toString('utf8');
|
|
|
|
if (this.cursor < this.line.length) {
|
|
|
|
var beg = this.line.slice(0, this.cursor);
|
|
|
|
var end = this.line.slice(this.cursor, this.line.length);
|
|
|
|
this.line = beg + c + end;
|
|
|
|
this.cursor += c.length;
|
|
|
|
this._refreshLine();
|
|
|
|
} else {
|
|
|
|
this.line += c;
|
|
|
|
this.cursor += c.length;
|
|
|
|
this.output.write(c);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.Interface = Interface;
|