import { Avatar } from "@components/UI/Avatar.tsx"; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@components/UI/Command.tsx"; import { usePinnedItems } from "@core/hooks/usePinnedItems.ts"; import { useAppStore, useDevice, useDeviceStore, useNodeDB, } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import { useNavigate } from "@tanstack/react-router"; import { useCommandState } from "cmdk"; import { ArrowLeftRightIcon, BoxSelectIcon, BugIcon, CloudOff, EraserIcon, FactoryIcon, HardDriveUpload, LayersIcon, LinkIcon, type LucideIcon, MapIcon, MessageSquareIcon, Pin, PlusIcon, PowerIcon, QrCodeIcon, RefreshCwIcon, SettingsIcon, SmartphoneIcon, TrashIcon, UsersIcon, } from "lucide-react"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; export interface Group { id: string; label: string; icon: LucideIcon; commands: Command[]; } export interface Command { label: string; icon: LucideIcon; action?: () => void; subItems?: SubItem[]; tags?: string[]; } export interface SubItem { label: string; icon: React.ReactNode; action: () => void; } export const CommandPalette = () => { const { commandPaletteOpen, setCommandPaletteOpen, setConnectDialogOpen, setSelectedDevice, } = useAppStore(); const { getDevices } = useDeviceStore(); const { setDialogOpen, connection } = useDevice(); const { getNode, removeAllNodeErrors, removeAllNodes } = useNodeDB(); const { pinnedItems, togglePinnedItem } = usePinnedItems({ storageName: "pinnedCommandMenuGroups", }); const { t } = useTranslation("commandPalette"); const navigate = useNavigate({ from: "/" }); const groups: Group[] = [ { id: "gotoGroup", label: t("goto.label"), icon: LinkIcon, commands: [ { label: t("goto.command.messages"), icon: MessageSquareIcon, action() { navigate({ to: "/messages" }); }, }, { label: t("goto.command.map"), icon: MapIcon, action() { navigate({ to: "/map" }); }, }, { label: t("goto.command.config"), icon: SettingsIcon, action() { navigate({ to: "/config" }); }, tags: ["settings"], }, { label: t("goto.command.channels"), icon: LayersIcon, action() { navigate({ to: "/channels" }); }, }, { label: t("goto.command.nodes"), icon: UsersIcon, action() { navigate({ to: "/nodes" }); }, }, ], }, { id: "manageGroup", label: t("manage.label"), icon: SmartphoneIcon, commands: [ { label: t("manage.command.switchNode"), icon: ArrowLeftRightIcon, subItems: getDevices().map((device) => ({ label: getNode(device.hardware.myNodeNum)?.user?.longName ?? t("unknown.shortName"), icon: ( ), action() { setSelectedDevice(device.id); }, })), }, { label: t("manage.command.connectNewNode"), icon: PlusIcon, action() { setConnectDialogOpen(true); }, }, ], }, { id: "contextualGroup", label: t("contextual.label"), icon: BoxSelectIcon, commands: [ { label: t("contextual.command.qrCode"), icon: QrCodeIcon, subItems: [ { label: t("contextual.command.qrGenerator"), icon: , action() { setDialogOpen("QR", true); }, }, { label: t("contextual.command.qrImport"), icon: , action() { setDialogOpen("import", true); }, }, ], }, { label: t("contextual.command.scheduleShutdown"), icon: PowerIcon, action() { setDialogOpen("shutdown", true); }, }, { label: t("contextual.command.scheduleReboot"), icon: RefreshCwIcon, action() { setDialogOpen("reboot", true); }, }, { label: t("contextual.command.dfuMode"), icon: HardDriveUpload, action() { connection?.enterDfuMode(); }, }, { label: t("contextual.command.resetNodeDb"), icon: TrashIcon, action() { connection?.resetNodes(); removeAllNodeErrors(); removeAllNodes(true); }, }, { label: t("contextual.command.disconnect"), icon: CloudOff, action() { connection?.disconnect().catch((error) => { console.error("Failed to disconnect:", error); }); }, }, { label: t("contextual.command.factoryResetDevice"), icon: FactoryIcon, action() { connection?.factoryResetDevice(); removeAllNodeErrors(); removeAllNodes(); }, }, { label: t("contextual.command.factoryResetConfig"), icon: FactoryIcon, action() { connection?.factoryResetConfig(); }, }, ], }, { id: "debugGroup", label: t("debug.label"), icon: BugIcon, commands: [ { label: t("debug.command.reconfigure"), icon: RefreshCwIcon, action() { void connection?.configure(); }, }, { label: t("debug.command.clearAllStoredMessages"), icon: EraserIcon, action() { setDialogOpen("deleteMessages", true); }, }, ], }, ]; const sortedGroups = [...groups].sort((a, b) => { const aPinned = pinnedItems.includes(a.id) ? 1 : 0; const bPinned = pinnedItems.includes(b.id) ? 1 : 0; return bPinned - aPinned; }); useEffect(() => { const handleKeydown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setCommandPaletteOpen(true); } }; globalThis.addEventListener("keydown", handleKeydown); return () => globalThis.removeEventListener("keydown", handleKeydown); }, [setCommandPaletteOpen]); return ( {t("emptyState")} {sortedGroups.map((group) => ( {group.label} } > {group.commands.map((command) => (
{ command.action?.(); setCommandPaletteOpen(false); }} > {command.label} {command.subItems?.map((subItem) => ( ))}
))}
))}
); }; const SubItem = ({ label, icon, action, }: { label: string; icon: React.ReactNode; action: () => void; }) => { const search = useCommandState((state) => state.search); if (!search) { return null; } return ( {icon} {label} ); };