mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 23:13:15 +00:00
* feat: add UI hotkey wiring and Cmd/Ctrl+B sidebar toggle Add a reusable useHotkey hook for registering global keyboard shortcuts (with 'mod' resolving to Cmd on macOS, Ctrl elsewhere), and wire up the first shortcut: mod+b toggles the left sidebar. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: ignore key repeat for sidebar toggle hotkey Holding Cmd/Ctrl+B fired repeated keydown events, toggling the sidebar multiple times. Add an ignoreRepeat option to useHotkey and enable it for the sidebar toggle so one held press toggles once. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: Brendan Whiting <16016903+bwhiting2356@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
123 lines
3.1 KiB
TypeScript
123 lines
3.1 KiB
TypeScript
import { useEffect, useRef } from "react"
|
|
|
|
export interface HotkeyOptions {
|
|
enabled?: boolean
|
|
preventDefault?: boolean
|
|
enableInFormFields?: boolean
|
|
ignoreRepeat?: boolean
|
|
}
|
|
|
|
interface ParsedCombo {
|
|
key: string
|
|
mod: boolean
|
|
meta: boolean
|
|
ctrl: boolean
|
|
alt: boolean
|
|
shift: boolean
|
|
}
|
|
|
|
function isMac(): boolean {
|
|
if (typeof navigator === "undefined") return false
|
|
return /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent)
|
|
}
|
|
|
|
/** Parse a combo string like "mod+b" or "shift+escape" into its parts. */
|
|
function parseCombo(combo: string): ParsedCombo {
|
|
const parsed: ParsedCombo = {
|
|
key: "",
|
|
mod: false,
|
|
meta: false,
|
|
ctrl: false,
|
|
alt: false,
|
|
shift: false,
|
|
}
|
|
for (const part of combo
|
|
.toLowerCase()
|
|
.split("+")
|
|
.map((p) => p.trim())) {
|
|
switch (part) {
|
|
case "":
|
|
break
|
|
case "mod":
|
|
parsed.mod = true
|
|
break
|
|
case "meta":
|
|
case "cmd":
|
|
case "command":
|
|
parsed.meta = true
|
|
break
|
|
case "ctrl":
|
|
case "control":
|
|
parsed.ctrl = true
|
|
break
|
|
case "alt":
|
|
case "option":
|
|
parsed.alt = true
|
|
break
|
|
case "shift":
|
|
parsed.shift = true
|
|
break
|
|
default:
|
|
parsed.key = part
|
|
}
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function eventMatchesCombo(event: KeyboardEvent, combo: ParsedCombo): boolean {
|
|
if (event.key.toLowerCase() !== combo.key) return false
|
|
const mac = isMac()
|
|
const expectMeta = combo.meta || (combo.mod && mac)
|
|
const expectCtrl = combo.ctrl || (combo.mod && !mac)
|
|
return (
|
|
event.metaKey === expectMeta &&
|
|
event.ctrlKey === expectCtrl &&
|
|
event.altKey === combo.alt &&
|
|
event.shiftKey === combo.shift
|
|
)
|
|
}
|
|
|
|
function isFormField(target: EventTarget | null): boolean {
|
|
if (!(target instanceof HTMLElement)) return false
|
|
return (
|
|
target.tagName === "INPUT" ||
|
|
target.tagName === "TEXTAREA" ||
|
|
target.tagName === "SELECT" ||
|
|
target.isContentEditable
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Register a global keyboard shortcut. Use "mod" for the platform meta key
|
|
* (Cmd on macOS, Ctrl elsewhere). Accepts one combo or several aliases.
|
|
*/
|
|
export function useHotkey(
|
|
combo: string | string[],
|
|
handler: (event: KeyboardEvent) => void,
|
|
options: HotkeyOptions = {}
|
|
) {
|
|
const {
|
|
enabled = true,
|
|
preventDefault = true,
|
|
enableInFormFields = false,
|
|
ignoreRepeat = false,
|
|
} = options
|
|
const handlerRef = useRef(handler)
|
|
handlerRef.current = handler
|
|
|
|
const comboKey = Array.isArray(combo) ? combo.join(",") : combo
|
|
|
|
useEffect(() => {
|
|
if (!enabled || typeof window === "undefined") return
|
|
const combos = comboKey.split(",").map(parseCombo)
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (ignoreRepeat && event.repeat) return
|
|
if (!enableInFormFields && isFormField(event.target)) return
|
|
if (!combos.some((c) => eventMatchesCombo(event, c))) return
|
|
if (preventDefault) event.preventDefault()
|
|
handlerRef.current(event)
|
|
}
|
|
window.addEventListener("keydown", onKeyDown)
|
|
return () => window.removeEventListener("keydown", onKeyDown)
|
|
}, [enabled, preventDefault, enableInFormFields, ignoreRepeat, comboKey])
|
|
}
|