All files / virtual-keyboard/src/engine randomizeLayout.ts

100% Statements 20/20
100% Branches 3/3
100% Functions 6/6
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73                  115x                                                                     11x 11x 11x 39x 115x 94x 94x           11x 11x 84x 84x 84x 84x       39x 11x 94x     11x    
import { resolveKey } from "./resolveLayout";
import type { KeyboardLayout, KeyDefinition } from "../types";
 
/**
 * Whether a key is a character key (the only kind whose position is shuffled).
 * Uses the same type inference as the resolver so untyped keys are classified
 * consistently.
 */
function isCharKey(key: KeyDefinition): boolean {
  return (
    resolveKey(key, { shift: false, capsLock: false, layer: false }).type ===
    "char"
  );
}
 
/**
 * Return a **new** layout with the positions of its `type: "char"` keys shuffled
 * (Fisher–Yates), leaving every action / modifier / spacer key exactly where it
 * was. Pure and seedable — pass a custom `rng` (returning `[0, 1)`) for
 * deterministic output, e.g. in tests or for a reproducible per-session pad.
 *
 * This backs the `randomize` prop for secure PIN pads: shuffling key positions
 * each mount frustrates positional shoulder-surfing / keyloggers (SPEC §10). The
 * emitted values are unchanged — only where each key sits moves.
 *
 * @param layout - The layout to shuffle (not mutated).
 * @param rng - A random source returning a float in `[0, 1)` (default `Math.random`).
 * @returns A new {@link KeyboardLayout} with char-key positions permuted.
 *
 * @example
 * ```ts
 * // Deterministic shuffle with a seeded RNG. Divide by 0x80000000 (2^31) so the
 * // value stays in [0, 1) — dividing by its own max could yield exactly 1.0,
 * // which would index out of bounds in the Fisher–Yates swap.
 * let seed = 42;
 * const rng = () => ((seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x80000000);
 * const shuffled = randomizeLayout(numericLayout, rng);
 * ```
 */
export function randomizeLayout(
  layout: KeyboardLayout,
  rng: () => number = Math.random
): KeyboardLayout {
  // Collect the char keys in reading order along with their (row, col) slots.
  const slots: Array<[number, number]> = [];
  const charKeys: KeyDefinition[] = [];
  layout.rows.forEach((row, r) => {
    row.forEach((key, c) => {
      if (isCharKey(key)) {
        slots.push([r, c]);
        charKeys.push(key);
      }
    });
  });
 
  // Fisher–Yates shuffle of the char keys.
  const shuffled = charKeys.slice();
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    const tmp = shuffled[i];
    shuffled[i] = shuffled[j];
    shuffled[j] = tmp;
  }
 
  // Rebuild rows, dropping the shuffled char keys back into the char slots.
  const rows = layout.rows.map((row) => row.slice());
  slots.forEach(([r, c], index) => {
    rows[r][c] = shuffled[index];
  });
 
  return { ...layout, rows };
}