add prettier CommandTree formatting for help command and usage hint

This commit is contained in:
user622628252416 2024-08-21 17:59:54 +02:00
parent 59a4fe525d
commit 5041b441eb
9 changed files with 185 additions and 81 deletions

View File

@ -15,7 +15,7 @@ use crate::commands::CommandSender;
use crate::commands::CommandSender::Player;
use crate::entity::player::GameMode;
pub(crate) const NAME: &str = "gamemode";
const NAMES: [&str; 1] = ["gamemode"];
const DESCRIPTION: &str = "Change a player's gamemode.";
@ -57,7 +57,7 @@ pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result<GameMode, Inva
}
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(DESCRIPTION).with_child(
CommandTree::new(NAMES, DESCRIPTION).with_child(
require(&|sender| sender.permission_lvl() >= 2).with_child(
argument(ARG_GAMEMODE, consume_arg_gamemode)
.with_child(

View File

@ -5,8 +5,7 @@ use crate::commands::tree_builder::argument;
use crate::commands::{dispatcher_init, CommandSender, DISPATCHER};
use pumpkin_text::TextComponent;
pub(crate) const NAME: &str = "help";
pub(crate) const ALIAS: &str = "?";
const NAMES: [&str; 3] = ["help", "h", "?"];
const DESCRIPTION: &str = "Print a help message.";
@ -40,7 +39,7 @@ fn parse_arg_command<'a>(
}
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(DESCRIPTION)
CommandTree::new(NAMES, DESCRIPTION)
.with_child(
argument(ARG_COMMAND, consume_arg_command).execute(&|sender, args| {
let dispatcher = DISPATCHER.get_or_init(dispatcher_init);
@ -48,10 +47,8 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
let (name, tree) = parse_arg_command(args, dispatcher)?;
sender.send_message(TextComponent::text(&format!(
"{} - {} Usage:{}",
name,
tree.description,
tree.paths_formatted(name)
"{} - {} Usage: {}",
name, tree.description, tree
)));
Ok(())
@ -60,12 +57,15 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
.execute(&|sender, _args| {
let dispatcher = DISPATCHER.get_or_init(dispatcher_init);
for (name, tree) in &dispatcher.commands {
let mut names: Vec<&str> = dispatcher.commands.keys().copied().collect();
names.sort();
for name in names {
let tree = &dispatcher.commands[name];
sender.send_message(TextComponent::text(&format!(
"{} - {} Usage:{}",
name,
tree.description,
tree.paths_formatted(name)
"{} - {} Usage: {}",
name, tree.description, tree
)));
}

View File

@ -4,12 +4,12 @@ use pumpkin_text::{color::NamedColor, TextComponent};
use crate::commands::tree::CommandTree;
pub(crate) const NAME: &str = "pumpkin";
const NAMES: [&str; 1] = ["pumpkin"];
const DESCRIPTION: &str = "Display information about Pumpkin.";
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(DESCRIPTION).execute(&|sender, _| {
CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, _| {
let version = env!("CARGO_PKG_VERSION");
let description = env!("CARGO_PKG_DESCRIPTION");

View File

@ -1,12 +1,12 @@
use crate::commands::tree::CommandTree;
use crate::commands::tree_builder::require;
pub(crate) const NAME: &str = "stop";
const NAMES: [&str; 1] = ["stop"];
const DESCRIPTION: &str = "Stop the server.";
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(DESCRIPTION).with_child(
CommandTree::new(NAMES, DESCRIPTION).with_child(
require(&|sender| sender.permission_lvl() >= 4)
.execute(&|_sender, _args| std::process::exit(0)),
)

View File

@ -49,10 +49,7 @@ impl<'a> CommandDispatcher<'a> {
}
}
Err(format!(
"Invalid Syntax. Usage:{}",
tree.paths_formatted(key)
))
Err(format!("Invalid Syntax. Usage: {}", tree))
}
fn try_is_fitting_path(
@ -99,4 +96,17 @@ impl<'a> CommandDispatcher<'a> {
Ok(false)
}
/// Register a command with the dispatcher.
pub(crate) fn register(&mut self, tree: CommandTree<'a>) {
let mut names = tree.names.iter();
let primary_name = names.next().expect("at least one name must be provided");
for &name in names {
self.commands.insert(name, tree.clone());
}
self.commands.insert(primary_name, tree);
}
}

View File

@ -12,6 +12,7 @@ mod cmd_stop;
mod dispatcher;
mod tree;
mod tree_builder;
mod tree_format;
pub enum CommandSender<'a> {
Rcon(&'a mut Vec<String>),
@ -70,15 +71,16 @@ static DISPATCHER: OnceLock<CommandDispatcher> = OnceLock::new();
/// create [CommandDispatcher] instance for [DISPATCHER]
fn dispatcher_init<'a>() -> CommandDispatcher<'a> {
let mut map = HashMap::new();
let mut dispatcher = CommandDispatcher {
commands: HashMap::new(),
};
map.insert(cmd_pumpkin::NAME, cmd_pumpkin::init_command_tree());
map.insert(cmd_gamemode::NAME, cmd_gamemode::init_command_tree());
map.insert(cmd_stop::NAME, cmd_stop::init_command_tree());
map.insert(cmd_help::NAME, cmd_help::init_command_tree());
map.insert(cmd_help::ALIAS, cmd_help::init_command_tree());
dispatcher.register(cmd_pumpkin::init_command_tree());
dispatcher.register(cmd_gamemode::init_command_tree());
dispatcher.register(cmd_stop::init_command_tree());
dispatcher.register(cmd_help::init_command_tree());
CommandDispatcher { commands: map }
dispatcher
}
pub fn handle_command(sender: &mut CommandSender, cmd: &str) {

View File

@ -2,6 +2,7 @@ use std::collections::{HashMap, VecDeque};
use crate::commands::dispatcher::InvalidTreeError;
use crate::commands::CommandSender;
/// see [crate::commands::tree_builder::argument]
pub(crate) type RawArgs<'a> = Vec<&'a str>;
@ -11,11 +12,13 @@ pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, String>;
/// see [crate::commands::tree_builder::argument]
pub(crate) type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option<String>;
#[derive(Clone)]
pub(crate) struct Node<'a> {
pub(crate) children: Vec<usize>,
pub(crate) node_type: NodeType<'a>,
}
#[derive(Clone)]
pub(crate) enum NodeType<'a> {
ExecuteLeaf {
run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync),
@ -31,9 +34,11 @@ pub(crate) enum NodeType<'a> {
},
}
#[derive(Clone)]
pub(crate) struct CommandTree<'a> {
pub(crate) nodes: Vec<Node<'a>>,
pub(crate) children: Vec<usize>,
pub(crate) names: Vec<&'a str>,
pub(crate) description: &'a str,
}
@ -51,57 +56,6 @@ impl<'a> CommandTree<'a> {
todo,
}
}
/// format possible paths as [String], using ```name``` as the command name
///
/// todo: merge into single line
pub(crate) fn paths_formatted(&'a self, name: &str) -> String {
let paths: Vec<Vec<&NodeType>> = self
.iter_paths()
.map(|path| path.iter().map(|&i| &self.nodes[i].node_type).collect())
.collect();
let len = paths
.iter()
.map(|path| {
path.iter()
.map(|node| match node {
NodeType::ExecuteLeaf { .. } => 0,
NodeType::Literal { string } => string.len() + 1,
NodeType::Argument { name, .. } => name.len() + 3,
NodeType::Require { .. } => 0,
})
.sum::<usize>()
+ name.len()
+ 2
})
.sum::<usize>();
let mut s = String::with_capacity(len);
for path in paths.iter() {
s.push(if paths.len() > 1 { '\n' } else { ' ' });
s.push('/');
s.push_str(name);
for node in path {
match node {
NodeType::Literal { string } => {
s.push(' ');
s.push_str(string);
}
NodeType::Argument { name, .. } => {
s.push(' ');
s.push('<');
s.push_str(name);
s.push('>');
}
_ => {}
}
}
}
s
}
}
struct TraverseAllPathsIter<'a> {

View File

@ -11,10 +11,23 @@ impl<'a> CommandTree<'a> {
self
}
pub fn new(description: &'a str) -> Self {
/// provide at least one name
pub fn new<const NAME_COUNT: usize>(
names: [&'a str; NAME_COUNT],
description: &'a str,
) -> Self {
assert!(NAME_COUNT > 0);
let mut names_vec = Vec::with_capacity(NAME_COUNT);
for name in names {
names_vec.push(name);
}
Self {
nodes: Vec::new(),
children: Vec::new(),
names: names_vec,
description,
}
}

View File

@ -0,0 +1,125 @@
use crate::commands::tree::{CommandTree, Node, NodeType};
use std::collections::VecDeque;
use std::fmt::{Display, Formatter, Write};
trait IsVisible {
/// whether node should be printed in help command/usage hint
fn is_visible(&self) -> bool;
}
impl<'a> IsVisible for Node<'a> {
fn is_visible(&self) -> bool {
match self.node_type {
NodeType::ExecuteLeaf { .. } => false,
NodeType::Literal { .. } => true,
NodeType::Argument { .. } => true,
NodeType::Require { .. } => false,
}
}
}
impl<'a> Display for Node<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.node_type {
NodeType::Literal { string } => {
f.write_str(string)?;
}
NodeType::Argument { name, .. } => {
f.write_char('<')?;
f.write_str(name)?;
f.write_char('>')?;
}
_ => {}
};
Ok(())
}
}
fn flatten_require_nodes(nodes: &[Node], children: &[usize]) -> Vec<usize> {
let mut new_children = Vec::with_capacity(children.len());
for &i in children {
let node = &nodes[i];
match &node.node_type {
NodeType::Require { .. } => {
new_children.extend(flatten_require_nodes(nodes, node.children.as_slice()))
}
_ => new_children.push(i),
}
}
new_children
}
impl<'a> Display for CommandTree<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_char('/')?;
f.write_str(self.names[0])?;
let mut todo = VecDeque::<&[usize]>::with_capacity(self.children.len());
todo.push_back(&self.children);
loop {
let Some(children) = todo.pop_front() else {
break;
};
let flattened_children = flatten_require_nodes(&self.nodes, children);
let visible_children = flattened_children
.iter()
.copied()
.filter(|&i| self.nodes[i].is_visible())
.collect::<Vec<_>>();
if visible_children.is_empty() {
break;
};
f.write_char(' ')?;
let is_optional = flattened_children
.iter()
.map(|&i| &self.nodes[i].node_type)
.any(|node| matches!(node, NodeType::ExecuteLeaf { .. }));
if is_optional {
f.write_char('[')?;
}
match visible_children.as_slice() {
[] => unreachable!(),
[i] => {
let node = &self.nodes[*i];
node.fmt(f)?;
todo.push_back(&node.children);
}
_ => {
// todo: handle cases where one of these nodes has visible children
f.write_char('(')?;
let mut iter = visible_children.iter().map(|&i| &self.nodes[i]);
if let Some(node) = iter.next() {
node.fmt(f)?;
}
for node in iter {
f.write_char('|')?;
node.fmt(f)?;
}
f.write_char(')')?;
}
}
if is_optional {
f.write_char(']')?;
}
}
Ok(())
}
}