blob: cf494c9173c91fc1fefd9d36d4f7c87965fbf754 [file] [log] [blame]
var util = require('util')
var adb = require('adbkit')
var Promise = require('bluebird')
var keyutil = module.exports = Object.create(null)
keyutil.parseKeyCharacterMap = function(stream) {
var resolver = Promise.defer()
var state = 'type_t'
var keymap = {
type: null
, keys: []
}
var lastKey, lastRule, lastModifier, lastBehavior
function fail(char, state) {
throw new Error(util.format(
'Unexpected character "%s" in state "%s"'
, char
, state
))
}
function parse(char) {
switch (state) {
case 'comment_before_type_t':
if (char === '\n') {
state = 'type_t'
break
}
return true
case 'type_t':
if (char === '\n') {
return true
}
if (char === '#') {
state = 'comment_before_type_t'
return true
}
if (char === 'k') {
state = 'key_k'
return parse(char)
}
if (char === 't') {
state = 'type_y'
return true
}
return fail(char, state)
case 'type_y':
if (char === 'y') {
state = 'type_p'
return true
}
return fail(char, state)
case 'type_p':
if (char === 'p') {
state = 'type_e'
return true
}
return fail(char, state)
case 'type_e':
if (char === 'e') {
state = 'type_name_start'
keymap.type = ''
return true
}
return fail(char, state)
case 'type_name_start':
if (char === ' ') {
return true
}
if (char >= 'A' && char <= 'Z') {
keymap.type += char
state = 'type_name_continued'
return true
}
return fail(char, state)
case 'type_name_continued':
if (char === '\n') {
// Could have more of these, although it doesn't make much sense
state = 'type_t'
return true
}
if (char >= 'A' && char <= 'Z') {
keymap.type += char
return true
}
return fail(char, state)
case 'comment_before_key_k':
if (char === '\n') {
state = 'key_k'
break
}
return true
case 'key_k':
if (char === '\n') {
return true
}
if (char === '#') {
state = 'comment_before_key_k'
return true
}
if (char === 'k') {
state = 'key_e'
return true
}
return fail(char, state)
case 'key_e':
if (char === 'e') {
state = 'key_y'
return true
}
return fail(char, state)
case 'key_y':
if (char === 'y') {
state = 'key_name_start'
return true
}
return fail(char, state)
case 'key_name_start':
if (char === ' ') {
return true
}
if ((char >= '0' && char <= '9') ||
(char >= 'A' && char <= 'Z')) {
keymap.keys.push(lastKey = {
key: char
, rules: []
})
state = 'key_name_continued'
return true
}
return fail(char, state)
case 'key_name_continued':
if (char === ' ') {
state = 'key_start_block'
return true
}
if ((char >= '0' && char <= '9') ||
(char >= 'A' && char <= 'Z') ||
(char === '_')) {
lastKey.key += char
return true
}
return fail(char, state)
case 'key_start_block':
if (char === ' ') {
return true
}
if (char === '{') {
state = 'filter_name_start'
return true
}
return fail(char, state)
case 'filter_name_start':
if (char === '\n' || char === '\t' || char === ' ') {
return true
}
if (char === '}') {
state = 'key_k'
return true
}
if (char >= 'a' && char <= 'z') {
lastKey.rules.push(lastRule = {
modifiers: [lastModifier = {
type: char
}]
, behaviors: []
})
state = 'filter_name_continued'
return true
}
return fail(char, state)
case 'filter_name_continued':
if (char === ':') {
state = 'filter_behavior_start'
return true
}
if (char === ',') {
state = 'filter_name_or_start'
return true
}
if (char === '+') {
state = 'filter_name_and_start'
return true
}
if (char >= 'a' && char <= 'z') {
lastModifier.type += char
return true
}
return fail(char, state)
case 'filter_name_or_start':
if (char === ' ') {
return true
}
if (char >= 'a' && char <= 'z') {
lastKey.rules.push(lastRule = {
modifiers: [lastModifier = {
type: char
}]
, behaviors: lastRule.behaviors
})
state = 'filter_name_continued'
return true
}
return fail(char, state)
case 'filter_name_and_start':
if (char === ' ') {
return true
}
if (char >= 'a' && char <= 'z') {
lastRule.modifiers.push(lastModifier = {
type: char
})
state = 'filter_name_continued'
return true
}
return fail(char, state)
case 'filter_behavior_literal':
if (char === '\\') {
state = 'filter_behavior_literal_escape'
return true
}
if (char !== "'") {
lastRule.behaviors.push({
type: 'literal'
, value: char
})
state = 'filter_behavior_literal_end'
return true
}
return fail(char, state)
case 'filter_behavior_literal_escape':
if (char === '\\' || char === '\'' || char === '"') {
lastRule.behaviors.push({
type: 'literal'
, value: char
})
state = 'filter_behavior_literal_end'
return true
}
if (char === 'n') {
lastRule.behaviors.push({
type: 'literal'
, value: '\n'
})
state = 'filter_behavior_literal_end'
return true
}
if (char === 't') {
lastRule.behaviors.push({
type: 'literal'
, value: '\t'
})
state = 'filter_behavior_literal_end'
return true
}
if (char === 'u') {
state = 'filter_behavior_literal_unicode_1'
return true
}
return fail(char, state)
case 'filter_behavior_literal_end':
if (char === '\'') {
state = 'filter_behavior_start'
return true
}
return fail(char, state)
case 'filter_behavior_start':
if (char === '\n') {
state = 'filter_name_start'
return true
}
if (char === ' ') {
return true
}
if (char === "'") {
state = 'filter_behavior_literal'
return true
}
if (char === 'n') {
state = 'filter_behavior_none_2'
return true
}
if (char === 'f') {
state = 'filter_behavior_fallback_2'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_2':
if (char === 'a') {
state = 'filter_behavior_fallback_3'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_3':
if (char === 'l') {
state = 'filter_behavior_fallback_4'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_4':
if (char === 'l') {
state = 'filter_behavior_fallback_5'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_5':
if (char === 'b') {
state = 'filter_behavior_fallback_6'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_6':
if (char === 'a') {
state = 'filter_behavior_fallback_7'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_7':
if (char === 'c') {
state = 'filter_behavior_fallback_8'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_8':
if (char === 'k') {
state = 'filter_behavior_fallback_key_start'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_key_start':
if (char === ' ') {
return true
}
if ((char >= '0' && char <= '9') ||
(char >= 'A' && char <= 'Z')) {
lastRule.behaviors.push(lastBehavior = {
type: 'fallback'
, key: char
})
state = 'filter_behavior_fallback_key_continued'
return true
}
return fail(char, state)
case 'filter_behavior_fallback_key_continued':
if (char === ' ') {
state = 'filter_behavior_start'
return true
}
if (char === '\n') {
state = 'filter_name_start'
return true
}
if ((char >= '0' && char <= '9') ||
(char >= 'A' && char <= 'Z') ||
(char === '_')) {
lastBehavior.key += char
return true
}
return fail(char, state)
case 'filter_behavior_none_2':
if (char === 'o') {
state = 'filter_behavior_none_3'
return true
}
return fail(char, state)
case 'filter_behavior_none_3':
if (char === 'n') {
state = 'filter_behavior_none_4'
return true
}
return fail(char, state)
case 'filter_behavior_none_4':
if (char === 'e') {
lastRule.behaviors.push({
type: 'none'
})
state = 'filter_behavior_start'
return true
}
return fail(char, state)
case 'filter_behavior_literal_unicode_1':
if ((char >= '0' && char <= '9') ||
(char >= 'a' && char <= 'f')) {
lastRule.behaviors.push(lastBehavior = {
type: 'literal'
, value: parseInt(char, 16) << 12
})
state = 'filter_behavior_literal_unicode_2'
return true
}
return fail(char, state)
case 'filter_behavior_literal_unicode_2':
if ((char >= '0' && char <= '9') ||
(char >= 'a' && char <= 'f')) {
lastBehavior.value += parseInt(char, 16) << 8
state = 'filter_behavior_literal_unicode_3'
return true
}
return fail(char, state)
case 'filter_behavior_literal_unicode_3':
if ((char >= '0' && char <= '9') ||
(char >= 'a' && char <= 'f')) {
lastBehavior.value += parseInt(char, 16) << 4
state = 'filter_behavior_literal_unicode_4'
return true
}
return fail(char, state)
case 'filter_behavior_literal_unicode_4':
if ((char >= '0' && char <= '9') ||
(char >= 'a' && char <= 'f')) {
lastBehavior.value += parseInt(char, 16)
lastBehavior.value = String.fromCharCode(lastBehavior.value)
state = 'filter_behavior_literal_end'
return true
}
return fail(char, state)
default:
throw new Error(util.format('Unexpected state "%s"', state))
}
}
function errorListener(err) {
resolver.reject(err)
}
function readableListener() {
var chunk = stream.read()
var i = 0
var l = chunk.length
try {
while (i < l) {
parse(String.fromCharCode(chunk[i++]))
}
}
catch (err) {
resolver.reject(err)
}
}
function endListener() {
resolver.resolve(keymap)
}
stream.on('error', errorListener)
stream.on('readable', readableListener)
stream.on('end', endListener)
return resolver.promise.finally(function() {
stream.removeListener('error', errorListener)
stream.removeListener('readable', readableListener)
stream.removeListener('end', endListener)
})
}
keyutil.namedKey = function(name) {
var key = adb.Keycode['KEYCODE_' + name.toUpperCase()]
if (typeof key === 'undefined') {
throw new Error(util.format('Unknown key "%s"', name))
}
return key
}
keyutil.buildCharMap = function(keymap) {
var charmap = Object.create(null)
keymap.keys.forEach(function(key) {
key.rules.forEach(function(rule) {
var combination = {
key: keyutil.namedKey(key.key)
, modifiers: []
, complexity: 0
}
var shouldHandle = rule.modifiers.every(function(modifier) {
switch (modifier.type) {
case 'label':
return false // ignore
case 'base':
return true
case 'shift':
case 'lshift':
combination.modifiers.push(adb.Keycode.KEYCODE_SHIFT_LEFT)
combination.complexity += 10
return true
case 'rshift':
combination.modifiers.push(adb.Keycode.KEYCODE_SHIFT_RIGHT)
combination.complexity += 10
return true
case 'alt':
case 'lalt':
combination.modifiers.push(adb.Keycode.KEYCODE_ALT_LEFT)
combination.complexity += 20
return true
case 'ralt':
combination.modifiers.push(adb.Keycode.KEYCODE_ALT_RIGHT)
combination.complexity += 20
return true
case 'ctrl':
case 'lctrl':
combination.modifiers.push(adb.Keycode.KEYCODE_CTRL_LEFT)
combination.complexity += 20
return true
case 'rctrl':
combination.modifiers.push(adb.Keycode.KEYCODE_CTRL_RIGHT)
combination.complexity += 20
return true
case 'meta':
case 'lmeta':
combination.modifiers.push(adb.Keycode.KEYCODE_META_LEFT)
combination.complexity += 20
return true
case 'rmeta':
combination.modifiers.push(adb.Keycode.KEYCODE_META_RIGHT)
combination.complexity += 20
return true
case 'sym':
combination.modifiers.push(adb.Keycode.KEYCODE_SYM)
combination.complexity += 10
return true
case 'fn':
combination.modifiers.push(adb.Keycode.KEYCODE_FUNCTION)
combination.complexity += 30
return true
case 'capslock':
combination.modifiers.push(adb.Keycode.KEYCODE_CAPS_LOCK)
combination.complexity += 30
return true
case 'numlock':
combination.modifiers.push(adb.Keycode.KEYCODE_NUM_LOCK)
combination.complexity += 30
return true
case 'scrolllock':
combination.modifiers.push(adb.Keycode.KEYCODE_SCROLL_LOCK)
combination.complexity += 30
return true
}
})
if (!shouldHandle) {
return
}
rule.behaviors.forEach(function(behavior) {
switch (behavior.type) {
case 'literal':
if (!charmap[behavior.value]) {
charmap[behavior.value] = [combination]
}
else {
charmap[behavior.value].push(combination)
// Could be more efficient, but we only have 1-4 combinations
// per key, so we don't really care.
charmap[behavior.value].sort(function(a, b) {
return a.complexity - b.complexity
})
}
break
}
})
})
})
return charmap
}