import type React from "react"; import { Fragment, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useDevice } from "@app/core/providers/useDevice.js"; import { useAppStore } from "@app/core/stores/appStore.js"; import { useDeviceStore } from "@app/core/stores/deviceStore.js"; import { GroupView } from "@components/CommandPalette/GroupView.js"; import { NoResults } from "@components/CommandPalette/NoResults.js"; import { PaletteTransition } from "@components/CommandPalette/PaletteTransition.js"; import { SearchBox } from "@components/CommandPalette/SearchBox.js"; import { SearchResult } from "@components/CommandPalette/SearchResult.js"; import { Hashicon } from "@emeraldpay/hashicon-react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; import { ArchiveBoxXMarkIcon, ArrowDownOnSquareStackIcon, ArrowPathIcon, ArrowPathRoundedSquareIcon, ArrowsRightLeftIcon, BeakerIcon, BugAntIcon, Cog8ToothIcon, CubeTransparentIcon, DevicePhoneMobileIcon, DocumentTextIcon, IdentificationIcon, InboxIcon, LinkIcon, MapIcon, MoonIcon, PlusIcon, PowerIcon, QrCodeIcon, QueueListIcon, Square3Stack3DIcon, SwatchIcon, TrashIcon, UsersIcon, WindowIcon, XCircleIcon } from "@heroicons/react/24/outline"; import { Blur } from "../generic/Blur.js"; import { ThemeController } from "../generic/ThemeController.js"; export interface Group { name: string; icon: (props: React.ComponentProps<"svg">) => JSX.Element; commands: Command[]; } export interface Command { name: string; icon: (props: React.ComponentProps<"svg">) => JSX.Element; action?: () => void; subItems?: SubItem[]; tags?: string[]; } export interface SubItem { name: string; icon: JSX.Element; action: () => void; } export const CommandPalette = (): JSX.Element => { const [query, setQuery] = useState(""); const { commandPaletteOpen, setCommandPaletteOpen, devices, setSelectedDevice, removeDevice, selectedDevice, darkMode, setDarkMode, setAccent } = useAppStore(); const { getDevices } = useDeviceStore(); const { setQRDialogOpen, setImportDialogOpen, setShutdownDialogOpen, setRebootDialogOpen, setActivePage, connection } = useDevice(); const groups: Group[] = [ { name: "Goto", icon: LinkIcon, commands: [ { name: "Messages", icon: InboxIcon, action() { setActivePage("messages"); } }, { name: "Map", icon: MapIcon, action() { setActivePage("map"); } }, { name: "Extensions", icon: BeakerIcon, action() { setActivePage("extensions"); } }, { name: "Config", icon: Cog8ToothIcon, action() { setActivePage("config"); }, tags: ["settings"] }, { name: "Channels", icon: Square3Stack3DIcon, action() { setActivePage("channels"); } }, { name: "Peers", icon: UsersIcon, action() { setActivePage("peers"); } }, { name: "Info", icon: IdentificationIcon, action() { setActivePage("info"); } }, { name: "Logs", icon: DocumentTextIcon, action() { setActivePage("logs"); } } ] }, { name: "Manage", icon: DevicePhoneMobileIcon, commands: [ { name: "Switch Node", icon: ArrowsRightLeftIcon, subItems: getDevices().map((device) => { return { name: device.nodes.find( (n) => n.data.num === device.hardware.myNodeNum )?.data.user?.longName ?? device.hardware.myNodeNum.toString(), icon: ( ), action() { setSelectedDevice(device.id); } }; }) }, { name: "Connect New Node", icon: PlusIcon, action() { setSelectedDevice(0); } } ] }, { name: "Contextual", icon: CubeTransparentIcon, commands: [ { name: "QR Code", icon: QrCodeIcon, subItems: [ { name: "Generator", icon: , action() { setQRDialogOpen(true); } }, { name: "Import", icon: , action() { setImportDialogOpen(true); } } ] }, { name: "Disconnect", icon: XCircleIcon, action() { void connection?.disconnect(); setSelectedDevice(0); removeDevice(selectedDevice ?? 0); } }, { name: "Schedule Shutdown", icon: PowerIcon, action() { setShutdownDialogOpen(true); } }, { name: "Schedule Reboot", icon: ArrowPathIcon, action() { setRebootDialogOpen(true); } }, { name: "Reset Peers", icon: TrashIcon, action() { if (connection) { void toast.promise(connection.resetPeers(), { loading: "Resetting...", success: "Succesfully reset peers", error: "No response received" }); } } }, { name: "Factory Reset", icon: ArrowPathRoundedSquareIcon, action() { if (connection) { void toast.promise(connection.factoryReset(), { loading: "Resetting...", success: "Succesfully factory peers", error: "No response received" }); } } } ] }, { name: "Debug", icon: BugAntIcon, commands: [ { name: "Reconfigure", icon: ArrowPathIcon, action() { void connection?.configure(); } }, { name: "[WIP] Clear Messages", icon: ArchiveBoxXMarkIcon, action() { alert("This feature is not implemented"); } } ] }, { name: "Application", icon: WindowIcon, commands: [ { name: "Toggle Dark Mode", icon: MoonIcon, action() { setDarkMode(!darkMode); } }, { name: "Accent Color", icon: SwatchIcon, subItems: [ { name: "Red", icon: ( ), action() { setAccent("red"); } }, { name: "Orange", icon: ( ), action() { setAccent("orange"); } }, { name: "Yellow", icon: ( ), action() { setAccent("yellow"); } }, { name: "Green", icon: ( ), action() { setAccent("green"); } }, { name: "Blue", icon: ( ), action() { setAccent("blue"); } }, { name: "Purple", icon: ( ), action() { setAccent("purple"); } }, { name: "Pink", icon: ( ), action() { setAccent("pink"); } } ] } ] } ]; const handleKeydown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setCommandPaletteOpen(true); } }; useEffect(() => { window.addEventListener("keydown", handleKeydown); return () => window.removeEventListener("keydown", handleKeydown); }, []); const filtered = query === "" ? [] : groups .map((group) => { return { ...group, commands: group.commands.filter((command) => { const nameIncludes = `${group.name} ${command.name}` .toLowerCase() .includes(query.toLowerCase()); const tagsInclude = ( command.tags ?.map((t) => t.includes(query.toLowerCase())) .filter(Boolean) ?? [] ).length; const subItemsInclude = ( command.subItems ?.map((s) => s.name.toLowerCase().includes(query.toLowerCase()) ) .filter(Boolean) ?? [] ).length; return nameIncludes || tagsInclude || subItemsInclude; }) }; }) .filter((group) => group.commands.length); return ( setQuery("")} appear > onChange={(input) => { if (typeof input === "string") { setQuery(input); } else if (input.action) { setCommandPaletteOpen(false); input.action(); } }} > {query === "" || filtered.length > 0 ? (
    • {filtered.map((group, index) => ( ))} {query === "" && groups.map((group, index) => ( ))}
  • ) : ( query !== "" && filtered.length === 0 && )}
    ); };