Files
kandev/apps/web/lib/keyboard/plugin-shortcuts.ts
T
2026-09-04 17:38:05 +00:00

117 lines
4.2 KiB
TypeScript

/**
* Dynamic layer over `CONFIGURABLE_SHORTCUTS`: builds the same
* label/default shape for plugin-declared keybindings (`ui.keybindings` on a
* `PluginRecord`), so the plugin detail shortcut editor can render
* user-overrides without widening `ConfigurableShortcutId` to an open string
* type.
*
* Namespacing: a plugin keybinding's effective override id is
* `plugin:{pluginId}:{keybindingId}` — this is what gets stored under
* `userSettings.keyboardShortcuts` (the same `StoredShortcutOverrides` map
* core shortcuts already use).
*/
import type { PluginRecord } from "@/lib/types/plugins";
import type { KeyboardShortcut } from "./constants";
import { parseCombo } from "./parse-combo";
import {
CONFIGURABLE_SHORTCUTS,
type ConfigurableShortcutId,
type StoredShortcutOverrides,
} from "./shortcut-overrides";
/** Prefix marking a namespaced id as belonging to a plugin, not core. */
export const PLUGIN_SHORTCUT_PREFIX = "plugin:";
export function pluginShortcutId(pluginId: string, keybindingId: string): string {
return `${PLUGIN_SHORTCUT_PREFIX}${pluginId}:${keybindingId}`;
}
/** One configurable shortcut entry, from either the core static list or a plugin manifest. */
export type ShortcutEntry =
| { source: "core"; id: ConfigurableShortcutId; label: string; default: KeyboardShortcut }
| {
source: "plugin";
id: string;
label: string;
default: KeyboardShortcut;
pluginId: string;
keybindingId: string;
};
/**
* The static core shortcuts, in `ShortcutEntry` shape — exported so callers
* that already hold a `ShortcutEntry[]` for plugins (e.g.
* `KeyboardShortcutsCard`, which receives plugin entries as a prop rather
* than raw `PluginRecord[]`) can merge core + plugin entries without
* re-deriving the core list themselves.
*/
type TranslateShortcutLabel = (key: string) => string;
export function coreShortcutEntries(
translate: TranslateShortcutLabel = (key) => key,
): ShortcutEntry[] {
return (Object.keys(CONFIGURABLE_SHORTCUTS) as ConfigurableShortcutId[]).map((id) => ({
source: "core",
id,
label: translate(CONFIGURABLE_SHORTCUTS[id].labelKey),
default: CONFIGURABLE_SHORTCUTS[id].default,
}));
}
/**
* Builds the plugin-sourced configurable shortcut entries for every installed
* plugin's declared `ui.keybindings`. A combo that fails to parse (should
* never happen — the backend validates manifests at registration) is
* skipped with a console warning rather than throwing.
*/
function buildPluginEntries(plugins: PluginRecord[]): ShortcutEntry[] {
const entries: ShortcutEntry[] = [];
for (const plugin of plugins) {
for (const keybinding of plugin.ui?.keybindings ?? []) {
const shortcut = parseCombo(keybinding.default);
if (!shortcut) {
console.warn(
`[plugins] "${plugin.id}" keybinding "${keybinding.id}" has an unparseable default combo "${keybinding.default}"`,
);
continue;
}
entries.push({
source: "plugin",
id: pluginShortcutId(plugin.id, keybinding.id),
label: `${plugin.display_name}: ${keybinding.description}`,
default: shortcut,
pluginId: plugin.id,
keybindingId: keybinding.id,
});
}
}
return entries;
}
/**
* The full list of configurable shortcuts — core (static) entries followed
* by plugin (dynamic) entries — for conflict calculation and plugin detail
* editors. Core behavior/order is unchanged from `CONFIGURABLE_SHORTCUTS`.
*/
export function buildConfigurableShortcutEntries(
plugins: PluginRecord[],
translate?: TranslateShortcutLabel,
): ShortcutEntry[] {
return [...coreShortcutEntries(translate), ...buildPluginEntries(plugins)];
}
/** Builds only the plugin-sourced entries — used by callers that already have core entries. */
export function buildPluginShortcutEntries(plugins: PluginRecord[]): ShortcutEntry[] {
return buildPluginEntries(plugins);
}
/** Resolves an entry's effective shortcut: the user override if set, else its default. */
export function resolveShortcutEntry(
entry: ShortcutEntry,
overrides?: StoredShortcutOverrides,
): KeyboardShortcut {
const override = overrides?.[entry.id];
if (override) return override as KeyboardShortcut;
return entry.default;
}