blob: ad4193b32c9177f545cfb6a13a359db6b069f9b7 [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
lib.rtdep('lib.wash.Termcap');
/**
* A partial clone of GNU readline.
*/
lib.wash.Readline = function(executeContext) {
this.executeContext = executeContext;
this.executeContext.onStdIn.addListener(this.onStdIn_, this);
this.executeContext.ready();
this.promptString_ = executeContext.arg.promptString || '';
this.promptVars_ = null;
var inputHistory = executeContext.arg.inputHistory;
if (inputHistory && !(inputHistory instanceof Array)) {
this.executeContext.closeError('wam.FileSystem.Error.BadOrMissingArgument',
['inputHistory', 'array']);
return;
}
this.history_ = [''].concat(inputHistory) || [];
this.historyIndex_ = 0;
this.line = '';
this.linePosition = 0;
// Cursor position when the read() started.
this.cursorHome_ = null;
// Cursor position after printing the prompt.
this.cursorPrompt_ = null;
this.verbose = false;
this.nextUndoIndex_ = 0;
this.undo_ = [['', 0]];
this.killRing_ = [];
this.previousLineHeight_ = 0;
this.pendingESC_ = false;
this.tc_ = new lib.wash.Termcap();
this.bindings = {};
this.addBindings(lib.wash.Readline.defaultBindings);
this.onComplete = new lib.Event(this.onComplete_.bind(this));
this.print('%get-row-column()');
this.read();
};
lib.wash.Readline.main = function(executeContext) {
if (typeof executeContext.arg != 'object') {
executeContext.closeError('wam.FileSystem.Error.UnexpectedArgvType',
['object']);
return;
}
return new lib.wash.Readline(executeContext);
};
/**
* Default mapping of key sequence to readline commands.
*
* Uses lib.wash.Termcap syntax for the keys.
*/
lib.wash.Readline.defaultBindings = {
'%(BACKSPACE)': 'backward-delete-char',
'%(ENTER)': 'accept-line',
'%(LEFT)': 'backward-char',
'%(RIGHT)': 'forward-char',
'%(UP)': 'previous-history',
'%(DOWN)': 'next-history',
'%(HOME)': 'beginning-of-line',
'%(END)': 'end-of-line',
'%(DELETE)': 'delete-char',
'%ctrl("A")': 'beginning-of-line',
'%ctrl("D")': 'delete-char-or-eof',
'%ctrl("E")': 'end-of-line',
'%ctrl("H")': 'backward-delete-char',
'%ctrl("K")': 'kill-line',
'%ctrl("L")': 'redraw-line',
'%ctrl("N")': 'next-history',
'%ctrl("P")': 'previous-history',
'%ctrl("Y")': 'yank',
'%ctrl("_")': 'undo',
'%ctrl("/")': 'undo',
'%ctrl(LEFT)': 'backward-word',
'%ctrl(RIGHT)': 'forward-word',
// Meta and key at the same time.
'%meta(BACKSPACE)': 'backward-kill-word',
'%meta(DELETE)': 'kill-word',
'%meta(">")': 'end-of-history',
'%meta("<")': 'beginning-of-history',
// Meta, then key.
//
// TODO(rginda): This would be better as a nested binding, like...
// '%(META)': { '%(DELETE)': 'kill-word', ... }
// ...which would also allow provide for C-c and M-x multi key sequences.
'%(META)%(DELETE)': 'kill-word',
'%(META).': 'yank-last-arg',
};
/**
* Read a line of input.
*
* Prints the given prompt, and waits while the user edits a line of text.
* Provides editing functionality through the keys specified in defaultBindings.
*/
lib.wash.Readline.prototype.read = function() {
this.line = this.history_[0] = '';
this.linePosition = 0;
this.nextUndoIndex_ = 0;
this.undo_ = [['', 0]];
this.cursorHome_ = null;
this.cursorPrompt_ = null;
this.previousLineHeight_ = 0;
};
/**
* Find the start of the word under linePosition in the given line.
*/
lib.wash.Readline.getWordStart = function(line, linePosition) {
var left = line.substr(0, linePosition);
var searchEnd = left.search(/[a-z0-9][^a-z0-9]*$/i);
left = left.substr(0, searchEnd);
var wordStart = left.search(/[^a-z0-9][a-z0-9]*$/i);
return (wordStart > 0) ? wordStart + 1 : 0;
};
/**
* Find the end of the word under linePosition in the given line.
*/
lib.wash.Readline.getWordEnd = function(line, linePosition) {
var right = line.substr(linePosition);
var searchStart = right.search(/[a-z0-9]/i);
right = right.substr(searchStart);
var wordEnd = right.search(/[^a-z0-9]/i);
if (wordEnd == -1)
return line.length;
return linePosition + searchStart + wordEnd;
};
/**
* Register multiple key bindings.
*/
lib.wash.Readline.prototype.addBindings = function(obj) {
for (var key in obj) {
this.addBinding(key, obj[key]);
}
};
/**
* Register a single key binding.
*/
lib.wash.Readline.prototype.addBinding = function(str, commandName) {
this.addRawBinding(this.tc_.input(str), commandName);
};
/**
* Register a binding without passing through termcap.
*/
lib.wash.Readline.prototype.addRawBinding = function(bytes, commandName) {
this.bindings[bytes] = commandName;
};
lib.wash.Readline.prototype.print = function(str, opt_vars) {
this.executeContext.stdout(this.tc_.output(str, opt_vars || {}));
};
lib.wash.Readline.prototype.setPrompt = function(str, vars) {
this.promptString_ = str;
this.promptVars_ = vars;
this.cursorPrompt_ = null;
if (this.executeContext.isReadyState('READY'))
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.dispatch = function(name, arg) {
this.commands[name].call(this, arg);
};
/**
* Instance method version of getWordStart.
*/
lib.wash.Readline.prototype.getWordStart = function() {
return lib.wash.Readline.getWordStart(this.line, this.linePosition);
};
/**
* Instance method version of getWordEnd.
*/
lib.wash.Readline.prototype.getWordEnd = function() {
return lib.wash.Readline.getWordEnd(this.line, this.linePosition);
};
lib.wash.Readline.prototype.killSlice = function(start, length) {
if (length == -1)
length = this.line.length - start;
var killed = this.line.substr(start, length);
this.killRing_.unshift(killed);
this.line = (this.line.substr(0, start) + this.line.substr(start + length));
};
lib.wash.Readline.prototype.dispatchMessage = function(msg) {
msg.dispatch(this, lib.wash.Readline.on);
};
/**
* Called when the terminal replys with the current cursor position.
*/
lib.wash.Readline.prototype.onCursorReport = function(row, column) {
if (!this.cursorHome_) {
this.cursorHome_ = {row: row, column: column};
this.dispatch('redraw-line');
return;
}
if (!this.cursorPrompt_) {
this.cursorPrompt_ = {row: row, column: column};
if (this.cursorHome_.row == this.cursorPrompt_.row) {
this.promptLength_ =
this.cursorPrompt_.column - this.cursorHome_.column;
} else {
var top = this.columns - this.cursorPrompt_.column;
var bottom = this.cursorHome_.column;
var middle = this.columns * (this.cursorPrompt_.row -
this.cursorHome_.row);
this.promptLength_ = top + middle + bottom;
}
this.dispatch('redraw-line');
return;
}
console.warn('Unexpected cursor position report: ' + string);
return;
};
lib.wash.Readline.prototype.onStdIn_ = function(value) {
if (typeof value != 'string')
return;
var string = value;
var ary = string.match(/^\x1b\[(\d+);(\d+)R$/);
if (ary) {
this.onCursorReport(parseInt(ary[1]), parseInt(ary[2]));
return;
}
if (string == '\x1b') {
this.pendingESC_ = true;
return;
}
if (this.pendingESC_) {
string = '\x1b' + string;
this.pendingESC_ = false;
}
var commandName = this.bindings[string];
if (commandName) {
if (this.verbose)
console.log('dispatch: ' + JSON.stringify(string) + ' => ' + commandName);
if (!(commandName in this.commands)) {
throw new Error('Unknown command "' + commandName + '", bound to: ' +
string);
}
var previousLine = this.line;
var previousPosition = this.linePosition;
if (commandName != 'undo')
this.nextUndoIndex_ = 0;
this.dispatch(commandName, string);
if (previousLine != this.line && previousLine != this.undo_[0][0])
this.undo_.unshift([previousLine, previousPosition]);
} else if (/^[\x20-\xff]+$/.test(string)) {
this.nextUndoIndex_ = 0;
this.commands['self-insert'].call(this, string);
} else {
console.log('unhandled: ' + JSON.stringify(string));
}
};
lib.wash.Readline.prototype.commands = {};
lib.wash.Readline.prototype.commands['redraw-line'] = function(string) {
if (!this.cursorHome_) {
console.warn('readline: Home cursor position unknown, won\'t redraw.');
return;
}
if (!this.cursorPrompt_) {
// We don't know where the cursor ends up after printing the prompt.
// We can't just depend on the string length of the prompt because
// it may have non-printing escapes. Instead we echo the prompt and then
// locate the cursor.
this.print('%set-row-column(row, column)',
{ row: this.cursorHome_.row,
column: this.cursorHome_.column,
});
this.print(this.promptString_, this.promptVars_);
this.print('%get-row-column()');
return;
}
this.print('%set-row-column(row, column)%(line)',
{ row: this.cursorPrompt_.row,
column: this.cursorPrompt_.column,
line: this.line
});
var tty = this.executeContext.getTTY();
var totalLineLength = this.cursorHome_.column - 1 + this.promptLength_ +
this.line.length;
var totalLineHeight = Math.ceil(totalLineLength / tty.columns);
var additionalLineHeight = totalLineHeight - 1;
var lastRowFilled = !(totalLineLength % tty.columns);
if (!lastRowFilled)
this.print('%erase-right()');
if (totalLineHeight < this.previousLineHeight_) {
for (var i = totalLineHeight; i < this.previousLineHeight_; i++) {
this.print('%set-row-column(row, 1)%erase-right()',
{row: this.cursorPrompt_.row + i});
}
}
this.previousLineHeight_ = totalLineHeight;
if (totalLineLength >= this.columns) {
// This line overflowed the terminal width. We need to see if it also
// overflowed the height causing a scroll that would invalidate our idea
// of the cursor home row.
var scrollCount;
if (this.cursorHome_.row + additionalLineHeight == tty.rows &&
lastRowFilled) {
// The line was exactly long enough to fill the terminal width and
// and height. Insert a newline to hold the new cursor position.
this.print('\n');
scrollCount = 1;
} else {
scrollCount = this.cursorHome_.row + additionalLineHeight - tty.rows;
}
if (scrollCount > 0) {
this.cursorPrompt_.row -= scrollCount;
this.cursorHome_.row -= scrollCount;
}
}
this.dispatch('reposition-cursor');
};
lib.wash.Readline.prototype.commands['abort-line'] = function() {
this.line = null;
this.onComplete_();
};
lib.wash.Readline.prototype.commands['reposition-cursor'] = function(string) {
// Count the number or rows it took to render the current line at the
// current terminal width.
var tty = this.executeContext.getTTY();
var rowOffset = Math.floor((this.cursorPrompt_.column - 1 +
this.linePosition) / tty.columns);
var column = (this.cursorPrompt_.column + this.linePosition -
(rowOffset * tty.columns));
this.print('%set-row-column(row, column)',
{ row: this.cursorPrompt_.row + rowOffset,
column: column
});
};
lib.wash.Readline.prototype.commands['self-insert'] = function(string) {
if (this.linePosition == this.line.length) {
this.line += string;
} else {
this.line = this.line.substr(0, this.linePosition) + string +
this.line.substr(this.linePosition);
}
this.linePosition += string.length;
this.history_[0] = this.line;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['accept-line'] = function() {
this.historyIndex_ = 0;
if (this.line && this.line != this.history_[1])
this.history_.splice(1, 0, this.line);
this.print('\r\n');
this.onComplete(this.line);
};
lib.wash.Readline.prototype.commands['beginning-of-history'] = function() {
this.historyIndex_ = this.history_.length - 1;
this.line = this.history_[this.historyIndex_];
this.linePosition = this.line.length;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['end-of-history'] = function() {
this.historyIndex_ = this.history_.length - 1;
this.line = this.history_[this.historyIndex_];
this.linePosition = this.line.length;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['previous-history'] = function() {
if (this.historyIndex_ == this.history_.length - 1) {
this.print('%bell()');
return;
}
this.historyIndex_ += 1;
this.line = this.history_[this.historyIndex_];
this.linePosition = this.line.length;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['next-history'] = function() {
if (this.historyIndex_ == 0) {
this.print('%bell()');
return;
}
this.historyIndex_ -= 1;
this.line = this.history_[this.historyIndex_];
this.linePosition = this.line.length;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['kill-word'] = function() {
var start = this.linePosition;
var length = this.getWordEnd() - start;
this.killSlice(start, length);
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['backward-kill-word'] = function() {
var start = this.getWordStart();
var length = this.linePosition - start;
this.killSlice(start, length);
this.linePosition = start;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['kill-line'] = function() {
this.killSlice(this.linePosition, -1);
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['yank'] = function() {
var text = this.killRing_[0];
this.line = (this.line.substr(0, this.linePosition) +
text +
this.line.substr(this.linePosition));
this.linePosition += text.length;
this.dispatch('redraw-line');
};
lib.wash.Readline.prototype.commands['yank-last-arg'] = function() {
if (this.history_.length < 2)
return;
var last = this.history_[1];
var i = lib.wash.Readline.getWordStart(last, last.length - 1);
if (i != -1)
this.dispatch('self-insert', last.substr(i));
};
lib.wash.Readline.prototype.commands['delete-char-or-eof'] = function() {
if (!this.line.length) {
this.dispatch('abort-line');
} else {
this.dispatch('delete-char');
}
};
lib.wash.Readline.prototype.commands['delete-char'] = function() {
if (this.linePosition < this.line.length) {
this.line = (this.line.substr(0, this.linePosition) +
this.line.substr(this.linePosition + 1));
this.dispatch('redraw-line');
} else {
this.print('%bell()');
}
};
lib.wash.Readline.prototype.commands['backward-delete-char'] = function() {
if (this.linePosition > 0) {
this.linePosition -= 1;
this.line = (this.line.substr(0, this.linePosition) +
this.line.substr(this.linePosition + 1));
this.dispatch('redraw-line');
} else {
this.print('%bell()');
}
};
lib.wash.Readline.prototype.commands['backward-char'] = function() {
if (this.linePosition > 0) {
this.linePosition -= 1;
this.dispatch('reposition-cursor');
} else {
this.print('%bell()');
}
};
lib.wash.Readline.prototype.commands['forward-char'] = function() {
if (this.linePosition < this.line.length) {
this.linePosition += 1;
this.dispatch('reposition-cursor');
} else {
this.print('%bell()');
}
};
lib.wash.Readline.prototype.commands['backward-word'] = function() {
this.linePosition = this.getWordStart();
this.dispatch('reposition-cursor');
};
lib.wash.Readline.prototype.commands['forward-word'] = function() {
this.linePosition = this.getWordEnd();
this.dispatch('reposition-cursor');
};
lib.wash.Readline.prototype.commands['beginning-of-line'] = function() {
if (this.linePosition == 0) {
this.print('%bell()');
return;
}
this.linePosition = 0;
this.dispatch('reposition-cursor');
};
lib.wash.Readline.prototype.commands['end-of-line'] = function() {
if (this.linePosition == this.line.length) {
this.print('%bell()');
return;
}
this.linePosition = this.line.length;
this.dispatch('reposition-cursor');
};
lib.wash.Readline.prototype.commands['undo'] = function() {
if ((this.nextUndoIndex_ == this.undo_.length)) {
this.print('%bell()');
return;
}
this.line = this.undo_[this.nextUndoIndex_][0];
this.linePosition = this.undo_[this.nextUndoIndex_][1];
this.dispatch('redraw-line');
this.nextUndoIndex_ += 2;
};
lib.wash.Readline.prototype.onComplete_ = function() {
if (this.executeContext.isOpen)
this.executeContext.closeOk(this.line);
};