268 lines
7.0 KiB
TypeScript
268 lines
7.0 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 ][ / ][+
|
|
*/
|
|
// prettier-ignore
|
|
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
|
|
*/
|
|
// prettier-ignore
|
|
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;
|
|
};
|