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 }