client/packages/hotkeys/src/registry.ts
Alois 85633a1c81
Some checks failed
/ build-web (push) Successful in 7m53s
/ build-desktop (linux) (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(feat): add hotkeys
2026-08-02 01:10:05 +02:00

94 lines
2.5 KiB
TypeScript

import { useSyncExternalStore } from "react";
import { validateHotkey, type Hotkey } from "@tanstack/react-hotkeys";
export type HotkeyDefinition = Readonly<{
id: string;
name: string;
description?: string;
category: string;
defaultBinding: Hotkey;
global?: boolean;
}>;
const definitions = new Map<string, HotkeyDefinition>();
const listeners = new Set<() => void>();
let snapshot: HotkeyDefinition[] = [];
export function defineHotkey(definition: HotkeyDefinition) {
const existing = definitions.get(definition.id);
if (existing) return existing;
definitions.set(definition.id, Object.freeze({ ...definition }));
snapshot = [...definitions.values()];
listeners.forEach((listener) => listener());
return definitions.get(definition.id)!;
}
export function getHotkeyDefinitions() {
return snapshot;
}
export function useHotkeyDefinitions() {
return useSyncExternalStore(
(listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
getHotkeyDefinitions,
getHotkeyDefinitions,
);
}
export function normalizeHotkeyOverrides(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value).filter(
([id, binding]) =>
id.length > 0 &&
id.length <= 128 &&
(binding === null ||
(typeof binding === "string" &&
binding.length > 0 &&
binding.length <= 128 &&
validateHotkey(binding).valid)),
),
) as Record<string, Hotkey | null>;
}
export function toElectronAccelerator(hotkey: Hotkey) {
const keyAliases: Record<string, string> = {
ArrowDown: "Down",
ArrowLeft: "Left",
ArrowRight: "Right",
ArrowUp: "Up",
" ": "Space",
};
const modifierAliases: Record<string, string> = {
Alt: "Alt",
Control: "Control",
Ctrl: "Control",
Meta: "Command",
Mod: "CommandOrControl",
Shift: "Shift",
};
const parts = hotkey.split("+");
if (parts.length === 0) return null;
const key = parts.at(-1)!;
const modifiers = parts.slice(0, -1).map((part) => modifierAliases[part]);
if (modifiers.some((part) => !part)) return null;
const acceleratorKey = keyAliases[key] ?? key;
if (
!/^[A-Za-z0-9]$/.test(acceleratorKey) &&
!keyAliases[key] &&
!/^(Backspace|Delete|End|Enter|Escape|F([1-9]|1[0-9]|2[0-4])|Home|PageDown|PageUp|Space|Tab)$/.test(
acceleratorKey,
)
) {
return null;
}
return [...modifiers, acceleratorKey].join("+");
}