/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { assert } from './helper'; import * as keyboardLayout from './usKeyboardLayout'; export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift'; export type Button = 'left' | 'right' | 'middle'; export type MouseClickOptions = { delay?: number; button?: Button; clickCount?: number; }; export type MouseMultiClickOptions = { delay?: number; button?: Button; }; export const keypadLocation = keyboardLayout.keypadLocation; type KeyDescription = { keyCode: number, keyCodeWithoutLocation: number, key: string, text: string, code: string, location: number, shifted?: KeyDescription; }; const kModifiers: Modifier[] = ['Alt', 'Control', 'Meta', 'Shift']; export interface RawKeyboard { keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise; keyup(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: 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) { 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 = description.text; await this._raw.keydown(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, description.key, description.location, autoRepeat, text); } private _keyDescriptionForString(keyString: string): KeyDescription { let description = usKeyboardLayout.get(keyString); assert(description, `Unknown key: "${keyString}"`); const shift = this._pressedModifiers.has('Shift'); description = shift && description.shifted ? description.shifted : description; // 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)) return { ...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.keyCodeWithoutLocation, description.key, description.location); } async insertText(text: string) { await this._raw.sendText(text); } async type(text: string, options?: { delay?: number }) { const delay = (options && options.delay) || undefined; for (const char of text) { if (usKeyboardLayout.has(char)) { await this.press(char, { delay }); } else { if (delay) await new Promise(f => setTimeout(f, delay)); await this.insertText(char); } } } async press(key: string, options: { delay?: number } = {}) { function split(keyString: string) { const keys = []; let building = ''; for (const char of keyString) { if (char === '+' && building) { keys.push(building); building = ''; } else { building += char; } } keys.push(building); return keys; } const tokens = split(key); key = tokens[tokens.length - 1]; for (let i = 0; i < tokens.length - 1; ++i) await this.down(tokens[i]); await this.down(key); if (options.delay) await new Promise(f => setTimeout(f, options.delay)); await this.up(key); for (let i = tokens.length - 2; i >= 0; --i) await this.up(tokens[i]); } async _ensureModifiers(modifiers: Modifier[]): Promise { for (const modifier of modifiers) { if (!kModifiers.includes(modifier)) throw new Error('Unknown 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