Files
SubMiner/src/settings/key-input.ts
T

297 lines
7.8 KiB
TypeScript

import type { Keybinding } from '../types/runtime';
export type KeyInputMode = 'accelerator' | 'dom-code' | 'code' | 'mpv-key';
export interface KeyboardInputLike {
code: string;
key: string;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}
export interface MouseInputLike {
button: number;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}
export interface MpvKeybindingRow {
defaultKey: string;
key: string;
command: (string | number)[] | null;
commandText: string;
isDefault: boolean;
}
const MODIFIER_CODES = new Set([
'AltLeft',
'AltRight',
'ControlLeft',
'ControlRight',
'MetaLeft',
'MetaRight',
'ShiftLeft',
'ShiftRight',
]);
const ELECTRON_KEY_BY_CODE: Record<string, string> = {
Backquote: 'Backquote',
Backslash: 'Backslash',
BracketLeft: 'BracketLeft',
BracketRight: 'BracketRight',
Comma: 'Comma',
Delete: 'Delete',
End: 'End',
Enter: 'Enter',
Equal: 'Plus',
Escape: 'Escape',
Home: 'Home',
Insert: 'Insert',
Minus: 'Minus',
PageDown: 'PageDown',
PageUp: 'PageUp',
Period: 'Period',
Quote: 'Quote',
Semicolon: 'Semicolon',
Slash: 'Slash',
Space: 'Space',
Tab: 'Tab',
};
const MPV_KEY_BY_CODE: Record<string, string> = {
Backspace: 'BS',
Backquote: '`',
Backslash: '\\',
BracketLeft: '[',
BracketRight: ']',
Comma: ',',
Delete: 'DEL',
End: 'END',
Enter: 'ENTER',
Equal: '=',
Escape: 'ESC',
Home: 'HOME',
Insert: 'INS',
Minus: '-',
PageDown: 'PGDWN',
PageUp: 'PGUP',
Period: '.',
Quote: "'",
Semicolon: ';',
Slash: '/',
Space: 'SPACE',
Tab: 'TAB',
};
const MPV_MOUSE_BUTTON_BY_BUTTON: Record<number, string> = {
0: 'MBTN_LEFT',
1: 'MBTN_MID',
2: 'MBTN_RIGHT',
3: 'MBTN_BACK',
4: 'MBTN_FORWARD',
};
function commandEquals(a: Keybinding['command'], b: Keybinding['command']): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
function normalizeUserBindings(userBindings: unknown): Keybinding[] {
if (!Array.isArray(userBindings)) return [];
return userBindings.filter((binding): binding is Keybinding => {
if (!binding || typeof binding !== 'object') return false;
const candidate = binding as Keybinding;
return (
typeof candidate.key === 'string' &&
(candidate.command === null || Array.isArray(candidate.command))
);
});
}
function electronKeyToken(input: KeyboardInputLike): string | null {
if (/^Key[A-Z]$/.test(input.code)) return input.code.slice(3);
if (/^Digit[0-9]$/.test(input.code)) return input.code.slice(5);
if (/^Numpad[0-9]$/.test(input.code)) return `num${input.code.slice(6)}`;
if (/^F\d{1,2}$/.test(input.code)) return input.code;
if (input.code.startsWith('Arrow')) return input.code.replace('Arrow', '');
return ELECTRON_KEY_BY_CODE[input.code] ?? null;
}
function mpvKeyToken(input: KeyboardInputLike): string | null {
if (/^Key[A-Z]$/.test(input.code)) {
return input.key.length === 1 ? input.key : input.code.slice(3).toLowerCase();
}
if (/^Digit[0-9]$/.test(input.code)) return input.code.slice(5);
if (/^Numpad[0-9]$/.test(input.code)) return `KP${input.code.slice(6)}`;
if (/^F\d{1,2}$/.test(input.code)) return input.code;
if (input.code.startsWith('Arrow')) return input.code.replace('Arrow', '').toUpperCase();
return MPV_KEY_BY_CODE[input.code] ?? null;
}
export function keyboardEventToConfigKey(
input: KeyboardInputLike,
mode: KeyInputMode,
): string | null {
if (!input.code || MODIFIER_CODES.has(input.code)) {
return null;
}
if (mode === 'code') {
return input.code;
}
const parts: string[] = [];
if (mode === 'mpv-key') {
if (input.ctrlKey) parts.push('Ctrl');
if (input.altKey) parts.push('Alt');
if (input.shiftKey) parts.push('Shift');
if (input.metaKey) parts.push('Meta');
const key = mpvKeyToken(input);
return key ? [...parts, key].join('+') : null;
}
if (mode === 'accelerator') {
if (input.ctrlKey || input.metaKey) parts.push('CommandOrControl');
if (input.altKey) parts.push('Alt');
if (input.shiftKey) parts.push('Shift');
const key = electronKeyToken(input);
return key ? [...parts, key].join('+') : null;
}
if (input.ctrlKey) parts.push('Ctrl');
if (input.altKey) parts.push('Alt');
if (input.shiftKey) parts.push('Shift');
if (input.metaKey) parts.push('Meta');
return [...parts, input.code].join('+');
}
export function mouseEventToConfigKey(input: MouseInputLike, mode: KeyInputMode): string | null {
if (mode !== 'dom-code') {
return null;
}
const key = MPV_MOUSE_BUTTON_BY_BUTTON[input.button];
if (!key) {
return null;
}
const parts: string[] = [];
if (input.ctrlKey) parts.push('Ctrl');
if (input.altKey) parts.push('Alt');
if (input.shiftKey) parts.push('Shift');
if (input.metaKey) parts.push('Meta');
return [...parts, key].join('+');
}
export function createMpvKeybindingRows(
defaultBindings: Keybinding[],
userBindings: unknown,
): MpvKeybindingRow[] {
const normalizedUserBindings = normalizeUserBindings(userBindings);
const userByKey = new Map(normalizedUserBindings.map((binding) => [binding.key, binding]));
const consumedUserKeys = new Set<string>();
const rows = defaultBindings.map((binding) => {
const override = userByKey.get(binding.key);
if (override?.command === null) {
const movedOverride = normalizedUserBindings.find(
(candidate) =>
candidate.key !== binding.key && commandEquals(candidate.command, binding.command),
);
if (movedOverride) {
consumedUserKeys.add(binding.key);
consumedUserKeys.add(movedOverride.key);
return {
defaultKey: binding.key,
key: movedOverride.key,
command: movedOverride.command,
commandText: JSON.stringify(movedOverride.command ?? null),
isDefault: true,
};
}
}
if (override) {
consumedUserKeys.add(binding.key);
}
const command = override?.command ?? binding.command;
return {
defaultKey: binding.key,
key: binding.key,
command,
commandText: JSON.stringify(command ?? null),
isDefault: true,
};
});
for (const binding of normalizedUserBindings) {
if (consumedUserKeys.has(binding.key)) {
continue;
}
if (defaultBindings.some((defaultBinding) => defaultBinding.key === binding.key)) {
continue;
}
rows.push({
defaultKey: binding.key,
key: binding.key,
command: binding.command,
commandText: JSON.stringify(binding.command ?? null),
isDefault: false,
});
}
return rows;
}
export function parseMpvCommandText(value: string): Keybinding['command'] | undefined {
try {
const parsed = JSON.parse(value);
if (parsed === null) return null;
if (
Array.isArray(parsed) &&
parsed.every((entry) => typeof entry === 'string' || typeof entry === 'number')
) {
return parsed;
}
} catch {
return undefined;
}
return undefined;
}
export function buildMpvKeybindingConfigValue(
defaultBindings: Keybinding[],
rows: MpvKeybindingRow[],
): Keybinding[] {
const next: Keybinding[] = [];
for (const defaultBinding of defaultBindings) {
const row = rows.find((candidate) => candidate.defaultKey === defaultBinding.key);
if (!row) {
next.push({ key: defaultBinding.key, command: null });
continue;
}
if (row.key !== defaultBinding.key) {
next.push({ key: defaultBinding.key, command: null });
if (row.command !== null) {
next.push({ key: row.key, command: row.command });
}
continue;
}
if (!commandEquals(row.command, defaultBinding.command)) {
next.push({ key: row.key, command: row.command });
}
}
for (const row of rows.filter((candidate) => !candidate.isDefault)) {
next.push({ key: row.key, command: row.command });
}
return next;
}