summaryrefslogtreecommitdiff
path: root/apps/web/src/actions/keybindings/persistence.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-29 00:37:56 +0200
committerMaze Winther <mazewinther@gmail.com>2026-05-02 03:27:23 +0200
commita9b9cf6bf50f08723896659634f2e6afc17bcd42 (patch)
tree9175b49ff21f4326872ac9e541d2765cc7994ebc /apps/web/src/actions/keybindings/persistence.ts
parentd6622dc6b3523a5658cd512b44f15f7a66fd540c (diff)
downloadopencut-a9b9cf6bf50f08723896659634f2e6afc17bcd42.tar.gz
chore: switch from biome to eslint + prettier; fix ton of lint issues
Diffstat (limited to 'apps/web/src/actions/keybindings/persistence.ts')
-rw-r--r--apps/web/src/actions/keybindings/persistence.ts119
1 files changed, 119 insertions, 0 deletions
diff --git a/apps/web/src/actions/keybindings/persistence.ts b/apps/web/src/actions/keybindings/persistence.ts
new file mode 100644
index 00000000..ced44bd0
--- /dev/null
+++ b/apps/web/src/actions/keybindings/persistence.ts
@@ -0,0 +1,119 @@
+import type { ShortcutKey } from "@/actions/keybinding";
+import { isShortcutKey } from "@/actions/keybinding";
+import type { TActionWithOptionalArgs } from "@/actions";
+import { isActionWithOptionalArgs } from "@/actions";
+import { runMigrations } from "./migrations";
+import {
+ getPersistedKeybindingsState,
+ type PersistedKeybindingsState,
+} from "./persisted-state";
+
+export interface DecodedKeybindingsState {
+ keybindings: Map<ShortcutKey, TActionWithOptionalArgs>;
+ isCustomized: boolean;
+}
+
+export function serializeKeybindingsState({
+ keybindings,
+ isCustomized,
+}: DecodedKeybindingsState): PersistedKeybindingsState {
+ return {
+ keybindings: Object.fromEntries(keybindings),
+ isCustomized,
+ };
+}
+
+export function migratePersistedKeybindingsState({
+ state,
+ fromVersion,
+}: {
+ state: unknown;
+ fromVersion: number;
+}): unknown {
+ return runMigrations({ state, fromVersion });
+}
+
+/**
+ * Decode a persisted/migrated keybindings blob into the in-memory shape.
+ *
+ * Lossy by design: invalid entries are dropped and a warning is emitted, so the
+ * user falls back to (mostly) sensible defaults instead of a broken store.
+ * Returns `null` if the top-level shape is unrecognizable, in which case the
+ * caller should keep its current state.
+ */
+export function decodePersistedKeybindingsState({
+ state,
+}: {
+ state: unknown;
+}): DecodedKeybindingsState | null {
+ const persisted = getPersistedKeybindingsState({ state });
+ if (!persisted) {
+ console.warn(
+ "[keybindings] Persisted state has unexpected shape; keeping current keybindings.",
+ state,
+ );
+ return null;
+ }
+
+ const keybindings = new Map<ShortcutKey, TActionWithOptionalArgs>();
+ const dropped: Array<{ key: string; action: string | undefined }> = [];
+ for (const [key, action] of Object.entries(persisted.keybindings)) {
+ if (action === undefined) continue;
+ if (!isShortcutKey(key) || !isActionWithOptionalArgs(action)) {
+ dropped.push({ key, action });
+ continue;
+ }
+
+ keybindings.set(key, action);
+ }
+
+ if (dropped.length > 0) {
+ console.warn(
+ "[keybindings] Dropped invalid persisted entries:",
+ dropped,
+ );
+ }
+
+ return {
+ keybindings,
+ isCustomized: persisted.isCustomized,
+ };
+}
+
+/**
+ * Parse a user-supplied keybindings configuration (typically the output of
+ * `JSON.parse` on an imported file).
+ *
+ * Strict by design: throws on the first invalid entry so the caller can surface
+ * the failure to the user instead of silently producing a half-applied import.
+ * Accepts `unknown` because the input has already crossed a trust boundary.
+ */
+export function parseImportedKeybindings({
+ config,
+}: {
+ config: unknown;
+}): Map<ShortcutKey, TActionWithOptionalArgs> {
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
+ throw new Error("Imported keybindings must be a JSON object");
+ }
+
+ const result = new Map<ShortcutKey, TActionWithOptionalArgs>();
+ for (const [key, action] of Object.entries(config)) {
+ if (action === undefined) continue;
+ if (typeof action !== "string") {
+ throw new Error(
+ `Invalid action for "${key}": expected string, got ${typeof action}`,
+ );
+ }
+ if (!isShortcutKey(key)) {
+ throw new Error(`Invalid shortcut key: ${JSON.stringify(key)}`);
+ }
+ if (!isActionWithOptionalArgs(action)) {
+ throw new Error(
+ `Invalid action for "${key}": ${JSON.stringify(action)}`,
+ );
+ }
+ result.set(key, action);
+ }
+ return result;
+}