terminal: make XtermTerminal works with ssh
We also use XtermTerminalIO, a subclass of hterm.Terminal.IO, instead of
letting XtermTerminal pretend to be the io class now.
Bug: b/236205389
Change-Id: I72a1370f86f35f0acf1d3591686a62252d8236ce
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/3837635
Reviewed-by: Joel Hockey <joelhockey@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
diff --git a/terminal/js/terminal_emulator.js b/terminal/js/terminal_emulator.js
index 00f3f49..1e9f9f2 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -10,8 +10,8 @@
// terminal_tests.js for XtermTerminal.
import {Terminal, FitAddon, WebglAddon} from './xterm.js';
-import {FontManager, TERMINAL_EMULATORS, delayedScheduler, fontManager,
- getOSInfo, sleep} from './terminal_common.js';
+import {FontManager, ORIGINAL_URL, TERMINAL_EMULATORS, delayedScheduler,
+ fontManager, getOSInfo, sleep} from './terminal_common.js';
const ANSI_COLOR_NAMES = [
'black',
@@ -34,7 +34,6 @@
const PrefToXtermOptions = {
'font-family': 'fontFamily',
- 'font-size': 'fontSize',
};
/**
@@ -47,13 +46,46 @@
export let XtermTerminalTestParams;
/**
+ * A "terminal io" class for xterm. We don't want the vanilla hterm.Terminal.IO
+ * because it always convert utf8 data to strings, which is not necessary for
+ * xterm.
+ */
+class XtermTerminalIO extends hterm.Terminal.IO {
+ /** @override */
+ writeUTF8(buffer) {
+ this.terminal_.write(new Uint8Array(buffer));
+ }
+
+ /** @override */
+ writelnUTF8(buffer) {
+ this.terminal_.writeln(new Uint8Array(buffer));
+ }
+
+ /** @override */
+ print(string) {
+ this.terminal_.write(string);
+ }
+
+ /** @override */
+ writeUTF16(string) {
+ this.print(string);
+ }
+
+ /** @override */
+ println(string) {
+ this.terminal_.writeln(string);
+ }
+
+ /** @override */
+ writelnUTF16(string) {
+ this.println(string);
+ }
+}
+
+/**
* A terminal class that 1) uses xterm.js and 2) behaves like a `hterm.Terminal`
* so that it can be used in existing code.
*
- * TODO: Currently, this also behaves like a `hterm.Terminal.IO` object, which
- * kind of works but it is weird. We might want to just use the real
- * `hterm.Terminal.IO`.
- *
* @extends {hterm.Terminal}
* @unrestricted
*/
@@ -67,6 +99,7 @@
* }} args
*/
constructor({storage, profileId, enableWebGL, testParams}) {
+ this.profileId_ = profileId;
/** @type {!hterm.PreferenceManager} */
this.prefs_ = new hterm.PreferenceManager(storage, profileId);
this.enableWebGL_ = enableWebGL;
@@ -89,12 +122,13 @@
this.observePrefs_();
- this.term.onResize(({cols, rows}) => this.onTerminalResize(cols, rows));
- this.term.onData((data) => this.sendString(data));
+ this.term.onResize(({cols, rows}) => this.io.onTerminalResize(cols, rows));
+ // We could also use `this.io.sendString()` except for the nassh exit
+ // prompt, which only listens to onVTKeystroke().
+ this.term.onData((data) => this.io.onVTKeystroke(data));
- // Also pretends to be a `hterm.Terminal.IO` object.
- this.io = this;
- this.terminal_ = this;
+ this.io = new XtermTerminalIO(this);
+ this.notificationCenter_ = null;
}
/**
@@ -116,17 +150,10 @@
this.keyboard.keyMap.keyDefs[78] = {};
const methodNames = [
- 'hideOverlay',
'setAccessibilityEnabled',
'setBackgroundImage',
'setCursorPosition',
'setCursorVisible',
- 'setTerminalProfile',
- 'showOverlay',
-
- // This two are for `hterm.Terminal.IO`.
- 'push',
- 'pop',
];
for (const name of methodNames) {
@@ -138,6 +165,12 @@
console.warn('.contextMenu.setItems() is not implemented');
},
};
+
+ this.vt = {
+ resetParseState: () => {
+ console.warn('.vt.resetParseState() is not implemented');
+ },
+ };
}
/**
@@ -149,6 +182,25 @@
this.onTerminalReady();
}
+ /**
+ * Write data to the terminal.
+ *
+ * @param {string|!Uint8Array} data string for UTF-16 data, Uint8Array for
+ * UTF-8 data
+ */
+ write(data) {
+ this.term.write(data);
+ }
+
+ /**
+ * Like `this.write()` but also write a line break.
+ *
+ * @param {string|!Uint8Array} data
+ */
+ writeln(data) {
+ this.term.writeln(data);
+ }
+
get screenSize() {
return new hterm.Size(this.term.cols, this.term.rows);
}
@@ -169,7 +221,24 @@
if (this.enableWebGL_) {
this.term.loadAddon(new WebglAddon());
}
+ this.term.focus();
(new ResizeObserver(() => this.scheduleFit_())).observe(elem);
+ // TODO: Make a11y work. Maybe we can just use `hterm.AccessibilityReader`.
+ this.notificationCenter_ = new hterm.NotificationCenter(document.body);
+ }
+
+ /** @override */
+ showOverlay(msg, timeout = 1500) {
+ if (this.notificationCenter_) {
+ this.notificationCenter_.show(msg, {timeout});
+ }
+ }
+
+ /** @override */
+ hideOverlay() {
+ if (this.notificationCenter_) {
+ this.notificationCenter_.hide();
+ }
}
/** @override */
@@ -182,36 +251,25 @@
return window.document;
}
- /**
- * This is a method from `hterm.Terminal.IO`.
- *
- * @param {!ArrayBuffer|!Array<number>} buffer The UTF-8 data to print.
- */
- writeUTF8(buffer) {
- this.term.write(new Uint8Array(buffer));
+ /** @override */
+ reset() {
+ this.term.reset();
}
/** @override */
- print(data) {
- this.term.write(data);
+ setProfile(profileId, callback = undefined) {
+ this.prefs_.setProfile(profileId, callback);
}
- /**
- * This is a method from `hterm.Terminal.IO`.
- *
- * @param {string} data
- */
- println(data) {
- this.term.writeln(data);
+ /** @override */
+ interpret(string) {
+ this.term.write(string);
}
- /**
- * This is a method from `hterm.Terminal.IO`.
- *
- * @param {number} width
- * @param {number} height
- */
- onTerminalResize(width, height) {}
+ /** @override */
+ focus() {
+ this.term.focus();
+ }
/** @override */
onOpenOptionsPage() {}
@@ -219,20 +277,6 @@
/** @override */
onTerminalReady() {}
- /**
- * This is a method from `hterm.Terminal.IO`.
- *
- * @param {string} v
- */
- onVTKeystoke(v) {}
-
- /**
- * This is a method from `hterm.Terminal.IO`.
- *
- * @param {string} v
- */
- sendString(v) {}
-
observePrefs_() {
for (const pref in PrefToXtermOptions) {
this.prefs_.addObserver(pref, (v) => {
@@ -240,13 +284,31 @@
});
}
+ // This is for this.notificationCenter_.
+ const setHtermCSSVariable = (name, value) => {
+ document.body.style.setProperty(`--hterm-${name}`, value);
+ };
+
+ const setHtermColorCSSVariable = (name, color) => {
+ const css = lib.notNull(lib.colors.normalizeCSS(color));
+ const rgb = lib.colors.crackRGB(css).slice(0, 3).join(',');
+ setHtermCSSVariable(name, rgb);
+ };
+
+ this.prefs_.addObserver('font-size', (v) => {
+ this.updateOption_('fontSize', v);
+ setHtermCSSVariable('font-size', `${v}px`);
+ });
+
// Theme-related preference items.
this.prefs_.addObservers(null, {
'background-color': (v) => {
this.updateTheme_({background: v});
+ setHtermColorCSSVariable('background-color', v);
},
'foreground-color': (v) => {
this.updateTheme_({foreground: v});
+ setHtermColorCSSVariable('foreground-color', v);
},
'cursor-color': (v) => {
this.updateTheme_({cursor: v});
@@ -381,9 +443,13 @@
let config = TERMINAL_EMULATORS.get('hterm');
if (getOSInfo().alternative_emulator) {
- const prefKey = `/hterm/profiles/${profileId}/terminal-emulator`;
+ // TODO: remove the url param logic. This is temporary to make manual
+ // testing a bit easier, which is also why this is not in
+ // './js/terminal_info.js'.
+ const emulator = ORIGINAL_URL.searchParams.get('emulator') ||
+ await storage.getItem(`/hterm/profiles/${profileId}/terminal-emulator`);
// Use the default (i.e. first) one if the pref is not set or invalid.
- config = TERMINAL_EMULATORS.get(await storage.getItem(prefKey)) ||
+ config = TERMINAL_EMULATORS.get(emulator) ||
TERMINAL_EMULATORS.values().next().value;
console.log('Terminal emulator config: ', config);
}