blob: 272e1a59355852639c3753efa2be3046f8c0b9d8 [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
7lib.rtdep('hterm.Keyboard.KeyMap');
8
rgindafeaf3142012-01-31 15:14:20 -08009/**
10 * Keyboard handler.
11 *
12 * Consumes onKey* events and invokes onVTKeystroke on the associated
13 * hterm.Terminal object.
14 *
15 * See also: [XTERM] as referenced in vt.js.
16 *
17 * @param {hterm.Terminal} The Terminal object associated with this keyboard.
18 */
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_ = [
Andrew de los Reyes574e10e2013-04-04 09:31:57 -070029 ['blur', this.onBlur_.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)],
Robert Ginda11390c52012-09-13 14:53:34 -070033 ['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
41 /**
rginda4bba5e12012-06-20 16:15:30 -070042 * If true, Shift-Insert will fall through to the browser as a paste.
43 * If false, the keystroke will be sent to the host.
44 */
Robert Ginda57f03b42012-09-13 11:02:48 -070045 this.shiftInsertPaste = true;
rginda4bba5e12012-06-20 16:15:30 -070046
47 /**
rgindafeaf3142012-01-31 15:14:20 -080048 * If true, home/end will control the terminal scrollbar and shift home/end
49 * will send the VT keycodes. If false then home/end sends VT codes and
50 * shift home/end scrolls.
51 */
Robert Ginda57f03b42012-09-13 11:02:48 -070052 this.homeKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080053
54 /**
55 * Same as above, except for page up/page down.
56 */
Robert Ginda57f03b42012-09-13 11:02:48 -070057 this.pageKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080058
59 /**
60 * Enable/disable application keypad.
61 *
62 * This changes the way numeric keys are sent from the keyboard.
63 */
64 this.applicationKeypad = false;
65
66 /**
67 * Enable/disable the application cursor mode.
68 *
69 * This changes the way cursor keys are sent from the keyboard.
70 */
71 this.applicationCursor = false;
72
73 /**
74 * If true, the backspace should send BS ('\x08', aka ^H). Otherwise
75 * the backspace key should send '\x7f'.
76 */
Robert Ginda57f03b42012-09-13 11:02:48 -070077 this.backspaceSendsBackspace = false;
rgindafeaf3142012-01-31 15:14:20 -080078
79 /**
80 * Set whether the meta key sends a leading escape or not.
81 */
Robert Ginda57f03b42012-09-13 11:02:48 -070082 this.metaSendsEscape = true;
rginda30f20f62012-04-05 16:36:19 -070083
84 /**
rginda39bdf6f2012-04-10 16:50:55 -070085 * Controls how the alt key is handled.
86 *
87 * escape....... Send an ESC prefix.
88 * 8-bit........ Add 128 to the unshifted character as in xterm.
89 * browser-key.. Wait for the keypress event and see what the browser says.
90 * (This won't work well on platforms where the browser
91 * performs a default action for some alt sequences.)
rginda30f20f62012-04-05 16:36:19 -070092 *
93 * This setting only matters when alt is distinct from meta (altIsMeta is
94 * false.)
95 */
Robert Ginda57f03b42012-09-13 11:02:48 -070096 this.altSendsWhat = 'escape';
rginda42ad71d2012-02-16 14:06:28 -080097
98 /**
99 * Set whether the alt key acts as a meta key, instead of producing 8-bit
100 * characters.
rginda30f20f62012-04-05 16:36:19 -0700101 *
102 * True to enable, false to disable, null to autodetect based on platform.
rginda42ad71d2012-02-16 14:06:28 -0800103 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700104 this.altIsMeta = false;
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700105
106 /**
107 * If true, tries to detect DEL key events that are from alt-backspace on
108 * Chrome OS vs from a true DEL key press.
109 *
110 * Background: At the time of writing, on Chrome OS, alt-backspace is mapped
111 * to DEL. Some users may be happy with this, but others may be frustrated
112 * that it's impossible to do meta-backspace. If the user enables this pref,
113 * we use a trick to tell a true DEL keypress from alt-backspace: on
114 * alt-backspace, we will see the alt key go down, then get a DEL keystroke
115 * that indicates that alt is not pressed. See http://crbug.com/174410 .
116 */
117 this.altBackspaceIsMetaBackspace = false;
118
119 /**
120 * Used to keep track of the current alt-key state, which is necessary for
121 * the altBackspaceIsMetaBackspace preference above.
122 */
123 this.altIsPressed = false;
rgindafeaf3142012-01-31 15:14:20 -0800124};
125
126/**
rgindafeaf3142012-01-31 15:14:20 -0800127 * Special handling for keyCodes in a keyboard layout.
128 */
129hterm.Keyboard.KeyActions = {
130 /**
131 * Call preventDefault and stopPropagation for this key event and nothing
132 * else.
133 */
134 CANCEL: new String('CANCEL'),
135
136 /**
137 * This performs the default terminal action for the key. If used in the
138 * 'normal' action and the the keystroke represents a printable key, the
139 * character will be sent to the host. If used in one of the modifier
140 * actions, the terminal will perform the normal action after (possibly)
141 * altering it.
142 *
143 * - If the normal sequence starts with CSI, the sequence will be adjusted
144 * to include the modifier parameter as described in [XTERM] in the final
145 * table of the "PC-Style Function Keys" section.
146 *
147 * - If the control key is down and the key represents a printable character,
148 * and the uppercase version of the unshifted keycap is between
149 * 64 (ASCII '@') and 95 (ASCII '_'), then the uppercase version of the
150 * unshifted keycap minus 64 is sent. This makes '^@' send '\x00' and
151 * '^_' send '\x1f'. (Note that one higher that 0x1f is 0x20, which is
152 * the first printable ASCII value.)
153 *
154 * - If the alt key is down and the key represents a printable character then
155 * the value of the character is shifted up by 128.
156 *
157 * - If meta is down and configured to send an escape, '\x1b' will be sent
158 * before the normal action is performed.
159 */
160 DEFAULT: new String('DEFAULT'),
161
162 /**
163 * Causes the terminal to opt out of handling the key event, instead letting
164 * the browser deal with it.
165 */
166 PASS: new String('PASS'),
167
168 /**
169 * Insert the first or second character of the keyCap, based on e.shiftKey.
170 * The key will be handled in onKeyDown, and e.preventDefault() will be
171 * called.
172 *
173 * It is useful for a modified key action, where it essentially strips the
174 * modifier while preventing the browser from reacting to the key.
175 */
176 STRIP: new String('STRIP')
177};
178
179/**
180 * Capture keyboard events sent to the associated element.
181 *
182 * This enables the keyboard. Captured events are consumed by this class
183 * and will not perform their default action or bubble to other elements.
184 *
185 * Passing a null element will uninstall the keyboard handlers.
186 *
187 * @param {HTMLElement} element The element whose events should be captured, or
188 * null to disable the keyboard.
189 */
190hterm.Keyboard.prototype.installKeyboard = function(element) {
191 if (element == this.keyboardElement_)
192 return;
193
194 if (element && this.keyboardElement_)
195 this.installKeyboard(null);
196
197 for (var i = 0; i < this.handlers_.length; i++) {
198 var handler = this.handlers_[i];
199 if (element) {
200 element.addEventListener(handler[0], handler[1]);
201 } else {
202 this.keyboardElement_.removeEventListener(handler[0], handler[1]);
203 }
204 }
205
206 this.keyboardElement_ = element;
207};
208
209/**
210 * Disable keyboard event capture.
211 *
212 * This will allow the browser to process key events normally.
213 */
214hterm.Keyboard.prototype.uninstallKeyboard = function() {
215 this.installKeyboard(null);
216};
217
218/**
Robert Ginda11390c52012-09-13 14:53:34 -0700219 * Handle onTextInput events.
220 *
221 * We're not actually supposed to get these, but we do on the Mac in the case
222 * where a third party app sends synthetic keystrokes to Chrome.
223 */
224hterm.Keyboard.prototype.onTextInput_ = function(e) {
225 if (!e.data)
226 return;
227
228 e.data.split('').forEach(this.terminal.onVTKeystroke.bind(this.terminal));
229};
230
231/**
rgindafeaf3142012-01-31 15:14:20 -0800232 * Handle onKeyPress events.
233 */
234hterm.Keyboard.prototype.onKeyPress_ = function(e) {
rginda39bdf6f2012-04-10 16:50:55 -0700235 var code;
236
237 if (e.altKey && this.altSendsWhat == 'browser-key' && e.charCode == 0) {
238 // If we got here because we were expecting the browser to handle an
239 // alt sequence but it didn't do it, then we might be on an OS without
240 // an enabled IME system. In that case we fall back to xterm-like
241 // behavior.
242 //
243 // This happens here only as a fallback. Typically these platforms should
244 // set altSendsWhat to either 'escape' or '8-bit'.
245 var ch = String.fromCharCode(e.keyCode);
246 if (!e.shiftKey)
247 ch = ch.toLowerCase();
248 code = ch.charCodeAt(0) + 128;
249
250 } else if (e.charCode >= 32) {
251 ch = e.charCode;
252 }
253
254 if (ch) {
255 var str = this.terminal.vt.encodeUTF8(String.fromCharCode(ch));
256 this.terminal.onVTKeystroke(str);
257 }
rgindafeaf3142012-01-31 15:14:20 -0800258
259 e.preventDefault();
260 e.stopPropagation();
261};
262
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700263hterm.Keyboard.prototype.onBlur_ = function(e) {
264 this.altIsPressed = false;
265};
266
267hterm.Keyboard.prototype.onKeyUp_ = function(e) {
268 if (e.keyCode == 18)
269 this.altIsPressed = false;
270};
271
rgindafeaf3142012-01-31 15:14:20 -0800272/**
273 * Handle onKeyDown events.
274 */
275hterm.Keyboard.prototype.onKeyDown_ = function(e) {
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700276 if (e.keyCode == 18)
277 this.altIsPressed = true;
278
rgindafeaf3142012-01-31 15:14:20 -0800279 var keyDef = this.keyMap.keyDefs[e.keyCode];
280 if (!keyDef) {
281 console.warn('No definition for keyCode: ' + e.keyCode);
282 return;
283 }
284
Robert Ginda21de3762012-09-20 16:38:18 -0700285 // The type of action we're going to use.
286 var resolvedActionType = null;
287
rgindafeaf3142012-01-31 15:14:20 -0800288 var self = this;
289 function getAction(name) {
290 // Get the key action for the given action name. If the action is a
291 // function, dispatch it. If the action defers to the normal action,
292 // resolve that instead.
293
Robert Ginda21de3762012-09-20 16:38:18 -0700294 resolvedActionType = name;
295
rgindafeaf3142012-01-31 15:14:20 -0800296 var action = keyDef[name];
297 if (typeof action == 'function')
298 action = action.apply(self.keyMap, [e, keyDef]);
299
300 if (action === DEFAULT && name != 'normal')
301 action = getAction('normal');
302
303 return action;
304 }
305
306 // Note that we use the triple-equals ('===') operator to test equality for
307 // these constants, in order to distingush usage of the constant from usage
308 // of a literal string that happens to contain the same bytes.
309 var CANCEL = hterm.Keyboard.KeyActions.CANCEL;
310 var DEFAULT = hterm.Keyboard.KeyActions.DEFAULT;
311 var PASS = hterm.Keyboard.KeyActions.PASS;
312 var STRIP = hterm.Keyboard.KeyActions.STRIP;
313
314 var shift = e.shiftKey;
315 var control = e.ctrlKey;
rginda42ad71d2012-02-16 14:06:28 -0800316 var alt = this.altIsMeta ? false : e.altKey;
317 var meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey;
rgindafeaf3142012-01-31 15:14:20 -0800318
319 var action;
320
321 if (control) {
322 action = getAction('control');
323 } else if (alt) {
324 action = getAction('alt');
rginda42ad71d2012-02-16 14:06:28 -0800325 } else if (meta) {
326 action = getAction('meta');
rgindafeaf3142012-01-31 15:14:20 -0800327 } else {
328 action = getAction('normal');
329 }
330
rginda39bdf6f2012-04-10 16:50:55 -0700331 if (alt && this.altSendsWhat == 'browser-key' && action == DEFAULT) {
332 // When altSendsWhat is 'browser-key', we wait for the keypress event.
333 // In keypress, the browser should have set the event.charCode to the
334 // appropriate character.
335 // TODO(rginda): Character compositions will need some black magic.
336 action = PASS;
337 }
338
rgindafeaf3142012-01-31 15:14:20 -0800339 if (action === PASS || (action === DEFAULT && !(control || alt || meta))) {
340 // If this key is supposed to be handled by the browser, or it is an
341 // unmodified key with the default action, then exit this event handler.
342 // If it's an unmodified key, it'll be handled in onKeyPress where we
343 // can tell for sure which ASCII code to insert.
344 //
345 // This block needs to come before the STRIP test, otherwise we'll strip
346 // the modifier and think it's ok to let the browser handle the keypress.
347 // The browser won't know we're trying to ignore the modifiers and might
348 // perform some default action.
349 return;
350 }
351
352 if (action === STRIP) {
353 alt = control = false;
354 action = keyDef.normal;
355 if (typeof action == 'function')
356 action = action.apply(this.keyMap, [e, keyDef]);
357
358 if (action == DEFAULT && keyDef.keyCap.length == 2)
359 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
360 }
361
362 e.preventDefault();
363 e.stopPropagation();
364
365 if (action === CANCEL)
366 return;
367
rginda42ad71d2012-02-16 14:06:28 -0800368 if (action !== DEFAULT && typeof action != 'string') {
369 console.warn('Invalid action: ' + JSON.stringify(action));
370 return;
371 }
372
Robert Gindacc6d3a72012-09-24 14:06:08 -0700373 // Strip the modifier that is associated with the action, since we assume that
374 // modifier has already been accounted for in the action.
375 if (resolvedActionType == 'control') {
376 control = false;
377 } else if (resolvedActionType == 'alt') {
378 alt = false;
379 } else if (resolvedActionType == 'meta') {
380 meta = false;
381 }
382
383 if (action.substr(0, 2) == '\x1b[' && (alt || control || shift)) {
384 // The action is an escape sequence that and it was triggered in the
385 // presence of a keyboard modifier, we may need to alter the action to
386 // include the modifier before sending it.
rginda42ad71d2012-02-16 14:06:28 -0800387
rgindafeaf3142012-01-31 15:14:20 -0800388 var mod;
389
390 if (shift && !(alt || control)) {
391 mod = ';2';
392 } else if (alt && !(shift || control)) {
393 mod = ';3';
394 } else if (shift && alt && !control) {
395 mod = ';4';
396 } else if (control && !(shift || alt)) {
397 mod = ';5';
398 } else if (shift && control && !alt) {
399 mod = ';6';
400 } else if (alt && control && !shift) {
401 mod = ';7';
402 } else if (shift && alt && control) {
403 mod = ';8';
404 }
405
406 if (action.length == 3) {
407 // Some of the CSI sequences have zero parameters unless modified.
408 action = '\x1b[1' + mod + action.substr(2, 1);
409 } else {
410 // Others always have at least one parameter.
Robert Ginda21de3762012-09-20 16:38:18 -0700411 action = action.substr(0, action.length - 1) + mod +
rgindafeaf3142012-01-31 15:14:20 -0800412 action.substr(action.length - 1);
413 }
Robert Ginda21de3762012-09-20 16:38:18 -0700414
415 } else {
416 // Just send it as-is.
417
418 if (action === DEFAULT) {
419 if (control) {
420 var unshifted = keyDef.keyCap.substr(0, 1);
421 var code = unshifted.charCodeAt(0);
422 if (code >= 64 && code <= 95) {
423 action = String.fromCharCode(code - 64);
424 }
425 } else if (alt && this.altSendsWhat == '8-bit') {
426 var ch = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
427 var code = ch.charCodeAt(0) + 128;
428 action = this.terminal.vt.encodeUTF8(String.fromCharCode(code));
429 } else {
430 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
431 }
432 }
433
434 // We respect alt/metaSendsEscape even if the keymap action was a literal
435 // string. Otherwise, every overridden alt/meta action would have to
436 // check alt/metaSendsEscape.
Robert Gindacc6d3a72012-09-24 14:06:08 -0700437 if ((alt && this.altSendsWhat == 'escape') ||
438 (meta && this.metaSendsEscape)) {
Robert Ginda21de3762012-09-20 16:38:18 -0700439 action = '\x1b' + action;
440 }
rgindafeaf3142012-01-31 15:14:20 -0800441 }
442
443 this.terminal.onVTKeystroke(action);
444};