use crate::{serenity_prelude as serenity, CreateReply};
use std::fmt::Write as _;
pub struct HelpConfiguration<'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,
#[doc(hidden)]
pub __non_exhaustive: (),
}
impl Default for HelpConfiguration<'_> {
fn default() -> Self {
Self {
extra_text_at_bottom: "",
ephemeral: true,
show_context_menu_commands: false,
show_subcommands: false,
include_description: true,
__non_exhaustive: (),
}
}
}
struct TwoColumnList(Vec<(String, Option<String>)>);
impl TwoColumnList {
fn new() -> Self {
Self(Vec::new())
}
fn push_two_colums(&mut self, command: String, description: String) {
self.0.push((command, Some(description)));
}
fn push_heading(&mut self, category: &str) {
if !self.0.is_empty() {
self.0.push(("".to_string(), None));
}
let mut category = category.to_string();
category += ":";
self.0.push((category, None));
}
fn into_string(self) -> String {
let longest_command = self
.0
.iter()
.filter_map(|(command, description)| {
if description.is_some() {
Some(command.len())
} else {
None
}
})
.max()
.unwrap_or(0);
let mut text = String::new();
for (command, description) in self.0 {
if let Some(description) = description {
let padding = " ".repeat(longest_command - command.len() + 3);
writeln!(text, "{}{}{}", command, padding, description).unwrap();
} else {
writeln!(text, "{}", command).unwrap();
}
}
text
}
}
pub(super) async fn get_prefix_from_options<U, E>(ctx: crate::Context<'_, U, E>) -> Option<String> {
let options = &ctx.framework().options().prefix_options;
match &options.prefix {
Some(fixed_prefix) => Some(fixed_prefix.clone()),
None => match options.dynamic_prefix {
Some(dynamic_prefix_callback) => {
match dynamic_prefix_callback(crate::PartialContext::from(ctx)).await {
Ok(Some(dynamic_prefix)) => Some(dynamic_prefix),
_ => None,
}
}
None => None,
},
}
}
fn format_context_menu_name<U, E>(command: &crate::Command<U, E>) -> Option<String> {
let kind = match command.context_menu_action {
Some(crate::ContextMenuCommandAction::User(_)) => "user",
Some(crate::ContextMenuCommandAction::Message(_)) => "message",
Some(crate::ContextMenuCommandAction::__NonExhaustive) => unreachable!(),
None => return None,
};
Some(format!(
"{} (on {})",
command
.context_menu_name
.as_deref()
.unwrap_or(&command.name),
kind
))
}
async fn help_single_command<U, E>(
ctx: crate::Context<'_, U, E>,
command_name: &str,
config: HelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
let commands = &ctx.framework().options().commands;
let mut command = commands.iter().find(|command| {
if let Some(context_menu_name) = &command.context_menu_name {
if context_menu_name.eq_ignore_ascii_case(command_name) {
return true;
}
}
false
});
if command.is_none() {
if let Some((c, _, _)) = crate::find_command(commands, command_name, true, &mut vec![]) {
command = Some(c);
}
}
let reply = if let Some(command) = command {
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 = match get_prefix_from_options(ctx).await {
Some(prefix) => prefix,
None => String::from("<prefix>"),
};
invocations.push(format!("`{}{}`", prefix, command.name));
if subprefix.is_none() {
subprefix = Some(format!(" {}{}", prefix, command.name));
}
}
if command.context_menu_name.is_some() && command.context_menu_action.is_some() {
invocations.push(format_context_menu_name(command).unwrap());
if subprefix.is_none() {
subprefix = Some(String::from(" "));
}
}
assert!(subprefix.is_some());
assert!(!invocations.is_empty());
let invocations = invocations.join("\n");
let mut text = match (&command.description, &command.help_text) {
(Some(description), Some(help_text)) => {
if config.include_description {
format!("{}\n\n{}", description, help_text)
} else {
help_text.clone()
}
}
(Some(description), None) => description.to_owned(),
(None, Some(help_text)) => help_text.clone(),
(None, None) => "No help available".to_string(),
};
if !command.parameters.is_empty() {
text += "\n\n```\nParameters:\n";
let mut parameterlist = TwoColumnList::new();
for parameter in &command.parameters {
let name = parameter.name.clone();
let description = parameter.description.as_deref().unwrap_or("");
let description = format!(
"({}) {}",
if parameter.required {
"required"
} else {
"optional"
},
description,
);
parameterlist.push_two_colums(name, description);
}
text += ¶meterlist.into_string();
text += "```";
}
if !command.subcommands.is_empty() {
text += "\n\n```\nSubcommands:\n";
let mut commandlist = TwoColumnList::new();
preformat_subcommands(
&mut commandlist,
command,
&subprefix.unwrap_or_else(|| String::from(" ")),
);
text += &commandlist.into_string();
text += "```";
}
format!("**{}**\n\n{}", invocations, text)
} else {
format!("No such command `{}`", command_name)
};
let reply = CreateReply::default()
.content(reply)
.ephemeral(config.ephemeral);
ctx.send(reply).await?;
Ok(())
}
fn preformat_subcommands<U, E>(
commands: &mut TwoColumnList,
command: &crate::Command<U, E>,
prefix: &str,
) {
let as_context_command = command.slash_action.is_none() && command.prefix_action.is_none();
for subcommand in &command.subcommands {
let command = if as_context_command {
let name = format_context_menu_name(subcommand);
if name.is_none() {
continue;
};
name.unwrap()
} else {
format!("{} {}", prefix, subcommand.name)
};
let description = subcommand.description.as_deref().unwrap_or("").to_string();
commands.push_two_colums(command, description);
}
}
fn preformat_command<U, E>(
commands: &mut TwoColumnList,
config: &HelpConfiguration<'_>,
command: &crate::Command<U, E>,
indent: &str,
options_prefix: Option<&str>,
) {
let prefix = if command.slash_action.is_some() {
String::from("/")
} else if command.prefix_action.is_some() {
options_prefix.map(String::from).unwrap_or_default()
} else {
unreachable!();
};
let prefix = format!("{}{}{}", indent, prefix, command.name);
commands.push_two_colums(
prefix.clone(),
command.description.as_deref().unwrap_or("").to_string(),
);
if config.show_subcommands {
preformat_subcommands(commands, command, &prefix)
}
}
async fn generate_all_commands<U, E>(
ctx: crate::Context<'_, U, E>,
config: &HelpConfiguration<'_>,
) -> Result<String, serenity::Error> {
let mut categories = indexmap::IndexMap::<Option<&str>, Vec<&crate::Command<U, E>>>::new();
for cmd in &ctx.framework().options().commands {
categories
.entry(cmd.category.as_deref())
.or_default()
.push(cmd);
}
let options_prefix = get_prefix_from_options(ctx).await;
let mut menu = String::from("```\n");
let mut commandlist = TwoColumnList::new();
for (category_name, commands) in categories {
let commands = commands
.into_iter()
.filter(|cmd| {
!cmd.hide_in_help && (cmd.prefix_action.is_some() || cmd.slash_action.is_some())
})
.collect::<Vec<_>>();
if commands.is_empty() {
continue;
}
commandlist.push_heading(category_name.unwrap_or("Commands"));
for command in commands {
preformat_command(
&mut commandlist,
config,
command,
" ",
options_prefix.as_deref(),
);
}
}
menu += &commandlist.into_string();
if config.show_context_menu_commands {
menu += "\nContext menu commands:\n";
for command in &ctx.framework().options().commands {
let name = format_context_menu_name(command);
if name.is_none() {
continue;
};
let _ = writeln!(menu, " {}", name.unwrap());
}
}
menu += "\n";
menu += config.extra_text_at_bottom;
menu += "\n```";
Ok(menu)
}
async fn help_all_commands<U, E>(
ctx: crate::Context<'_, U, E>,
config: HelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
let menu = generate_all_commands(ctx, &config).await?;
let reply = CreateReply::default()
.content(menu)
.ephemeral(config.ephemeral);
ctx.send(reply).await?;
Ok(())
}
pub async fn help<U, E>(
ctx: crate::Context<'_, U, E>,
command: Option<&str>,
config: HelpConfiguration<'_>,
) -> Result<(), serenity::Error> {
match command {
Some(command) => help_single_command(ctx, command, config).await,
None => help_all_commands(ctx, config).await,
}
}