1use std::fmt::Write as _;
9
10use crate::color::Paint;
11pub use crate::color::{Depth, Palette, Theme};
12
13#[derive(Debug)]
19pub struct Command {
20 pub name: &'static str,
22 pub about: &'static str,
24 pub long_about: &'static str,
26 pub after_help: &'static str,
28 pub after_long_help: &'static str,
30 pub items: &'static [Item],
32 pub subcommands: &'static [Sub],
34 pub subcommand_required: bool,
36 pub help_flag: bool,
38 pub help_short: bool,
40 pub version: Option<&'static str>,
42 pub default_subcommand: Option<&'static str>,
45 pub default_subcommand_flags: bool,
48 pub default_subcommand_help: bool,
50 pub flatten_help: bool,
53 pub examples: &'static [Example],
55 pub headings: &'static [Heading],
57}
58
59impl Command {
60 pub const EMPTY: Command = Command {
64 name: "",
65 about: "",
66 long_about: "",
67 after_help: "",
68 after_long_help: "",
69 items: &[],
70 subcommands: &[],
71 subcommand_required: false,
72 help_flag: false,
73 help_short: false,
74 version: None,
75 default_subcommand: None,
76 default_subcommand_flags: false,
77 default_subcommand_help: false,
78 flatten_help: false,
79 examples: &[],
80 headings: &[],
81 };
82
83 pub(crate) fn default_target(&self) -> Option<&'static Sub> {
85 let name = self.default_subcommand?;
86 let subs = self.subcommands;
87 subs.iter()
88 .find(|sub| sub.name == name)
89 .or_else(|| subs.iter().find(|sub| sub.names.contains(&name)))
90 }
91
92 fn default_sub(&self) -> Option<&'static Sub> {
94 self.default_target().filter(|sub| !sub.hide)
95 }
96
97 fn heading_help(&self, title: &str) -> Option<&'static str> {
99 let heading = self.headings.iter().find(|h| h.title == title)?;
100 Some(heading.help).filter(|help| !help.is_empty())
101 }
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct Example {
107 pub code: &'static str,
109 pub header: &'static str,
111 pub help: &'static str,
113}
114
115#[derive(Debug, Clone, Copy)]
117pub struct Heading {
118 pub title: &'static str,
120 pub help: &'static str,
122}
123
124#[derive(Debug, Clone, Copy)]
126pub struct Item {
127 pub short: Option<char>,
129 pub long: Option<&'static str>,
131 pub aliases: &'static [&'static str],
133 pub negate: Option<&'static str>,
135 pub value_name: Option<&'static str>,
137 pub help: &'static str,
139 pub long_help: &'static str,
141 pub heading: Option<&'static str>,
143 pub hide: bool,
145 pub positional: bool,
147 pub required: bool,
149 pub multiple: bool,
151 pub trailing: bool,
153 pub default: Option<&'static str>,
155 pub env: Option<&'static str>,
157 pub choices: &'static [&'static str],
159 pub require_equals: bool,
161 pub global: bool,
163 pub more_shorts: &'static [char],
165 pub plus: Option<char>,
167 pub optional_value: bool,
169 pub values: usize,
171 pub more_values: bool,
173 pub hint: crate::complete::Hint,
175 pub complete: Option<fn(&str) -> Vec<crate::complete::Candidate>>,
178 pub warning: &'static str,
181}
182
183impl Item {
184 pub const EMPTY: Item = Item {
187 short: None,
188 long: None,
189 aliases: &[],
190 negate: None,
191 value_name: None,
192 help: "",
193 long_help: "",
194 heading: None,
195 hide: false,
196 positional: false,
197 required: false,
198 multiple: false,
199 trailing: false,
200 default: None,
201 env: None,
202 choices: &[],
203 require_equals: false,
204 global: false,
205 more_shorts: &[],
206 plus: None,
207 optional_value: false,
208 values: 1,
209 more_values: false,
210 hint: crate::complete::Hint::Nothing,
211 complete: None,
212 warning: "",
213 };
214}
215
216#[doc(hidden)]
220pub const fn items_clash(parts: &[&[Item]]) -> bool {
221 const fn same(a: Option<&str>, b: Option<&str>) -> bool {
222 let (Some(a), Some(b)) = (a, b) else {
223 return false;
224 };
225 let (a, b) = (a.as_bytes(), b.as_bytes());
226 if a.len() != b.len() {
227 return false;
228 }
229 let mut i = 0;
230 while i < a.len() {
231 if a[i] != b[i] {
232 return false;
233 }
234 i += 1;
235 }
236 true
237 }
238 const fn has_short(item: &Item, letter: char) -> bool {
240 if matches!(item.short, Some(c) if c == letter) {
241 return true;
242 }
243 let mut i = 0;
244 while i < item.more_shorts.len() {
245 if item.more_shorts[i] == letter {
246 return true;
247 }
248 i += 1;
249 }
250 false
251 }
252 const fn has_long(item: &Item, name: Option<&str>) -> bool {
254 if same(item.long, name) || same(item.negate, name) {
255 return true;
256 }
257 let mut i = 0;
258 while i < item.aliases.len() {
259 if same(Some(item.aliases[i]), name) {
260 return true;
261 }
262 i += 1;
263 }
264 false
265 }
266 const fn clash(a: &Item, b: &Item) -> bool {
267 if a.positional || b.positional {
268 return false;
269 }
270 if matches!(a.short, Some(c) if has_short(b, c)) {
271 return true;
272 }
273 let mut i = 0;
274 while i < a.more_shorts.len() {
275 if has_short(b, a.more_shorts[i]) {
276 return true;
277 }
278 i += 1;
279 }
280 if has_long(b, a.long) || has_long(b, a.negate) {
281 return true;
282 }
283 let mut i = 0;
284 while i < a.aliases.len() {
285 if has_long(b, Some(a.aliases[i])) {
286 return true;
287 }
288 i += 1;
289 }
290 false
291 }
292 let mut p = 0;
293 while p < parts.len() {
294 let mut q = p + 1;
295 while q < parts.len() {
296 let (mut i, a, b) = (0, parts[p], parts[q]);
297 while i < a.len() {
298 let mut j = 0;
299 while j < b.len() {
300 if clash(&a[i], &b[j]) {
301 return true;
302 }
303 j += 1;
304 }
305 i += 1;
306 }
307 q += 1;
308 }
309 p += 1;
310 }
311 false
312}
313
314#[doc(hidden)]
322pub const fn concat_items<const N: usize>(parts: &[&[Item]]) -> [Item; N] {
323 let mut out = [Item::EMPTY; N];
324 let (mut n, mut p) = (0, 0);
325 while p < parts.len() {
326 let part = parts[p];
327 let mut i = 0;
328 while i < part.len() {
329 out[n] = part[i];
330 n += 1;
331 i += 1;
332 }
333 p += 1;
334 }
335 assert!(n == N, "concat_items: the length is not the parts' total");
336 out
337}
338
339#[doc(hidden)]
346pub const fn concat_headings<const N: usize>(parts: &[&[Heading]]) -> [Heading; N] {
347 let mut out = [Heading {
348 title: "",
349 help: "",
350 }; N];
351 let (mut n, mut p) = (0, 0);
352 while p < parts.len() {
353 let part = parts[p];
354 let mut i = 0;
355 while i < part.len() {
356 out[n] = part[i];
357 n += 1;
358 i += 1;
359 }
360 p += 1;
361 }
362 assert!(
363 n == N,
364 "concat_headings: the length is not the parts' total"
365 );
366 out
367}
368
369#[derive(Debug)]
371pub struct Sub {
372 pub name: &'static str,
374 pub aliases: &'static [&'static str],
376 pub names: &'static [&'static str],
378 pub command: &'static Command,
380 pub about: &'static str,
382 pub hide: bool,
384}
385
386pub fn resolve<'w>(
389 root: &'static Command,
390 words: impl IntoIterator<Item = &'w [u8]>,
391) -> (&'static Command, Vec<&'static str>) {
392 let (mut command, mut path) = (root, Vec::new());
393 for word in words {
394 let Some(sub) = command
395 .subcommands
396 .iter()
397 .find(|s| s.names.iter().any(|n| n.as_bytes() == word))
398 else {
399 break;
400 };
401 path.push(sub.name);
402 command = sub.command;
403 }
404 (command, path)
405}
406
407pub fn width() -> usize {
414 crate::env::var("COLUMNS")
415 .and_then(|columns| columns.to_str()?.trim().parse().ok())
416 .filter(|&columns| columns > 0)
417 .or_else(terminal_width)
418 .unwrap_or(100)
419}
420
421#[cfg(feature = "terminal-size")]
422fn terminal_width() -> Option<usize> {
423 if crate::env::overridden() {
424 return None;
425 }
426 terminal_size::terminal_size()
427 .map(|(width, _)| usize::from(width.0))
428 .filter(|&width| width > 0)
429}
430
431#[cfg(not(feature = "terminal-size"))]
432fn terminal_width() -> Option<usize> {
433 None
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub struct Style {
440 pub palette: Palette,
442 pub depth: Depth,
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub(crate) struct Ink {
449 paint: Paint,
450 depth: Depth,
451}
452
453impl Ink {
454 const NONE: Ink = Ink {
456 paint: Paint::NONE,
457 depth: Depth::None,
458 };
459}
460
461impl Style {
462 pub const PLAIN: Style = Style::at(Palette::PLAIN, Depth::None);
464
465 pub const COLORED: Style = Style::at(Palette::DEFAULT, Depth::Ansi16);
467
468 pub const CLAP: Style = Style::at(Palette::CLAP, Depth::Ansi16);
470
471 pub const fn at(palette: Palette, depth: Depth) -> Style {
473 Style { palette, depth }
474 }
475
476 pub fn auto() -> Style {
479 use std::io::IsTerminal as _;
480 Style::themed(&Theme::DEFAULT, std::io::stdout().is_terminal())
481 }
482
483 pub fn auto_stderr() -> Style {
485 use std::io::IsTerminal as _;
486 Style::themed(&Theme::DEFAULT, std::io::stderr().is_terminal())
487 }
488
489 pub fn auto_for(is_terminal: bool) -> Style {
491 Style::themed(&Theme::DEFAULT, is_terminal)
492 }
493
494 pub fn themed(theme: &Theme, is_terminal: bool) -> Style {
497 let depth = Depth::detect(is_terminal);
498 Style::at(theme.palette(depth), depth)
499 }
500
501 fn ink(self, paint: Paint) -> Ink {
502 Ink {
503 paint,
504 depth: self.depth,
505 }
506 }
507
508 pub(crate) fn header(self) -> Ink {
509 self.ink(self.palette.header)
510 }
511
512 pub(crate) fn program(self) -> Ink {
513 self.ink(self.palette.program)
514 }
515
516 pub(crate) fn flag(self) -> Ink {
517 self.ink(self.palette.flag)
518 }
519
520 pub(crate) fn command(self) -> Ink {
521 self.ink(self.palette.command)
522 }
523
524 pub(crate) fn dim(self) -> Ink {
525 self.ink(self.palette.dim)
526 }
527
528 pub(crate) fn code(self) -> Ink {
529 self.ink(self.palette.code)
530 }
531
532 pub(crate) fn emphasis(self) -> Ink {
533 self.ink(self.palette.emphasis)
534 }
535
536 pub(crate) fn strong(self) -> Ink {
537 self.ink(self.palette.strong)
538 }
539
540 pub(crate) fn placeholder(self) -> Ink {
541 self.ink(self.palette.placeholder)
542 }
543
544 pub(crate) fn error(self) -> Ink {
545 self.ink(self.palette.error)
546 }
547
548 pub(crate) fn invalid(self) -> Ink {
549 self.ink(self.palette.invalid)
550 }
551
552 pub(crate) fn valid(self) -> Ink {
553 self.ink(self.palette.valid)
554 }
555
556 pub(crate) fn env(self) -> Ink {
557 self.ink(self.palette.env)
558 }
559
560 pub(crate) fn default(self) -> Ink {
561 self.ink(self.palette.default)
562 }
563
564 pub(crate) fn choice(self) -> Ink {
565 self.ink(self.palette.choice)
566 }
567
568 pub(crate) fn paint(ink: Ink, text: &str) -> String {
570 let mut out = String::new();
571 push_painted(&mut out, ink, text);
572 out
573 }
574}
575
576fn push_painted(out: &mut String, ink: Ink, text: &str) {
578 ink.paint.write(out, ink.depth, text);
579}
580
581#[derive(Default)]
583struct Cell {
584 text: String,
585 width: usize,
586}
587
588impl Cell {
589 fn push(&mut self, ink: Ink, text: &str) {
590 push_painted(&mut self.text, ink, text);
591 self.width += text.chars().count();
592 }
593
594 fn push_placeholder(&mut self, style: Style, required: bool, name: &str) {
597 if required {
598 self.push(style.placeholder(), &format!("<{name}>"));
599 } else {
600 self.push(style.dim(), "[");
601 self.push(style.placeholder(), name);
602 self.push(style.dim(), "]");
603 }
604 }
605}
606
607pub fn render(command: &Command, path: &[&str], long: bool) -> String {
611 render_styled(command, path, long, width(), Style::PLAIN)
612}
613
614pub fn render_width(command: &Command, path: &[&str], long: bool, width: usize) -> String {
616 render_styled(command, path, long, width, Style::PLAIN)
617}
618
619pub fn render_styled(
621 command: &Command,
622 path: &[&str],
623 long: bool,
624 width: usize,
625 style: Style,
626) -> String {
627 render_page(command, path, long, width, style, &[])
628}
629
630pub(crate) fn render_page(
633 command: &Command,
634 path: &[&str],
635 long: bool,
636 width: usize,
637 style: Style,
638 inherited: &[&Item],
639) -> String {
640 let mut out = String::new();
641 let layout = Layout { long, width, style };
642 page(&mut out, command, path, layout, inherited);
643 if let Some(sub) = command.default_sub()
645 && command.default_subcommand_help
646 && !command.flatten_help
647 {
648 trim_blank_lines(&mut out);
649 out.push('\n');
650 heading(&mut out, "Default command:", style);
651 out.push(' ');
652 push_painted(&mut out, style.command(), sub.name);
653 out.push_str("\n It runs when no command is named.\n\n");
654 let mut path = path.to_vec();
655 path.push(sub.name);
656 page(&mut out, sub.command, &path, layout, &[]);
658 }
659 trim_blank_lines(&mut out);
660 out
661}
662
663fn trim_blank_lines(out: &mut String) {
665 while out.ends_with("\n\n") {
666 out.pop();
667 }
668}
669
670#[derive(Clone, Copy)]
672struct Layout {
673 long: bool,
674 width: usize,
675 style: Style,
676}
677
678fn page(out: &mut String, command: &Command, path: &[&str], layout: Layout, inherited: &[&Item]) {
680 let Layout { long, width, style } = layout;
681 let about = if long && !command.long_about.is_empty() {
682 command.long_about
683 } else {
684 command.about
685 };
686 if !about.is_empty() {
687 let about = painted_marks(about, style);
688 let _ = writeln!(out, "{}\n", wrap_text(&about, width));
689 }
690 let visible_subs: Vec<&Sub> = command.subcommands.iter().filter(|s| !s.hide).collect();
691 let flat = command.flatten_help && !visible_subs.is_empty();
692 push_painted(out, style.header(), "Usage:");
693 let mut lines = Vec::new();
694 if !flat || !command.subcommand_required {
695 lines.push(usage(command, path, style));
696 }
697 if flat {
698 lines.extend(visible_subs.iter().map(|sub| {
699 let mut path = path.to_vec();
700 path.push(sub.name);
701 usage(sub.command, &path, style)
702 }));
703 }
704 for (n, line) in lines.iter().enumerate() {
705 let _ = writeln!(out, "{}{line}", if n == 0 { " " } else { " " });
706 }
707 out.push('\n');
708
709 if !flat && !visible_subs.is_empty() {
710 let default = command.default_sub().map(|sub| sub.name);
711 let rows: Vec<(Cell, String)> = visible_subs
712 .iter()
713 .map(|s| {
714 let mut name = Cell::default();
715 name.push(style.command(), s.name);
716 for alias in s.aliases {
717 name.push(Ink::NONE, ", ");
718 name.push(style.command(), alias);
719 }
720 let mut about = painted_marks(s.about, style);
721 if default == Some(s.name) {
722 if !about.is_empty() {
723 about.push(' ');
724 }
725 push_painted(&mut about, style.default(), "(default)");
727 }
728 (name, about)
729 })
730 .collect();
731 section(
733 out,
734 "Commands",
735 None,
736 &rows,
737 Layout {
738 long: false,
739 ..layout
740 },
741 );
742 }
743 if flat {
744 flat_commands(out, command, path, layout);
745 }
746
747 section(
748 out,
749 "Arguments",
750 None,
751 &argument_rows(command, layout),
752 layout,
753 );
754
755 let visible: Vec<&Item> = command.items.iter().filter(|i| !i.hide).collect();
756 let mut headings: Vec<Option<&str>> = vec![None];
757 for item in visible.iter().filter(|i| !i.positional) {
758 if !headings.contains(&item.heading) {
759 headings.push(item.heading);
760 }
761 }
762 let builtin = |short: &str, long_name: &str| {
763 let mut cell = Cell::default();
764 cell.push(style.flag(), short);
765 cell.push(Ink::NONE, ", ");
766 cell.push(style.flag(), long_name);
767 cell
768 };
769 for title in headings {
770 let mut rows: Vec<(Cell, String)> = visible
771 .iter()
772 .filter(|i| !i.positional && i.heading == title)
773 .map(|i| (flag_spec(i, style), describe(i, long, style)))
774 .collect();
775 if title.is_none() {
776 if command.help_flag {
777 let what = if long {
778 "Print help (see a summary with '-h')"
779 } else {
780 "Print help (see more with '--help')"
781 };
782 let cell = if command.help_short {
783 builtin("-h", "--help")
784 } else {
785 let mut cell = Cell::default();
786 cell.push(Ink::NONE, " ");
787 cell.push(style.flag(), "--help");
788 cell
789 };
790 let what = if command.help_short {
791 what
792 } else {
793 "Print help"
794 };
795 rows.push((cell, what.into()));
796 }
797 if command.version.is_some() {
798 rows.push((builtin("-V", "--version"), "Print version".into()));
799 }
800 }
801 let prose = title.and_then(|title| command.heading_help(title));
802 section(out, title.unwrap_or("Options"), prose, &rows, layout);
803 }
804
805 let rows: Vec<(Cell, String)> = inherited
806 .iter()
807 .map(|i| (flag_spec(i, style), describe(i, long, style)))
808 .collect();
809 section(out, "Global options", None, &rows, layout);
810
811 examples(out, command.examples, layout);
812
813 let after = if long && !command.after_long_help.is_empty() {
814 command.after_long_help
815 } else {
816 command.after_help
817 };
818 if !after.is_empty() {
819 let _ = writeln!(out, "{}", wrap_text(after, width));
820 }
821}
822
823fn argument_rows(command: &Command, layout: Layout) -> Vec<(Cell, String)> {
825 let shown = command.items.iter().filter(|i| i.positional && !i.hide);
826 shown
827 .map(|i| {
828 let mut cell = Cell::default();
829 push_item_placeholder(&mut cell, layout.style, i);
830 (cell, describe(i, layout.long, layout.style))
831 })
832 .collect()
833}
834
835fn flat_commands(out: &mut String, command: &Command, path: &[&str], layout: Layout) {
839 let Layout { long, style, .. } = layout;
840 for sub in command.subcommands.iter().filter(|sub| !sub.hide) {
841 let mut path = path.to_vec();
842 path.push(sub.name);
843 heading(out, &format!("{}:", path.join(" ")), style);
844 out.push('\n');
845 let about = if long && !sub.command.long_about.is_empty() {
846 sub.command.long_about
847 } else {
848 sub.about
849 };
850 indented(out, about, 2, layout);
851 let mut rows = argument_rows(sub.command, layout);
852 let flags = sub
853 .command
854 .items
855 .iter()
856 .filter(|i| !i.positional && !i.hide);
857 rows.extend(flags.map(|i| (flag_spec(i, style), describe(i, long, style))));
858 if long && !about.is_empty() && !rows.is_empty() {
859 out.push('\n');
860 }
861 list(out, &rows, layout);
862 if !long || rows.is_empty() {
863 out.push('\n');
864 }
865 if sub.command.flatten_help {
866 flat_commands(out, sub.command, &path, layout);
867 }
868 }
869}
870
871fn examples(out: &mut String, examples: &[Example], layout: Layout) {
874 let Layout { long, style, .. } = layout;
875 if examples.is_empty() {
876 return;
877 }
878 heading(out, "Examples:", style);
879 out.push('\n');
880 let mut spaced = false;
881 for (n, example) in examples.iter().enumerate() {
882 let help = Some(example.help).filter(|help| long && !help.is_empty());
883 let said = !example.header.is_empty() || help.is_some();
884 if n > 0 && (said || spaced) {
886 out.push('\n');
887 }
888 spaced = said;
889 if !example.header.is_empty() {
890 let _ = writeln!(out, " {}:", painted_marks(example.header, style));
891 }
892 if let Some(help) = help {
893 indented(out, help, 4, layout);
894 }
895 out.push_str(if said { " " } else { " " });
896 push_painted(out, style.dim(), "$");
897 out.push(' ');
898 push_painted(out, style.code(), example.code);
899 out.push('\n');
900 }
901 out.push('\n');
902}
903
904pub(crate) fn usage(command: &Command, path: &[&str], style: Style) -> String {
906 let mut line = Style::paint(style.program(), &path.join(" "));
907 let mut cell = Cell::default();
908 if command.items.iter().any(|i| !i.positional && !i.hide) || command.help_flag {
909 cell.push(Ink::NONE, " ");
910 cell.push_placeholder(style, false, "OPTIONS");
911 }
912 for item in command
913 .items
914 .iter()
915 .filter(|i| i.positional && !i.hide && !i.trailing)
916 {
917 cell.push(Ink::NONE, " ");
918 push_item_placeholder(&mut cell, style, item);
919 }
920 if !command.subcommands.is_empty() {
921 cell.push(Ink::NONE, " ");
922 cell.push_placeholder(style, command.subcommand_required, "COMMAND");
923 }
924 line.push_str(&cell.text);
925 for item in command
926 .items
927 .iter()
928 .filter(|i| i.positional && !i.hide && i.trailing)
929 {
930 let mut cell = Cell::default();
932 cell.push(Ink::NONE, " ");
933 cell.push(style.dim(), "[");
934 cell.push(style.flag(), "--");
935 cell.push(Ink::NONE, " ");
936 push_item_placeholder(&mut cell, style, item);
937 cell.push(style.dim(), "]");
938 line.push_str(&cell.text);
939 }
940 line
941}
942
943fn push_item_placeholder(cell: &mut Cell, style: Style, item: &Item) {
945 cell.push_placeholder(style, item.required, item.value_name.unwrap_or("VALUE"));
946 if item.multiple {
947 cell.push(style.dim(), "...");
948 }
949}
950
951fn flag_spec(item: &Item, style: Style) -> Cell {
952 let mut spec = Cell::default();
953 let shorts = item.short.iter().chain(item.more_shorts);
956 let mut names: Vec<String> = shorts.map(|s| format!("-{s}")).collect();
957 names.extend(item.plus.map(|c| format!("+{c}")));
958 if names.is_empty() && item.long.is_some() {
959 spec.push(Ink::NONE, " ");
960 }
961 names.extend(item.long.map(|l| format!("--{l}")));
962 for (i, name) in names.iter().enumerate() {
963 if i > 0 {
964 spec.push(Ink::NONE, ", ");
965 }
966 spec.push(style.flag(), name);
967 }
968 if let Some(no) = item.negate {
969 spec.push(Ink::NONE, " / ");
970 spec.push(style.flag(), &format!("--{no}"));
971 }
972 if let Some(value) = item.value_name {
973 let more = if item.more_values { "..." } else { "" };
976 let value = vec![format!("<{value}>"); item.values.max(1)].join(" ") + more;
977 let (open, close) = match (item.require_equals, item.optional_value) {
978 (true, true) => ("[=", "]"),
979 (true, false) => ("=", ""),
980 (false, true) => (" [", "]"),
981 (false, false) => (" ", ""),
982 };
983 spec.push(Ink::NONE, open);
984 spec.push(style.placeholder(), &value);
985 spec.push(Ink::NONE, close);
986 }
987 if item.multiple {
988 spec.push(style.dim(), "...");
989 }
990 spec
991}
992
993fn describe(item: &Item, long: bool, style: Style) -> String {
997 let help = if long && !item.long_help.is_empty() {
998 item.long_help
999 } else {
1000 item.help
1001 };
1002 let mut text = painted_marks(help, style);
1003 let mut add = |extra: String| {
1004 if !text.is_empty() {
1005 text.push(if long { '\n' } else { ' ' });
1006 }
1007 text.push_str(&extra);
1008 };
1009 let label = |label: &str| Style::paint(style.dim(), label);
1010 let close = || Style::paint(style.dim(), "]");
1011 if let Some(env) = item.env {
1012 let env = Style::paint(style.env(), env);
1013 add(format!("{}{env}{}", label("[env: "), close()));
1014 }
1015 if let Some(default) = item.default {
1016 let default = Style::paint(style.default(), default);
1017 add(format!("{}{default}{}", label("[default: "), close()));
1018 }
1019 if !item.choices.is_empty() {
1020 let choices: Vec<String> = item
1021 .choices
1022 .iter()
1023 .map(|c| Style::paint(style.choice(), c))
1024 .collect();
1025 add(format!(
1026 "{}{}{}",
1027 label("[possible values: "),
1028 choices.join(", "),
1029 close()
1030 ));
1031 }
1032 if long && !item.warning.is_empty() {
1033 let label = Style::paint(style.error(), "Warning:");
1034 add(format!("{label} {}", painted_marks(item.warning, style)));
1035 }
1036 text
1037}
1038
1039fn heading(out: &mut String, title: &str, style: Style) {
1043 let header = style.header();
1044 let inks = [style.code(), style.emphasis(), style.strong()];
1045 let mut at = 0;
1046 if style.depth != Depth::None {
1047 while let Some((open, end, ink)) = marked(title, at, inks) {
1048 if at < open {
1049 push_painted(out, header, &title[at..open]);
1050 }
1051 let both = Paint {
1052 fg: header.paint.fg.or(ink.paint.fg),
1053 bold: header.paint.bold || ink.paint.bold,
1054 dim: header.paint.dim || ink.paint.dim,
1055 italic: header.paint.italic || ink.paint.italic,
1056 underline: header.paint.underline || ink.paint.underline,
1057 };
1058 push_painted(out, style.ink(both), &title[open..end]);
1059 at = end;
1060 }
1061 }
1062 push_painted(out, header, &title[at..]);
1063}
1064
1065fn painted_marks(text: &str, style: Style) -> String {
1070 let inks = [style.code(), style.emphasis(), style.strong()];
1071 if style.depth == Depth::None
1072 || inks.iter().all(|ink| ink.paint == Paint::NONE)
1073 || !text.contains(['`', '*'])
1074 {
1075 return text.to_owned();
1076 }
1077 let mut out = String::with_capacity(text.len() + 16);
1078 for (n, line) in text.split('\n').enumerate() {
1079 if n > 0 {
1080 out.push('\n');
1081 }
1082 let mut at = 0;
1083 while let Some((open, end, ink)) = marked(line, at, inks) {
1084 out.push_str(&line[at..open]);
1085 for (i, word) in line[open..end].split(' ').enumerate() {
1086 if i > 0 {
1087 out.push(' ');
1088 }
1089 if !word.is_empty() {
1090 push_painted(&mut out, ink, word);
1091 }
1092 }
1093 at = end;
1094 }
1095 out.push_str(&line[at..]);
1096 }
1097 out
1098}
1099
1100fn marked(
1111 line: &str,
1112 from: usize,
1113 [code, emphasis, strong]: [Ink; 3],
1114) -> Option<(usize, usize, Ink)> {
1115 let bytes = line.as_bytes();
1116 let mut at = from;
1117 while at < bytes.len() {
1118 match bytes[at] {
1119 b'`' => {
1120 if let Some(len) = line[at + 1..].find('`') {
1121 return Some((at, at + len + 2, code));
1122 }
1123 at += 1;
1124 }
1125 b'*' => {
1126 let stars = run(bytes, at);
1127 let before = line[..at].chars().next_back();
1128 let after = line[at + stars..].chars().next();
1129 let ink = if stars == 2 { strong } else { emphasis };
1130 if stars <= 2
1131 && ink.paint != Paint::NONE
1132 && before.is_none_or(|c| !c.is_alphanumeric() && c != '\\')
1133 && after.is_some_and(|c| !c.is_whitespace())
1134 && let Some(end) = closing(line, at + stars, stars)
1135 {
1136 return Some((at, end, ink));
1137 }
1138 at += stars;
1139 }
1140 _ => at += 1,
1141 }
1142 }
1143 None
1144}
1145
1146fn run(bytes: &[u8], at: usize) -> usize {
1148 bytes[at..].iter().take_while(|b| **b == b'*').count()
1149}
1150
1151fn closing(line: &str, from: usize, stars: usize) -> Option<usize> {
1156 let bytes = line.as_bytes();
1157 let mut at = from;
1158 while at < bytes.len() {
1159 match bytes[at] {
1160 b'`' => at += line[at + 1..].find('`').map_or(1, |len| len + 2),
1161 b'*' => {
1162 let found = run(bytes, at);
1163 let before = line[..at].chars().next_back();
1164 let after = line[at + found..].chars().next();
1165 if found == stars {
1166 let closes = before.is_some_and(|c| !c.is_whitespace() && c != '\\')
1167 && after.is_none_or(|c| !c.is_alphanumeric());
1168 return closes.then_some(at + found);
1169 }
1170 at += found;
1171 }
1172 _ => at += 1,
1173 }
1174 }
1175 None
1176}
1177
1178fn section(
1180 out: &mut String,
1181 title: &str,
1182 prose: Option<&str>,
1183 rows: &[(Cell, String)],
1184 layout: Layout,
1185) {
1186 if rows.is_empty() {
1187 return;
1188 }
1189 heading(out, &format!("{title}:"), layout.style);
1190 out.push('\n');
1191 if let Some(prose) = prose.filter(|_| layout.long) {
1192 indented(out, prose, 2, layout);
1193 out.push('\n');
1194 }
1195 list(out, rows, layout);
1196 if !layout.long {
1197 out.push('\n');
1198 }
1199}
1200
1201fn list(out: &mut String, rows: &[(Cell, String)], layout: Layout) {
1207 let Layout { long, width, .. } = layout;
1208 let longest = rows.iter().map(|(left, _)| left.width).max().unwrap_or(0);
1209 let available = width.saturating_sub(4);
1210 let column = longest.min(available / 5 * 2 + available % 5 * 2 / 5);
1211 let (indent, room) = if long {
1213 (10, width.saturating_sub(10).max(20))
1214 } else {
1215 (column + 4, width.saturating_sub(column + 4).max(20))
1216 };
1217 for (left, right) in rows {
1218 let beside = !long && !right.is_empty() && left.width <= column;
1219 out.push_str(" ");
1220 out.push_str(&left.text);
1221 if beside {
1222 let _ = write!(out, "{:pad$} ", "", pad = column - left.width);
1224 } else {
1225 out.push('\n');
1226 }
1227 let mut first = beside;
1228 for line in right.lines() {
1229 if line.is_empty() {
1230 out.push('\n');
1231 continue;
1232 }
1233 for piece in Wrap::new(line, room) {
1234 if !first {
1235 let _ = write!(out, "{:indent$}", "");
1236 }
1237 first = false;
1238 out.push_str(piece);
1239 out.push('\n');
1240 }
1241 }
1242 if long {
1243 out.push('\n');
1245 }
1246 }
1247}
1248
1249fn indented(out: &mut String, text: &str, indent: usize, layout: Layout) {
1252 let text = painted_marks(text, layout.style);
1253 let room = layout.width.saturating_sub(indent).max(20);
1254 for line in text.lines() {
1255 if line.trim().is_empty() {
1256 out.push('\n');
1257 }
1258 for piece in Wrap::new(line, room) {
1259 let _ = writeln!(out, "{:indent$}{piece}", "");
1260 }
1261 }
1262}
1263
1264fn wrap_text(text: &str, width: usize) -> String {
1266 let mut out = String::with_capacity(text.len() + text.len() / width.max(1));
1267 for (i, line) in text.lines().enumerate() {
1268 if i > 0 {
1269 out.push('\n');
1270 }
1271 for (j, piece) in Wrap::new(line, width.max(20)).enumerate() {
1272 if j > 0 {
1273 out.push('\n');
1274 }
1275 out.push_str(piece);
1276 }
1277 }
1278 out
1279}
1280
1281struct Wrap<'a> {
1285 rest: &'a str,
1286 width: usize,
1287}
1288
1289impl<'a> Wrap<'a> {
1290 fn new(line: &'a str, width: usize) -> Self {
1291 Wrap {
1292 rest: line.trim_end(),
1293 width,
1294 }
1295 }
1296}
1297
1298impl<'a> Iterator for Wrap<'a> {
1299 type Item = &'a str;
1300
1301 fn next(&mut self) -> Option<&'a str> {
1302 if self.rest.is_empty() {
1303 return None;
1304 }
1305 let (mut end, mut space, mut count, mut text) = (self.rest.len(), None, 0, false);
1307 let mut escape = false;
1308 for (at, c) in self.rest.char_indices() {
1309 if escape {
1311 escape = c != 'm';
1312 continue;
1313 }
1314 if c == '\u{1b}' {
1315 escape = true;
1316 continue;
1317 }
1318 if count == self.width {
1319 end = at;
1320 break;
1321 }
1322 if c != ' ' {
1324 text = true;
1325 } else if text {
1326 space = Some(at);
1327 }
1328 count += 1;
1329 }
1330 let cut = match (end == self.rest.len(), space) {
1331 (true, _) => end,
1332 (false, _) if self.rest[end..].starts_with(' ') => end,
1334 (false, Some(at)) => at,
1335 (false, None) => self.rest[end..]
1337 .find(' ')
1338 .map_or(self.rest.len(), |at| end + at),
1339 };
1340 let piece = self.rest[..cut].trim_end();
1341 self.rest = self.rest[cut..].trim_start();
1342 Some(piece)
1343 }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348 use super::Wrap;
1349
1350 fn wrap(line: &str, width: usize) -> Vec<&str> {
1351 Wrap::new(line, width).collect()
1352 }
1353
1354 #[test]
1355 fn pieces_break_at_spaces_within_the_width() {
1356 assert_eq!(wrap("aa bb cc", 5), ["aa bb", "cc"]);
1357 assert_eq!(wrap("aa bb", 5), ["aa bb"]);
1358 assert_eq!(wrap("aa bb cc", 6), ["aa bb", "cc"]);
1359 }
1360
1361 #[test]
1362 fn a_long_word_stands_alone() {
1363 assert_eq!(wrap("a verylongword b", 5), ["a", "verylongword", "b"]);
1364 }
1365
1366 #[test]
1367 fn indentation_stays_on_the_first_piece() {
1368 assert_eq!(wrap(" $ mise use node", 12), [" $ mise", "use node"]);
1369 }
1370
1371 #[test]
1372 fn escapes_take_no_columns() {
1373 let green = |s: &str| format!("\u{1b}[32m{s}\u{1b}[0m");
1374 let line = format!("[possible values: {}, {}]", green("auto"), green("never"));
1375 let pieces = wrap(&line, 24);
1376 assert_eq!(
1377 pieces,
1378 [
1379 format!("[possible values: {},", green("auto")),
1380 format!("{}]", green("never"))
1381 ]
1382 );
1383 }
1384
1385 #[test]
1386 fn width_counts_characters() {
1387 assert_eq!(wrap("éé éé éé", 5), ["éé éé", "éé"]);
1388 }
1389}