diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index be356add63e9..e9bd04545b14 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -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) => { diff --git a/frontend/src/ts/test/layout-emulator.ts b/frontend/src/ts/test/layout-emulator.ts index caa50048652d..ec6bc69841af 100644 --- a/frontend/src/ts/test/layout-emulator.ts +++ b/frontend/src/ts/test/layout-emulator.ts @@ -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 { function emulatedLayoutGetVariant( - event: JQuery.KeyDownEvent, + event: JQuery.KeyDownEvent | JQuery.KeyUpEvent, keyVariants: string ): string | undefined { let isCapitalized = event.shiftKey; diff --git a/frontend/src/ts/test/monkey.ts b/frontend/src/ts/test/monkey.ts index 1873c4c5666f..63597d3b6109 100644 --- a/frontend/src/ts/test/monkey.ts +++ b/frontend/src/ts/test/monkey.ts @@ -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(); } diff --git a/frontend/src/ts/test/shift-tracker.ts b/frontend/src/ts/test/shift-tracker.ts index 802100a6a6bd..a73bce199f5f 100644 --- a/frontend/src/ts/test/shift-tracker.ts +++ b/frontend/src/ts/test/shift-tracker.ts @@ -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 { (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; -} diff --git a/frontend/src/ts/utils/key-converter.ts b/frontend/src/ts/utils/key-converter.ts new file mode 100644 index 000000000000..3491f2158df2 --- /dev/null +++ b/frontend/src/ts/utils/key-converter.ts @@ -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 }; +}