diff --git a/disagreement/ext/commands/help.py b/disagreement/ext/commands/help.py index 61c69d1..516f5ce 100644 --- a/disagreement/ext/commands/help.py +++ b/disagreement/ext/commands/help.py @@ -1,6 +1,8 @@ +from collections import defaultdict from typing import List, Optional -from .core import Command, CommandContext, CommandHandler +from ...utils import Paginator +from .core import Command, CommandContext, CommandHandler, Group class HelpCommand(Command): @@ -15,17 +17,12 @@ class HelpCommand(Command): if not cmd or cmd.name.lower() != command.lower(): await ctx.send(f"Command '{command}' not found.") return - description = cmd.description or cmd.brief or "No description provided." - await ctx.send(f"**{ctx.prefix}{cmd.name}**\n{description}") - else: - lines: List[str] = [] - for registered in dict.fromkeys(handler.commands.values()): - brief = registered.brief or registered.description or "" - lines.append(f"{ctx.prefix}{registered.name} - {brief}".strip()) - if lines: - await ctx.send("\n".join(lines)) + if isinstance(cmd, Group): + await self.send_group_help(ctx, cmd) else: - await ctx.send("No commands available.") + await self.send_command_help(ctx, cmd) + else: + await self.send_bot_help(ctx) super().__init__( callback, @@ -33,3 +30,42 @@ class HelpCommand(Command): brief="Show command help.", description="Displays help for commands.", ) + + async def send_bot_help(self, ctx: CommandContext) -> None: + groups = defaultdict(list) + for cmd in dict.fromkeys(self.handler.commands.values()): + key = cmd.cog.cog_name if cmd.cog else "No Category" + groups[key].append(cmd) + + paginator = Paginator() + for cog_name, cmds in groups.items(): + paginator.add_line(f"**{cog_name}**") + for cmd in cmds: + brief = cmd.brief or cmd.description or "" + paginator.add_line(f"{ctx.prefix}{cmd.name} - {brief}".strip()) + paginator.add_line("") + + pages = paginator.pages + if not pages: + await ctx.send("No commands available.") + return + for page in pages: + await ctx.send(page) + + async def send_command_help(self, ctx: CommandContext, command: Command) -> None: + description = command.description or command.brief or "No description provided." + await ctx.send(f"**{ctx.prefix}{command.name}**\n{description}") + + async def send_group_help(self, ctx: CommandContext, group: Group) -> None: + paginator = Paginator() + description = group.description or group.brief or "No description provided." + paginator.add_line(f"**{ctx.prefix}{group.name}**\n{description}") + if group.commands: + for sub in dict.fromkeys(group.commands.values()): + brief = sub.brief or sub.description or "" + paginator.add_line( + f"{ctx.prefix}{group.name} {sub.name} - {brief}".strip() + ) + + for page in paginator.pages: + await ctx.send(page) diff --git a/docs/commands.md b/docs/commands.md index ef312ec..68d1bd7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -11,7 +11,11 @@ The command handler registers a `help` command automatically. Use it to list all !help ping # shows help for the "ping" command ``` -The help command will show each command's brief description if provided. +Commands are grouped by their Cog name and paginated so that long help +lists are split into multiple messages using the `Paginator` utility. + +If you need custom formatting you can subclass +`HelpCommand` and override `send_command_help` or `send_group_help`. ## Checks diff --git a/tests/test_help_command.py b/tests/test_help_command.py index 23a2c7a..6e1e30b 100644 --- a/tests/test_help_command.py +++ b/tests/test_help_command.py @@ -1,6 +1,7 @@ import pytest -from disagreement.ext.commands.core import CommandHandler, Command +from disagreement.ext import commands +from disagreement.ext.commands.core import CommandHandler, Command, Group from disagreement.models import Message @@ -13,15 +14,21 @@ class DummyBot: return {"id": "1", "channel_id": channel_id, "content": content} +class MyCog(commands.Cog): + def __init__(self, client) -> None: + super().__init__(client) + + @commands.command() + async def foo(self, ctx: commands.CommandContext) -> None: + pass + + @pytest.mark.asyncio async def test_help_lists_commands(): bot = DummyBot() handler = CommandHandler(client=bot, prefix="!") - async def foo(ctx): - pass - - handler.add_command(Command(foo, name="foo", brief="Foo cmd")) + handler.add_cog(MyCog(bot)) msg_data = { "id": "1", @@ -33,6 +40,7 @@ async def test_help_lists_commands(): msg = Message(msg_data, client_instance=bot) await handler.process_commands(msg) assert any("foo" in m for m in bot.sent) + assert any("MyCog" in m for m in bot.sent) @pytest.mark.asyncio @@ -55,3 +63,39 @@ async def test_help_specific_command(): msg = Message(msg_data, client_instance=bot) await handler.process_commands(msg) assert any("Bar desc" in m for m in bot.sent) + + +from disagreement.ext.commands.help import HelpCommand + + +class CustomHelp(HelpCommand): + async def send_command_help(self, ctx, command): + await ctx.send(f"custom {command.name}") + + async def send_group_help(self, ctx, group): + await ctx.send(f"group {group.name}") + + +@pytest.mark.asyncio +async def test_custom_help_methods(): + bot = DummyBot() + handler = CommandHandler(client=bot, prefix="!") + handler.remove_command("help") + handler.add_command(CustomHelp(handler)) + + async def sub(ctx): + pass + + group = Group(sub, name="grp") + handler.add_command(group) + + msg_data = { + "id": "1", + "channel_id": "c", + "author": {"id": "2", "username": "u", "discriminator": "0001"}, + "content": "!help grp", + "timestamp": "t", + } + msg = Message(msg_data, client_instance=bot) + await handler.process_commands(msg) + assert any("group grp" in m for m in bot.sent)