ce82e2a4ab
Co-authored-by: Kandev Agent <agent@kandev.dev>
117 lines
4.2 KiB
TypeScript
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 Settings > Keyboard Shortcuts can render and
|
|
* user-override plugin shortcuts alongside the static core list 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 active
|
|
* 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 rendering in Settings > Keyboard
|
|
* Shortcuts. 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;
|
|
}
|