UNPKG

gpii-windows

Version:

Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™

373 lines (320 loc) 12.6 kB
/* * Sendkeys - Simulates key presses. * * Copyright 2019 Raising the Floor - International * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * The R&D leading to these results received funding from the * Department of Education - Grant H421A150005 (GPII-APCP). However, * these results do not necessarily represent the policy of the * Department of Education, and you should not assume endorsement by the * Federal Government. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); fluid.registerNamespace("gpii.windows.sendKeys"); // Other special key names, used in addition to gpii.windows.API_constants.virtualKeyCodes. Used to keep consistent with // AutoHotKey: https://hotkeyit.github.io/v2/docs/commands/Send.htm gpii.windows.sendKeys.keyNames = { "BS": gpii.windows.API_constants.virtualKeyCodes.VK_BACK, "BACKSPACE": gpii.windows.API_constants.virtualKeyCodes.VK_BACK, "DEL": gpii.windows.API_constants.virtualKeyCodes.VK_DELETE, "INS": gpii.windows.API_constants.virtualKeyCodes.VK_INSERT, "ESC": gpii.windows.API_constants.virtualKeyCodes.VK_ESCAPE, "ENTER": gpii.windows.API_constants.virtualKeyCodes.VK_RETURN, "CTRL": gpii.windows.API_constants.virtualKeyCodes.VK_CONTROL, "LCTRL": gpii.windows.API_constants.virtualKeyCodes.VK_LCONTROL, "RCTRL": gpii.windows.API_constants.virtualKeyCodes.VK_RCONTROL, "ALT": gpii.windows.API_constants.virtualKeyCodes.VK_MENU, "LALT": gpii.windows.API_constants.virtualKeyCodes.VK_LMENU, "RALT": gpii.windows.API_constants.virtualKeyCodes.VK_RMENU, "WIN": gpii.windows.API_constants.virtualKeyCodes.VK_LWIN, "PGUP": gpii.windows.API_constants.virtualKeyCodes.VK_PRIOR, "PGDOWN": gpii.windows.API_constants.virtualKeyCodes.VK_NEXT, "MEDIA_NEXT": gpii.windows.API_constants.virtualKeyCodes.VK_MEDIA_NEXT_TRACK, "MEDIA_PREV": gpii.windows.API_constants.virtualKeyCodes.VK_MEDIA_PREV_TRACK, "PRINTSCREEN": gpii.windows.API_constants.virtualKeyCodes.VK_SNAPSHOT, "CTRLBREAK": gpii.windows.API_constants.virtualKeyCodes.VK_CANCEL, "APPSKEY": gpii.windows.API_constants.virtualKeyCodes.VK_APPS }; // Modifier keys which are prefixed to a key gpii.windows.sendKeys.modifiers = { "+": gpii.windows.API_constants.virtualKeyCodes.VK_SHIFT, "^": gpii.windows.API_constants.virtualKeyCodes.VK_CONTROL, "!": gpii.windows.API_constants.virtualKeyCodes.VK_MENU, "#": gpii.windows.API_constants.virtualKeyCodes.VK_LWIN }; /** * Simulated key input. * @typedef KeyPress {Object} * @property {String} keyName The key name. * @property {Array<String>} args The text after the key name, split by whitespace. * @property {Function} command A function to call, instead of pressing a key. * @property {Number} virtualKey The virtual key code. * @property {Number} unicode The character's unicode number (for characters with no corresponding virtual key code) * @property {Array<String>} modifiers The modifier keys. * @property {String} state "up", "down", or null to press and release. * @property {Number} count Number of times to apply the key. */ /** * Sends a sequence of key presses, as described by a string in a format inspired by AutoHotKey's `Send` command. * @param {String} keySequence The key sequence. * @return {Promise} Resolves when the keys have been sent. */ gpii.windows.sendKeys.send = function (keySequence) { var keys = gpii.windows.sendKeys.parse(keySequence); var promise = fluid.promise(); var current = 0; var nextKey = function () { if (current >= keys.length) { promise.resolve(); } else { var result = gpii.windows.sendKeys.sendKey(keys[current++]); var p = fluid.toPromise(result); p.then(nextKey); } }; nextKey(); return promise; }; /** * Parses a sequence of key presses * @param {String} keyString The key sequence. * @return {Array<KeyPress>} Key press information for each key in the sequence. */ gpii.windows.sendKeys.parse = function (keyString) { // A single key token can be a literal character or something between "{" and "}", prefixed with zero or more // modifier key characters (!, +, ^, or #). var keyMatch = /[!+^#]*(\{(.[^}]*)\}|.)/g; return fluid.transform(keyString.match(keyMatch), gpii.windows.sendKeys.getKey); }; /** * Gets key press information for a single key. * * @param {String} keyText String identifying a single key. * @return {KeyPress} The key press information for the given key. */ gpii.windows.sendKeys.getKey = function (keyText) { /** @type KeyPress */ var key = { args: [], modifiers: {} }; // Keys with a modifier prefix (^C, !^{DEL}) var modifier; while ((modifier = gpii.windows.sendKeys.modifiers[keyText[0]])) { key.modifiers[modifier] = true; keyText = keyText.substr(1); } // {key [up|down] [number of times]} if (keyText.startsWith("{") && keyText.endsWith("}")) { var content = keyText.substr(1, keyText.length - 2).split(/\s+/); key.keyName = content.shift(); key.args = content; fluid.each(key.args, function (arg) { if (arg.toLowerCase() === "down" || arg.toLowerCase() === "up") { key.state = arg.toLowerCase(); } else { var number = parseInt(arg); if (number >= 0) { key.count = number; } } }); } else { key.keyName = keyText; } var keyValue; if (key.keyName.length === 1) { if (Object.keys(key.modifiers).length > 0) { // The key-press has explicit modifiers - Lower the letter to avoid an additional 'shift' modifier because // something like "^C" usually means just ctrl + C key.keyName = key.keyName.toLowerCase(); } keyValue = gpii.windows.sendKeys.getCharacterKey(key.keyName); } else { // upper case the key names key.keyName = key.keyName.toUpperCase(); keyValue = gpii.windows.sendKeys.getSpecialKey(key.keyName); } if (keyValue.modifiers) { Object.assign(key.modifiers, keyValue.modifiers); delete keyValue.modifiers; } Object.assign(key, keyValue); return key; }; /** * Gets a virtual key code, and the modifiers required to send a character key. * * @param {String} character Character of the key. * @return {KeyPress} The key press required to generate the given character. */ gpii.windows.sendKeys.getCharacterKey = function (character) { var togo = { modifiers: {} }; var charCode = character.charCodeAt(0); var ascii; if (charCode <= 0xff) { ascii = charCode; } var modifierBits; if (ascii) { // Get the key+modifiers required to generate the character var keys = gpii.windows.user32.VkKeyScanW(ascii); if (keys === -1) { // Might be an OEM key (non-standard layout) var scan = gpii.windows.user32.OemKeyScan(charCode); if (scan !== gpii.windows.API_constants.UINT_MAX) { togo.unicode = scan & 0xff; modifierBits = (scan >> 8) & 0xff; keys = 0; } } else { togo.virtualKey = keys & 0xff; modifierBits = (keys >> 8) & 0xff; } } if (!ascii || !(togo.virtualKey || togo.unicode)) { // no such key combination for this character - send it as a virtual unicode key togo.unicode = charCode; } // Add the modifiers required for the character. if (modifierBits & 1) { togo.modifiers[gpii.windows.API_constants.virtualKeyCodes.VK_SHIFT] = true; } if (modifierBits & 2) { togo.modifiers[gpii.windows.API_constants.virtualKeyCodes.VK_CONTROL] = true; } if (modifierBits & 4) { togo.modifiers[gpii.windows.API_constants.virtualKeyCodes.VK_MENU] = true; } return togo; }; /** * Gets the key press information from a key's name. * * @param {String} keyName The name of the key, such as "ESC". * @return {KeyPress} The key press required to generate the given character. */ gpii.windows.sendKeys.getSpecialKey = function (keyName) { var togo = {}; var command = gpii.windows.sendKeys.commands[keyName.toLowerCase()]; if (command) { togo.command = command; } else { keyName = keyName.toUpperCase(keyName); var vk = gpii.windows.sendKeys.keyNames[keyName] || gpii.windows.API_constants.virtualKeyCodes["VK_" + keyName]; if (vk) { togo.virtualKey = vk; } else if (keyName.startsWith("U+")) { // U+nnnn: Send a unicode character var number = parseInt(keyName.substring(2), 16); if (number) { togo.unicode = number; } } } return togo; }; // Keys which are really function calls. gpii.windows.sendKeys.commands = {}; /** * Pauses the key input for a given number of seconds, and/or until another window has been activated. * * @param {String} window [optional] If "window", wait for another window to be activated. * @param {String} delay Numeric string specifying the seconds to wait. * @return {Promise} Resolves after `delay` seconds, or another window to be activated if `window` is set (whichever is * first). */ gpii.windows.sendKeys.commands.wait = function (window, delay) { var promise = fluid.promise(); var waitForWindow = window.toLowerCase() === "window"; if (!waitForWindow && delay === undefined) { delay = window; }; var number = parseFloat(delay); var timeout; if (isFinite(number)) { timeout = number * 1000; } else { timeout = waitForWindow ? 10000 : 500; } if (waitForWindow) { // Wait for the active window to change. var currentWindow = gpii.windows.user32.GetForegroundWindow(); return gpii.windows.waitForCondition(function () { return currentWindow === gpii.windows.user32.GetForegroundWindow(); }, {dontReject: true, timeout: timeout}); } else { setTimeout(promise.resolve, timeout); } return promise; }; /** * Sends a simulated key press event to the OS. * * @param {KeyPress} key Information about the key press. * @return {Promise|undefined} The result of the key.command function, or undefined. */ gpii.windows.sendKeys.sendKey = function (key) { var INPUT_KEYBOARD = 1; var inputSize = 28; var keyInput = new gpii.windows.KEY_INPUT(); keyInput.ref().fill(0); keyInput.type = INPUT_KEYBOARD; if (key.unicode) { keyInput.wScan = key.unicode; keyInput.dwFlags = gpii.windows.API_constants.KEYEVENTF_UNICODE; } else { keyInput.wVk = key.virtualKey; } var modifiersApplied = gpii.windows.sendKeys.setModifiers(key.modifiers, true); var togo; if (key.command) { togo = key.command.apply(null, key.args); } else { var count = key.count || 1; for (var n = 0; n < count; n++) { // Press the key if (key.state !== "up") { keyInput.dwFlags &= ~gpii.windows.API_constants.KEYEVENTF_KEYUP; gpii.windows.user32.SendInput(1, keyInput.ref(), inputSize); } // Release the key if (key.state !== "down") { keyInput.dwFlags |= gpii.windows.API_constants.KEYEVENTF_KEYUP; gpii.windows.user32.SendInput(1, keyInput.ref(), inputSize); } } gpii.windows.sendKeys.setModifiers(modifiersApplied, false); } return togo; }; gpii.windows.sendKeys.setModifiers = function (modifiers) { var changed = {}; fluid.each(modifiers, function (state, modifier) { var currentState = gpii.windows.user32.GetAsyncKeyState(modifier) & 0x8000; if (!!currentState !== !!state) { gpii.windows.sendKeys.sendKey({ virtualKey: modifier, state: state ? "down" : "up" }); changed[modifier] = !!currentState; } }); return changed; }; fluid.defaults("gpii.windows.sendKeys.send", { gradeNames: "fluid.function", argumentMap: { keys: 0 } });