v1.2.0: Major visual redesign — cleaner, more organized help

Embed mode (default):
  - set_author with bot icon for clean header (no giant title)
  - Overview: welcome message, compact 2-column category grid
    with command previews, padded for clean alignment
  - Category pages: each command shown as bold code name + indented
    description with ╰ connector for visual hierarchy
  - Command detail: yaml syntax block, organized sections with
    emoji labels (📝 Description, 🔀 Aliases, ⏱️ Cooldown, 📁 Subcommands)
  - Smart page numbering with real page counts
  - Proper parameter hint footer (<required> [optional])
  - 6 commands per page for readability (not cramped)

CV2 mode:
  - Same visual improvements adapted for containers
  - Clean separator-based hierarchy
  - Prefix stripped from display names for cleaner look

Both modes:
  - ActionRow fix: Close button + Select in separate rows
  - Category cap at 18/20 to avoid Discord limits
  - Consistent footer/navigation styling
This commit is contained in:
everestmcarthur 2026-05-08 02:44:22 +00:00
parent 84459daf63
commit ccd5079ead
2 changed files with 232 additions and 140 deletions

View file

@ -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 <command>` for details on a command.\n"
f"Use `{prefix}help <category>` 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 <command>` — details on a command",
f"▸ `{prefix}help <category>` — 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 <category>` 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 <command> 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 <command> 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 <command> 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 <command> for more details.")
embed.set_footer(text=f"<required> [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 <command> for more details.")
embed.set_footer(text=f"{prefix}help <command> 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 <command>` 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 <command>` — details on a command\n"
f"▸ `{prefix}help <category>` — 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"-# <required> [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:

View file

@ -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 <command> for details")
await destination.send(embed=embed)
return