hterm: change CSI-J-3 to clear scrollback
We've long had CSI-J-3 (clear scrollback) wired to behave like CSI-J-2
(clear screen) because letting the remote clear local scrollback might
not be the best behavior. Add a config setting to control it so we can
change the CSI-J-3 extension to match xterm where we got it from.
Bug: chromium:881507
Change-Id: I13cfa6d087e2ba456c80c9d0c7518b3db650fcfa
Reviewed-on: https://chromium-review.googlesource.com/1214627
Reviewed-by: Vitaliy Shipitsyn <vsh@google.com>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/hterm/js/hterm_preference_manager.js b/hterm/js/hterm_preference_manager.js
index 9bb17bc..2b0b5ef 100644
--- a/hterm/js/hterm_preference_manager.js
+++ b/hterm/js/hterm_preference_manager.js
@@ -289,6 +289,13 @@
'Respect the host\'s attempt to change the text cursor blink status using ' +
'DEC Private Mode 12.'],
+ 'enable-csi-j-3':
+ [hterm.PreferenceManager.categories.Miscellaneous, true, 'bool',
+ 'Whether CSI-J (Erase Display) mode 3 may clear the terminal scrollback ' +
+ 'buffer.\n' +
+ '\n' +
+ 'Enabling this by default is safe.'],
+
'environment':
[hterm.PreferenceManager.categories.Miscellaneous,
{
diff --git a/hterm/js/hterm_terminal.js b/hterm/js/hterm_terminal.js
index ada7478..30c3adc 100644
--- a/hterm/js/hterm_terminal.js
+++ b/hterm/js/hterm_terminal.js
@@ -410,6 +410,10 @@
terminal.vt.enableDec12 = !!v;
},
+ 'enable-csi-j-3': function(v) {
+ terminal.vt.enableCsiJ3 = !!v;
+ },
+
'font-family': function(v) {
terminal.syncFontFamily();
},
diff --git a/hterm/js/hterm_vt.js b/hterm/js/hterm_vt.js
index 58f471f..fbf9ee4 100644
--- a/hterm/js/hterm_vt.js
+++ b/hterm/js/hterm_vt.js
@@ -80,6 +80,11 @@
this.enableDec12 = false;
/**
+ * Respect the host's attempt to clear the scrollback buffer using CSI-J-3.
+ */
+ this.enableCsiJ3 = true;
+
+ /**
* The expected encoding method for data received from the host.
*/
this.characterEncoding = 'utf-8';
@@ -2152,9 +2157,9 @@
} else if (arg == 2) {
this.terminal.clear();
} else if (arg == 3) {
- // The xterm docs say this means "Erase saved lines", but we'll just clear
- // the display since killing the scrollback seems rude.
- this.terminal.clear();
+ if (this.enableCsiJ3) {
+ this.terminal.clearScrollback();
+ }
}
};
diff --git a/hterm/js/hterm_vt_tests.js b/hterm/js/hterm_vt_tests.js
index 7c841d7..66629d2 100644
--- a/hterm/js/hterm_vt_tests.js
+++ b/hterm/js/hterm_vt_tests.js
@@ -3399,3 +3399,160 @@
result.pass();
});
+
+/**
+ * Verify CSI-J-0 (erase below) works.
+ */
+hterm.VT.Tests.addTest('csi-j-0', function(result, cx) {
+ const terminal = this.terminal;
+
+ // Fill the screen with something useful.
+ for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+ terminal.interpret(`ab${i}\n\r`);
+ }
+ const rowCount = terminal.getRowCount();
+ terminal.scrollEnd();
+ terminal.scrollPort_.redraw_();
+
+ // Move to the middle of the screen.
+ terminal.setCursorPosition(3, 1);
+ result.assertEQ('ab9', terminal.getRowText(9));
+ result.assertEQ('ab10', terminal.getRowText(10));
+
+ // Clear after & including the cursor (implicit arg=0).
+ terminal.interpret('\x1b[J');
+ result.assertEQ(3, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('ab9', terminal.getRowText(9));
+ result.assertEQ('a', terminal.getRowText(10));
+ result.assertEQ('', terminal.getRowText(11));
+
+ // Move up and clear after & including the cursor (explicit arg=0).
+ terminal.setCursorPosition(2, 1);
+ terminal.interpret('\x1b[0J');
+ result.assertEQ(2, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('ab8', terminal.getRowText(8));
+ result.assertEQ('a', terminal.getRowText(9));
+ result.assertEQ('', terminal.getRowText(10));
+
+ // The scrollback should stay intact.
+ result.assertEQ('ab0', terminal.getRowText(0));
+ result.assertEQ(rowCount, terminal.getRowCount());
+
+ result.pass();
+});
+
+/**
+ * Verify CSI-J-1 (erase above) works.
+ */
+hterm.VT.Tests.addTest('csi-j-1', function(result, cx) {
+ const terminal = this.terminal;
+
+ // Fill the screen with something useful.
+ for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+ terminal.interpret(`ab${i}\n\r`);
+ }
+ const rowCount = terminal.getRowCount();
+ terminal.scrollEnd();
+ terminal.scrollPort_.redraw_();
+
+ // Move to the middle of the screen.
+ terminal.setCursorPosition(3, 1);
+ result.assertEQ('ab9', terminal.getRowText(9));
+ result.assertEQ('ab10', terminal.getRowText(10));
+
+ // Clear before & including the cursor (arg=1).
+ terminal.interpret('\x1b[1J');
+ result.assertEQ(3, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('', terminal.getRowText(9));
+ result.assertEQ(' 10', terminal.getRowText(10));
+ result.assertEQ('ab11', terminal.getRowText(11));
+
+ // The scrollback should stay intact.
+ result.assertEQ('ab0', terminal.getRowText(0));
+ result.assertEQ(rowCount, terminal.getRowCount());
+
+ result.pass();
+});
+
+/**
+ * Verify CSI-J-2 (erase screen) works.
+ */
+hterm.VT.Tests.addTest('csi-j-2', function(result, cx) {
+ const terminal = this.terminal;
+
+ // Fill the screen with something useful.
+ for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+ terminal.interpret(`ab${i}\n\r`);
+ }
+ const rowCount = terminal.getRowCount();
+ terminal.scrollEnd();
+ terminal.scrollPort_.redraw_();
+
+ // Move to the middle of the screen.
+ terminal.setCursorPosition(3, 1);
+ result.assertEQ('ab9', terminal.getRowText(9));
+ result.assertEQ('ab10', terminal.getRowText(10));
+
+ // Clear the screen (arg=2).
+ terminal.interpret('\x1b[2J');
+ result.assertEQ(3, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('', terminal.getRowText(9));
+ result.assertEQ('', terminal.getRowText(10));
+ result.assertEQ('', terminal.getRowText(11));
+
+ // The scrollback should stay intact.
+ result.assertEQ('ab0', terminal.getRowText(0));
+ result.assertEQ(rowCount, terminal.getRowCount());
+
+ result.pass();
+});
+
+/**
+ * Verify CSI-J-3 (erase scrollback) works.
+ */
+hterm.VT.Tests.addTest('csi-j-3', function(result, cx) {
+ const terminal = this.terminal;
+
+ // Fill the screen with something useful.
+ for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+ terminal.interpret(`ab${i}\n\r`);
+ }
+ const rowCount = terminal.getRowCount();
+ terminal.scrollEnd();
+ terminal.scrollPort_.redraw_();
+
+ // Move to the middle of the screen.
+ terminal.setCursorPosition(3, 1);
+ result.assertEQ('ab9', terminal.getRowText(9));
+ result.assertEQ('ab10', terminal.getRowText(10));
+
+ // Disable this feature. It should make it a nop.
+ terminal.vt.enableCsiJ3 = false;
+ terminal.interpret('\x1b[3J');
+ result.assertEQ(3, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('ab0', terminal.getRowText(0));
+ result.assertEQ(rowCount, terminal.getRowCount());
+
+ // Re-enable the feature.
+ terminal.vt.enableCsiJ3 = true;
+
+ // Clear the scrollback (arg=3).
+ // The current screen should stay intact.
+ terminal.interpret('\x1b[3J');
+ result.assertEQ(3, terminal.getCursorRow());
+ result.assertEQ(1, terminal.getCursorColumn());
+ result.assertEQ('ab7', terminal.getRowText(0));
+ result.assertEQ('ab8', terminal.getRowText(1));
+ result.assertEQ('ab11', terminal.getRowText(this.visibleRowCount - 2));
+
+ // The scrollback should be gone.
+ result.assertEQ(this.visibleRowCount, terminal.getRowCount());
+ result.assertEQ([], terminal.scrollbackRows_);
+
+ result.pass();
+});