terminal: fix arrow and 6-pack keys with modifiers for xterm.js
Fixed: b/271450792
Change-Id: If5142710851d3ee3f4f1069acd982de500a3b4e2
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/4310761
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 ab02f4d..72f36be 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -34,7 +34,7 @@
// This is just a static map from key names to key codes. It helps make the code
// a bit more readable.
-const keyCodes = hterm.Parser.identifiers.keyCodes;
+export const keyCodes = hterm.Parser.identifiers.keyCodes;
/**
* Encode a key combo (i.e. modifiers + a normal key) to an unique number.
@@ -85,6 +85,25 @@
];
/**
+ * The value is the CSI code to send when no modifier keys are pressed.
+ *
+ * @type {!Map<number, string>}
+ */
+const ARROW_AND_SIX_PACK_KEYS = new Map([
+ [keyCodes.UP, '\x1b[A'],
+ [keyCodes.DOWN, '\x1b[B'],
+ [keyCodes.RIGHT, '\x1b[C'],
+ [keyCodes.LEFT, '\x1b[D'],
+ // 6-pack keys.
+ [keyCodes.INSERT, '\x1b[2~'],
+ [keyCodes.DEL, '\x1b[3~'],
+ [keyCodes.HOME, '\x1b[H'],
+ [keyCodes.END, '\x1b[F'],
+ [keyCodes.PAGE_UP, '\x1b[5~'],
+ [keyCodes.PAGE_DOWN, '\x1b[6~'],
+]);
+
+/**
* @typedef {{
* term: !Terminal,
* fontManager: !FontManager,
@@ -471,8 +490,7 @@
this.scheduleResetKeyDownHandlers_ =
delayedScheduler(() => this.resetKeyDownHandlers_(), 250);
- this.term.attachCustomKeyEventHandler(
- this.customKeyEventHandler_.bind(this));
+ this.term.attachCustomKeyEventHandler((ev) => !this.handleKeyEvent_(ev));
this.io = new XtermTerminalIO(this);
this.notificationCenter_ = null;
@@ -1005,7 +1023,7 @@
/**
* @param {!MouseEvent} e
*/
- async onMouseDown_(e) {
+ onMouseDown_(e) {
if (this.term.modes.mouseTrackingMode !== 'none') {
// xterm.js is in mouse mode and will handle the event.
return;
@@ -1020,11 +1038,14 @@
if (e.button === MIDDLE || (e.button === RIGHT &&
this.prefs_.getBoolean('mouse-right-click-paste'))) {
- // Paste.
- if (navigator.clipboard && navigator.clipboard.readText) {
- const text = await navigator.clipboard.readText();
- this.term.paste(text);
- }
+ this.pasteFromClipboard_();
+ }
+ }
+
+ async pasteFromClipboard_() {
+ const text = await navigator.clipboard?.readText?.();
+ if (text) {
+ this.term.paste(text);
}
}
@@ -1093,28 +1114,102 @@
/**
* @param {!KeyboardEvent} ev
- * @return {boolean} Return false if xterm.js should not handle the key event.
+ * @return {boolean} Return true if the key event is handled.
*/
- customKeyEventHandler_(ev) {
+ handleKeyEvent_(ev) {
// Without this, <alt-tab> (or <alt-shift-tab) is consumed by xterm.js
- // (instead the OS) when terminal is full screen.
+ // (instead of the OS) when terminal is full screen.
if (ev.altKey && ev.keyCode === 9) {
- return false;
+ return true;
}
const modifiers = (ev.shiftKey ? Modifier.Shift : 0) |
(ev.altKey ? Modifier.Alt : 0) |
(ev.ctrlKey ? Modifier.Ctrl : 0) |
(ev.metaKey ? Modifier.Meta : 0);
+
+ if (this.handleArrowAndSixPackKeys_(ev, modifiers)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ return true;
+ }
+
const handler = this.keyDownHandlers_.get(
encodeKeyCombo(modifiers, ev.keyCode));
if (handler) {
if (ev.type === 'keydown') {
handler(ev);
}
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle arrow keys and the "six pack keys" (e.g. home, insert...) because
+ * xterm.js does not always handle them correctly with modifier keys.
+ *
+ * The behavior here is mostly the same as hterm, but there are some
+ * differences. For example, we send a code instead of scrolling the screen
+ * with shift+up/down to follow the behavior of xterm and other popular
+ * terminals (e.g. gnome-terminal).
+ *
+ * We don't use `this.keyDownHandlers_` for this because it needs one entry
+ * per modifier combination.
+ *
+ * @param {!KeyboardEvent} ev
+ * @param {number} modifiers
+ * @return {boolean} Return true if the key event is handled.
+ */
+ handleArrowAndSixPackKeys_(ev, modifiers) {
+ let code = ARROW_AND_SIX_PACK_KEYS.get(ev.keyCode);
+ if (!code) {
return false;
}
+ // For this case, we need to consider the "application cursor mode". We will
+ // just let xterm.js handle it.
+ if (modifiers === 0) {
+ return false;
+ }
+
+ if (ev.type !== 'keydown') {
+ // Do nothing for non-keydown event, and also don't let xterm.js handle
+ // it.
+ return true;
+ }
+
+ // Special handling if only shift is depressed.
+ if (modifiers === Modifier.Shift) {
+ switch (ev.keyCode) {
+ case keyCodes.INSERT:
+ this.pasteFromClipboard_();
+ return true;
+ case keyCodes.PAGE_UP:
+ this.term.scrollPages(-1);
+ return true;
+ case keyCodes.PAGE_DOWN:
+ this.term.scrollPages(1);
+ return true;
+ case keyCodes.HOME:
+ this.term.scrollToTop();
+ return true;
+ case keyCodes.END:
+ this.term.scrollToBottom();
+ return true;
+ }
+ }
+
+ const mod = `;${modifiers + 1}`;
+ if (code.length === 3) {
+ // Convert code from "CSI x" to "CSI 1 mod x";
+ code = '\x1b[1' + mod + code[2];
+ } else {
+ // Convert code from "CSI ... ~" to "CSI ... mod ~";
+ code = code.slice(0, -1) + mod + '~';
+ }
+ this.io.onVTKeystroke(code);
return true;
}