Commit 2b304724 authored by rginda@chromium.org's avatar rginda@chromium.org

Initial landing of Screen, Terminal, and VT100 classes.

Some of this code is based on Cory Maccarrone's html terminal, developed
internally in Google as part of a different project.  Thanks to Cory for
allowing us to repurpose it here!

This gets us to a point where we have the core classes largely fleshed out.  The code passes 45 tests, mostly related to correct handling of vt100 escape sequences.

Still to come: network connectivity, keyboard input, and text attributes!

BUG=chromium-os:23271
TEST=test_harness.html:45/45 tests passed.


Review URL: http://codereview.chromium.org/8680034

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@113066 0039d316-1c4b-4281-b951-d872f2087c98
parent 2ffbb484
<!DOCTYPE html>
<html>
<head>
<script src='../js/test_manager.js'></script>
<script src='../js/mock_row_provider.js'></script>
<!-- PubSub is currently unscoped (it's not in hterm.*) -->
<script src='../js/pubsub.js'></script>
<!-- hterm.* things -->
<script src='../js/hterm.js'></script>
<script src='../js/scrollport.js'></script>
<script src='../js/terminal.js'></script>
<script src='../js/options.js'></script>
<script src='../js/screen.js'></script>
<script src='../js/vt100.js'></script>
<script src='../js/pubsub_tests.js'></script>
<!-- Test specific things -->
<script src='../js/test_manager.js'></script>
<script src='../js/mock_row_provider.js'></script>
<!-- Tests -->
<script src='../js/scrollport_tests.js'></script>
<script src='../js/screen_tests.js'></script>
<script src='../js/terminal_tests.js'></script>
<script src='../js/vt100_tests.js'></script>
<script>
var testManager;
......@@ -17,10 +29,12 @@
function init() {
testManager = new TestManager();
testRun = testManager.createTestRun({window: window});
// Stop after the first failure to make it easier to debug in the
// JS console.
testRun.maxFailures = 1;
testRun.selectPattern(testRun.ALL_TESTS);
testRun.run();
}
</script>
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Declares the hterm.* namespace and some basic shared utilities
* that are too small to deserve dedicated files.
*/
var hterm = {};
/**
* Clamp a given integer to a specified range.
*
* @param {integer} v The value to be clamped.
* @param {integer} min The minimum acceptable value.
* @param {integer} max The maximum acceptable value.
*/
hterm.clamp = function(v, min, max) {
if (v < min)
return min;
if (v > max)
return max;
return v;
};
/**
* Return a string containing a given number of space characters.
*
* This method maintains a static cache of the largest amount of whitespace
* ever requested. It shouldn't be used to generate an insanely huge amount of
* whitespace.
*
* @param {integer} length The desired amount of whitespace.
* @param {string} A string of spaces of the requested length.
*/
hterm.getWhitespace = function(length) {
if (length == 0)
return '';
var f = this.getWhitespace;
if (!f.whitespace)
f.whitespace = ' ';
while (length > f.whitespace.length) {
f.whitespace += f.whitespace;
}
return f.whitespace.substr(0, length);
};
/**
* Constructor for a hterm.Size record.
*
* Instances of this class have public read/write members for width and height.
*
* @param {integer} width The width of this record.
* @param {integer} height The height of this record.
*/
hterm.Size = function(width, height) {
this.width = width;
this.height = height;
};
/**
* Adjust the width and height of this record.
*
* @param {integer} width The new width of this record.
* @param {integer} height The new height of this record.
*/
hterm.Size.prototype.resize = function(width, height) {
this.width = width;
this.height = height;
};
/**
* Return a copy of this record.
*
* @return {hterm.Size} A new hterm.Size instance with the same width and
* height.
*/
hterm.Size.prototype.clone = function() {
return new hterm.Size(this.width, this.height);
};
/**
* Test if another hterm.Size instance is equal to this one.
*
* @param {hterm.Size} that The other hterm.Size instance.
* @return {boolen} True if both instances have the same width/height, false
* otherwise.
*/
hterm.Size.prototype.equals = function(that) {
return this.width == that.width && this.height == that.height;
};
/**
* Return a string representation of this instance.
*
* @return {string} A string that identifies the width and height of this
* instance.
*/
hterm.Size.prototype.toString = function() {
return '[hterm.Size: ' + this.width + ', ' + this.height + ']';
};
/**
* Constructor for a hterm.RowCol record.
*
* Instances of this class have public read/write members for row and column.
*
* @param {integer} row The row of this record.
* @param {integer} column The column of this record.
*/
hterm.RowCol = function(row, column) {
this.row = row;
this.column = column;
};
/**
* Adjust the row and column of this record.
*
* @param {integer} row The new row of this record.
* @param {integer} column The new column of this record.
*/
hterm.RowCol.prototype.move = function(row, column) {
this.row = row;
this.column = column;
};
/**
* Return a copy of this record.
*
* @return {hterm.RowCol} A new hterm.RowCol instance with the same row and
* column.
*/
hterm.RowCol.prototype.clone = function() {
return new hterm.RowCol(this.row, this.column);
};
/**
* Test if another hterm.RowCol instance is equal to this one.
*
* @param {hterm.RowCol} that The other hterm.RowCol instance.
* @return {boolen} True if both instances have the same row/column, false
* otherwise.
*/
hterm.RowCol.prototype.equals = function(that) {
return this.row == that.row && this.column == that.column;
};
/**
* Return a string representation of this instance.
*
* @return {string} A string that identifies the row and column of this
* instance.
*/
hterm.RowCol.prototype.toString = function() {
return '[hterm.RowCol: ' + this.row + ', ' + this.column + ']';
};
......@@ -84,7 +84,7 @@ MockRowProvider.prototype.getRowNode = function(index) {
return this.rowNodeCache_[index];
var rec = this.getRowRecord_(index);
var rowNode = this.document_.createElement('div');
var rowNode = this.document_.createElement('x-row');
rowNode.rowIndex = index;
rowNode.innerHTML = rec.html;
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This file implements the hterm.Options class,
* which stores current operating conditions for the terminal. This object is
* used instead of a series of parameters to allow saving/restoring of cursor
* conditions easily, and to provide an easy place for common configuration
* options.
*
* Original code by Cory Maccarrone.
*/
/**
* Constructor for the hterm.Options class, optionally acting as a copy
* constructor.
*
* @param {hterm.Options=} opt_copy Optional instance to copy.
* @constructor
*/
hterm.Options = function(opt_copy) {
// All attributes in this class are public to allow easy access by the
// terminal.
this.wraparound = opt_copy ? opt_copy.wraparound : true;
this.reverseWraparound = opt_copy ? opt_copy.reverseWraparound : false;
this.originMode = opt_copy ? opt_copy.originMode : false;
this.autoLinefeed = opt_copy ? opt_copy.autoLinefeed : true;
this.specialChars = opt_copy ? opt_copy.specialChars : false;
this.cursorVisible = opt_copy ? opt_copy.cursorVisible : true;
this.cursorBlink = opt_copy ? opt_copy.cursorBlink : true;
this.insertMode = opt_copy ? opt_copy.insertMode : false;
this.reverseVideo = opt_copy ? opt_copy.reverseVideo : false;
};
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This class represents a single terminal screen full of text.
*
* It maintains the current cursor position and has basic methods for text
* insert and overwrite, and adding or removing rows from the screen.
*
* This class has no knowledge of the scrollback buffer.
*
* The number of rows on the screen is determined only by the number of rows
* that the caller inserts into the screen. If a caller wants to ensure a
* constant number of rows on the screen, it's their responsibility to remove a
* row for each row inserted.
*
* The screen width, in contrast, is enforced locally.
*
*
* In practice...
* - The hterm.Terminal class holds two hterm.Screen instances. One for the
* primary screen and one for the alternate screen.
*
* - The html.Screen class only cares that rows are HTMLElements. In the
* larger context of hterm, however, the rows happen to be displayed by an
* hterm.ScrollPort and have to follow a few rules as a result. Each
* row must be rooted by the custom HTML tag 'x-row', and each must have a
* rowIndex property that corresponds to the index of the row in the context
* of the scrollback buffer. These invariants are enforced by hterm.Terminal
* because that is the class using the hterm.Screen in the context of an
* hterm.ScrollPort.
*/
/**
* Create a new screen instance.
*
* The screen initially has no rows and a maximum column count of 0.
*
* @param {integer} opt_columnCount The maximum number of columns for this
* screen. See insertString() and overwriteString() for information about
* what happens when too many characters are added too a row. Defaults to
* 0 if not provided.
*/
hterm.Screen = function(opt_columnCount) {
/**
* Public, read-only access to the rows in this screen.
*/
this.rowsArray = [];
// The max column width for this screen.
this.columnCount_ = opt_columnCount || 0;
// Current zero-based cursor coordinates. (-1, -1) implies that the cursor
// is uninitialized.
this.cursorPosition = new hterm.RowCol(-1, -1);
// The node containing the row that the cursor is positioned on.
this.cursorRowNode_ = null;
// The node containing the span of text that the cursor is positioned on.
this.cursorNode_ = null;
// The offset into cursorNode_ where the cursor is positioned.
this.cursorOffset_ = null;
};
/**
* Return the screen size as an hterm.Size object.
*
* @return {hterm.Size} hterm.Size object representing the current number
* of rows and columns in this screen.
*/
hterm.Screen.prototype.getSize = function() {
return new hterm.Size(this.columnCount_, this.rowsArray.length);
};
/**
* Return the current number of rows in this screen.
*
* @return {integer} The number of rows in this screen.
*/
hterm.Screen.prototype.getHeight = function() {
return this.rowsArray.length;
};
/**
* Return the current number of columns in this screen.
*
* @return {integer} The number of columns in this screen.
*/
hterm.Screen.prototype.getWidth = function() {
return this.columnCount_;
};
/**
* Set the maximum number of columns per row.
*
* TODO(rginda): This should probably clip existing rows if the count is
* decreased.
*
* @param {integer} count The maximum number of columns per row.
*/
hterm.Screen.prototype.setColumnCount = function(count) {
this.columnCount_ = count;
};
/**
* Remove the first row from the screen and return it.
*
* @return {HTMLElement} The first row in this screen.
*/
hterm.Screen.prototype.shiftRow = function() {
return this.shiftRows(1)[0];
}
/**
* Remove rows from the top of the screen and return them as an array.
*
* @param {integer} count The number of rows to remove.
* @return {Array.<HTMLElement>} The selected rows.
*/
hterm.Screen.prototype.shiftRows = function(count) {
return this.rowsArray.splice(0, count);
};
/**
* Insert a row at the top of the screen.
*
* @param {HTMLElement} The row to insert.
*/
hterm.Screen.prototype.unshiftRow = function(row) {
this.rowsArray.splice(0, 0, row);
};
/**
* Insert rows at the top of the screen.
*
* @param {Array.<HTMLElement>} The rows to insert.
*/
hterm.Screen.prototype.unshiftRows = function(rows) {
this.rowsArray.unshift.apply(this.rowsArray, rows);
};
/**
* Remove the last row from the screen and return it.
*
* @return {HTMLElement} The last row in this screen.
*/
hterm.Screen.prototype.popRow = function() {
return this.popRows(1)[0];
};
/**
* Remove rows from the bottom of the screen and return them as an array.
*
* @param {integer} count The number of rows to remove.
* @return {Array.<HTMLElement>} The selected rows.
*/
hterm.Screen.prototype.popRows = function(count) {
return this.rowsArray.splice(this.rowsArray.length - count, count);
};
/**
* Insert a row at the bottom of the screen.
*
* @param {HTMLElement} The row to insert.
*/
hterm.Screen.prototype.pushRow = function(row) {
this.rowsArray.push(row);
};
/**
* Insert rows at the bottom of the screen.
*
* @param {Array.<HTMLElement>} The rows to insert.
*/
hterm.Screen.prototype.pushRows = function(rows) {
rows.push.apply(this.rowsArray, rows);
};
/**
* Insert a row at the specified column of the screen.
*
* @param {HTMLElement} The row to insert.
*/
hterm.Screen.prototype.insertRow = function(index, row) {
this.rowsArray.splice(index, 0, row);
};
/**
* Insert rows at the specified column of the screen.
*
* @param {Array.<HTMLElement>} The rows to insert.
*/
hterm.Screen.prototype.insertRows = function(index, rows) {
for (var i = 0; i < rows.length; i++) {
this.rowsArray.splice(index + i, 0, rows[i]);
}
};
/**
* Remove a last row from the specified column of the screen and return it.
*
* @return {HTMLElement} The selected row.
*/
hterm.Screen.prototype.removeRow = function(index) {
return this.rowsArray.splice(index, 1)[0];
};
/**
* Remove rows from the bottom of the screen and return them as an array.
*
* @param {integer} count The number of rows to remove.
* @return {Array.<HTMLElement>} The selected rows.
*/
hterm.Screen.prototype.removeRows = function(index, count) {
return this.rowsArray.splice(index, count);
};
/**
* Invalidate the current cursor position.
*
* This sets this.cursorPosition to (-1, -1) and clears out some internal
* data.
*
* Attempting to insert or overwrite text while the cursor position is invalid
* will raise an obscure exception.
*/
hterm.Screen.prototype.invalidateCursorPosition = function() {
this.cursorPosition.move(-1, -1);
this.cursorRowNode_ = null;
this.cursorNode_ = null;
this.cursorOffset_ = null;
};
/**
* Clear the contents of a selected row.
*
* TODO: Make this clear in the current style... somehow. We can't just
* fill the row with spaces, since they would have potential to mess up the
* terminal (for example, in insert mode, they might wrap around to the next
* line.
*
* @param {integer} index The zero-based index to clear.
*/
hterm.Screen.prototype.clearRow = function(index) {
if (index == this.cursorPosition.row) {
this.clearCursorRow();
} else {
var row = this.rowsArray[index];
row.innerHTML = '';
row.appendChild(row.ownerDocument.createTextNode(''));
}
};
/**
* Clear the contents of the cursor row.
*
* TODO: Same comment as clearRow().
*/
hterm.Screen.prototype.clearCursorRow = function() {
this.cursorRowNode_.innerHTML = '';
var text = this.cursorRowNode_.ownerDocument.createTextNode('');
this.cursorRowNode_.appendChild(text);
this.cursorOffset_ = 0;
this.cursorNode_ = text;
this.cursorPosition.column = 0;
};
/**
* Relocate the cursor to a give row and column.
*
* @param {integer} row The zero based row.
* @param {integer} column The zero based column.
*/
hterm.Screen.prototype.setCursorPosition = function(row, column) {
var currentColumn = 0;
if (row >= this.rowsArray.length)
throw 'Row out of bounds: ' + row;
var rowNode = this.rowsArray[row];
var node = rowNode.firstChild;
if (!node) {
node = rowNode.ownerDocument.createTextNode('');
rowNode.appendChild(node);
}
if (rowNode == this.cursorRowNode_) {
if (column >= this.cursorPosition.column - this.cursorOffset_) {
node = this.cursorNode_;
currentColumn = this.cursorPosition.column - this.cursorOffset_;
}
} else {
this.cursorRowNode_ = rowNode;
}
this.cursorPosition.move(row, column);
while (node) {
var offset = column - currentColumn;
var textContent = node.textContent;
if (!node.nextSibling || textContent.length > offset) {
this.cursorNode_ = node;
this.cursorOffset_ = offset;
return;
}
currentColumn += textContent.length;
node = node.nextSibling;
}
};
/**
* Insert the given string at the cursor position, with the understanding that
* the insert will cause the column to overflow, and the overflow will be
* in a different text style than where the cursor is currently located.
*
* TODO: Implement this.
*/
hterm.Screen.prototype.spliceStringAndWrap_ = function(str) {
throw 'NOT IMPLEMENTED';
};
/**
* Insert a string at the current cursor position.
*
* If the insert causes the column to overflow, the extra text is returned.
*
* @return {string} Text that overflowed the column, or null if nothing
* overflowed.
*/
hterm.Screen.prototype.insertString = function(str) {
if (this.cursorPosition.column == this.columnCount_)
return str;
var totalRowText = this.cursorRowNode_.textContent;
// There may not be underlying characters to support the current cursor
// position, since they don't get inserted until they're necessary.
var missingSpaceCount = Math.max(this.cursorPosition.column -
totalRowText.length,
0);
var overflowCount = Math.max(totalRowText.length + missingSpaceCount +
str.length - this.columnCount_,
0);
if (overflowCount > 0 && this.cursorNode_.nextSibling) {
// We're going to overflow, but there is text after the cursor with a
// different set of attributes. This is going to take some effort.
return this.spliceStringAndWrap_(str);
}
// Wrapping is simple since the cursor is located in the last block of text
// on the line.
var cursorNodeText = this.cursorNode_.textContent;
var leadingText = cursorNodeText.substr(0, this.cursorOffset_);
var trailingText = str + cursorNodeText.substr(this.cursorOffset_);
var overflowText = trailingText.substr(trailingText.length - overflowCount);
trailingText = trailingText.substr(0, trailingText.length - overflowCount);
this.cursorNode_.textContent = (
leadingText +
hterm.getWhitespace(missingSpaceCount) +
trailingText
);
var cursorDelta = Math.min(str.length, trailingText.length);
this.cursorOffset_ += cursorDelta;
this.cursorPosition.column += cursorDelta;
return overflowText || null;
};
/**
* Overwrite the text at the current cursor position.
*
* If the text causes the column to overflow, the extra text is returned.
*
* @return {string} Text that overflowed the column, or null if nothing
* overflowed.
*/
hterm.Screen.prototype.overwriteString = function(str) {
var maxLength = this.columnCount_ - this.cursorPosition.column;
if (!maxLength)
return str;
this.deleteChars(Math.min(str.length, maxLength));
return this.insertString(str);
};
/**
* Forward-delete one or more characters at the current cursor position.
*
* Text to the right of the deleted characters is shifted left. Only affects
* characters on the same row as the cursor.
*
* @param {integer} count The number of characters to delete. This is clamped
* to the column width minus the cursor column.
*/
hterm.Screen.prototype.deleteChars = function(count) {
var node = this.cursorNode_;
var offset = this.cursorOffset_;
while (node && count) {
var startLength = node.textContent.length;
node.textContent = node.textContent.substr(0, offset) +
node.textContent.substr(offset + count);
var endLength = node.textContent.length;
count -= startLength - endLength;
if (endLength == 0 && node != this.cursorNode_) {
var nextNode = node.nextSibling;
node.parentNode.removeChild(node);
node = nextNode;
} else {
node = node.nextSibling;
}
offset = 0;
}
};
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Unit tests for the hterm.Screen class.
*/
hterm.Screen.Tests = new TestManager.Suite('hterm.Screen.Tests');
/**
* Clear out the current document and create a new hterm.Screen object for
* testing.
*
* Called before each test case in this suite.
*/
hterm.Screen.Tests.prototype.preamble = function(result, cx) {
cx.window.document.body.innerHTML = '';
cx.window.screen = this.screen = new hterm.Screen();
cx.window.screen.setColumnCount(80);
};
/**
* Test the push and pop functionality of the hterm.Screen.
*/
hterm.Screen.Tests.addTest('push-pop', function(result, cx) {
// Push one at a time.
var ary = [];
for (var i = 0; i < 10; i++) {
ary[i] = document.createElement('div');
ary[i].textContent = i;
this.screen.pushRow(ary[i]);
}
result.assertEQ(ary.length, this.screen.getHeight());
// Pop one at a time.
for (var i = ary.length - 1; i >= 0; i--) {
result.assertEQ(ary[i], this.screen.popRow(), 'i:' + i);
}
// Bulk push.
this.screen.pushRows(ary);
result.assertEQ(ary.length, this.screen.rowsArray.length);
// Bulk pop.
var popary = this.screen.popRows(ary.length);
result.assertEQ(ary.length, popary.length);
for (var i = ary.length - 1; i >= 0; i--) {
result.assertEQ(ary[i], popary[i], 'i:' + i);
}
// Reset, then partial bulk pop.
this.screen.pushRows(ary);
result.assertEQ(ary.length, this.screen.rowsArray.length);
var popary = this.screen.popRows(5);
for (var i = 0; i < 5; i++) {
result.assertEQ(ary[i + 5], popary[i], 'i:' + i);
}
result.pass();
});
/**
* Test the unshift and shift functionality of the hterm.Screen.
*/
hterm.Screen.Tests.addTest('unshift-shift', function(result, cx) {
// Unshift one at a time.
var ary = [];
for (var i = 0; i < 10; i++) {
ary[i] = document.createElement('div');
ary[i].textContent = i;
this.screen.unshiftRow(ary[i]);
}
result.assertEQ(ary.length, this.screen.rowsArray.length);
// Shift one at a time.
for (var i = ary.length - 1; i >= 0; i--) {
result.assertEQ(ary[i], this.screen.shiftRow(), 'i:' + i);
}
// Bulk unshift.
this.screen.unshiftRows(ary);
result.assertEQ(ary.length, this.screen.rowsArray.length);
// Bulk shift.
var shiftary = this.screen.shiftRows(ary.length);
result.assertEQ(ary.length, shiftary.length);
for (var i = ary.length - 1; i >= 0; i--) {
result.assertEQ(ary[i], shiftary[i], 'i:' + i);
}
// Reset, then partial bulk shift.
this.screen.unshiftRows(ary);
result.assertEQ(ary.length, this.screen.rowsArray.length);
var shiftary = this.screen.shiftRows(5);
for (var i = 0; i < 5; i++) {
result.assertEQ(ary[i], shiftary[i], 'i:' + i);
}
result.pass();
});
/**
* Test cursor positioning functionality.
*/
hterm.Screen.Tests.addTest('cursor-movement', function(result, cx) {
var ary = [];
for (var i = 0; i < 3; i++) {
ary[i] = document.createElement('div');
ary[i].textContent = i;
this.screen.pushRow(ary[i]);
}
this.screen.setCursorPosition(0, 0);
result.assertEQ(this.screen.cursorRowNode_, ary[0]);
result.assertEQ(this.screen.cursorNode_, ary[0].firstChild);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(1, 0);
result.assertEQ(this.screen.cursorRowNode_, ary[1]);
result.assertEQ(this.screen.cursorNode_, ary[1].firstChild);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(1, 10);
result.assertEQ(this.screen.cursorRowNode_, ary[1]);
result.assertEQ(this.screen.cursorNode_, ary[1].firstChild);
result.assertEQ(this.screen.cursorOffset_, 10);
this.screen.setCursorPosition(1, 5);
result.assertEQ(this.screen.cursorRowNode_, ary[1]);
result.assertEQ(this.screen.cursorNode_, ary[1].firstChild);
result.assertEQ(this.screen.cursorOffset_, 5);
this.screen.setCursorPosition(1, 10);
result.assertEQ(this.screen.cursorRowNode_, ary[1]);
result.assertEQ(this.screen.cursorNode_, ary[1].firstChild);
result.assertEQ(this.screen.cursorOffset_, 10);
ary[2].innerHTML = '01<div>23</div>45<div>67</div>89';
this.screen.setCursorPosition(2, 0);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].firstChild);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(2, 1);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].firstChild);
result.assertEQ(this.screen.cursorOffset_, 1);
this.screen.setCursorPosition(2, 2);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[1]);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(2, 3);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[1]);
result.assertEQ(this.screen.cursorOffset_, 1);
this.screen.setCursorPosition(2, 4);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[2]);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(2, 5);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[2]);
result.assertEQ(this.screen.cursorOffset_, 1);
this.screen.setCursorPosition(2, 6);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[3]);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(2, 7);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[3]);
result.assertEQ(this.screen.cursorOffset_, 1);
this.screen.setCursorPosition(2, 8);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[4]);
result.assertEQ(this.screen.cursorOffset_, 0);
this.screen.setCursorPosition(2, 9);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[4]);
result.assertEQ(this.screen.cursorOffset_, 1);
this.screen.setCursorPosition(2, 18);
result.assertEQ(this.screen.cursorRowNode_, ary[2]);
result.assertEQ(this.screen.cursorNode_, ary[2].childNodes[4]);
result.assertEQ(this.screen.cursorOffset_, 10);
result.pass();
});
/**
* Test character removal.
*/
hterm.Screen.Tests.addTest('delete-chars', function(result, cx) {
var row = document.createElement('div');
row.innerHTML = 'hello<div id="1"> </div><div id="2">world</div>';
this.screen.pushRow(row);
this.screen.setCursorPosition(0, 3);
this.screen.deleteChars(5);
result.assertEQ(row.innerHTML, 'hel<div id="2">rld</div>');
result.pass();
});
/**
* Test the ability to insert text in a line.
*/
hterm.Screen.Tests.addTest('insert', function(result, cx) {
// Sample rows. Row 0 is a simple, empty row. Row 1 simulates rows with
// mixed text attributes.
var ary = [document.createElement('div'), document.createElement('div')];
ary[1].innerHTML = 'hello<div id="1"> </div><div id="2">world</div>';
this.screen.pushRows(ary);
// Basic insert.
this.screen.setCursorPosition(0, 0);
this.screen.insertString('XXXXX');
result.assertEQ(ary[0].innerHTML, 'XXXXX');
// Test that positioning the cursor beyond the end of the current text does
// not cause spaces to be printed.
this.screen.clearCursorRow();
this.screen.setCursorPosition(0, 3);
result.assertEQ(ary[0].innerHTML, '');
// Print some text at this cursor position and make sure the spaces show up.
this.screen.insertString('XXXXX');
result.assertEQ(ary[0].innerHTML, ' XXXXX');
// Fetch enough whitespace to ensure that the row is full.
var ws = hterm.getWhitespace(this.screen.getWidth());
// Check simple overflow.
this.screen.clearCursorRow();
this.screen.insertString('XXXX');
this.screen.setCursorPosition(0, 0);
var overflow = this.screen.insertString(ws);
result.assertEQ(overflow, 'XXXX');
// Insert into a more complicated row.
this.screen.setCursorPosition(1, 3);
this.screen.insertString('XXXXX');
result.assertEQ(ary[1].innerHTML, 'helXXXXXlo<div id="1"> </div>' +
'<div id="2">world</div>');
// Check that multi-attribute is not implemented. We'll want a better test
// once its implemented.
this.screen.setCursorPosition(1, 0);
try {
this.screen.insertString(ws);
result.assert(false);
} catch (ex) {
result.assertEQ(ex, 'NOT IMPLEMENTED');
}
result.pass();
});
/**
* Test the ability to overwrite test.
*/
hterm.Screen.Tests.addTest('overwrite', function(result, cx) {
var ary = [];
ary[0] = document.createElement('div');
ary[0].innerHTML = 'hello<div id="1"> </div><div id="2">world</div>';
ary[1] = document.createElement('div');
this.screen.pushRows(ary);
this.screen.setCursorPosition(0, 3);
this.screen.overwriteString('XXXXX');
result.assertEQ(ary[0].innerHTML, 'helXXXXX<div id="2">rld</div>');
this.screen.setCursorPosition(1, 0);
this.screen.overwriteString('XXXXX');
result.assertEQ(ary[1].innerHTML, 'XXXXX');
result.pass();
});
......@@ -19,20 +19,24 @@
* of the selection is off screen. It would be difficult to fix this without
* adding significant overhead to pathologically large selection cases.
*
* The RowProvider should return rows rooted by the custom tag name 'x-row'.
* This ensures that we can quickly assign the correct display height
* to the rows with css.
*
* @param {RowProvider} rowProvider An object capable of providing rows as
* raw text or row nodes.
* @param {integer} fontSize The css font-size, in pixels.
* @param {integer} opt_lineHeight Optional css line-height in pixels.
* If omitted it will be computed based on the fontSize.
*/
function ScrollPort(rowProvider, fontSize, opt_lineHeight) {
hterm.ScrollPort = function(rowProvider, fontSize, opt_lineHeight) {
PubSub.addBehavior(this);
this.rowProvider_ = rowProvider;
this.fontSize_ = fontSize;
this.rowHeight_ = opt_lineHeight || fontSize + 2;
this.selection_ = new ScrollPort.Selection(this);
this.selection_ = new hterm.ScrollPort.Selection(this);
// A map of rowIndex => rowNode for each row that is drawn as part of a
// pending redraw_() call. Null if there is no pending redraw_ call.
......@@ -42,6 +46,9 @@ function ScrollPort(rowProvider, fontSize, opt_lineHeight) {
// previous redraw_() call.
this.previousRowNodeCache_ = {};
// The css rule that we use to control the height of a row.
this.xrowCssRule_ = null;
this.div_ = null;
this.document_ = null;
......@@ -54,9 +61,9 @@ function ScrollPort(rowProvider, fontSize, opt_lineHeight) {
* Proxy for the native selection object which understands how to walk up the
* DOM to find the containing row node and sort out which comes first.
*
* @param {ScrollPort} scrollPort The parent ScrollPort instance.
* @param {hterm.ScrollPort} scrollPort The parent hterm.ScrollPort instance.
*/
ScrollPort.Selection = function(scrollPort) {
hterm.ScrollPort.Selection = function(scrollPort) {
this.scrollPort_ = scrollPort;
this.selection_ = null;
......@@ -101,7 +108,7 @@ ScrollPort.Selection = function(scrollPort) {
* This is a one-way synchronization, the DOM selection is copied to this
* object, not the other way around.
*/
ScrollPort.Selection.prototype.sync = function() {
hterm.ScrollPort.Selection.prototype.sync = function() {
var selection = this.scrollPort_.getDocument().getSelection();
this.startRow = null;
......@@ -146,9 +153,9 @@ ScrollPort.Selection.prototype.sync = function() {
};
/**
* Turn a div into this ScrollPort.
* Turn a div into this hterm.ScrollPort.
*/
ScrollPort.prototype.decorate = function(div) {
hterm.ScrollPort.prototype.decorate = function(div) {
this.div_ = div;
this.iframe_ = div.ownerDocument.createElement('iframe');
......@@ -173,6 +180,14 @@ ScrollPort.prototype.decorate = function(div) {
'white-space: pre;' +
'-webkit-user-select: none;');
var style = doc.createElement('style');
style.textContent = 'x-row {}';
doc.head.appendChild(style);
this.xrowCssRule_ = doc.styleSheets[0].cssRules[0];
this.xrowCssRule_.style.display = 'block';
this.xrowCssRule_.style.height = this.rowHeight_ + 'px';
this.screen_ = doc.createElement('x-screen');
this.screen_.style.cssText = (
'display: block;' +
......@@ -229,19 +244,35 @@ ScrollPort.prototype.decorate = function(div) {
this.setRowMetrics(this.fontSize_, this.rowHeight_);
};
ScrollPort.prototype.getRowHeight = function() {
hterm.ScrollPort.prototype.getForegroundColor = function() {
return this.document_.body.style.color;
};
hterm.ScrollPort.prototype.setForegroundColor = function(color) {
this.document_.body.style.color = color;
};
hterm.ScrollPort.prototype.getBackgroundColor = function() {
return this.document_.body.style.backgroundColor;
};
hterm.ScrollPort.prototype.setBackgroundColor = function(color) {
this.document_.body.style.backgroundColor = color;
};
hterm.ScrollPort.prototype.getRowHeight = function() {
return this.rowHeight_;
};
ScrollPort.prototype.getScreenWidth = function() {
hterm.ScrollPort.prototype.getScreenWidth = function() {
return this.screen_.clientWidth;
};
ScrollPort.prototype.getScreenWidth = function() {
hterm.ScrollPort.prototype.getScreenHeight = function() {
return this.screen_.clientHeight;
};
ScrollPort.prototype.getCharacterWidth = function() {
hterm.ScrollPort.prototype.getCharacterWidth = function() {
var span = this.document_.createElement('span');
span.textContent = '\xa0'; // &nbsp;
this.rowNodes_.appendChild(span);
......@@ -251,16 +282,16 @@ ScrollPort.prototype.getCharacterWidth = function() {
};
/**
* Return the document that holds the visible rows of this ScrollPort.
* Return the document that holds the visible rows of this hterm.ScrollPort.
*/
ScrollPort.prototype.getDocument = function() {
hterm.ScrollPort.prototype.getDocument = function() {
return this.document_;
};
/**
* Clear out any cached rowNodes.
*/
ScrollPort.prototype.resetCache = function() {
hterm.ScrollPort.prototype.resetCache = function() {
this.currentRowNodeCache_ = null;
this.previousRowNodeCache_ = {};
};
......@@ -271,27 +302,62 @@ ScrollPort.prototype.resetCache = function() {
* This will clear the row cache and cause a redraw.
*
* @param {Object} rowProvider An object capable of providing the rows
* in this ScrollPort.
* in this hterm.ScrollPort.
*/
ScrollPort.prototype.setRowProvider = function(rowProvider) {
this.resetCache_();
hterm.ScrollPort.prototype.setRowProvider = function(rowProvider) {
this.resetCache();
this.rowProvider_ = rowProvider;
this.redraw_();
};
/**
* Set the fontSize and lineHeight of this ScrollPort.
* Inform the ScrollPort that a given range of rows is invalid.
*
* The RowProvider should call this method if the underlying x-row instance
* for a given rowIndex is no longer valid.
*
* Note that this is not necessary when only the *content* of the x-row has
* changed. It's only needed when getRowNode(N) would return a different
* x-row than it used to.
*
* If rows in the sepecified range are visible, they will be redrawn.
*/
hterm.ScrollPort.prototype.invalidateRowRange = function(start, end) {
this.resetCache();
var node = this.rowNodes_.firstChild;
while (node) {
if ('rowIndex' in node &&
node.rowIndex >= start && node.rowIndex <= end) {
var nextSibling = node.nextSibling;
this.rowNodes_.removeChild(node);
var newNode = this.rowProvider_.getRowNode(node.rowIndex);
this.rowNodes_.insertBefore(newNode, nextSibling);
this.previousRowNodeCache_[node.rowIndex] = newNode;
node = nextSibling;
} else {
node = node.nextSibling;
}
}
};
/**
* Set the fontSize and lineHeight of this hterm.ScrollPort.
*
* @param {integer} fontSize The css font-size, in pixels.
* @param {integer} opt_lineHeight Optional css line-height in pixels.
* If omitted it will be computed based on the fontSize.
*/
ScrollPort.prototype.setRowMetrics = function(fontSize, opt_lineHeight) {
hterm.ScrollPort.prototype.setRowMetrics = function(fontSize, opt_lineHeight) {
this.fontSize_ = fontSize;
this.rowHeight_ = opt_lineHeight || fontSize + 2;
this.screen_.style.fontSize = this.fontSize_ + 'px';
this.screen_.style.lineHeight = this.rowHeight_ + 'px';
this.xrowCssRule_.style.height = this.rowHeight_ + 'px';
this.topSelectBag_.style.height = this.rowHeight_ + 'px';
this.bottomSelectBag_.style.height = this.rowHeight_ + 'px';
......@@ -310,7 +376,7 @@ ScrollPort.prototype.setRowMetrics = function(fontSize, opt_lineHeight) {
* Reset dimensions and visible row count to account for a change in the
* dimensions of the 'x-screen'.
*/
ScrollPort.prototype.resize = function() {
hterm.ScrollPort.prototype.resize = function() {
var screenWidth = this.screen_.clientWidth;
var screenHeight = this.screen_.clientHeight;
......@@ -327,36 +393,43 @@ ScrollPort.prototype.resize = function() {
this.visibleRowTopMargin = screenHeight - visibleRowsHeight;
this.topFold_.style.marginBottom = this.visibleRowTopMargin + 'px';
// Resize the scroll area to appear as though it contains every row.
this.scrollArea_.style.height = (this.rowHeight_ *
this.rowProvider_.getRowCount() +
this.visibleRowTopMargin + 'px');
// Set the dimensions of the visible rows container.
this.rowNodes_.style.width = screenWidth + 'px';
this.rowNodes_.style.height = visibleRowsHeight + 'px';
this.rowNodes_.style.left = this.screen_.offsetLeft + 'px';
var self = this;
this.publish('resize',
{ scrollPort: this },
function() { self.redraw_() });
this.publish
('resize', { scrollPort: this },
function() {
var index = self.bottomFold_.previousSibling.rowIndex;
self.scrollRowToBottom(index);
});
};
hterm.ScrollPort.prototype.syncScrollHeight = function() {
// Resize the scroll area to appear as though it contains every row.
this.scrollArea_.style.height = (this.rowHeight_ *
this.rowProvider_.getRowCount() +
this.visibleRowTopMargin + 'px');
};
/**
* Redraw the current ScrollPort based on the current scrollbar position.
* Redraw the current hterm.ScrollPort based on the current scrollbar position.
*
* When redrawing, we are careful to make sure that the rows that start or end
* the current selection are not touched in any way. Doing so would disturb
* the selection, and cleaning up after that would cause flashes at best and
* incorrect selection at worst. Instead, we modify the DOM around these nodes.
* We even stash the selection start/end outside of the visible area if
* they are not supposed to be visible in the ScrollPort.
* they are not supposed to be visible in the hterm.ScrollPort.
*/
ScrollPort.prototype.redraw_ = function() {
hterm.ScrollPort.prototype.redraw_ = function() {
this.resetSelectBags_();
this.selection_.sync();
this.syncScrollHeight();
this.currentRowNodeCache_ = {};
var topRowIndex = this.getTopRowIndex();
......@@ -376,8 +449,8 @@ ScrollPort.prototype.redraw_ = function() {
* Ensure that the nodes above the top fold are as they should be.
*
* If the selection start and/or end nodes are above the visible range
* of this ScrollPort then the dom will be adjusted so that they appear before
* the top fold (the first x-fold element, aka this.topFold).
* of this hterm.ScrollPort then the dom will be adjusted so that they appear
* before the top fold (the first x-fold element, aka this.topFold).
*
* If not, the top fold will be the first element.
*
......@@ -385,7 +458,7 @@ ScrollPort.prototype.redraw_ = function() {
* so would clear the current selection. Instead, the rest of the DOM is
* adjusted around them.
*/
ScrollPort.prototype.drawTopFold_ = function(topRowIndex) {
hterm.ScrollPort.prototype.drawTopFold_ = function(topRowIndex) {
if (!this.selection_.startRow ||
this.selection_.startRow.rowIndex >= topRowIndex) {
// Selection is entirely below the top fold, just make sure the fold is
......@@ -425,8 +498,8 @@ ScrollPort.prototype.drawTopFold_ = function(topRowIndex) {
* Ensure that the nodes below the bottom fold are as they should be.
*
* If the selection start and/or end nodes are below the visible range
* of this ScrollPort then the dom will be adjusted so that they appear after
* the bottom fold (the second x-fold element, aka this.bottomFold).
* of this hterm.ScrollPort then the dom will be adjusted so that they appear
* after the bottom fold (the second x-fold element, aka this.bottomFold).
*
* If not, the bottom fold will be the last element.
*
......@@ -434,7 +507,7 @@ ScrollPort.prototype.drawTopFold_ = function(topRowIndex) {
* so would clear the current selection. Instead, the rest of the DOM is
* adjusted around them.
*/
ScrollPort.prototype.drawBottomFold_ = function(bottomRowIndex) {
hterm.ScrollPort.prototype.drawBottomFold_ = function(bottomRowIndex) {
if (!this.selection_.endRow ||
this.selection_.endRow.rowIndex <= bottomRowIndex) {
// Selection is entirely above the bottom fold, just make sure the fold is
......@@ -484,7 +557,8 @@ ScrollPort.prototype.drawBottomFold_ = function(bottomRowIndex) {
* so would clear the current selection. Instead, the rest of the DOM is
* adjusted around them.
*/
ScrollPort.prototype.drawVisibleRows_ = function(topRowIndex, bottomRowIndex) {
hterm.ScrollPort.prototype.drawVisibleRows_ = function(
topRowIndex, bottomRowIndex) {
var self = this;
// Keep removing nodes, starting with currentNode, until we encounter
......@@ -511,12 +585,20 @@ ScrollPort.prototype.drawVisibleRows_ = function(topRowIndex, bottomRowIndex) {
// The node we're examining during the current iteration.
var node = this.topFold_.nextSibling;
for (var drawCount = 0; drawCount < this.visibleRowCount; drawCount++) {
var targetDrawCount = Math.min(this.visibleRowCount,
this.rowProvider_.getRowCount());
for (var drawCount = 0; drawCount < targetDrawCount; drawCount++) {
var rowIndex = topRowIndex + drawCount;
if (node == bottomFold) {
// We've hit the bottom fold, we need to insert a new row.
var newNode = this.fetchRowNode_(rowIndex);
if (!newNode) {
console.log("Couldn't fetch row index: " + rowIndex);
break;
}
this.rowNodes_.insertBefore(newNode, node);
continue;
}
......@@ -547,6 +629,11 @@ ScrollPort.prototype.drawVisibleRows_ = function(topRowIndex, bottomRowIndex) {
// We encountered the start/end of the selection, but we don't want it
// yet. Insert a new row instead.
var newNode = this.fetchRowNode_(rowIndex);
if (!newNode) {
console.log("Couldn't fetch row index: " + rowIndex);
break;
}
this.rowNodes_.insertBefore(newNode, node);
continue;
}
......@@ -554,7 +641,19 @@ ScrollPort.prototype.drawVisibleRows_ = function(topRowIndex, bottomRowIndex) {
// There is nothing special about this node, but it's in our way. Replace
// it with the node that should be here.
var newNode = this.fetchRowNode_(rowIndex);
if (!newNode) {
console.log("Couldn't fetch row index: " + rowIndex);
break;
}
if (node == newNode) {
node = node.nextSibling;
continue;
}
this.rowNodes_.insertBefore(newNode, node);
if (!newNode.nextSibling)
debugger;
this.rowNodes_.removeChild(node);
node = newNode.nextSibling;
}
......@@ -570,7 +669,7 @@ ScrollPort.prototype.drawVisibleRows_ = function(topRowIndex, bottomRowIndex) {
* when that text is otherwise off screen. They are filled out in the
* onCopy_ event.
*/
ScrollPort.prototype.resetSelectBags_ = function() {
hterm.ScrollPort.prototype.resetSelectBags_ = function() {
if (this.topSelectBag_.parentNode) {
this.topSelectBag_.textContent = '';
this.topSelectBag_.parentNode.removeChild(this.topSelectBag_);
......@@ -590,7 +689,7 @@ ScrollPort.prototype.resetSelectBags_ = function() {
* so that the first node *after* the top fold is always the first visible
* DOM node.
*/
ScrollPort.prototype.syncRowNodesTop_ = function() {
hterm.ScrollPort.prototype.syncRowNodesTop_ = function() {
var topMargin = 0;
var node = this.topFold_.previousSibling;
while (node) {
......@@ -606,7 +705,7 @@ ScrollPort.prototype.syncRowNodesTop_ = function() {
*
* This method may only be used during a redraw_.
*/
ScrollPort.prototype.cacheRowNode_ = function(rowNode) {
hterm.ScrollPort.prototype.cacheRowNode_ = function(rowNode) {
this.currentRowNodeCache_[rowNode.rowIndex] = rowNode;
};
......@@ -618,7 +717,7 @@ ScrollPort.prototype.cacheRowNode_ = function(rowNode) {
*
* If a redraw_ is in progress the row will be added to the current cache.
*/
ScrollPort.prototype.fetchRowNode_ = function(rowIndex) {
hterm.ScrollPort.prototype.fetchRowNode_ = function(rowIndex) {
var node;
if (this.previousRowNodeCache_ && rowIndex in this.previousRowNodeCache_) {
......@@ -636,7 +735,7 @@ ScrollPort.prototype.fetchRowNode_ = function(rowIndex) {
/**
* Select all rows in the viewport.
*/
ScrollPort.prototype.selectAll = function() {
hterm.ScrollPort.prototype.selectAll = function() {
var firstRow;
if (this.topFold_.nextSibling.rowIndex != 0) {
......@@ -675,17 +774,19 @@ ScrollPort.prototype.selectAll = function() {
/**
* Return the maximum scroll position in pixels.
*/
ScrollPort.prototype.getScrollMax_ = function(e) {
hterm.ScrollPort.prototype.getScrollMax_ = function(e) {
return (this.scrollArea_.clientHeight + this.visibleRowTopMargin -
this.screen_.clientHeight);
};
/**
* Scroll the given rowIndex to the top of the ScrollPort.
* Scroll the given rowIndex to the top of the hterm.ScrollPort.
*
* @param {integer} rowIndex Index of the target row.
*/
ScrollPort.prototype.scrollRowToTop = function(rowIndex) {
hterm.ScrollPort.prototype.scrollRowToTop = function(rowIndex) {
this.syncScrollHeight();
var scrollTop = rowIndex * this.rowHeight_ + this.visibleRowTopMargin;
var scrollMax = this.getScrollMax_();
......@@ -697,11 +798,13 @@ ScrollPort.prototype.scrollRowToTop = function(rowIndex) {
};
/**
* Scroll the given rowIndex to the bottom of the ScrollPort.
* Scroll the given rowIndex to the bottom of the hterm.ScrollPort.
*
* @param {integer} rowIndex Index of the target row.
*/
ScrollPort.prototype.scrollRowToBottom = function(rowIndex) {
hterm.ScrollPort.prototype.scrollRowToBottom = function(rowIndex) {
this.syncScrollHeight();
var scrollTop = rowIndex * this.rowHeight_ + this.visibleRowTopMargin;
scrollTop -= (this.visibleRowCount - 1) * this.rowHeight_;
......@@ -718,7 +821,7 @@ ScrollPort.prototype.scrollRowToBottom = function(rowIndex) {
* This is based on the scroll position. If a redraw_ is in progress this
* returns the row that *should* be at the top.
*/
ScrollPort.prototype.getTopRowIndex = function() {
hterm.ScrollPort.prototype.getTopRowIndex = function() {
return Math.floor(this.screen_.scrollTop / this.rowHeight_);
};
......@@ -728,29 +831,30 @@ ScrollPort.prototype.getTopRowIndex = function() {
* This is based on the scroll position. If a redraw_ is in progress this
* returns the row that *should* be at the bottom.
*/
ScrollPort.prototype.getBottomRowIndex = function(topRowIndex) {
hterm.ScrollPort.prototype.getBottomRowIndex = function(topRowIndex) {
return topRowIndex + this.visibleRowCount - 1;
};
/**
* Handler for scroll events.
*
* The onScroll event fires when the user moves the scrollbar associated with
* this ScrollPort.
* The onScroll event fires when scrollArea's scrollTop property changes. This
* may be due to the user manually move the scrollbar, or a programmatic change.
*/
ScrollPort.prototype.onScroll_ = function(e) {
hterm.ScrollPort.prototype.onScroll_ = function(e) {
this.redraw_();
this.publish('scroll', { scrollPort: this });
};
/**
* Handler for scroll-wheel events.
*
* The onScrollWheel event fires when the user moves their scrollwheel over this
* ScrollPort. Because the frontmost element in the ScrollPort is a fixed
* position DIV, the scroll wheel does nothing by default. Instead, we have
* to handle it manually.
* hterm.ScrollPort. Because the frontmost element in the hterm.ScrollPort is
* a fixed position DIV, the scroll wheel does nothing by default. Instead, we
* have to handle it manually.
*/
ScrollPort.prototype.onScrollWheel_ = function(e) {
hterm.ScrollPort.prototype.onScrollWheel_ = function(e) {
var top = this.screen_.scrollTop - e.wheelDeltaY;
if (top < 0)
top = 0;
......@@ -769,10 +873,8 @@ ScrollPort.prototype.onScrollWheel_ = function(e) {
* The browser will resize us such that the top row stays at the top, but we
* prefer to the bottom row to stay at the bottom.
*/
ScrollPort.prototype.onResize = function(e) {
var index = this.bottomFold_.previousSibling.rowIndex;
hterm.ScrollPort.prototype.onResize = function(e) {
this.resize();
this.scrollRowToBottom(index);
};
/**
......@@ -783,7 +885,7 @@ ScrollPort.prototype.onResize = function(e) {
* if we're missing some of the selected text, and if so populates one or both
* of the "select bags" with the missing text.
*/
ScrollPort.prototype.onCopy_ = function(e) {
hterm.ScrollPort.prototype.onCopy_ = function(e) {
this.resetSelectBags_();
this.selection_.sync();
......
......@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
ScrollPort.Tests = new TestManager.Suite('ScrollPort.Tests');
hterm.ScrollPort.Tests = new TestManager.Suite('hterm.ScrollPort.Tests');
ScrollPort.Tests.prototype.setup = function(cx) {
hterm.ScrollPort.Tests.prototype.setup = function(cx) {
this.setDefaults(cx,
{ visibleColumnCount: 80,
visibleRowCount: 25,
......@@ -25,7 +25,7 @@ ScrollPort.Tests.prototype.setup = function(cx) {
div.style.width = '100%';
document.body.appendChild(div);
this.scrollPort = new ScrollPort(this.rowProvider,
this.scrollPort = new hterm.ScrollPort(this.rowProvider,
this.fontSize, this.lineHeight);
this.scrollPort.decorate(div);
};
......@@ -34,7 +34,7 @@ ScrollPort.Tests.prototype.setup = function(cx) {
* Ensure the selection is collapsed, row caching is on, and we're at the
* top of the scroll port.
*/
ScrollPort.Tests.prototype.preamble = function(result, cx) {
hterm.ScrollPort.Tests.prototype.preamble = function(result, cx) {
var selection = cx.window.getSelection();
if (!selection.isCollapsed)
selection.collapseToStart();
......@@ -49,7 +49,7 @@ ScrollPort.Tests.prototype.preamble = function(result, cx) {
* Basic test to make sure that the viewport contains the right number of
* rows at the right places after some scrolling.
*/
ScrollPort.Tests.addTest('basic-scroll', function(result, cx) {
hterm.ScrollPort.Tests.addTest('basic-scroll', function(result, cx) {
var topRow = this.scrollPort.getTopRowIndex();
result.assertEQ(topRow, 0);
result.assertEQ(this.scrollPort.getBottomRowIndex(topRow),
......@@ -66,9 +66,9 @@ ScrollPort.Tests.addTest('basic-scroll', function(result, cx) {
});
/**
* Make sure the ScrollPort is reusing the same row nodes when it can.
* Make sure the hterm.ScrollPort is reusing the same row nodes when it can.
*/
ScrollPort.Tests.addTest('node-recycler', function(result, cx) {
hterm.ScrollPort.Tests.addTest('node-recycler', function(result, cx) {
this.rowProvider.resetCallCount('getRowNode');
this.scrollPort.scrollRowToTop(1);
var count = this.rowProvider.getCallCount('getRowNode');
......@@ -82,7 +82,7 @@ ScrollPort.Tests.addTest('node-recycler', function(result, cx) {
/**
* Make sure the selection is maintained even after scrolling off screen.
*/
ScrollPort.Tests.addTest('scroll-selection', function(result, cx) {
hterm.ScrollPort.Tests.addTest('scroll-selection', function(result, cx) {
var doc = this.scrollPort.getDocument();
// Scroll into a part of the buffer that can be scrolled off the top
......@@ -122,7 +122,7 @@ ScrollPort.Tests.addTest('scroll-selection', function(result, cx) {
/**
* Test the select-all function.
*/
ScrollPort.Tests.addTest('select-all', function(result, cx) {
hterm.ScrollPort.Tests.addTest('select-all', function(result, cx) {
this.scrollPort.selectAll();
result.assertEQ(0, this.scrollPort.selection_.startRow.rowIndex);
result.assertEQ(this.totalRowCount - 1,
......@@ -137,7 +137,7 @@ ScrollPort.Tests.addTest('select-all', function(result, cx) {
* This should always be the last test of the suite, since it leaves the user
* with a full page scrollPort to poke at.
*/
ScrollPort.Tests.addTest('fullscreen', function(result, cx) {
hterm.ScrollPort.Tests.addTest('fullscreen', function(result, cx) {
var document = cx.window.document;
document.body.innerHTML = '';
......@@ -150,7 +150,7 @@ ScrollPort.Tests.addTest('fullscreen', function(result, cx) {
div.style.width = '100%';
document.body.appendChild(div);
this.scrollPort = new ScrollPort(this.rowProvider,
this.scrollPort = new hterm.ScrollPort(this.rowProvider,
this.fontSize, this.lineHeight);
this.scrollPort.decorate(div);
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Constructor for the Terminal class.
*
* A Terminal pulls together the hterm.ScrollPort, hterm.Screen and hterm.VT100
* classes to provide the complete terminal functionality.
*
* There are a number of lower-level Terminal methods that can be called
* directly to manipulate the cursor, text, scroll region, and other terminal
* attributes. However, the primary method is interpret(), which parses VT
* escape sequences and invokes the appropriate Terminal methods.
*
* This class was heavily influenced by Cory Maccarrone's Framebuffer class.
*
* TODO(rginda): Eventually we're going to need to support characters which are
* displayed twice as wide as standard latin characters. This is to support
* CJK (and possibly other character sets).
*/
hterm.Terminal = function() {
// Two screen instances.
this.primaryScreen_ = new hterm.Screen();
this.alternateScreen_ = new hterm.Screen();
// The "current" screen.
this.screen_ = this.primaryScreen_;
// The VT escape sequence interpreter.
this.vt100_ = new hterm.VT100(this);
// The local notion of the screen size. ScreenBuffers also have a size which
// indicates their present size. During size changes, the two may disagree.
// Also, the inactive screen's size is not altered until it is made the active
// screen.
this.screenSize = new hterm.Size(0, 0);
// The pixel dimensions of a single character on the screen.
this.characterSize_ = new hterm.Size(0, 0);
// The scroll port we'll be using to display the visible rows.
this.scrollPort_ = new hterm.ScrollPort(this, 15);
this.scrollPort_.subscribe('resize', this.onResize_.bind(this));
this.scrollPort_.subscribe('scroll', this.onScroll_.bind(this));
// The rows that have scrolled off screen and are no longer addressable.
this.scrollbackRows_ = [];
// The VT's notion of the top and bottom rows. Used during some VT
// cursor positioning and scrolling commands.
this.vtScrollTop_ = null;
this.vtScrollBottom_ = null;
// The DIV element for the visible cursor.
this.cursorNode_ = null;
// The default colors for text with no other color attributes.
this.backgroundColor = 'black';
this.foregroundColor = 'white';
// The color of the cursor.
this.cursorColor = 'rgba(255,0,0,0.5)';
// The current mode bits for the terminal.
this.options_ = new hterm.Options();
// Timeouts we might need to clear.
this.timeouts_ = {};
};
/**
* Methods called by Cory's vt100 interpreter which we haven't implemented yet.
*/
hterm.Terminal.prototype.reset =
hterm.Terminal.prototype.clearColorAndAttributes =
hterm.Terminal.prototype.setForegroundColor256 =
hterm.Terminal.prototype.setBackgroundColor256 =
hterm.Terminal.prototype.setForegroundColor =
hterm.Terminal.prototype.setBackgroundColor =
hterm.Terminal.prototype.setAttributes =
hterm.Terminal.prototype.resize =
hterm.Terminal.prototype.setSpecialCharactersEnabled =
hterm.Terminal.prototype.setTabStopAtCursor =
hterm.Terminal.prototype.clearTabStops =
hterm.Terminal.prototype.saveCursor =
hterm.Terminal.prototype.restoreCursor =
hterm.Terminal.prototype.reverseLineFeed = function() {
throw 'NOT IMPLEMENTED';
};
/**
* Interpret a sequence of characters.
*
* Incomplete escape sequences are buffered until the next call.
*
* @param {string} str Sequence of characters to interpret or pass through.
*/
hterm.Terminal.prototype.interpret = function(str) {
this.vt100_.interpretString(str);
this.scheduleSyncCursorPosition_();
};
/**
* Take over the given DIV for use as the terminal display.
*
* @param {HTMLDivElement} div The div to use as the terminal display.
*/
hterm.Terminal.prototype.decorate = function(div) {
this.scrollPort_.decorate(div);
this.document_ = this.scrollPort_.getDocument();
// Get character dimensions from the scrollPort.
this.characterSize_.height = this.scrollPort_.getRowHeight();
this.characterSize_.width = this.scrollPort_.getCharacterWidth();
this.cursorNode_ = this.document_.createElement('div');
this.cursorNode_.style.cssText =
('position: absolute;' +
'display: none;' +
'width: ' + this.characterSize_.width + 'px;' +
'height: ' + this.characterSize_.height + 'px;' +
'background-color: ' + this.cursorColor);
this.document_.body.appendChild(this.cursorNode_);
this.setReverseVideo(false);
};
/**
* Return the HTML Element for a given row index.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch rows on demand as they are scrolled into view.
*
* TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
* pairs to conserve memory.
*
* @param {integer} index The zero-based row index, measured relative to the
* start of the scrollback buffer. On-screen rows will always have the
* largest indicies.
* @return {HTMLElement} The 'x-row' element containing for the requested row.
*/
hterm.Terminal.prototype.getRowNode = function(index) {
if (index < this.scrollbackRows_.length)
return this.scrollbackRows_[index];
var screenIndex = index - this.scrollbackRows_.length;
return this.screen_.rowsArray[screenIndex];
};
/**
* Return the text content for a given range of rows.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch text content on demand when the user attempts to copy their
* selection to the clipboard.
*
* @param {integer} start The zero-based row index to start from, measured
* relative to the start of the scrollback buffer. On-screen rows will
* always have the largest indicies.
* @param {integer} end The zero-based row index to end on, measured
* relative to the start of the scrollback buffer.
* @return {string} A single string containing the text value of the range of
* rows. Lines will be newline delimited, with no trailing newline.
*/
hterm.Terminal.prototype.getRowsText = function(start, end) {
var ary = [];
for (var i = start; i < end; i++) {
var node = this.getRowNode(i);
ary.push(node.textContent);
}
return ary.join('\n');
};
/**
* Return the text content for a given row.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch text content on demand when the user attempts to copy their
* selection to the clipboard.
*
* @param {integer} index The zero-based row index to return, measured
* relative to the start of the scrollback buffer. On-screen rows will
* always have the largest indicies.
* @return {string} A string containing the text value of the selected row.
*/
hterm.Terminal.prototype.getRowText = function(index) {
var node = this.getRowNode(index);
return row.textContent;
};
/**
* Return the total number of rows in the addressable screen and in the
* scrollback buffer of this terminal.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to compute the size of the scrollbar.
*
* @return {integer} The number of rows in this terminal.
*/
hterm.Terminal.prototype.getRowCount = function() {
return this.scrollbackRows_.length + this.screen_.rowsArray.length;
};
/**
* Create DOM nodes for new rows and append them to the end of the terminal.
*
* This is the only correct way to add a new DOM node for a row. Notice that
* the new row is appended to the bottom of the list of rows, and does not
* require renumbering (of the rowIndex property) of previous rows.
*
* If you think you want a new blank row somewhere in the middle of the
* terminal, look into moveRows_().
*
* This method does not pay attention to vtScrollTop/Bottom, since you should
* be using moveRows() in cases where they would matter.
*
* The cursor will be positioned at column 0 of the first inserted line.
*/
hterm.Terminal.prototype.appendRows_ = function(count) {
var cursorRow = this.screen_.rowsArray.length;
var offset = this.scrollbackRows_.length + cursorRow;
for (var i = 0; i < count; i++) {
var row = this.document_.createElement('x-row');
row.appendChild(this.document_.createTextNode(''));
row.rowIndex = offset + i;
this.screen_.pushRow(row);
}
var extraRows = this.screen_.rowsArray.length - this.screenSize.height;
if (extraRows > 0) {
var ary = this.screen_.shiftRows(extraRows);
Array.prototype.push.apply(this.scrollbackRows_, ary);
this.scheduleScrollDown_();
}
if (cursorRow >= this.screen_.rowsArray.length)
cursorRow = this.screen_.rowsArray.length - 1;
this.screen_.setCursorPosition(cursorRow, 0);
};
/**
* Relocate rows from one part of the addressable screen to another.
*
* This is used to recycle rows during VT scrolls (those which are driven
* by VT commands, rather than by the user manipulating the scrollbar.)
*
* In this case, the blank lines scrolled into the scroll region are made of
* the nodes we scrolled off. These have their rowIndex properties carefully
* renumbered so as not to confuse the ScrollPort.
*
* TODO(rginda): I'm not sure why this doesn't require a scrollport repaint.
* It may just be luck. I wouldn't be surprised if we actually needed to call
* scrollPort_.invalidateRowRange, but I'm going to wait for evidence before
* adding it.
*/
hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
var ary = this.screen_.removeRows(fromIndex, count);
this.screen_.insertRows(toIndex, ary);
var start, end;
if (fromIndex < toIndex) {
start = fromIndex;
end = fromIndex + count;
} else {
start = toIndex;
end = toIndex + count;
}
this.renumberRows_(start, end);
};
/**
* Renumber the rowIndex property of the given range of rows.
*
* The start and end indicies are relative to the screen, not the scrollback.
* Rows in the scrollback buffer cannot be renumbered. Since they are not
* addressable (you cant delete them, scroll them, etc), you should have
* no need to renumber scrollback rows.
*/
hterm.Terminal.prototype.renumberRows_ = function(start, end) {
var offset = this.scrollbackRows_.length;
for (var i = start; i < end; i++) {
this.screen_.rowsArray[i].rowIndex = offset + i;
}
};
/**
* Print a string to the terminal.
*
* This respects the current insert and wraparound modes. It will add new lines
* to the end of the terminal, scrolling off the top into the scrollback buffer
* if necessary.
*
* The string is *not* parsed for escape codes. Use the interpret() method if
* that's what you're after.
*
* @param{string} str The string to print.
*/
hterm.Terminal.prototype.print = function(str) {
do {
if (this.options_.insertMode) {
str = this.screen_.insertString(str);
} else {
str = this.screen_.overwriteString(str);
}
if (this.options_.wraparound && str) {
this.newLine();
} else {
break;
}
} while (str);
this.scheduleSyncCursorPosition_();
};
/**
* Return the top row index according to the VT.
*
* This will return 0 unless the terminal has been told to restrict scrolling
* to some lower row. It is used for some VT cursor positioning and scrolling
* commands.
*
* @return {integer} The topmost row in the terminal's scroll region.
*/
hterm.Terminal.prototype.getVTScrollTop = function() {
if (this.vtScrollTop_ != null)
return this.vtScrollTop_;
return 0;
}
/**
* Return the bottom row index according to the VT.
*
* This will return the height of the terminal unless the it has been told to
* restrict scrolling to some higher row. It is used for some VT cursor
* positioning and scrolling commands.
*
* @return {integer} The bottommost row in the terminal's scroll region.
*/
hterm.Terminal.prototype.getVTScrollBottom = function() {
if (this.vtScrollBottom_ != null)
return this.vtScrollBottom_;
return this.screenSize.height;
}
/**
* Process a '\n' character.
*
* If the cursor is on the final row of the terminal this will append a new
* blank row to the screen and scroll the topmost row into the scrollback
* buffer.
*
* Otherwise, this moves the cursor to column zero of the next row.
*/
hterm.Terminal.prototype.newLine = function() {
if (this.screen_.cursorPosition.row == this.screen_.rowsArray.length - 1) {
this.appendRows_(1);
} else {
this.screen_.setCursorPosition(this.screen_.cursorPosition.row + 1, 0);
}
};
/**
* Like newLine(), except maintain the cursor column.
*/
hterm.Terminal.prototype.lineFeed = function() {
var column = this.screen_.cursorPosition.column;
this.newLine();
this.setCursorColumn(column);
};
/**
* Replace all characters to the left of the current cursor with the space
* character.
*
* TODO(rginda): This should probably *remove* the characters (not just replace
* with a space) if there are no characters at or beyond the current cursor
* position. Once it does that, it'll have the same text-attribute related
* issues as hterm.Screen.prototype.clearCursorRow :/
*/
hterm.Terminal.prototype.eraseToLeft = function() {
var currentColumn = this.screen_.cursorPosition.column;
this.setCursorColumn(0);
this.screen_.overwriteString(hterm.getWhitespace(currentColumn + 1));
this.setCursorColumn(currentColumn);
};
/**
* Erase a given number of characters to the right of the cursor, shifting
* remaining characters to the left.
*
* The cursor position is unchanged.
*
* TODO(rginda): Test that this works even when the cursor is positioned beyond
* the end of the text.
*
* TODO(rginda): This likely has text-attribute related troubles similar to the
* todo on hterm.Screen.prototype.clearCursorRow.
*/
hterm.Terminal.prototype.eraseToRight = function(opt_count) {
var currentColumn = this.screen_.cursorPosition.column;
var maxCount = this.screenSize.width - currentColumn;
var count = (opt_count && opt_count < maxCount) ? opt_count : maxCount;
this.screen_.deleteChars(count);
this.setCursorColumn(currentColumn);
};
/**
* Erase the current line.
*
* The cursor position is unchanged.
*
* TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
* has a text-attribute related TODO.
*/
hterm.Terminal.prototype.eraseLine = function() {
var currentColumn = this.screen_.cursorPosition.column;
this.screen_.clearCursorRow();
this.setCursorColumn(currentColumn);
};
/**
* Erase all characters from the start of the scroll region to the current
* cursor position.
*
* The cursor position is unchanged.
*
* TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
* has a text-attribute related TODO.
*/
hterm.Terminal.prototype.eraseAbove = function() {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
var top = this.getVTScrollTop();
for (var i = top; i < currentRow; i++) {
this.screen_.setCursorPosition(i, 0);
this.screen_.clearCursorRow();
}
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* Erase all characters from the current cursor position to the end of the
* scroll region.
*
* The cursor position is unchanged.
*
* TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
* has a text-attribute related TODO.
*/
hterm.Terminal.prototype.eraseBelow = function() {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
var bottom = this.getVTScrollBottom();
for (var i = currentRow + 1; i < bottom; i++) {
this.screen_.setCursorPosition(i, 0);
this.screen_.clearCursorRow();
}
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* Erase the entire scroll region.
*
* The cursor position is unchanged.
*
* TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
* has a text-attribute related TODO.
*/
hterm.Terminal.prototype.clear = function() {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
var top = this.getVTScrollTop();
var bottom = this.getVTScrollBottom();
for (var i = top; i < bottom; i++) {
this.screen_.setCursorPosition(i, 0);
this.screen_.clearCursorRow();
}
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* VT command to insert lines at the current cursor row.
*
* This respects the current scroll region. Rows pushed off the bottom are
* lost (they won't show up in the scrollback buffer).
*
* TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
* has a text-attribute related TODO.
*
* @param {integer} count The number of lines to insert.
*/
hterm.Terminal.prototype.insertLines = function(count) {
var currentRow = this.screen_.cursorPosition.row;
var bottom = this.getVTScrollBottom();
count = Math.min(count, bottom - currentRow);
var start = bottom - count;
if (start != currentRow)
this.moveRows_(start, count, currentRow);
for (var i = 0; i < count; i++) {
this.screen_.setCursorPosition(currentRow + i, 0);
this.screen_.clearCursorRow();
}
this.screen_.setCursorPosition(currentRow, 0);
};
/**
* VT command to delete lines at the current cursor row.
*
* New rows are added to the bottom of scroll region to take their place. New
* rows are strictly there to take up space and have no content or style.
*/
hterm.Terminal.prototype.deleteLines = function(count) {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
var top = currentRow;
var bottom = this.getVTScrollBottom();
var maxCount = bottom - top;
count = Math.min(count, maxCount);
var moveStart = bottom - count;
if (count != maxCount)
this.moveRows_(top, count, moveStart);
for (var i = 0; i < count; i++) {
this.screen_.setCursorPosition(moveStart + i, 0);
this.screen_.clearCursorRow();
}
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* Inserts the given number of spaces at the current cursor position.
*
* The cursor is left at the end of the inserted spaces.
*/
hterm.Terminal.prototype.insertSpace = function(count) {
var ws = hterm.getWhitespace(count);
this.screen_.insertString(ws);
};
/**
* Forward-delete the specified number of characters starting at the cursor
* position.
*
* @param {integer} count The number of characters to delete.
*/
hterm.Terminal.prototype.deleteChars = function(count) {
this.screen_.deleteChars(count);
};
/**
* Shift rows in the scroll region upwards by a given number of lines.
*
* New rows are inserted at the bottom of the scroll region to fill the
* vacated rows. The new rows not filled out with the current text attributes.
*
* This function does not affect the scrollback rows at all. Rows shifted
* off the top are lost.
*
* @param {integer} count The number of rows to scroll.
*/
hterm.Terminal.prototype.vtScrollUp = function(count) {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
this.setCursorRow(this.getVTScrollTop());
this.deleteLines(count);
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* Shift rows below the cursor down by a given number of lines.
*
* This function respects the current scroll region.
*
* New rows are inserted at the top of the scroll region to fill the
* vacated rows. The new rows not filled out with the current text attributes.
*
* This function does not affect the scrollback rows at all. Rows shifted
* off the bottom are lost.
*
* @param {integer} count The number of rows to scroll.
*/
hterm.Terminal.prototype.vtScrollDown = function(opt_count) {
var currentRow = this.screen_.cursorPosition.row;
var currentColumn = this.screen_.cursorPosition.column;
this.setCursorRow(this.getVTScrollTop());
this.insertLines(opt_count);
this.screen_.setCursorPosition(currentRow, currentColumn);
};
/**
* Set the cursor position.
*
* The cursor row is relative to the scroll region if the terminal has
* 'origin mode' enabled, or relative to the addressable screen otherwise.
*
* @param {integer} row The new zero-based cursor row.
* @param {integer} row The new zero-based cursor column.
*/
hterm.Terminal.prototype.setCursorPosition = function(row, column) {
if (this.options_.originMode) {
var scrollTop = this.getScrollTop();
row = hterm.clamp(row + scrollTop, scrollTop, this.getScrollBottom());
} else {
row = hterm.clamp(row, 0, this.screenSize.height);
}
this.screen_.setCursorPosition(row, column);
};
/**
* Set the cursor column.
*
* @param {integer} column The new zero-based cursor column.
*/
hterm.Terminal.prototype.setCursorColumn = function(column) {
this.screen_.setCursorPosition(this.screen_.cursorPosition.row, column);
};
/**
* Return the cursor column.
*
* @return {integer} The zero-based cursor column.
*/
hterm.Terminal.prototype.getCursorColumn = function() {
return this.screen_.cursorPosition.column;
};
/**
* Set the cursor row.
*
* The cursor row is relative to the scroll region if the terminal has
* 'origin mode' enabled, or relative to the addressable screen otherwise.
*
* @param {integer} row The new cursor row.
*/
hterm.Terminal.prototype.setCursorRow = function(row) {
this.setCursorPosition(row, this.screen_.cursorPosition.column);
};
/**
* Return the cursor row.
*
* @return {integer} The zero-based cursor row.
*/
hterm.Terminal.prototype.getCursorRow = function(row) {
return this.screen_.cursorPosition.row;
};
/**
* Request that the ScrollPort redraw itself soon.
*
* The redraw will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single redraw.
*/
hterm.Terminal.prototype.scheduleRedraw_ = function() {
if (this.redrawTimeout_)
clearTimeout(this.redrawTimeout_);
var self = this;
setTimeout(function() {
self.redrawTimeout_ = null;
self.scrollPort_.redraw_();
}, 0);
};
/**
* Request that the ScrollPort be scrolled to the bottom.
*
* The scroll will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single scroll.
*
* This affects the scrollbar position of the ScrollPort, and has nothing to
* do with the VT scroll commands.
*/
hterm.Terminal.prototype.scheduleScrollDown_ = function() {
if (this.timeouts_.scrollDown)
clearTimeout(this.timeouts_.scrollDown);
var self = this;
this.timeouts_.scrollDown = setTimeout(function() {
delete self.timeouts_.scrollDown;
self.scrollPort_.scrollRowToBottom(self.getRowCount());
}, 10);
};
/**
* Move the cursor up a specified number of rows.
*
* @param {integer} count The number of rows to move the cursor.
*/
hterm.Terminal.prototype.cursorUp = function(count) {
return this.cursorDown(-count);
};
/**
* Move the cursor down a specified number of rows.
*
* @param {integer} count The number of rows to move the cursor.
*/
hterm.Terminal.prototype.cursorDown = function(count) {
var minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
var maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
this.screenSize.height - 1);
var row = hterm.clamp(this.screen_.cursorPosition.row + count,
minHeight, maxHeight);
this.setCursorRow(row);
};
/**
* Move the cursor left a specified number of columns.
*
* @param {integer} count The number of columns to move the cursor.
*/
hterm.Terminal.prototype.cursorLeft = function(count) {
return this.cursorRight(-count);
};
/**
* Move the cursor right a specified number of columns.
*
* @param {integer} count The number of columns to move the cursor.
*/
hterm.Terminal.prototype.cursorRight = function(count) {
var column = hterm.clamp(this.screen_.cursorPosition.column + count,
0, this.screenSize.width);
this.setCursorColumn(column);
};
/**
* Reverse the foreground and background colors of the terminal.
*
* This only affects text that was drawn with no attributes.
*
* TODO(rginda): Test xterm to see if reverse is respected for text that has
* been drawn with attributes that happen to coincide with the default
* 'no-attribute' colors. My guess is probably not.
*/
hterm.Terminal.prototype.setReverseVideo = function(state) {
if (state) {
this.scrollPort_.setForegroundColor(this.backgroundColor);
this.scrollPort_.setBackgroundColor(this.foregroundColor);
} else {
this.scrollPort_.setForegroundColor(this.foregroundColor);
this.scrollPort_.setBackgroundColor(this.backgroundColor);
}
};
/**
* Set the origin mode bit.
*
* If origin mode is on, certain VT cursor and scrolling commands measure their
* row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
* to the top of the addressable screen.
*
* Defaults to off.
*
* @param {boolean} state True to set origin mode, false to unset.
*/
hterm.Terminal.prototype.setOriginMode = function(state) {
this.options_.originMode = state;
};
/**
* Set the insert mode bit.
*
* If insert mode is on, existing text beyond the cursor position will be
* shifted right to make room for new text. Otherwise, new text overwrites
* any existing text.
*
* Defaults to off.
*
* @param {boolean} state True to set insert mode, false to unset.
*/
hterm.Terminal.prototype.setInsertMode = function(state) {
this.options_.insertMode = state;
};
/**
* Set the wraparound mode bit.
*
* If wraparound mode is on, certain VT commands will allow the cursor to wrap
* to the start of the following row. Otherwise, the cursor is clamped to the
* end of the screen and attempts to write past it are ignored.
*
* Defaults to on.
*
* @param {boolean} state True to set wraparound mode, false to unset.
*/
hterm.Terminal.prototype.setWraparound = function(state) {
this.options_.wraparound = state;
};
/**
* Set the reverse-wraparound mode bit.
*
* If wraparound mode is off, certain VT commands will allow the cursor to wrap
* to the end of the previous row. Otherwise, the cursor is clamped to column
* 0.
*
* Defaults to off.
*
* @param {boolean} state True to set reverse-wraparound mode, false to unset.
*/
hterm.Terminal.prototype.setReverseWraparound = function(state) {
this.options_.reverseWraparound = state;
};
/**
* Selects between the primary and alternate screens.
*
* If alternate mode is on, the alternate screen is active. Otherwise the
* primary screen is active.
*
* Swapping screens has no effect on the scrollback buffer.
*
* Each screen maintains its own cursor position.
*
* Defaults to off.
*
* @param {boolean} state True to set alternate mode, false to unset.
*/
hterm.Terminal.prototype.setAlternateMode = function(state) {
this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
this.screen_.setColumnCount(this.screenSize.width);
var rowDelta = this.screenSize.height - this.screen_.getHeight();
if (rowDelta > 0)
this.appendRows_(rowDelta);
this.scrollPort_.invalidateRowRange(
this.scrollbackRows_.length,
this.scrollbackRows_.length + this.screenSize.height);
if (this.screen_.cursorPosition.row == -1)
this.screen_.setCursorPosition(0, 0);
this.syncCursorPosition_();
};
/**
* Set the cursor-blink mode bit.
*
* If cursor-blink is on, the cursor will blink when it is visible. Otherwise
* a visible cursor does not blink.
*
* You should make sure to turn blinking off if you're going to dispose of a
* terminal, otherwise you'll leak a timeout.
*
* Defaults to on.
*
* @param {boolean} state True to set cursor-blink mode, false to unset.
*/
hterm.Terminal.prototype.setCursorBlink = function(state) {
this.options_.cursorBlink = state;
if (!state && this.timeouts_.cursorBlink) {
clearTimeout(this.timeouts_.cursorBlink);
delete this.timeouts_.cursorBlink;
}
if (this.options_.cursorVisible)
this.setCursorVisible(true);
};
/**
* Set the cursor-visible mode bit.
*
* If cursor-visible is on, the cursor will be visible. Otherwise it will not.
*
* Defaults to on.
*
* @param {boolean} state True to set cursor-visible mode, false to unset.
*/
hterm.Terminal.prototype.setCursorVisible = function(state) {
this.options_.cursorVisible = state;
if (!state) {
this.cursorNode_.style.display = 'none';
return;
}
this.cursorNode_.style.display = 'block';
if (this.options_.cursorBlink) {
if (this.timeouts_.cursorBlink)
return;
this.timeouts_.cursorBlink = setInterval(this.onCursorBlink_.bind(this),
500);
} else {
if (this.timeouts_.cursorBlink) {
clearTimeout(this.timeouts_.cursorBlink);
delete this.timeouts_.cursorBlink;
}
}
};
/**
* Synchronizes the visible cursor with the current cursor coordinates.
*/
hterm.Terminal.prototype.syncCursorPosition_ = function() {
var topRowIndex = this.scrollPort_.getTopRowIndex();
var bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
var cursorRowIndex = this.scrollbackRows_.length +
this.screen_.cursorPosition.row;
if (cursorRowIndex > bottomRowIndex) {
// Cursor is scrolled off screen, move it outside of the visible area.
this.cursorNode_.style.top = -this.characterSize_.height;
return;
}
this.cursorNode_.style.top = this.scrollPort_.visibleRowTopMargin +
this.characterSize_.height * (cursorRowIndex - topRowIndex);
this.cursorNode_.style.left = this.characterSize_.width *
this.screen_.cursorPosition.column;
};
/**
* Synchronizes the visible cursor with the current cursor coordinates.
*
* The sync will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single sync.
*/
hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
if (this.timeouts_.syncCursor)
clearTimeout(this.timeouts_.syncCursor);
var self = this;
this.timeouts_.syncCursor = setTimeout(function() {
self.syncCursorPosition_();
delete self.timeouts_.syncCursor;
}, 100);
};
/**
* React when the ScrollPort is scrolled.
*/
hterm.Terminal.prototype.onScroll_ = function() {
this.scheduleSyncCursorPosition_();
};
/**
* React when the ScrollPort is resized.
*/
hterm.Terminal.prototype.onResize_ = function() {
var width = Math.floor(this.scrollPort_.getScreenWidth() /
this.characterSize_.width);
var height = this.scrollPort_.visibleRowCount;
if (width == this.screenSize.width && height == this.screenSize.height)
return;
this.screenSize.resize(width, height);
var screenHeight = this.screen_.getHeight();
var deltaRows = this.screenSize.height - screenHeight;
if (deltaRows < 0) {
// Screen got smaller.
var ary = this.screen_.shiftRows(-deltaRows);
this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
} else if (deltaRows > 0) {
// Screen got larger.
if (deltaRows <= this.scrollbackRows_.length) {
var scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
var rows = this.scrollbackRows_.splice(
0, this.scrollbackRows_.length - scrollbackCount);
this.screen_.unshiftRows(rows);
deltaRows -= scrollbackCount;
}
if (deltaRows)
this.appendRows_(deltaRows);
}
this.screen_.setColumnCount(this.screenSize.width);
if (this.screen_.cursorPosition.row == -1)
this.screen_.setCursorPosition(0, 0);
};
/**
* Service the cursor blink timeout.
*/
hterm.Terminal.prototype.onCursorBlink_ = function() {
if (this.cursorNode_.style.display == 'block') {
this.cursorNode_.style.display = 'none';
} else {
this.cursorNode_.style.display = 'block';
}
};
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview hterm.Terminal unit tests.
*/
hterm.Terminal.Tests = new TestManager.Suite('hterm.Terminal.Tests');
hterm.Terminal.Tests.prototype.setup = function(cx) {
this.setDefaults(cx,
{ visibleColumnCount: 80,
visibleRowCount: 25,
fontSize: 15,
lineHeight: 17,
charWidth: 9,
scrollbarWidth: 16,
});
};
/**
* Clear out the current document and create a new hterm.Terminal object for
* testing.
*
* Called before each test case in this suite.
*/
hterm.Terminal.Tests.prototype.preamble = function(result, cx) {
var document = cx.window.document;
document.body.innerHTML = '';
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.height = this.lineHeight * this.visibleRowCount + 'px';
div.style.width = this.charWidth * this.visibleColumnCount +
this.scrollbarWidth + 'px';
document.body.appendChild(div);
cx.window.terminal = this.terminal = new hterm.Terminal(
this.fontSize, this.lineHeight);
this.terminal.decorate(div);
};
/**
* Overridden addTest method.
*
* Every test in this suite needs to wait for the terminal initialization to
* complete asynchronously. Rather than stick a bunch of biolerplate into each
* test case, we use this overridden addTest method to add a proxy around the
* actual test.
*/
hterm.Terminal.Tests.addTest = function(name, callback) {
function testProxy(result, cx) {
setTimeout(function() {
this.terminal.setCursorPosition(0, 0);
callback.apply(this, [result, cx]);
}, 0);
result.requestTime(200);
}
TestManager.Suite.addTest.apply(this, [name, testProxy]);
};
/**
* Fill the screen with 'X' characters one character at a time, in a way
* that should stress the cursor positioning code.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-cursor-ltr',
function(result, cx) {
for (var col = 0; col < this.visibleColumnCount; col++) {
for (var row = 0; row < this.visibleRowCount; row++) {
console.log(row, col);
this.terminal.screen_.setCursorPosition(row, col);
this.terminal.screen_.insertString('X');
}
}
result.pass();
});
/**
* Fill the screen with 'X' characters one character at a time, in a way
* that should stress the cursor positioning code and the overwriteString()
* code.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-cursor-rtl',
function(result, cx) {
for (var col = this.visibleColumnCount - 1; col >= 0; col--) {
for (var row = 0; row < this.visibleRowCount; row++) {
this.terminal.screen_.setCursorPosition(row, col);
this.terminal.screen_.overwriteString('X');
}
}
result.pass();
});
/**
* Fill the terminal with a lot of text as quickly as possible.
*
* This test doesn't actually assert anything, but the timing data in the test
* log is useful.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-insert',
function(result, cx) {
var chunkSize = 1000;
var testCount = 10;
var self = this;
function test(count) {
for (var i = count * chunkSize; i < (count + 1) * chunkSize; i++) {
if (i != 0)
self.terminal.newLine();
self.terminal.screen_.insertString(
'line ' + i + ': All work and no play makes jack a dull boy.');
}
if (count + 1 >= testCount) {
result.pass();
} else {
result.requestTime(200);
setTimeout(test, 0, count + 1);
}
}
test(0);
});
......@@ -331,6 +331,22 @@ TestManager.Suite.prototype.setup = function(cx) {};
*/
TestManager.Suite.prototype.preamble = function(result, cx) {};
/**
* Subclassable method called to do post-test tear-down.
*
* The default implementation of this method is a no-op. If your test suite
* requires some kind of pre-test setup, this is the place to do it.
*
* This can be used to avoid a bunch of boilerplate setup/teardown code in
* this suite's testcases.
*
* Any exception here will abort the remainder of the test run.
*
* @param {TestManager.Result} result The result object for the upcoming test.
* @param {Object} cx The context object for a test run.
*/
TestManager.Suite.prototype.postamble = function(result, cx) {};
/**
* Object representing a single test in a test suite.
*
......@@ -474,9 +490,9 @@ TestManager.TestRun.prototype.selectTest = function(test) {
this.testQueue_.push(test);
};
TestManager.TestRun.prototype.selectSuite = function(suiteClass, pattern) {
TestManager.TestRun.prototype.selectSuite = function(suiteClass, opt_pattern) {
var pattern = opt_pattern || this.ALL_TESTS;
var selectCount = 0;
var testList = suiteClass.getTestList();
for (var j = 0; j < testList.length; j++) {
......@@ -487,7 +503,7 @@ TestManager.TestRun.prototype.selectSuite = function(suiteClass, pattern) {
if (pattern instanceof RegExp) {
if (!pattern.test(test.testName))
continue;
} else if (testName != pattern) {
} else if (test.testName != pattern) {
continue;
}
}
......@@ -537,7 +553,7 @@ TestManager.TestRun.prototype.onUncaughtException_ = function(
// This is a result.pass() or result.fail() call from a callback. We're
// already going to deal with it as part of the completeTest_() call
// that raised it. We can safely squelch this error message.
return false;
return true;
}
if (!this.currentResult)
......@@ -605,6 +621,14 @@ TestManager.TestRun.prototype.onTestRunComplete_ = function(opt_skipTimeout) {
* completed.
*/
TestManager.TestRun.prototype.onResultComplete = function(result) {
try {
result.suite.postamble();
} catch (ex) {
this.log.println('Unexpected exception in postamble: ' +
(ex.stack ? ex.stack : ex));
this.panic = true;
}
this.log.popPrefix();
this.log.print('} ' + result.status + ', ' +
this.msToSeconds_(result.duration));
......@@ -749,7 +773,7 @@ TestManager.TestRun.prototype.run = function() {
* Format milliseconds as fractional seconds.
*/
TestManager.TestRun.prototype.msToSeconds_ = function(ms) {
var secs = (ms / 100).toFixed(2);
var secs = (ms / 1000).toFixed(2);
return secs + 's';
};
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This file implements the VT100 interpreter, which
* operates in conjunction with a hterm.Terminal to provide
* interpretation of VT100-style control sequences.
*
* Original code by Cory Maccarrone.
*/
/**
* Constructor for the VT100 Interpreter.
*
* The interpreter operates on a terminal object capable of performing cursor
* move operations, painting characters, etc.
*
* @param {hterm.Terminal} terminal Terminal to use with
* the interpreter. Direct commands are sent to it in the presence of
* control characters -- otherwise, normal characters are passed straight
* through to its render functions.
* @constructor
*/
hterm.VT100 = function(terminal) {
this.terminal_ = terminal;
// Sequence being processed -- that seen so far
this.pendingSequence_ = [];
// Response to be sent back to the guest
this.pendingResponse_ = '';
/**
* Enable/disable application keypad.
*
* This changes the way numeric keys are sent from the keyboard.
*/
this.applicationKeypad = false;
/**
* Enable/disable the application cursor mode.
*
* This changes the way cursor keys are sent from the keyboard.
*/
this.applicationCursor = false;
/**
* Whether backspace should send ^H or not.
*/
this.backspaceSendsBackspace = false;
/**
* Set whether the alt key sends a leading escape or not.
*/
this.altSendsEscape = true;
/**
* Set whether the meta key sends a leading escape or not.
*/
this.metaSendsEscape = true;
};
/**
* Interpret a sequence of characters.
*
* Incomplete escape sequences are buffered until the next call.
*
* @param {string} str Sequence of characters to interpret or pass through.
*/
hterm.VT100.prototype.interpretString = function(str) {
var i = 0;
while (i < str.length) {
while (this.pendingSequence_.length && i < str.length) {
this.interpretCharacter(str.substr(i, 1));
i++;
}
if (i == str.length)
break;
var nextEscape = str.substr(i).search(/[\x1b\n\t]|$/);
if (nextEscape == -1)
nextEscape = str.length;
if (nextEscape != 0) {
var plainText = str.substr(i, nextEscape);
this.terminal_.print(plainText);
i += nextEscape;
}
if (i == str.length)
break;
this.interpretCharacter(str.substr(i, 1));
i++;
}
};
/**
* Interpret a single character in a sequence.
*
* This function is called for each character in terminal input, and
* accumulates characters until a recognized control sequence is read. If the
* character is not part of a control sequence, it is queued up for rendering.
*
* @param {string} character Character to interpret or pass through.
*/
hterm.VT100.prototype.interpretCharacter = function(character) {
var interpret = false;
if (character == '\n') {
this.terminal_.newLine();
return;
}
if (character == '\t') {
// TODO(rginda): I don't think this is the correct behavior.
this.terminal_.cursorRight(4);
return;
}
if (character == '\x1b') {
this.pendingSequence_.length = 1;
this.pendingSequence_[0] = character;
return;
}
if (!this.pendingSequence_.length ||
(character < '\x20' && character != '\x07')) {
// We don't have a pending escape, or this character is invalid in the
// context of an escape sequence. The VT100 spec says to just print it.
this.terminal_.print(character);
return;
}
this.pendingSequence_.push(character);
var sequence = this.pendingSequence_;
var processed = true;
switch (sequence[1]) {
case '[':
if (!this.interpretControlSequenceInducer_(sequence.slice(2))) {
processed = false;
}
break;
case ']':
if (!this.interpretOperatingSystemCommand_(sequence.slice(2))) {
processed = false;
}
break;
case '=': // Application keypad
this.applicationKeypad = true;
break;
case '>': // Normal keypad
this.applicationKeypad = false;
break;
case '7': // Save cursor
this.terminal_.saveCursor();
break;
case '8': // Restore cursor
this.terminal_.restoreCursor();
break;
case 'D': // Index, like newline, only keep the X position
this.terminal_.lineFeed();
break;
case 'E': // Next line. Like newline, but doesn't add lines.
this.terminal_.setCursorColumn(0);
this.terminal_.cursorDown(1);
break;
case 'M': // Reverse index.
// This is like newline, but in reverse. When we hit the top of the
// terminal, lines are added at the top while swapping out the bottom
// lines.
this.terminal_.reverseLineFeed();
break;
case 'c': // Full reset
this.terminal_.reset();
break;
case '#': // DEC commands
if (sequence.length < 3) {
processed = false;
break;
}
switch (sequence[2]) {
case '8': // DEC screen alignment test
this.fill('E');
break;
default:
console.log('Unsupported DEC command: ' + sequence[2]);
break;
}
break;
case '(': // Designate G0 character set
if (sequence.length < 3) {
processed = false;
break;
}
switch (sequence[2]) {
case '0': // Line drawing
this.terminal_.setSpecialCharsEnabled(true);
break;
default:
this.terminal_.setSpecialCharsEnabled(false);
break;
}
break;
case ')': // Designate G1 character set
case '*': // Designate G2 character set
case '+': // Designate G3 character set
if (sequence.length < 3) {
processed = false;
break;
}
console.log('Code ' + sequence[2]);
break;
case 'H': // Set a tab stop at the cursor position
this.terminal_.setTabStopAtCursor(true);
break;
default:
console.log('Unsupported escape sequence: ' + sequence[1]);
break;
}
if (processed) {
//console.log('Escape sequence: ' + sequence.slice(1));
this.pendingSequence_.length = 0;
}
return;
};
/**
* Return any pending response from the interpretation of control sequences.
*
* The response should be returned as if the user typed it, and the pending
* response is cleared from the interpreter.
*
* @return {string} response to send.
*/
hterm.VT100.prototype.getAndClearPendingResponse = function() {
var response = this.pendingResponse_;
this.pendingResponse_ = '';
return response;
};
/**
* Interpret an operating system command (OSC) sequence.
*
* @param {Array} sequence Sequence to interpret.
* @return {boolean} Whether the sequence was interpreted or not.
* @private
*/
hterm.VT100.prototype.interpretOperatingSystemCommand_ =
function(sequence) {
// These commands tend to do things like change the window title and other
// things.
var processed = false;
var length = sequence.length;
var i = 0;
var args = [];
var currentArg = '';
var leadingZeroFilter = true;
// Parse the command into a sequence command and series of numeric arguments.
while (true) {
if (i >= length) {
// We ran out of characters interpreting the string
break;
}
if (sequence[i] == ';') {
// New argument
args.push(currentArg);
currentArg = '';
leadingZeroFilter = true;
} else if (sequence[i] == '\x7' ||
(sequence[i] == '\x1b' &&
sequence[i + 1] == '\\')) {
// Terminating character. This'll tell us how to interpret the control
// sequence.
if (currentArg != '') {
args.push(currentArg);
}
processed = true;
break;
} else {
// Part of the arg, just add it, filtering out leadining zeros.
if (!(leadingZeroFilter && sequence[i] == '0')) {
leadingZeroFilter = false;
currentArg += sequence[i];
}
}
i++;
}
if (!processed)
return processed;
// Interpret the command
if (args[0] == '') {
// The leading-zero filter dropped our zero, so put it back.
args[0] = 0;
}
switch (parseInt(args[0], 10)) {
case 0:
case 2:
// Change the window title to args[1]
// TODO(rginda): this.
break;
default:
console.log('Unsupported OSC command: ' + sequence.slice(0, i + 1));
break;
}
return processed;
};
/**
* Interpret a control sequence inducer (CSI) command.
*
* @param {Array} sequence Sequence to interpret.
* @return {boolean} Whether the sequence was interpreted succesfully or not.
* @private
*/
hterm.VT100.prototype.interpretControlSequenceInducer_ =
function(sequence) {
// These escape codes all end with a letter, and have arguments separated by
// a semicolon.
var processed = false;
var args = [];
var currentArg = '';
var terminator = /[A-Za-z@]/;
var seqCommand = '';
var query = false;
var leadingZeroFilter = true;
// Parse the command into a sequence command and series of numeric arguments.
for (var i = 0; i < sequence.length; ++i) {
if (sequence[i] == '?') {
// Some commands have different meaning with a leading '?'. We'll call
// that the 'query' flag.
query = true;
leadingZeroFilter = true;
} else if (sequence[i] == ';') {
// New argument
args.push(parseInt(currentArg, 10));
currentArg = '';
leadingZeroFilter = true;
} else if (terminator.test(sequence[i])) {
// Terminating character. This'll tell us how to interpret the control
// sequence.
seqCommand = sequence[i];
if (currentArg != '') {
args.push(parseInt(currentArg, 10));
}
processed = true;
break;
} else {
// Part of the arg, just add it, filtering out leading zeros.
if (!(leadingZeroFilter && sequence[i] == '0')) {
leadingZeroFilter = false;
currentArg += sequence[i];
}
}
}
if (!processed) {
return processed;
}
// Interpret the command
switch (seqCommand) {
case 'A': // Cursor up
this.terminal_.cursorUp(args[0] || 1);
break;
case 'B': // Cursor down
this.terminal_.cursorDown(args[0] || 1);
break;
case 'C': // Cursor right
this.terminal_.cursorRight(args[0] || 1);
break;
case 'D': // Cursor left
this.terminal_.cursorLeft(args[0] || 1);
break;
case 'E': // Next line
// This is like Cursor Down, except the cursor moves to the beginning of
// the line as well.
this.terminal_.cursorDown(args[0] || 1);
this.terminal_.setCursorColumn(0);
break;
case 'F': // Previous line
// This is like Cursor Up, except the cursor moves to the beginning of the
// line as well.
this.terminal_.cursorUp(args[0] || 1);
this.terminal_.setCursorColumn(0);
break;
case 'G': // Cursor absolute column
var position = args[0] ? args[0] - 1 : 0;
this.terminal_.setCursorColumn(position);
break;
case 'H': // Cursor absolute row;col
case 'f': // Horizontal & Vertical Position
var row = args[0] ? args[0] - 1 : 0;
var col = args[1] ? args[1] - 1 : 0;
this.terminal_.setCursorPosition(row, col);
break;
case 'K': // Erase in Line
switch (args[0]) {
case 1: // Erase to left
this.terminal_.eraseToLeft();
break;
case 2: // Erase the line
this.terminal_.eraseLine();
break;
case 0: // Erase to right
default:
// Erase to right
this.terminal_.eraseToRight();
break;
}
break;
case 'J': // Erase in display
switch (args[0]) {
case 1: // Erase above
this.terminal_.eraseToLeft();
this.terminal_.eraseAbove();
break;
case 2: // Erase all
this.terminal_.clear();
break;
case 0: // Erase below
default:
this.terminal_.eraseToRight();
this.terminal_.eraseBelow();
break;
}
break;
case 'X': // Erase character
this.terminal_.eraseToRight(args[0] || 1);
break;
case 'L': // Insert lines
this.terminal_.insertLines(args[0] || 1);
break;
case 'M': // Delete lines
this.terminal_.deleteLines(args[0] || 1);
break;
case '@': // Insert characters
var amount = 1;
if (args[0]) {
amount = args[0];
}
this.terminal_.insertSpace(amount);
break;
case 'P': // Delete characters
// This command shifts the line contents left, starting at the cursor
// position.
this.terminal_.deleteChars(args[0] || 1);
break;
case 'S': // Scroll up an amount
this.terminal_.vtScrollUp(args[0] || 1);
break;
case 'T': // Scroll down an amount
this.terminal_.vtScrollDown(args[0] || 1);
break;
case 'c': // Send device attributes
if (!args[0]) {
this.pendingResponse_ += '\x1b[?1;2c';
}
break;
case 'd': // Line position absolute
this.terminal_.setCursorRow((args[0] - 1) || 0);
break;
case 'g': // Clear tab stops
switch (args[0] || 0) {
case 0:
this.terminal_.setTabStopAtCursor(false);
break;
case 3: // Clear all tab stops in the page
this.terminal_.clearTabStops();
break;
default:
break;
}
break;
case 'm': // Color change
if (args.length == 0) {
this.terminal_.clearColorAndAttributes();
} else {
if (args.length == 3 &&
(args[0] == 38 || args[0] == 48) && args[1] == 5) {
// This is code for the 256-color palette, skip the normal processing.
if (args[0] == 38) {
// Set the foreground color to the 3rd argument.
this.terminal_.setForegroundColor256(args[2]);
} else if (args[0] == 48) {
// Set the background color to the 3rd argument.
this.terminal_.setBackgroundColor256(args[2]);
}
} else {
var numArgs = args.length;
for (var argNum = 0; argNum < numArgs; ++argNum) {
var arg = args[argNum];
if (isNaN(arg)) {
// This is the same as an attribute of zero.
this.terminal_.setAttributes(0);
} else if (arg < 30) {
// This is an attribute argument.
this.terminal_.setAttributes(arg);
} else if (arg < 40) {
// This is a foreground color argument.
this.terminal_.setForegroundColor(arg);
} else if (arg < 50) {
// This is a background color argument.
this.terminal_.setBackgroundColor(arg);
}
}
}
}
break;
case 'n': // Device status report
switch (args[0]) {
case 5:
if (!query) {
var response = '\x1b0n';
this.pendingResponse_ += response;
}
break;
case 6:
var curX = this.terminal_.getCursorColumn() + 1;
var curY = this.terminal_.getCursorRow() + 1;
var response = '\x1b[' + curY + ';' + curX + 'R';
this.pendingResponse_ += response;
break;
}
break;
case 'l': // Reset mode
case 'h': // Set mode
var set = (seqCommand == 'h' ? true : false);
if (query) {
switch (args[0]) {
case 1: // Normal (l) or application (h) cursor keys
this.applicationCursor = set;
break;
case 3: // 80 (if l) or 132 (if h) column mode
// Our size is always determined by the window size, so we ignore
// attempts to resize from remote end.
break;
case 4: // Fast (l) or slow (h) scroll
// This is meaningless to us.
break;
case 5: // Normal (l) or reverse (h) video mode
this.terminal_.setReverseVideo(set);
break;
case 6: // Normal (l) or origin (h) cursor mode
this.terminal_.setOriginMode(set);
break;
case 7: // No (l) wraparound mode or wraparound (h) mode
this.terminal_.setWraparound(set);
break;
case 12: // Stop (l) or start (h) blinking cursor
this.terminal_.setCursorBlink(set);
break;
case 25: // Hide (l) or show (h) cursor
this.terminal_.setCursorVisible(set);
break;
case 45: // Disable (l) or enable (h) reverse wraparound
this.terminal_.setReverseWraparound(set);
break;
case 67: // Backspace is delete (h) or backspace (l)
this.backspaceSendsBackspace = set;
break;
case 1036: // Meta sends (h) or doesn't send (l) escape
this.metaSendsEscape = set;
break;
case 1039: // Alt sends (h) or doesn't send (l) escape
this.altSendsEscape = set;
break;
case 1049: // Switch to/from alternate, save/restore cursor
this.terminal_.setAlternateMode(set);
break;
default:
console.log('Unimplemented l/h command: ' +
(query ? '?' : '') + args[0]);
break;
}
} else {
switch (args[0]) {
case 4: // Replace (l) or insert (h) mode
this.terminal_.setInsertMode(set);
break;
case 20:
// Normal linefeed (l), \n means move down only
// Automatic linefeed (h), \n means \n\r
this.terminal_.setAutoLinefeed(set);
break;
default:
console.log('Unimplemented l/h command: ' +
(query ? '?' : '') + args[0]);
break;
}
}
break;
case 'r':
if (query) {
// Restore DEC private mode values
// TODO(maccarro): Implement this
} else {
// Set scroll region
var scrollTop = args[0] || null;
var scrollBottom = args[1] || null;
this.terminal_.setScrollRegion(scrollTop, scrollBottom);
}
break;
default:
console.log('Unknown control: ' + seqCommand);
break;
}
return processed;
};
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview VT100 test suite.
*
* This is more of an integration test suite for the VT100 and Terminal classes,
* as each test typically sends strings into the VT100 parser and then reads
* the terminal to verify that everyone did the right thing.
*/
hterm.VT100.Tests = new TestManager.Suite('hterm.VT100.Tests');
hterm.VT100.Tests.prototype.setup = function(cx) {
this.setDefaults(cx,
{ visibleColumnCount: 15,
visibleRowCount: 6,
fontSize: 15,
lineHeight: 17,
charWidth: 9,
scrollbarWidth: 16,
});
};
/**
* Clear out the current document and create a new hterm.Terminal object for
* testing.
*
* Called before each test case in this suite.
*/
hterm.VT100.Tests.prototype.preamble = function(result, cx) {
var document = cx.window.document;
document.body.innerHTML = '';
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.height = this.lineHeight * this.visibleRowCount + 'px';
div.style.width = this.charWidth * this.visibleColumnCount +
this.scrollbarWidth + 'px';
document.body.appendChild(div);
cx.window.terminal = this.terminal = new hterm.Terminal(
this.fontSize, this.lineHeight);
this.terminal.decorate(div);
};
/**
* Ensure that blink is off after the test so we don't have runaway timeouts.
*
* Called after each test case in this suite.
*/
hterm.VT100.Tests.prototype.postamble = function(result, cx) {
this.terminal.setCursorBlink(false);
};
/**
* Overridden addTest method.
*
* Every test in this suite needs to wait for the terminal initialization to
* complete asynchronously. Rather than stick a bunch of biolerplate into each
* test case, we use this overridden addTest method to add a proxy around the
* actual test.
*/
hterm.VT100.Tests.addTest = function(name, callback) {
function testProxy(result, cx) {
setTimeout(function() {
this.terminal.setCursorPosition(0, 0);
this.terminal.setCursorVisible(true);
callback.apply(this, [result, cx]);
}, 0);
result.requestTime(200);
}
TestManager.Suite.addTest.apply(this, [name, testProxy]);
};
/**
* Basic sanity test to make sure that when we insert plain text it appears
* on the screen and scrolls into the scrollback buffer correctly.
*/
hterm.VT100.Tests.addTest('sanity', function(result, cx) {
this.terminal.interpret('0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12');
var text = this.terminal.getRowsText(0, 13);
result.assertEQ(text, '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12');
result.assertEQ(this.terminal.scrollbackRows_.length, 7);
result.pass();
});
/**
* Basic cursor positioning tests.
*
* TODO(rginda): Test the VT52 variants too.
*/
hterm.VT100.Tests.addTest('cursor-relative', function(result, cx) {
this.terminal.interpret('line 1\nline 2\nline 3');
this.terminal.interpret('\x1b[A\x1b[Dtwo' +
'\x1b[3D' +
'\x1b[Aone' +
'\x1b[4D' +
'\x1b[2B' +
'\x1b[Cthree');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text, 'line one\nline two\nline three');
result.pass();
});
/**
* Test absolute cursor positioning.
*/
hterm.VT100.Tests.addTest('cursor-absolute', function(result, cx) {
this.terminal.interpret('line 1\nline 2\nline 3');
this.terminal.interpret('\x1b[1Gline three' +
'\x1b[2;6Htwo' +
'\x1b[1;5f one');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text, 'line one\nline two\nline three');
result.pass();
});
/**
* Test line positioning.
*/
hterm.VT100.Tests.addTest('line-position', function(result, cx) {
this.terminal.interpret('line 1\nline 2\nline 3');
this.terminal.interpret('\x1b[Fline two' +
'\x1b[Fline one' +
'\x1b[E\x1b[Eline three');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text, 'line one\nline two\nline three');
result.pass();
});
/**
* Test that a partial sequence is buffered until the entire sequence is
* received.
*/
hterm.VT100.Tests.addTest('partial-sequence', function(result, cx) {
this.terminal.interpret('line 1\nline 2\nline three');
this.terminal.interpret('\x1b');
this.terminal.interpret('[');
this.terminal.interpret('5');
this.terminal.interpret('D');
this.terminal.interpret('\x1b[');
this.terminal.interpret('Atwo\x1b[3');
this.terminal.interpret('D\x1b[Aone');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text, 'line one\nline two\nline three');
result.pass();
});
/**
* Test that two ESC characters in a row are handled properly.
*/
hterm.VT100.Tests.addTest('double-sequence', function(result, cx) {
this.terminal.interpret('line one\nline two\nline 3');
this.terminal.interpret('\x1b[\x1b[Dthree');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text, 'line one\nline two\nline three');
result.pass();
});
/**
* Test the erase left command.
*/
hterm.VT100.Tests.addTest('erase-left', function(result, cx) {
this.terminal.interpret('line one\noooooooo\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[1Ktw');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
' two\n' +
'line three');
result.pass();
});
/**
* Test the erase right command.
*/
hterm.VT100.Tests.addTest('erase-right', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[0Ktwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the erase line command.
*/
hterm.VT100.Tests.addTest('erase-line', function(result, cx) {
this.terminal.interpret('line one\nline twoo\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[2Ktwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
' two\n' +
'line three');
result.pass();
});
/**
* Test the erase above command.
*/
hterm.VT100.Tests.addTest('erase-above', function(result, cx) {
this.terminal.interpret('line one\noooooooo\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[1Jtw');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'\n' +
' two\n' +
'line three');
result.pass();
});
/**
* Test the erase all command.
*/
hterm.VT100.Tests.addTest('erase-all', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[2Jtwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'\n' +
' two\n' +
'');
result.pass();
});
/**
* Test the erase below command.
*/
hterm.VT100.Tests.addTest('erase-below', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[0Jtwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'');
result.pass();
});
/**
* Test the erase character command.
*/
hterm.VT100.Tests.addTest('erase-char', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A' +
'\x1b[4Xtwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the insert line command.
*/
hterm.VT100.Tests.addTest('insert-line', function(result, cx) {
this.terminal.interpret('line two\nline three');
this.terminal.interpret('\x1b[5D\x1b[2A\x1b[L' +
'line one');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the insert line command with an argument.
*/
hterm.VT100.Tests.addTest('insert-lines', function(result, cx) {
this.terminal.interpret('line three\n\n');
this.terminal.interpret('\x1b[5D\x1b[2A\x1b[2L' +
'line one\nline two');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test that the insert line command handles overflow properly.
*/
hterm.VT100.Tests.addTest('insert-toomany-lines', function(result, cx) {
this.terminal.interpret('XXXXX');
this.terminal.interpret('\x1b[6L' +
'line one\nline two\nline three');
var text = this.terminal.getRowsText(0, 5);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three\n' +
'\n' +
'');
result.pass();
});
/**
* Test the delete line command.
*/
hterm.VT100.Tests.addTest('delete-line', function(result, cx) {
this.terminal.interpret('line one\nline two\n' +
'XXXXXXXX\n' +
'line XXXXX');
this.terminal.interpret('\x1b[5D\x1b[A\x1b[Mthree');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the delete line command with an argument.
*/
hterm.VT100.Tests.addTest('delete-lines', function(result, cx) {
this.terminal.interpret('line one\nline two\n' +
'XXXXXXXX\nXXXXXXXX\n' +
'line XXXXX');
this.terminal.interpret('\x1b[5D\x1b[2A\x1b[2Mthree');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the insert space command.
*/
hterm.VT100.Tests.addTest('insert-space', function(result, cx) {
this.terminal.interpret('line one\nlinetwo\nline three');
this.terminal.interpret('\x1b[6D\x1b[A\x1b[@');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the insert space command with an argument.
*/
hterm.VT100.Tests.addTest('insert-spaces', function(result, cx) {
this.terminal.interpret('line one\nlinetwo\nline three');
this.terminal.interpret('\x1b[6D\x1b[A\x1b[3@');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the delete characters command.
*/
hterm.VT100.Tests.addTest('delete-chars', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A\x1b[4Ptwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test that the delete characters command handles overflow properly.
*/
hterm.VT100.Tests.addTest('delete-toomany', function(result, cx) {
this.terminal.interpret('line one\nline XXXX\nline three');
this.terminal.interpret('\x1b[5D\x1b[A\x1b[20Ptwo');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the scroll up command.
*/
hterm.VT100.Tests.addTest('scroll-up', function(result, cx) {
this.terminal.interpret('\n\nline one\nline two\nline XXXXX');
this.terminal.interpret('\x1b[5D\x1b[2A\x1b[2Sthree');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the scroll down command.
*/
hterm.VT100.Tests.addTest('scroll-down', function(result, cx) {
this.terminal.interpret('line one\nline two\nline XXXXX\n');
this.terminal.interpret(' \x1b[Tthree');
var text = this.terminal.getRowsText(0, 5);
result.assertEQ(text,
'\n' +
'line one\n' +
'line two\n' +
'line three\n' +
' ');
result.pass();
});
/**
* Test the absolute line positioning command.
*/
hterm.VT100.Tests.addTest('line-position-absolute', function(result, cx) {
this.terminal.interpret('line XXX\nline YYY\nline ZZZZZ\n');
this.terminal.interpret(' \x1b[3dthree\x1b[5D');
this.terminal.interpret('\x1b[2dtwo\x1b[3D');
this.terminal.interpret('\x1b[1done');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test the device attributes command.
*/
hterm.VT100.Tests.addTest('device-attributes', function(result, cx) {
this.terminal.interpret('\x1b[c');
result.assertEQ(this.terminal.vt100_.getAndClearPendingResponse(),
'\x1b[?1;2c');
result.pass();
});
/**
* TODO(rginda): Test the clear tabstops on this line command.
*/
hterm.VT100.Tests.disableTest('clear-line-tabstops', function(result, cx) {
'[0g';
});
/**
* TODO(rginda): Test the clear all tabstops command.
*/
hterm.VT100.Tests.disableTest('clear-all-tabstops', function(result, cx) {
'[3g';
});
/**
* TODO(rginda): Test text attributes.
*/
hterm.VT100.Tests.disableTest('color-change', function(result, cx) {
'[Xm';
});
/**
* Test the status report command.
*/
hterm.VT100.Tests.addTest('status-report', function(result, cx) {
this.terminal.interpret('\x1b[5n');
result.assertEQ(this.terminal.vt100_.getAndClearPendingResponse(),
'\x1b0n');
this.terminal.interpret('line one\nline two\nline three');
// Reposition the cursor and ask for a position report.
this.terminal.interpret('\x1b[5D\x1b[A\x1b[6n');
result.assertEQ(this.terminal.vt100_.getAndClearPendingResponse(),
'\x1b[2;6R');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test that various mode commands correctly change the state of the terminal.
*
* Most of these should have more in-depth testing below.
*/
hterm.VT100.Tests.addTest('mode-bits', function(result, cx) {
this.terminal.interpret('\x1b[?1h');
result.assertEQ(this.terminal.vt100_.applicationCursor, true);
this.terminal.interpret('\x1b[?1l');
result.assertEQ(this.terminal.vt100_.applicationCursor, false);
var fg = this.terminal.foregroundColor;
var bg = this.terminal.backgroundColor;
this.terminal.interpret('\x1b[?5h');
result.assertEQ(this.terminal.scrollPort_.getForegroundColor(), bg);
result.assertEQ(this.terminal.scrollPort_.getBackgroundColor(), fg);
this.terminal.interpret('\x1b[?5l');
result.assertEQ(this.terminal.scrollPort_.getForegroundColor(), fg);
result.assertEQ(this.terminal.scrollPort_.getBackgroundColor(), bg);
this.terminal.interpret('\x1b[?5l');
result.assertEQ(this.terminal.scrollPort_.getForegroundColor(), fg);
result.assertEQ(this.terminal.scrollPort_.getBackgroundColor(), bg);
this.terminal.interpret('\x1b[?6h');
result.assertEQ(this.terminal.options_.originMode, true);
this.terminal.interpret('\x1b[?6l');
result.assertEQ(this.terminal.options_.originMode, false);
this.terminal.interpret('\x1b[4h');
result.assertEQ(this.terminal.options_.insertMode, true);
this.terminal.interpret('\x1b[4l');
result.assertEQ(this.terminal.options_.insertMode, false);
this.terminal.interpret('\x1b[?7h');
result.assertEQ(this.terminal.options_.wraparound, true);
this.terminal.interpret('\x1b[?7l');
result.assertEQ(this.terminal.options_.wraparound, false);
this.terminal.interpret('\x1b[?12l');
result.assertEQ(this.terminal.options_.cursorBlink, false);
result.assert(!('cursorBlink' in this.terminal.timeouts_));
this.terminal.interpret('\x1b[?12h');
result.assertEQ(this.terminal.options_.cursorBlink, true);
result.assert('cursorBlink' in this.terminal.timeouts_);
this.terminal.interpret('\x1b[?25l');
result.assertEQ(this.terminal.options_.cursorVisible, false);
result.assertEQ(this.terminal.cursorNode_.style.display, 'none');
this.terminal.interpret('\x1b[?25h');
result.assertEQ(this.terminal.options_.cursorVisible, true);
// Turn off blink so we know the cursor should be on.
this.terminal.interpret('\x1b[?12l');
result.assertEQ(this.terminal.cursorNode_.style.display, 'block');
this.terminal.interpret('\x1b[?45h');
result.assertEQ(this.terminal.options_.reverseWraparound, true);
this.terminal.interpret('\x1b[?45l');
result.assertEQ(this.terminal.options_.reverseWraparound, false);
this.terminal.interpret('\x1b[?67h');
result.assertEQ(this.terminal.vt100_.backspaceSendsBackspace, true);
this.terminal.interpret('\x1b[?67l');
result.assertEQ(this.terminal.vt100_.backspaceSendsBackspace, false);
this.terminal.interpret('\x1b[?1036h');
result.assertEQ(this.terminal.vt100_.metaSendsEscape, true);
this.terminal.interpret('\x1b[?1036l');
result.assertEQ(this.terminal.vt100_.metaSendsEscape, false);
this.terminal.interpret('\x1b[?1039h');
result.assertEQ(this.terminal.vt100_.altSendsEscape, true);
this.terminal.interpret('\x1b[?1039l');
result.assertEQ(this.terminal.vt100_.altSendsEscape, false);
result.assertEQ(this.terminal.screen_,
this.terminal.primaryScreen_);
this.terminal.interpret('\x1b[?1049h');
result.assertEQ(this.terminal.screen_,
this.terminal.alternateScreen_);
this.terminal.interpret('\x1b[?1049l');
result.assertEQ(this.terminal.screen_,
this.terminal.primaryScreen_);
result.pass();
});
/**
* TODO(rginda): Test origin mode.
*/
hterm.VT100.Tests.disableTest('origin-mode', function(result, cx) {
});
/**
* Test insert/overwrite mode.
*/
hterm.VT100.Tests.addTest('insert-mode', function(result, cx) {
// Should be off by default.
result.assertEQ(this.terminal.options_.insertMode, false);
this.terminal.interpret('\x1b[4h');
this.terminal.interpret(' one\x1b[4Dline\n');
this.terminal.interpret('\x1b[4l');
this.terminal.interpret('XXXXXXXX\x1b[8Dline two\n');
this.terminal.interpret('\x1b[4h');
this.terminal.interpret(' three\x1b[6Dline');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'line one\n' +
'line two\n' +
'line three');
result.pass();
});
/**
* Test wraparound mode.
*/
hterm.VT100.Tests.addTest('wraparound-mode', function(result, cx) {
// Should be on by default.
result.assertEQ(this.terminal.options_.wraparound, true);
this.terminal.interpret('----- 1 -----');
this.terminal.interpret('----- 2 -----');
var text = this.terminal.getRowsText(0, 3);
result.assertEQ(text,
'----- 1 -----\n' +
'----- 2 -----\n' +
'');
result.pass();
});
/**
* Test the interactions between insert and wraparound modes.
*/
hterm.VT100.Tests.addTest('insert-wrap', function(result, cx) {
// Should be on by default.
result.assertEQ(this.terminal.options_.wraparound, true);
this.terminal.interpret('' + // Insert off, wrap on (default).
'AAAAXX\n' +
'[?7l' + // Insert on, wrap off.
'AAAAXX\n' +
'[?7h' + // Insert on, wrap on.
'AAAAXX\n' +
'[?7l' + // Insert off, wrap off.
'AAAAXX');
var text = this.terminal.getRowsText(0, 6);
result.assertEQ(text,
' A\n' +
'XXA\n' +
'XX \n' +
' A\n' +
'XXAAA\n' +
'XX A');
result.pass();
});
hterm.VT100.Tests.addTest('alternate-screen', function(result, cx) {
this.terminal.interpret('1\n2\n3\n4\n5\n6\n7\n8\n9\n10');
this.terminal.interpret('\x1b[3;3f'); // Leave the cursor at (3,3)
var text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n5\n6\n7\n8\n9\n10');
// Switch to alternate screen.
this.terminal.interpret('\x1b[?1049h');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n\n\n\n\n\n');
this.terminal.interpret('\nhi');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n\nhi\n\n\n\n');
// Switch back to primary screen.
this.terminal.interpret('\x1b[?1049l');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n5\n6\n7\n8\n9\n10');
this.terminal.interpret('XX');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n5\n6\n7 XX\n8\n9\n10');
// Aand back to alternate screen.
this.terminal.interpret('\x1b[?1049h');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n\nhi\n\n\n\n');
this.terminal.interpret('XX');
text = this.terminal.getRowsText(0, 10);
result.assertEQ(text, '1\n2\n3\n4\n\nhiXX\n\n\n\n');
result.pass();
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment