Skip to main content

cli_engine/cli/
help.rs

1use std::collections::BTreeMap;
2
3use crate::output::NextAction;
4
5/// Help template for the root command. Renders the curated long-about (which
6/// already lists every command grouped by category) and usage, but omits
7/// clap's auto-generated subcommand list and the global options wall — those
8/// are noise on the top-level navigation page.
9pub const ROOT_HELP_TEMPLATE: &str = "\
10{before-help}{about-with-newline}
11{usage-heading} {usage}{after-help}";
12
13/// Help template for group (noun) commands. Keeps the child command list, which
14/// is the point of a group page, but drops the global options wall. Leaf
15/// commands keep clap's default template so their flags remain documented.
16pub const GROUP_HELP_TEMPLATE: &str = "\
17{before-help}{about-with-newline}
18{usage-heading} {usage}
19
20Commands:
21{subcommands}{after-help}";
22
23/// One module row in root long help.
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct ModuleHelpEntry {
26    /// Help category label.
27    pub category: String,
28    /// Top-level module group name.
29    pub name: String,
30    /// One-line module description.
31    pub short: String,
32}
33
34/// Builds the root long help text from module categories and built-in command hints.
35#[must_use]
36pub fn build_root_long(intro: &str, entries: &[ModuleHelpEntry], has_guide: bool) -> String {
37    // Group by category. `BTreeMap` keeps categories in sorted order so the
38    // rendered sections are deterministic regardless of registration order.
39    let mut by_category = BTreeMap::<&str, Vec<&ModuleHelpEntry>>::new();
40    for entry in entries {
41        by_category
42            .entry(entry.category.as_str())
43            .or_default()
44            .push(entry);
45    }
46
47    let max_width = entries
48        .iter()
49        .map(|entry| entry.name.len())
50        .max()
51        .unwrap_or_default();
52    let mut out = intro.to_owned();
53    for (category, category_entries) in &mut by_category {
54        category_entries.sort_by(|left, right| left.name.cmp(&right.name));
55        out.push_str(&format!("\n\n  {category}:"));
56        for entry in category_entries {
57            out.push_str(&format!(
58                "\n    {:<width$}  {}",
59                entry.name,
60                entry.short,
61                width = max_width
62            ));
63        }
64    }
65    out.push_str("\n\n  Find Commands:");
66    let mut find_commands: Vec<(&str, &str)> = vec![
67        (
68            "search <keyword>",
69            "Search all commands and guides by keyword",
70        ),
71        ("tree", "Display full command tree"),
72        ("completion", "Generate or install shell completion scripts"),
73    ];
74    if has_guide {
75        find_commands.push(("guide", "Built-in guides for AI agents and developers"));
76    }
77    let find_commands_width = find_commands
78        .iter()
79        .map(|(label, _)| label.len())
80        .max()
81        .unwrap_or_default();
82    for (label, description) in find_commands {
83        out.push_str(&format!(
84            "\n    {:<find_commands_width$}  {description}",
85            label
86        ));
87    }
88    out
89}
90
91/// Builds a "Next actions" section appended to bare-root human help. Returns an
92/// empty string when there are no actions so the help output is unchanged.
93#[must_use]
94pub fn render_next_actions_human(actions: &[NextAction]) -> String {
95    if actions.is_empty() {
96        return String::new();
97    }
98    let max_width = actions
99        .iter()
100        .map(|action| action.command.len())
101        .max()
102        .unwrap_or_default();
103    let mut out = String::from("\n\n  Suggested next actions:");
104    for action in actions {
105        out.push_str(&format!(
106            "\n    {:<width$}  {}",
107            action.command,
108            action.description,
109            width = max_width
110        ));
111    }
112    out
113}