432 lines
8.9 KiB
TypeScript
432 lines
8.9 KiB
TypeScript
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<typeof SPECIAL_KEY_MAP> => {
|
|
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<typeof SPECIAL_KEY_CODE> => {
|
|
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<typeof keys2>;
|
|
|
|
/**
|
|
* 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<typeof SHIFTED> => {
|
|
return k in SHIFTED;
|
|
};
|
|
|
|
export type Key2e = DeepMemberOf<typeof keys2e>;
|
|
|
|
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<HTMLElement>,
|
|
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;
|
|
};
|