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 pub hint: crate::complete::Hint,
94 pub complete: Option<fn(&str) -> Vec<crate::complete::Candidate>>,
97}
98
99impl Item {
100 const EMPTY: Item = Item {
102 short: None,
103 long: None,
104 aliases: &[],
105 negate: None,
106 value_name: None,
107 help: "",
108 long_help: "",
109 heading: None,
110 hide: false,
111 positional: false,
112 required: false,
113 multiple: false,
114 trailing: false,
115 default: None,
116 env: None,
117 choices: &[],
118 require_equals: false,
119 global: false,
120 more_shorts: &[],
121 plus: None,
122 optional_value: false,
123 values: 1,
124 more_values: false,
125 hint: crate::complete::Hint::Nothing,
126 complete: None,
127 };
128}
129
130#[doc(hidden)]
134pub const fn items_clash(parts: &[&[Item]]) -> bool {
135 const fn same(a: Option<&str>, b: Option<&str>) -> bool {
136 let (Some(a), Some(b)) = (a, b) else {
137 return false;
138 };
139 let (a, b) = (a.as_bytes(), b.as_bytes());
140 if a.len() != b.len() {
141 return false;
142 }
143 let mut i = 0;
144 while i < a.len() {
145 if a[i] != b[i] {
146 return false;
147 }
148 i += 1;
149 }
150 true
151 }
152 const fn has_short(item: &Item, letter: char) -> bool {
154 if matches!(item.short, Some(c) if c == letter) {
155 return true;
156 }
157 let mut i = 0;
158 while i < item.more_shorts.len() {
159 if item.more_shorts[i] == letter {
160 return true;
161 }
162 i += 1;
163 }
164 false
165 }
166 const fn has_long(item: &Item, name: Option<&str>) -> bool {
168 if same(item.long, name) || same(item.negate, name) {
169 return true;
170 }
171 let mut i = 0;
172 while i < item.aliases.len() {
173 if same(Some(item.aliases[i]), name) {
174 return true;
175 }
176 i += 1;
177 }
178 false
179 }
180 const fn clash(a: &Item, b: &Item) -> bool {
181 if a.positional || b.positional {
182 return false;
183 }
184 if matches!(a.short, Some(c) if has_short(b, c)) {
185 return true;
186 }
187 let mut i = 0;
188 while i < a.more_shorts.len() {
189 if has_short(b, a.more_shorts[i]) {
190 return true;
191 }
192 i += 1;
193 }
194 if has_long(b, a.long) || has_long(b, a.negate) {
195 return true;
196 }
197 let mut i = 0;
198 while i < a.aliases.len() {
199 if has_long(b, Some(a.aliases[i])) {
200 return true;
201 }
202 i += 1;
203 }
204 false
205 }
206 let mut p = 0;
207 while p < parts.len() {
208 let mut q = p + 1;
209 while q < parts.len() {
210 let (mut i, a, b) = (0, parts[p], parts[q]);
211 while i < a.len() {
212 let mut j = 0;
213 while j < b.len() {
214 if clash(&a[i], &b[j]) {
215 return true;
216 }
217 j += 1;
218 }
219 i += 1;
220 }
221 q += 1;
222 }
223 p += 1;
224 }
225 false
226}
227
228#[doc(hidden)]
236pub const fn concat_items<const N: usize>(parts: &[&[Item]]) -> [Item; N] {
237 let mut out = [Item::EMPTY; N];
238 let (mut n, mut p) = (0, 0);
239 while p < parts.len() {
240 let part = parts[p];
241 let mut i = 0;
242 while i < part.len() {
243 out[n] = part[i];
244 n += 1;
245 i += 1;
246 }
247 p += 1;
248 }
249 assert!(n == N, "concat_items: the length is not the parts' total");
250 out
251}
252
253#[derive(Debug)]
255pub struct Sub {
256 pub name: &'static str,
258 pub aliases: &'static [&'static str],
260 pub names: &'static [&'static str],
262 pub command: &'static Command,
264 pub about: &'static str,
266 pub hide: bool,
268}
269
270pub fn resolve<'w>(
273 root: &'static Command,
274 words: impl IntoIterator<Item = &'w [u8]>,
275) -> (&'static Command, Vec<&'static str>) {
276 let (mut command, mut path) = (root, Vec::new());
277 for word in words {
278 let Some(sub) = command
279 .subcommands
280 .iter()
281 .find(|s| s.names.iter().any(|n| n.as_bytes() == word))
282 else {
283 break;
284 };
285 path.push(sub.name);
286 command = sub.command;
287 }
288 (command, path)
289}
290
291pub fn width() -> usize {
298 crate::env::var("COLUMNS")
299 .and_then(|columns| columns.to_str()?.trim().parse().ok())
300 .filter(|&columns| columns > 0)
301 .or_else(terminal_width)
302 .unwrap_or(100)
303}
304
305#[cfg(feature = "terminal-size")]
306fn terminal_width() -> Option<usize> {
307 if crate::env::overridden() {
308 return None;
309 }
310 terminal_size::terminal_size()
311 .map(|(width, _)| usize::from(width.0))
312 .filter(|&width| width > 0)
313}
314
315#[cfg(not(feature = "terminal-size"))]
316fn terminal_width() -> Option<usize> {
317 None
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct Style {
324 pub palette: Palette,
326 pub depth: Depth,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub(crate) struct Ink {
333 paint: Paint,
334 depth: Depth,
335}
336
337impl Ink {
338 const NONE: Ink = Ink {
340 paint: Paint::NONE,
341 depth: Depth::None,
342 };
343}
344
345impl Style {
346 pub const PLAIN: Style = Style::at(Palette::PLAIN, Depth::None);
348
349 pub const COLORED: Style = Style::at(Palette::DEFAULT, Depth::Ansi16);
351
352 pub const CLAP: Style = Style::at(Palette::CLAP, Depth::Ansi16);
354
355 pub const fn at(palette: Palette, depth: Depth) -> Style {
357 Style { palette, depth }
358 }
359
360 pub fn auto() -> Style {
363 use std::io::IsTerminal as _;
364 Style::themed(&Theme::DEFAULT, std::io::stdout().is_terminal())
365 }
366
367 pub fn auto_stderr() -> Style {
369 use std::io::IsTerminal as _;
370 Style::themed(&Theme::DEFAULT, std::io::stderr().is_terminal())
371 }
372
373 pub fn auto_for(is_terminal: bool) -> Style {
375 Style::themed(&Theme::DEFAULT, is_terminal)
376 }
377
378 pub fn themed(theme: &Theme, is_terminal: bool) -> Style {
381 let depth = Depth::detect(is_terminal);
382 Style::at(theme.palette(depth), depth)
383 }
384
385 fn ink(self, paint: Paint) -> Ink {
386 Ink {
387 paint,
388 depth: self.depth,
389 }
390 }
391
392 pub(crate) fn header(self) -> Ink {
393 self.ink(self.palette.header)
394 }
395
396 pub(crate) fn program(self) -> Ink {
397 self.ink(self.palette.program)
398 }
399
400 pub(crate) fn flag(self) -> Ink {
401 self.ink(self.palette.flag)
402 }
403
404 pub(crate) fn command(self) -> Ink {
405 self.ink(self.palette.command)
406 }
407
408 pub(crate) fn dim(self) -> Ink {
409 self.ink(self.palette.dim)
410 }
411
412 pub(crate) fn code(self) -> Ink {
413 self.ink(self.palette.code)
414 }
415
416 pub(crate) fn emphasis(self) -> Ink {
417 self.ink(self.palette.emphasis)
418 }
419
420 pub(crate) fn strong(self) -> Ink {
421 self.ink(self.palette.strong)
422 }
423
424 pub(crate) fn placeholder(self) -> Ink {
425 self.ink(self.palette.placeholder)
426 }
427
428 pub(crate) fn error(self) -> Ink {
429 self.ink(self.palette.error)
430 }
431
432 pub(crate) fn invalid(self) -> Ink {
433 self.ink(self.palette.invalid)
434 }
435
436 pub(crate) fn valid(self) -> Ink {
437 self.ink(self.palette.valid)
438 }
439
440 pub(crate) fn env(self) -> Ink {
441 self.ink(self.palette.env)
442 }
443
444 pub(crate) fn default(self) -> Ink {
445 self.ink(self.palette.default)
446 }
447
448 pub(crate) fn choice(self) -> Ink {
449 self.ink(self.palette.choice)
450 }
451
452 pub(crate) fn paint(ink: Ink, text: &str) -> String {
454 let mut out = String::new();
455 push_painted(&mut out, ink, text);
456 out
457 }
458}
459
460fn push_painted(out: &mut String, ink: Ink, text: &str) {
462 ink.paint.write(out, ink.depth, text);
463}
464
465#[derive(Default)]
467struct Cell {
468 text: String,
469 width: usize,
470}
471
472impl Cell {
473 fn push(&mut self, ink: Ink, text: &str) {
474 push_painted(&mut self.text, ink, text);
475 self.width += text.chars().count();
476 }
477
478 fn push_placeholder(&mut self, style: Style, required: bool, name: &str) {
481 if required {
482 self.push(style.placeholder(), &format!("<{name}>"));
483 } else {
484 self.push(style.dim(), "[");
485 self.push(style.placeholder(), name);
486 self.push(style.dim(), "]");
487 }
488 }
489}
490
491pub fn render(command: &Command, path: &[&str], long: bool) -> String {
495 render_styled(command, path, long, width(), Style::PLAIN)
496}
497
498pub fn render_width(command: &Command, path: &[&str], long: bool, width: usize) -> String {
500 render_styled(command, path, long, width, Style::PLAIN)
501}
502
503pub fn render_styled(
505 command: &Command,
506 path: &[&str],
507 long: bool,
508 width: usize,
509 style: Style,
510) -> String {
511 let mut out = String::new();
512 let about = if long && !command.long_about.is_empty() {
513 command.long_about
514 } else {
515 command.about
516 };
517 if !about.is_empty() {
518 let about = painted_marks(about, style);
519 let _ = writeln!(out, "{}\n", wrap_text(&about, width));
520 }
521 push_painted(&mut out, style.header(), "Usage:");
522 let _ = writeln!(out, " {}\n", usage(command, path, style));
523
524 let visible_subs: Vec<&Sub> = command.subcommands.iter().filter(|s| !s.hide).collect();
525 if !visible_subs.is_empty() {
526 let rows: Vec<(Cell, String)> = visible_subs
527 .iter()
528 .map(|s| {
529 let mut name = Cell::default();
530 name.push(style.command(), s.name);
531 for alias in s.aliases {
532 name.push(Ink::NONE, ", ");
533 name.push(style.command(), alias);
534 }
535 (name, painted_marks(s.about, style))
536 })
537 .collect();
538 section(&mut out, "Commands", &rows, false, width, style);
539 }
540
541 let visible: Vec<&Item> = command.items.iter().filter(|i| !i.hide).collect();
542 let arguments: Vec<(Cell, String)> = visible
543 .iter()
544 .filter(|i| i.positional)
545 .map(|i| {
546 let mut cell = Cell::default();
547 push_item_placeholder(&mut cell, style, i);
548 (cell, describe(i, long, style))
549 })
550 .collect();
551 section(&mut out, "Arguments", &arguments, long, width, style);
552
553 let mut headings: Vec<Option<&str>> = vec![None];
554 for item in visible.iter().filter(|i| !i.positional) {
555 if !headings.contains(&item.heading) {
556 headings.push(item.heading);
557 }
558 }
559 let builtin = |short: &str, long_name: &str| {
560 let mut cell = Cell::default();
561 cell.push(style.flag(), short);
562 cell.push(Ink::NONE, ", ");
563 cell.push(style.flag(), long_name);
564 cell
565 };
566 for heading in headings {
567 let mut rows: Vec<(Cell, String)> = visible
568 .iter()
569 .filter(|i| !i.positional && i.heading == heading)
570 .map(|i| (flag_spec(i, style), describe(i, long, style)))
571 .collect();
572 if heading.is_none() {
573 if command.help_flag {
574 let what = if long {
575 "Print help (see a summary with '-h')"
576 } else {
577 "Print help (see more with '--help')"
578 };
579 let cell = if command.help_short {
580 builtin("-h", "--help")
581 } else {
582 let mut cell = Cell::default();
583 cell.push(Ink::NONE, " ");
584 cell.push(style.flag(), "--help");
585 cell
586 };
587 let what = if command.help_short {
588 what
589 } else {
590 "Print help"
591 };
592 rows.push((cell, what.into()));
593 }
594 if command.version.is_some() {
595 rows.push((builtin("-V", "--version"), "Print version".into()));
596 }
597 }
598 section(
599 &mut out,
600 heading.unwrap_or("Options"),
601 &rows,
602 long,
603 width,
604 style,
605 );
606 }
607
608 let after = if long && !command.after_long_help.is_empty() {
609 command.after_long_help
610 } else {
611 command.after_help
612 };
613 if !after.is_empty() {
614 let _ = writeln!(out, "{}", wrap_text(after, width));
615 }
616 while out.ends_with("\n\n") {
617 out.pop();
618 }
619 out
620}
621
622pub(crate) fn usage(command: &Command, path: &[&str], style: Style) -> String {
624 let mut line = Style::paint(style.program(), &path.join(" "));
625 let mut cell = Cell::default();
626 if command.items.iter().any(|i| !i.positional && !i.hide) || command.help_flag {
627 cell.push(Ink::NONE, " ");
628 cell.push_placeholder(style, false, "OPTIONS");
629 }
630 for item in command
631 .items
632 .iter()
633 .filter(|i| i.positional && !i.hide && !i.trailing)
634 {
635 cell.push(Ink::NONE, " ");
636 push_item_placeholder(&mut cell, style, item);
637 }
638 if !command.subcommands.is_empty() {
639 cell.push(Ink::NONE, " ");
640 cell.push_placeholder(style, command.subcommand_required, "COMMAND");
641 }
642 line.push_str(&cell.text);
643 for item in command
644 .items
645 .iter()
646 .filter(|i| i.positional && !i.hide && i.trailing)
647 {
648 let mut cell = Cell::default();
650 cell.push(Ink::NONE, " ");
651 cell.push(style.dim(), "[");
652 cell.push(style.flag(), "--");
653 cell.push(Ink::NONE, " ");
654 push_item_placeholder(&mut cell, style, item);
655 cell.push(style.dim(), "]");
656 line.push_str(&cell.text);
657 }
658 line
659}
660
661fn push_item_placeholder(cell: &mut Cell, style: Style, item: &Item) {
663 cell.push_placeholder(style, item.required, item.value_name.unwrap_or("VALUE"));
664 if item.multiple {
665 cell.push(style.dim(), "...");
666 }
667}
668
669fn flag_spec(item: &Item, style: Style) -> Cell {
670 let mut spec = Cell::default();
671 let shorts = item.short.iter().chain(item.more_shorts);
674 let mut names: Vec<String> = shorts.map(|s| format!("-{s}")).collect();
675 names.extend(item.plus.map(|c| format!("+{c}")));
676 if names.is_empty() && item.long.is_some() {
677 spec.push(Ink::NONE, " ");
678 }
679 names.extend(item.long.map(|l| format!("--{l}")));
680 for (i, name) in names.iter().enumerate() {
681 if i > 0 {
682 spec.push(Ink::NONE, ", ");
683 }
684 spec.push(style.flag(), name);
685 }
686 if let Some(no) = item.negate {
687 spec.push(Ink::NONE, " / ");
688 spec.push(style.flag(), &format!("--{no}"));
689 }
690 if let Some(value) = item.value_name {
691 let more = if item.more_values { "..." } else { "" };
694 let value = vec![format!("<{value}>"); item.values.max(1)].join(" ") + more;
695 let (open, close) = match (item.require_equals, item.optional_value) {
696 (true, true) => ("[=", "]"),
697 (true, false) => ("=", ""),
698 (false, true) => (" [", "]"),
699 (false, false) => (" ", ""),
700 };
701 spec.push(Ink::NONE, open);
702 spec.push(style.placeholder(), &value);
703 spec.push(Ink::NONE, close);
704 }
705 if item.multiple {
706 spec.push(style.dim(), "...");
707 }
708 spec
709}
710
711fn describe(item: &Item, long: bool, style: Style) -> String {
715 let help = if long && !item.long_help.is_empty() {
716 item.long_help
717 } else {
718 item.help
719 };
720 let mut text = painted_marks(help, style);
721 let mut add = |extra: String| {
722 if !text.is_empty() {
723 text.push(if long { '\n' } else { ' ' });
724 }
725 text.push_str(&extra);
726 };
727 let label = |label: &str| Style::paint(style.dim(), label);
728 let close = || Style::paint(style.dim(), "]");
729 if let Some(env) = item.env {
730 let env = Style::paint(style.env(), env);
731 add(format!("{}{env}{}", label("[env: "), close()));
732 }
733 if let Some(default) = item.default {
734 let default = Style::paint(style.default(), default);
735 add(format!("{}{default}{}", label("[default: "), close()));
736 }
737 if !item.choices.is_empty() {
738 let choices: Vec<String> = item
739 .choices
740 .iter()
741 .map(|c| Style::paint(style.choice(), c))
742 .collect();
743 add(format!(
744 "{}{}{}",
745 label("[possible values: "),
746 choices.join(", "),
747 close()
748 ));
749 }
750 text
751}
752
753fn heading(out: &mut String, title: &str, style: Style) {
757 let header = style.header();
758 let inks = [style.code(), style.emphasis(), style.strong()];
759 let mut at = 0;
760 if style.depth != Depth::None {
761 while let Some((open, end, ink)) = marked(title, at, inks) {
762 if at < open {
763 push_painted(out, header, &title[at..open]);
764 }
765 let both = Paint {
766 fg: header.paint.fg.or(ink.paint.fg),
767 bold: header.paint.bold || ink.paint.bold,
768 dim: header.paint.dim || ink.paint.dim,
769 italic: header.paint.italic || ink.paint.italic,
770 underline: header.paint.underline || ink.paint.underline,
771 };
772 push_painted(out, style.ink(both), &title[open..end]);
773 at = end;
774 }
775 }
776 push_painted(out, header, &title[at..]);
777}
778
779fn painted_marks(text: &str, style: Style) -> String {
784 let inks = [style.code(), style.emphasis(), style.strong()];
785 if style.depth == Depth::None
786 || inks.iter().all(|ink| ink.paint == Paint::NONE)
787 || !text.contains(['`', '*'])
788 {
789 return text.to_owned();
790 }
791 let mut out = String::with_capacity(text.len() + 16);
792 for (n, line) in text.split('\n').enumerate() {
793 if n > 0 {
794 out.push('\n');
795 }
796 let mut at = 0;
797 while let Some((open, end, ink)) = marked(line, at, inks) {
798 out.push_str(&line[at..open]);
799 for (i, word) in line[open..end].split(' ').enumerate() {
800 if i > 0 {
801 out.push(' ');
802 }
803 if !word.is_empty() {
804 push_painted(&mut out, ink, word);
805 }
806 }
807 at = end;
808 }
809 out.push_str(&line[at..]);
810 }
811 out
812}
813
814fn marked(
825 line: &str,
826 from: usize,
827 [code, emphasis, strong]: [Ink; 3],
828) -> Option<(usize, usize, Ink)> {
829 let bytes = line.as_bytes();
830 let mut at = from;
831 while at < bytes.len() {
832 match bytes[at] {
833 b'`' => {
834 if let Some(len) = line[at + 1..].find('`') {
835 return Some((at, at + len + 2, code));
836 }
837 at += 1;
838 }
839 b'*' => {
840 let stars = run(bytes, at);
841 let before = line[..at].chars().next_back();
842 let after = line[at + stars..].chars().next();
843 let ink = if stars == 2 { strong } else { emphasis };
844 if stars <= 2
845 && ink.paint != Paint::NONE
846 && before.is_none_or(|c| !c.is_alphanumeric() && c != '\\')
847 && after.is_some_and(|c| !c.is_whitespace())
848 && let Some(end) = closing(line, at + stars, stars)
849 {
850 return Some((at, end, ink));
851 }
852 at += stars;
853 }
854 _ => at += 1,
855 }
856 }
857 None
858}
859
860fn run(bytes: &[u8], at: usize) -> usize {
862 bytes[at..].iter().take_while(|b| **b == b'*').count()
863}
864
865fn closing(line: &str, from: usize, stars: usize) -> Option<usize> {
870 let bytes = line.as_bytes();
871 let mut at = from;
872 while at < bytes.len() {
873 match bytes[at] {
874 b'`' => at += line[at + 1..].find('`').map_or(1, |len| len + 2),
875 b'*' => {
876 let found = run(bytes, at);
877 let before = line[..at].chars().next_back();
878 let after = line[at + found..].chars().next();
879 if found == stars {
880 let closes = before.is_some_and(|c| !c.is_whitespace() && c != '\\')
881 && after.is_none_or(|c| !c.is_alphanumeric());
882 return closes.then_some(at + found);
883 }
884 at += found;
885 }
886 _ => at += 1,
887 }
888 }
889 None
890}
891
892fn section(
898 out: &mut String,
899 title: &str,
900 rows: &[(Cell, String)],
901 long: bool,
902 width: usize,
903 style: Style,
904) {
905 if rows.is_empty() {
906 return;
907 }
908 heading(out, &format!("{title}:"), style);
909 out.push('\n');
910 let longest = rows.iter().map(|(left, _)| left.width).max().unwrap_or(0);
911 let available = width.saturating_sub(4);
912 let column = longest.min(available / 5 * 2 + available % 5 * 2 / 5);
913 let (indent, room) = if long {
915 (10, width.saturating_sub(10).max(20))
916 } else {
917 (column + 4, width.saturating_sub(column + 4).max(20))
918 };
919 for (left, right) in rows {
920 let beside = !long && !right.is_empty() && left.width <= column;
921 out.push_str(" ");
922 out.push_str(&left.text);
923 if beside {
924 let _ = write!(out, "{:pad$} ", "", pad = column - left.width);
926 } else {
927 out.push('\n');
928 }
929 let mut first = beside;
930 for line in right.lines() {
931 if line.is_empty() {
932 out.push('\n');
933 continue;
934 }
935 for piece in Wrap::new(line, room) {
936 if !first {
937 let _ = write!(out, "{:indent$}", "");
938 }
939 first = false;
940 out.push_str(piece);
941 out.push('\n');
942 }
943 }
944 if long {
945 out.push('\n');
947 }
948 }
949 if !long {
950 out.push('\n');
951 }
952}
953
954fn wrap_text(text: &str, width: usize) -> String {
956 let mut out = String::with_capacity(text.len() + text.len() / width.max(1));
957 for (i, line) in text.lines().enumerate() {
958 if i > 0 {
959 out.push('\n');
960 }
961 for (j, piece) in Wrap::new(line, width.max(20)).enumerate() {
962 if j > 0 {
963 out.push('\n');
964 }
965 out.push_str(piece);
966 }
967 }
968 out
969}
970
971struct Wrap<'a> {
975 rest: &'a str,
976 width: usize,
977}
978
979impl<'a> Wrap<'a> {
980 fn new(line: &'a str, width: usize) -> Self {
981 Wrap {
982 rest: line.trim_end(),
983 width,
984 }
985 }
986}
987
988impl<'a> Iterator for Wrap<'a> {
989 type Item = &'a str;
990
991 fn next(&mut self) -> Option<&'a str> {
992 if self.rest.is_empty() {
993 return None;
994 }
995 let (mut end, mut space, mut count, mut text) = (self.rest.len(), None, 0, false);
997 let mut escape = false;
998 for (at, c) in self.rest.char_indices() {
999 if escape {
1001 escape = c != 'm';
1002 continue;
1003 }
1004 if c == '\u{1b}' {
1005 escape = true;
1006 continue;
1007 }
1008 if count == self.width {
1009 end = at;
1010 break;
1011 }
1012 if c != ' ' {
1014 text = true;
1015 } else if text {
1016 space = Some(at);
1017 }
1018 count += 1;
1019 }
1020 let cut = match (end == self.rest.len(), space) {
1021 (true, _) => end,
1022 (false, _) if self.rest[end..].starts_with(' ') => end,
1024 (false, Some(at)) => at,
1025 (false, None) => self.rest[end..]
1027 .find(' ')
1028 .map_or(self.rest.len(), |at| end + at),
1029 };
1030 let piece = self.rest[..cut].trim_end();
1031 self.rest = self.rest[cut..].trim_start();
1032 Some(piece)
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::Wrap;
1039
1040 fn wrap(line: &str, width: usize) -> Vec<&str> {
1041 Wrap::new(line, width).collect()
1042 }
1043
1044 #[test]
1045 fn pieces_break_at_spaces_within_the_width() {
1046 assert_eq!(wrap("aa bb cc", 5), ["aa bb", "cc"]);
1047 assert_eq!(wrap("aa bb", 5), ["aa bb"]);
1048 assert_eq!(wrap("aa bb cc", 6), ["aa bb", "cc"]);
1049 }
1050
1051 #[test]
1052 fn a_long_word_stands_alone() {
1053 assert_eq!(wrap("a verylongword b", 5), ["a", "verylongword", "b"]);
1054 }
1055
1056 #[test]
1057 fn indentation_stays_on_the_first_piece() {
1058 assert_eq!(wrap(" $ mise use node", 12), [" $ mise", "use node"]);
1059 }
1060
1061 #[test]
1062 fn escapes_take_no_columns() {
1063 let green = |s: &str| format!("\u{1b}[32m{s}\u{1b}[0m");
1064 let line = format!("[possible values: {}, {}]", green("auto"), green("never"));
1065 let pieces = wrap(&line, 24);
1066 assert_eq!(
1067 pieces,
1068 [
1069 format!("[possible values: {},", green("auto")),
1070 format!("{}]", green("never"))
1071 ]
1072 );
1073 }
1074
1075 #[test]
1076 fn width_counts_characters() {
1077 assert_eq!(wrap("éé éé éé", 5), ["éé éé", "éé"]);
1078 }
1079}