| // Controls use physical KeyboardEvent.code positions; getLayoutMap() provides | |
| // the characters printed on those keys for the active layout. | |
| const MOVEMENT_CODES = ["KeyW", "KeyA", "KeyS", "KeyD"]; | |
| const MOVEMENT_FALLBACK_HINT = "arrows or WASD"; | |
| function keycap(value) { | |
| if (typeof value !== "string") return null; | |
| const glyph = value.trim(); | |
| if ([...glyph].length !== 1 || /[\p{M}\p{C}\p{Z}]/u.test(glyph)) return null; | |
| const upper = glyph.toUpperCase(); | |
| return [...upper].length === 1 ? upper : glyph; // "ß" must not become "SS" | |
| } | |
| export function resolveKeycaps(codes, layoutMap) { | |
| const required = [...new Set([...MOVEMENT_CODES, ...codes])]; | |
| const labels = {}; | |
| let complete = true; | |
| for (const code of required) { | |
| let label = null; | |
| try { | |
| label = keycap(layoutMap?.get?.(code)); | |
| } catch { | |
| label = null; | |
| } | |
| if (!label) complete = false; | |
| labels[code] = label; | |
| } | |
| if (!complete) { | |
| for (const code of required) labels[code] = code.replace(/^Key/, ""); | |
| return { labels, moveHint: MOVEMENT_FALLBACK_HINT }; | |
| } | |
| return { | |
| labels, | |
| moveHint: `arrows or ${MOVEMENT_CODES.map((code) => labels[code]).join("")}`, | |
| }; | |
| } | |
| export async function readLayoutMap(nav = globalThis.navigator) { | |
| try { | |
| return (await nav?.keyboard?.getLayoutMap?.()) ?? null; | |
| } catch { | |
| return null; | |
| } | |
| } | |