use std::fmt::Write as _;
use std::io::{self, Write};
use clap::{Command, CommandFactory};
use comfy_table::presets::UTF8_FULL_CONDENSED;
use comfy_table::{ContentArrangement, Table};
use crate::color::ColorMode;
use crate::layout;
use crate::style::{HEADING, MUTED, OPTION, VALUE, styled};
const NARROW_HELP_WIDTH: u16 = 64;
pub fn try_emit<C: CommandFactory>() -> io::Result<bool> {
let args = std::env::args_os()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
try_emit_from::<C>(&args)
}
pub fn try_emit_from<C: CommandFactory>(args: &[String]) -> io::Result<bool> {
let wants_help = args.len() == 1 || args.iter().any(|arg| arg == "-h" || arg == "--help");
if !wants_help {
return Ok(false);
}
let color = ColorMode::from_args(args.iter().map(String::as_str));
let mut root = C::command();
root.build();
let command = select_command(root, args.get(1..).unwrap_or(&[]));
let output = render(command);
let mut stream = anstream::AutoStream::new(io::stdout().lock(), color.choice());
stream.write_all(output.as_bytes())?;
stream.flush()?;
Ok(true)
}
fn select_command(mut command: Command, args: &[String]) -> Command {
for value in args {
if value == "-h" || value == "--help" {
break;
}
if value.starts_with('-') {
continue;
}
let Some(next) = command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == value)
.cloned()
else {
continue;
};
command = next;
}
command
}
#[must_use]
pub fn render(mut command: Command) -> String {
command.build();
if let Some(width) = layout::terminal_width() {
command = command.term_width(usize::from(width));
}
let mut output = String::new();
let usage = format!(
"{}{}{}",
HEADING.render(),
command.render_usage().to_string().trim(),
HEADING.render_reset()
);
layout::push_line(&mut output, &usage);
output.push('\n');
if let Some(about) = command.get_about() {
layout::push_line(&mut output, &about.to_string());
output.push('\n');
}
let subcommands = command
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(|subcommand| {
vec![
styled(HEADING, subcommand.get_name()),
subcommand
.get_about()
.map_or_else(String::new, ToString::to_string),
]
})
.collect::<Vec<_>>();
if !subcommands.is_empty() {
section(&mut output, "Commands", subcommands);
}
let positionals = command
.get_positionals()
.filter(|arg| !arg.is_hide_set())
.map(|arg| {
vec![
String::new(),
styled(VALUE, &value_label(arg)),
String::new(),
description(arg),
]
})
.collect::<Vec<_>>();
if !positionals.is_empty() {
section(&mut output, "Arguments", positionals);
}
let mut headings = Vec::<String>::new();
for arg in command
.get_arguments()
.filter(|arg| !arg.is_positional() && !arg.is_hide_set() && arg.get_id().as_str() != "help")
{
let heading = arg
.get_help_heading()
.map_or_else(|| "Options".to_string(), ToString::to_string);
if !headings.contains(&heading) {
headings.push(heading);
}
}
if command
.get_arguments()
.any(|arg| arg.get_id().as_str() == "help")
{
headings.push("Help".into());
}
for heading in headings {
let rows = command
.get_arguments()
.filter(|arg| {
if heading == "Help" {
return arg.get_id().as_str() == "help";
}
!arg.is_positional()
&& !arg.is_hide_set()
&& arg.get_id().as_str() != "help"
&& arg.get_help_heading().map_or("Options", |value| value) == heading
})
.map(|arg| {
vec![
arg.get_short()
.map_or_else(String::new, |value| styled(OPTION, &format!("-{value}"))),
arg.get_long()
.map_or_else(String::new, |value| styled(OPTION, &format!("--{value}"))),
styled(VALUE, &value_label(arg)),
description(arg),
]
})
.collect::<Vec<_>>();
if !rows.is_empty() {
section(&mut output, &heading, rows);
}
}
output
}
fn section(output: &mut String, title: &str, rows: Vec<Vec<String>>) {
let _ = writeln!(
output,
"{}{}{}",
HEADING.render(),
title,
HEADING.render_reset()
);
if layout::terminal_width().is_some_and(|width| width < NARROW_HELP_WIDTH) {
narrow_rows(output, rows);
return;
}
let mut table = Table::new();
table
.load_style(UTF8_FULL_CONDENSED)
.set_content_arrangement(ContentArrangement::Dynamic);
layout::constrain(&mut table);
for row in rows {
table.add_row(row);
}
let _ = writeln!(output, "{table}");
}
fn narrow_rows(output: &mut String, rows: Vec<Vec<String>>) {
for row in rows {
let (label, description) = if row.len() == 2 {
(row[0].clone(), row[1].clone())
} else {
(
row.iter()
.take(3)
.filter(|value| !value.is_empty())
.cloned()
.collect::<Vec<_>>()
.join(" "),
row.get(3).cloned().unwrap_or_default(),
)
};
layout::push_indented(output, &label, 2);
if !description.is_empty() {
layout::push_indented(output, &description, 4);
}
}
output.push('\n');
}
fn value_label(arg: &clap::Arg) -> String {
if matches!(
arg.get_action(),
clap::ArgAction::SetTrue
| clap::ArgAction::SetFalse
| clap::ArgAction::Help
| clap::ArgAction::Version
) {
return String::new();
}
let names = arg
.get_value_names()
.map(|names| {
names
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
let choices = arg
.get_possible_values()
.into_iter()
.filter(|value| !value.is_hide_set())
.map(|value| value.get_name().to_string())
.collect::<Vec<_>>();
if choices.is_empty() {
names
} else {
format!("[{}]", choices.join("|"))
}
}
fn description(arg: &clap::Arg) -> String {
let mut description = arg.get_help().map_or_else(String::new, ToString::to_string);
let defaults = arg
.get_default_values()
.iter()
.map(|value| value.to_string_lossy())
.collect::<Vec<_>>();
if !defaults.is_empty()
&& !matches!(
arg.get_action(),
clap::ArgAction::SetTrue | clap::ArgAction::SetFalse
)
{
if !description.is_empty() {
description.push(' ');
}
description.push_str(&styled(
MUTED,
&format!("[default: {}]", defaults.join(", ")),
));
}
description
}
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser};
use super::{render, try_emit_from};
use crate::flags::{DryRunArgs, OutputArgs};
#[derive(Parser)]
#[command(version, about = "toy ctl", arg_required_else_help = true)]
struct Toy {
#[command(flatten)]
output: OutputArgs,
#[command(flatten)]
dry: DryRunArgs,
#[command(subcommand)]
command: ToyCmd,
}
#[derive(clap::Subcommand)]
enum ToyCmd {
Status,
}
#[test]
fn render_lists_commands_and_flags() {
let text = render(Toy::command());
assert!(text.contains("Commands"));
assert!(text.contains("status"));
assert!(text.contains("--dry-run"));
assert!(text.contains("--format"));
assert!(text.contains("--no-color"));
}
#[test]
fn try_emit_skips_without_help() {
let args = ["toy", "status"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
assert!(!try_emit_from::<Toy>(&args).unwrap());
}
}