blob: a026cd9d5af85296518b09bc1a4dc910f860e602 [file] [log] [blame]
rgindafeaf3142012-01-31 15:14:20 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rgindacbbd7482012-06-13 15:06:16 -07005'use strict';
6
rgindafeaf3142012-01-31 15:14:20 -08007/**
8 * Keyboard handler.
9 *
10 * Consumes onKey* events and invokes onVTKeystroke on the associated
11 * hterm.Terminal object.
12 *
13 * See also: [XTERM] as referenced in vt.js.
14 *
Joel Hockey0f933582019-08-27 18:01:51 -070015 * @param {!hterm.Terminal} terminal The Terminal object associated with this
16 * keyboard.
17 * @constructor
rgindafeaf3142012-01-31 15:14:20 -080018 */
19hterm.Keyboard = function(terminal) {
20 // The parent vt interpreter.
21 this.terminal = terminal;
22
23 // The element we're currently capturing keyboard events for.
24 this.keyboardElement_ = null;
25
26 // The event handlers we are interested in, and their bound callbacks, saved
27 // so they can be uninstalled with removeEventListener, when required.
28 this.handlers_ = [
Mike Frysingerc5271612017-04-12 02:24:30 -040029 ['focusout', this.onFocusOut_.bind(this)],
Robert Ginda11390c52012-09-13 14:53:34 -070030 ['keydown', this.onKeyDown_.bind(this)],
Andrew de los Reyes574e10e2013-04-04 09:31:57 -070031 ['keypress', this.onKeyPress_.bind(this)],
32 ['keyup', this.onKeyUp_.bind(this)],
Mike Frysinger989f34b2020-04-08 00:53:43 -040033 ['textInput', this.onTextInput_.bind(this)],
rgindafeaf3142012-01-31 15:14:20 -080034 ];
35
36 /**
37 * The current key map.
38 */
rgindacbbd7482012-06-13 15:06:16 -070039 this.keyMap = new hterm.Keyboard.KeyMap(this);
rgindafeaf3142012-01-31 15:14:20 -080040
Joel Hockeyed101442019-09-20 15:07:13 -070041 this.bindings = new hterm.Keyboard.Bindings();
Robert Gindaf82267d2015-06-09 15:32:04 -070042
rgindafeaf3142012-01-31 15:14:20 -080043 /**
Mike Frysinger2edd8e42020-04-19 08:25:03 -040044 * none: Disable the AltGr emulation.
Robert Ginda034ffa72015-02-26 14:02:37 -080045 * ctrl-alt: Assume Ctrl+Alt means AltGr.
46 * left-alt: Assume left Alt means AltGr.
47 * right-alt: Assume right Alt means AltGr.
48 */
49 this.altGrMode = 'none';
50
51 /**
Joel Hockey46a6e1d2020-03-11 20:01:57 -070052 * If true, Shift+Insert will fall through to the browser as a paste.
rginda4bba5e12012-06-20 16:15:30 -070053 * If false, the keystroke will be sent to the host.
54 */
Robert Ginda57f03b42012-09-13 11:02:48 -070055 this.shiftInsertPaste = true;
rginda4bba5e12012-06-20 16:15:30 -070056
57 /**
rgindafeaf3142012-01-31 15:14:20 -080058 * If true, home/end will control the terminal scrollbar and shift home/end
59 * will send the VT keycodes. If false then home/end sends VT codes and
60 * shift home/end scrolls.
61 */
Robert Ginda57f03b42012-09-13 11:02:48 -070062 this.homeKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080063
64 /**
65 * Same as above, except for page up/page down.
66 */
Robert Ginda57f03b42012-09-13 11:02:48 -070067 this.pageKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080068
69 /**
Joel Hockey46a6e1d2020-03-11 20:01:57 -070070 * If true, Ctrl+Plus/Minus/Zero controls zoom.
71 * If false, Ctrl+Shift+Plus/Minus/Zero controls zoom, Ctrl+Minus sends ^_,
72 * Ctrl+Plus/Zero do nothing.
Robert Ginda7e5e9522014-03-14 12:23:58 -070073 */
74 this.ctrlPlusMinusZeroZoom = true;
75
76 /**
Robert Gindafb5a3f92014-05-13 14:12:00 -070077 * Ctrl+C copies if true, sends ^C to host if false.
78 * Ctrl+Shift+C sends ^C to host if true, copies if false.
79 */
80 this.ctrlCCopy = false;
81
82 /**
83 * Ctrl+V pastes if true, sends ^V to host if false.
84 * Ctrl+Shift+V sends ^V to host if true, pastes if false.
Leonardo Mesquita61e7c312014-01-04 12:53:12 +010085 */
86 this.ctrlVPaste = false;
87
88 /**
rgindafeaf3142012-01-31 15:14:20 -080089 * Enable/disable application keypad.
90 *
91 * This changes the way numeric keys are sent from the keyboard.
92 */
93 this.applicationKeypad = false;
94
95 /**
96 * Enable/disable the application cursor mode.
97 *
98 * This changes the way cursor keys are sent from the keyboard.
99 */
100 this.applicationCursor = false;
101
102 /**
103 * If true, the backspace should send BS ('\x08', aka ^H). Otherwise
104 * the backspace key should send '\x7f'.
105 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700106 this.backspaceSendsBackspace = false;
rgindafeaf3142012-01-31 15:14:20 -0800107
108 /**
109 * Set whether the meta key sends a leading escape or not.
110 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700111 this.metaSendsEscape = true;
rginda30f20f62012-04-05 16:36:19 -0700112
113 /**
Marius Schilder77857b32014-05-14 16:21:26 -0700114 * Set whether meta-V gets passed to host.
115 */
116 this.passMetaV = true;
117
118 /**
rginda39bdf6f2012-04-10 16:50:55 -0700119 * Controls how the alt key is handled.
120 *
121 * escape....... Send an ESC prefix.
122 * 8-bit........ Add 128 to the unshifted character as in xterm.
123 * browser-key.. Wait for the keypress event and see what the browser says.
124 * (This won't work well on platforms where the browser
125 * performs a default action for some alt sequences.)
rginda30f20f62012-04-05 16:36:19 -0700126 *
127 * This setting only matters when alt is distinct from meta (altIsMeta is
128 * false.)
129 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700130 this.altSendsWhat = 'escape';
rginda42ad71d2012-02-16 14:06:28 -0800131
132 /**
133 * Set whether the alt key acts as a meta key, instead of producing 8-bit
134 * characters.
rginda30f20f62012-04-05 16:36:19 -0700135 *
136 * True to enable, false to disable, null to autodetect based on platform.
rginda42ad71d2012-02-16 14:06:28 -0800137 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700138 this.altIsMeta = false;
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700139
140 /**
141 * If true, tries to detect DEL key events that are from alt-backspace on
142 * Chrome OS vs from a true DEL key press.
143 *
144 * Background: At the time of writing, on Chrome OS, alt-backspace is mapped
145 * to DEL. Some users may be happy with this, but others may be frustrated
146 * that it's impossible to do meta-backspace. If the user enables this pref,
147 * we use a trick to tell a true DEL keypress from alt-backspace: on
148 * alt-backspace, we will see the alt key go down, then get a DEL keystroke
Mike Frysingere4533182017-04-19 00:18:29 -0400149 * that indicates that alt is not pressed. See https://crbug.com/174410 .
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700150 */
151 this.altBackspaceIsMetaBackspace = false;
152
153 /**
154 * Used to keep track of the current alt-key state, which is necessary for
Robert Ginda034ffa72015-02-26 14:02:37 -0800155 * the altBackspaceIsMetaBackspace preference above and for the altGrMode
156 * preference. This is a bitmap with where bit positions correspond to the
157 * "location" property of the key event.
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700158 */
Robert Ginda034ffa72015-02-26 14:02:37 -0800159 this.altKeyPressed = 0;
Andrew de los Reyes6af23ae2013-04-04 14:17:50 -0700160
161 /**
162 * If true, Chrome OS media keys will be mapped to their F-key equivalent.
163 * E.g. "Back" will be mapped to F1. If false, Chrome will handle the keys.
164 */
165 this.mediaKeysAreFKeys = false;
Brad Town5f375a42015-03-12 01:30:58 -0700166
167 /**
168 * Holds the previous setting of altSendsWhat when DECSET 1039 is used. When
169 * DECRST 1039 is used, altSendsWhat is changed back to this and this is
170 * nulled out.
171 */
172 this.previousAltSendsWhat_ = null;
rgindafeaf3142012-01-31 15:14:20 -0800173};
174
175/**
rgindafeaf3142012-01-31 15:14:20 -0800176 * Special handling for keyCodes in a keyboard layout.
Joel Hockey0f933582019-08-27 18:01:51 -0700177 *
178 * @enum {symbol}
179 * @const
rgindafeaf3142012-01-31 15:14:20 -0800180 */
181hterm.Keyboard.KeyActions = {
182 /**
183 * Call preventDefault and stopPropagation for this key event and nothing
184 * else.
185 */
Mike Frysingerc8fe3092019-01-04 00:45:18 -0500186 CANCEL: Symbol('CANCEL'),
rgindafeaf3142012-01-31 15:14:20 -0800187
188 /**
189 * This performs the default terminal action for the key. If used in the
190 * 'normal' action and the the keystroke represents a printable key, the
191 * character will be sent to the host. If used in one of the modifier
192 * actions, the terminal will perform the normal action after (possibly)
193 * altering it.
194 *
195 * - If the normal sequence starts with CSI, the sequence will be adjusted
196 * to include the modifier parameter as described in [XTERM] in the final
197 * table of the "PC-Style Function Keys" section.
198 *
199 * - If the control key is down and the key represents a printable character,
200 * and the uppercase version of the unshifted keycap is between
201 * 64 (ASCII '@') and 95 (ASCII '_'), then the uppercase version of the
202 * unshifted keycap minus 64 is sent. This makes '^@' send '\x00' and
203 * '^_' send '\x1f'. (Note that one higher that 0x1f is 0x20, which is
204 * the first printable ASCII value.)
205 *
206 * - If the alt key is down and the key represents a printable character then
207 * the value of the character is shifted up by 128.
208 *
209 * - If meta is down and configured to send an escape, '\x1b' will be sent
210 * before the normal action is performed.
211 */
Mike Frysingerc8fe3092019-01-04 00:45:18 -0500212 DEFAULT: Symbol('DEFAULT'),
rgindafeaf3142012-01-31 15:14:20 -0800213
214 /**
215 * Causes the terminal to opt out of handling the key event, instead letting
216 * the browser deal with it.
217 */
Mike Frysingerc8fe3092019-01-04 00:45:18 -0500218 PASS: Symbol('PASS'),
rgindafeaf3142012-01-31 15:14:20 -0800219
220 /**
221 * Insert the first or second character of the keyCap, based on e.shiftKey.
222 * The key will be handled in onKeyDown, and e.preventDefault() will be
223 * called.
224 *
225 * It is useful for a modified key action, where it essentially strips the
226 * modifier while preventing the browser from reacting to the key.
227 */
Mike Frysinger989f34b2020-04-08 00:53:43 -0400228 STRIP: Symbol('STRIP'),
rgindafeaf3142012-01-31 15:14:20 -0800229};
230
Joel Hockeyed101442019-09-20 15:07:13 -0700231/** @typedef {string|!hterm.Keyboard.KeyActions} */
232hterm.Keyboard.KeyAction;
233
rgindafeaf3142012-01-31 15:14:20 -0800234/**
235 * Capture keyboard events sent to the associated element.
236 *
237 * This enables the keyboard. Captured events are consumed by this class
238 * and will not perform their default action or bubble to other elements.
239 *
240 * Passing a null element will uninstall the keyboard handlers.
241 *
Joel Hockeyed101442019-09-20 15:07:13 -0700242 * @param {?Element} element The element whose events should be captured, or
rgindafeaf3142012-01-31 15:14:20 -0800243 * null to disable the keyboard.
244 */
245hterm.Keyboard.prototype.installKeyboard = function(element) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400246 if (element == this.keyboardElement_) {
rgindafeaf3142012-01-31 15:14:20 -0800247 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400248 }
rgindafeaf3142012-01-31 15:14:20 -0800249
Mike Frysingerbdb34802020-04-07 03:47:32 -0400250 if (element && this.keyboardElement_) {
rgindafeaf3142012-01-31 15:14:20 -0800251 this.installKeyboard(null);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400252 }
rgindafeaf3142012-01-31 15:14:20 -0800253
Mike Frysingerdc727792020-04-10 01:41:13 -0400254 for (let i = 0; i < this.handlers_.length; i++) {
255 const handler = this.handlers_[i];
rgindafeaf3142012-01-31 15:14:20 -0800256 if (element) {
257 element.addEventListener(handler[0], handler[1]);
258 } else {
259 this.keyboardElement_.removeEventListener(handler[0], handler[1]);
260 }
261 }
262
263 this.keyboardElement_ = element;
264};
265
266/**
267 * Disable keyboard event capture.
268 *
269 * This will allow the browser to process key events normally.
270 */
271hterm.Keyboard.prototype.uninstallKeyboard = function() {
272 this.installKeyboard(null);
273};
274
275/**
Joel Hockey0f933582019-08-27 18:01:51 -0700276 * Handle textInput events.
Robert Ginda11390c52012-09-13 14:53:34 -0700277 *
Mike Frysingere5a61b02017-09-13 21:58:29 -0400278 * These are generated when using IMEs, Virtual Keyboards (VKs), compose keys,
279 * Unicode input, etc...
Joel Hockey0f933582019-08-27 18:01:51 -0700280 *
Joel Hockeyed101442019-09-20 15:07:13 -0700281 * @param {!InputEvent} e The event to process.
Robert Ginda11390c52012-09-13 14:53:34 -0700282 */
283hterm.Keyboard.prototype.onTextInput_ = function(e) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400284 if (!e.data) {
Robert Ginda11390c52012-09-13 14:53:34 -0700285 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400286 }
Robert Ginda11390c52012-09-13 14:53:34 -0700287
Mike Frysingere5a61b02017-09-13 21:58:29 -0400288 // Just pass the generated buffer straight down. No need for us to split it
289 // up or otherwise parse it ahead of times.
290 this.terminal.onVTKeystroke(e.data);
Robert Ginda11390c52012-09-13 14:53:34 -0700291};
292
293/**
Joel Hockey0f933582019-08-27 18:01:51 -0700294 * Handle keypress events.
Mike Frysingerd30bd512019-01-04 02:00:22 -0500295 *
296 * TODO(vapier): Drop this event entirely and only use keydown.
Joel Hockey0f933582019-08-27 18:01:51 -0700297 *
298 * @param {!KeyboardEvent} e The event to process.
rgindafeaf3142012-01-31 15:14:20 -0800299 */
300hterm.Keyboard.prototype.onKeyPress_ = function(e) {
Mike Frysingerd30bd512019-01-04 02:00:22 -0500301 // FF doesn't set keyCode reliably in keypress events. Stick to the which
302 // field here until we can move to keydown entirely.
303 const key = String.fromCharCode(e.which).toLowerCase();
304 if ((e.ctrlKey || e.metaKey) && (key == 'c' || key == 'v')) {
Rob Spies0bec09b2014-06-06 15:58:09 -0700305 // On FF the key press (not key down) event gets fired for copy/paste.
Zhu Qunying30d40712017-03-14 16:27:00 -0700306 // Let it fall through for the default browser behavior.
Rob Spies0bec09b2014-06-06 15:58:09 -0700307 return;
308 }
309
Adrián Pérez-Orozcod76f2f02018-08-13 10:18:12 -0700310 if (e.keyCode == 9 /* Tab */) {
311 // On FF, a key press event will be fired in addition of key down for the
312 // Tab key if key down isn't handled. This would only happen if a custom
313 // PASS binding has been created and therefore this should be handled by the
314 // browser.
315 return;
316 }
317
Joel Hockeyed101442019-09-20 15:07:13 -0700318 /** @type {string} */
Mike Frysingerdc727792020-04-10 01:41:13 -0400319 let ch;
rginda39bdf6f2012-04-10 16:50:55 -0700320 if (e.altKey && this.altSendsWhat == 'browser-key' && e.charCode == 0) {
321 // If we got here because we were expecting the browser to handle an
322 // alt sequence but it didn't do it, then we might be on an OS without
323 // an enabled IME system. In that case we fall back to xterm-like
324 // behavior.
325 //
326 // This happens here only as a fallback. Typically these platforms should
327 // set altSendsWhat to either 'escape' or '8-bit'.
Joel Hockeyed101442019-09-20 15:07:13 -0700328 ch = String.fromCharCode(e.keyCode);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400329 if (!e.shiftKey) {
rginda39bdf6f2012-04-10 16:50:55 -0700330 ch = ch.toLowerCase();
Mike Frysingerbdb34802020-04-07 03:47:32 -0400331 }
rginda39bdf6f2012-04-10 16:50:55 -0700332
333 } else if (e.charCode >= 32) {
Joel Hockeyed101442019-09-20 15:07:13 -0700334 ch = String.fromCharCode(e.charCode);
rginda39bdf6f2012-04-10 16:50:55 -0700335 }
336
Mike Frysingerbdb34802020-04-07 03:47:32 -0400337 if (ch) {
Joel Hockeyed101442019-09-20 15:07:13 -0700338 this.terminal.onVTKeystroke(ch);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400339 }
rgindafeaf3142012-01-31 15:14:20 -0800340
341 e.preventDefault();
342 e.stopPropagation();
343};
344
Marius Schilder45703172014-12-22 17:10:01 -0800345/**
Marius Schilder9dc111b2015-08-13 14:09:39 -0700346 * Prevent default handling for non-ctrl-shifted event.
Marius Schilder45703172014-12-22 17:10:01 -0800347 *
348 * When combined with Chrome permission 'app.window.fullscreen.overrideEsc',
349 * and called for both key down and key up events,
350 * the ESC key remains usable within fullscreen Chrome app windows.
Joel Hockey0f933582019-08-27 18:01:51 -0700351 *
352 * @param {!KeyboardEvent} e The event to process.
Marius Schilder45703172014-12-22 17:10:01 -0800353 */
Marius Schilder9dc111b2015-08-13 14:09:39 -0700354hterm.Keyboard.prototype.preventChromeAppNonCtrlShiftDefault_ = function(e) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400355 if (!window.chrome || !window.chrome.app || !window.chrome.app.window) {
Marius Schilder45703172014-12-22 17:10:01 -0800356 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400357 }
358 if (!e.ctrlKey || !e.shiftKey) {
Marius Schilder45703172014-12-22 17:10:01 -0800359 e.preventDefault();
Mike Frysingerbdb34802020-04-07 03:47:32 -0400360 }
Marius Schilder45703172014-12-22 17:10:01 -0800361};
362
Joel Hockey0f933582019-08-27 18:01:51 -0700363/**
364 * Handle focusout events.
365 *
366 * @param {!FocusEvent} e The event to process.
367 */
Mike Frysingerc5271612017-04-12 02:24:30 -0400368hterm.Keyboard.prototype.onFocusOut_ = function(e) {
Robert Ginda034ffa72015-02-26 14:02:37 -0800369 this.altKeyPressed = 0;
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700370};
371
Joel Hockey0f933582019-08-27 18:01:51 -0700372/**
373 * Handle keyup events.
374 *
375 * @param {!KeyboardEvent} e The event to process.
376 */
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700377hterm.Keyboard.prototype.onKeyUp_ = function(e) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400378 if (e.keyCode == 18) {
Robert Ginda034ffa72015-02-26 14:02:37 -0800379 this.altKeyPressed = this.altKeyPressed & ~(1 << (e.location - 1));
Mike Frysingerbdb34802020-04-07 03:47:32 -0400380 }
Robert Ginda034ffa72015-02-26 14:02:37 -0800381
Mike Frysingerbdb34802020-04-07 03:47:32 -0400382 if (e.keyCode == 27) {
Marius Schilder9dc111b2015-08-13 14:09:39 -0700383 this.preventChromeAppNonCtrlShiftDefault_(e);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400384 }
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700385};
386
rgindafeaf3142012-01-31 15:14:20 -0800387/**
Joel Hockey0f933582019-08-27 18:01:51 -0700388 * Handle keydown events.
389 *
390 * @param {!KeyboardEvent} e The event to process.
rgindafeaf3142012-01-31 15:14:20 -0800391 */
392hterm.Keyboard.prototype.onKeyDown_ = function(e) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400393 if (e.keyCode == 18) {
Robert Ginda034ffa72015-02-26 14:02:37 -0800394 this.altKeyPressed = this.altKeyPressed | (1 << (e.location - 1));
Mike Frysingerbdb34802020-04-07 03:47:32 -0400395 }
Robert Ginda034ffa72015-02-26 14:02:37 -0800396
Mike Frysingerbdb34802020-04-07 03:47:32 -0400397 if (e.keyCode == 27) {
Marius Schilder9dc111b2015-08-13 14:09:39 -0700398 this.preventChromeAppNonCtrlShiftDefault_(e);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400399 }
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700400
Mike Frysingerdc727792020-04-10 01:41:13 -0400401 let keyDef = this.keyMap.keyDefs[e.keyCode];
rgindafeaf3142012-01-31 15:14:20 -0800402 if (!keyDef) {
Mike Frysinger53e29992017-12-12 09:36:27 -0500403 // If this key hasn't been explicitly registered, fall back to the unknown
404 // key mapping (keyCode == 0), and then automatically register it to avoid
405 // any further warnings here.
406 console.warn(`No definition for key ${e.key} (keyCode ${e.keyCode})`);
407 keyDef = this.keyMap.keyDefs[0];
408 this.keyMap.addKeyDef(e.keyCode, keyDef);
rgindafeaf3142012-01-31 15:14:20 -0800409 }
410
Robert Ginda21de3762012-09-20 16:38:18 -0700411 // The type of action we're going to use.
Mike Frysingerdc727792020-04-10 01:41:13 -0400412 let resolvedActionType = null;
Robert Ginda21de3762012-09-20 16:38:18 -0700413
Joel Hockeyed101442019-09-20 15:07:13 -0700414 /**
415 * @param {string} name
416 * @return {!hterm.Keyboard.KeyDefAction}
417 */
Mike Frysinger2acd3a52020-04-10 02:20:57 -0400418 const getAction = (name) => {
rgindafeaf3142012-01-31 15:14:20 -0800419 // Get the key action for the given action name. If the action is a
420 // function, dispatch it. If the action defers to the normal action,
421 // resolve that instead.
422
Robert Ginda21de3762012-09-20 16:38:18 -0700423 resolvedActionType = name;
424
Mike Frysingerdc727792020-04-10 01:41:13 -0400425 let action = keyDef[name];
Mike Frysingerbdb34802020-04-07 03:47:32 -0400426 if (typeof action == 'function') {
Mike Frysinger2acd3a52020-04-10 02:20:57 -0400427 action = action.call(this.keyMap, e, keyDef);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400428 }
rgindafeaf3142012-01-31 15:14:20 -0800429
Mike Frysingerbdb34802020-04-07 03:47:32 -0400430 if (action === DEFAULT && name != 'normal') {
rgindafeaf3142012-01-31 15:14:20 -0800431 action = getAction('normal');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400432 }
rgindafeaf3142012-01-31 15:14:20 -0800433
434 return action;
Mike Frysinger2acd3a52020-04-10 02:20:57 -0400435 };
rgindafeaf3142012-01-31 15:14:20 -0800436
437 // Note that we use the triple-equals ('===') operator to test equality for
Zhu Qunying30d40712017-03-14 16:27:00 -0700438 // these constants, in order to distinguish usage of the constant from usage
rgindafeaf3142012-01-31 15:14:20 -0800439 // of a literal string that happens to contain the same bytes.
Mike Frysingerdc727792020-04-10 01:41:13 -0400440 const CANCEL = hterm.Keyboard.KeyActions.CANCEL;
441 const DEFAULT = hterm.Keyboard.KeyActions.DEFAULT;
442 const PASS = hterm.Keyboard.KeyActions.PASS;
443 const STRIP = hterm.Keyboard.KeyActions.STRIP;
rgindafeaf3142012-01-31 15:14:20 -0800444
Mike Frysingerdc727792020-04-10 01:41:13 -0400445 let control = e.ctrlKey;
446 let alt = this.altIsMeta ? false : e.altKey;
447 let meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey;
rgindafeaf3142012-01-31 15:14:20 -0800448
Robert Ginda4c66c9a2015-02-18 13:16:21 -0800449 // In the key-map, we surround the keyCap for non-printables in "[...]"
Mike Frysingerdc727792020-04-10 01:41:13 -0400450 const isPrintable = !(/^\[\w+\]$/.test(keyDef.keyCap));
Robert Ginda4c66c9a2015-02-18 13:16:21 -0800451
Robert Ginda034ffa72015-02-26 14:02:37 -0800452 switch (this.altGrMode) {
453 case 'ctrl-alt':
454 if (isPrintable && control && alt) {
455 // ctrl-alt-printable means altGr. We clear out the control and
456 // alt modifiers and wait to see the charCode in the keydown event.
457 control = false;
458 alt = false;
459 }
460 break;
461
462 case 'right-alt':
463 if (isPrintable && (this.terminal.keyboard.altKeyPressed & 2)) {
464 control = false;
465 alt = false;
466 }
467 break;
468
469 case 'left-alt':
470 if (isPrintable && (this.terminal.keyboard.altKeyPressed & 1)) {
471 control = false;
472 alt = false;
473 }
474 break;
Robert Ginda4c66c9a2015-02-18 13:16:21 -0800475 }
476
Joel Hockeyed101442019-09-20 15:07:13 -0700477 /** @type {?hterm.Keyboard.KeyDefAction} */
Mike Frysingerdc727792020-04-10 01:41:13 -0400478 let action;
rgindafeaf3142012-01-31 15:14:20 -0800479
480 if (control) {
481 action = getAction('control');
482 } else if (alt) {
483 action = getAction('alt');
rginda42ad71d2012-02-16 14:06:28 -0800484 } else if (meta) {
485 action = getAction('meta');
rgindafeaf3142012-01-31 15:14:20 -0800486 } else {
487 action = getAction('normal');
488 }
489
Robert Gindaf82267d2015-06-09 15:32:04 -0700490 // If e.maskShiftKey was set (during getAction) it means the shift key is
491 // already accounted for in the action, and we should not act on it any
Joel Hockey46a6e1d2020-03-11 20:01:57 -0700492 // further. This is currently only used for Ctrl+Shift+Tab, which should send
Robert Gindaf82267d2015-06-09 15:32:04 -0700493 // "CSI Z", not "CSI 1 ; 2 Z".
Mike Frysingerdc727792020-04-10 01:41:13 -0400494 let shift = !e.maskShiftKey && e.shiftKey;
Robert Gindaf82267d2015-06-09 15:32:04 -0700495
Joel Hockeyed101442019-09-20 15:07:13 -0700496 /** @type {!hterm.Keyboard.KeyDown} */
Mike Frysingerdc727792020-04-10 01:41:13 -0400497 const keyDown = {
Robert Gindaf82267d2015-06-09 15:32:04 -0700498 keyCode: e.keyCode,
499 shift: e.shiftKey, // not `var shift` from above.
500 ctrl: control,
501 alt: alt,
Mike Frysinger989f34b2020-04-08 00:53:43 -0400502 meta: meta,
Robert Gindaf82267d2015-06-09 15:32:04 -0700503 };
504
Mike Frysingerdc727792020-04-10 01:41:13 -0400505 const binding = this.bindings.getBinding(keyDown);
Robert Gindaf82267d2015-06-09 15:32:04 -0700506
507 if (binding) {
508 // Clear out the modifier bits so we don't try to munge the sequence
509 // further.
510 shift = control = alt = meta = false;
511 resolvedActionType = 'normal';
Robert Gindaf82267d2015-06-09 15:32:04 -0700512
Joel Hockeyed101442019-09-20 15:07:13 -0700513 if (typeof binding.action == 'function') {
514 const bindingFn =
515 /** @type {!hterm.Keyboard.KeyBindingFunction} */ (binding.action);
516 action = bindingFn.call(this, this.terminal, keyDown);
517 } else {
518 action = /** @type {!hterm.Keyboard.KeyAction} */ (binding.action);
519 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700520 }
521
Joel Hockey981bb182020-03-12 21:30:47 -0700522 // Call keyDef function now that we have given bindings a chance to override.
523 if (typeof action == 'function') {
Mike Frysinger2acd3a52020-04-10 02:20:57 -0400524 action = action.call(this.keyMap, e, keyDef);
Joel Hockey981bb182020-03-12 21:30:47 -0700525 }
526
rginda39bdf6f2012-04-10 16:50:55 -0700527 if (alt && this.altSendsWhat == 'browser-key' && action == DEFAULT) {
528 // When altSendsWhat is 'browser-key', we wait for the keypress event.
529 // In keypress, the browser should have set the event.charCode to the
530 // appropriate character.
531 // TODO(rginda): Character compositions will need some black magic.
532 action = PASS;
533 }
534
Mike Frysinger7afaade2020-04-25 04:17:57 -0400535 // If we are going to handle the key, we most likely want to hide the context
536 // menu before doing so. This way we hide it when pressing a printable key,
537 // or navigate (arrow keys/etc...), or press Escape. But we don't want to
538 // hide it when only pressing modifiers like Alt/Ctrl/Meta because those might
539 // be used by the OS & hterm to show the context menu in the first place. The
540 // bare modifier keys are all marked as PASS.
541 if (action !== PASS) {
542 this.terminal.contextMenu.hide();
543 }
544
rgindafeaf3142012-01-31 15:14:20 -0800545 if (action === PASS || (action === DEFAULT && !(control || alt || meta))) {
546 // If this key is supposed to be handled by the browser, or it is an
547 // unmodified key with the default action, then exit this event handler.
548 // If it's an unmodified key, it'll be handled in onKeyPress where we
549 // can tell for sure which ASCII code to insert.
550 //
551 // This block needs to come before the STRIP test, otherwise we'll strip
552 // the modifier and think it's ok to let the browser handle the keypress.
553 // The browser won't know we're trying to ignore the modifiers and might
554 // perform some default action.
555 return;
556 }
557
558 if (action === STRIP) {
559 alt = control = false;
560 action = keyDef.normal;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400561 if (typeof action == 'function') {
Joel Hockeyed101442019-09-20 15:07:13 -0700562 action = action.call(this.keyMap, e, keyDef);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400563 }
rgindafeaf3142012-01-31 15:14:20 -0800564
Mike Frysingerbdb34802020-04-07 03:47:32 -0400565 if (action == DEFAULT && keyDef.keyCap.length == 2) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700566 action = keyDef.keyCap.substr((shift ? 1 : 0), 1);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400567 }
rgindafeaf3142012-01-31 15:14:20 -0800568 }
569
570 e.preventDefault();
571 e.stopPropagation();
572
Mike Frysingerbdb34802020-04-07 03:47:32 -0400573 if (action === CANCEL) {
rgindafeaf3142012-01-31 15:14:20 -0800574 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400575 }
rgindafeaf3142012-01-31 15:14:20 -0800576
rginda42ad71d2012-02-16 14:06:28 -0800577 if (action !== DEFAULT && typeof action != 'string') {
578 console.warn('Invalid action: ' + JSON.stringify(action));
579 return;
580 }
581
Robert Gindacc6d3a72012-09-24 14:06:08 -0700582 // Strip the modifier that is associated with the action, since we assume that
583 // modifier has already been accounted for in the action.
584 if (resolvedActionType == 'control') {
585 control = false;
586 } else if (resolvedActionType == 'alt') {
587 alt = false;
588 } else if (resolvedActionType == 'meta') {
589 meta = false;
590 }
591
Mike Frysingerc8fe3092019-01-04 00:45:18 -0500592 if (typeof action == 'string' && action.substr(0, 2) == '\x1b[' &&
593 (alt || control || shift || meta)) {
Robert Gindacc6d3a72012-09-24 14:06:08 -0700594 // The action is an escape sequence that and it was triggered in the
595 // presence of a keyboard modifier, we may need to alter the action to
596 // include the modifier before sending it.
rginda42ad71d2012-02-16 14:06:28 -0800597
Mike Frysinger86780fb2017-09-15 23:17:05 -0400598 // The math is funky but aligns w/xterm.
599 let imod = 1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400600 if (shift) {
Mike Frysinger86780fb2017-09-15 23:17:05 -0400601 imod += 1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400602 }
603 if (alt) {
Mike Frysinger86780fb2017-09-15 23:17:05 -0400604 imod += 2;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400605 }
606 if (control) {
Mike Frysinger86780fb2017-09-15 23:17:05 -0400607 imod += 4;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400608 }
609 if (meta) {
Mike Frysinger86780fb2017-09-15 23:17:05 -0400610 imod += 8;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400611 }
Mike Frysingerdc727792020-04-10 01:41:13 -0400612 const mod = ';' + imod;
rgindafeaf3142012-01-31 15:14:20 -0800613
614 if (action.length == 3) {
615 // Some of the CSI sequences have zero parameters unless modified.
616 action = '\x1b[1' + mod + action.substr(2, 1);
617 } else {
618 // Others always have at least one parameter.
Robert Ginda21de3762012-09-20 16:38:18 -0700619 action = action.substr(0, action.length - 1) + mod +
rgindafeaf3142012-01-31 15:14:20 -0800620 action.substr(action.length - 1);
621 }
Robert Ginda21de3762012-09-20 16:38:18 -0700622
623 } else {
Robert Ginda21de3762012-09-20 16:38:18 -0700624 if (action === DEFAULT) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700625 action = keyDef.keyCap.substr((shift ? 1 : 0), 1);
Robert Ginda8cb7d902013-06-20 14:37:18 -0700626
Robert Ginda21de3762012-09-20 16:38:18 -0700627 if (control) {
Mike Frysingerdc727792020-04-10 01:41:13 -0400628 const unshifted = keyDef.keyCap.substr(0, 1);
629 const code = unshifted.charCodeAt(0);
Robert Ginda21de3762012-09-20 16:38:18 -0700630 if (code >= 64 && code <= 95) {
Robert Ginda8cb7d902013-06-20 14:37:18 -0700631 action = String.fromCharCode(code - 64);
Robert Ginda21de3762012-09-20 16:38:18 -0700632 }
Robert Ginda21de3762012-09-20 16:38:18 -0700633 }
634 }
635
Robert Ginda8cb7d902013-06-20 14:37:18 -0700636 if (alt && this.altSendsWhat == '8-bit' && action.length == 1) {
Mike Frysingerdc727792020-04-10 01:41:13 -0400637 const code = action.charCodeAt(0) + 128;
Robert Ginda8cb7d902013-06-20 14:37:18 -0700638 action = String.fromCharCode(code);
639 }
640
Robert Ginda21de3762012-09-20 16:38:18 -0700641 // We respect alt/metaSendsEscape even if the keymap action was a literal
642 // string. Otherwise, every overridden alt/meta action would have to
643 // check alt/metaSendsEscape.
Robert Gindacc6d3a72012-09-24 14:06:08 -0700644 if ((alt && this.altSendsWhat == 'escape') ||
645 (meta && this.metaSendsEscape)) {
Robert Ginda21de3762012-09-20 16:38:18 -0700646 action = '\x1b' + action;
647 }
rgindafeaf3142012-01-31 15:14:20 -0800648 }
649
Joel Hockeyed101442019-09-20 15:07:13 -0700650 this.terminal.onVTKeystroke(/** @type {string} */ (action));
rgindafeaf3142012-01-31 15:14:20 -0800651};