1use std::fmt::Write as _;
9
10use crate::color::Paint;
11pub use crate::color::{Depth, Palette, Theme};
12
13#[derive(Debug)]
18pub struct Command {
19 pub name: &'static str,
21 pub about: &'static str,
23 pub long_about: &'static str,
25 pub after_help: &'static str,
27 pub after_long_help: &'static str,
29 pub items: &'static [Item],
31 pub subcommands: &'static [Sub],
33 pub subcommand_required: bool,
35 pub help_flag: bool,
37 pub help_short: bool,
39 pub version: Option<&'static str>,
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct Item {
46 pub short: Option<char>,
48 pub long: Option<&'static str>,
50 pub aliases: &'static [&'static str],
52 pub negate: Option<&'static str>,
54 pub value_name: Option<&'static str>,
56 pub help: &'static str,
58 pub long_help: &'static str,
60 pub heading: Option<&'static str>,
62 pub hide: bool,
64 pub positional: bool,
66 pub required: bool,
68 pub multiple: bool,
70 pub trailing: bool,
72 pub default: Option<&'static str>,
74 pub env: Option<&'static str>,
76 pub choices: &'static [&'static str],
78 pub require_equals: bool,
80 pub global: bool,
82 pub more_shorts: &'static [char],
84 pub plus: Option<char>,
86 pub optional_value: bool,
88 pub values: usize,
90 pub more_values: bool,
92}
93
94impl Item {
95 const EMPTY: Item = Item {
97 short: None,
98 long: None,
99 aliases: &[],
100 negate: None,
101 value_name: None,
102 help: "",
103 long_help: "",
104 heading: None,
105 hide: false,
106 positional: false,
107 required: false,
108 multiple: false,
109 trailing: false,
110 default: None,
111 env: None,
112 choices: &[],
113 require_equals: false,
114 global: false,
115 more_shorts: &[],
116 plus: None,
117 optional_value: false,
118 values: 1,
119 more_values: false,
120 };
121}
122
123#[doc(hidden)]
127pub const fn items_clash(parts: &[&[Item]]) -> bool {
128 const fn same(a: Option<&str>, b: Option<&str>) -> bool {
129 let (Some(a), Some(b)) = (a, b) else {
130 return false;
131 };
132 let (a, b) = (a.as_bytes(), b.as_bytes());
133 if a.len() != b.len() {
134 return false;
135 }
136 let mut i = 0;
137 while i < a.len() {
138 if a[i] != b[i] {
139 return false;
140 }
141 i += 1;
142 }
143 true
144 }
145 const fn has_short(item: &Item, letter: char) -> bool {
147 if matches!(item.short, Some(c) if c == letter) {
148 return true;
149 }
150 let mut i = 0;
151 while i < item.more_shorts.len() {
152 if item.more_shorts[i] == letter {
153 return true;
154 }
155 i += 1;
156 }
157 false
158 }
159 const fn has_long(item: &Item, name: Option<&str>) -> bool {
161 if same(item.long, name) || same(item.negate, name) {
162 return true;
163 }
164 let mut i = 0;
165 while i < item.aliases.len() {
166 if same(Some(item.aliases[i]), name) {
167 return true;
168 }
169 i += 1;
170 }
171 false
172 }
173 const fn clash(a: &Item, b: &Item) -> bool {
174 if a.positional || b.positional {
175 return false;
176 }
177 if matches!(a.short, Some(c) if has_short(b, c)) {
178 return true;
179 }
180 let mut i = 0;
181 while i < a.more_shorts.len() {
182 if has_short(b, a.more_shorts[i]) {
183 return true;
184 }
185 i += 1;
186 }
187 if has_long(b, a.long) || has_long(b, a.negate) {
188 return true;
189 }
190 let mut i = 0;
191 while i < a.aliases.len() {
192 if has_long(b, Some(a.aliases[i])) {
193 return true;
194 }
195 i += 1;
196 }
197 false
198 }
199 let mut p = 0;
200 while p < parts.len() {
201 let mut q = p + 1;
202 while q < parts.len() {
203 let (mut i, a, b) = (0, parts[p], parts[q]);
204 while i < a.len() {
205 let mut j = 0;
206 while j < b.len() {
207 if clash(&a[i], &b[j]) {
208 return true;
209 }
210 j += 1;
211 }
212 i += 1;
213 }
214 q += 1;
215 }
216 p += 1;
217 }
218 false
219}
220
221#[doc(hidden)]
229pub const fn concat_items<const N: usize>(parts: &[&[Item]]) -> [Item; N] {
230 let mut out = [Item::EMPTY; N];
231 let (mut n, mut p) = (0, 0);
232 while p < parts.len() {
233 let part = parts[p];
234 let mut i = 0;
235 while i < part.len() {
236 out[n] = part[i];
237 n += 1;
238 i += 1;
239 }
240 p += 1;
241 }
242 assert!(n == N, "concat_items: the length is not the parts' total");
243 out
244}
245
246#[derive(Debug)]
248pub struct Sub {
249 pub name: &'static str,
251 pub aliases: &'static [&'static str],
253 pub names: &'static [&'static str],
255 pub command: &'static Command,
257 pub about: &'static str,
259 pub hide: bool,
261}
262
263pub fn resolve<'w>(
266 root: &'static Command,
267 words: impl IntoIterator<Item = &'w [u8]>,
268) -> (&'static Command, Vec<&'static str>) {
269 let (mut command, mut path) = (root, Vec::new());
270 for word in words {
271 let Some(sub) = command
272 .subcommands
273 .iter()
274 .find(|s| s.names.iter().any(|n| n.as_bytes() == word))
275 else {
276 break;
277 };
278 path.push(sub.name);
279 command = sub.command;
280 }
281 (command, path)
282}
283
284pub fn width() -> usize {
291 crate::env::var("COLUMNS")
292 .and_then(|columns| columns.to_str()?.trim().parse().ok())
293 .filter(|&columns| columns > 0)
294 .or_else(terminal_width)
295 .unwrap_or(100)
296}
297
298#[cfg(feature = "terminal-size")]
299fn terminal_width() -> Option<usize> {
300 if crate::env::overridden() {
301 return None;
302 }
303 terminal_size::terminal_size()
304 .map(|(width, _)| usize::from(width.0))
305 .filter(|&width| width > 0)
306}
307
308#[cfg(not(feature = "terminal-size"))]
309fn terminal_width() -> Option<usize> {
310 None
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct Style {
317 pub palette: Palette,
319 pub depth: Depth,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub(crate) struct Ink {
326 paint: Paint,
327 depth: Depth,
328}
329
330impl Ink {
331 const NONE: Ink = Ink {
333 paint: Paint::NONE,
334 depth: Depth::None,
335 };
336}
337
338impl Style {
339 pub const PLAIN: Style = Style::at(Palette::PLAIN, Depth::None);
341
342 pub const COLORED: Style = Style::at(Palette::DEFAULT, Depth::Ansi16);
344
345 pub const CLAP: Style = Style::at(Palette::CLAP, Depth::Ansi16);
347
348 pub const fn at(palette: Palette, depth: Depth) -> Style {
350 Style { palette, depth }
351 }
352
353 pub fn auto() -> Style {
356 use std::io::IsTerminal as _;
357 Style::themed(&Theme::DEFAULT, std::io::stdout().is_terminal())
358 }
359
360 pub fn auto_stderr() -> Style {
362 use std::io::IsTerminal as _;
363 Style::themed(&Theme::DEFAULT, std::io::stderr().is_terminal())
364 }
365
366 pub fn auto_for(is_terminal: bool) -> Style {
368 Style::themed(&Theme::DEFAULT, is_terminal)
369 }
370
371 pub fn themed(theme: &Theme, is_terminal: bool) -> Style {
374 let depth = Depth::detect(is_terminal);
375 Style::at(theme.palette(depth), depth)
376 }
377
378 fn ink(self, paint: Paint) -> Ink {
379 Ink {
380 paint,
381 depth: self.depth,
382 }
383 }
384
385 pub(crate) fn header(self) -> Ink {
386 self.ink(self.palette.header)
387 }
388
389 pub(crate) fn program(self) -> Ink {
390 self.ink(self.palette.program)
391 }
392
393 pub(crate) fn flag(self) -> Ink {
394 self.ink(self.palette.flag)
395 }
396
397 pub(crate) fn command(self) -> Ink {
398 self.ink(self.palette.command)
399 }
400
401 pub(crate) fn dim(self) -> Ink {
402 self.ink(self.palette.dim)
403 }
404
405 pub(crate) fn code(self) -> Ink {
406 self.ink(self.palette.code)
407 }
408
409 pub(crate) fn placeholder(self) -> Ink {
410 self.ink(self.palette.placeholder)
411 }
412
413 pub(crate) fn error(self) -> Ink {
414 self.ink(self.palette.error)
415 }
416
417 pub(crate) fn invalid(self) -> Ink {
418 self.ink(self.palette.invalid)
419 }
420
421 pub(crate) fn valid(self) -> Ink {
422 self.ink(self.palette.valid)
423 }
424
425 pub(crate) fn env(self) -> Ink {
426 self.ink(self.palette.env)
427 }
428
429 pub(crate) fn default(self) -> Ink {
430 self.ink(self.palette.default)
431 }
432
433 pub(crate) fn choice(self) -> Ink {
434 self.ink(self.palette.choice)
435 }
436
437 pub(crate) fn paint(ink: Ink, text: &str) -> String {
439 let mut out = String::new();
440 push_painted(&mut out, ink, text);
441 out
442 }
443}
444
445fn push_painted(out: &mut String, ink: Ink, text: &str) {
447 ink.paint.write(out, ink.depth, text);
448}
449
450#[derive(Default)]
452struct Cell {
453 text: String,
454 width: usize,
455}
456
457impl Cell {
458 fn push(&mut self, ink: Ink, text: &str) {
459 push_painted(&mut self.text, ink, text);
460 self.width += text.chars().count();
461 }
462
463 fn push_placeholder(&mut self, style: Style, required: bool, name: &str) {
466 if required {
467 self.push(style.placeholder(), &format!("<{name}>"));
468 } else {
469 self.push(style.dim(), "[");
470 self.push(style.placeholder(), name);
471 self.push(style.dim(), "]");
472 }
473 }
474}
475
476pub fn render(command: &Command, path: &[&str], long: bool) -> String {
480 render_styled(command, path, long, width(), Style::PLAIN)
481}
482
483pub fn render_width(command: &Command, path: &[&str], long: bool, width: usize) -> String {
485 render_styled(command, path, long, width, Style::PLAIN)
486}
487
488pub fn render_styled(
490 command: &Command,
491 path: &[&str],
492 long: bool,
493 width: usize,
494 style: Style,
495) -> String {
496 let mut out = String::new();
497 let about = if long && !command.long_about.is_empty() {
498 command.long_about
499 } else {
500 command.about
501 };
502 if !about.is_empty() {
503 let _ = writeln!(out, "{}\n", wrap_text(about, width));
504 }
505 push_painted(&mut out, style.header(), "Usage:");
506 let _ = writeln!(out, " {}\n", usage(command, path, style));
507
508 let visible_subs: Vec<&Sub> = command.subcommands.iter().filter(|s| !s.hide).collect();
509 if !visible_subs.is_empty() {
510 let rows: Vec<(Cell, String)> = visible_subs
511 .iter()
512 .map(|s| {
513 let mut name = Cell::default();
514 name.push(style.command(), s.name);
515 for alias in s.aliases {
516 name.push(Ink::NONE, ", ");
517 name.push(style.command(), alias);
518 }
519 (name, painted_code(s.about, style))
520 })
521 .collect();
522 section(&mut out, "Commands", &rows, false, width, style);
523 }
524
525 let visible: Vec<&Item> = command.items.iter().filter(|i| !i.hide).collect();
526 let arguments: Vec<(Cell, String)> = visible
527 .iter()
528 .filter(|i| i.positional)
529 .map(|i| {
530 let mut cell = Cell::default();
531 push_item_placeholder(&mut cell, style, i);
532 (cell, describe(i, long, style))
533 })
534 .collect();
535 section(&mut out, "Arguments", &arguments, long, width, style);
536
537 let mut headings: Vec<Option<&str>> = vec![None];
538 for item in visible.iter().filter(|i| !i.positional) {
539 if !headings.contains(&item.heading) {
540 headings.push(item.heading);
541 }
542 }
543 let builtin = |short: &str, long_name: &str| {
544 let mut cell = Cell::default();
545 cell.push(style.flag(), short);
546 cell.push(Ink::NONE, ", ");
547 cell.push(style.flag(), long_name);
548 cell
549 };
550 for heading in headings {
551 let mut rows: Vec<(Cell, String)> = visible
552 .iter()
553 .filter(|i| !i.positional && i.heading == heading)
554 .map(|i| (flag_spec(i, style), describe(i, long, style)))
555 .collect();
556 if heading.is_none() {
557 if command.help_flag {
558 let what = if long {
559 "Print help (see a summary with '-h')"
560 } else {
561 "Print help (see more with '--help')"
562 };
563 let cell = if command.help_short {
564 builtin("-h", "--help")
565 } else {
566 let mut cell = Cell::default();
567 cell.push(Ink::NONE, " ");
568 cell.push(style.flag(), "--help");
569 cell
570 };
571 let what = if command.help_short {
572 what
573 } else {
574 "Print help"
575 };
576 rows.push((cell, what.into()));
577 }
578 if command.version.is_some() {
579 rows.push((builtin("-V", "--version"), "Print version".into()));
580 }
581 }
582 section(
583 &mut out,
584 heading.unwrap_or("Options"),
585 &rows,
586 long,
587 width,
588 style,
589 );
590 }
591
592 let after = if long && !command.after_long_help.is_empty() {
593 command.after_long_help
594 } else {
595 command.after_help
596 };
597 if !after.is_empty() {
598 let _ = writeln!(out, "{}", wrap_text(after, width));
599 }
600 while out.ends_with("\n\n") {
601 out.pop();
602 }
603 out
604}
605
606pub(crate) fn usage(command: &Command, path: &[&str], style: Style) -> String {
608 let mut line = Style::paint(style.program(), &path.join(" "));
609 let mut cell = Cell::default();
610 if command.items.iter().any(|i| !i.positional && !i.hide) || command.help_flag {
611 cell.push(Ink::NONE, " ");
612 cell.push_placeholder(style, false, "OPTIONS");
613 }
614 for item in command
615 .items
616 .iter()
617 .filter(|i| i.positional && !i.hide && !i.trailing)
618 {
619 cell.push(Ink::NONE, " ");
620 push_item_placeholder(&mut cell, style, item);
621 }
622 if !command.subcommands.is_empty() {
623 cell.push(Ink::NONE, " ");
624 cell.push_placeholder(style, command.subcommand_required, "COMMAND");
625 }
626 line.push_str(&cell.text);
627 for item in command
628 .items
629 .iter()
630 .filter(|i| i.positional && !i.hide && i.trailing)
631 {
632 let mut cell = Cell::default();
634 cell.push(Ink::NONE, " ");
635 cell.push(style.dim(), "[");
636 cell.push(style.flag(), "--");
637 cell.push(Ink::NONE, " ");
638 push_item_placeholder(&mut cell, style, item);
639 cell.push(style.dim(), "]");
640 line.push_str(&cell.text);
641 }
642 line
643}
644
645fn push_item_placeholder(cell: &mut Cell, style: Style, item: &Item) {
647 cell.push_placeholder(style, item.required, item.value_name.unwrap_or("VALUE"));
648 if item.multiple {
649 cell.push(style.dim(), "...");
650 }
651}
652
653fn flag_spec(item: &Item, style: Style) -> Cell {
654 let mut spec = Cell::default();
655 let shorts = item.short.iter().chain(item.more_shorts);
658 let mut names: Vec<String> = shorts.map(|s| format!("-{s}")).collect();
659 names.extend(item.plus.map(|c| format!("+{c}")));
660 if names.is_empty() && item.long.is_some() {
661 spec.push(Ink::NONE, " ");
662 }
663 names.extend(item.long.map(|l| format!("--{l}")));
664 for (i, name) in names.iter().enumerate() {
665 if i > 0 {
666 spec.push(Ink::NONE, ", ");
667 }
668 spec.push(style.flag(), name);
669 }
670 if let Some(no) = item.negate {
671 spec.push(Ink::NONE, " / ");
672 spec.push(style.flag(), &format!("--{no}"));
673 }
674 if let Some(value) = item.value_name {
675 let more = if item.more_values { "..." } else { "" };
678 let value = vec![format!("<{value}>"); item.values.max(1)].join(" ") + more;
679 let (open, close) = match (item.require_equals, item.optional_value) {
680 (true, true) => ("[=", "]"),
681 (true, false) => ("=", ""),
682 (false, true) => (" [", "]"),
683 (false, false) => (" ", ""),
684 };
685 spec.push(Ink::NONE, open);
686 spec.push(style.placeholder(), &value);
687 spec.push(Ink::NONE, close);
688 }
689 if item.multiple {
690 spec.push(style.dim(), "...");
691 }
692 spec
693}
694
695fn describe(item: &Item, long: bool, style: Style) -> String {
699 let help = if long && !item.long_help.is_empty() {
700 item.long_help
701 } else {
702 item.help
703 };
704 let mut text = painted_code(help, style);
705 let mut add = |extra: String| {
706 if !text.is_empty() {
707 text.push(if long { '\n' } else { ' ' });
708 }
709 text.push_str(&extra);
710 };
711 let label = |label: &str| Style::paint(style.dim(), label);
712 let close = || Style::paint(style.dim(), "]");
713 if let Some(env) = item.env {
714 let env = Style::paint(style.env(), env);
715 add(format!("{}{env}{}", label("[env: "), close()));
716 }
717 if let Some(default) = item.default {
718 let default = Style::paint(style.default(), default);
719 add(format!("{}{default}{}", label("[default: "), close()));
720 }
721 if !item.choices.is_empty() {
722 let choices: Vec<String> = item
723 .choices
724 .iter()
725 .map(|c| Style::paint(style.choice(), c))
726 .collect();
727 add(format!(
728 "{}{}{}",
729 label("[possible values: "),
730 choices.join(", "),
731 close()
732 ));
733 }
734 text
735}
736
737fn painted_code(text: &str, style: Style) -> String {
742 let ink = style.code();
743 if ink.paint == Paint::NONE || ink.depth == Depth::None || !text.contains('`') {
744 return text.to_owned();
745 }
746 let mut out = String::with_capacity(text.len() + 16);
747 for (n, line) in text.split('\n').enumerate() {
748 if n > 0 {
749 out.push('\n');
750 }
751 let mut rest = line;
752 while let Some(open) = rest.find('`') {
753 let Some(len) = rest[open + 1..].find('`') else {
754 break;
755 };
756 let close = open + 1 + len;
757 out.push_str(&rest[..open]);
758 for (i, word) in rest[open..=close].split(' ').enumerate() {
759 if i > 0 {
760 out.push(' ');
761 }
762 push_painted(&mut out, ink, word);
763 }
764 rest = &rest[close + 1..];
765 }
766 out.push_str(rest);
767 }
768 out
769}
770
771fn section(
777 out: &mut String,
778 title: &str,
779 rows: &[(Cell, String)],
780 long: bool,
781 width: usize,
782 style: Style,
783) {
784 if rows.is_empty() {
785 return;
786 }
787 push_painted(out, style.header(), &format!("{title}:"));
788 out.push('\n');
789 let longest = rows.iter().map(|(left, _)| left.width).max().unwrap_or(0);
790 let available = width.saturating_sub(4);
791 let column = longest.min(available / 5 * 2 + available % 5 * 2 / 5);
792 let (indent, room) = if long {
794 (10, width.saturating_sub(10).max(20))
795 } else {
796 (column + 4, width.saturating_sub(column + 4).max(20))
797 };
798 for (left, right) in rows {
799 let beside = !long && !right.is_empty() && left.width <= column;
800 out.push_str(" ");
801 out.push_str(&left.text);
802 if beside {
803 let _ = write!(out, "{:pad$} ", "", pad = column - left.width);
805 } else {
806 out.push('\n');
807 }
808 let mut first = beside;
809 for line in right.lines() {
810 if line.is_empty() {
811 out.push('\n');
812 continue;
813 }
814 for piece in Wrap::new(line, room) {
815 if !first {
816 let _ = write!(out, "{:indent$}", "");
817 }
818 first = false;
819 out.push_str(piece);
820 out.push('\n');
821 }
822 }
823 if long {
824 out.push('\n');
826 }
827 }
828 if !long {
829 out.push('\n');
830 }
831}
832
833fn wrap_text(text: &str, width: usize) -> String {
835 let mut out = String::with_capacity(text.len() + text.len() / width.max(1));
836 for (i, line) in text.lines().enumerate() {
837 if i > 0 {
838 out.push('\n');
839 }
840 for (j, piece) in Wrap::new(line, width.max(20)).enumerate() {
841 if j > 0 {
842 out.push('\n');
843 }
844 out.push_str(piece);
845 }
846 }
847 out
848}
849
850struct Wrap<'a> {
854 rest: &'a str,
855 width: usize,
856}
857
858impl<'a> Wrap<'a> {
859 fn new(line: &'a str, width: usize) -> Self {
860 Wrap {
861 rest: line.trim_end(),
862 width,
863 }
864 }
865}
866
867impl<'a> Iterator for Wrap<'a> {
868 type Item = &'a str;
869
870 fn next(&mut self) -> Option<&'a str> {
871 if self.rest.is_empty() {
872 return None;
873 }
874 let (mut end, mut space, mut count, mut text) = (self.rest.len(), None, 0, false);
876 let mut escape = false;
877 for (at, c) in self.rest.char_indices() {
878 if escape {
880 escape = c != 'm';
881 continue;
882 }
883 if c == '\u{1b}' {
884 escape = true;
885 continue;
886 }
887 if count == self.width {
888 end = at;
889 break;
890 }
891 if c != ' ' {
893 text = true;
894 } else if text {
895 space = Some(at);
896 }
897 count += 1;
898 }
899 let cut = match (end == self.rest.len(), space) {
900 (true, _) => end,
901 (false, _) if self.rest[end..].starts_with(' ') => end,
903 (false, Some(at)) => at,
904 (false, None) => self.rest[end..]
906 .find(' ')
907 .map_or(self.rest.len(), |at| end + at),
908 };
909 let piece = self.rest[..cut].trim_end();
910 self.rest = self.rest[cut..].trim_start();
911 Some(piece)
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use super::Wrap;
918
919 fn wrap(line: &str, width: usize) -> Vec<&str> {
920 Wrap::new(line, width).collect()
921 }
922
923 #[test]
924 fn pieces_break_at_spaces_within_the_width() {
925 assert_eq!(wrap("aa bb cc", 5), ["aa bb", "cc"]);
926 assert_eq!(wrap("aa bb", 5), ["aa bb"]);
927 assert_eq!(wrap("aa bb cc", 6), ["aa bb", "cc"]);
928 }
929
930 #[test]
931 fn a_long_word_stands_alone() {
932 assert_eq!(wrap("a verylongword b", 5), ["a", "verylongword", "b"]);
933 }
934
935 #[test]
936 fn indentation_stays_on_the_first_piece() {
937 assert_eq!(wrap(" $ mise use node", 12), [" $ mise", "use node"]);
938 }
939
940 #[test]
941 fn escapes_take_no_columns() {
942 let green = |s: &str| format!("\u{1b}[32m{s}\u{1b}[0m");
943 let line = format!("[possible values: {}, {}]", green("auto"), green("never"));
944 let pieces = wrap(&line, 24);
945 assert_eq!(
946 pieces,
947 [
948 format!("[possible values: {},", green("auto")),
949 format!("{}]", green("never"))
950 ]
951 );
952 }
953
954 #[test]
955 fn width_counts_characters() {
956 assert_eq!(wrap("éé éé éé", 5), ["éé éé", "éé"]);
957 }
958}