import { Combobox, Dialog, Transition } from '@headlessui/react' import { Dispatch, Fragment, SetStateAction, createContext, useState, } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { ActionIcon } from './ActionIcon' import { faSearch } from '@fortawesome/free-solid-svg-icons' import Fuse from 'fuse.js' import { Command, SubCommand } from '../lib/commands' import { useCommandsContext } from 'hooks/useCommandsContext' export type SortedCommand = { item: Partial & { name: string } } export const CommandsContext = createContext( {} as { commands: Command[] addCommands: (commands: Command[]) => void removeCommands: (commands: Command[]) => void commandBarOpen: boolean setCommandBarOpen: Dispatch> } ) export const CommandBarProvider = ({ children, }: { children: React.ReactNode }) => { const [commands, internalSetCommands] = useState([] as Command[]) const [commandBarOpen, setCommandBarOpen] = useState(false) const addCommands = (newCommands: Command[]) => { internalSetCommands((prevCommands) => [...newCommands, ...prevCommands]) } const removeCommands = (newCommands: Command[]) => { internalSetCommands((prevCommands) => prevCommands.filter((command) => !newCommands.includes(command)) ) } return ( {children} ) } const CommandBar = () => { const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext() useHotkeys(['meta+k', 'meta+/'], () => { if (commands.length === 0) return setCommandBarOpen(!commandBarOpen) }) const [selectedCommand, setSelectedCommand] = useState( null ) // keep track of the current subcommand index const [subCommandIndex, setSubCommandIndex] = useState() const [subCommandData, setSubCommandData] = useState<{ [key: string]: string }>({}) // if the subcommand index is null, we're not in a subcommand const inSubCommand = selectedCommand && 'meta' in selectedCommand.item && selectedCommand.item.meta?.args !== undefined && subCommandIndex !== undefined const currentSubCommand = inSubCommand && 'meta' in selectedCommand.item ? selectedCommand.item.meta?.args[subCommandIndex] : undefined const [query, setQuery] = useState('') const availableCommands = inSubCommand && currentSubCommand ? currentSubCommand.type === 'string' ? query ? [{ name: query }] : currentSubCommand.options : currentSubCommand.options : commands const fuse = new Fuse(availableCommands || [], { keys: ['name', 'description'], }) const filteredCommands = query ? fuse.search(query) : availableCommands?.map((c) => ({ item: c } as SortedCommand)) function clearState() { setQuery('') setCommandBarOpen(false) setSelectedCommand(null) setSubCommandIndex(undefined) setSubCommandData({}) } function handleCommandSelection(entry: SortedCommand) { // If we have subcommands and have not yet gathered all the // data required from them, set the selected command to the // current command and increment the subcommand index if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) { setSelectedCommand(entry) setSubCommandIndex(0) setQuery('') return } const { item } = entry // If we have just selected a command with no subcommands, run it const isCommandWithoutSubcommands = 'callback' in item && !('meta' in item && item.meta) if (isCommandWithoutSubcommands) { if (item.callback === undefined) return item.callback() setCommandBarOpen(false) return } // If we have subcommands and have not yet gathered all the // data required from them, set the selected command to the // current command and increment the subcommand index if ( selectedCommand && subCommandIndex !== undefined && 'meta' in selectedCommand.item ) { const subCommand = selectedCommand.item.meta?.args[subCommandIndex] if (subCommand) { const newSubCommandData = { ...subCommandData, [subCommand.name]: item.name, } const newSubCommandIndex = subCommandIndex + 1 // If we have subcommands and have gathered all the data required // from them, run the command with the gathered data if ( selectedCommand.item.callback && selectedCommand.item.meta?.args.length === newSubCommandIndex ) { selectedCommand.item.callback(newSubCommandData) setCommandBarOpen(false) } else { // Otherwise, set the subcommand data and increment the subcommand index setSubCommandData(newSubCommandData) setSubCommandIndex(newSubCommandIndex) setQuery('') } } } } function getDisplayValue(command: Command) { if (command.meta?.displayValue === undefined || !command.meta.args) return command.name return command.meta?.displayValue( command.meta.args.map((c) => subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>` ) ) } return ( 0 } as={Fragment} afterLeave={() => clearState()} > { setCommandBarOpen(false) clearState() }} className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]" >
{inSubCommand && (

{selectedCommand.item && getDisplayValue(selectedCommand.item as Command)}

)} setQuery(event.target.value)} className="w-full bg-transparent focus:outline-none" onKeyDown={(event) => { if (event.metaKey && event.key === 'k') setCommandBarOpen(false) if ( inSubCommand && event.key === 'Backspace' && !event.currentTarget.value ) { setSubCommandIndex(subCommandIndex - 1) setSelectedCommand(null) } }} displayValue={(command: SortedCommand) => command !== null ? command.item.name : '' } placeholder={ inSubCommand ? `Enter <${currentSubCommand?.name}>` : 'Search for a command' } value={query} autoCapitalize="off" autoComplete="off" autoCorrect="off" spellCheck="false" />
{filteredCommands?.map((commandResult) => (

{commandResult.item.name}

{(commandResult.item as SubCommand).description && (

{(commandResult.item as SubCommand).description}

)}
))}
) } export default CommandBarProvider