// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import * as fs from 'fs'; import * as path from 'path'; import { assert, helper } from './helper'; import * as types from './types'; import * as keyboardLayout from './USKeyboardLayout'; const readFileAsync = helper.promisify(fs.readFile); export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift'; export type Button = 'left' | 'right' | 'middle'; export type PointerActionOptions = { modifiers?: Modifier[]; relativePoint?: types.Point; }; export type ClickOptions = PointerActionOptions & { delay?: number; button?: Button; clickCount?: number; }; export type MultiClickOptions = PointerActionOptions & { delay?: number; button?: Button; }; export type SelectOption = { value?: string; label?: string; index?: number; }; export const keypadLocation = keyboardLayout.keypadLocation; type KeyDescription = { keyCode: number, key: string, text: string, code: string, location: number, }; const kModifiers: Modifier[] = ['Alt', 'Control', 'Meta', 'Shift']; export interface RawKeyboard { keydown(modifiers: Set, code: string, keyCode: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise; keyup(modifiers: Set, code: string, keyCode: number, key: string, location: number): Promise; sendText(text: string): Promise; } export class Keyboard { private _raw: RawKeyboard; private _pressedModifiers = new Set(); private _pressedKeys = new Set(); constructor(raw: RawKeyboard) { this._raw = raw; } async down(key: string, options: { text?: string; } = { text: undefined }) { const description = this._keyDescriptionForString(key); const autoRepeat = this._pressedKeys.has(description.code); this._pressedKeys.add(description.code); if (kModifiers.includes(description.key as Modifier)) this._pressedModifiers.add(description.key as Modifier); const text = options.text === undefined ? description.text : options.text; await this._raw.keydown(this._pressedModifiers, description.code, description.keyCode, description.key, description.location, autoRepeat, text); } private _keyDescriptionForString(keyString: string): KeyDescription { const shift = this._pressedModifiers.has('Shift'); const description: KeyDescription = { key: '', keyCode: 0, code: '', text: '', location: 0 }; const definition = keyboardLayout.keyDefinitions[keyString]; assert(definition, `Unknown key: "${keyString}"`); if (definition.key) description.key = definition.key; if (shift && definition.shiftKey) description.key = definition.shiftKey; if (definition.keyCode) description.keyCode = definition.keyCode; if (shift && definition.shiftKeyCode) description.keyCode = definition.shiftKeyCode; if (definition.code) description.code = definition.code; if (definition.location) description.location = definition.location; if (description.key.length === 1) description.text = description.key; if (definition.text) description.text = definition.text; if (shift && definition.shiftText) description.text = definition.shiftText; // if any modifiers besides shift are pressed, no text should be sent if (this._pressedModifiers.size > 1 || (!this._pressedModifiers.has('Shift') && this._pressedModifiers.size === 1)) description.text = ''; return description; } async up(key: string) { const description = this._keyDescriptionForString(key); if (kModifiers.includes(description.key as Modifier)) this._pressedModifiers.delete(description.key as Modifier); this._pressedKeys.delete(description.code); await this._raw.keyup(this._pressedModifiers, description.code, description.keyCode, description.key, description.location); } async sendCharacters(text: string) { await this._raw.sendText(text); } async type(text: string, options: { delay: (number | undefined); } | undefined) { const delay = (options && options.delay) || null; for (const char of text) { if (keyboardLayout.keyDefinitions[char]) { await this.press(char, {delay}); } else { if (delay) await new Promise(f => setTimeout(f, delay)); await this.sendCharacters(char); } } } async press(key: string, options: { delay?: number; text?: string; } = {}) { const {delay = null} = options; await this.down(key, options); if (delay) await new Promise(f => setTimeout(f, options.delay)); await this.up(key); } async _ensureModifiers(modifiers: Modifier[]): Promise { for (const modifier of modifiers) { if (!kModifiers.includes(modifier)) throw new Error('Uknown modifier ' + modifier); } const restore: Modifier[] = Array.from(this._pressedModifiers); const promises: Promise[] = []; for (const key of kModifiers) { const needDown = modifiers.includes(key); const isDown = this._pressedModifiers.has(key); if (needDown && !isDown) promises.push(this.down(key)); else if (!needDown && isDown) promises.push(this.up(key)); } await Promise.all(promises); return restore; } _modifiers(): Set { return this._pressedModifiers; } } export interface RawMouse { move(x: number, y: number, button: Button | 'none', buttons: Set