use std::fmt::Write as _;
use crate::color::Paint;
pub use crate::color::{Depth, Palette, Theme};
#[derive(Debug)]
pub struct Command {
pub name: &'static str,
pub about: &'static str,
pub long_about: &'static str,
pub after_help: &'static str,
pub after_long_help: &'static str,
pub items: &'static [Item],
pub subcommands: &'static [Sub],
pub subcommand_required: bool,
pub help_flag: bool,
pub help_short: bool,
pub version: Option<&'static str>,
}
#[derive(Debug, Clone, Copy)]
pub struct Item {
pub short: Option<char>,
pub long: Option<&'static str>,
pub aliases: &'static [&'static str],
pub negate: Option<&'static str>,
pub value_name: Option<&'static str>,
pub help: &'static str,
pub long_help: &'static str,
pub heading: Option<&'static str>,
pub hide: bool,
pub positional: bool,
pub required: bool,
pub multiple: bool,
pub trailing: bool,
pub default: Option<&'static str>,
pub env: Option<&'static str>,
pub choices: &'static [&'static str],
pub require_equals: bool,
pub global: bool,
pub more_shorts: &'static [char],
pub plus: Option<char>,
pub optional_value: bool,
pub values: usize,
pub more_values: bool,
}
impl Item {
const EMPTY: Item = Item {
short: None,
long: None,
aliases: &[],
negate: None,
value_name: None,
help: "",
long_help: "",
heading: None,
hide: false,
positional: false,
required: false,
multiple: false,
trailing: false,
default: None,
env: None,
choices: &[],
require_equals: false,
global: false,
more_shorts: &[],
plus: None,
optional_value: false,
values: 1,
more_values: false,
};
}
#[doc(hidden)]
pub const fn items_clash(parts: &[&[Item]]) -> bool {
const fn same(a: Option<&str>, b: Option<&str>) -> bool {
let (Some(a), Some(b)) = (a, b) else {
return false;
};
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn has_short(item: &Item, letter: char) -> bool {
if matches!(item.short, Some(c) if c == letter) {
return true;
}
let mut i = 0;
while i < item.more_shorts.len() {
if item.more_shorts[i] == letter {
return true;
}
i += 1;
}
false
}
const fn has_long(item: &Item, name: Option<&str>) -> bool {
if same(item.long, name) || same(item.negate, name) {
return true;
}
let mut i = 0;
while i < item.aliases.len() {
if same(Some(item.aliases[i]), name) {
return true;
}
i += 1;
}
false
}
const fn clash(a: &Item, b: &Item) -> bool {
if a.positional || b.positional {
return false;
}
if matches!(a.short, Some(c) if has_short(b, c)) {
return true;
}
let mut i = 0;
while i < a.more_shorts.len() {
if has_short(b, a.more_shorts[i]) {
return true;
}
i += 1;
}
if has_long(b, a.long) || has_long(b, a.negate) {
return true;
}
let mut i = 0;
while i < a.aliases.len() {
if has_long(b, Some(a.aliases[i])) {
return true;
}
i += 1;
}
false
}
let mut p = 0;
while p < parts.len() {
let mut q = p + 1;
while q < parts.len() {
let (mut i, a, b) = (0, parts[p], parts[q]);
while i < a.len() {
let mut j = 0;
while j < b.len() {
if clash(&a[i], &b[j]) {
return true;
}
j += 1;
}
i += 1;
}
q += 1;
}
p += 1;
}
false
}
#[doc(hidden)]
pub const fn concat_items<const N: usize>(parts: &[&[Item]]) -> [Item; N] {
let mut out = [Item::EMPTY; N];
let (mut n, mut p) = (0, 0);
while p < parts.len() {
let part = parts[p];
let mut i = 0;
while i < part.len() {
out[n] = part[i];
n += 1;
i += 1;
}
p += 1;
}
assert!(n == N, "concat_items: the length is not the parts' total");
out
}
#[derive(Debug)]
pub struct Sub {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub names: &'static [&'static str],
pub command: &'static Command,
pub about: &'static str,
pub hide: bool,
}
pub fn resolve<'w>(
root: &'static Command,
words: impl IntoIterator<Item = &'w [u8]>,
) -> (&'static Command, Vec<&'static str>) {
let (mut command, mut path) = (root, Vec::new());
for word in words {
let Some(sub) = command
.subcommands
.iter()
.find(|s| s.names.iter().any(|n| n.as_bytes() == word))
else {
break;
};
path.push(sub.name);
command = sub.command;
}
(command, path)
}
pub fn width() -> usize {
crate::env::var("COLUMNS")
.and_then(|columns| columns.to_str()?.trim().parse().ok())
.filter(|&columns| columns > 0)
.or_else(terminal_width)
.unwrap_or(100)
}
#[cfg(feature = "terminal-size")]
fn terminal_width() -> Option<usize> {
if crate::env::overridden() {
return None;
}
terminal_size::terminal_size()
.map(|(width, _)| usize::from(width.0))
.filter(|&width| width > 0)
}
#[cfg(not(feature = "terminal-size"))]
fn terminal_width() -> Option<usize> {
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style {
pub palette: Palette,
pub depth: Depth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Ink {
paint: Paint,
depth: Depth,
}
impl Ink {
const NONE: Ink = Ink {
paint: Paint::NONE,
depth: Depth::None,
};
}
impl Style {
pub const PLAIN: Style = Style::at(Palette::PLAIN, Depth::None);
pub const COLORED: Style = Style::at(Palette::DEFAULT, Depth::Ansi16);
pub const CLAP: Style = Style::at(Palette::CLAP, Depth::Ansi16);
pub const fn at(palette: Palette, depth: Depth) -> Style {
Style { palette, depth }
}
pub fn auto() -> Style {
use std::io::IsTerminal as _;
Style::themed(&Theme::DEFAULT, std::io::stdout().is_terminal())
}
pub fn auto_stderr() -> Style {
use std::io::IsTerminal as _;
Style::themed(&Theme::DEFAULT, std::io::stderr().is_terminal())
}
pub fn auto_for(is_terminal: bool) -> Style {
Style::themed(&Theme::DEFAULT, is_terminal)
}
pub fn themed(theme: &Theme, is_terminal: bool) -> Style {
let depth = Depth::detect(is_terminal);
Style::at(theme.palette(depth), depth)
}
fn ink(self, paint: Paint) -> Ink {
Ink {
paint,
depth: self.depth,
}
}
pub(crate) fn header(self) -> Ink {
self.ink(self.palette.header)
}
pub(crate) fn program(self) -> Ink {
self.ink(self.palette.program)
}
pub(crate) fn flag(self) -> Ink {
self.ink(self.palette.flag)
}
pub(crate) fn command(self) -> Ink {
self.ink(self.palette.command)
}
pub(crate) fn dim(self) -> Ink {
self.ink(self.palette.dim)
}
pub(crate) fn code(self) -> Ink {
self.ink(self.palette.code)
}
pub(crate) fn placeholder(self) -> Ink {
self.ink(self.palette.placeholder)
}
pub(crate) fn error(self) -> Ink {
self.ink(self.palette.error)
}
pub(crate) fn invalid(self) -> Ink {
self.ink(self.palette.invalid)
}
pub(crate) fn valid(self) -> Ink {
self.ink(self.palette.valid)
}
pub(crate) fn env(self) -> Ink {
self.ink(self.palette.env)
}
pub(crate) fn default(self) -> Ink {
self.ink(self.palette.default)
}
pub(crate) fn choice(self) -> Ink {
self.ink(self.palette.choice)
}
pub(crate) fn paint(ink: Ink, text: &str) -> String {
let mut out = String::new();
push_painted(&mut out, ink, text);
out
}
}
fn push_painted(out: &mut String, ink: Ink, text: &str) {
ink.paint.write(out, ink.depth, text);
}
#[derive(Default)]
struct Cell {
text: String,
width: usize,
}
impl Cell {
fn push(&mut self, ink: Ink, text: &str) {
push_painted(&mut self.text, ink, text);
self.width += text.chars().count();
}
fn push_placeholder(&mut self, style: Style, required: bool, name: &str) {
if required {
self.push(style.placeholder(), &format!("<{name}>"));
} else {
self.push(style.dim(), "[");
self.push(style.placeholder(), name);
self.push(style.dim(), "]");
}
}
}
pub fn render(command: &Command, path: &[&str], long: bool) -> String {
render_styled(command, path, long, width(), Style::PLAIN)
}
pub fn render_width(command: &Command, path: &[&str], long: bool, width: usize) -> String {
render_styled(command, path, long, width, Style::PLAIN)
}
pub fn render_styled(
command: &Command,
path: &[&str],
long: bool,
width: usize,
style: Style,
) -> String {
let mut out = String::new();
let about = if long && !command.long_about.is_empty() {
command.long_about
} else {
command.about
};
if !about.is_empty() {
let _ = writeln!(out, "{}\n", wrap_text(about, width));
}
push_painted(&mut out, style.header(), "Usage:");
let _ = writeln!(out, " {}\n", usage(command, path, style));
let visible_subs: Vec<&Sub> = command.subcommands.iter().filter(|s| !s.hide).collect();
if !visible_subs.is_empty() {
let rows: Vec<(Cell, String)> = visible_subs
.iter()
.map(|s| {
let mut name = Cell::default();
name.push(style.command(), s.name);
for alias in s.aliases {
name.push(Ink::NONE, ", ");
name.push(style.command(), alias);
}
(name, painted_code(s.about, style))
})
.collect();
section(&mut out, "Commands", &rows, false, width, style);
}
let visible: Vec<&Item> = command.items.iter().filter(|i| !i.hide).collect();
let arguments: Vec<(Cell, String)> = visible
.iter()
.filter(|i| i.positional)
.map(|i| {
let mut cell = Cell::default();
push_item_placeholder(&mut cell, style, i);
(cell, describe(i, long, style))
})
.collect();
section(&mut out, "Arguments", &arguments, long, width, style);
let mut headings: Vec<Option<&str>> = vec![None];
for item in visible.iter().filter(|i| !i.positional) {
if !headings.contains(&item.heading) {
headings.push(item.heading);
}
}
let builtin = |short: &str, long_name: &str| {
let mut cell = Cell::default();
cell.push(style.flag(), short);
cell.push(Ink::NONE, ", ");
cell.push(style.flag(), long_name);
cell
};
for heading in headings {
let mut rows: Vec<(Cell, String)> = visible
.iter()
.filter(|i| !i.positional && i.heading == heading)
.map(|i| (flag_spec(i, style), describe(i, long, style)))
.collect();
if heading.is_none() {
if command.help_flag {
let what = if long {
"Print help (see a summary with '-h')"
} else {
"Print help (see more with '--help')"
};
let cell = if command.help_short {
builtin("-h", "--help")
} else {
let mut cell = Cell::default();
cell.push(Ink::NONE, " ");
cell.push(style.flag(), "--help");
cell
};
let what = if command.help_short {
what
} else {
"Print help"
};
rows.push((cell, what.into()));
}
if command.version.is_some() {
rows.push((builtin("-V", "--version"), "Print version".into()));
}
}
section(
&mut out,
heading.unwrap_or("Options"),
&rows,
long,
width,
style,
);
}
let after = if long && !command.after_long_help.is_empty() {
command.after_long_help
} else {
command.after_help
};
if !after.is_empty() {
let _ = writeln!(out, "{}", wrap_text(after, width));
}
while out.ends_with("\n\n") {
out.pop();
}
out
}
pub(crate) fn usage(command: &Command, path: &[&str], style: Style) -> String {
let mut line = Style::paint(style.program(), &path.join(" "));
let mut cell = Cell::default();
if command.items.iter().any(|i| !i.positional && !i.hide) || command.help_flag {
cell.push(Ink::NONE, " ");
cell.push_placeholder(style, false, "OPTIONS");
}
for item in command
.items
.iter()
.filter(|i| i.positional && !i.hide && !i.trailing)
{
cell.push(Ink::NONE, " ");
push_item_placeholder(&mut cell, style, item);
}
if !command.subcommands.is_empty() {
cell.push(Ink::NONE, " ");
cell.push_placeholder(style, command.subcommand_required, "COMMAND");
}
line.push_str(&cell.text);
for item in command
.items
.iter()
.filter(|i| i.positional && !i.hide && i.trailing)
{
let mut cell = Cell::default();
cell.push(Ink::NONE, " ");
cell.push(style.dim(), "[");
cell.push(style.flag(), "--");
cell.push(Ink::NONE, " ");
push_item_placeholder(&mut cell, style, item);
cell.push(style.dim(), "]");
line.push_str(&cell.text);
}
line
}
fn push_item_placeholder(cell: &mut Cell, style: Style, item: &Item) {
cell.push_placeholder(style, item.required, item.value_name.unwrap_or("VALUE"));
if item.multiple {
cell.push(style.dim(), "...");
}
}
fn flag_spec(item: &Item, style: Style) -> Cell {
let mut spec = Cell::default();
let shorts = item.short.iter().chain(item.more_shorts);
let mut names: Vec<String> = shorts.map(|s| format!("-{s}")).collect();
names.extend(item.plus.map(|c| format!("+{c}")));
if names.is_empty() && item.long.is_some() {
spec.push(Ink::NONE, " ");
}
names.extend(item.long.map(|l| format!("--{l}")));
for (i, name) in names.iter().enumerate() {
if i > 0 {
spec.push(Ink::NONE, ", ");
}
spec.push(style.flag(), name);
}
if let Some(no) = item.negate {
spec.push(Ink::NONE, " / ");
spec.push(style.flag(), &format!("--{no}"));
}
if let Some(value) = item.value_name {
let more = if item.more_values { "..." } else { "" };
let value = vec![format!("<{value}>"); item.values.max(1)].join(" ") + more;
let (open, close) = match (item.require_equals, item.optional_value) {
(true, true) => ("[=", "]"),
(true, false) => ("=", ""),
(false, true) => (" [", "]"),
(false, false) => (" ", ""),
};
spec.push(Ink::NONE, open);
spec.push(style.placeholder(), &value);
spec.push(Ink::NONE, close);
}
if item.multiple {
spec.push(style.dim(), "...");
}
spec
}
fn describe(item: &Item, long: bool, style: Style) -> String {
let help = if long && !item.long_help.is_empty() {
item.long_help
} else {
item.help
};
let mut text = painted_code(help, style);
let mut add = |extra: String| {
if !text.is_empty() {
text.push(if long { '\n' } else { ' ' });
}
text.push_str(&extra);
};
let label = |label: &str| Style::paint(style.dim(), label);
let close = || Style::paint(style.dim(), "]");
if let Some(env) = item.env {
let env = Style::paint(style.env(), env);
add(format!("{}{env}{}", label("[env: "), close()));
}
if let Some(default) = item.default {
let default = Style::paint(style.default(), default);
add(format!("{}{default}{}", label("[default: "), close()));
}
if !item.choices.is_empty() {
let choices: Vec<String> = item
.choices
.iter()
.map(|c| Style::paint(style.choice(), c))
.collect();
add(format!(
"{}{}{}",
label("[possible values: "),
choices.join(", "),
close()
));
}
text
}
fn painted_code(text: &str, style: Style) -> String {
let ink = style.code();
if ink.paint == Paint::NONE || ink.depth == Depth::None || !text.contains('`') {
return text.to_owned();
}
let mut out = String::with_capacity(text.len() + 16);
for (n, line) in text.split('\n').enumerate() {
if n > 0 {
out.push('\n');
}
let mut rest = line;
while let Some(open) = rest.find('`') {
let Some(len) = rest[open + 1..].find('`') else {
break;
};
let close = open + 1 + len;
out.push_str(&rest[..open]);
for (i, word) in rest[open..=close].split(' ').enumerate() {
if i > 0 {
out.push(' ');
}
push_painted(&mut out, ink, word);
}
rest = &rest[close + 1..];
}
out.push_str(rest);
}
out
}
fn section(
out: &mut String,
title: &str,
rows: &[(Cell, String)],
long: bool,
width: usize,
style: Style,
) {
if rows.is_empty() {
return;
}
push_painted(out, style.header(), &format!("{title}:"));
out.push('\n');
let longest = rows.iter().map(|(left, _)| left.width).max().unwrap_or(0);
let available = width.saturating_sub(4);
let column = longest.min(available / 5 * 2 + available % 5 * 2 / 5);
let (indent, room) = if long {
(10, width.saturating_sub(10).max(20))
} else {
(column + 4, width.saturating_sub(column + 4).max(20))
};
for (left, right) in rows {
let beside = !long && !right.is_empty() && left.width <= column;
out.push_str(" ");
out.push_str(&left.text);
if beside {
let _ = write!(out, "{:pad$} ", "", pad = column - left.width);
} else {
out.push('\n');
}
let mut first = beside;
for line in right.lines() {
if line.is_empty() {
out.push('\n');
continue;
}
for piece in Wrap::new(line, room) {
if !first {
let _ = write!(out, "{:indent$}", "");
}
first = false;
out.push_str(piece);
out.push('\n');
}
}
if long {
out.push('\n');
}
}
if !long {
out.push('\n');
}
}
fn wrap_text(text: &str, width: usize) -> String {
let mut out = String::with_capacity(text.len() + text.len() / width.max(1));
for (i, line) in text.lines().enumerate() {
if i > 0 {
out.push('\n');
}
for (j, piece) in Wrap::new(line, width.max(20)).enumerate() {
if j > 0 {
out.push('\n');
}
out.push_str(piece);
}
}
out
}
struct Wrap<'a> {
rest: &'a str,
width: usize,
}
impl<'a> Wrap<'a> {
fn new(line: &'a str, width: usize) -> Self {
Wrap {
rest: line.trim_end(),
width,
}
}
}
impl<'a> Iterator for Wrap<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.rest.is_empty() {
return None;
}
let (mut end, mut space, mut count, mut text) = (self.rest.len(), None, 0, false);
let mut escape = false;
for (at, c) in self.rest.char_indices() {
if escape {
escape = c != 'm';
continue;
}
if c == '\u{1b}' {
escape = true;
continue;
}
if count == self.width {
end = at;
break;
}
if c != ' ' {
text = true;
} else if text {
space = Some(at);
}
count += 1;
}
let cut = match (end == self.rest.len(), space) {
(true, _) => end,
(false, _) if self.rest[end..].starts_with(' ') => end,
(false, Some(at)) => at,
(false, None) => self.rest[end..]
.find(' ')
.map_or(self.rest.len(), |at| end + at),
};
let piece = self.rest[..cut].trim_end();
self.rest = self.rest[cut..].trim_start();
Some(piece)
}
}
#[cfg(test)]
mod tests {
use super::Wrap;
fn wrap(line: &str, width: usize) -> Vec<&str> {
Wrap::new(line, width).collect()
}
#[test]
fn pieces_break_at_spaces_within_the_width() {
assert_eq!(wrap("aa bb cc", 5), ["aa bb", "cc"]);
assert_eq!(wrap("aa bb", 5), ["aa bb"]);
assert_eq!(wrap("aa bb cc", 6), ["aa bb", "cc"]);
}
#[test]
fn a_long_word_stands_alone() {
assert_eq!(wrap("a verylongword b", 5), ["a", "verylongword", "b"]);
}
#[test]
fn indentation_stays_on_the_first_piece() {
assert_eq!(wrap(" $ mise use node", 12), [" $ mise", "use node"]);
}
#[test]
fn escapes_take_no_columns() {
let green = |s: &str| format!("\u{1b}[32m{s}\u{1b}[0m");
let line = format!("[possible values: {}, {}]", green("auto"), green("never"));
let pieces = wrap(&line, 24);
assert_eq!(
pieces,
[
format!("[possible values: {},", green("auto")),
format!("{}]", green("never"))
]
);
}
#[test]
fn width_counts_characters() {
assert_eq!(wrap("éé éé éé", 5), ["éé éé", "éé"]);
}
}