terminal: support background image for xterm.js
Bug: b/236205389
Change-Id: I17f1795bca77a7bd295f0a52904e675efb7c7dde
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/3959038
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 31db963..ed7d19a 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -301,6 +301,43 @@
}
}
+const BACKGROUND_IMAGE_KEY = 'background-image';
+
+class BackgroundImageWatcher {
+ /**
+ * @param {!hterm.PreferenceManager} prefs
+ * @param {function(string)} onChange This is called with the background image
+ * (could be empty) whenever it changes.
+ */
+ constructor(prefs, onChange) {
+ this.prefs_ = prefs;
+ this.onChange_ = onChange;
+ }
+
+ /**
+ * Call once to start watching for background image changes.
+ */
+ watch() {
+ window.addEventListener('storage', (e) => {
+ if (e.key === BACKGROUND_IMAGE_KEY) {
+ this.onChange_(this.getBackgroundImage());
+ }
+ });
+ this.prefs_.addObserver(BACKGROUND_IMAGE_KEY, () => {
+ this.onChange_(this.getBackgroundImage());
+ });
+ }
+
+ getBackgroundImage() {
+ const image = window.localStorage.getItem(BACKGROUND_IMAGE_KEY);
+ if (image) {
+ return `url(${image})`;
+ }
+
+ return this.prefs_.getString(BACKGROUND_IMAGE_KEY);
+ }
+}
+
/**
* A terminal class that 1) uses xterm.js and 2) behaves like a `hterm.Terminal`
* so that it can be used in existing code.
@@ -399,6 +436,9 @@
this.a11yButtons_ = null;
this.copyNotice_ = null;
this.scrollOnOutputListener_ = null;
+ this.backgroundImageWatcher_ = new BackgroundImageWatcher(this.prefs_,
+ this.setBackgroundImage.bind(this));
+ this.webglAddon_ = null;
this.term.options.linkHandler = new LinkHandler(this.term);
this.term.options.theme = {
@@ -451,6 +491,16 @@
this.term.options.screenReaderMode = enabled;
}
+ hasBackgroundImage() {
+ return !!this.container_.style.backgroundImage;
+ }
+
+ /** @override */
+ setBackgroundImage(image) {
+ this.container_.style.backgroundImage = image || '';
+ this.updateBackgroundColor_(this.prefs_.getString('background-color'));
+ }
+
/**
* Install stubs for stuff that we haven't implemented yet so that the code
* still runs.
@@ -471,7 +521,6 @@
const methodNames = [
'eraseLine',
- 'setBackgroundImage',
'setCursorColumn',
'setCursorPosition',
'setCursorVisible',
@@ -563,6 +612,8 @@
*/
decorate(elem) {
this.container_ = elem;
+ elem.style.backgroundSize = '100% 100%';
+
(async () => {
await new Promise((resolve) => this.prefs_.readStorage(resolve));
// This will trigger all the observers to set the terminal options before
@@ -573,11 +624,15 @@
this.prefs_.get('screen-padding-size'));
elem.style.paddingTop = elem.style.paddingLeft = `${screenPaddingSize}px`;
+ this.setBackgroundImage(
+ this.backgroundImageWatcher_.getBackgroundImage());
+ this.backgroundImageWatcher_.watch();
+
this.inited_ = true;
this.term.open(elem);
if (this.enableWebGL_) {
- this.term.loadAddon(new WebglAddon());
+ this.reloadWebglAddon_();
}
this.term.focus();
(new ResizeObserver(() => this.scheduleFit_())).observe(elem);
@@ -685,7 +740,7 @@
this.bell_.showNotification = v;
},
'background-color': (v) => {
- this.updateTheme_({background: v});
+ this.updateBackgroundColor_(v);
setHtermColorCSSVariable('background-color', v);
},
'color-palette-overrides': (v) => {
@@ -760,6 +815,48 @@
}
}
+ reloadWebglAddon_() {
+ if (this.webglAddon_) {
+ this.webglAddon_.dispose();
+ }
+ this.webglAddon_ = new WebglAddon();
+ this.term.loadAddon(this.webglAddon_);
+ }
+
+ /**
+ * Update the background color. This will also adjust the transparency based
+ * on whether there is a background image.
+ *
+ * @param {string} color
+ */
+ updateBackgroundColor_(color) {
+ const hasBackgroundImage = this.hasBackgroundImage();
+
+ // We only set allowTransparency when it is necessary becuase 1) xterm.js
+ // documentation states that allowTransparency can affect performance; 2) I
+ // find that the rendering is better with allowTransparency being false.
+ // This could be a bug with xterm.js.
+ if (!!this.term.options.allowTransparency !== hasBackgroundImage) {
+ this.term.options.allowTransparency = hasBackgroundImage;
+ if (this.enableWebGL_ && this.inited_) {
+ // Setting allowTransparency in the middle messes up webgl rendering,
+ // so we need to reload it here.
+ this.reloadWebglAddon_();
+ }
+ }
+
+ if (this.hasBackgroundImage()) {
+ const css = lib.notNull(lib.colors.normalizeCSS(color));
+ const rgb = lib.colors.crackRGB(css).slice(0, 3).join(',');
+ // Note that we still want to set the RGB part correctly even though it is
+ // completely transparent. This is because the background color without
+ // the alpha channel is used in reverse video mode.
+ color = `rgba(${rgb}, 0)`;
+ }
+
+ this.updateTheme_({background: color});
+ }
+
/**
* @param {!Object} theme
*/
@@ -1063,6 +1160,8 @@
setWithShiftVersion(Modifier.Ctrl, keyCodes.C, this.ctrlCKeyDownHandler_);
setWithShiftVersion(Modifier.Ctrl, keyCodes.V, this.ctrlVKeyDownHandler_);
}
+
+ handleOnTerminalReady() {}
}
class HtermTerminal extends hterm.Terminal {
@@ -1071,7 +1170,14 @@
super.decorate(div);
definePrefs(this.getPrefs());
+ }
+ /**
+ * This needs to be called in the `onTerminalReady()` callback. This is
+ * awkward, but it is temporary since we will drop support for hterm at some
+ * point.
+ */
+ handleOnTerminalReady() {
const fontManager = new FontManager(this.getDocument());
fontManager.loadPowerlineCSS().then(() => {
const prefs = this.getPrefs();
@@ -1080,6 +1186,11 @@
'font-family',
(v) => fontManager.loadFont(/** @type {string} */(v)));
});
+
+ const backgroundImageWatcher = new BackgroundImageWatcher(this.getPrefs(),
+ (image) => this.setBackgroundImage(image));
+ this.setBackgroundImage(backgroundImageWatcher.getBackgroundImage());
+ backgroundImageWatcher.watch();
}
/**