summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 00:26:31 +0200
committercashmere <cashmere@autistici.org>2026-07-27 00:26:31 +0200
commitb4d8145f2adb981b462a4581bdf3bf96bc32e3d9 (patch)
tree728d90210d573f3ac577a44edf8e6815cc05e140 /apps/web/src
parent607f9717ae44efe8f135b6c79ed85a583f3d2d1d (diff)
downloadopencut-b4d8145f2adb981b462a4581bdf3bf96bc32e3d9.tar.gz
fix: restore missing keybinding validation guards
persistence.ts imported isActionWithOptionalArgs and isShortcutKey, neither of which existed at runtime — loading persisted custom keybindings would crash. Adds actions/guards.ts (excluding required-args actions per TActionArgsMap) and isShortcutKey in keybinding.ts; the nine previously dead persistence tests now pass.
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/actions/guards.ts29
-rw-r--r--apps/web/src/actions/index.ts1
-rw-r--r--apps/web/src/actions/keybinding.ts20
3 files changed, 50 insertions, 0 deletions
diff --git a/apps/web/src/actions/guards.ts b/apps/web/src/actions/guards.ts
new file mode 100644
index 00000000..c4fbe8a7
--- /dev/null
+++ b/apps/web/src/actions/guards.ts
@@ -0,0 +1,29 @@
+import { ACTIONS } from "./definitions";
+import type { TActionArgsMap, TActionWithOptionalArgs } from "./types";
+
+type TActionWithRequiredArgs = {
+ [K in keyof TActionArgsMap]: undefined extends TActionArgsMap[K]
+ ? never
+ : K;
+}[keyof TActionArgsMap];
+
+/**
+ * Actions that require arguments cannot be bound to a shortcut key — a key
+ * press has no way to supply them. Keep in sync with the required (non-
+ * optional) members of `TActionArgsMap`; the type annotation rejects keys
+ * that are not actually required-args actions.
+ */
+const ACTIONS_WITH_REQUIRED_ARGS: readonly TActionWithRequiredArgs[] = [
+ "remove-media-asset",
+ "remove-media-assets",
+];
+
+export function isActionWithOptionalArgs(
+ action: unknown,
+): action is TActionWithOptionalArgs {
+ return (
+ typeof action === "string" &&
+ action in ACTIONS &&
+ !(ACTIONS_WITH_REQUIRED_ARGS as readonly string[]).includes(action)
+ );
+}
diff --git a/apps/web/src/actions/index.ts b/apps/web/src/actions/index.ts
index 801a846a..d74a31a5 100644
--- a/apps/web/src/actions/index.ts
+++ b/apps/web/src/actions/index.ts
@@ -1,3 +1,4 @@
export * from "./definitions";
export * from "./types";
export * from "./registry";
+export * from "./guards";
diff --git a/apps/web/src/actions/keybinding.ts b/apps/web/src/actions/keybinding.ts
index aeec2fd6..a71b7e98 100644
--- a/apps/web/src/actions/keybinding.ts
+++ b/apps/web/src/actions/keybinding.ts
@@ -32,6 +32,26 @@ export function isKey(value: string): value is Key {
return KEY_SET.has(value);
}
+const MODIFIER_KEYS: readonly ModifierKeys[] = [
+ "ctrl",
+ "alt",
+ "shift",
+ "ctrl+shift",
+ "alt+shift",
+ "ctrl+alt",
+ "ctrl+alt+shift",
+];
+
+export function isShortcutKey(value: string): value is ShortcutKey {
+ if (isKey(value)) {
+ return true;
+ }
+ const parts = value.split("+");
+ const key = parts.at(-1) ?? "";
+ const modifiers = parts.slice(0, -1).join("+");
+ return (MODIFIER_KEYS as readonly string[]).includes(modifiers) && isKey(key);
+}
+
export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}`;
// Singular keybindings (these will be disabled when an input-ish area has been focused)
export type SingleCharacterShortcutKey = `${Key}`;