chkc_help/
help_page.rs

1//! typed snapshot of clap data that the renderer can turn into markdown.
2
3use clap::builder::OsStr;
4
5use crate::doc_registry::CommandDoc;
6
7/// everything we need to print help for a command path.
8#[derive(Debug, Clone)]
9pub struct HelpPage {
10    /// Name of the binary / application (e.g. "git")
11    pub app_name: String,
12
13    /// Application version (from clap metadata).
14    pub version: Option<String>,
15
16    /// Full command path (e.g. "commit main")
17    pub path: String,
18
19    /// One-line summary (from clap)
20    pub summary: Option<String>,
21
22    /// Optional longer description
23    pub description: Option<String>,
24
25    /// Usage string (from clap)
26    pub usage: String,
27
28    /// Positional arguments
29    pub positionals: Vec<HelpArg>,
30
31    /// Flags and options
32    pub options: Vec<HelpOption>,
33
34    /// Subcommands (for category-level help)
35    pub subcommands: Vec<HelpSubcommand>,
36
37    /// Examples
38    pub examples: Vec<String>,
39
40    /// Notes / tips / caveats
41    pub notes: Vec<String>,
42}
43
44/// a flag/option with optional value and default info.
45#[derive(Debug, Clone)]
46pub struct HelpOption {
47    pub short: Option<char>,
48    pub long: Option<String>,
49    pub value: Option<String>,
50    pub description: String,
51    pub default: String,
52}
53
54/// positional argument.
55#[derive(Debug, Clone)]
56pub struct HelpArg {
57    pub name: String,
58    pub description: Option<String>,
59    pub required: bool,
60    pub multiple: bool,
61}
62
63/// child command for category-level help.
64#[derive(Debug, Clone)]
65pub struct HelpSubcommand {
66    pub name: String,
67    pub summary: Option<String>,
68}
69
70impl HelpPage {
71    /// build a page straight from a `clap::Command`.
72    pub fn from_clap(
73        app_name: &str,
74        version: Option<&str>,
75        path: &str,
76        cmd: &clap::Command,
77    ) -> Self {
78        let positionals = cmd
79            .get_positionals()
80            .map(|arg| HelpArg {
81                name: arg.get_id().to_string(),
82                description: arg.get_help().map(|s| s.to_string()),
83                required: arg.is_required_set(),
84                multiple: arg
85                    .get_num_args()
86                    .map(|n| n.min_values() != n.max_values() || 1 < n.min_values())
87                    .unwrap_or_default(),
88            })
89            .collect();
90
91        let options = cmd
92            .get_arguments()
93            .filter(|a| !a.is_positional())
94            .map(|arg| HelpOption {
95                short: arg.get_short(),
96                long: arg.get_long().map(str::to_string),
97                value: if arg.get_action().takes_values() {
98                    arg.get_value_names()
99                        .and_then(|v| v.first())
100                        .map(|v| v.to_string())
101                } else {
102                    None
103                },
104                description: arg.get_help().unwrap_or_default().to_string(),
105                default: arg
106                    .get_default_values()
107                    .join(&OsStr::from(", "))
108                    .to_str()
109                    .unwrap_or_default()
110                    .to_string(),
111            })
112            .collect();
113
114        let subcommands = cmd
115            .get_subcommands()
116            .map(|sc| HelpSubcommand {
117                name: sc.get_name().to_string(),
118                summary: sc.get_about().map(|s| s.to_string()),
119            })
120            .collect();
121
122        Self {
123            app_name: app_name.to_string(),
124            version: version.map(|s| s.to_string()),
125            path: path.to_string(),
126            summary: cmd.get_about().map(|s| s.to_string()),
127            description: cmd.get_long_about().map(|s| s.to_string()),
128            usage: cmd.clone().render_usage().to_string(),
129            positionals,
130            options,
131            subcommands,
132            examples: Vec::new(),
133            notes: Vec::new(),
134        }
135    }
136
137    /// merge data from a [`CommandDoc`], falling back to clap metadata when missing.
138    pub fn with_docs(mut self, doc: Option<&CommandDoc>) -> Self {
139        if let Some(doc) = doc {
140            self.description = doc.description.clone().or(self.description);
141            self.examples = doc.examples.clone();
142            self.notes = doc.notes.clone();
143        }
144        self
145    }
146}