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