import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" import { formatKeybind, parseKeybind, useCommand } from "@/context/command" import { useSettings } from "@/context/settings" const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) const PALETTE_ID = "command.palette" const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt" type KeybindMeta = { title: string group: KeybindGroup } const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] function groupFor(id: string): KeybindGroup { if (id === PALETTE_ID) return "General" if (id.startsWith("terminal.")) return "Terminal" if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" if (id.startsWith("file.")) return "Navigation" if (id.startsWith("prompt.")) return "Prompt" if ( id.startsWith("session.") || id.startsWith("message.") || id.startsWith("permissions.") || id.startsWith("steps.") || id.startsWith("review.") ) return "Session" return "General" } function isModifier(key: string) { return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta" } function normalizeKey(key: string) { if (key === ",") return "comma" if (key === "+") return "plus" if (key === " ") return "space" return key.toLowerCase() } function recordKeybind(event: KeyboardEvent) { if (isModifier(event.key)) return const parts: string[] = [] const mod = IS_MAC ? event.metaKey : event.ctrlKey if (mod) parts.push("mod") if (IS_MAC && event.ctrlKey) parts.push("ctrl") if (!IS_MAC && event.metaKey) parts.push("meta") if (event.altKey) parts.push("alt") if (event.shiftKey) parts.push("shift") const key = normalizeKey(event.key) if (!key) return parts.push(key) return parts.join("+") } function signatures(config: string | undefined) { if (!config) return [] const sigs: string[] = [] for (const kb of parseKeybind(config)) { const parts: string[] = [] if (kb.ctrl) parts.push("ctrl") if (kb.alt) parts.push("alt") if (kb.shift) parts.push("shift") if (kb.meta) parts.push("meta") if (kb.key) parts.push(kb.key) if (parts.length === 0) continue sigs.push(parts.join("+")) } return sigs } export const SettingsKeybinds: Component = () => { const command = useCommand() const settings = useSettings() const [active, setActive] = createSignal(null) const stop = () => { if (!active()) return setActive(null) command.keybinds(true) } const start = (id: string) => { if (active() === id) { stop() return } if (active()) stop() setActive(id) command.keybinds(false) } const hasOverrides = createMemo(() => { const keybinds = settings.current.keybinds as Record | undefined if (!keybinds) return false return Object.values(keybinds).some((x) => typeof x === "string") }) const resetAll = () => { stop() settings.keybinds.resetAll() showToast({ title: "Shortcuts reset", description: "Keyboard shortcuts have been reset to defaults." }) } const list = createMemo(() => { const out = new Map() out.set(PALETTE_ID, { title: "Command palette", group: "General" }) for (const opt of command.catalog) { if (opt.id.startsWith("suggested.")) continue out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) } for (const opt of command.options) { if (opt.id.startsWith("suggested.")) continue out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) } const keybinds = settings.current.keybinds as Record | undefined if (keybinds) { for (const [id, value] of Object.entries(keybinds)) { if (typeof value !== "string") continue if (out.has(id)) continue out.set(id, { title: id, group: groupFor(id) }) } } return out }) const title = (id: string) => list().get(id)?.title ?? "" const grouped = createMemo(() => { const map = list() const out = new Map() for (const group of GROUPS) out.set(group, []) for (const [id, item] of map) { const ids = out.get(item.group) if (!ids) continue ids.push(id) } for (const group of GROUPS) { const ids = out.get(group) if (!ids) continue ids.sort((a, b) => { const at = map.get(a)?.title ?? "" const bt = map.get(b)?.title ?? "" return at.localeCompare(bt) }) } return out }) const used = createMemo(() => { const map = new Map() const add = (key: string, value: { id: string; title: string }) => { const list = map.get(key) if (!list) { map.set(key, [value]) return } list.push(value) } const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND for (const sig of signatures(palette)) { add(sig, { id: PALETTE_ID, title: "Command palette" }) } const valueFor = (id: string) => { const custom = settings.keybinds.get(id) if (typeof custom === "string") return custom const live = command.options.find((x) => x.id === id) if (live?.keybind) return live.keybind const meta = command.catalog.find((x) => x.id === id) return meta?.keybind } for (const id of list().keys()) { if (id === PALETTE_ID) continue for (const sig of signatures(valueFor(id))) { add(sig, { id, title: title(id) }) } } return map }) const setKeybind = (id: string, keybind: string) => { settings.keybinds.set(id, keybind) } onMount(() => { const handle = (event: KeyboardEvent) => { const id = active() if (!id) return event.preventDefault() event.stopPropagation() event.stopImmediatePropagation() if (event.key === "Escape") { stop() return } const clear = (event.key === "Backspace" || event.key === "Delete") && !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey if (clear) { setKeybind(id, "none") stop() return } const next = recordKeybind(event) if (!next) return const map = used() const conflicts = new Map() for (const sig of signatures(next)) { const list = map.get(sig) ?? [] for (const item of list) { if (item.id === id) continue conflicts.set(item.id, item.title) } } if (conflicts.size > 0) { showToast({ title: "Shortcut already in use", description: `${formatKeybind(next)} is already assigned to ${[...conflicts.values()].join(", ")}.`, }) return } setKeybind(id, next) stop() } document.addEventListener("keydown", handle, true) onCleanup(() => { document.removeEventListener("keydown", handle, true) }) }) onCleanup(() => { if (active()) command.keybinds(true) }) return (

Keyboard shortcuts

Click a shortcut to edit. Press Esc to cancel.

{(group) => ( 0}>

{group}

{(id) => (
{title(id)}
)}
)}
) }