import type { PreviewAutomationPressInput } from "@t3tools/contracts"; interface KeyDefinition { readonly code: string; readonly key: string; readonly keyCode: number; readonly text?: string; readonly location?: number; readonly shiftedKey?: string;} export interface PreviewAutomationKeyEvent { readonly [key: string]: unknown; readonly type: "keyDown" | "rawKeyDown" | "keyUp"; readonly key: string; readonly code: string; readonly modifiers: number; readonly windowsVirtualKeyCode: number; readonly location: number; readonly isKeypad: boolean; readonly text?: string; readonly unmodifiedText?: string; readonly commands?: ReadonlyArray<string>;} export interface PreviewAutomationKeySequence { readonly keyDown: PreviewAutomationKeyEvent; readonly keyUp: PreviewAutomationKeyEvent; readonly signal: { readonly kind: "key"; readonly key: string; readonly code: string; };} const NAMED_KEYS: Readonly<Record<string, KeyDefinition>> = { Escape: { code: "Escape", key: "Escape", keyCode: 27 }, Backspace: { code: "Backspace", key: "Backspace", keyCode: 8 }, Tab: { code: "Tab", key: "Tab", keyCode: 9 }, Enter: { code: "Enter", key: "Enter", keyCode: 13, text: "\r" }, Shift: { code: "ShiftLeft", key: "Shift", keyCode: 16, location: 1 }, Control: { code: "ControlLeft", key: "Control", keyCode: 17, location: 1 }, Alt: { code: "AltLeft", key: "Alt", keyCode: 18, location: 1 }, Meta: { code: "MetaLeft", key: "Meta", keyCode: 91, location: 1 }, CapsLock: { code: "CapsLock", key: "CapsLock", keyCode: 20 }, Space: { code: "Space", key: " ", keyCode: 32, text: " " }, PageUp: { code: "PageUp", key: "PageUp", keyCode: 33 }, PageDown: { code: "PageDown", key: "PageDown", keyCode: 34 }, End: { code: "End", key: "End", keyCode: 35 }, Home: { code: "Home", key: "Home", keyCode: 36 }, ArrowLeft: { code: "ArrowLeft", key: "ArrowLeft", keyCode: 37 }, ArrowUp: { code: "ArrowUp", key: "ArrowUp", keyCode: 38 }, ArrowRight: { code: "ArrowRight", key: "ArrowRight", keyCode: 39 }, ArrowDown: { code: "ArrowDown", key: "ArrowDown", keyCode: 40 }, Insert: { code: "Insert", key: "Insert", keyCode: 45 }, Delete: { code: "Delete", key: "Delete", keyCode: 46 },}; const PRINTABLE_KEYS: ReadonlyArray<KeyDefinition> = [ { code: "Backquote", key: "`", shiftedKey: "~", keyCode: 192 }, { code: "Digit1", key: "1", shiftedKey: "!", keyCode: 49 }, { code: "Digit2", key: "2", shiftedKey: "@", keyCode: 50 }, { code: "Digit3", key: "3", shiftedKey: "#", keyCode: 51 }, { code: "Digit4", key: "4", shiftedKey: "$", keyCode: 52 }, { code: "Digit5", key: "5", shiftedKey: "%", keyCode: 53 }, { code: "Digit6", key: "6", shiftedKey: "^", keyCode: 54 }, { code: "Digit7", key: "7", shiftedKey: "&", keyCode: 55 }, { code: "Digit8", key: "8", shiftedKey: "*", keyCode: 56 }, { code: "Digit9", key: "9", shiftedKey: "(", keyCode: 57 }, { code: "Digit0", key: "0", shiftedKey: ")", keyCode: 48 }, { code: "Minus", key: "-", shiftedKey: "_", keyCode: 189 }, { code: "Equal", key: "=", shiftedKey: "+", keyCode: 187 }, { code: "Backslash", key: "\\", shiftedKey: "|", keyCode: 220 }, { code: "BracketLeft", key: "[", shiftedKey: "{", keyCode: 219 }, { code: "BracketRight", key: "]", shiftedKey: "}", keyCode: 221 }, { code: "Semicolon", key: ";", shiftedKey: ":", keyCode: 186 }, { code: "Quote", key: "'", shiftedKey: '"', keyCode: 222 }, { code: "Comma", key: ",", shiftedKey: "<", keyCode: 188 }, { code: "Period", key: ".", shiftedKey: ">", keyCode: 190 }, { code: "Slash", key: "/", shiftedKey: "?", keyCode: 191 },]; /** * Chromium does not infer macOS editing commands from synthetic Meta chords. * Keep the common browser editing/navigation shortcuts explicit so dispatched * key events behave like their physical-key equivalents. */const MAC_EDITING_COMMANDS: Readonly<Record<string, string>> = { "Meta+Backspace": "deleteToBeginningOfLine", "Meta+ArrowUp": "moveToBeginningOfDocument", "Meta+ArrowDown": "moveToEndOfDocument", "Meta+ArrowLeft": "moveToLeftEndOfLine", "Meta+ArrowRight": "moveToRightEndOfLine", "Shift+Meta+ArrowUp": "moveToBeginningOfDocumentAndModifySelection", "Shift+Meta+ArrowDown": "moveToEndOfDocumentAndModifySelection", "Shift+Meta+ArrowLeft": "moveToLeftEndOfLineAndModifySelection", "Shift+Meta+ArrowRight": "moveToRightEndOfLineAndModifySelection", "Meta+KeyA": "selectAll", "Meta+KeyC": "copy", "Meta+KeyX": "cut", "Meta+KeyV": "paste", "Meta+KeyZ": "undo", "Shift+Meta+KeyZ": "redo",};const SHORTCUT_MODIFIER_ORDER = ["Shift", "Control", "Alt", "Meta"] as const; const macEditingCommands = ( code: string, modifiers: PreviewAutomationPressInput["modifiers"],): ReadonlyArray<string> => { const shortcut = [ ...SHORTCUT_MODIFIER_ORDER.filter((modifier) => modifiers?.includes(modifier)), code, ].join("+"); const command = MAC_EDITING_COMMANDS[shortcut]; return command ? [command] : [];}; const modifierMask = (modifiers: PreviewAutomationPressInput["modifiers"]): number => (modifiers ?? []).reduce((value, modifier) => { switch (modifier) { case "Alt": return value | 1; case "Control": return value | 2; case "Meta": return value | 4; case "Shift": return value | 8; } }, 0); function resolveKeyDefinition(input: PreviewAutomationPressInput): KeyDefinition { const named = NAMED_KEYS[input.key]; if (named) return named; const functionKey = /^F([1-9]|1[0-2])$/.exec(input.key); if (functionKey) { const number = Number(functionKey[1]); return { code: input.key, key: input.key, keyCode: 111 + number }; } if (/^[a-z]$/i.test(input.key)) { const upper = input.key.toUpperCase(); const shifted = input.modifiers?.includes("Shift") ?? false; const key = shifted || input.key === upper ? upper : input.key; return { code: `Key${upper}`, key, keyCode: upper.charCodeAt(0), text: key }; } const printable = PRINTABLE_KEYS.find( (definition) => definition.key === input.key || definition.shiftedKey === input.key, ); if (printable) { const shifted = input.modifiers?.includes("Shift") ?? false; const key = printable.shiftedKey && (shifted || input.key === printable.shiftedKey) ? printable.shiftedKey : printable.key; return { ...printable, key, text: key }; } return { code: input.key.length > 1 ? input.key : "", key: input.key, keyCode: 0, ...(input.key.length === 1 ? { text: input.key } : {}), };} /** * Build Chromium CDP key packets using the same required fields and down-event * choice as Playwright's pinned Chromium keyboard implementation. */export function makePreviewAutomationKeySequence( input: PreviewAutomationPressInput, options?: { readonly isMac?: boolean },): PreviewAutomationKeySequence { const definition = resolveKeyDefinition(input); const modifiers = modifierMask(input.modifiers); const suppressText = input.modifiers?.some((modifier) => modifier !== "Shift") ?? false; const text = suppressText ? "" : (definition.text ?? ""); const location = definition.location ?? 0; const commands = options?.isMac ? macEditingCommands(definition.code, input.modifiers) : []; const shared = { key: definition.key, code: definition.code, modifiers, windowsVirtualKeyCode: definition.keyCode, location, isKeypad: location === 3, }; return { keyDown: { type: text ? "keyDown" : "rawKeyDown", ...shared, ...(text ? { text, unmodifiedText: text } : {}), ...(commands.length > 0 ? { commands } : {}), }, keyUp: { type: "keyUp", ...shared }, signal: { kind: "key", key: definition.key, code: definition.code }, };}
import type { PreviewAutomationPressInput } from "@t3tools/contracts";
interface KeyDefinition {
readonly code: string;
readonly key: string;
readonly keyCode: number;
readonly text?: string;
readonly location?: number;
readonly shiftedKey?: string;
}
export interface PreviewAutomationKeyEvent {
readonly [key: string]: unknown;
readonly type: "keyDown" | "rawKeyDown" | "keyUp";
readonly key: string;
readonly code: string;
readonly modifiers: number;
readonly windowsVirtualKeyCode: number;
readonly location: number;
readonly isKeypad: boolean;
readonly text?: string;
readonly unmodifiedText?: string;
readonly commands?: ReadonlyArray<string>;
}
export interface PreviewAutomationKeySequence {
readonly keyDown: PreviewAutomationKeyEvent;
readonly keyUp: PreviewAutomationKeyEvent;
readonly signal: {
readonly kind: "key";
readonly key: string;
readonly code: string;
};
}
const NAMED_KEYS: Readonly<Record<string, KeyDefinition>> = {
Escape: { code: "Escape", key: "Escape", keyCode: 27 },
Backspace: { code: "Backspace", key: "Backspace", keyCode: 8 },
Tab: { code: "Tab", key: "Tab", keyCode: 9 },
Enter: { code: "Enter", key: "Enter", keyCode: 13, text: "\r" },
Shift: { code: "ShiftLeft", key: "Shift", keyCode: 16, location: 1 },
Control: { code: "ControlLeft", key: "Control", keyCode: 17, location: 1 },
Alt: { code: "AltLeft", key: "Alt", keyCode: 18, location: 1 },
Meta: { code: "MetaLeft", key: "Meta", keyCode: 91, location: 1 },
CapsLock: { code: "CapsLock", key: "CapsLock", keyCode: 20 },
Space: { code: "Space", key: " ", keyCode: 32, text: " " },
PageUp: { code: "PageUp", key: "PageUp", keyCode: 33 },
PageDown: { code: "PageDown", key: "PageDown", keyCode: 34 },
End: { code: "End", key: "End", keyCode: 35 },
Home: { code: "Home", key: "Home", keyCode: 36 },
ArrowLeft: { code: "ArrowLeft", key: "ArrowLeft", keyCode: 37 },
ArrowUp: { code: "ArrowUp", key: "ArrowUp", keyCode: 38 },
ArrowRight: { code: "ArrowRight", key: "ArrowRight", keyCode: 39 },
ArrowDown: { code: "ArrowDown", key: "ArrowDown", keyCode: 40 },
Insert: { code: "Insert", key: "Insert", keyCode: 45 },
Delete: { code: "Delete", key: "Delete", keyCode: 46 },
};
const PRINTABLE_KEYS: ReadonlyArray<KeyDefinition> = [
{ code: "Backquote", key: "`", shiftedKey: "~", keyCode: 192 },
{ code: "Digit1", key: "1", shiftedKey: "!", keyCode: 49 },
{ code: "Digit2", key: "2", shiftedKey: "@", keyCode: 50 },
{ code: "Digit3", key: "3", shiftedKey: "#", keyCode: 51 },
{ code: "Digit4", key: "4", shiftedKey: "$", keyCode: 52 },
{ code: "Digit5", key: "5", shiftedKey: "%", keyCode: 53 },
{ code: "Digit6", key: "6", shiftedKey: "^", keyCode: 54 },
{ code: "Digit7", key: "7", shiftedKey: "&", keyCode: 55 },
{ code: "Digit8", key: "8", shiftedKey: "*", keyCode: 56 },
{ code: "Digit9", key: "9", shiftedKey: "(", keyCode: 57 },
{ code: "Digit0", key: "0", shiftedKey: ")", keyCode: 48 },
{ code: "Minus", key: "-", shiftedKey: "_", keyCode: 189 },
{ code: "Equal", key: "=", shiftedKey: "+", keyCode: 187 },
{ code: "Backslash", key: "\\", shiftedKey: "|", keyCode: 220 },
{ code: "BracketLeft", key: "[", shiftedKey: "{", keyCode: 219 },
{ code: "BracketRight", key: "]", shiftedKey: "}", keyCode: 221 },
{ code: "Semicolon", key: ";", shiftedKey: ":", keyCode: 186 },
{ code: "Quote", key: "'", shiftedKey: '"', keyCode: 222 },
{ code: "Comma", key: ",", shiftedKey: "<", keyCode: 188 },
{ code: "Period", key: ".", shiftedKey: ">", keyCode: 190 },
{ code: "Slash", key: "/", shiftedKey: "?", keyCode: 191 },
];
/**
* Chromium does not infer macOS editing commands from synthetic Meta chords.
* Keep the common browser editing/navigation shortcuts explicit so dispatched
* key events behave like their physical-key equivalents.
*/
const MAC_EDITING_COMMANDS: Readonly<Record<string, string>> = {
"Meta+Backspace": "deleteToBeginningOfLine",
"Meta+ArrowUp": "moveToBeginningOfDocument",
"Meta+ArrowDown": "moveToEndOfDocument",
"Meta+ArrowLeft": "moveToLeftEndOfLine",
"Meta+ArrowRight": "moveToRightEndOfLine",
"Shift+Meta+ArrowUp": "moveToBeginningOfDocumentAndModifySelection",
"Shift+Meta+ArrowDown": "moveToEndOfDocumentAndModifySelection",
"Shift+Meta+ArrowLeft": "moveToLeftEndOfLineAndModifySelection",
"Shift+Meta+ArrowRight": "moveToRightEndOfLineAndModifySelection",
"Meta+KeyA": "selectAll",
"Meta+KeyC": "copy",
"Meta+KeyX": "cut",
"Meta+KeyV": "paste",
"Meta+KeyZ": "undo",
"Shift+Meta+KeyZ": "redo",
};
const SHORTCUT_MODIFIER_ORDER = ["Shift", "Control", "Alt", "Meta"] as const;
const macEditingCommands = (
code: string,
modifiers: PreviewAutomationPressInput["modifiers"],
): ReadonlyArray<string> => {
const shortcut = [
...SHORTCUT_MODIFIER_ORDER.filter((modifier) => modifiers?.includes(modifier)),
code,
].join("+");
const command = MAC_EDITING_COMMANDS[shortcut];
return command ? [command] : [];
};
const modifierMask = (modifiers: PreviewAutomationPressInput["modifiers"]): number =>
(modifiers ?? []).reduce((value, modifier) => {
switch (modifier) {
case "Alt":
return value | 1;
case "Control":
return value | 2;
case "Meta":
return value | 4;
case "Shift":
return value | 8;
}
}, 0);
function resolveKeyDefinition(input: PreviewAutomationPressInput): KeyDefinition {
const named = NAMED_KEYS[input.key];
if (named) return named;
const functionKey = /^F([1-9]|1[0-2])$/.exec(input.key);
if (functionKey) {
const number = Number(functionKey[1]);
return { code: input.key, key: input.key, keyCode: 111 + number };
}
if (/^[a-z]$/i.test(input.key)) {
const upper = input.key.toUpperCase();
const shifted = input.modifiers?.includes("Shift") ?? false;
const key = shifted || input.key === upper ? upper : input.key;
return { code: `Key${upper}`, key, keyCode: upper.charCodeAt(0), text: key };
}
const printable = PRINTABLE_KEYS.find(
(definition) => definition.key === input.key || definition.shiftedKey === input.key,
);
if (printable) {
const shifted = input.modifiers?.includes("Shift") ?? false;
const key =
printable.shiftedKey && (shifted || input.key === printable.shiftedKey)
? printable.shiftedKey
: printable.key;
return { ...printable, key, text: key };
}
return {
code: input.key.length > 1 ? input.key : "",
key: input.key,
keyCode: 0,
...(input.key.length === 1 ? { text: input.key } : {}),
};
}
/**
* Build Chromium CDP key packets using the same required fields and down-event
* choice as Playwright's pinned Chromium keyboard implementation.
*/
export function makePreviewAutomationKeySequence(
input: PreviewAutomationPressInput,
options?: { readonly isMac?: boolean },
): PreviewAutomationKeySequence {
const definition = resolveKeyDefinition(input);
const modifiers = modifierMask(input.modifiers);
const suppressText = input.modifiers?.some((modifier) => modifier !== "Shift") ?? false;
const text = suppressText ? "" : (definition.text ?? "");
const location = definition.location ?? 0;
const commands = options?.isMac ? macEditingCommands(definition.code, input.modifiers) : [];
const shared = {
key: definition.key,
code: definition.code,
modifiers,
windowsVirtualKeyCode: definition.keyCode,
location,
isKeypad: location === 3,
};
return {
keyDown: {
type: text ? "keyDown" : "rawKeyDown",
...shared,
...(text ? { text, unmodifiedText: text } : {}),
...(commands.length > 0 ? { commands } : {}),
},
keyUp: { type: "keyUp", ...shared },
signal: { kind: "key", key: definition.key, code: definition.code },
};
}