""" commands/help_command.py ======================== Custom help command for the Discord ComfyUI bot. Replaces discord.py's default help with a categorised listing that automatically includes every registered command. How it works ------------ Each ``@bot.command()`` decorator should carry an ``extras`` dict with a ``"category"`` key: @bot.command(name="my-command", extras={"category": "Generation"}) async def my_command(ctx): \"""One-line brief shown in the listing. Longer description shown in ttr!help my-command. \""" The first line of the docstring becomes the brief shown in the main listing. The full docstring is shown when the user asks for per-command detail. Commands without a category appear under **Other**. Usage ----- ttr!help — list all commands grouped by category ttr!help — detailed help for a specific command """ from __future__ import annotations from collections import defaultdict from typing import List, Mapping, Optional from discord.ext import commands # Order in which categories appear in the full help listing. # Any category not listed here appears at the end, sorted alphabetically. CATEGORY_ORDER = ["Generation", "Workflow", "Upload", "History", "Presets", "Utility"] def _category_sort_key(name: str) -> tuple: """Return a sort key that respects CATEGORY_ORDER, then alphabetical.""" try: return (CATEGORY_ORDER.index(name), name) except ValueError: return (len(CATEGORY_ORDER), name) class CustomHelpCommand(commands.HelpCommand): """ Categorised help command. Groups commands by the ``"category"`` value in their ``extras`` dict. Commands that omit this appear under **Other**. Adding a new command to the help output requires no changes here — just set ``extras={"category": "..."}`` on the decorator and write a descriptive docstring. """ # ------------------------------------------------------------------ # Main listing — ttr!help # ------------------------------------------------------------------ async def send_bot_help( self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]], ) -> None: """Send the full command listing grouped by category.""" # Collect all visible commands across every cog / None bucket all_commands: List[commands.Command] = [] for cmds in mapping.values(): filtered = await self.filter_commands(cmds) all_commands.extend(filtered) # Group by category categories: dict[str, list[commands.Command]] = defaultdict(list) for cmd in all_commands: cat = cmd.extras.get("category", "Other") categories[cat].append(cmd) prefix = self.context.prefix lines: list[str] = [f"**Commands** — prefix: `{prefix}`\n"] for cat in sorted(categories.keys(), key=_category_sort_key): cmds = sorted(categories[cat], key=lambda c: c.name) lines.append(f"**{cat}**") for cmd in cmds: aliases = ( f" ({', '.join(cmd.aliases)})" if cmd.aliases else "" ) brief = cmd.short_doc or "No description." lines.append(f" `{cmd.name}`{aliases} — {brief}") lines.append("") lines.append( f"Use `{prefix}help ` for details on a specific command." ) await self.get_destination().send("\n".join(lines)) # ------------------------------------------------------------------ # Per-command detail — ttr!help # ------------------------------------------------------------------ async def send_command_help(self, command: commands.Command) -> None: """Send detailed help for a single command.""" prefix = self.context.prefix header = f"`{prefix}{command.name}`" if command.aliases: alias_list = ", ".join(f"`{a}`" for a in command.aliases) header += f" (aliases: {alias_list})" category = command.extras.get("category", "Other") lines: list[str] = [header, f"Category: **{category}**", ""] if command.help: lines.append(command.help.strip()) else: lines.append("No description available.") await self.get_destination().send("\n".join(lines)) # ------------------------------------------------------------------ # Error — unknown command name # ------------------------------------------------------------------ async def send_error_message(self, error: str) -> None: """Forward the error text to the channel.""" await self.get_destination().send(error)