chkc_help/
help_command.rs

1//! helpers for wiring clap commands into nice terminal help output.
2
3use clap::{Args, Command};
4use termimad::crossterm::style::Stylize;
5
6use crate::{DocRegistry, HelpPage, HelpTheme};
7
8/// args for your help command.
9#[derive(Args, Debug, Clone)]
10pub struct HelpArgs {
11    /// Command to print information about
12    pub topic: Vec<String>,
13}
14
15/// resolved help target (a command path, a guide, or the program root).
16pub enum HelpTarget<'a> {
17    Command { path: String, cmd: &'a Command },
18    Guide { path: String },
19    Program { cmd: &'a Command },
20}
21
22/// walk the clap tree to find the thing the user asked for.
23///
24/// `guide` is special-cased so `app help foo bar guide` opens the `foo.bar` guide instead of looking
25/// for a `guide` subcommand.
26pub fn resolve_help<'a>(root: &'a Command, topic: &[String]) -> anyhow::Result<HelpTarget<'a>> {
27    let mut cmd = root;
28    let mut path = Vec::new();
29
30    if topic.is_empty() {
31        return Ok(HelpTarget::Program { cmd });
32    }
33
34    for segment in topic {
35        if segment == "guide" {
36            return Ok(HelpTarget::Guide {
37                path: path.join("."),
38            });
39        }
40
41        cmd = cmd
42            .get_subcommands()
43            .find(|c| c.get_name() == segment)
44            .ok_or_else(|| anyhow::anyhow!("Unknown help topic"))?;
45
46        path.push(segment.clone());
47    }
48
49    Ok(HelpTarget::Command {
50        path: path.join("."),
51        cmd,
52    })
53}
54
55/// handle a `help <topic>` command without custom docs.
56pub fn help_command(
57    app_name: &str,
58    app_version: Option<&str>,
59    root: &Command,
60    theme: &HelpTheme,
61    args: &HelpArgs,
62) -> anyhow::Result<()> {
63    run_help_topic(app_name, app_version, root, &DocRegistry::new(), theme, &args.topic)
64}
65
66/// like [`help_command`] but with an attached [`DocRegistry`].
67pub fn help_command_docs(
68    app_name: &str,
69    app_version: Option<&str>,
70    root: &Command,
71    docs: &DocRegistry,
72    theme: &HelpTheme,
73    args: &HelpArgs,
74) -> anyhow::Result<()> {
75    run_help_topic(app_name, app_version, root, docs, theme, &args.topic)
76}
77
78/// show program help when no command was provided.
79pub fn help_command_program(
80    app_name: &str,
81    app_version: Option<&str>,
82    root: &Command,
83    theme: &HelpTheme,
84) -> anyhow::Result<()> {
85    run_help_topic(app_name, app_version, root, &DocRegistry::new(), theme, &Vec::new())
86}
87
88/// program help with attached docs.
89pub fn help_command_program_docs(
90    app_name: &str,
91    app_version: Option<&str>,
92    root: &Command,
93    docs: &DocRegistry,
94    theme: &HelpTheme,
95) -> anyhow::Result<()> {
96    run_help_topic(app_name, app_version, root, docs, theme, &Vec::new())
97}
98
99/// shared execution path for all help entrypoints.
100pub fn run_help_topic(
101    app_name: &str,
102    app_version: Option<&str>,
103    root: &Command,
104    docs: &DocRegistry,
105    theme: &HelpTheme,
106    topic: &[String],
107) -> anyhow::Result<()> {
108    let target = resolve_help(root, topic)?;
109
110    match target {
111        HelpTarget::Command { path, cmd } => {
112            let page = HelpPage::from_clap(
113                std::env::current_exe()
114                    .expect("Failed to get executable path")
115                    .file_name()
116                    .expect("Failed to get executable name")
117                    .to_str()
118                    .unwrap(),
119                app_version,
120                &path,
121                cmd,
122            )
123            .with_docs(docs.command(&path));
124
125            crate::render_command_help(theme, &page);
126        }
127        HelpTarget::Guide { path } => {
128            let path = if path.is_empty() {
129                app_name.to_string()
130            } else {
131                path
132            };
133            if let Some(guide) = docs.guide(&path) {
134                let (_, rows) = termimad::crossterm::terminal::size().unwrap();
135                if guide.lines().count() > rows.into() {
136                    crate::run_scrollable_help(theme, app_name, guide.to_string())?;
137                } else {
138                    println!("{}", theme.skin.term_text(&guide));
139                }
140            } else {
141                println!(
142                    "Guide for {} was {}.",
143                    &path.with(theme.accent).bold(),
144                    "not found".red().bold()
145                )
146            }
147        }
148        HelpTarget::Program { cmd } => {
149            let page = HelpPage::from_clap(app_name, app_version, "", cmd)
150                .with_docs(docs.command(""));
151
152            crate::render_command_help(theme, &page);
153        }
154    }
155
156    Ok(())
157}