import { useEffect, useState } from "react"; import { Command } from "cmdk"; import { useGrimoire } from "@/core/state"; import { manPages } from "@/types/man"; import "./command-launcher.css"; interface CommandLauncherProps { open: boolean; onOpenChange: (open: boolean) => void; } export default function CommandLauncher({ open, onOpenChange, }: CommandLauncherProps) { const [input, setInput] = useState(""); const { addWindow } = useGrimoire(); useEffect(() => { if (!open) { setInput(""); } }, [open]); // Parse input into command and arguments const parseInput = (value: string) => { const parts = value.trim().split(/\s+/); const commandName = parts[0]?.toLowerCase() || ""; const args = parts.slice(1); return { commandName, args, fullInput: value }; }; const { commandName, args } = parseInput(input); const recognizedCommand = commandName && manPages[commandName]; // Filter commands by partial match on command name only const filteredCommands = Object.entries(manPages).filter(([name]) => name.toLowerCase().includes(commandName.toLowerCase()), ); // Execute command (async to support async argParsers) const executeCommand = async () => { if (!recognizedCommand) return; const command = recognizedCommand; // Use argParser if available, otherwise use defaultProps // argParser can now be async const props = command.argParser ? await Promise.resolve(command.argParser(args)) : command.defaultProps || {}; // Generate title const title = args.length > 0 ? `${commandName.toUpperCase()} ${args.join(" ")}` : commandName.toUpperCase(); // Execute command addWindow(command.appId, props, title); onOpenChange(false); }; // Handle item selection (populate input, don't execute) const handleSelect = (selectedCommand: string) => { setInput(selectedCommand + " "); }; // Handle Enter key const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); executeCommand(); } }; // Define category order: Nostr first, then Documentation, then System const categoryOrder = ["Nostr", "Documentation", "System"]; const categories = Array.from( new Set(filteredCommands.map(([_, cmd]) => cmd.category)), ).sort((a, b) => { const indexA = categoryOrder.indexOf(a); const indexB = categoryOrder.indexOf(b); return (indexA === -1 ? 999 : indexA) - (indexB === -1 ? 999 : indexB); }); // Dynamic placeholder const placeholder = recognizedCommand ? recognizedCommand.synopsis : "Type a command..."; return (
{recognizedCommand && args.length > 0 && (
Parsed: {commandName} {args.join(" ")}
)} {commandName ? `No command found: ${commandName}` : "Start typing..."} {categories.map((category) => ( {filteredCommands .filter(([_, cmd]) => cmd.category === category) .map(([name, cmd]) => { const isExactMatch = name === commandName; return ( handleSelect(name)} className="command-item" data-exact-match={isExactMatch} >
{name} {cmd.synopsis !== name && ( {cmd.synopsis.replace(name, "").trim()} )} {isExactMatch && ( )}
{cmd.description.split(".")[0]}
); })}
))}
↑↓ navigate execute esc close
{recognizedCommand && (
Ready to execute
)}
); }