blob: 2a86e8331b8f52c085440bfca86263463049d2d3 [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)],
30 ['keydown', this.onKeyDown_.bind(this)]
31 ];
32
33 /**
34 * The current key map.
35 */
rgindacbbd7482012-06-13 15:06:16 -070036 this.keyMap = new hterm.Keyboard.KeyMap(this);
rgindafeaf3142012-01-31 15:14:20 -080037
38 /**
39 * If true, home/end will control the terminal scrollbar and shift home/end
40 * will send the VT keycodes. If false then home/end sends VT codes and
41 * shift home/end scrolls.
42 */
rginda7e9460a2012-04-11 11:36:02 -070043 this.homeKeysScroll = terminal.prefs_.get('home-keys-scroll');
rgindafeaf3142012-01-31 15:14:20 -080044
45 /**
46 * Same as above, except for page up/page down.
47 */
rginda30f20f62012-04-05 16:36:19 -070048 this.pageKeysScroll = terminal.prefs_.get('page-keys-scroll');
rgindafeaf3142012-01-31 15:14:20 -080049
50 /**
51 * Enable/disable application keypad.
52 *
53 * This changes the way numeric keys are sent from the keyboard.
54 */
55 this.applicationKeypad = false;
56
57 /**
58 * Enable/disable the application cursor mode.
59 *
60 * This changes the way cursor keys are sent from the keyboard.
61 */
62 this.applicationCursor = false;
63
64 /**
65 * If true, the backspace should send BS ('\x08', aka ^H). Otherwise
66 * the backspace key should send '\x7f'.
67 */
rginda30f20f62012-04-05 16:36:19 -070068 this.backspaceSendsBackspace = terminal.prefs_.get(
69 'backspace-sends-backspace');
rgindafeaf3142012-01-31 15:14:20 -080070
71 /**
72 * Set whether the meta key sends a leading escape or not.
73 */
rginda30f20f62012-04-05 16:36:19 -070074 this.metaSendsEscape = terminal.prefs_.get('meta-sends-escape');
75
76 /**
rginda39bdf6f2012-04-10 16:50:55 -070077 * Controls how the alt key is handled.
78 *
79 * escape....... Send an ESC prefix.
80 * 8-bit........ Add 128 to the unshifted character as in xterm.
81 * browser-key.. Wait for the keypress event and see what the browser says.
82 * (This won't work well on platforms where the browser
83 * performs a default action for some alt sequences.)
rginda30f20f62012-04-05 16:36:19 -070084 *
85 * This setting only matters when alt is distinct from meta (altIsMeta is
86 * false.)
87 */
rginda39bdf6f2012-04-10 16:50:55 -070088 this.altSendsWhat = terminal.prefs_.get('alt-sends-what');
rginda42ad71d2012-02-16 14:06:28 -080089
90 /**
91 * Set whether the alt key acts as a meta key, instead of producing 8-bit
92 * characters.
rginda30f20f62012-04-05 16:36:19 -070093 *
94 * True to enable, false to disable, null to autodetect based on platform.
rginda42ad71d2012-02-16 14:06:28 -080095 */
rginda30f20f62012-04-05 16:36:19 -070096 this.altIsMeta = terminal.prefs_.get('alt-is-meta');
rgindafeaf3142012-01-31 15:14:20 -080097};
98
99/**
rgindafeaf3142012-01-31 15:14:20 -0800100 * Special handling for keyCodes in a keyboard layout.
101 */
102hterm.Keyboard.KeyActions = {
103 /**
104 * Call preventDefault and stopPropagation for this key event and nothing
105 * else.
106 */
107 CANCEL: new String('CANCEL'),
108
109 /**
110 * This performs the default terminal action for the key. If used in the
111 * 'normal' action and the the keystroke represents a printable key, the
112 * character will be sent to the host. If used in one of the modifier
113 * actions, the terminal will perform the normal action after (possibly)
114 * altering it.
115 *
116 * - If the normal sequence starts with CSI, the sequence will be adjusted
117 * to include the modifier parameter as described in [XTERM] in the final
118 * table of the "PC-Style Function Keys" section.
119 *
120 * - If the control key is down and the key represents a printable character,
121 * and the uppercase version of the unshifted keycap is between
122 * 64 (ASCII '@') and 95 (ASCII '_'), then the uppercase version of the
123 * unshifted keycap minus 64 is sent. This makes '^@' send '\x00' and
124 * '^_' send '\x1f'. (Note that one higher that 0x1f is 0x20, which is
125 * the first printable ASCII value.)
126 *
127 * - If the alt key is down and the key represents a printable character then
128 * the value of the character is shifted up by 128.
129 *
130 * - If meta is down and configured to send an escape, '\x1b' will be sent
131 * before the normal action is performed.
132 */
133 DEFAULT: new String('DEFAULT'),
134
135 /**
136 * Causes the terminal to opt out of handling the key event, instead letting
137 * the browser deal with it.
138 */
139 PASS: new String('PASS'),
140
141 /**
142 * Insert the first or second character of the keyCap, based on e.shiftKey.
143 * The key will be handled in onKeyDown, and e.preventDefault() will be
144 * called.
145 *
146 * It is useful for a modified key action, where it essentially strips the
147 * modifier while preventing the browser from reacting to the key.
148 */
149 STRIP: new String('STRIP')
150};
151
152/**
153 * Capture keyboard events sent to the associated element.
154 *
155 * This enables the keyboard. Captured events are consumed by this class
156 * and will not perform their default action or bubble to other elements.
157 *
158 * Passing a null element will uninstall the keyboard handlers.
159 *
160 * @param {HTMLElement} element The element whose events should be captured, or
161 * null to disable the keyboard.
162 */
163hterm.Keyboard.prototype.installKeyboard = function(element) {
164 if (element == this.keyboardElement_)
165 return;
166
167 if (element && this.keyboardElement_)
168 this.installKeyboard(null);
169
170 for (var i = 0; i < this.handlers_.length; i++) {
171 var handler = this.handlers_[i];
172 if (element) {
173 element.addEventListener(handler[0], handler[1]);
174 } else {
175 this.keyboardElement_.removeEventListener(handler[0], handler[1]);
176 }
177 }
178
179 this.keyboardElement_ = element;
180};
181
182/**
183 * Disable keyboard event capture.
184 *
185 * This will allow the browser to process key events normally.
186 */
187hterm.Keyboard.prototype.uninstallKeyboard = function() {
188 this.installKeyboard(null);
189};
190
191/**
192 * Handle onKeyPress events.
193 */
194hterm.Keyboard.prototype.onKeyPress_ = function(e) {
rginda39bdf6f2012-04-10 16:50:55 -0700195 var code;
196
197 if (e.altKey && this.altSendsWhat == 'browser-key' && e.charCode == 0) {
198 // If we got here because we were expecting the browser to handle an
199 // alt sequence but it didn't do it, then we might be on an OS without
200 // an enabled IME system. In that case we fall back to xterm-like
201 // behavior.
202 //
203 // This happens here only as a fallback. Typically these platforms should
204 // set altSendsWhat to either 'escape' or '8-bit'.
205 var ch = String.fromCharCode(e.keyCode);
206 if (!e.shiftKey)
207 ch = ch.toLowerCase();
208 code = ch.charCodeAt(0) + 128;
209
210 } else if (e.charCode >= 32) {
211 ch = e.charCode;
212 }
213
214 if (ch) {
215 var str = this.terminal.vt.encodeUTF8(String.fromCharCode(ch));
216 this.terminal.onVTKeystroke(str);
217 }
rgindafeaf3142012-01-31 15:14:20 -0800218
219 e.preventDefault();
220 e.stopPropagation();
221};
222
223/**
224 * Handle onKeyDown events.
225 */
226hterm.Keyboard.prototype.onKeyDown_ = function(e) {
227 var keyDef = this.keyMap.keyDefs[e.keyCode];
228 if (!keyDef) {
229 console.warn('No definition for keyCode: ' + e.keyCode);
230 return;
231 }
232
233 var self = this;
234 function getAction(name) {
235 // Get the key action for the given action name. If the action is a
236 // function, dispatch it. If the action defers to the normal action,
237 // resolve that instead.
238
239 var action = keyDef[name];
240 if (typeof action == 'function')
241 action = action.apply(self.keyMap, [e, keyDef]);
242
243 if (action === DEFAULT && name != 'normal')
244 action = getAction('normal');
245
246 return action;
247 }
248
249 // Note that we use the triple-equals ('===') operator to test equality for
250 // these constants, in order to distingush usage of the constant from usage
251 // of a literal string that happens to contain the same bytes.
252 var CANCEL = hterm.Keyboard.KeyActions.CANCEL;
253 var DEFAULT = hterm.Keyboard.KeyActions.DEFAULT;
254 var PASS = hterm.Keyboard.KeyActions.PASS;
255 var STRIP = hterm.Keyboard.KeyActions.STRIP;
256
257 var shift = e.shiftKey;
258 var control = e.ctrlKey;
rginda42ad71d2012-02-16 14:06:28 -0800259 var alt = this.altIsMeta ? false : e.altKey;
260 var meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey;
rgindafeaf3142012-01-31 15:14:20 -0800261
262 var action;
263
264 if (control) {
265 action = getAction('control');
266 } else if (alt) {
267 action = getAction('alt');
rginda42ad71d2012-02-16 14:06:28 -0800268 } else if (meta) {
269 action = getAction('meta');
rgindafeaf3142012-01-31 15:14:20 -0800270 } else {
271 action = getAction('normal');
272 }
273
rginda39bdf6f2012-04-10 16:50:55 -0700274 if (alt && this.altSendsWhat == 'browser-key' && action == DEFAULT) {
275 // When altSendsWhat is 'browser-key', we wait for the keypress event.
276 // In keypress, the browser should have set the event.charCode to the
277 // appropriate character.
278 // TODO(rginda): Character compositions will need some black magic.
279 action = PASS;
280 }
281
rgindafeaf3142012-01-31 15:14:20 -0800282 if (action === PASS || (action === DEFAULT && !(control || alt || meta))) {
283 // If this key is supposed to be handled by the browser, or it is an
284 // unmodified key with the default action, then exit this event handler.
285 // If it's an unmodified key, it'll be handled in onKeyPress where we
286 // can tell for sure which ASCII code to insert.
287 //
288 // This block needs to come before the STRIP test, otherwise we'll strip
289 // the modifier and think it's ok to let the browser handle the keypress.
290 // The browser won't know we're trying to ignore the modifiers and might
291 // perform some default action.
292 return;
293 }
294
295 if (action === STRIP) {
296 alt = control = false;
297 action = keyDef.normal;
298 if (typeof action == 'function')
299 action = action.apply(this.keyMap, [e, keyDef]);
300
301 if (action == DEFAULT && keyDef.keyCap.length == 2)
302 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
303 }
304
305 e.preventDefault();
306 e.stopPropagation();
307
308 if (action === CANCEL)
309 return;
310
rginda42ad71d2012-02-16 14:06:28 -0800311 if (action !== DEFAULT && typeof action != 'string') {
312 console.warn('Invalid action: ' + JSON.stringify(action));
313 return;
314 }
315
316 if (action.substr(0, 2) != '\x1b[') {
317 // The action is not an escape sequence...
318
319 if (action === DEFAULT) {
320 if (control) {
321 var unshifted = keyDef.keyCap.substr(0, 1);
322 var code = unshifted.charCodeAt(0);
323 if (code >= 64 && code <= 95) {
rgindafeaf3142012-01-31 15:14:20 -0800324 action = String.fromCharCode(code - 64);
rginda42ad71d2012-02-16 14:06:28 -0800325 }
rginda39bdf6f2012-04-10 16:50:55 -0700326 } else if (alt && this.altSendsWhat == '8-bit') {
rginda42ad71d2012-02-16 14:06:28 -0800327 var ch = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
328 var code = ch.charCodeAt(0) + 128;
rginda39bdf6f2012-04-10 16:50:55 -0700329 action = this.terminal.vt.encodeUTF8(String.fromCharCode(code));
rginda42ad71d2012-02-16 14:06:28 -0800330 } else {
331 action = keyDef.keyCap.substr((e.shiftKey ? 1 : 0), 1);
rgindafeaf3142012-01-31 15:14:20 -0800332 }
rgindafeaf3142012-01-31 15:14:20 -0800333 }
334
rginda30f20f62012-04-05 16:36:19 -0700335 // We respect alt/metaSendsEscape even if the keymap action was a literal
336 // string. Otherwise, every overridden alt/meta action would have to
rginda39bdf6f2012-04-10 16:50:55 -0700337 // check alt/metaSendsEscape.
338 if ((alt && this.altSendsWhat == 'escape') ||
339 (this.metaSendsEscape && meta)) {
rginda42ad71d2012-02-16 14:06:28 -0800340 action = '\x1b' + action;
rginda39bdf6f2012-04-10 16:50:55 -0700341 }
rginda42ad71d2012-02-16 14:06:28 -0800342
343 } else if (alt || control || shift) {
344 // It's an escape sequence in the presence of a keyboard modifier...
rgindafeaf3142012-01-31 15:14:20 -0800345 var mod;
346
347 if (shift && !(alt || control)) {
348 mod = ';2';
349 } else if (alt && !(shift || control)) {
350 mod = ';3';
351 } else if (shift && alt && !control) {
352 mod = ';4';
353 } else if (control && !(shift || alt)) {
354 mod = ';5';
355 } else if (shift && control && !alt) {
356 mod = ';6';
357 } else if (alt && control && !shift) {
358 mod = ';7';
359 } else if (shift && alt && control) {
360 mod = ';8';
361 }
362
363 if (action.length == 3) {
364 // Some of the CSI sequences have zero parameters unless modified.
365 action = '\x1b[1' + mod + action.substr(2, 1);
366 } else {
367 // Others always have at least one parameter.
368 action = action.substr(0, action.length - 2) + mod +
369 action.substr(action.length - 1);
370 }
371 }
372
373 this.terminal.onVTKeystroke(action);
374};