0
0
mirror of https://github.com/nodejs/node.git synced 2024-11-29 23:16:30 +01:00
nodejs/lib/readline.js

372 lines
9.5 KiB
JavaScript
Raw Normal View History

2010-06-08 01:03:57 +02:00
// 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, completer) {
return new Interface(output, completer);
};
function Interface (output, completer) {
this.output = output;
this.completer = completer;
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;
// Cursor position on the line.
this.cursor = 0;
this.history = [];
this.historyIndex = -1;
}
}
inherits(Interface, EventEmitter);
Interface.prototype.__defineGetter__("columns", function () {
if (this._tty) {
return stdio.getColumns();
}
});
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._insertString = function (c) {
//BUG: Problem when adding tabs with following content.
// Perhaps the bug is in _refreshLine(). Not sure.
// A hack would be to insert spaces instead of literal '\t'.
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);
}
};
Interface.prototype._tabComplete = function () {
var self = this;
var rv = this.completer(self.line.slice(0, self.cursor));
var completions = rv[0],
completeOn = rv[1]; // the text that was completed
if (completions && completions.length) {
// Apply/show completions.
if (completions.length === 1) {
self._insertString(completions[0].slice(completeOn.length));
self._refreshLine();
} else {
//TODO: If there is a common prefix to all matches (e.g. Python `sys.exi<Tab>`) then apply that portion
self.output.write("\r\n");
var width = completions.reduce(function(a, b) {
return a.length > b.length ? a : b;
}).length + 2; // 2 space padding
var maxColumns = Math.floor(this.columns / width) || 1;
function handleGroup(group) {
if (group.length == 0) {
return;
}
var minRows = Math.ceil(group.length / maxColumns);
for (var row = 0; row < minRows; row++) {
for (var col = 0; col < maxColumns; col++) {
var idx = row * maxColumns + col;
if (idx >= group.length) {
break;
}
var item = group[idx];
self.output.write(item);
if (col < maxColumns - 1) {
for (var s = 0; s < width - item.length; s++) {
self.output.write(' ');
}
}
}
self.output.write('\r\n');
}
self.output.write('\r\n');
}
var group = [], c;
for (var i = 0; i < completions.length; i++) {
c = completions[i];
if (c === "") {
handleGroup(group);
group = [];
} else {
group.push(c);
}
}
handleGroup(group);
self._refreshLine();
}
}
};
2010-06-08 01:43:50 +02:00
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.
2010-06-08 01:43:50 +02:00
this._refreshLine();
2010-06-08 01:43:50 +02:00
} else if (this.historyIndex === 0) {
this.historyIndex = -1;
this.cursor = 0;
this.line = '';
2010-06-08 01:43:50 +02:00
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.
2010-06-08 01:43:50 +02:00
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;
2010-06-08 01:43:50 +02:00
case 4: // control-d, delete right or EOF
if (this.cursor === 0 && this.line.length === 0) {
2010-06-08 01:43:50 +02:00
this.close();
} else if (this.cursor < this.line.length) {
2010-06-30 08:12:46 +02:00
this.line = this.line.slice(0, this.cursor)
+ this.line.slice(this.cursor+1, this.line.length)
;
2010-06-08 01:43:50 +02:00
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;
2010-06-08 01:43:50 +02:00
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) {
2010-06-08 01:43:50 +02:00
this.cursor++;
this._refreshLine();
}
break;
case 14: // control-n, next history item
this._historyNext();
break;
case 9: // tab, completion
if (this.completer) {
this._tabComplete();
}
break;
2010-06-08 01:43:50 +02:00
case 16: // control-p, previous history item
this._historyPrev();
break;
2010-06-09 02:49:04 +02:00
case 26: /* ctrl+z */
process.kill(process.pid, "SIGTSTP");
return;
case 27: /* escape sequence */
2010-06-08 01:43:50 +02:00
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
2010-06-08 01:43:50 +02:00
} else if (b[1] === 91 && b[2] === 68) { // left arrow
if (this.cursor > 0) {
this.cursor--;
this.output.write('\x1b[0D');
}
2010-06-08 01:43:50 +02:00
} else if (b[1] === 91 && b[2] === 67) { // right arrow
if (this.cursor != this.line.length) {
this.cursor++;
this.output.write('\x1b[0C');
}
2010-07-26 08:38:37 +02:00
} 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();
2010-06-08 01:43:50 +02:00
} else if (b[1] === 91 && b[2] === 65) { // up arrow
this._historyPrev();
} else if (b[1] === 91 && b[2] === 66) { // down arrow
this._historyNext();
2010-07-19 01:12:27 +02:00
} else if (b[1] === 91 && b[2] === 51 && this.cursor < this.line.length) { // delete right
2010-07-19 19:16:20 +02:00
this.line = this.line.slice(0, this.cursor) +
this.line.slice(this.cursor+1, this.line.length);
2010-07-19 01:12:27 +02:00
this._refreshLine();
}
break;
default:
var c = b.toString('utf8');
var lines = c.split(/\r\n|\n|\r/);
for (var i = 0; i < lines.length; i++) {
if (i > 0) {
this._ttyWrite(new Buffer([13]));
}
this._insertString(lines[i]);
}
break;
}
};
2010-08-08 20:52:31 +02:00
exports.Interface = Interface;