From ea53f92f629a21ae57d3db587519feede0366768 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Wed, 22 Jul 2026 10:39:29 -0400 Subject: [PATCH] HotKeys Bug Fixes --- bun.lock | 3 + package.json | 1 + src/lib/components/Editor.svelte | 18 +-- src/lib/components/SettingsModal.svelte | 200 +++++++++++++++++++++++- src/lib/ts/editor-actions.ts | 51 ++++-- src/lib/ts/editor-theme.ts | 2 +- src/lib/ts/hotkeys.ts | 154 ++++++++++++++++++ src/routes/+page.svelte | 73 +++++++-- 8 files changed, 463 insertions(+), 39 deletions(-) create mode 100644 src/lib/ts/hotkeys.ts diff --git a/bun.lock b/bun.lock index 00f5bd8..e97f642 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "@tauri-apps/plugin-opener": "^2.5.4", "codemirror": "^6.0.2", "codemirror-lang-typst": "^0.4.0", + "hotkeys-js": "^4.0.4", "y-codemirror.next": "^0.3.5", "y-websocket": "^3.0.0", "yjs": "^13.6.31", @@ -345,6 +346,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hotkeys-js": ["hotkeys-js@4.0.4", "", {}, "sha512-hseNiqaskxSnujuGp8aRMLJfcjaFiTSS0I2GQhqru82N/sx6CGyUf6pvU5X1iycvw2EqmvILkFIb5OzYFXY+9A=="], + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], "isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="], diff --git a/package.json b/package.json index 62ae2db..a71f1f1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@tauri-apps/plugin-opener": "^2.5.4", "codemirror": "^6.0.2", "codemirror-lang-typst": "^0.4.0", + "hotkeys-js": "^4.0.4", "y-codemirror.next": "^0.3.5", "y-websocket": "^3.0.0", "yjs": "^13.6.31" diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 74c3503..04d6547 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -7,12 +7,7 @@ highlightActiveLine, } from "@codemirror/view"; import { EditorState, Compartment, StateField } from "@codemirror/state"; - import { - defaultKeymap, - history, - historyKeymap, - indentWithTab, - } from "@codemirror/commands"; + import { defaultKeymap, history, indentWithTab } from "@codemirror/commands"; import { bracketMatching, indentOnInput, @@ -46,7 +41,6 @@ diagnostics?: Diagnostic[]; collab?: { text: Y.Text; awareness: Awareness } | null; onchange: (value: string) => void; - onsave: () => void; onlspstatus?: (status: "off" | "starting" | "on" | "unavailable") => void; onready?: (view: EditorView | null) => void; } @@ -59,7 +53,6 @@ diagnostics = [], collab = null, onchange, - onsave, onlspstatus, onready, }: Props = $props(); @@ -170,17 +163,8 @@ : [autocompletion({ override: [typstCompletions] })]), EditorView.lineWrapping, keymap.of([ - { - key: "Mod-s", - preventDefault: true, - run: () => { - onsave(); - return true; - }, - }, ...closeBracketsKeymap, ...defaultKeymap, - ...historyKeymap, indentWithTab, ]), EditorView.updateListener.of((update) => { diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 3ba5e2c..5be31cd 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -6,6 +6,14 @@ import Modal from "./Modal.svelte"; import * as api from "$lib/ts/api"; import type { AppInfo, CompatibilityStatus } from "$lib/ts/api"; + import { + HOTKEY_DEFS, + comboFromEvent, + isCustomized, + keysFor, + rebindHotkey, + resetHotkey, + } from "$lib/ts/hotkeys"; import { app, applyTheme, @@ -35,6 +43,7 @@ | "accessibility" | "account" | "lsp" + | "hotkeys" | "about"; const sections: { id: Section; label: string; icon: string }[] = [ @@ -43,9 +52,115 @@ { id: "accessibility", label: "Accessibility", icon: "ph:wheelchair" }, { id: "account", label: "Account", icon: "ph:user-circle" }, { id: "lsp", label: "Language Server", icon: "ph:plugs-connected" }, + { id: "hotkeys", label: "Hotkeys", icon: "ph:keyboard" }, { id: "about", label: "About", icon: "ph:info" }, ]; + const isMac = + typeof navigator !== "undefined" && + /mac/i.test(navigator.platform ?? navigator.userAgent); + + function formatKeys(combo: string): string[] { + const variants = combo.split(","); + const preferred = + variants.find((variant) => + isMac ? variant.includes("command") : !variant.includes("command"), + ) ?? variants[0]; + + return preferred.split("+").map((part) => { + switch (part) { + case "command": + return "⌘"; + case "ctrl": + return "Ctrl"; + case "alt": + return isMac ? "⌥" : "Alt"; + case "shift": + return "Shift"; + case "esc": + return "Esc"; + case "space": + return "Space"; + case "up": + return "↑"; + case "down": + return "↓"; + case "left": + return "←"; + case "right": + return "→"; + default: + return part.length === 1 ? part.toUpperCase() : part; + } + }); + } + + let editingId = $state(null); + let hotkeyVersion = $state(0); + + const editableHotkeyGroups = $derived.by(() => { + hotkeyVersion; + const groups = new Map(); + for (const def of HOTKEY_DEFS) { + if (!groups.has(def.group)) groups.set(def.group, []); + groups.get(def.group)!.push(def); + } + return Array.from(groups.entries()).map(([title, defs]) => ({ + title, + items: defs.map((def) => ({ + id: def.id, + label: def.label, + keys: keysFor(def.id), + customized: isCustomized(def.id), + })), + })); + }); + + $effect(() => { + if (!editingId) return; + const id = editingId; + + function handleCapture(event: KeyboardEvent) { + event.preventDefault(); + event.stopPropagation(); + if (event.key === "Escape") { + editingId = null; + return; + } + const combo = comboFromEvent(event); + if (!combo) return; + rebindHotkey(id, combo); + hotkeyVersion++; + editingId = null; + } + + window.addEventListener("keydown", handleCapture, true); + return () => window.removeEventListener("keydown", handleCapture, true); + }); + + const staticHotkeyGroups: { title: string; items: { keys: string[]; label: string }[] }[] = [ + { + title: "File browser", + items: [ + { keys: ["F2"], label: "Rename selected entry" }, + { keys: ["Delete"], label: "Delete selected entries" }, + { keys: ["Esc"], label: "Cancel rename or clear selection" }, + ], + }, + { + title: "Image viewer", + items: [ + { keys: ["←"], label: "Previous image" }, + { keys: ["→"], label: "Next image" }, + { keys: ["Esc"], label: "Close viewer" }, + ], + }, + { + title: "General", + items: [{ keys: ["Esc"], label: "Close dialog" }], + }, + ]; + const lspLabel: Record = { off: "Off", starting: "Starting…", @@ -507,6 +622,88 @@ {/if} + {:else if section === "hotkeys"} +
+

+ Click the pencil next to a shortcut and press a new key combination. + Press Esc while listening to cancel. +

+ + {#each editableHotkeyGroups as group} +
+ + {group.title} + +
+ {#each group.items as item} +
+ {item.label} + {#if editingId === item.id} + + Press keys… (Esc to cancel) + + {:else} + + {#each formatKeys(item.keys) as key} + + {key} + + {/each} + {#if item.customized} + + {/if} + + + {/if} +
+ {/each} +
+
+ {/each} + + {#each staticHotkeyGroups as group} +
+ + {group.title} + +
+ {#each group.items as item} +
+ {item.label} + + {#each item.keys as key} + + {key} + + {/each} + +
+ {/each} +
+
+ {/each} +
{:else}
@@ -569,7 +766,8 @@ section === "about" || section === "appearance" || section === "accessibility" || - section === "lsp"} + section === "lsp" || + section === "hotkeys"}