hterm: implement tabstops, vtscope dev/text refinements
vtscope.py:
* Better escape sequence classification.
* Speed up dev/test cycle:
- Deal with clients that disconnect (like when you reload hterm after making
a change).
- Allow adding of new clients without affecting existing ones (like when
the reload is complete and you need to re-connect, but xterm is still
connected).
* Fix infinite loop when executing 'seek 0' before loading any data.
terminal.js:
* Make it possible to use the terminal object without waiting for the
(asynchronous) resize handler to run.
* Implement tab handling.
BUG=chromium-os:24770
TEST=test_harness.html, 53/53 tests passed
Change-Id: Iffaa153a42dbee3ae422443d184b5d1e5003bc7a
Reviewed-on: https://gerrit.chromium.org/gerrit/14396
Commit-Ready: Robert Ginda <rginda@chromium.org>
Reviewed-by: Robert Ginda <rginda@chromium.org>
Tested-by: Robert Ginda <rginda@chromium.org>
diff --git a/hterm/js/terminal.js b/hterm/js/terminal.js
index 3e36eb0..5fe3536 100644
--- a/hterm/js/terminal.js
+++ b/hterm/js/terminal.js
@@ -44,12 +44,17 @@
// The div that contains this terminal.
this.div_ = null;
- // The document that contains the scrollPort. Set in decorate().
- this.document_ = null;
+ // The document that contains the scrollPort. Defaulted to the global
+ // document here so that the terminal is functional even if it hasn't been
+ // inserted into a document yet, but re-set in decorate().
+ this.document_ = window.document;
// The rows that have scrolled off screen and are no longer addressable.
this.scrollbackRows_ = [];
+ // Saved tab stops.
+ this.tabStops_ = [];
+
// The VT's notion of the top and bottom rows. Used during some VT
// cursor positioning and scrolling commands.
this.vtScrollTop_ = null;
@@ -62,6 +67,9 @@
this.backgroundColor = 'black';
this.foregroundColor = 'white';
+ // Default tab with of 8 to match xterm.
+ this.tabWidth = 8;
+
// The color of the cursor.
this.cursorColor = 'rgba(255,0,0,0.5)';
@@ -86,6 +94,10 @@
// General IO interface that can be given to third parties without exposing
// the entire terminal object.
this.io = new hterm.Terminal.IO(this);
+
+ this.realizeWidth_(80);
+ this.realizeHeight_(24);
+ this.setDefaultTabStops();
};
/**
@@ -134,12 +146,98 @@
*/
hterm.Terminal.prototype.setWidth = function(columnCount) {
this.div_.style.width = this.characterSize_.width * columnCount + 16 + 'px'
+ this.realizeWidth_(columnCount);
+ this.scheduleSyncCursorPosition_();
+};
- // The resizing of the UI will happen asynchronously, so we need to take
- // care of this bookeeping here instead of letting the resize handlers deal
- // with it.
+/**
+ * Deal with terminal width changes.
+ *
+ * This function does what needs to be done when the terminal width changes
+ * out from under us. It happens here rather than in onResize_() because this
+ * code may need to run synchronously to handle programmatic changes of
+ * terminal width.
+ *
+ * Relying on the browser to send us an async resize event means we may not be
+ * in the correct state yet when the next escape sequence hits.
+ */
+hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
+ var deltaColumns = columnCount - this.screen_.getWidth();
+
this.screenSize.width = columnCount;
this.screen_.setColumnCount(columnCount);
+
+ if (deltaColumns > 0) {
+ this.setDefaultTabStops(this.screenSize.width - deltaColumns);
+ } else {
+ for (var i = this.tabStops_.length - 1; i >= 0; i--) {
+ if (this.tabStops_[i] <= columnCount)
+ break;
+
+ this.tabStops_.pop();
+ }
+ }
+
+ this.screen_.setColumnCount(this.screenSize.width);
+};
+
+/**
+ * Deal with terminal height changes.
+ *
+ * This function does what needs to be done when the terminal height changes
+ * out from under us. It happens here rather than in onResize_() because this
+ * code may need to run synchronously to handle programmatic changes of
+ * terminal height.
+ *
+ * Relying on the browser to send us an async resize event means we may not be
+ * in the correct state yet when the next escape sequence hits.
+ */
+hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
+ var deltaRows = rowCount - this.screen_.getHeight();
+
+ this.screenSize.height = rowCount;
+
+ var cursor = this.saveCursor();
+
+ if (deltaRows < 0) {
+ // Screen got smaller.
+ deltaRows *= -1;
+ while (deltaRows) {
+ var lastRow = this.getRowCount() - 1;
+ if (lastRow - this.scrollbackRows_.length == cursor.row)
+ break;
+
+ if (this.getRowText(lastRow))
+ break;
+
+ this.screen_.popRow();
+ deltaRows--;
+ }
+
+ var ary = this.screen_.shiftRows(deltaRows);
+ this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
+
+ // We just removed rows from the top of the screen, we need to update
+ // the cursor to match.
+ cursor.row -= deltaRows;
+
+ } 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(
+ this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
+ this.screen_.unshiftRows(rows);
+ deltaRows -= scrollbackCount;
+ cursor.row += scrollbackCount;
+ }
+
+ if (deltaRows)
+ this.appendRows_(deltaRows);
+ }
+
+ this.restoreCursor(cursor);
};
/**
@@ -174,12 +272,24 @@
this.scrollPort_.scrollRowToTop(i + this.screenSize.height - 1);
};
+/**
+ * Full terminal reset.
+ */
hterm.Terminal.prototype.reset = function() {
- console.log('reset');
+ this.clearAllTabStops();
+ this.setDefaultTabStops();
+ this.clearColorAndAttributes();
+ this.setVTScrollRegion(null, null);
+ this.clear();
+ this.setAbsoluteCursorPosition(0, 0);
+ this.softReset();
};
+/**
+ * Soft terminal reset.
+ */
hterm.Terminal.prototype.softReset = function() {
- console.log('softReset');
+ this.options_ = new hterm.Options();
};
hterm.Terminal.prototype.clearColorAndAttributes = function() {
@@ -214,20 +324,100 @@
//console.log('setSpecialCharactersEnabled');
};
-hterm.Terminal.prototype.forwardTabStop = function(count) {
- this.cursorRight(4);
+/**
+ * Move the cursor forward to the next tab stop, or to the last column
+ * if no more tab stops are set.
+ */
+hterm.Terminal.prototype.forwardTabStop = function() {
+ var column = this.screen_.cursorPosition.column;
+
+ for (var i = 0; i < this.tabStops_.length; i++) {
+ if (this.tabStops_[i] > column) {
+ this.setCursorColumn(this.tabStops_[i]);
+ return;
+ }
+ }
+
+ this.setCursorColumn(this.screenSize.width - 1);
};
-hterm.Terminal.prototype.backwardTabStop = function(count) {
- console.error('Not implemented: backwardTabStop');
+/**
+ * Move the cursor backward to the previous tab stop, or to the first column
+ * if no previous tab stops are set.
+ */
+hterm.Terminal.prototype.backwardTabStop = function() {
+ var column = this.screen_.cursorPosition.column;
+
+ for (var i = this.tabStops_.length - 1; i >= 0; i--) {
+ if (this.tabStops_[i] < column) {
+ this.setCursorColumn(this.tabStops_[i]);
+ return;
+ }
+ }
+
+ this.setCursorColumn(1);
};
-hterm.Terminal.prototype.setTabStopAtCursor = function() {
- console.log('setTabStopAtCursor');
+/**
+ * Set a tab stop at the given column.
+ *
+ * @param {int} column Zero based column.
+ */
+hterm.Terminal.prototype.setTabStop = function(column) {
+ for (var i = this.tabStops_.length - 1; i >= 0; i--) {
+ if (this.tabStops_[i] == column)
+ return;
+
+ if (this.tabStops_[i] < column) {
+ this.tabStops_.splice(i + 1, 0, column);
+ return;
+ }
+ }
+
+ this.tabStops_.splice(0, 0, column);
};
-hterm.Terminal.prototype.clearTabStops = function() {
- console.log('clearTabStops');
+/**
+ * Clear the tab stop at the current cursor position.
+ *
+ * No effect if there is no tab stop at the current cursor position.
+ */
+hterm.Terminal.prototype.clearTabStopAtCursor = function() {
+ var column = this.screen_.cursorPosition.column;
+
+ var i = this.tabStops_.indexOf(column);
+ if (i == -1)
+ return;
+
+ this.tabStops_.splice(i, 1);
+};
+
+/**
+ * Clear all tab stops.
+ */
+hterm.Terminal.prototype.clearAllTabStops = function() {
+ this.tabStops_.length = 0;
+};
+
+/**
+ * Set up the default tab stops, starting from a given column.
+ *
+ * This sets a tabstop every (column % this.tabWidth) column, starting
+ * from the specified column, or 0 if no column is provided.
+ *
+ * This does not clear the existing tab stops first, use clearAllTabStops
+ * for that.
+ *
+ * @param {int} opt_start Optional starting zero based starting column, useful
+ * for filling out missing tab stops when the terminal is resized.
+ */
+hterm.Terminal.prototype.setDefaultTabStops = function(opt_start) {
+ var start = opt_start || 0;
+ var w = this.tabWidth;
+ var stopCount = Math.floor((this.screenSize.width - start) / this.tabWidth)
+ for (var i = 0; i < stopCount; i++) {
+ this.setTabStop(Math.floor((start + i * w) / w) * w + w);
+ }
};
/**
@@ -1293,66 +1483,25 @@
/**
* React when the ScrollPort is resized.
+ *
+ * Note: This function should not directly contain code that alters the internal
+ * state of the terminal. That kind of code belongs in realizeWidth or
+ * realizeHeight, so that it can be executed synchronously in the case of a
+ * programmatic width change.
*/
hterm.Terminal.prototype.onResize_ = function() {
- var width = Math.floor(this.scrollPort_.getScreenWidth() /
- this.characterSize_.width);
- var height = this.scrollPort_.visibleRowCount;
+ var columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
+ this.characterSize_.width);
- if (width == this.screenSize.width && height == this.screenSize.height) {
- this.syncCursorPosition_();
- return;
- }
+ if (columnCount != this.screenSize.width)
+ this.realizeWidth_(columnCount);
- this.screenSize.resize(width, height);
+ var rowCount = this.scrollPort_.visibleRowCount;
- var screenHeight = this.screen_.getHeight();
+ if (rowCount != this.screenSize.height)
+ this.realizeHeight_(rowCount);
- var deltaRows = this.screenSize.height - screenHeight;
-
- var cursor = this.saveCursor();
-
- if (deltaRows < 0) {
- // Screen got smaller.
- deltaRows *= -1;
- while (deltaRows) {
- var lastRow = this.getRowCount() - 1;
- if (lastRow - this.scrollbackRows_.length == cursor.row)
- break;
-
- if (this.getRowText(lastRow))
- break;
-
- this.screen_.popRow();
- deltaRows--;
- }
-
- var ary = this.screen_.shiftRows(deltaRows);
- this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
-
- // We just removed rows from the top of the screen, we need to update
- // the cursor to match.
- cursor.row -= deltaRows;
-
- } 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(
- this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
- this.screen_.unshiftRows(rows);
- deltaRows -= scrollbackCount;
- cursor.row += scrollbackCount;
- }
-
- if (deltaRows)
- this.appendRows_(deltaRows);
- }
-
- this.screen_.setColumnCount(this.screenSize.width);
- this.restoreCursor(cursor);
- this.syncCursorPosition_();
+ this.scheduleSyncCursorPosition_();
};
/**