tux.help
¶
Help command system for Tux.
This module implements an interactive help command with support for: - Category browsing - Command details - Subcommand navigation - Pagination for large command groups
Classes:
Name | Description |
---|---|
HelpState | Navigation states for the help command. |
TuxHelp | Interactive help command for Tux. |
Classes¶
TuxHelp()
¶
Bases: HelpCommand
Interactive help command for Tux.
This class implements an interactive help command with support for category browsing, command details, subcommand navigation, and pagination for large command groups.
Attributes:
Name | Type | Description |
---|---|---|
_prefix_cache | dict[int or None, str] | Cache for storing guild-specific command prefixes. |
_category_cache | dict[str, dict[str, str]] | Cache for storing command categories. |
current_category | str or None | Currently selected category. |
current_command | str or None | Currently selected command. |
current_page | HelpState | Current page state. |
current_subcommand_page | int | Current page index for subcommands. |
message | Message or None | Last message context. |
command_mapping | dict[str, dict[str, Command]] or None | Mapping of command names to command objects. |
current_command_obj | Command or None | The currently active command object. |
subcommand_pages | list[list[Command]] | List of pages containing subcommands. |
Initialize the help command with necessary attributes.
Notes
This also initializes caches and state tracking for the help command.
Methods:
Name | Description |
---|---|
on_category_select | Handle the event when a category is selected. |
on_command_select | Handle the event when a command is selected. |
on_subcommand_select | Handle the event when a subcommand is selected. |
on_back_button | Handle the event when the back button is clicked. |
on_next_button | Handle navigation to the next page of subcommands. |
on_prev_button | Handle navigation to the previous page of subcommands. |
send_bot_help | Send the main help screen with command categories. |
send_cog_help | Display help for a specific cog. |
send_command_help | Display help for a specific command. |
send_group_help | Display help for a command group. |
send_error_message | Display an error message. |
to_reference_list | Convert a list of commands to a reference list. |
Source code in tux/help.py
def __init__(self) -> None:
"""
Initialize the help command with necessary attributes.
Notes
-----
This also initializes caches and state tracking for the help command.
"""
super().__init__(
command_attrs={
"help": "Lists all commands and sub-commands.",
"aliases": ["h", "commands"],
"usage": "$help <command> or <sub-command>",
},
)
# Caches
self._prefix_cache: dict[int | None, str] = {}
self._category_cache: dict[str, dict[str, str]] = {}
# State tracking
self.current_category: str | None = None
self.current_command: str | None = None
self.current_page = HelpState.MAIN
self.current_subcommand_page: int = 0
# Message and command tracking
self.message: discord.Message | None = None
self.command_mapping: dict[str, dict[str, commands.Command[Any, Any, Any]]] | None = None
self.current_command_obj: commands.Command[Any, Any, Any] | None = None
self.subcommand_pages: list[list[commands.Command[Any, Any, Any]]] = []
Functions¶
_get_prefix() -> str
async
¶
Get the guild-specific command prefix.
Returns:
Type | Description |
---|---|
str | The command prefix for the current guild. |
Source code in tux/help.py
async def _get_prefix(self) -> str:
"""
Get the guild-specific command prefix.
Returns
-------
str
The command prefix for the current guild.
"""
guild_id = self.context.guild.id if self.context.guild else None
if guild_id not in self._prefix_cache:
# Fetch and cache the prefix specific to the guild
self._prefix_cache[guild_id] = self.context.clean_prefix or CONFIG.DEFAULT_PREFIX
return self._prefix_cache[guild_id]
_embed_base(title: str, description: str | None = None) -> discord.Embed
¶
Create a base embed with consistent styling.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
title | str | The embed title. | required |
description | str or None | The embed description (default is None). | None |
Returns:
Type | Description |
---|---|
Embed | A styled embed object. |
Source code in tux/help.py
def _embed_base(self, title: str, description: str | None = None) -> discord.Embed:
"""
Create a base embed with consistent styling.
Parameters
----------
title : str
The embed title.
description : str or None, optional
The embed description (default is None).
Returns
-------
discord.Embed
A styled embed object.
"""
return discord.Embed(
title=title,
description=description,
color=CONST.EMBED_COLORS["DEFAULT"],
)
_format_flag_details(command: commands.Command[Any, Any, Any]) -> str
¶
Format the details of command flags.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command | Command | The command for which to format the flags. | required |
Returns:
Type | Description |
---|---|
str | Formatted string of flag details. |
Source code in tux/help.py
def _format_flag_details(self, command: commands.Command[Any, Any, Any]) -> str:
"""
Format the details of command flags.
Parameters
----------
command : commands.Command
The command for which to format the flags.
Returns
-------
str
Formatted string of flag details.
"""
flag_details: list[str] = []
try:
type_hints = get_type_hints(command.callback)
except Exception:
return ""
for param_annotation in type_hints.values():
if not isinstance(param_annotation, type) or not issubclass(param_annotation, commands.FlagConverter):
continue
for flag in param_annotation.__commands_flags__.values():
flag_str = self._format_flag_name(flag)
if flag.aliases and not getattr(flag, "positional", False):
flag_str += f" ({', '.join(flag.aliases)})"
flag_str += f"\n\t{flag.description or 'No description provided'}"
if flag.default is not discord.utils.MISSING:
flag_str += f"\n\tDefault: {flag.default}"
flag_details.append(flag_str)
return "\n\n".join(flag_details)
_format_flag_name(flag: commands.Flag) -> str
staticmethod
¶
Format a flag name based on its properties.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
flag | Flag | The flag to format. | required |
Returns:
Type | Description |
---|---|
str | Formatted flag name string. |
Source code in tux/help.py
@staticmethod
def _format_flag_name(flag: commands.Flag) -> str:
"""
Format a flag name based on its properties.
Parameters
----------
flag : commands.Flag
The flag to format.
Returns
-------
str
Formatted flag name string.
"""
if getattr(flag, "positional", False):
return f"<{flag.name}>" if flag.required else f"[{flag.name}]"
return f"-{flag.name}" if flag.required else f"[-{flag.name}]"
_generate_default_usage(command: commands.Command[Any, Any, Any]) -> str
¶
Generate a default usage string for a command.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command | Command | The command for which to generate usage. | required |
Returns:
Type | Description |
---|---|
str | Formatted usage string. |
Source code in tux/help.py
def _generate_default_usage(self, command: commands.Command[Any, Any, Any]) -> str:
"""
Generate a default usage string for a command.
Parameters
----------
command : commands.Command
The command for which to generate usage.
Returns
-------
str
Formatted usage string.
"""
signature = command.signature.strip()
if not signature:
return command.qualified_name
# Format the signature to look more like Discord's native format
# Replace things like [optional] with <optional>
formatted_signature = signature.replace("[", "<").replace("]", ">")
return f"{command.qualified_name} {formatted_signature}"
_add_command_help_fields(embed: discord.Embed, command: commands.Command[Any, Any, Any]) -> None
async
¶
Add usage and alias fields to the command embed.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
embed | Embed | The embed object to add fields to. | required |
command | Command | The command for which to add help fields. | required |
Source code in tux/help.py
async def _add_command_help_fields(self, embed: discord.Embed, command: commands.Command[Any, Any, Any]) -> None:
"""
Add usage and alias fields to the command embed.
Parameters
----------
embed : discord.Embed
The embed object to add fields to.
command : commands.Command
The command for which to add help fields.
"""
prefix = await self._get_prefix()
usage = command.usage or self._generate_default_usage(command)
embed.add_field(name="Usage", value=f"`{prefix}{usage}`", inline=False)
embed.add_field(
name="Aliases",
value=(f"`{', '.join(command.aliases)}`" if command.aliases else "No aliases"),
inline=False,
)
_add_command_field(embed: discord.Embed, command: commands.Command[Any, Any, Any], prefix: str) -> None
staticmethod
¶
Add a command as a field in the embed.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
embed | Embed | The embed object to update. | required |
command | Command | The command to add. | required |
prefix | str | The command prefix. | required |
Source code in tux/help.py
@staticmethod
def _add_command_field(embed: discord.Embed, command: commands.Command[Any, Any, Any], prefix: str) -> None:
"""
Add a command as a field in the embed.
Parameters
----------
embed : discord.Embed
The embed object to update.
command : commands.Command
The command to add.
prefix : str
The command prefix.
"""
command_aliases = ", ".join(command.aliases) if command.aliases else "No aliases"
embed.add_field(
name=f"{prefix}{command.qualified_name} ({command_aliases})",
value=f"> {command.short_doc or 'No documentation summary'}",
inline=False,
)
_get_command_categories(mapping: Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]]) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, commands.Command[Any, Any, Any]]]]
async
¶
Retrieve command categories and mapping.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
mapping | Mapping[Cog | None, list[Command]] | Mapping of cogs to their commands. | required |
Returns:
Type | Description |
---|---|
tuple | A tuple containing: - dict: Category cache mapping category names to command details. - dict: Command mapping of categories to command objects. |
Source code in tux/help.py
async def _get_command_categories(
self,
mapping: Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]],
) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, commands.Command[Any, Any, Any]]]]:
"""
Retrieve command categories and mapping.
Parameters
----------
mapping : Mapping[commands.Cog | None, list[commands.Command]]
Mapping of cogs to their commands.
Returns
-------
tuple
A tuple containing:
- dict: Category cache mapping category names to command details.
- dict: Command mapping of categories to command objects.
"""
if self._category_cache:
return self._category_cache, self.command_mapping or {}
self._category_cache, self.command_mapping = create_cog_category_mapping(mapping)
return self._category_cache, self.command_mapping
_paginate_subcommands(commands_list: list[commands.Command[Any, Any, Any]], preserve_page: bool = False) -> None
¶
Split subcommands into pages for pagination.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
commands_list | list of commands.Command | List of commands to paginate. | required |
preserve_page | bool | If True, preserve the current page index; otherwise, reset to first page. | False |
Source code in tux/help.py
def _paginate_subcommands(
self,
commands_list: list[commands.Command[Any, Any, Any]],
preserve_page: bool = False,
) -> None:
"""
Split subcommands into pages for pagination.
Parameters
----------
commands_list : list of commands.Command
List of commands to paginate.
preserve_page : bool, optional
If True, preserve the current page index; otherwise, reset to first page.
"""
current_page = self.current_subcommand_page if preserve_page else 0
self.subcommand_pages = paginate_items(commands_list, 10)
# Restore or reset page counter
if preserve_page:
# Make sure the page index is valid for the new pagination
self.current_subcommand_page = min(current_page, len(self.subcommand_pages) - 1)
else:
# Reset to first page when paginating
self.current_subcommand_page = 0
_find_command(command_name: str) -> commands.Command[Any, Any, Any] | None
¶
Find and return the command object for a given command name.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command_name | str | The name of the command to search for. | required |
Returns:
Type | Description |
---|---|
Command or None | The command object if found; otherwise, None. |
Source code in tux/help.py
def _find_command(self, command_name: str) -> commands.Command[Any, Any, Any] | None:
"""
Find and return the command object for a given command name.
Parameters
----------
command_name : str
The name of the command to search for.
Returns
-------
commands.Command or None
The command object if found; otherwise, None.
"""
if (
self.current_category
and self.command_mapping
and (found := self.command_mapping[self.current_category].get(command_name))
):
return found
if (
self.current_command_obj
and isinstance(self.current_command_obj, commands.Group)
and (found := discord.utils.get(self.current_command_obj.commands, name=command_name))
):
return found
if self.command_mapping:
for category_commands in self.command_mapping.values():
for cmd in category_commands.values():
if isinstance(cmd, commands.Group) and (
found := discord.utils.get(cmd.commands, name=command_name)
):
return found
return None
_find_parent_command(subcommand_name: str) -> tuple[str, commands.Command[Any, Any, Any]] | None
¶
Find the parent command for a given subcommand.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
subcommand_name | str | The subcommand name to find the parent for. | required |
Returns:
Type | Description |
---|---|
tuple of (str, commands.Command) or None | A tuple containing the parent command name and object, or None if not found. |
Source code in tux/help.py
def _find_parent_command(self, subcommand_name: str) -> tuple[str, commands.Command[Any, Any, Any]] | None:
"""
Find the parent command for a given subcommand.
Parameters
----------
subcommand_name : str
The subcommand name to find the parent for.
Returns
-------
tuple of (str, commands.Command) or None
A tuple containing the parent command name and object, or None if not found.
"""
if self.command_mapping:
for category_commands in self.command_mapping.values():
for parent_name, cmd in category_commands.items():
if isinstance(cmd, commands.Group) and discord.utils.get(cmd.commands, name=subcommand_name):
return parent_name, cmd
return None
_create_category_options() -> list[discord.SelectOption]
async
¶
Create select options for category selection.
Returns:
Type | Description |
---|---|
list of discord.SelectOption | A list of select options for available command categories. |
Source code in tux/help.py
async def _create_category_options(self) -> list[discord.SelectOption]:
"""
Create select options for category selection.
Returns
-------
list of discord.SelectOption
A list of select options for available command categories.
"""
category_emoji_map = {
"info": "🔍",
"moderation": "🛡",
"utility": "🔧",
"snippets": "📝",
"admin": "👑",
"fun": "🎉",
"levels": "📈",
"services": "🔌",
"guild": "🏰",
}
options: list[discord.SelectOption] = []
for category in self._category_cache:
if any(self._category_cache[category].values()):
emoji = category_emoji_map.get(category, "❓")
options.append(
discord.SelectOption(
label=category.capitalize(),
value=category,
emoji=emoji,
description=f"View {category.capitalize()} commands",
),
)
return sorted(options, key=lambda o: o.label)
_create_command_options(category: str) -> list[discord.SelectOption]
async
¶
Create select options for commands within a specified category.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
category | str | The category for which to create command options. | required |
Returns:
Type | Description |
---|---|
list of discord.SelectOption | A list of select options corresponding to the commands in the category. |
Source code in tux/help.py
async def _create_command_options(self, category: str) -> list[discord.SelectOption]:
"""
Create select options for commands within a specified category.
Parameters
----------
category : str
The category for which to create command options.
Returns
-------
list of discord.SelectOption
A list of select options corresponding to the commands in the category.
"""
options: list[discord.SelectOption] = []
if self.command_mapping and category in self.command_mapping:
for cmd_name, cmd in self.command_mapping[category].items():
description = truncate_description(cmd.short_doc or "No description")
# Add an indicator for group commands
is_group = isinstance(cmd, commands.Group) and len(cmd.commands) > 0
label = f"{cmd_name}{'†' if is_group else ''}"
options.append(SelectOption(label=label, value=cmd_name, description=description))
else:
logger.warning(f"No commands found for category {category}")
return sorted(options, key=lambda o: o.label)
_create_subcommand_options(command: commands.Group[Any, Any, Any]) -> list[SelectOption]
async
¶
Create select options for subcommands within a command group.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command | Group | The command group for which to create subcommand options. | required |
Returns:
Type | Description |
---|---|
list of discord.SelectOption | A list of select options for the subcommands. |
Source code in tux/help.py
async def _create_subcommand_options(self, command: commands.Group[Any, Any, Any]) -> list[SelectOption]:
"""
Create select options for subcommands within a command group.
Parameters
----------
command : commands.Group
The command group for which to create subcommand options.
Returns
-------
list of discord.SelectOption
A list of select options for the subcommands.
"""
# Special handling for jishaku to prevent loading all subcommands
if command.name not in {"jsk", "jishaku"}:
# Normal handling for other command groups
return [
SelectOption(
label=subcmd.name,
value=subcmd.name,
description=truncate_description(subcmd.short_doc or "No description"),
)
for subcmd in sorted(command.commands, key=lambda x: x.name)
]
# Only include a few important jishaku commands
essential_subcmds = ["py", "shell", "cat", "curl", "pip", "git", "help"]
subcommand_options: list[SelectOption] = []
for subcmd_name in essential_subcmds:
if subcmd := discord.utils.get(command.commands, name=subcmd_name):
description = truncate_description(subcmd.short_doc or "No description")
subcommand_options.append(SelectOption(label=subcmd.name, value=subcmd.name, description=description))
# Add an option to suggest using jsk help
subcommand_options.append(
SelectOption(
label="See all commands",
value="_see_all",
description="Use jsk help command for complete list",
),
)
return subcommand_options
_create_main_embed() -> discord.Embed
async
¶
Create the main help embed.
Returns:
Type | Description |
---|---|
Embed | The main help embed to be displayed. |
Source code in tux/help.py
async def _create_main_embed(self) -> discord.Embed:
"""
Create the main help embed.
Returns
-------
discord.Embed
The main help embed to be displayed.
"""
if CONFIG.BOT_NAME != "Tux":
logger.info("Bot name is not Tux, using different help message.")
embed = self._embed_base(
"Hello! Welcome to the help command.",
f"{CONFIG.BOT_NAME} is a self-hosted instance of Tux. The bot is written in Python using discord.py.\n\nIf you enjoy using {CONFIG.BOT_NAME}, consider contributing to the original project.",
)
else:
embed = self._embed_base(
"Hello! Welcome to the help command.",
"Tux is an all-in-one bot by the All Things Linux Discord server. The bot is written in Python using discord.py, and we are actively seeking contributors.",
)
await self._add_bot_help_fields(embed)
return embed
_create_category_embed(category: str) -> discord.Embed
async
¶
Create an embed for a specific category.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
category | str | The category name. | required |
Returns:
Type | Description |
---|---|
Embed | The embed displaying commands for the category. |
Source code in tux/help.py
async def _create_category_embed(self, category: str) -> discord.Embed:
"""
Create an embed for a specific category.
Parameters
----------
category : str
The category name.
Returns
-------
discord.Embed
The embed displaying commands for the category.
"""
prefix = await self._get_prefix()
embed = self._embed_base(f"{category.capitalize()} Commands")
embed.set_footer(
text="Select a command from the dropdown to see details.",
)
sorted_commands = sorted(self._category_cache[category].items())
description = "\n".join(f"**`{prefix}{cmd}`** | {command_list}" for cmd, command_list in sorted_commands)
embed.description = description
return embed
_create_command_embed(command_name: str) -> discord.Embed
async
¶
Create an embed for a specific command.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command_name | str | The name of the command. | required |
Returns:
Type | Description |
---|---|
Embed | The embed with command details. |
Source code in tux/help.py
async def _create_command_embed(self, command_name: str) -> discord.Embed:
"""
Create an embed for a specific command.
Parameters
----------
command_name : str
The name of the command.
Returns
-------
discord.Embed
The embed with command details.
"""
command = self._find_command(command_name)
if not command:
logger.error(
f"Command '{command_name}' not found. Category: {self.current_category}, Current command: {self.current_command}",
)
return self._embed_base("Error", "Command not found")
# Store the current command object for reference
self.current_command_obj = command
self.current_command = command_name
prefix = await self._get_prefix()
help_text = format_multiline_description(command.help)
embed = self._embed_base(
title=f"{prefix}{command.qualified_name}",
description=help_text,
)
# Add command fields
await self._add_command_help_fields(embed, command)
# Add flag details if present
if flag_details := self._format_flag_details(command):
embed.add_field(name="Flags", value=f"```\n{flag_details}\n```", inline=False)
# Add subcommands section if this is a group
if isinstance(command, commands.Group) and command.commands:
sorted_cmds = sorted(command.commands, key=lambda x: x.name)
if nested_groups := [cmd for cmd in sorted_cmds if isinstance(cmd, commands.Group) and cmd.commands]:
nested_groups_text = "\n".join(
f"• `{g.name}` - {truncate_description(g.short_doc or 'No description')} ({len(g.commands)} subcommands)"
for g in nested_groups
)
embed.add_field(
name="Nested Command Groups",
value=(
f"This command has the following subcommand groups:\n\n{nested_groups_text}\n\nSelect a group command to see its subcommands."
),
inline=False,
)
self._paginate_subcommands(sorted_cmds, preserve_page=True)
# For large command groups like JSK, show paginated view
if command.name in {"jsk", "jishaku"} or len(sorted_cmds) > 15:
valid_page = self.subcommand_pages and 0 <= self.current_subcommand_page < len(self.subcommand_pages)
current_page_cmds = (
self.subcommand_pages[self.current_subcommand_page] if valid_page else sorted_cmds[:10]
)
if not valid_page:
logger.warning(
f"Invalid page index: {self.current_subcommand_page}, pages: {len(self.subcommand_pages)}",
)
subcommands_list = "\n".join(
f"• `{c.name}{'†' if isinstance(c, commands.Group) and c.commands else ''}` - {c.short_doc or 'No description'}"
for c in current_page_cmds
)
total_count = len(sorted_cmds)
page_num = self.current_subcommand_page + 1
total_pages = len(self.subcommand_pages) or 1
embed.add_field(
name=f"Subcommands (Page {page_num}/{total_pages})",
value=(
f"This command has {total_count} subcommands:\n\n{subcommands_list}\n\nUse the navigation buttons to browse all subcommands."
),
inline=False,
)
else:
subcommands_list = "\n".join(
f"• `{c.name}{'†' if isinstance(c, commands.Group) and c.commands else ''}` - {c.short_doc or 'No description'}"
for c in sorted_cmds
)
embed.add_field(
name="Subcommands",
value=(
f"This command group has the following subcommands:\n\n{subcommands_list}\n\nSelect a subcommand from the dropdown to see more details."
),
inline=False,
)
return embed
_create_subcommand_embed(subcommand_name: str) -> discord.Embed
async
¶
Create an embed for a specific subcommand.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
subcommand_name | str | The name of the subcommand. | required |
Returns:
Type | Description |
---|---|
Embed | The embed with subcommand details. |
Source code in tux/help.py
async def _create_subcommand_embed(self, subcommand_name: str) -> discord.Embed:
"""
Create an embed for a specific subcommand.
Parameters
----------
subcommand_name : str
The name of the subcommand.
Returns
-------
discord.Embed
The embed with subcommand details.
"""
if not self.current_command_obj or not isinstance(self.current_command_obj, commands.Group):
return self._embed_base("Error", "Parent command not found")
# Find the subcommand
subcommand = discord.utils.get(self.current_command_obj.commands, name=subcommand_name)
if not subcommand:
return self._embed_base("Error", "Subcommand not found")
prefix = await self._get_prefix()
# Format help text with proper quoting
help_text = format_multiline_description(subcommand.help)
embed = self._embed_base(
title=f"{prefix}{subcommand.qualified_name}",
description=help_text,
)
await self._add_command_help_fields(embed, subcommand)
if flag_details := self._format_flag_details(subcommand):
embed.add_field(name="Flags", value=f"```\n{flag_details}\n```", inline=False)
return embed
_add_bot_help_fields(embed: discord.Embed) -> None
async
¶
Add additional help information about the bot to the embed.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
embed | Embed | The embed to which the help information will be added. | required |
Source code in tux/help.py
async def _add_bot_help_fields(self, embed: discord.Embed) -> None:
"""
Add additional help information about the bot to the embed.
Parameters
----------
embed : discord.Embed
The embed to which the help information will be added.
"""
prefix = await self._get_prefix()
embed.add_field(
name="How to Use",
value=f"Most commands are hybrid meaning they can be used via prefix `{prefix}` OR slash `/`. Commands strictly available via `/` are not listed in the help menu.",
inline=False,
)
embed.add_field(
name="Command Help",
value="Select a category from the dropdown, then select a command to view details.",
inline=False,
)
embed.add_field(
name="Flag Help",
value=f"Flags in `[]` are optional. Most flags have aliases that can be used.\n> e.g. `{prefix}ban @user spamming` or `{prefix}b @user spamming -silent true`",
inline=False,
)
embed.add_field(
name="Support Server",
value="-# [Need support? Join Server](https://discord.gg/gpmSjcjQxg)",
inline=True,
)
embed.add_field(
name="GitHub Repository",
value="-# [Help contribute! View Repo](https://github.com/allthingslinux/tux)",
inline=True,
)
bot_name_display = "Tux" if CONFIG.BOT_NAME == "Tux" else f"{CONFIG.BOT_NAME} (Tux)"
environment = get_current_env()
owner_info = f"Bot Owner: <@{CONFIG.BOT_OWNER_ID}>" if not CONFIG.HIDE_BOT_OWNER and CONFIG.BOT_OWNER_ID else ""
embed.add_field(
name="Bot Instance",
value=f"-# Running {bot_name_display} version `{CONFIG.BOT_VERSION}` in `{environment}` mode | {owner_info}",
inline=False,
)
_create_main_view() -> HelpView
async
¶
Create the main help view with category selection.
Returns:
Type | Description |
---|---|
HelpView | A view containing category selection and a close button. |
Source code in tux/help.py
async def _create_main_view(self) -> HelpView:
"""
Create the main help view with category selection.
Returns
-------
HelpView
A view containing category selection and a close button.
"""
view = HelpView(self)
# Add category select
category_options = await self._create_category_options()
category_select = CategorySelectMenu(self, category_options, "Select a category")
view.add_item(category_select)
# Add close button
view.add_item(CloseButton())
return view
_create_category_view(category: str) -> HelpView
async
¶
Create a view for a specific category with command selection.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
category | str | The category name. | required |
Returns:
Type | Description |
---|---|
HelpView | The view for the selected category. |
Source code in tux/help.py
async def _create_category_view(self, category: str) -> HelpView:
"""
Create a view for a specific category with command selection.
Parameters
----------
category : str
The category name.
Returns
-------
HelpView
The view for the selected category.
"""
view = HelpView(self)
# Add command select for this category
command_options = await self._create_command_options(category)
command_select = CommandSelectMenu(self, command_options, f"Select a {category} command")
view.add_item(command_select)
# Add back button and close button
view.add_item(BackButton(self))
view.add_item(CloseButton())
return view
_create_command_view() -> HelpView
async
¶
Create a view for a command with navigation options.
Returns:
Type | Description |
---|---|
HelpView | A view for navigating command details. |
Source code in tux/help.py
async def _create_command_view(self) -> HelpView:
"""
Create a view for a command with navigation options.
Returns
-------
HelpView
A view for navigating command details.
"""
view = HelpView(self)
# Add back button first
view.add_item(BackButton(self))
# If this is a command group, handle navigation
if (
self.current_command_obj
and isinstance(self.current_command_obj, commands.Group)
and len(self.current_command_obj.commands) > 0
):
sorted_cmds = sorted(self.current_command_obj.commands, key=lambda x: x.name)
# For large command groups like JSK, use pagination buttons and add a select menu for the current page
if self.current_command_obj.name in {"jsk", "jishaku"} or len(sorted_cmds) > 15:
if not self.subcommand_pages:
self._paginate_subcommands(sorted_cmds, preserve_page=True)
if len(self.subcommand_pages) > 1:
view.add_item(PrevButton(self))
view.add_item(NextButton(self))
valid_page = self.subcommand_pages and 0 <= self.current_subcommand_page < len(self.subcommand_pages)
current_page_cmds = self.subcommand_pages[self.current_subcommand_page] if valid_page else []
if not valid_page:
logger.warning(
f"Invalid page index: {self.current_subcommand_page}, pages: {len(self.subcommand_pages)}",
)
if jsk_select_options := [
discord.SelectOption(
label=cmd.name,
value=cmd.name,
description=truncate_description(cmd.short_doc or "No description"),
)
for cmd in current_page_cmds
]:
jsk_select = CommandSelectMenu(self, jsk_select_options, "Select a command")
view.add_item(jsk_select)
else:
logger.info(
f"Creating dropdown for command group: {self.current_command_obj.name} with {len(sorted_cmds)} subcommands",
)
if subcommand_options := await self._create_subcommand_options(self.current_command_obj):
subcommand_select = SubcommandSelectMenu(self, subcommand_options, "Select a subcommand")
view.add_item(subcommand_select)
if nested_groups := [cmd for cmd in sorted_cmds if isinstance(cmd, commands.Group) and cmd.commands]:
for group_cmd in nested_groups:
logger.info(
f"Adding nested group handling for {group_cmd.name} with {len(group_cmd.commands)} subcommands",
)
# Add close button last
view.add_item(CloseButton())
return view
_create_subcommand_view() -> HelpView
async
¶
Create a view for a subcommand with back navigation.
Returns:
Type | Description |
---|---|
HelpView | A view for displaying subcommand details. |
Source code in tux/help.py
async def _create_subcommand_view(self) -> HelpView:
"""
Create a view for a subcommand with back navigation.
Returns
-------
HelpView
A view for displaying subcommand details.
"""
view = HelpView(self)
# Add back buttons and close button
view.add_item(BackButton(self))
view.add_item(CloseButton())
return view
on_category_select(interaction: discord.Interaction, category: str) -> None
async
¶
Handle the event when a category is selected.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
category | str | The selected category. | required |
Source code in tux/help.py
async def on_category_select(self, interaction: discord.Interaction, category: str) -> None:
"""
Handle the event when a category is selected.
Parameters
----------
interaction : discord.Interaction
The interaction event.
category : str
The selected category.
"""
self.current_category = category
self.current_page = HelpState.CATEGORY
embed = await self._create_category_embed(category)
view = await self._create_category_view(category)
if interaction.message:
await interaction.message.edit(embed=embed, view=view)
on_command_select(interaction: discord.Interaction, command_name: str) -> None
async
¶
Handle the event when a command is selected.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
command_name | str | The selected command. | required |
Source code in tux/help.py
async def on_command_select(self, interaction: discord.Interaction, command_name: str) -> None:
"""
Handle the event when a command is selected.
Parameters
----------
interaction : discord.Interaction
The interaction event.
command_name : str
The selected command.
"""
self.current_page = HelpState.COMMAND
embed = await self._create_command_embed(command_name)
view = await self._create_command_view()
# Special handling for nested command groups (groups within groups)
if (
self.current_command_obj
and isinstance(self.current_command_obj, commands.Group)
and self.current_command_obj.commands
):
# Just log nested groups for debugging
for subcommand in self.current_command_obj.commands:
if isinstance(subcommand, commands.Group) and subcommand.commands:
logger.info(
f"Found nested command group: {subcommand.name} with {len(subcommand.commands)} subcommands",
)
if interaction.message:
await interaction.message.edit(embed=embed, view=view)
else:
logger.warning("Command selection: No message to update")
on_subcommand_select(interaction: discord.Interaction, subcommand_name: str) -> None
async
¶
Handle the event when a subcommand is selected.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
subcommand_name | str | The selected subcommand. | required |
Source code in tux/help.py
async def on_subcommand_select(self, interaction: discord.Interaction, subcommand_name: str) -> None:
"""
Handle the event when a subcommand is selected.
Parameters
----------
interaction : discord.Interaction
The interaction event.
subcommand_name : str
The selected subcommand.
"""
# Special handling for the "see all" option in jsk
if subcommand_name == "_see_all":
embed = discord.Embed(
title="Jishaku Help",
description="For a complete list of Jishaku commands, please use:\n`jsk help`",
color=CONST.EMBED_COLORS["INFO"],
)
if interaction.message:
await interaction.message.edit(embed=embed)
return
# Find the selected subcommand object
if not self.current_command_obj or not isinstance(self.current_command_obj, commands.Group):
logger.error(f"Cannot find parent command object for subcommand {subcommand_name}")
return
selected_command = discord.utils.get(self.current_command_obj.commands, name=subcommand_name)
if not selected_command:
logger.error(f"Subcommand {subcommand_name} not found in {self.current_command_obj.name}")
return
# Check if this subcommand is itself a group with subcommands
if isinstance(selected_command, commands.Group) and selected_command.commands:
logger.info(
f"Selected subcommand '{subcommand_name}' is a group with {len(selected_command.commands)} subcommands",
)
# Set this subcommand as the current command to view
self.current_command = selected_command.name
self.current_command_obj = selected_command
# Create a command view for this subcommand group
embed = await self._create_command_embed(selected_command.name)
view = await self._create_command_view()
if interaction.message:
await interaction.message.edit(embed=embed, view=view)
# Use command state so back button logic will work correctly
self.current_page = HelpState.COMMAND
return
# Normal subcommand handling for non-group subcommands
self.current_page = HelpState.SUBCOMMAND
embed = await self._create_subcommand_embed(subcommand_name)
view = await self._create_subcommand_view()
if interaction.message:
await interaction.message.edit(embed=embed, view=view)
else:
logger.warning("Subcommand selection: No message to update")
on_back_button(interaction: discord.Interaction) -> None
async
¶
Handle the event when the back button is clicked.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
Source code in tux/help.py
async def on_back_button(self, interaction: discord.Interaction) -> None:
"""
Handle the event when the back button is clicked.
Parameters
----------
interaction : discord.Interaction
The interaction event.
"""
if not interaction.message:
return
if (
self.current_page == HelpState.SUBCOMMAND
and self.current_command
and self.current_category
and self.command_mapping
and (command := self.command_mapping[self.current_category].get(self.current_command))
):
self.current_page = HelpState.COMMAND
self.current_command_obj = command
embed = await self._create_command_embed(self.current_command)
view = await self._create_command_view()
await interaction.message.edit(embed=embed, view=view)
return
if (
self.current_page == HelpState.COMMAND
and self.current_command
and (parent := self._find_parent_command(self.current_command))
):
parent_name, parent_obj = parent
logger.info(f"Found parent command {parent_name} for {self.current_command}")
self.current_command = parent_name
self.current_command_obj = parent_obj
embed = await self._create_command_embed(parent_name)
view = await self._create_command_view()
await interaction.message.edit(embed=embed, view=view)
return
if self.current_page == HelpState.SUBCOMMAND:
self.current_page = HelpState.CATEGORY
self.current_command = None
self.current_command_obj = None
if self.current_page == HelpState.COMMAND and self.current_category:
self.current_page = HelpState.CATEGORY
embed = await self._create_category_embed(self.current_category)
view = await self._create_category_view(self.current_category)
else:
self.current_page = HelpState.MAIN
self.current_category = None
embed = await self._create_main_embed()
view = await self._create_main_view()
await interaction.message.edit(embed=embed, view=view)
on_next_button(interaction: discord.Interaction) -> None
async
¶
Handle navigation to the next page of subcommands.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
Source code in tux/help.py
async def on_next_button(self, interaction: discord.Interaction) -> None:
"""
Handle navigation to the next page of subcommands.
Parameters
----------
interaction : discord.Interaction
The interaction event.
"""
if not self.subcommand_pages:
logger.warning("Pagination: No subcommand pages available")
return
# Read current page directly from self
current_page = self.current_subcommand_page
total_pages = len(self.subcommand_pages)
# Increment the page counter
if current_page < total_pages - 1:
self.current_subcommand_page = current_page + 1
else:
logger.info(f"Pagination: Already at last page ({current_page})")
# Update the embed with the new page
if self.current_command:
if interaction.message:
embed = await self._create_command_embed(self.current_command)
view = await self._create_command_view()
await interaction.message.edit(embed=embed, view=view)
else:
logger.warning("Pagination: No message to update")
on_prev_button(interaction: discord.Interaction) -> None
async
¶
Handle navigation to the previous page of subcommands.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
interaction | Interaction | The interaction event. | required |
Source code in tux/help.py
async def on_prev_button(self, interaction: discord.Interaction) -> None:
"""
Handle navigation to the previous page of subcommands.
Parameters
----------
interaction : discord.Interaction
The interaction event.
"""
if not self.subcommand_pages:
logger.warning("Pagination: No subcommand pages available")
return
# Read current page directly from self
current_page = self.current_subcommand_page
# total_pages = len(self.subcommand_pages)
# Decrement the page counter
if current_page > 0:
self.current_subcommand_page = current_page - 1
else:
logger.info(f"Pagination: Already at first page ({current_page})")
# Update the embed with the new page
if self.current_command:
if interaction.message:
embed = await self._create_command_embed(self.current_command)
view = await self._create_command_view()
await interaction.message.edit(embed=embed, view=view)
else:
logger.warning("Pagination: No message to update")
send_bot_help(mapping: Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]]) -> None
async
¶
Send the main help screen with command categories.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
mapping | Mapping[Cog | None, list[Command]] | Mapping of cogs to their commands. | required |
Source code in tux/help.py
async def send_bot_help(self, mapping: Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]]) -> None:
"""
Send the main help screen with command categories.
Parameters
----------
mapping : Mapping[commands.Cog | None, list[commands.Command]]
Mapping of cogs to their commands.
"""
await self._get_command_categories(mapping)
embed = await self._create_main_embed()
view = await self._create_main_view()
self.message = await self.get_destination().send(embed=embed, view=view)
send_cog_help(cog: commands.Cog) -> None
async
¶
Display help for a specific cog.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cog | Cog | The cog for which to display help. | required |
Source code in tux/help.py
async def send_cog_help(self, cog: commands.Cog) -> None:
"""
Display help for a specific cog.
Parameters
----------
cog : commands.Cog
The cog for which to display help.
"""
prefix = await self._get_prefix()
embed = self._embed_base(f"{cog.qualified_name} Commands")
for command in cog.get_commands():
self._add_command_field(embed, command, prefix)
if isinstance(command, commands.Group):
for subcommand in command.commands:
self._add_command_field(embed, subcommand, prefix)
await self.get_destination().send(embed=embed)
send_command_help(command: commands.Command[Any, Any, Any]) -> None
async
¶
Display help for a specific command.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
command | Command | The command for which to display help. | required |
Source code in tux/help.py
async def send_command_help(self, command: commands.Command[Any, Any, Any]) -> None:
"""
Display help for a specific command.
Parameters
----------
command : commands.Command
The command for which to display help.
"""
prefix = await self._get_prefix()
# Format help text with proper quoting for all lines
help_text = format_multiline_description(command.help)
embed = self._embed_base(
title=f"{prefix}{command.qualified_name}",
description=help_text,
)
await self._add_command_help_fields(embed, command)
if flag_details := self._format_flag_details(command):
embed.add_field(name="Flags", value=f"```\n{flag_details}\n```", inline=False)
view = HelpView(self)
view.add_item(CloseButton())
await self.get_destination().send(embed=embed, view=view)
send_group_help(group: commands.Group[Any, Any, Any]) -> None
async
¶
Display help for a command group.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
group | Group | The command group for which to display help. | required |
Source code in tux/help.py
async def send_group_help(self, group: commands.Group[Any, Any, Any]) -> None:
"""
Display help for a command group.
Parameters
----------
group : commands.Group
The command group for which to display help.
"""
# For large command groups or JSK, use pagination
if group.name in {"jsk", "jishaku"} or len(group.commands) > 15:
# Paginate subcommands
subcommands = sorted(group.commands, key=lambda x: x.name)
pages = paginate_items(subcommands, 8)
# Create direct help view with navigation
view = DirectHelpView(self, group, pages)
embed = await view.get_embed()
else:
# For smaller groups, add a dropdown to view individual subcommands
prefix = await self._get_prefix()
# Format help text with proper quoting for all lines
help_text = format_multiline_description(group.help)
embed = self._embed_base(
title=f"{prefix}{group.qualified_name}",
description=help_text,
)
await self._add_command_help_fields(embed, group)
# Add all subcommands non-inline
sorted_cmds = sorted(group.commands, key=lambda x: x.name)
subcommands_list = "\n".join(f"• `{c.name}` - {c.short_doc or 'No description'}" for c in sorted_cmds)
embed.add_field(
name="Subcommands",
value=f"This command group has the following subcommands:\n\n{subcommands_list}\n\nSelect a subcommand from the dropdown to see more details.",
inline=False,
)
# Create view with dropdown
view = HelpView(self)
if subcommand_options := [
discord.SelectOption(
label=cmd.name,
value=cmd.name,
description=truncate_description(cmd.short_doc or "No description"),
)
for cmd in sorted_cmds
]:
subcommand_select = SubcommandSelectMenu(self, subcommand_options, "View detailed subcommand help")
view.add_item(subcommand_select)
view.add_item(CloseButton())
# Create a special handler for this message
self.current_command = group.name
self.current_command_obj = group
await self.get_destination().send(embed=embed, view=view)
send_error_message(error: str) -> None
async
¶
Display an error message.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
error | str | The error message to display. | required |
Source code in tux/help.py
async def send_error_message(self, error: str) -> None:
"""
Display an error message.
Parameters
----------
error : str
The error message to display.
"""
embed = EmbedCreator.create_embed(
EmbedCreator.ERROR,
user_name=self.context.author.name,
user_display_avatar=self.context.author.display_avatar.url,
description=error,
)
await self.get_destination().send(embed=embed, delete_after=CONST.DEFAULT_DELETE_AFTER)
# Only log errors that are not related to command not found
if "no command called" not in error.lower():
logger.warning(f"An error occurred while sending a help message: {error}")
to_reference_list(ctx: commands.Context[commands.Bot], commands_list: list[commands.Command[Any, Any, Any]], with_groups: bool = True) -> list[tuple[commands.Command[Any, Any, Any], str | None]]
¶
Convert a list of commands to a reference list.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
ctx | Context[Bot] | The context of the command. | required |
commands_list | list of commands.Command | The list of commands to convert. | required |
with_groups | bool | Whether to include command groups. | True |
Returns:
Type | Description |
---|---|
list of tuple | A list of tuples, each containing a command and its cog group (or None). |
Source code in tux/help.py
def to_reference_list(
self,
ctx: commands.Context[commands.Bot],
commands_list: list[commands.Command[Any, Any, Any]],
with_groups: bool = True,
) -> list[tuple[commands.Command[Any, Any, Any], str | None]]:
"""
Convert a list of commands to a reference list.
Parameters
----------
ctx : commands.Context[commands.Bot]
The context of the command.
commands_list : list of commands.Command
The list of commands to convert.
with_groups : bool, optional
Whether to include command groups.
Returns
-------
list of tuple
A list of tuples, each containing a command and its cog group (or None).
"""
references: list[tuple[commands.Command[Any, Any, Any], str | None]] = []
# Helper function to extract cog group from a command
def get_command_group(cmd: commands.Command[Any, Any, Any]) -> str | None:
"""Extract the command's cog group."""
if cmd.cog:
module = getattr(cmd.cog, "__module__", "")
parts = module.split(".")
# Assuming the structure is: tux.cogs.<group>...
if len(parts) >= 3 and parts[1].lower() == "cogs":
return parts[2].lower()
return None
for cmd in commands_list:
if isinstance(cmd, commands.Group) and with_groups and cmd.commands:
child_commands = list(cmd.commands)
references.append((cmd, get_command_group(cmd)))
references.extend(
(child_cmd, get_command_group(cmd)) for child_cmd in sorted(child_commands, key=lambda x: x.name)
)
else:
references.append((cmd, get_command_group(cmd)))
return references