1use clap::ArgAction;
2use roff::{bold, italic, roman, Inline, Roff};
3
4pub(crate) fn subcommand_heading(cmd: &clap::Command) -> &str {
5    match cmd.get_subcommand_help_heading() {
6        Some(title) => title,
7        None => "SUBCOMMANDS",
8    }
9}
10
11pub(crate) fn about(roff: &mut Roff, cmd: &clap::Command) {
12    let s = match cmd.get_about().or_else(|| cmd.get_long_about()) {
13        Some(about) => format!("{} - {}", cmd.get_name(), about),
14        None => cmd.get_name().to_string(),
15    };
16    roff.text([roman(s)]);
17}
18
19pub(crate) fn description(roff: &mut Roff, cmd: &clap::Command) {
20    if let Some(about) = cmd.get_long_about().or_else(|| cmd.get_about()) {
21        for line in about.to_string().lines() {
22            if line.trim().is_empty() {
23                roff.control("PP", []);
24            } else {
25                roff.text([roman(line)]);
26            }
27        }
28    }
29}
30
31pub(crate) fn synopsis(roff: &mut Roff, cmd: &clap::Command) {
32    let mut line = vec![bold(cmd.get_name()), roman(" ")];
33
34    for opt in cmd.get_arguments().filter(|i| !i.is_hide_set()) {
35        let (lhs, rhs) = option_markers(opt);
36        match (opt.get_short(), opt.get_long()) {
37            (Some(short), Some(long)) => {
38                line.push(roman(lhs));
39                line.push(bold(format!("-{short}")));
40                line.push(roman("|"));
41                line.push(bold(format!("--{long}",)));
42                line.push(roman(rhs));
43            }
44            (Some(short), None) => {
45                line.push(roman(lhs));
46                line.push(bold(format!("-{short} ")));
47                line.push(roman(rhs));
48            }
49            (None, Some(long)) => {
50                line.push(roman(lhs));
51                line.push(bold(format!("--{long}")));
52                line.push(roman(rhs));
53            }
54            (None, None) => continue,
55        };
56
57        if matches!(opt.get_action(), ArgAction::Count) {
58            line.push(roman("..."))
59        }
60        line.push(roman(" "));
61    }
62
63    for arg in cmd.get_positionals() {
64        let (lhs, rhs) = option_markers(arg);
65        line.push(roman(lhs));
66        if let Some(value) = arg.get_value_names() {
67            line.push(italic(value.join(" ")));
68        } else {
69            line.push(italic(arg.get_id().as_str()));
70        }
71        line.push(roman(rhs));
72        line.push(roman(" "));
73    }
74
75    if cmd.has_subcommands() {
76        let (lhs, rhs) = subcommand_markers(cmd);
77        line.push(roman(lhs));
78        line.push(italic(
79            cmd.get_subcommand_value_name()
80                .unwrap_or_else(|| subcommand_heading(cmd))
81                .to_lowercase(),
82        ));
83        line.push(roman(rhs));
84    }
85
86    roff.text(line);
87}
88
89pub(crate) fn options(roff: &mut Roff, cmd: &clap::Command) {
90    let items: Vec<_> = cmd.get_arguments().filter(|i| !i.is_hide_set()).collect();
91
92    for opt in items.iter().filter(|a| !a.is_positional()) {
93        let mut header = match (opt.get_short(), opt.get_long()) {
94            (Some(short), Some(long)) => {
95                vec![short_option(short), roman(", "), long_option(long)]
96            }
97            (Some(short), None) => vec![short_option(short)],
98            (None, Some(long)) => vec![long_option(long)],
99            (None, None) => vec![],
100        };
101
102        if opt.get_action().takes_values() {
103            if let Some(value) = &opt.get_value_names() {
104                header.push(roman("="));
105                header.push(italic(value.join(" ")));
106            }
107        }
108
109        if let Some(defs) = option_default_values(opt) {
110            header.push(roman(" "));
111            header.push(roman(defs));
112        }
113
114        let mut body = vec![];
115        let mut arg_help_written = false;
116        if let Some(help) = option_help(opt) {
117            arg_help_written = true;
118            body.push(roman(help.to_string()));
119        }
120
121        roff.control("TP", []);
122        roff.text(header);
123        roff.text(body);
124
125        if let Some((possible_values_text, with_help)) = get_possible_values(opt) {
126            if arg_help_written {
127                // It looks nice to have a separation between the help and the values
128                roff.text([Inline::LineBreak]);
129            }
130            if with_help {
131                roff.text([Inline::LineBreak, italic("Possible values:")]);
132
133                // Need to indent twice to get it to look right, because .TP heading indents, but
134                // that indent doesn't Carry over to the .IP for the bullets. The standard shift
135                // size is 7 for terminal devices
136                roff.control("RS", ["14"]);
137                for line in possible_values_text {
138                    roff.control("IP", ["\\(bu", "2"]);
139                    roff.text([roman(line)]);
140                }
141                roff.control("RE", []);
142            } else {
143                let possible_value_text: Vec<Inline> = vec![
144                    Inline::LineBreak,
145                    roman("["),
146                    italic("possible values: "),
147                    roman(possible_values_text.join(", ")),
148                    roman("]"),
149                ];
150                roff.text(possible_value_text);
151            }
152        }
153
154        if let Some(env) = option_environment(opt) {
155            roff.control("RS", []);
156            roff.text(env);
157            roff.control("RE", []);
158        }
159    }
160
161    for pos in items.iter().filter(|a| a.is_positional()) {
162        let mut header = vec![];
163        let (lhs, rhs) = option_markers(pos);
164        header.push(roman(lhs));
165        if let Some(value) = pos.get_value_names() {
166            header.push(italic(value.join(" ")));
167        } else {
168            header.push(italic(pos.get_id().as_str()));
169        };
170        header.push(roman(rhs));
171
172        if let Some(defs) = option_default_values(pos) {
173            header.push(roman(format!(" {defs}")));
174        }
175
176        let mut body = vec![];
177        let mut arg_help_written = false;
178        if let Some(help) = option_help(pos) {
179            body.push(roman(help.to_string()));
180            arg_help_written = true;
181        }
182
183        roff.control("TP", []);
184        roff.text(header);
185        roff.text(body);
186
187        if let Some(env) = option_environment(pos) {
188            roff.control("RS", []);
189            roff.text(env);
190            roff.control("RE", []);
191        }
192        // If possible options are available
193        if let Some((possible_values_text, with_help)) = get_possible_values(pos) {
194            if arg_help_written {
195                // It looks nice to have a separation between the help and the values
196                roff.text([Inline::LineBreak]);
197            }
198            if with_help {
199                roff.text([Inline::LineBreak, italic("Possible values:")]);
200
201                // Need to indent twice to get it to look right, because .TP heading indents, but
202                // that indent doesn't Carry over to the .IP for the bullets. The standard shift
203                // size is 7 for terminal devices
204                roff.control("RS", ["14"]);
205                for line in possible_values_text {
206                    roff.control("IP", ["\\(bu", "2"]);
207                    roff.text([roman(line)]);
208                }
209                roff.control("RE", []);
210            } else {
211                let possible_value_text: Vec<Inline> = vec![
212                    Inline::LineBreak,
213                    roman("["),
214                    italic("possible values: "),
215                    roman(possible_values_text.join(", ")),
216                    roman("]"),
217                ];
218                roff.text(possible_value_text);
219            }
220        }
221    }
222}
223
224pub(crate) fn subcommands(roff: &mut Roff, cmd: &clap::Command, section: &str) {
225    for sub in cmd.get_subcommands().filter(|s| !s.is_hide_set()) {
226        roff.control("TP", []);
227
228        let name = format!(
229            "{}-{}({})",
230            cmd.get_display_name().unwrap_or_else(|| cmd.get_name()),
231            sub.get_name(),
232            section
233        );
234        roff.text([roman(name)]);
235
236        if let Some(about) = sub.get_about().or_else(|| sub.get_long_about()) {
237            for line in about.to_string().lines() {
238                roff.text([roman(line)]);
239            }
240        }
241    }
242}
243
244pub(crate) fn version(cmd: &clap::Command) -> String {
245    format!(
246        "v{}",
247        cmd.get_long_version()
248            .or_else(|| cmd.get_version())
249            .unwrap()
250    )
251}
252
253pub(crate) fn after_help(roff: &mut Roff, cmd: &clap::Command) {
254    if let Some(about) = cmd.get_after_long_help().or_else(|| cmd.get_after_help()) {
255        for line in about.to_string().lines() {
256            roff.text([roman(line)]);
257        }
258    }
259}
260
261fn subcommand_markers(cmd: &clap::Command) -> (&'static str, &'static str) {
262    markers(cmd.is_subcommand_required_set())
263}
264
265fn option_markers(opt: &clap::Arg) -> (&'static str, &'static str) {
266    markers(opt.is_required_set())
267}
268
269fn markers(required: bool) -> (&'static str, &'static str) {
270    if required {
271        ("<", ">")
272    } else {
273        ("[", "]")
274    }
275}
276
277fn short_option(opt: char) -> Inline {
278    bold(format!("-{opt}"))
279}
280
281fn long_option(opt: &str) -> Inline {
282    bold(format!("--{opt}"))
283}
284
285fn option_help(opt: &clap::Arg) -> Option<&clap::builder::StyledStr> {
286    if !opt.is_hide_long_help_set() {
287        let long_help = opt.get_long_help();
288        if long_help.is_some() {
289            return long_help;
290        }
291    }
292    if !opt.is_hide_short_help_set() {
293        return opt.get_help();
294    }
295
296    None
297}
298
299fn option_environment(opt: &clap::Arg) -> Option<Vec<Inline>> {
300    if opt.is_hide_env_set() {
301        return None;
302    } else if let Some(env) = opt.get_env() {
303        return Some(vec![
304            roman("May also be specified with the "),
305            bold(env.to_string_lossy().into_owned()),
306            roman(" environment variable. "),
307        ]);
308    }
309
310    None
311}
312
313fn option_default_values(opt: &clap::Arg) -> Option<String> {
314    if opt.is_hide_default_value_set() || !opt.get_action().takes_values() {
315        return None;
316    } else if !opt.get_default_values().is_empty() {
317        let values = opt
318            .get_default_values()
319            .iter()
320            .map(|s| s.to_string_lossy())
321            .collect::<Vec<_>>()
322            .join(",");
323
324        return Some(format!("[default: {values}]"));
325    }
326
327    None
328}
329
330fn get_possible_values(arg: &clap::Arg) -> Option<(Vec<String>, bool)> {
331    let possibles = &arg.get_possible_values();
332    let possibles: Vec<&clap::builder::PossibleValue> =
333        possibles.iter().filter(|pos| !pos.is_hide_set()).collect();
334
335    if !(possibles.is_empty() || arg.is_hide_possible_values_set()) {
336        return Some(format_possible_values(&possibles));
337    }
338    None
339}
340
341fn format_possible_values(possibles: &Vec<&clap::builder::PossibleValue>) -> (Vec<String>, bool) {
342    let mut lines = vec![];
343    let with_help = possibles.iter().any(|p| p.get_help().is_some());
344    if with_help {
345        for value in possibles {
346            let val_name = value.get_name();
347            match value.get_help() {
348                Some(help) => lines.push(format!("{val_name}: {help}")),
349                None => lines.push(val_name.to_string()),
350            }
351        }
352    } else {
353        lines.append(&mut possibles.iter().map(|p| p.get_name().to_string()).collect());
354    }
355    (lines, with_help)
356}
357