terminal: makes bell works for xterm.js

xterm.js v5 changes how the bell works. This CL makes it working again.

Bug: b/236205389
Change-Id: I6f2349cbbd384007cde41c68b28839e3823206dd
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/3944189
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 ccdec56..aead328 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -192,6 +192,49 @@
   }
 }
 
+class Bell {
+  constructor() {
+    this.showNotification = false;
+
+    /** @type {?Audio} */
+    this.audio_ = null;
+    /** @type {?Notification} */
+    this.notification_ = null;
+    this.coolDownUntil_ = 0;
+  }
+
+  /**
+   * Set whether a bell audio should be played.
+   *
+   * @param {boolean} value
+   */
+  set playAudio(value) {
+    this.audio_ = value ?
+        new Audio(lib.resource.getDataUrl('hterm/audio/bell')) : null;
+  }
+
+  ring() {
+    const now = Date.now();
+    if (now < this.coolDownUntil_) {
+      return;
+    }
+    this.coolDownUntil_ = now + 500;
+
+    this.audio_?.play();
+    if (this.showNotification && !document.hasFocus() && !this.notification_) {
+      this.notification_ = new Notification(
+          `\u266A ${document.title} \u266A`,
+          {icon: lib.resource.getDataUrl('hterm/images/icon-96')});
+      // Close the notification after a timeout. Note that this is different
+      // from hterm's behavior, but I think it makes more sense to do so.
+      setTimeout(() => {
+        this.notification_.close();
+        this.notification_ = null;
+      }, 5000);
+    }
+  }
+}
+
 /**
  * A terminal class that 1) uses xterm.js and 2) behaves like a `hterm.Terminal`
  * so that it can be used in existing code.
@@ -225,7 +268,7 @@
 
     /** @type {?Element} */
     this.container_;
-
+    this.bell_ = new Bell();
     this.scheduleFit_ = delayedScheduler(() => this.fit_(),
         testParams ? 0 : 250);
 
@@ -250,6 +293,7 @@
     this.term.onBinary((data) => this.io.onVTKeystroke(data));
     this.term.onTitleChange((title) => document.title = title);
     this.term.onSelectionChange(() => this.copySelection_());
+    this.term.onBell(() => this.ringBell());
 
     /**
      * A mapping from key combo (see encodeKeyCombo()) to a handler function.
@@ -290,6 +334,11 @@
     this.observePrefs_();
   }
 
+  /** @override */
+  ringBell() {
+    this.bell_.ring();
+  }
+
   /**
    * Install stubs for stuff that we haven't implemented yet so that the code
    * still runs.
@@ -510,13 +559,10 @@
     // TODO(lxj): support option "lineHeight", "scrollback".
     this.prefs_.addObservers(null, {
       'audible-bell-sound': (v) => {
-        if (v) {
-          this.updateOption_('bellStyle', 'sound', false);
-          this.updateOption_('bellSound', lib.resource.getDataUrl(
-              'hterm/audio/bell'), false);
-        } else {
-          this.updateOption_('bellStyle', 'none', false);
-        }
+        this.bell_.playAudio = !!v;
+      },
+      'desktop-notification-bell': (v) => {
+        this.bell_.showNotification = v;
       },
       'background-color': (v) => {
         this.updateTheme_({background: v});