impl HelpGenerator {
pub fn new(registry: CommandRegistry) -> Self {
Self {
registry,
color: std::io::stdout().is_terminal(),
width: 80, }
}
pub fn with_color(mut self, color: bool) -> Self {
self.color = color;
self
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = width;
self
}
pub fn generate(&self, path: &str) -> String {
match self.registry.find_command(path) {
Some(metadata) => self.format_command_help(metadata),
None => self.format_command_not_found(path),
}
}
pub fn generate_overview(&self) -> String {
let mut out = String::new();
out.push_str(&format!("pmat {}\n", self.registry.version));
out.push_str("Professional project quantitative scaffolding and analysis toolkit\n\n");
out.push_str("USAGE:\n");
out.push_str(" pmat [OPTIONS] <COMMAND>\n\n");
if !self.registry.global_flags.is_empty() {
out.push_str("OPTIONS:\n");
for flag in &self.registry.global_flags {
out.push_str(&self.format_flag(flag));
}
out.push('\n');
}
let mut categories: std::collections::HashMap<&str, Vec<&CommandMetadata>> =
std::collections::HashMap::new();
for cmd in self.registry.commands.values() {
let category = if cmd.category.is_empty() {
"Other"
} else {
&cmd.category
};
categories.entry(category).or_default().push(cmd);
}
out.push_str("COMMANDS:\n");
let mut sorted_categories: Vec<_> = categories.keys().collect();
sorted_categories.sort();
for category in sorted_categories {
let cmds = categories.get(category).expect("internal error");
let mut sorted_cmds: Vec<_> = cmds.iter().collect();
sorted_cmds.sort_by_key(|c| &c.name);
for cmd in sorted_cmds {
let name_with_aliases = if cmd.aliases.is_empty() {
cmd.name.clone()
} else {
format!("{} ({})", cmd.name, cmd.aliases.join(", "))
};
out.push_str(&format!(
" {:30} {}\n",
name_with_aliases,
truncate_str(&cmd.short_description, 45)
));
}
}
out.push_str("\nUse 'pmat <COMMAND> --help' for more information about a command.\n");
out
}
fn format_command_help(&self, cmd: &CommandMetadata) -> String {
let mut out = String::new();
out.push_str(&format!("{}\n", cmd.name));
if !cmd.short_description.is_empty() {
out.push_str(&format!("{}\n", cmd.short_description));
}
out.push('\n');
if !cmd.long_description.is_empty() {
out.push_str(&format!("{}\n\n", cmd.long_description));
}
if let Some(dep) = &cmd.deprecated {
out.push_str(&format!(
"DEPRECATED: {} (since {})\n",
dep.reason, dep.since_version
));
if let Some(replacement) = &dep.replacement {
out.push_str(&format!("Use '{}' instead.\n", replacement));
}
out.push('\n');
}
out.push_str("USAGE:\n");
out.push_str(&format!(" pmat {}", self.format_usage(cmd)));
out.push_str("\n\n");
if !cmd.subcommands.is_empty() {
out.push_str("SUBCOMMANDS:\n");
for sub in &cmd.subcommands {
let name_with_aliases = if sub.aliases.is_empty() {
sub.name.clone()
} else {
format!("{} ({})", sub.name, sub.aliases.join(", "))
};
out.push_str(&format!(
" {:30} {}\n",
name_with_aliases,
truncate_str(&sub.short_description, 45)
));
}
out.push('\n');
}
let positional: Vec<_> = cmd.arguments.iter().filter(|a| a.positional).collect();
let flags: Vec<_> = cmd.arguments.iter().filter(|a| !a.positional).collect();
if !positional.is_empty() {
out.push_str("ARGUMENTS:\n");
for arg in &positional {
out.push_str(&self.format_argument(arg));
}
out.push('\n');
}
if !flags.is_empty() {
out.push_str("OPTIONS:\n");
for arg in &flags {
out.push_str(&self.format_argument(arg));
}
out.push('\n');
}
if !cmd.examples.is_empty() {
out.push_str("EXAMPLES:\n");
for ex in &cmd.examples {
out.push_str(&format!(" # {}\n", ex.description));
out.push_str(&format!(" $ {}\n\n", ex.command));
}
}
if !cmd.related.is_empty() {
out.push_str("SEE ALSO:\n");
out.push_str(&format!(" {}\n", cmd.related.join(", ")));
}
match cmd.execution_time {
ExecutionTime::Slow => {
out.push_str("\nNote: This command may take several seconds to complete.\n");
}
_ => {}
}
out
}
fn format_command_not_found(&self, path: &str) -> String {
let mut out = String::new();
out.push_str(&format!("error: unrecognized command '{}'\n\n", path));
let suggestions = self.find_similar_commands(path, 3);
if !suggestions.is_empty() {
out.push_str("Did you mean:\n");
for (cmd, _score) in suggestions {
out.push_str(&format!(" pmat {}\n", cmd));
}
out.push('\n');
}
out.push_str("Use 'pmat --help' to see all available commands.\n");
out
}
fn format_usage(&self, cmd: &CommandMetadata) -> String {
let mut usage = cmd.name.clone();
if !cmd.subcommands.is_empty() {
usage.push_str(" <COMMAND>");
}
for arg in cmd.arguments.iter().filter(|a| a.positional) {
if arg.required {
usage.push_str(&format!(" <{}>", arg.name.to_uppercase()));
} else {
usage.push_str(&format!(" [{}]", arg.name.to_uppercase()));
}
}
let has_options = cmd.arguments.iter().any(|a| !a.positional);
if has_options {
usage.push_str(" [OPTIONS]");
}
usage
}
fn format_argument(&self, arg: &ArgumentMetadata) -> String {
let mut line = String::new();
let flag_part = if arg.positional {
format!("<{}>", arg.name.to_uppercase())
} else {
let short = arg.short.map(|s| format!("-{}", s));
let long = arg.long.as_ref().map(|l| format!("--{}", l));
match (short, long) {
(Some(s), Some(l)) => format!("{}, {}", s, l),
(Some(s), None) => s,
(None, Some(l)) => l,
(None, None) => arg.name.clone(),
}
};
let value_indicator = match arg.value_type {
ValueType::Boolean => String::new(),
ValueType::Enum => {
if !arg.possible_values.is_empty() {
format!(" <{}>", arg.possible_values.join("|"))
} else {
" <VALUE>".to_string()
}
}
_ => format!(" <{}>", arg.name.to_uppercase()),
};
let full_flag = format!("{}{}", flag_part, value_indicator);
line.push_str(&format!(" {:30} ", full_flag));
line.push_str(&arg.description);
if let Some(default) = &arg.default {
line.push_str(&format!(" [default: {}]", default));
}
if arg.required {
line.push_str(" (required)");
}
if let Some(env) = &arg.env_var {
line.push_str(&format!(" [env: {}]", env));
}
line.push('\n');
line
}
fn format_flag(&self, flag: &crate::cli::registry::FlagMetadata) -> String {
let mut line = String::new();
let flag_part = match (&flag.short, &flag.long) {
(Some(s), Some(l)) => format!("-{}, --{}", s, l),
(Some(s), None) => format!("-{}", s),
(None, Some(l)) => format!("--{}", l),
(None, None) => flag.name.clone(),
};
line.push_str(&format!(" {:30} ", flag_part));
line.push_str(&flag.description);
if let Some(default) = &flag.default {
line.push_str(&format!(" [default: {}]", default));
}
line.push('\n');
line
}
fn find_similar_commands(&self, query: &str, limit: usize) -> Vec<(String, usize)> {
let all_paths = self.registry.all_command_paths();
let mut scored: Vec<(String, usize)> = all_paths
.into_iter()
.map(|path| {
let distance = levenshtein(&path, query);
(path, distance)
})
.collect();
scored.sort_by_key(|(_, score)| *score);
scored.truncate(limit);
scored
.into_iter()
.filter(|(_, score)| *score <= query.len())
.collect()
}
pub fn print_help(&self, path: Option<&str>) -> std::io::Result<()> {
let help = match path {
Some(p) => self.generate(p),
None => self.generate_overview(),
};
if self.color {
self.print_colored(&help)
} else {
print!("{}", help);
Ok(())
}
}
fn print_colored(&self, text: &str) -> std::io::Result<()> {
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const YELLOW: &str = "\x1b[33m";
const RED: &str = "\x1b[31m";
const CYAN: &str = "\x1b[36m";
const GREEN: &str = "\x1b[32m";
for line in text.lines() {
if line.starts_with("USAGE:")
|| line.starts_with("COMMANDS:")
|| line.starts_with("OPTIONS:")
|| line.starts_with("ARGUMENTS:")
|| line.starts_with("EXAMPLES:")
|| line.starts_with("SEE ALSO:")
|| line.starts_with("SUBCOMMANDS:")
{
println!("{BOLD}{YELLOW}{line}{RESET}");
} else if line.starts_with("DEPRECATED:") {
println!("{BOLD}{RED}{line}{RESET}");
} else if line.starts_with(" #") {
println!("{CYAN}{line}{RESET}");
} else if line.starts_with(" $") {
println!("{GREEN}{line}{RESET}");
} else if line.starts_with("error:") {
println!("{BOLD}{RED}{line}{RESET}");
} else {
println!("{line}");
}
}
Ok(())
}
}