impr: monkey now types with the same hand as the user (@ShaneBerhoff) (#5930)

This PR also refactors some layout conversion code to reduce repetition.
This commit is contained in:
ShaneBerhoff 2024-10-14 09:33:33 -04:00 committed by GitHub
parent ebf98f15f6
commit ac639348ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 355 additions and 182 deletions

View file

@ -35,6 +35,7 @@ import { IgnoredKeys } from "../constants/ignored-keys";
import { ModifierKeys } from "../constants/modifier-keys";
import { navigate } from "./route-controller";
import * as Loader from "../elements/loader";
import * as KeyConverter from "../utils/key-converter";
let dontInsertSpace = false;
let correctShiftUsed = true;
@ -1047,8 +1048,6 @@ $(document).on("keydown", async (event) => {
}
}
Monkey.type();
if (event.key === "Backspace" && TestInput.input.current.length === 0) {
backspaceToPrevious();
if (TestInput.input.current) {
@ -1123,14 +1122,16 @@ $(document).on("keydown", async (event) => {
return;
}
const keycode = ShiftTracker.layoutKeyToKeycode(event.key, keymapLayout);
const keycode = KeyConverter.layoutKeyToKeycode(event.key, keymapLayout);
correctShiftUsed =
keycode === undefined
? true
: ShiftTracker.isUsingOppositeShift(keycode);
} else {
correctShiftUsed = ShiftTracker.isUsingOppositeShift(event.code);
correctShiftUsed = ShiftTracker.isUsingOppositeShift(
event.code as KeyConverter.Keycode
);
}
}
@ -1182,6 +1183,7 @@ $("#wordsInput").on("keydown", (event) => {
return;
}
Monkey.type(event);
// console.debug("Event: keydown", event);
if (event.code === "NumpadEnter" && Config.funbox.includes("58008")) {
@ -1235,10 +1237,11 @@ $("#wordsInput").on("keyup", (event) => {
return;
}
Monkey.stop(event);
if (IgnoredKeys.includes(event.key)) return;
if (TestUI.resultVisible) return;
Monkey.stop();
});
$("#wordsInput").on("beforeinput", (event) => {

View file

@ -8,10 +8,10 @@ let isAltGrPressed = false;
const isPunctuationPattern = /\p{P}/u;
export async function getCharFromEvent(
event: JQuery.KeyDownEvent
event: JQuery.KeyDownEvent | JQuery.KeyUpEvent
): Promise<string | null> {
function emulatedLayoutGetVariant(
event: JQuery.KeyDownEvent,
event: JQuery.KeyDownEvent | JQuery.KeyUpEvent,
keyVariants: string
): string | undefined {
let isCapitalized = event.shiftKey;

View file

@ -2,6 +2,7 @@ import { mapRange } from "@monkeytype/util/numbers";
import Config from "../config";
import * as ConfigEvent from "../observables/config-event";
import * as TestState from "../test/test-state";
import * as KeyConverter from "../utils/key-converter";
ConfigEvent.subscribe((eventKey) => {
if (eventKey === "monkey" && TestState.isActive) {
@ -15,6 +16,7 @@ ConfigEvent.subscribe((eventKey) => {
let left = false;
let right = false;
const middleKeysState = { left: false, right: false, last: "right" };
// 0 hand up
// 1 hand down
@ -38,8 +40,6 @@ const elementsFast = {
"11": document.querySelector("#monkey .fast .both"),
};
let last = "right";
function toBit(b: boolean): "1" | "0" {
return b ? "1" : "0";
}
@ -70,25 +70,68 @@ export function updateFastOpacity(num: number): void {
$("#monkey").css({ animationDuration: animDuration + "s" });
}
export function type(): void {
export function type(event: JQuery.KeyDownEvent): void {
if (!Config.monkey) return;
if (!left && last === "right") {
left = true;
last = "left";
} else if (!right) {
right = true;
last = "right";
const { leftSide, rightSide } = KeyConverter.keycodeToKeyboardSide(
event.code as KeyConverter.Keycode
);
if (leftSide && rightSide) {
// if its a middle key handle special case
if (middleKeysState.last === "left") {
if (!right) {
right = true;
middleKeysState.last = "right";
middleKeysState.right = true;
} else if (!left) {
left = true;
middleKeysState.last = "left";
middleKeysState.left = true;
}
} else {
if (!left) {
left = true;
middleKeysState.last = "left";
middleKeysState.left = true;
} else if (!right) {
right = true;
middleKeysState.last = "right";
middleKeysState.right = true;
}
}
} else {
// normal key set hand
left = left || leftSide;
right = right || rightSide;
}
update();
}
export function stop(): void {
export function stop(event: JQuery.KeyUpEvent): void {
if (!Config.monkey) return;
if (left) {
left = false;
} else if (right) {
right = false;
const { leftSide, rightSide } = KeyConverter.keycodeToKeyboardSide(
event.code as KeyConverter.Keycode
);
if (leftSide && rightSide) {
// if middle key handle special case
if (middleKeysState.left && middleKeysState.last === "left") {
left = false;
middleKeysState.left = false;
} else if (middleKeysState.right && middleKeysState.last === "right") {
right = false;
middleKeysState.right = false;
} else {
left = left && !middleKeysState.left;
right = right && !middleKeysState.right;
}
} else {
// normal key unset hand
left = left && !leftSide;
right = right && !rightSide;
}
update();
}

View file

@ -2,71 +2,13 @@ import Config from "../config";
import * as JSONData from "../utils/json-data";
import { capsState } from "./caps-warning";
import * as Notifications from "../elements/notifications";
import * as KeyConverter from "../utils/key-converter";
export let leftState = false;
export let rightState = false;
type KeymapLegendStates = [letters: boolean, symbols: boolean];
const qwertyKeycodeKeymap = [
[
"Backquote",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
"Digit7",
"Digit8",
"Digit9",
"Digit0",
"Minus",
"Equal",
],
[
"KeyQ",
"KeyW",
"KeyE",
"KeyR",
"KeyT",
"KeyY",
"KeyU",
"KeyI",
"KeyO",
"KeyP",
"BracketLeft",
"BracketRight",
"Backslash",
],
[
"KeyA",
"KeyS",
"KeyD",
"KeyF",
"KeyG",
"KeyH",
"KeyJ",
"KeyK",
"KeyL",
"Semicolon",
"Quote",
],
[
"KeyZ",
"KeyX",
"KeyC",
"KeyV",
"KeyB",
"KeyN",
"KeyM",
"Comma",
"Period",
"Slash",
],
["Space"],
];
const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u;
const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
@ -149,11 +91,10 @@ async function updateKeymapLegendCasing(): Promise<void> {
(character) => symbolsPattern.test(character ?? "")
);
const keycode = layoutKeyToKeycode(lowerCaseCharacter, layout);
const keycode = KeyConverter.layoutKeyToKeycode(lowerCaseCharacter, layout);
if (keycode === undefined) {
return;
}
const oppositeShift = isUsingOppositeShift(keycode);
const state = keyIsSymbol ? symbolsState : lettersState;
@ -195,71 +136,7 @@ export function reset(): void {
rightState = false;
}
const leftSideKeys = [
"KeyQ",
"KeyW",
"KeyE",
"KeyR",
"KeyT",
"KeyY",
"KeyA",
"KeyS",
"KeyD",
"KeyF",
"KeyG",
"IntlBackslash",
"KeyZ",
"KeyX",
"KeyC",
"KeyV",
"KeyB",
"Backquote",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
];
const rightSideKeys = [
"KeyY",
"KeyU",
"KeyI",
"KeyO",
"KeyP",
"KeyH",
"KeyJ",
"KeyK",
"KeyL",
"KeyB",
"KeyN",
"KeyM",
"Digit6",
"Digit7",
"Digit8",
"Digit9",
"Digit0",
"Minus",
"Equal",
"Backslash",
"BracketLeft",
"BracketRight",
"Semicolon",
"Quote",
"Comma",
"Period",
"Slash",
];
export function isUsingOppositeShift(keycode: string): boolean {
export function isUsingOppositeShift(keycode: KeyConverter.Keycode): boolean {
if (!leftState && !rightState) {
return true;
}
@ -268,46 +145,14 @@ export function isUsingOppositeShift(keycode: string): boolean {
return true;
}
const isRight = rightSideKeys.includes(keycode);
const isLeft = leftSideKeys.includes(keycode);
if (!isRight && !isLeft) {
const { leftSide, rightSide } = KeyConverter.keycodeToKeyboardSide(keycode);
if (!leftSide && !rightSide) {
return true;
}
if ((leftState && isRight) || (rightState && isLeft)) {
if ((leftState && rightSide) || (rightState && leftSide)) {
return true;
}
return false;
}
export function layoutKeyToKeycode(
key: string,
layout: JSONData.Layout
): string | undefined {
const rows: string[][] = Object.values(layout.keys);
const rowIndex = rows.findIndex((row) => row.find((k) => k.includes(key)));
const row = rows[rowIndex];
if (row === undefined) {
return;
}
const keyIndex = row.findIndex((k) => k.includes(key));
if (keyIndex === -1) {
return;
}
let keycode = qwertyKeycodeKeymap[rowIndex]?.[keyIndex];
if (layout.type === "iso") {
if (rowIndex === 2 && keyIndex === 11) {
keycode = "Backslash";
} else if (rowIndex === 3 && keyIndex === 0) {
keycode = "IntlBackslash";
} else if (rowIndex === 3) {
keycode = qwertyKeycodeKeymap[3]?.[keyIndex - 1];
}
}
return keycode;
}

View file

@ -0,0 +1,282 @@
import * as JSONData from "../utils/json-data";
export type Keycode =
| "Backquote"
| "Digit1"
| "Digit2"
| "Digit3"
| "Digit4"
| "Digit5"
| "Digit6"
| "Digit7"
| "Digit8"
| "Digit9"
| "Digit0"
| "Minus"
| "Equal"
| "KeyQ"
| "KeyW"
| "KeyE"
| "KeyR"
| "KeyT"
| "KeyY"
| "KeyU"
| "KeyI"
| "KeyO"
| "KeyP"
| "BracketLeft"
| "BracketRight"
| "Backslash"
| "KeyA"
| "KeyS"
| "KeyD"
| "KeyF"
| "KeyG"
| "KeyH"
| "KeyJ"
| "KeyK"
| "KeyL"
| "Semicolon"
| "Quote"
| "KeyZ"
| "KeyX"
| "KeyC"
| "KeyV"
| "KeyB"
| "KeyN"
| "KeyM"
| "Comma"
| "Period"
| "Slash"
| "Space"
| "ShiftLeft"
| "IntlBackslash"
| "ShiftRight"
| "ArrowUp"
| "ArrowLeft"
| "ArrowDown"
| "ArrowRight"
| "NumpadMultiply"
| "NumpadSubtract"
| "NumpadAdd"
| "NumpadDecimal"
| "NumpadEqual"
| "NumpadDivide"
| "Numpad0"
| "Numpad1"
| "Numpad2"
| "Numpad3"
| "Numpad4"
| "Numpad5"
| "Numpad6"
| "Numpad7"
| "Numpad8"
| "Numpad9"
| "NumpadEnter"
| "Enter"
| "Backspace";
const qwertyKeycodeKeymap: Keycode[][] = [
[
"Backquote",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
"Digit7",
"Digit8",
"Digit9",
"Digit0",
"Minus",
"Equal",
],
[
"KeyQ",
"KeyW",
"KeyE",
"KeyR",
"KeyT",
"KeyY",
"KeyU",
"KeyI",
"KeyO",
"KeyP",
"BracketLeft",
"BracketRight",
"Backslash",
],
[
"KeyA",
"KeyS",
"KeyD",
"KeyF",
"KeyG",
"KeyH",
"KeyJ",
"KeyK",
"KeyL",
"Semicolon",
"Quote",
],
[
"KeyZ",
"KeyX",
"KeyC",
"KeyV",
"KeyB",
"KeyN",
"KeyM",
"Comma",
"Period",
"Slash",
],
["Space"],
];
const leftSideKeys: Keycode[] = [
"Backquote",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
"KeyQ",
"KeyW",
"KeyE",
"KeyR",
"KeyT",
"KeyY",
"KeyA",
"KeyS",
"KeyD",
"KeyF",
"KeyG",
"ShiftLeft",
"IntlBackslash",
"KeyZ",
"KeyX",
"KeyC",
"KeyV",
"KeyB",
"Space",
];
const rightSideKeys: Keycode[] = [
"Digit6",
"Digit7",
"Digit8",
"Digit9",
"Digit0",
"Minus",
"Equal",
"Backspace",
"KeyY",
"KeyU",
"KeyI",
"KeyO",
"KeyP",
"BracketLeft",
"BracketRight",
"Backslash",
"KeyH",
"KeyJ",
"KeyK",
"KeyL",
"Semicolon",
"Quote",
"Enter",
"KeyB",
"KeyN",
"KeyM",
"Comma",
"Period",
"Slash",
"ShiftRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
"NumpadMultiply",
"NumpadSubtract",
"NumpadAdd",
"NumpadDecimal",
"NumpadEqual",
"NumpadDivide",
"Numpad0",
"Numpad1",
"Numpad2",
"Numpad3",
"Numpad4",
"Numpad5",
"Numpad6",
"Numpad7",
"Numpad8",
"Numpad9",
"NumpadEnter",
"Space",
];
/**
* Converts a key to a keycode based on a layout
* @param key Key to convert (e.g., "a")
* @param layout Layout object from our JSON data (e.g., `layouts["qwerty"]`)
* @returns Keycode location of the key (e.g., "KeyA")
*/
export function layoutKeyToKeycode(
key: string,
layout: JSONData.Layout
): Keycode | undefined {
const rows: string[][] = Object.values(layout.keys);
const rowIndex = rows.findIndex((row) => row.find((k) => k.includes(key)));
const row = rows[rowIndex];
if (row === undefined) {
return;
}
const keyIndex = row.findIndex((k) => k.includes(key));
if (keyIndex === -1) {
return;
}
let keycode = qwertyKeycodeKeymap[rowIndex]?.[keyIndex];
if (layout.type === "iso") {
if (rowIndex === 2 && keyIndex === 11) {
keycode = "Backslash";
} else if (rowIndex === 3 && keyIndex === 0) {
keycode = "IntlBackslash";
} else if (rowIndex === 3) {
keycode = qwertyKeycodeKeymap[3]?.[keyIndex - 1];
}
}
return keycode;
}
/**
* Converts a keycode to a keyboard side. Can return true for both sides if the key is in the location KeyY, KeyB or Space.
* @param keycode Keycode to convert (e.g., "KeyA")
* @returns Object with leftSide and rightSide booleans
*/
export function keycodeToKeyboardSide(keycode: Keycode): {
leftSide: boolean;
rightSide: boolean;
} {
const left = leftSideKeys.includes(keycode);
const right = rightSideKeys.includes(keycode);
return { leftSide: left, rightSide: right };
}