2022-05-10 06:52:06 -07:00
|
|
|
import { JSX } from 'preact';
|
2022-08-25 03:23:22 +02:00
|
|
|
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',
|
2022-05-10 06:52:06 -07:00
|
|
|
} as const;
|
|
|
|
|
2022-08-25 03:23:22 +02:00
|
|
|
export const isSpecialKey = (k: string): k is KnownKeys<typeof SPECIAL_KEY_MAP> => {
|
|
|
|
return k in SPECIAL_KEY_MAP;
|
2022-05-10 06:52:06 -07:00
|
|
|
};
|
|
|
|
|
2022-08-25 03:23:22 +02:00
|
|
|
export const SPECIAL_KEY_CODE = {
|
|
|
|
'TAB': 9,
|
|
|
|
'RETURN': 13,
|
|
|
|
'ESC': 27,
|
|
|
|
'↑': 11,
|
|
|
|
'↓': 10,
|
|
|
|
'→': 21,
|
|
|
|
'←': 8,
|
|
|
|
'DELETE': 127,
|
2022-05-10 06:52:06 -07:00
|
|
|
} as const;
|
|
|
|
|
2022-08-25 03:23:22 +02:00
|
|
|
export const hasSpecialKeyCode = (k: string): k is KnownKeys<typeof SPECIAL_KEY_CODE> => {
|
|
|
|
return k in SPECIAL_KEY_CODE;
|
2022-05-10 06:52:06 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2022-08-25 03:23:22 +02:00
|
|
|
/** 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;
|
|
|
|
};
|
|
|
|
|
2022-05-10 06:52:06 -07:00
|
|
|
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
|
2022-08-25 03:23:22 +02:00
|
|
|
* an Apple II can recognize.
|
2022-05-10 06:52:06 -07:00
|
|
|
*
|
|
|
|
* @param evt Event to convert
|
|
|
|
* @param caps Caps Lock state
|
2022-08-25 03:23:22 +02:00
|
|
|
* @returns a tuple of:
|
|
|
|
* * `key`: the symbol of the key
|
|
|
|
* * `keyLabel`: the label on the keycap
|
|
|
|
* * `keyCode`: the corresponding byte for the Apple II
|
2022-05-10 06:52:06 -07:00
|
|
|
*/
|
2022-08-25 03:23:22 +02:00
|
|
|
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';
|
2022-05-10 06:52:06 -07:00
|
|
|
} else {
|
2022-08-25 03:23:22 +02:00
|
|
|
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;
|
2022-05-10 06:52:06 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 03:23:22 +02:00
|
|
|
return { key, keyLabel, keyCode};
|
2022-05-10 06:52:06 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|