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:
parent
84459daf63
commit
ccd5079ead
2 changed files with 232 additions and 140 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue