use crate::{serenity_prelude as serenity, CreateReply};
use std::fmt::Write as _;
pub struct PrettyHelpConfiguration<'a> {
pub extra_text_at_bottom: &'a str,
pub ephemeral: bool,
pub show_context_menu_commands: bool,
pub show_subcommands: bool,
pub include_description: bool,
pub color: (u8, u8, u8),
#[doc(hidden)]
pub __non_exhaustive: (),
}
impl Default for PrettyHelpConfiguration<'_> {
fn default() -> Self {
Self {
extra_text_at_bottom: "",
ephemeral: true,
show_context_menu_commands: false,
show_subcommands: false,
include_description: true,
color: (0, 110, 51),
__non_exhaustive: (),
}
}
}
pub async fn pretty_help<U, E>(
ctx: crate::Context<'_, U, E>,
command: Option<&str>,
config: PrettyHelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
match command {
Some(command) => pretty_help_single_command(ctx, command, config).await,
None => pretty_help_all_commands(ctx, config).await,
}
}
async fn pretty_help_all_commands<U, E>(
ctx: crate::Context<'_, U, E>,
config: PrettyHelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
let commands = ctx.framework().options().commands.iter().filter(|cmd| {
!cmd.hide_in_help
&& (cmd.prefix_action.is_some()
|| cmd.slash_action.is_some()
|| (cmd.context_menu_action.is_some() && config.show_context_menu_commands))
});
let mut categories = indexmap::IndexMap::<Option<&str>, Vec<&crate::Command<U, E>>>::new();
for cmd in commands {
categories
.entry(cmd.category.as_deref())
.or_default()
.push(cmd);
}
let options_prefix = super::help::get_prefix_from_options(ctx).await;
let fields = categories
.into_iter()
.filter(|(_, cmds)| !cmds.is_empty())
.map(|(category, mut cmds)| {
cmds.sort_by_key(|cmd| cmd.slash_action.is_none() && cmd.prefix_action.is_none());
let mut buffer = String::new();
for cmd in cmds {
let name = cmd.context_menu_name.as_deref().unwrap_or(&cmd.name);
let prefix = format_cmd_prefix(cmd, &options_prefix);
if let Some(description) = cmd.description.as_deref() {
writeln!(buffer, "{}{}`: *{}*", prefix, name, description).ok();
} else {
writeln!(buffer, "{}{}`.", prefix, name).ok();
}
if config.show_subcommands {
for sbcmd in &cmd.subcommands {
let name = sbcmd.context_menu_name.as_deref().unwrap_or(&sbcmd.name);
let prefix = format_cmd_prefix(sbcmd, &options_prefix);
if let Some(description) = sbcmd.description.as_deref() {
writeln!(buffer, "> {}{}`: *{}*", prefix, name, description).ok();
} else {
writeln!(buffer, "> {}{}`.", prefix, name).ok();
}
}
}
}
if let Some((i, _)) = buffer.char_indices().nth(1024) {
buffer.truncate(i);
}
(category.unwrap_or_default(), buffer, false)
})
.collect::<Vec<_>>();
let embed = serenity::CreateEmbed::new()
.title("Help")
.fields(fields)
.color(config.color)
.footer(serenity::CreateEmbedFooter::new(
config.extra_text_at_bottom,
));
let reply = crate::CreateReply::default()
.embed(embed)
.ephemeral(config.ephemeral);
ctx.send(reply).await?;
Ok(())
}
fn format_cmd_prefix<U, E>(cmd: &crate::Command<U, E>, options_prefix: &Option<String>) -> String {
if cmd.slash_action.is_some() {
"`/".into()
} else if cmd.prefix_action.is_some() {
format!("`{}", options_prefix.as_deref().unwrap_or_default())
} else if cmd.context_menu_action.is_some() {
match cmd.context_menu_action {
Some(crate::ContextMenuCommandAction::Message(_)) => "Message menu: `".into(),
Some(crate::ContextMenuCommandAction::User(_)) => "User menu: `".into(),
Some(crate::ContextMenuCommandAction::__NonExhaustive) | None => {
unreachable!()
}
}
} else {
"`".into()
}
}
async fn pretty_help_single_command<U, E>(
ctx: crate::Context<'_, U, E>,
command_name: &str,
config: PrettyHelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
let commands = &ctx.framework().options().commands;
let command = commands
.iter()
.find(|cmd| {
cmd.context_menu_name
.as_ref()
.is_some_and(|n| n.eq_ignore_ascii_case(command_name))
})
.or(crate::find_command(commands, command_name, true, &mut vec![]).map(|(c, _, _)| c));
let Some(command) = command else {
ctx.send(
CreateReply::default()
.content(format!("No such command `{}`", command_name))
.ephemeral(config.ephemeral),
)
.await?;
return Ok(());
};
let mut invocations = Vec::new();
let mut subprefix = None;
if command.slash_action.is_some() {
invocations.push(format!("`/{}`", command.name));
subprefix = Some(format!("> `/{}`", command.name));
}
if command.prefix_action.is_some() {
let prefix = super::help::get_prefix_from_options(ctx)
.await
.unwrap_or_else(|| String::from("<prefix>"));
invocations.push(format!("`{}{}`", prefix, command.name));
subprefix = subprefix.or(Some(format!("> `{}{}`", prefix, command.name)));
}
if command.context_menu_name.is_some() && command.context_menu_action.is_some() {
let kind = match command.context_menu_action {
Some(crate::ContextMenuCommandAction::User(_)) => "user",
Some(crate::ContextMenuCommandAction::Message(_)) => "message",
Some(crate::ContextMenuCommandAction::__NonExhaustive) | None => unreachable!(),
};
invocations.push(format!(
"`{}` (on {})",
command
.context_menu_name
.as_deref()
.unwrap_or(&command.name),
kind
));
subprefix = subprefix.or(Some(String::from("> ")));
}
assert!(!invocations.is_empty());
assert!(subprefix.is_some());
let invocations = invocations
.into_iter()
.reduce(|x, y| format!("{x}\n{y}"))
.map(|s| ("", s, false));
let description = match (&command.description, &command.help_text) {
(Some(description), Some(help_text)) if config.include_description => {
format!("{}\n\n{}", description, help_text)
}
(_, Some(help_text)) => help_text.clone(),
(Some(description), None) => description.clone(),
(None, None) => "No help available".to_string(),
};
let parameters = command
.parameters
.iter()
.map(|parameter| {
let req = if parameter.required {
"required"
} else {
"optional"
};
if let Some(description) = parameter.description.as_deref() {
format!("`{}` ({}) *{} *.", parameter.name, req, description)
} else {
format!("`{}` ({}).", parameter.name, req)
}
})
.reduce(|x, y| format!("{x}\n{y}"))
.map(|s| ("Parameters", s, false));
let sbcmds = command
.subcommands
.iter()
.map(|sbcmd| {
let prefix = format_cmd_prefix(sbcmd, &subprefix); let name = sbcmd.context_menu_name.as_deref().unwrap_or(&sbcmd.name);
if let Some(description) = sbcmd.description.as_deref() {
format!("> {}{}`: *{} *", prefix, name, description)
} else {
format!("> {}{}`", prefix, name,)
}
})
.reduce(|x, y| format!("{x}\n{y}"))
.map(|s| ("Subcommands", s, false));
let fields = invocations
.into_iter()
.chain(parameters.into_iter())
.chain(sbcmds.into_iter());
let embed = serenity::CreateEmbed::default()
.description(description)
.fields(fields);
let reply = CreateReply::default()
.embed(embed)
.ephemeral(config.ephemeral);
ctx.send(reply).await?;
Ok(())
}