use crate::errors::ClarError;
use crate::model::{ClarArgument, ClarDefinition, ClarItem, ClarOption};
use crate::path::ClarPath;
use crate::{ClarCommand, ClarTerminator};
use antex::{Color, ColorMode, StyledText, Text};
use std::fmt::Write;
const INDENT: &str = " ";
const DISTANCE: &str = " ";
const NL: char = '\n';
pub fn get_cfg_error_text(reason: &ClarError, cm: ColorMode) -> Text {
error_label(Color::Magenta, cm) + reason.as_text(cm) + NL
}
pub fn get_error_text(reason: &ClarError, app: &str, def: &ClarDefinition, names: &[String], cm: ColorMode) -> Text {
error_label(Color::Red, cm) + reason.as_text(cm) + NL + NL + get_usage(app, names, def, cm)
}
pub fn get_help_text(app: &str, description: &Option<String>, def: &ClarDefinition, detailed: bool, cm: ColorMode) -> Text {
get_description(description, cm) + get_usage(app, &[], def, cm) + get_details(def, detailed, cm)
}
pub fn get_help_text_for_command(app: &str, path: &ClarPath, def: &ClarDefinition, detailed: bool, cm: ColorMode) -> Text {
if let Some(command) = def.find_command(path) {
get_description(command.get_help(), cm) + get_usage(app, path.names(), command.get_definition(), cm) + get_details(command.get_definition(), detailed, cm)
} else {
error_label(Color::Magenta, cm) + Text::new(cm).s("command '").yellow().bold().s(path).reset().s("' is not defined")
}
}
fn get_description(description: &Option<String>, cm: ColorMode) -> Text {
if let Some(description) = description {
Text::new(cm).s(description).s("\n\n")
} else {
Text::new(cm)
}
}
fn get_usage(app: &str, names: &[String], def: &ClarDefinition, cm: ColorMode) -> Text {
let mut usage = Text::new(cm)
.bright_green()
.bold()
.s("Usage:")
.reset()
.s(' ')
.bright_cyan()
.bold()
.s(app)
.reset();
names.iter().for_each(|name| usage += Text::new(cm).s(' ').bright_cyan().bold().s(name).reset());
for item in def.items() {
match item {
ClarItem::Options(options) => {
usage += " " + get_usage_for_options(options, cm);
}
ClarItem::Commands(commands) => {
usage += " " + get_usage_for_commands(commands, cm);
}
ClarItem::Arguments(arguments) => {
usage += " " + get_usage_for_arguments(arguments, cm);
}
ClarItem::Terminator(option_terminator) => {
usage += " " + get_usage_for_option_terminator(option_terminator, cm);
}
}
}
usage + NL
}
fn get_usage_for_options(_options: &[ClarOption], cm: ColorMode) -> Text {
Text::new(cm).cyan().s("[OPTIONS]")
}
fn get_usage_for_commands(_subcommands: &[ClarCommand], cm: ColorMode) -> Text {
Text::new(cm).cyan().s("<COMMAND>")
}
fn get_usage_for_arguments(arguments: &[ClarArgument], cm: ColorMode) -> Text {
let mut has_optional_arguments = false;
let mut text = Text::new(cm);
for (index, argument) in arguments.iter().enumerate() {
if argument.is_required() {
if index > 0 {
text += " ";
}
text += Text::new(cm).cyan().s('<').s(argument.get_caption()).s('>').reset();
} else {
has_optional_arguments = true;
}
}
if has_optional_arguments {
text += Text::new(cm).s(' ').cyan().s("[ARGS]").reset();
}
text
}
fn get_usage_for_option_terminator(option_terminator: &ClarTerminator, cm: ColorMode) -> Text {
let mut text = Text::new(cm);
if option_terminator.is_required() {
text += Text::new(cm).cyan().s("-- [ARG]...").reset();
} else {
text += Text::new(cm).cyan().s("[-- [ARG]...]").reset();
}
text
}
fn get_details(def: &ClarDefinition, detailed: bool, cm: ColorMode) -> Text {
let mut text = Text::new(cm);
for item in def.items() {
if let ClarItem::Commands(subcommands) = item {
text += get_details_for_subcommands(subcommands, detailed, cm);
}
}
for item in def.items() {
if let ClarItem::Arguments(arguments) = item {
text += get_details_for_arguments(arguments, detailed, cm);
}
}
for item in def.items() {
if let ClarItem::Options(options) = item {
text += get_details_for_options(options, detailed, cm);
}
}
text
}
fn get_details_for_options(options: &[ClarOption], detailed: bool, cm: ColorMode) -> Text {
let mut text = group_label("Options:", cm);
let captions = options.iter().map(|option| get_caption_for_option(option, cm)).collect::<Vec<Text>>();
let column_width = captions.iter().map(|text| text.count()).max().unwrap_or(0);
for (option, caption) in options.iter().zip(captions) {
let option_hints = option_hints(option, detailed);
let details = if detailed {
format!("{}{option_hints}", option.get_help_long().clone().unwrap_or_default())
} else {
format!("{}{option_hints}", option.get_help().clone().unwrap_or_default())
};
if details.is_empty() {
text += Text::new(cm) + INDENT + caption + NL;
} else {
text += Text::new(cm) + INDENT + caption.fill(' ', column_width) + DISTANCE + get_indented_column(details, column_width) + NL;
}
}
text
}
fn get_details_for_subcommands(commands: &[ClarCommand], detailed: bool, cm: ColorMode) -> Text {
let mut text = group_label("Commands:", cm);
let captions = commands.iter().map(|subcommand| get_caption_for_command(subcommand, cm)).collect::<Vec<Text>>();
let column_width = captions.iter().map(|t| t.count()).max().unwrap_or(0);
for (command, caption) in commands.iter().zip(captions) {
let details = if detailed {
get_indented_column(command.get_help_long().clone().unwrap_or_default(), column_width)
} else {
get_indented_column(command.get_help().clone().unwrap_or_default(), column_width)
};
text += Text::new(cm) + INDENT + caption.fill(' ', column_width) + DISTANCE + details + NL;
}
text
}
fn get_details_for_arguments(arguments: &[ClarArgument], detailed: bool, cm: ColorMode) -> Text {
let mut text = group_label("Arguments:", cm);
let captions = arguments.iter().map(|a| get_caption_for_argument(a, cm)).collect::<Vec<Text>>();
let column_width = captions.iter().map(|t| t.count()).max().unwrap_or(0);
for (argument, caption) in arguments.iter().zip(captions) {
let details = if detailed {
get_indented_column(argument.get_help_long().clone().unwrap_or_default(), column_width)
} else {
get_indented_column(argument.get_help().clone().unwrap_or_default(), column_width)
};
text += Text::new(cm) + INDENT + caption.fill(' ', column_width) + DISTANCE + details + NL;
}
text
}
fn get_caption_for_option(option: &ClarOption, cm: ColorMode) -> Text {
let mut text = Text::new(cm);
if let Some(short_name) = option.get_short_label() {
text += Text::new(cm).bright_cyan().bold().s("-").s(short_name).reset();
} else {
text += INDENT;
text += " ";
}
if let Some(long_label) = option.get_long_label() {
text += Text::new(cm)
.choose(option.get_short_label().is_some(), ',', ' ')
.s(' ')
.bright_cyan()
.bold()
.s("--")
.s(long_label)
.reset();
}
if let Some(caption) = option.get_takes_value() {
if option.get_default_missing_value().is_some() {
text += Text::new(cm).s(' ').cyan().s("[<").s(caption).s(">]").reset();
} else {
text += Text::new(cm).s(' ').cyan().s("<").s(caption).s(">").reset();
}
}
text
}
fn get_caption_for_command(subcommand: &ClarCommand, cm: ColorMode) -> Text {
Text::new(cm).bright_cyan().s(subcommand.get_name()).reset()
}
fn get_caption_for_argument(argument: &ClarArgument, cm: ColorMode) -> Text {
if argument.is_required() {
Text::new(cm).cyan().s('<').s(argument.get_caption()).s('>').reset()
} else {
Text::new(cm).cyan().s('[').s(argument.get_caption()).s(']').reset()
}
}
fn get_indented_column(input: String, column_width: usize) -> String {
let mut text = String::new();
for (index, line) in input.lines().enumerate() {
if index > 0 {
_ = write!(&mut text, "\n{INDENT}{}{DISTANCE}", " ".repeat(column_width));
}
_ = write!(&mut text, "{line}");
}
text
}
fn group_label(label: &str, cm: ColorMode) -> Text {
Text::new(cm).s('\n').bright_green().bold().s(label).reset().s('\n')
}
fn error_label(color: Color, cm: ColorMode) -> Text {
Text::new(cm).color(color).bold().s("error:").reset().s(' ')
}
fn option_hints(option: &ClarOption, long_help: bool) -> String {
let mut hints = String::new();
let mut prefix = "";
if let Some(value) = option.get_default_value() {
_ = write!(&mut hints, "[default: {value}]");
prefix = " ";
}
if let Some(value) = option.get_default_missing_value() {
_ = write!(&mut hints, "{prefix}[implicit: {value}]");
prefix = " ";
}
if !option.get_possible_values().is_empty() {
_ = write!(&mut hints, "{prefix}[possible values: {}]", option.get_possible_values().join(", "));
}
if hints.is_empty() {
hints
} else {
prefix = match (long_help, option.get_help().is_some(), option.get_help_long().is_some()) {
(false, false, _) => "",
(false, true, _) => " ",
(true, _, false) => "",
(true, _, true) => "\n ",
};
format!("{prefix}{hints}")
}
}