gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
373 lines (320 loc) • 12.6 kB
JavaScript
/*
* 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
*/
;
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
}
});