blob: ba9bef629ae10bd289c927de258fad85ff1dd804 [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_ = [
29 ['keypress', this.onKeyPress_.bind(this)],
Robert Ginda11390c52012-09-13 14:53:34 -070030 ['keydown', this.onKeyDown_.bind(this)],
31 ['textInput', this.onTextInput_.bind(this)]
rgindafeaf3142012-01-31 15:14:20 -080032 ];
33
34 /**
35 * The current key map.
36 */
rgindacbbd7482012-06-13 15:06:16 -070037 this.keyMap = new hterm.Keyboard.KeyMap(this);
rgindafeaf3142012-01-31 15:14:20 -080038
39 /**
rginda4bba5e12012-06-20 16:15:30 -070040 * If true, Shift-Insert will fall through to the browser as a paste.
41 * If false, the keystroke will be sent to the host.
42 */
Robert Ginda57f03b42012-09-13 11:02:48 -070043 this.shiftInsertPaste = true;
rginda4bba5e12012-06-20 16:15:30 -070044
45 /**
rgindafeaf3142012-01-31 15:14:20 -080046 * If true, home/end will control the terminal scrollbar and shift home/end
47 * will send the VT keycodes. If false then home/end sends VT codes and
48 * shift home/end scrolls.
49 */
Robert Ginda57f03b42012-09-13 11:02:48 -070050 this.homeKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080051
52 /**
53 * Same as above, except for page up/page down.
54 */
Robert Ginda57f03b42012-09-13 11:02:48 -070055 this.pageKeysScroll = false;
rgindafeaf3142012-01-31 15:14:20 -080056
57 /**
58 * Enable/disable application keypad.
59 *
60 * This changes the way numeric keys are sent from the keyboard.
61 */
62 this.applicationKeypad = false;
63
64 /**
65 * Enable/disable the application cursor mode.
66 *
67 * This changes the way cursor keys are sent from the keyboard.
68 */
69 this.applicationCursor = false;
70
71 /**
72 * If true, the backspace should send BS ('\x08', aka ^H). Otherwise
73 * the backspace key should send '\x7f'.
74 */
Robert Ginda57f03b42012-09-13 11:02:48 -070075 this.backspaceSendsBackspace = false;
rgindafeaf3142012-01-31 15:14:20 -080076
77 /**
78 * Set whether the meta key sends a leading escape or not.
79 */
Robert Ginda57f03b42012-09-13 11:02:48 -070080 this.metaSendsEscape = true;
rginda30f20f62012-04-05 16:36:19 -070081
82 /**
rginda39bdf6f2012-04-10 16:50:55 -070083 * Controls how the alt key is handled.
84 *
85 * escape....... Send an ESC prefix.
86 * 8-bit........ Add 128 to the unshifted character as in xterm.
87 * browser-key.. Wait for the keypress event and see what the browser says.
88 * (This won't work well on platforms where the browser
89 * performs a default action for some alt sequences.)
rginda30f20f62012-04-05 16:36:19 -070090 *
91 * This setting only matters when alt is distinct from meta (altIsMeta is
92 * false.)
93 */
Robert Ginda57f03b42012-09-13 11:02:48 -070094 this.altSendsWhat = 'escape';
rginda42ad71d2012-02-16 14:06:28 -080095
96 /**
97 * Set whether the alt key acts as a meta key, instead of producing 8-bit
98 * characters.
rginda30f20f62012-04-05 16:36:19 -070099 *
100 * True to enable, false to disable, null to autodetect based on platform.
rginda42ad71d2012-02-16 14:06:28 -0800101 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700102 this.altIsMeta = false;
rgindafeaf3142012-01-31 15:14:20 -0800103};
104
105/**
rgindafeaf3142012-01-31 15:14:20 -0800106 * Special handling for keyCodes in a keyboard layout.
107 */
108hterm.Keyboard.KeyActions = {
109 /**
110 * Call preventDefault and stopPropagation for this key event and nothing
111 * else.
112 */
113 CANCEL: new String('CANCEL'),
114
115 /**
116 * This performs the default terminal action for the key. If used in the
117 * 'normal' action and the the keystroke represents a printable key, the
118 * character will be sent to the host. If used in one of the modifier
119 * actions, the terminal will perform the normal action after (possibly)
120 * altering it.
121 *
122 * - If the normal sequence starts with CSI, the sequence will be adjusted
123 * to include the modifier parameter as described in [XTERM] in the final
124 * table of the "PC-Style Function Keys" section.
125 *
126 * - If the control key is down and the key represents a printable character,
127 * and the uppercase version of the unshifted keycap is between
128 * 64 (ASCII '@') and 95 (ASCII '_'), then the uppercase version of the
129 * unshifted keycap minus 64 is sent. This makes '^@' send '\x00' and
130 * '^_' send '\x1f'. (Note that one higher that 0x1f is 0x20, which is
131 * the first printable ASCII value.)
132 *
133 * - If the alt key is down and the key represents a printable character then
134 * the value of the character is shifted up by 128.
135 *
136 * - If meta is down and configured to send an escape, '\x1b' will be sent
137 * before the normal action is performed.
138 */
139 DEFAULT: new String('DEFAULT'),
140
141 /**
142 * Causes the terminal to opt out of handling the key event, instead letting
143 * the browser deal with it.
144 */
145 PASS: new String('PASS'),
146
147 /**
148 * Insert the first or second character of the keyCap, based on e.shiftKey.
149 * The key will be handled in onKeyDown, and e.preventDefault() will be
150 * called.
151 *
152 * It is useful for a modified key action, where it essentially strips the
153 * modifier while preventing the browser from reacting to the key.
154 */
155 STRIP: new String('STRIP')
156};
157
158/**
159 * Capture keyboard events sent to the associated element.
160 *
161 * This enables the keyboard. Captured events are consumed by this class
162 * and will not perform their default action or bubble to other elements.
163 *
164 * Passing a null element will uninstall the keyboard handlers.
165 *
166 * @param {HTMLElement} element The element whose events should be captured, or
167 * null to disable the keyboard.
168 */
169hterm.Keyboard.prototype.installKeyboard = function(element) {
170 if (element == this.keyboardElement_)
171 return;
172
173 if (element && this.keyboardElement_)
174 this.installKeyboard(null);
175
176 for (var i = 0; i < this.handlers_.length; i++) {
177 var handler = this.handlers_[i];
178 if (element) {
179 element.addEventListener(handler[0], handler[1]);
180 } else {
181 this.keyboardElement_.removeEventListener(handler[0], handler[1]);
182 }
183 }
184
185 this.keyboardElement_ = element;
186};
187
188/**
189 * Disable keyboard event capture.
190 *
191 * This will allow the browser to process key events normally.
192 */
193hterm.Keyboard.prototype.uninstallKeyboard = function() {
194 this.installKeyboard(null);
195};
196
197/**
Robert Ginda11390c52012-09-13 14:53:34 -0700198 * Handle onTextInput events.
199 *
200 * We're not actually supposed to get these, but we do on the Mac in the case
201 * where a third party app sends synthetic keystrokes to Chrome.
202 */
203hterm.Keyboard.prototype.onTextInput_ = function(e) {
204 if (!e.data)
205 return;
206
207 e.data.split('').forEach(this.terminal.onVTKeystroke.bind(this.terminal));
208};
209
210/**
rgindafeaf3142012-01-31 15:14:20 -0800211 * Handle onKeyPress events.
212 */
213hterm.Keyboard.prototype.onKeyPress_ = function(e) {
rginda39bdf6f2012-04-10 16:50:55 -0700214 var code;
215
216 if (e.altKey && this.altSendsWhat == 'browser-key' && e.charCode == 0) {
217 // If we got here because we were expecting the browser to handle an
218 // alt sequence but it didn't do it, then we might be on an OS without
219 // an enabled IME system. In that case we fall back to xterm-like
220 // behavior.
221 //
222 // This happens here only as a fallback. Typically these platforms should
223 // set altSendsWhat to either 'escape' or '8-bit'.
224 var ch = String.fromCharCode(e.keyCode);
225 if (!e.shiftKey)
226 ch = ch.toLowerCase();
227 code = ch.charCodeAt(0) + 128;
228
229 } else if (e.charCode >= 32) {
230 ch = e.charCode;
231 }
232
233 if (ch) {
234 var str = this.terminal.vt.encodeUTF8(String.fromCharCode(ch));
235 this.terminal.onVTKeystroke(str);
236 }
rgindafeaf3142012-01-31 15:14:20 -0800237
238 e.preventDefault();
239 e.stopPropagation();
240};
241
242/**
243 * Handle onKeyDown events.
244 */
245hterm.Keyboard.prototype.onKeyDown_ = function(e) {
246 var keyDef = this.keyMap.keyDefs[e.keyCode];
247 if (!keyDef) {
248 console.warn('No definition for keyCode: ' + e.keyCode);
249 return;
250 }
251
Robert Ginda21de3762012-09-20 16:38:18 -0700252 // The type of action we're going to use.
253 var resolvedActionType = null;
254
rgindafeaf3142012-01-31 15:14:20 -0800255 var self = this;
256 function getAction(name) {
257 // Get the key action for the given action name. If the action is a
258 // function, dispatch it. If the action defers to the normal action,
259 // resolve that instead.
260
Robert Ginda21de3762012-09-20 16:38:18 -0700261 resolvedActionType = name;
262
rgindafeaf3142012-01-31 15:14:20 -0800263 var action = keyDef[name];
264 if (typeof action == 'function')
265 action = action.apply(self.keyMap, [e, keyDef]);
266
267 if (action === DEFAULT && name != 'normal')
268 action = getAction('normal');
269
270 return action;
271 }
272
273 // Note that we use the triple-equals ('===') operator to test equality for
274 // these constants, in order to distingush usage of the constant from usage
275 // of a literal string that happens to contain the same bytes.
276 var CANCEL = hterm.Keyboard.KeyActions.CANCEL;
277 var DEFAULT = hterm.Keyboard.KeyActions.DEFAULT;
278 var PASS = hterm.Keyboard.KeyActions.PASS;
279 var STRIP = hterm.Keyboard.KeyActions.STRIP;
280
281 var shift = e.shiftKey;
282 var control = e.ctrlKey;
rginda42ad71d2012-02-16 14:06:28 -0800283 var alt = this.altIsMeta ? false : e.altKey;
284 var meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey;
rgindafeaf3142012-01-31 15:14:20 -0800285
286 var action;
287
288 if (control) {
289 action = getAction('control');
290 } else if (alt) {
291 action = getAction('alt');
rginda42ad71d2012-02-16 14:06:28 -0800292 } else if (meta) {
293 action = getAction('meta');
rgindafeaf3142012-01-31 15:14:20 -0800294 } else {
295 action = getAction('normal');
296 }
297
rginda39bdf6f2012-04-10 16:50:55 -0700298 if (alt && this.altSendsWhat == 'browser-key' && action == DEFAULT) {
299 // When altSendsWhat is 'browser-key', we wait for the keypress event.
300 // In keypress, the browser should have set the event.charCode to the
301 // appropriate character.
302 // TODO(rginda): Character compositions will need some black magic.
303 action = PASS;
304 }
305
rgindafeaf3142012-01-31 15:14:20 -0800306 if (action === PASS || (action === DEFAULT && !(control || alt || meta))) {
307 // If this key is supposed to be handled by the browser, or it is an
308 // unmodified key with the default action, then exit this event handler.
309 // If it's an unmodified key, it'll be handled in onKeyPress where we
310 // can tell for sure which ASCII code to insert.
311 //
312 // This block needs to come before the STRIP test, otherwise we'll strip
313 // the modifier and think it's ok to let the browser handle the keypress.
314 // The browser won't know we're trying to ignore the modifiers and might
315 // perform some default action.
316 return;
317 }
318
319 if (action === STRIP) {
320 alt = control = false;
321 action = keyDef.normal;
322 if (typeof action == 'function')
323 action = action.apply(this.keyMap, [e, keyDef]);
324
325 if (action == DEFAULT && keyDef.keyCap.length == 2)
326 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
327 }
328
329 e.preventDefault();
330 e.stopPropagation();
331
332 if (action === CANCEL)
333 return;
334
rginda42ad71d2012-02-16 14:06:28 -0800335 if (action !== DEFAULT && typeof action != 'string') {
336 console.warn('Invalid action: ' + JSON.stringify(action));
337 return;
338 }
339
Robert Gindacc6d3a72012-09-24 14:06:08 -0700340 // Strip the modifier that is associated with the action, since we assume that
341 // modifier has already been accounted for in the action.
342 if (resolvedActionType == 'control') {
343 control = false;
344 } else if (resolvedActionType == 'alt') {
345 alt = false;
346 } else if (resolvedActionType == 'meta') {
347 meta = false;
348 }
349
350 if (action.substr(0, 2) == '\x1b[' && (alt || control || shift)) {
351 // The action is an escape sequence that and it was triggered in the
352 // presence of a keyboard modifier, we may need to alter the action to
353 // include the modifier before sending it.
rginda42ad71d2012-02-16 14:06:28 -0800354
rgindafeaf3142012-01-31 15:14:20 -0800355 var mod;
356
357 if (shift && !(alt || control)) {
358 mod = ';2';
359 } else if (alt && !(shift || control)) {
360 mod = ';3';
361 } else if (shift && alt && !control) {
362 mod = ';4';
363 } else if (control && !(shift || alt)) {
364 mod = ';5';
365 } else if (shift && control && !alt) {
366 mod = ';6';
367 } else if (alt && control && !shift) {
368 mod = ';7';
369 } else if (shift && alt && control) {
370 mod = ';8';
371 }
372
373 if (action.length == 3) {
374 // Some of the CSI sequences have zero parameters unless modified.
375 action = '\x1b[1' + mod + action.substr(2, 1);
376 } else {
377 // Others always have at least one parameter.
Robert Ginda21de3762012-09-20 16:38:18 -0700378 action = action.substr(0, action.length - 1) + mod +
rgindafeaf3142012-01-31 15:14:20 -0800379 action.substr(action.length - 1);
380 }
Robert Ginda21de3762012-09-20 16:38:18 -0700381
382 } else {
383 // Just send it as-is.
384
385 if (action === DEFAULT) {
386 if (control) {
387 var unshifted = keyDef.keyCap.substr(0, 1);
388 var code = unshifted.charCodeAt(0);
389 if (code >= 64 && code <= 95) {
390 action = String.fromCharCode(code - 64);
391 }
392 } else if (alt && this.altSendsWhat == '8-bit') {
393 var ch = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
394 var code = ch.charCodeAt(0) + 128;
395 action = this.terminal.vt.encodeUTF8(String.fromCharCode(code));
396 } else {
397 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
398 }
399 }
400
401 // We respect alt/metaSendsEscape even if the keymap action was a literal
402 // string. Otherwise, every overridden alt/meta action would have to
403 // check alt/metaSendsEscape.
Robert Gindacc6d3a72012-09-24 14:06:08 -0700404 if ((alt && this.altSendsWhat == 'escape') ||
405 (meta && this.metaSendsEscape)) {
Robert Ginda21de3762012-09-20 16:38:18 -0700406 action = '\x1b' + action;
407 }
rgindafeaf3142012-01-31 15:14:20 -0800408 }
409
410 this.terminal.onVTKeystroke(action);
411};