import { JSX } from 'preact'; import { DeepMemberOf, KnownKeys } from '../../types'; export const SPECIAL_KEY_MAP = { 'Shift': 'SHIFT', 'Enter': 'RETURN', 'CapsLock': 'LOCK', 'Control': 'CTRL', 'Escape': 'ESC', 'Delete': 'RESET', 'Tab': 'TAB', 'Backspace': 'DELETE', 'ArrowUp': '↑', 'ArrowDown': '↓', 'ArrowRight': '→', 'ArrowLeft': '←', // UiKit symbols 'UIKeyInputLeftArrow': '←', 'UIKeyInputRightArrow': '→', 'UIKeyInputUpArrow': '↑', 'UIKeyInputDownArrow': '↓', 'UIKeyInputEscape': 'ESC', } as const; export const isSpecialKey = (k: string): k is KnownKeys => { return k in SPECIAL_KEY_MAP; }; export const SPECIAL_KEY_CODE = { 'TAB': 9, 'RETURN': 13, 'ESC': 27, '↑': 11, '↓': 10, '→': 21, '←': 8, 'DELETE': 127, } as const; export const hasSpecialKeyCode = (k: string): k is KnownKeys => { return k in SPECIAL_KEY_CODE; }; /** * Keyboard layout for the Apple ][ / ][+ */ export const keys2 = [ [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'RESET'], ['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'REPT', 'RETURN'], ['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '←', '→'], ['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'], ['POWER', ' '] ], [ ['!', '"', '#', '$', '%', '&', '\'', '(', ')', '0', '*', '=', 'RESET'], ['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', '@', 'REPT', 'RETURN'], ['CTRL', 'A', 'S', 'D', 'F', 'BELL', 'H', 'J', 'K', 'L', '+', '←', '→'], ['SHIFT', 'Z', 'X', 'C', 'V', 'B', '^', ']', '<', '>', '?', 'SHIFT'], ['POWER', ' '] ] ] as const; export type Key2 = DeepMemberOf; /** * Keyboard layout for the Apple //e */ export const keys2e = [ [ ['ESC', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'DELETE'], ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'], ['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '"', 'RETURN'], ['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'], ['LOCK', '`', 'POW', 'OPEN_APPLE', ' ', 'CLOSED_APPLE', '←', '→', '↓', '↑'] ], [ ['ESC', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'DELETE'], ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'], ['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\'', 'RETURN'], ['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'SHIFT'], ['CAPS', '~', 'POW', 'OPEN_APPLE', ' ', 'CLOSED_APPLE', '←', '→', '↓', '↑'] ] ] as const; /** Shifted */ const SHIFTED = { '!' : '1' , '@' : '2' , '#' : '3' , '$' : '4' , '%' : '5' , '^' : '6' , '&' : '7' , '*' : '8' , '(' : '9' , ')' : '0' , '_' : '-' , '+' : '=' , '{' : '[' , '}' : ']' , '|' : '\\', ':' : ';' , '\'' : '"', '<' : ',' , '>' : '.' , '?' : '/' , '~' : '`' , } as const; export const isShiftyKey = (k: string): k is KnownKeys => { return k in SHIFTED; }; export type Key2e = DeepMemberOf; export type Key = Key2 | Key2e; export type KeyFunction = (key: KeyboardEvent) => void; /** * Convert a DOM keyboard event into an ASCII equivalent that * an Apple II can recognize. * * @param evt Event to convert * @param caps Caps Lock state * @returns a tuple of: * * `key`: the symbol of the key * * `keyLabel`: the label on the keycap * * `keyCode`: the corresponding byte for the Apple II */ export const mapKeyboardEvent = (event: KeyboardEvent, caps: boolean = false, control: boolean = false) => { let key: string; if (isSpecialKey(event.key)) { key = SPECIAL_KEY_MAP[event.key]; } else if (event.key === 'Alt') { key = event.location === 1 ? 'OPEN_APPLE' : 'CLOSED_APPLE'; } else { key = event.key; } let keyLabel = key; if (key.length === 1) { if (isShiftyKey(key)) { keyLabel = SHIFTED[key]; } else { keyLabel = key.toUpperCase(); } } let keyCode = 0xff; if (hasSpecialKeyCode(key)) { keyCode = SPECIAL_KEY_CODE[key]; } else if (key.length === 1) { keyCode = key.charCodeAt(0); } if ((caps || control) && keyCode >= 0x61 && keyCode <= 0x7A) { keyCode -= 0x20; } if (control && keyCode >= 0x40 && keyCode < 0x60) { keyCode -= 0x40; } return { key, keyLabel, keyCode}; }; /** * Convert a mouse event into an ASCII character based on the * data attached to the target DOM element. * * @param event Event to convert * @param shifted Shift key state * @param controlled Control key state * @param caps Caps Lock state * @param e //e status * @returns ASCII character */ export const mapMouseEvent = ( event: JSX.TargetedMouseEvent, shifted: boolean, controlled: boolean, caps: boolean, e: boolean, ) => { const keyLabel = event.currentTarget?.dataset.key2 ?? ''; let key = event.currentTarget?.dataset[shifted ? 'key2' : 'key1'] ?? ''; let keyCode = 0xff; switch (key) { case 'BELL': key = 'G'; break; case 'RETURN': key = '\r'; break; case 'TAB': key = '\t'; break; case 'DELETE': key = '\x7F'; break; case '←': key = '\x08'; break; case '→': key = '\x15'; break; case '↓': key = '\x0A'; break; case '↑': key = '\x0B'; break; case ' ': key = ' '; break; case 'ESC': key = '\x1B'; break; default: break; } if (key.length === 1) { if (controlled && key >= '@' && key <= '_') { keyCode = key.charCodeAt(0) - 0x40; } else if ( e && !shifted && !caps && key >= 'A' && key <= 'Z' ) { keyCode = key.charCodeAt(0) + 0x20; } else { keyCode = key.charCodeAt(0); } } return { keyCode, key, keyLabel }; }; /** * Remap keys so that upper and lower are a tuple per row instead of * separate rows * * @param inKeys keys2 or keys2e * @returns Keys remapped */ export const keysAsTuples = (inKeys: typeof keys2e | typeof keys2): string[][][] => { const rows = []; for (let idx = 0; idx < inKeys[0].length; idx++) { const upper = inKeys[0][idx]; const lower = inKeys[1][idx]; const keys = []; for (let jdx = 0; jdx < upper.length; jdx++) { keys.push([upper[jdx], lower[jdx]]); } rows.push(keys); } return rows; };