diff --git a/newhelpmenu/formatter.py b/newhelpmenu/formatter.py index b808d65..108f44b 100644 --- a/newhelpmenu/formatter.py +++ b/newhelpmenu/formatter.py @@ -11,6 +11,7 @@ from __future__ import annotations import asyncio import logging +import math from collections import defaultdict from typing import Any, Dict, List, Optional, Sequence, Tuple, Union @@ -23,8 +24,8 @@ from redbot.core.utils.chat_formatting import humanize_list, pagify log = logging.getLogger("red.evecogs.newhelpmenu.formatter") -MAX_FIELDS_PER_PAGE = 8 -EMBED_CHAR_LIMIT = 5800 # Leave margin under 6000 +MAX_FIELDS_PER_PAGE = 6 # Keep pages clean, not cramped +EMBED_CHAR_LIMIT = 5800 # ══════════════════════════════════════════════════════════════════ @@ -52,7 +53,6 @@ async def gather_bot_help_data( blacklisted_commands = blacklisted_commands or [] prefix = ctx.clean_prefix - # Gather all visible commands grouped by cog cog_commands: Dict[str, List[commands.Command]] = defaultdict(list) for cmd in sorted(bot.commands, key=lambda c: c.qualified_name): @@ -71,19 +71,17 @@ async def gather_bot_help_data( continue cog_commands[cog_name].append(cmd) - # Map cogs → categories cog_to_category: Dict[str, str] = {} for cat_name, cog_list in categories.items(): for cog_name in cog_list: cog_to_category[cog_name] = cat_name - # Group by category category_data: Dict[str, List[Tuple[str, str]]] = defaultdict(list) total = 0 for cog_name, cmds in cog_commands.items(): cat = cog_to_category.get(cog_name, cog_name) for cmd in cmds: - short = cmd.format_shortdoc_for_context(ctx) if hasattr(cmd, 'format_shortdoc_for_context') else (cmd.short_doc or "No description") + short = cmd.format_shortdoc_for_context(ctx) if hasattr(cmd, "format_shortdoc_for_context") else (cmd.short_doc or "No description") category_data[cat].append((f"{prefix}{cmd.qualified_name}", short)) total += 1 @@ -110,7 +108,7 @@ async def gather_cog_help_data( prefix = ctx.clean_prefix cog_name = cog.qualified_name cog_doc = "" - if hasattr(cog, 'format_help_for_context'): + if hasattr(cog, "format_help_for_context"): cog_doc = cog.format_help_for_context(ctx) if not cog_doc: cog_doc = cog.help or cog.__doc__ or "No description." @@ -125,7 +123,7 @@ async def gather_cog_help_data( except Exception: continue sig = cmd.signature or "" - short = cmd.format_shortdoc_for_context(ctx) if hasattr(cmd, 'format_shortdoc_for_context') else (cmd.short_doc or "No description") + short = cmd.format_shortdoc_for_context(ctx) if hasattr(cmd, "format_shortdoc_for_context") else (cmd.short_doc or "No description") cmds.append((f"{prefix}{cmd.qualified_name}", sig, short)) return cog_name, cog_doc, cmds @@ -135,10 +133,7 @@ async def gather_command_help_data( ctx: commands.Context, cmd: commands.Command, ) -> Dict[str, Any]: - """Gather help data for a single command. - - Returns dict with: name, signature, description, aliases, subcommands, cooldown - """ + """Gather help data for a single command.""" prefix = ctx.clean_prefix parent_sig = "" parent = cmd.parent @@ -155,7 +150,7 @@ async def gather_command_help_data( signature = f"{prefix}{parent_sig}{cmd.name} {cmd.signature}".strip() help_text = "" - if hasattr(cmd, 'format_help_for_context'): + if hasattr(cmd, "format_help_for_context"): help_text = cmd.format_help_for_context(ctx) if not help_text: help_text = cmd.help or cmd.description or cmd.short_doc or "No description provided." @@ -177,7 +172,7 @@ async def gather_command_help_data( for sub in sorted(cmd.commands, key=lambda c: c.name): if sub.hidden: continue - sub_short = sub.format_shortdoc_for_context(ctx) if hasattr(sub, 'format_shortdoc_for_context') else (sub.short_doc or "No description") + sub_short = sub.format_shortdoc_for_context(ctx) if hasattr(sub, "format_shortdoc_for_context") else (sub.short_doc or "No description") data["subcommands"].append((f"{prefix}{sub.qualified_name}", sub_short)) if cmd.cooldown: @@ -188,7 +183,7 @@ async def gather_command_help_data( # ══════════════════════════════════════════════════════════════════ -# EMBED MODE — Rich embeds with discord.ui.View buttons/selects +# EMBED MODE — Polished embeds with discord.ui.View # ══════════════════════════════════════════════════════════════════ @@ -209,10 +204,10 @@ class EmbedHelpView(discord.ui.View): self.author_id = author_id self.message: Optional[discord.Message] = None - # Add category select if we have categories + # Category select (row 0) if category_options and len(category_options) > 1: select = discord.ui.Select( - placeholder="Jump to category…", + placeholder="⏩ Jump to category…", options=category_options[:25], custom_id="help_cat_select", row=0, @@ -220,17 +215,17 @@ class EmbedHelpView(discord.ui.View): select.callback = self._select_callback self.add_item(select) - # Navigation buttons - self.first_btn = discord.ui.Button(emoji="⏮️", style=discord.ButtonStyle.secondary, row=1) + # Navigation buttons (row 1) + self.first_btn = discord.ui.Button(emoji="⏮", style=discord.ButtonStyle.secondary, row=1, disabled=True) self.first_btn.callback = self._first self.add_item(self.first_btn) - self.prev_btn = discord.ui.Button(emoji="◀️", style=discord.ButtonStyle.primary, row=1) + self.prev_btn = discord.ui.Button(emoji="◀️", style=discord.ButtonStyle.primary, row=1, disabled=True) self.prev_btn.callback = self._prev self.add_item(self.prev_btn) self.indicator = discord.ui.Button( - label=f"1/{len(pages)}", style=discord.ButtonStyle.secondary, disabled=True, row=1 + label=f"Page 1 of {len(pages)}", style=discord.ButtonStyle.secondary, disabled=True, row=1 ) self.add_item(self.indicator) @@ -238,13 +233,12 @@ class EmbedHelpView(discord.ui.View): self.next_btn.callback = self._next self.add_item(self.next_btn) - self.last_btn = discord.ui.Button(emoji="⏭️", style=discord.ButtonStyle.secondary, row=1) + self.last_btn = discord.ui.Button(emoji="⏭", style=discord.ButtonStyle.secondary, row=1) self.last_btn.callback = self._last self.add_item(self.last_btn) - self.close_btn = discord.ui.Button( - label="Close", emoji="🗑️", style=discord.ButtonStyle.danger, row=2 - ) + # Close button (row 2) + self.close_btn = discord.ui.Button(label="Close", emoji="✖️", style=discord.ButtonStyle.danger, row=2) self.close_btn.callback = self._close self.add_item(self.close_btn) @@ -257,7 +251,7 @@ class EmbedHelpView(discord.ui.View): self.prev_btn.disabled = at_start self.next_btn.disabled = at_end self.last_btn.disabled = at_end - self.indicator.label = f"{self.current_page + 1}/{len(self.pages)}" + self.indicator.label = f"Page {self.current_page + 1} of {len(self.pages)}" async def _go_to(self, interaction: discord.Interaction, page: int): self.current_page = max(0, min(page, len(self.pages) - 1)) @@ -310,6 +304,22 @@ class EmbedHelpView(discord.ui.View): pass +# ─── Embed builders ─────────────────────────────────────────────── + + +def _cmd_table(cmds: List[Tuple[str, str]], *, prefix: str = "") -> str: + """Format commands as a clean aligned table inside a description.""" + lines = [] + for cmd_name, short in cmds: + # Strip prefix from display name for cleaner look + display = cmd_name + if prefix and display.startswith(prefix): + display = display[len(prefix):] + desc = short[:50] if short else "No description" + lines.append(f"` {display} ` · {desc}") + return "\n".join(lines) + + def build_bot_help_embeds( ctx, bot, @@ -318,80 +328,101 @@ def build_bot_help_embeds( *, accent_color: int, ) -> Tuple[List[discord.Embed], List[discord.SelectOption]]: - """Build embed pages for the full bot help. - - Returns (embeds, select_options). - """ + """Build polished embed pages for the full bot help.""" prefix = ctx.clean_prefix bot_name = bot.user.display_name if bot.user else "Bot" bot_avatar = bot.user.display_avatar.url if bot.user else None color = discord.Colour(accent_color) + total_pages_estimate = 1 + sum(max(1, math.ceil(len(cmds) / MAX_FIELDS_PER_PAGE)) for _, _, cmds in category_data) pages: List[discord.Embed] = [] select_options: List[discord.SelectOption] = [] - # Page 0: Overview - overview = discord.Embed( - title=f"{bot_name} — Help", - description=( - f"Use `{prefix}help ` for details on a command.\n" - f"Use `{prefix}help ` for a category overview.\n\n" - f"**{len(category_data)} categories · {total_commands} commands**" - ), - color=color, - ) + # ── Page 0: Overview ───────────────────────────────── + overview = discord.Embed(color=color) + overview.set_author(name=f"{bot_name} · Help Menu", icon_url=bot_avatar) + + desc_lines = [ + f"Welcome! I have **{total_commands}** commands across **{len(category_data)}** categories.", + "", + f"▸ `{prefix}help ` — details on a command", + f"▸ `{prefix}help ` — browse a category", + "", + "**╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍**", + ] + overview.description = "\n".join(desc_lines) + if bot_avatar: overview.set_thumbnail(url=bot_avatar) + # Show categories as compact inline fields for i, (cat_name, emoji, cmds) in enumerate(category_data): - if i >= 20: # Discord max 25 fields; leave room + if i >= 18: # Stay well under 25 overview.add_field( - name="…and more", - value=f"Use the category select or `{prefix}help ` to see all.", + name=f"… +{len(category_data) - 18} more", + value=f"Use the dropdown to browse.", inline=False, ) break - cmd_names = ", ".join(f"`{name.split()[-1]}`" for name, _ in cmds[:12]) - if len(cmds) > 12: - cmd_names += f" *+{len(cmds) - 12} more*" + cmd_preview = ", ".join(f"`{n.split()[-1]}`" for n, _ in cmds[:6]) + if len(cmds) > 6: + cmd_preview += f" +{len(cmds) - 6}" overview.add_field( - name=f"{emoji} {cat_name} ({len(cmds)})", - value=cmd_names or "No commands", - inline=False, + name=f"{emoji} {cat_name} · {len(cmds)}", + value=cmd_preview or "*No commands*", + inline=True, ) - overview.set_footer(text=f"Type {prefix}help for more info on a command.") + # Pad with empty inline field if odd number for cleaner grid + field_count = min(len(category_data), 18) + if field_count % 2 == 1: + overview.add_field(name="\u200b", value="\u200b", inline=True) + + overview.set_footer(text=f"Page 1 of {total_pages_estimate} · Use the buttons below to navigate") pages.append(overview) - # Category pages - for idx, (cat_name, emoji, cmds) in enumerate(category_data): + # ── Category pages ─────────────────────────────────── + page_idx = 1 + for cat_i, (cat_name, emoji, cmds) in enumerate(category_data): select_options.append(discord.SelectOption( label=cat_name[:100], - value=str(idx + 1), + value=str(page_idx), emoji=emoji if len(emoji) <= 2 else None, - description=f"{len(cmds)} commands", + description=f"{len(cmds)} command{'s' if len(cmds) != 1 else ''}", )) - # Split into multiple pages if many commands - for chunk_start in range(0, len(cmds), MAX_FIELDS_PER_PAGE): - chunk = cmds[chunk_start:chunk_start + MAX_FIELDS_PER_PAGE] - embed = discord.Embed( - title=f"{emoji} {cat_name}", - color=color, - ) - if bot_avatar: - embed.set_thumbnail(url=bot_avatar) + chunks = [cmds[i:i + MAX_FIELDS_PER_PAGE] for i in range(0, len(cmds), MAX_FIELDS_PER_PAGE)] + for chunk_i, chunk in enumerate(chunks): + embed = discord.Embed(color=color) + title_suffix = f" ({chunk_i + 1}/{len(chunks)})" if len(chunks) > 1 else "" + embed.set_author(name=f"{emoji} {cat_name}{title_suffix}", icon_url=bot_avatar) + + # Build clean command list + cmd_lines = [] for cmd_name, short in chunk: - # Use inline=False for readability - embed.add_field( - name=cmd_name, - value=short[:100] if short else "No description", - inline=True, - ) + display = cmd_name + if prefix and display.startswith(prefix): + display = display[len(prefix):] + desc = short[:70] if short else "No description" + cmd_lines.append(f"**`{display}`**\n╰ {desc}") - embed.set_footer(text=f"Type {prefix}help for more details.") + embed.description = "\n\n".join(cmd_lines) + + embed.set_footer(text=f"Page {page_idx + 1} of {total_pages_estimate} · {prefix}help for details") pages.append(embed) + page_idx += 1 + + # Fix page counts now that we know the real total + real_total = len(pages) + for i, page in enumerate(pages): + if page.footer and page.footer.text: + old_footer = page.footer.text + # Replace estimated page count with real one + import re + new_footer = re.sub(r"of \d+", f"of {real_total}", old_footer) + new_footer = re.sub(r"Page \d+", f"Page {i + 1}", new_footer) + page.set_footer(text=new_footer) return pages, select_options @@ -402,42 +433,54 @@ def build_command_help_embed( *, accent_color: int, ) -> discord.Embed: - """Build an embed for a single command.""" + """Build a polished embed for a single command.""" prefix = ctx.clean_prefix bot_avatar = ctx.bot.user.display_avatar.url if ctx.bot.user else None color = discord.Colour(accent_color) - title = f"{'📁' if data['is_group'] else '🔹'} {data['name']}" - embed = discord.Embed(title=title, color=color) - if bot_avatar: - embed.set_thumbnail(url=bot_avatar) + icon = "📁" if data["is_group"] else "🔹" + embed = discord.Embed(color=color) + embed.set_author(name=f"{icon} {prefix}{data['name']}", icon_url=bot_avatar) - # Syntax - embed.add_field(name="Syntax", value=f"```\n{data['signature']}\n```", inline=False) + # Syntax block + embed.description = f"```yaml\n{data['signature']}\n```" # Description desc = data["description"] if len(desc) > 1024: - desc = desc[:1021] + "..." - embed.add_field(name="Description", value=desc, inline=False) + desc = desc[:1021] + "…" + embed.add_field(name="📝 Description", value=desc, inline=False) # Aliases if data["aliases"]: - alias_text = ", ".join(f"`{prefix}{data['name'].rsplit(' ', 1)[0] + ' ' if ' ' in data['name'] else ''}{a}`" for a in data["aliases"][:10]) - embed.add_field(name="Aliases", value=alias_text, inline=False) + alias_parts = [] + for a in data["aliases"][:10]: + # Build proper alias with parent chain + name_parts = data["name"].split() + if len(name_parts) > 1: + alias_parts.append(f"`{prefix}{' '.join(name_parts[:-1])} {a}`") + else: + alias_parts.append(f"`{prefix}{a}`") + embed.add_field(name="🔀 Aliases", value=" ".join(alias_parts), inline=True) # Cooldown if data["cooldown"]: - embed.add_field(name="Cooldown", value=data["cooldown"], inline=True) + embed.add_field(name="⏱️ Cooldown", value=data["cooldown"], inline=True) # Subcommands if data["subcommands"]: - sub_text = "\n".join(f"`{name}` — {short[:60]}" for name, short in data["subcommands"]) + sub_lines = [] + for name, short in data["subcommands"]: + display = name + if prefix and display.startswith(prefix): + display = display[len(prefix):] + sub_lines.append(f"**`{display}`** · {short[:50]}") + sub_text = "\n".join(sub_lines) if len(sub_text) > 1024: - sub_text = sub_text[:1021] + "..." - embed.add_field(name="Subcommands", value=sub_text, inline=False) + sub_text = sub_text[:1021] + "…" + embed.add_field(name=f"📁 Subcommands ({len(data['subcommands'])})", value=sub_text, inline=False) - embed.set_footer(text=f"Type {prefix}help for more details.") + embed.set_footer(text=f" [optional] · {prefix}help for main menu") return embed @@ -449,25 +492,30 @@ def build_cog_help_embed( *, accent_color: int, ) -> discord.Embed: - """Build an embed for a cog's help page.""" + """Build a polished embed for a cog's help page.""" prefix = ctx.clean_prefix bot_avatar = ctx.bot.user.display_avatar.url if ctx.bot.user else None color = discord.Colour(accent_color) - embed = discord.Embed(title=f"📂 {cog_name}", description=cog_doc[:2048], color=color) - if bot_avatar: - embed.set_thumbnail(url=bot_avatar) + embed = discord.Embed(color=color) + embed.set_author(name=f"📂 {cog_name}", icon_url=bot_avatar) + + if cog_doc: + embed.description = cog_doc[:2048] if cmds: cmd_lines = [] for qname, sig, short in cmds: - cmd_lines.append(f"`{qname}` — {short[:80]}") + display = qname + if prefix and display.startswith(prefix): + display = display[len(prefix):] + cmd_lines.append(f"**`{display}`** · {short[:60]}") cmd_text = "\n".join(cmd_lines) for i, chunk in enumerate(pagify(cmd_text, delims=["\n"], page_length=1024)): - name = "Commands" if i == 0 else "Commands (continued)" + name = f"🔹 Commands ({len(cmds)})" if i == 0 else "ㅤ" embed.add_field(name=name, value=chunk, inline=False) - embed.set_footer(text=f"Type {prefix}help for more details.") + embed.set_footer(text=f"{prefix}help for details · {prefix}help for main menu") return embed @@ -482,19 +530,23 @@ def _make_category_container( *, accent_color: int = 0x5865F2, emoji: str = "📂", + prefix: str = "", ) -> ui.Container: - """Build a CV2 container for a category.""" + """Build a polished CV2 container for a category.""" container = ui.Container(accent_colour=discord.Colour(accent_color)) container.add_item(ui.TextDisplay(f"### {emoji} {category_name}")) container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) lines = [] for cmd_name, cmd_short in commands_list: - short = cmd_short[:80] if cmd_short else "No description" - lines.append(f"`{cmd_name}` — {short}") + display = cmd_name + if prefix and display.startswith(prefix): + display = display[len(prefix):] + short = cmd_short[:70] if cmd_short else "No description" + lines.append(f"**`{display}`**\n╰ {short}") - text = "\n".join(lines) - for chunk in pagify(text, delims=["\n"], page_length=3900): + text = "\n\n".join(lines) + for chunk in pagify(text, delims=["\n\n", "\n"], page_length=3900): container.add_item(ui.TextDisplay(chunk)) return container @@ -526,21 +578,24 @@ class HelpPaginatorView(ui.LayoutView): if len(self.pages) > 1: self.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + nav_row = ui.ActionRow() - nav_row.add_item(ui.Button(emoji="⏮️", style=discord.ButtonStyle.secondary, custom_id="help_first", disabled=self.current_page == 0)) + nav_row.add_item(ui.Button(emoji="⏮", style=discord.ButtonStyle.secondary, custom_id="help_first", disabled=self.current_page == 0)) nav_row.add_item(ui.Button(emoji="◀️", style=discord.ButtonStyle.primary, custom_id="help_prev", disabled=self.current_page == 0)) - nav_row.add_item(ui.Button(label=f"{self.current_page + 1}/{len(self.pages)}", style=discord.ButtonStyle.secondary, custom_id="help_page_ind", disabled=True)) + nav_row.add_item(ui.Button(label=f"Page {self.current_page + 1} of {len(self.pages)}", style=discord.ButtonStyle.secondary, custom_id="help_page_ind", disabled=True)) nav_row.add_item(ui.Button(emoji="▶️", style=discord.ButtonStyle.primary, custom_id="help_next", disabled=self.current_page >= len(self.pages) - 1)) - nav_row.add_item(ui.Button(emoji="⏭️", style=discord.ButtonStyle.secondary, custom_id="help_last", disabled=self.current_page >= len(self.pages) - 1)) + nav_row.add_item(ui.Button(emoji="⏭", style=discord.ButtonStyle.secondary, custom_id="help_last", disabled=self.current_page >= len(self.pages) - 1)) self.add_item(nav_row) + # Close in its own row close_row = ui.ActionRow() - close_row.add_item(ui.Button(label="Close", emoji="🗑️", style=discord.ButtonStyle.danger, custom_id="help_close")) + close_row.add_item(ui.Button(label="Close", emoji="✖️", style=discord.ButtonStyle.danger, custom_id="help_close")) self.add_item(close_row) + # Select in its own row (ActionRow can't mix buttons + selects) if self.category_options and len(self.category_options) > 1: select_row = ui.ActionRow() - select_row.add_item(ui.Select(placeholder="Jump to category…", options=self.category_options[:25], custom_id="help_category_select")) + select_row.add_item(ui.Select(placeholder="⏩ Jump to category…", options=self.category_options[:25], custom_id="help_category_select")) self.add_item(select_row) async def interaction_check(self, interaction: discord.Interaction) -> bool: @@ -603,7 +658,7 @@ def build_cv2_bot_help_pages( *, accent_color: int, ) -> Tuple[List[List[Any]], List[discord.SelectOption]]: - """Build CV2 pages for the full bot help.""" + """Build polished CV2 pages for the full bot help.""" prefix = ctx.clean_prefix bot_name = bot.user.display_name if bot.user else "Bot" bot_avatar = bot.user.display_avatar.url if bot.user else None @@ -612,39 +667,48 @@ def build_cv2_bot_help_pages( pages: List[List[Any]] = [] select_options: List[discord.SelectOption] = [] - # Overview page - overview_components: List[Any] = [] + # ── Overview page ───────────────────────────── + overview: List[Any] = [] + header = ui.Container(accent_colour=color) - header_md = f"## {bot_name} — Help\nUse `{prefix}help ` for details.\n\n**{len(category_data)} categories · {total_commands} commands**" + header_md = ( + f"## {bot_name} · Help Menu\n" + f"**{total_commands}** commands across **{len(category_data)}** categories\n\n" + f"▸ `{prefix}help ` — details on a command\n" + f"▸ `{prefix}help ` — browse a category" + ) if bot_avatar: section = ui.Section(accessory=ui.Thumbnail(bot_avatar)) section.add_item(ui.TextDisplay(header_md)) header.add_item(section) else: header.add_item(ui.TextDisplay(header_md)) - overview_components.append(header) + overview.append(header) + # Categories summary — cap how many to avoid component limits + cats_to_show = category_data[:20] summary = ui.Container(accent_colour=color) - summary.add_item(ui.TextDisplay("### 📚 Categories")) + summary.add_item(ui.TextDisplay("### 📚 Categories")) summary.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) summary_lines = [] - for i, (cat_name, emoji, cmds) in enumerate(category_data): - summary_lines.append(f"{emoji} **{cat_name}** — {len(cmds)} command{'s' if len(cmds) != 1 else ''}") + for i, (cat_name, emoji, cmds) in enumerate(cats_to_show): + summary_lines.append(f"{emoji} **{cat_name}** — {len(cmds)} command{'s' if len(cmds) != 1 else ''}") select_options.append(discord.SelectOption( label=cat_name[:100], value=str(i + 1), emoji=emoji if len(emoji) <= 2 else None, description=f"{len(cmds)} commands", )) + if len(category_data) > 20: + summary_lines.append(f"… *+{len(category_data) - 20} more categories*") summary.add_item(ui.TextDisplay("\n".join(summary_lines))) - overview_components.append(summary) - pages.append(overview_components) + overview.append(summary) + pages.append(overview) - # Category pages + # ── Category pages ──────────────────────────── for cat_name, emoji, cmds in category_data: - for chunk_start in range(0, len(cmds), MAX_FIELDS_PER_PAGE): - chunk = cmds[chunk_start:chunk_start + MAX_FIELDS_PER_PAGE] - page_components: List[Any] = [_make_category_container(cat_name, chunk, accent_color=accent_color, emoji=emoji)] - pages.append(page_components) + chunks = [cmds[i:i + MAX_FIELDS_PER_PAGE] for i in range(0, len(cmds), MAX_FIELDS_PER_PAGE)] + for chunk in chunks: + pages.append([_make_category_container(cat_name, chunk, accent_color=accent_color, emoji=emoji, prefix=prefix)]) return pages, select_options @@ -655,29 +719,49 @@ def build_cv2_command_help( *, accent_color: int, ) -> List[Any]: - """Build CV2 components for a command.""" + """Build polished CV2 components for a command.""" prefix = ctx.clean_prefix color = discord.Colour(accent_color) container = ui.Container(accent_colour=color) icon = "📁" if data["is_group"] else "🔹" - container.add_item(ui.TextDisplay(f"## {icon} {data['name']}")) + container.add_item(ui.TextDisplay(f"## {icon} {prefix}{data['name']}")) container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) - container.add_item(ui.TextDisplay(f"**Syntax:** `{data['signature']}`")) - container.add_item(ui.TextDisplay(data["description"][:3900])) + # Syntax + container.add_item(ui.TextDisplay(f"```yaml\n{data['signature']}\n```")) + + # Description + container.add_item(ui.TextDisplay(f"📝 **Description**\n{data['description'][:3800]}")) + + # Aliases if data["aliases"]: - aliases = ", ".join(f"`{a}`" for a in data["aliases"][:10]) - container.add_item(ui.TextDisplay(f"**Aliases:** {aliases}")) + name_parts = data["name"].split() + alias_parts = [] + for a in data["aliases"][:10]: + if len(name_parts) > 1: + alias_parts.append(f"`{prefix}{' '.join(name_parts[:-1])} {a}`") + else: + alias_parts.append(f"`{prefix}{a}`") + container.add_item(ui.TextDisplay(f"🔀 **Aliases:** {' '.join(alias_parts)}")) + # Cooldown if data["cooldown"]: - container.add_item(ui.TextDisplay(f"**Cooldown:** {data['cooldown']}")) + container.add_item(ui.TextDisplay(f"⏱️ **Cooldown:** {data['cooldown']}")) + # Subcommands if data["subcommands"]: container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) - container.add_item(ui.TextDisplay("### Subcommands")) - sub_lines = [f"`{name}` — {short[:60]}" for name, short in data["subcommands"]] - container.add_item(ui.TextDisplay("\n".join(sub_lines))) + sub_lines = [] + for name, short in data["subcommands"]: + display = name + if prefix and display.startswith(prefix): + display = display[len(prefix):] + sub_lines.append(f"**`{display}`** · {short[:50]}") + container.add_item(ui.TextDisplay(f"### 📁 Subcommands ({len(data['subcommands'])})\n\n" + "\n".join(sub_lines))) + + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + container.add_item(ui.TextDisplay(f"-# [optional] · {prefix}help for main menu")) return [container] @@ -690,18 +774,20 @@ def build_cv2_cog_help( *, accent_color: int, ) -> List[Any]: - """Build CV2 components for a cog.""" + """Build polished CV2 components for a cog.""" + prefix = ctx.clean_prefix color = discord.Colour(accent_color) header = ui.Container(accent_colour=color) - header.add_item(ui.TextDisplay(f"## 📂 {cog_name}")) + header.add_item(ui.TextDisplay(f"## 📂 {cog_name}")) header.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) - header.add_item(ui.TextDisplay(cog_doc[:3900])) + if cog_doc: + header.add_item(ui.TextDisplay(cog_doc[:3900])) components: List[Any] = [header] if cmds: cmd_list = [(qname, short) for qname, _, short in cmds] - components.append(_make_category_container("Commands", cmd_list, accent_color=accent_color, emoji="🔹")) + components.append(_make_category_container("Commands", cmd_list, accent_color=accent_color, emoji="🔹", prefix=prefix)) return components @@ -743,9 +829,9 @@ class CV2MenuPaginator(ui.LayoutView): self.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) nav_row = ui.ActionRow() nav_row.add_item(ui.Button(emoji="◀️", style=discord.ButtonStyle.primary, custom_id="cv2menu_prev", disabled=self.current_page == 0)) - nav_row.add_item(ui.Button(label=f"{self.current_page + 1}/{len(self.raw_pages)}", style=discord.ButtonStyle.secondary, custom_id="cv2menu_ind", disabled=True)) + nav_row.add_item(ui.Button(label=f"Page {self.current_page + 1} of {len(self.raw_pages)}", style=discord.ButtonStyle.secondary, custom_id="cv2menu_ind", disabled=True)) nav_row.add_item(ui.Button(emoji="▶️", style=discord.ButtonStyle.primary, custom_id="cv2menu_next", disabled=self.current_page >= len(self.raw_pages) - 1)) - nav_row.add_item(ui.Button(label="Close", emoji="🗑️", style=discord.ButtonStyle.danger, custom_id="cv2menu_close")) + nav_row.add_item(ui.Button(label="Close", emoji="✖️", style=discord.ButtonStyle.danger, custom_id="cv2menu_close")) self.add_item(nav_row) async def interaction_check(self, interaction: discord.Interaction) -> bool: diff --git a/newhelpmenu/newhelpmenu.py b/newhelpmenu/newhelpmenu.py index 5e87f3e..2dd25f4 100644 --- a/newhelpmenu/newhelpmenu.py +++ b/newhelpmenu/newhelpmenu.py @@ -364,19 +364,25 @@ class NewHelpMenu(commands.Cog): all_cmds.append((f"{ctx.clean_prefix}{c.qualified_name}", short)) emoji = category_emojis.get(cat_name, "📂") + prefix = ctx.clean_prefix if use_cv2: - container = _make_category_container(cat_name, all_cmds, accent_color=accent, emoji=emoji) + container = _make_category_container(cat_name, all_cmds, accent_color=accent, emoji=emoji, prefix=prefix) layout = ui.LayoutView() layout.add_item(container) msg = await destination.send(view=layout) self._active_views[msg.id] = layout else: - embed = discord.Embed( - title=f"{emoji} {cat_name}", - color=discord.Colour(accent), - ) + bot_avatar = self.bot.user.display_avatar.url if self.bot.user else None + embed = discord.Embed(color=discord.Colour(accent)) + embed.set_author(name=f"{emoji} {cat_name}", icon_url=bot_avatar) + cmd_lines = [] for cmd_name, short in all_cmds: - embed.add_field(name=cmd_name, value=short[:100], inline=True) + display = cmd_name + if display.startswith(prefix): + display = display[len(prefix):] + cmd_lines.append(f"**`{display}`** · {short[:60]}") + embed.description = "\n".join(cmd_lines) + embed.set_footer(text=f"{prefix}help for details") await destination.send(embed=embed) return