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    out.push_str("\n    --search <keyword>  Search all commands and guides by keyword");
67    out.push_str("\n    tree                Display full command tree");
68    if has_guide {
69        out.push_str("\n    guide               Built-in guides for AI agents and developers");
70    }
71    out
72}
73
74/// Builds a "Next actions" section appended to bare-root human help. Returns an
75/// empty string when there are no actions so the help output is unchanged.
76#[must_use]
77pub fn render_next_actions_human(actions: &[NextAction]) -> String {
78    if actions.is_empty() {
79        return String::new();
80    }
81    let max_width = actions
82        .iter()
83        .map(|action| action.command.len())
84        .max()
85        .unwrap_or_default();
86    let mut out = String::from("\n\n  Suggested next actions:");
87    for action in actions {
88        out.push_str(&format!(
89            "\n    {:<width$}  {}",
90            action.command,
91            action.description,
92            width = max_width
93        ));
94    }
95    out
96}