blob: 415ccf6c788747dab5181cc16eee0644df83b26d [file] [log] [blame]
// Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @typedef {{
* keyCode: number,
* shift: (boolean|undefined),
* ctrl: (boolean|undefined),
* alt: (boolean|undefined),
* meta: (boolean|undefined),
* }}
*/
hterm.Keyboard.KeyDown;
/**
* @typedef {function(!hterm.Terminal, !hterm.Keyboard.KeyDown):
* !hterm.Keyboard.KeyAction}
*/
hterm.Keyboard.KeyBindingFunction;
/** @typedef {!hterm.Keyboard.KeyAction|!hterm.Keyboard.KeyBindingFunction} */
hterm.Keyboard.KeyBindingAction;
/**
* @typedef {{
* keyPattern: !hterm.Keyboard.KeyPattern,
* action: !hterm.Keyboard.KeyBindingAction,
* }}
*/
hterm.Keyboard.KeyBinding;
/**
* A mapping from hterm.Keyboard.KeyPattern to an action.
*
* TODO(rginda): For now this bindings code is only used for user overrides.
* hterm.Keyboard.KeyMap still handles all of the built-in key mappings.
* It'd be nice if we migrated that over to be hterm.Keyboard.Bindings based.
*
* @constructor
*/
hterm.Keyboard.Bindings = function() {
/** @private {!Object<number, !Array<!hterm.Keyboard.KeyBinding>>} */
this.bindings_ = {};
};
/**
* Default bindings for each OS.
*
* @type {!Object<string, !Object<string, string>>}
*/
hterm.Keyboard.Bindings.OsDefaults = {
'android': {
},
'cros': {
// Submit feedback.
'Alt+Shift+I': 'PASS',
// Toggle chromevox.
'Ctrl+Alt+Z': 'PASS',
// Switch input method.
'Ctrl+Space': 'PASS',
},
'linux': {
},
'mac': {
// Home.
'Meta+Left': '"\u001b[H"',
// End.
'Meta+Right': '"\u001b[F"',
},
'windows': {
},
};
/**
* Remove all bindings.
*/
hterm.Keyboard.Bindings.prototype.clear = function() {
this.bindings_ = {};
};
/**
* Add a new binding.
*
* Internal API that assumes parsed objects as inputs.
* See the public addBinding for more details.
*
* @param {!hterm.Keyboard.KeyPattern} keyPattern
* @param {!hterm.Keyboard.KeyBindingAction} action
*/
hterm.Keyboard.Bindings.prototype.addBinding_ = function(keyPattern, action) {
let binding = null;
const list = this.bindings_[keyPattern.keyCode];
if (list) {
for (let i = 0; i < list.length; i++) {
if (list[i].keyPattern.matchKeyPattern(keyPattern)) {
binding = list[i];
break;
}
}
}
if (binding) {
binding.action = action;
} else {
binding = {keyPattern: keyPattern, action: action};
if (!list) {
this.bindings_[keyPattern.keyCode] = [binding];
} else {
this.bindings_[keyPattern.keyCode].push(binding);
list.sort(function(a, b) {
return hterm.Keyboard.KeyPattern.sortCompare(
a.keyPattern, b.keyPattern);
});
}
}
};
/**
* Add a new binding.
*
* If a binding for the keyPattern already exists it will be overridden.
*
* More specific keyPatterns take precedence over those with wildcards. Given
* bindings for "Ctrl+A" and "Ctrl+*+A", and a "Ctrl+A" keydown, the "Ctrl+A"
* binding will match even if "Ctrl+*+A" was created last.
*
* If action is a string, it will be passed through hterm.Parser.parseKeyAction.
*
* For example:
* // Will replace Ctrl+P keystrokes with the string "hiya!".
* addBinding('Ctrl+P', "'hiya!'");
* // Will cancel the keystroke entirely (make it do nothing).
* addBinding('Alt+D', hterm.Keyboard.KeyActions.CANCEL);
* // Will execute the code and return the action.
* addBinding('Ctrl+T', function() {
* console.log('Got a T!');
* return hterm.Keyboard.KeyActions.PASS;
* });
*
* @param {string|!hterm.Keyboard.KeyPattern} key
* @param {!hterm.Keyboard.KeyBindingAction} action
*/
hterm.Keyboard.Bindings.prototype.addBinding = function(key, action) {
// If we're given a hterm.Keyboard.KeyPattern object, pass it down.
if (typeof key != 'string') {
this.addBinding_(key, action);
return;
}
// Here we treat key as a string.
const p = new hterm.Parser();
p.reset(key);
let sequence;
try {
sequence = p.parseKeySequence();
} catch (ex) {
console.error(ex);
return;
}
if (!p.isComplete()) {
console.error(p.error('Expected end of sequence: ' + sequence));
return;
}
// If action is a string, parse it. Otherwise assume it's callable.
if (typeof action == 'string') {
p.reset(action);
try {
action = p.parseKeyAction();
} catch (ex) {
console.error(ex);
return;
}
}
if (!p.isComplete()) {
console.error(p.error('Expected end of sequence: ' + sequence));
return;
}
this.addBinding_(new hterm.Keyboard.KeyPattern(sequence), action);
};
/**
* Add multiple bindings at a time using a map of {string: string, ...}
*
* This uses hterm.Parser to parse the maps key into KeyPatterns, and the
* map values into {!hterm.Keyboard.KeyBindingAction}.
*
* For example:
* {
* // Will replace Ctrl+P keystrokes with the string "hiya!".
* 'Ctrl+P': "'hiya!'",
* // Will cancel the keystroke entirely (make it do nothing).
* 'Alt+D': hterm.Keyboard.KeyActions.CANCEL,
* }
*
* @param {!Object<string, !hterm.Keyboard.KeyBindingAction>} map
* @param {boolean=} addOsDefaults If true, OS defaults are added first to
* ensure user defined mappings take precedence.
*/
hterm.Keyboard.Bindings.prototype.addBindings = function(
map, addOsDefaults = false) {
if (addOsDefaults) {
this.addBindings(hterm.Keyboard.Bindings.OsDefaults[hterm.os] || {});
}
for (const key in map) {
this.addBinding(key, map[key]);
}
};
/**
* Return the binding that is the best match for the given keyDown record,
* or null if there is no match.
*
* @param {!hterm.Keyboard.KeyDown} keyDown An object with a keyCode property
* and zero or more boolean properties representing key modifiers. These
* property names must match those defined in
* hterm.Keyboard.KeyPattern.modifiers.
* @return {?hterm.Keyboard.KeyBinding} The keyboard binding for this key.
*/
hterm.Keyboard.Bindings.prototype.getBinding = function(keyDown) {
const list = this.bindings_[keyDown.keyCode];
if (!list) {
return null;
}
for (let i = 0; i < list.length; i++) {
const binding = list[i];
if (binding.keyPattern.matchKeyDown(keyDown)) {
return binding;
}
}
return null;
};