]>
Commit | Line | Data |
---|---|---|
f035d41b XL |
1 | use std::borrow::Cow; |
2 | use std::collections::BTreeSet; | |
3 | use std::env; | |
4 | use std::fmt; | |
5 | use std::sync::atomic::{AtomicBool, Ordering}; | |
6 | ||
7 | use crate::term::{wants_emoji, Term}; | |
8 | use lazy_static::lazy_static; | |
9 | ||
10 | #[cfg(feature = "ansi-parsing")] | |
11 | use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; | |
12 | ||
13 | #[cfg(not(feature = "ansi-parsing"))] | |
14 | fn strip_ansi_codes(s: &str) -> &str { | |
15 | s | |
16 | } | |
17 | ||
18 | fn default_colors_enabled(out: &Term) -> bool { | |
19 | (out.features().colors_supported() && &env::var("CLICOLOR").unwrap_or("1".into()) != "0") | |
20 | || &env::var("CLICOLOR_FORCE").unwrap_or("0".into()) != "0" | |
21 | } | |
22 | ||
23 | lazy_static! { | |
24 | static ref STDOUT_COLORS: AtomicBool = AtomicBool::new(default_colors_enabled(&Term::stdout())); | |
25 | static ref STDERR_COLORS: AtomicBool = AtomicBool::new(default_colors_enabled(&Term::stderr())); | |
26 | } | |
27 | ||
28 | /// Returns `true` if colors should be enabled for stdout. | |
29 | /// | |
30 | /// This honors the [clicolors spec](http://bixense.com/clicolors/). | |
31 | /// | |
32 | /// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. | |
33 | /// * `CLICOLOR == 0`: Don't output ANSI color escape codes. | |
34 | /// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. | |
35 | #[inline] | |
36 | pub fn colors_enabled() -> bool { | |
37 | STDOUT_COLORS.load(Ordering::Relaxed) | |
38 | } | |
39 | ||
40 | /// Forces colorization on or off for stdout. | |
41 | /// | |
42 | /// This overrides the default for the current process and changes the return value of the | |
43 | /// `colors_enabled` function. | |
44 | #[inline] | |
45 | pub fn set_colors_enabled(val: bool) { | |
46 | STDOUT_COLORS.store(val, Ordering::Relaxed) | |
47 | } | |
48 | ||
49 | /// Returns `true` if colors should be enabled for stderr. | |
50 | /// | |
51 | /// This honors the [clicolors spec](http://bixense.com/clicolors/). | |
52 | /// | |
53 | /// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. | |
54 | /// * `CLICOLOR == 0`: Don't output ANSI color escape codes. | |
55 | /// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. | |
56 | #[inline] | |
57 | pub fn colors_enabled_stderr() -> bool { | |
58 | STDERR_COLORS.load(Ordering::Relaxed) | |
59 | } | |
60 | ||
61 | /// Forces colorization on or off for stderr. | |
62 | /// | |
63 | /// This overrides the default for the current process and changes the return value of the | |
64 | /// `colors_enabled` function. | |
65 | #[inline] | |
66 | pub fn set_colors_enabled_stderr(val: bool) { | |
67 | STDERR_COLORS.store(val, Ordering::Relaxed) | |
68 | } | |
69 | ||
70 | /// Measure the width of a string in terminal characters. | |
71 | pub fn measure_text_width(s: &str) -> usize { | |
72 | str_width(&strip_ansi_codes(s)) | |
73 | } | |
74 | ||
75 | /// A terminal color. | |
76 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
77 | pub enum Color { | |
78 | Black, | |
79 | Red, | |
80 | Green, | |
81 | Yellow, | |
82 | Blue, | |
83 | Magenta, | |
84 | Cyan, | |
85 | White, | |
86 | } | |
87 | ||
88 | impl Color { | |
89 | #[inline] | |
90 | fn ansi_num(self) -> usize { | |
91 | match self { | |
92 | Color::Black => 0, | |
93 | Color::Red => 1, | |
94 | Color::Green => 2, | |
95 | Color::Yellow => 3, | |
96 | Color::Blue => 4, | |
97 | Color::Magenta => 5, | |
98 | Color::Cyan => 6, | |
99 | Color::White => 7, | |
100 | } | |
101 | } | |
102 | } | |
103 | ||
104 | /// A terminal style attribute. | |
105 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] | |
106 | pub enum Attribute { | |
107 | Bold, | |
108 | Dim, | |
109 | Italic, | |
110 | Underlined, | |
111 | Blink, | |
112 | Reverse, | |
113 | Hidden, | |
114 | } | |
115 | ||
116 | impl Attribute { | |
117 | #[inline] | |
118 | fn ansi_num(self) -> usize { | |
119 | match self { | |
120 | Attribute::Bold => 1, | |
121 | Attribute::Dim => 2, | |
122 | Attribute::Italic => 3, | |
123 | Attribute::Underlined => 4, | |
124 | Attribute::Blink => 5, | |
125 | Attribute::Reverse => 7, | |
126 | Attribute::Hidden => 8, | |
127 | } | |
128 | } | |
129 | } | |
130 | ||
131 | /// Defines the alignment for padding operations. | |
132 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
133 | pub enum Alignment { | |
134 | Left, | |
135 | Center, | |
136 | Right, | |
137 | } | |
138 | ||
139 | /// A stored style that can be applied. | |
140 | #[derive(Clone, Debug, PartialEq, Eq)] | |
141 | pub struct Style { | |
142 | fg: Option<Color>, | |
143 | bg: Option<Color>, | |
144 | fg_bright: bool, | |
145 | bg_bright: bool, | |
146 | attrs: BTreeSet<Attribute>, | |
147 | force: Option<bool>, | |
148 | for_stderr: bool, | |
149 | } | |
150 | ||
151 | impl Default for Style { | |
152 | fn default() -> Style { | |
153 | Style::new() | |
154 | } | |
155 | } | |
156 | ||
157 | impl Style { | |
158 | /// Returns an empty default style. | |
159 | pub fn new() -> Style { | |
160 | Style { | |
161 | fg: None, | |
162 | bg: None, | |
163 | fg_bright: false, | |
164 | bg_bright: false, | |
165 | attrs: BTreeSet::new(), | |
166 | force: None, | |
167 | for_stderr: false, | |
168 | } | |
169 | } | |
170 | ||
171 | /// Creates a style from a dotted string. | |
172 | /// | |
173 | /// Effectively the string is split at each dot and then the | |
174 | /// terms in between are applied. For instance `red.on_blue` will | |
175 | /// create a string that is red on blue background. Unknown terms | |
176 | /// are ignored. | |
177 | pub fn from_dotted_str(s: &str) -> Style { | |
178 | let mut rv = Style::new(); | |
179 | for part in s.split('.') { | |
180 | rv = match part { | |
181 | "black" => rv.black(), | |
182 | "red" => rv.red(), | |
183 | "green" => rv.green(), | |
184 | "yellow" => rv.yellow(), | |
185 | "blue" => rv.blue(), | |
186 | "magenta" => rv.magenta(), | |
187 | "cyan" => rv.cyan(), | |
188 | "white" => rv.white(), | |
189 | "bright" => rv.bright(), | |
190 | "on_black" => rv.on_black(), | |
191 | "on_red" => rv.on_red(), | |
192 | "on_green" => rv.on_green(), | |
193 | "on_yellow" => rv.on_yellow(), | |
194 | "on_blue" => rv.on_blue(), | |
195 | "on_magenta" => rv.on_magenta(), | |
196 | "on_cyan" => rv.on_cyan(), | |
197 | "on_white" => rv.on_white(), | |
198 | "on_bright" => rv.on_bright(), | |
199 | "bold" => rv.bold(), | |
200 | "dim" => rv.dim(), | |
201 | "underlined" => rv.underlined(), | |
202 | "blink" => rv.blink(), | |
203 | "reverse" => rv.reverse(), | |
204 | "hidden" => rv.hidden(), | |
205 | _ => { | |
206 | continue; | |
207 | } | |
208 | }; | |
209 | } | |
210 | rv | |
211 | } | |
212 | ||
213 | /// Apply the style to something that can be displayed. | |
214 | pub fn apply_to<D>(&self, val: D) -> StyledObject<D> { | |
215 | StyledObject { | |
216 | style: self.clone(), | |
217 | val, | |
218 | } | |
219 | } | |
220 | ||
221 | /// Forces styling on or off. | |
222 | /// | |
223 | /// This overrides the detection from `clicolors-control`. | |
224 | #[inline] | |
225 | pub fn force_styling(mut self, value: bool) -> Style { | |
226 | self.force = Some(value); | |
227 | self | |
228 | } | |
229 | ||
230 | /// Specifies that style is applying to something being written on stderr. | |
231 | #[inline] | |
232 | pub fn for_stderr(mut self) -> Style { | |
233 | self.for_stderr = true; | |
234 | self | |
235 | } | |
236 | ||
237 | /// Specifies that style is applying to something being written on stdout. | |
238 | /// | |
239 | /// This is the default behaviour. | |
240 | #[inline] | |
241 | pub fn for_stdout(mut self) -> Style { | |
242 | self.for_stderr = false; | |
243 | self | |
244 | } | |
245 | ||
246 | /// Sets a foreground color. | |
247 | #[inline] | |
248 | pub fn fg(mut self, color: Color) -> Style { | |
249 | self.fg = Some(color); | |
250 | self | |
251 | } | |
252 | ||
253 | /// Sets a background color. | |
254 | #[inline] | |
255 | pub fn bg(mut self, color: Color) -> Style { | |
256 | self.bg = Some(color); | |
257 | self | |
258 | } | |
259 | ||
260 | /// Adds a attr. | |
261 | #[inline] | |
262 | pub fn attr(mut self, attr: Attribute) -> Style { | |
263 | self.attrs.insert(attr); | |
264 | self | |
265 | } | |
266 | ||
267 | #[inline] | |
268 | pub fn black(self) -> Style { | |
269 | self.fg(Color::Black) | |
270 | } | |
271 | #[inline] | |
272 | pub fn red(self) -> Style { | |
273 | self.fg(Color::Red) | |
274 | } | |
275 | #[inline] | |
276 | pub fn green(self) -> Style { | |
277 | self.fg(Color::Green) | |
278 | } | |
279 | #[inline] | |
280 | pub fn yellow(self) -> Style { | |
281 | self.fg(Color::Yellow) | |
282 | } | |
283 | #[inline] | |
284 | pub fn blue(self) -> Style { | |
285 | self.fg(Color::Blue) | |
286 | } | |
287 | #[inline] | |
288 | pub fn magenta(self) -> Style { | |
289 | self.fg(Color::Magenta) | |
290 | } | |
291 | #[inline] | |
292 | pub fn cyan(self) -> Style { | |
293 | self.fg(Color::Cyan) | |
294 | } | |
295 | #[inline] | |
296 | pub fn white(self) -> Style { | |
297 | self.fg(Color::White) | |
298 | } | |
299 | ||
300 | #[inline] | |
301 | pub fn bright(mut self) -> Style { | |
302 | self.fg_bright = true; | |
303 | self | |
304 | } | |
305 | ||
306 | #[inline] | |
307 | pub fn on_black(self) -> Style { | |
308 | self.bg(Color::Black) | |
309 | } | |
310 | #[inline] | |
311 | pub fn on_red(self) -> Style { | |
312 | self.bg(Color::Red) | |
313 | } | |
314 | #[inline] | |
315 | pub fn on_green(self) -> Style { | |
316 | self.bg(Color::Green) | |
317 | } | |
318 | #[inline] | |
319 | pub fn on_yellow(self) -> Style { | |
320 | self.bg(Color::Yellow) | |
321 | } | |
322 | #[inline] | |
323 | pub fn on_blue(self) -> Style { | |
324 | self.bg(Color::Blue) | |
325 | } | |
326 | #[inline] | |
327 | pub fn on_magenta(self) -> Style { | |
328 | self.bg(Color::Magenta) | |
329 | } | |
330 | #[inline] | |
331 | pub fn on_cyan(self) -> Style { | |
332 | self.bg(Color::Cyan) | |
333 | } | |
334 | #[inline] | |
335 | pub fn on_white(self) -> Style { | |
336 | self.bg(Color::White) | |
337 | } | |
338 | ||
339 | #[inline] | |
340 | pub fn on_bright(mut self) -> Style { | |
341 | self.bg_bright = true; | |
342 | self | |
343 | } | |
344 | ||
345 | #[inline] | |
346 | pub fn bold(self) -> Style { | |
347 | self.attr(Attribute::Bold) | |
348 | } | |
349 | #[inline] | |
350 | pub fn dim(self) -> Style { | |
351 | self.attr(Attribute::Dim) | |
352 | } | |
353 | #[inline] | |
354 | pub fn italic(self) -> Style { | |
355 | self.attr(Attribute::Italic) | |
356 | } | |
357 | #[inline] | |
358 | pub fn underlined(self) -> Style { | |
359 | self.attr(Attribute::Underlined) | |
360 | } | |
361 | #[inline] | |
362 | pub fn blink(self) -> Style { | |
363 | self.attr(Attribute::Blink) | |
364 | } | |
365 | #[inline] | |
366 | pub fn reverse(self) -> Style { | |
367 | self.attr(Attribute::Reverse) | |
368 | } | |
369 | #[inline] | |
370 | pub fn hidden(self) -> Style { | |
371 | self.attr(Attribute::Hidden) | |
372 | } | |
373 | } | |
374 | ||
375 | /// Wraps an object for formatting for styling. | |
376 | /// | |
377 | /// Example: | |
378 | /// | |
379 | /// ```rust,no_run | |
380 | /// # use console::style; | |
381 | /// format!("Hello {}", style("World").cyan()); | |
382 | /// ``` | |
383 | /// | |
384 | /// This is a shortcut for making a new style and applying it | |
385 | /// to a value: | |
386 | /// | |
387 | /// ```rust,no_run | |
388 | /// # use console::Style; | |
389 | /// format!("Hello {}", Style::new().cyan().apply_to("World")); | |
390 | /// ``` | |
391 | pub fn style<D>(val: D) -> StyledObject<D> { | |
392 | Style::new().apply_to(val) | |
393 | } | |
394 | ||
395 | /// A formatting wrapper that can be styled for a terminal. | |
396 | #[derive(Clone)] | |
397 | pub struct StyledObject<D> { | |
398 | style: Style, | |
399 | val: D, | |
400 | } | |
401 | ||
402 | impl<D> StyledObject<D> { | |
403 | /// Forces styling on or off. | |
404 | /// | |
405 | /// This overrides the detection from `clicolors-control`. | |
406 | #[inline] | |
407 | pub fn force_styling(mut self, value: bool) -> StyledObject<D> { | |
408 | self.style = self.style.force_styling(value); | |
409 | self | |
410 | } | |
411 | ||
412 | /// Specifies that style is applying to something being written on stderr | |
413 | #[inline] | |
414 | pub fn for_stderr(mut self) -> StyledObject<D> { | |
415 | self.style = self.style.for_stderr(); | |
416 | self | |
417 | } | |
418 | ||
419 | /// Specifies that style is applying to something being written on stdout | |
420 | /// | |
421 | /// This is the default | |
422 | #[inline] | |
423 | pub fn for_stdout(mut self) -> StyledObject<D> { | |
424 | self.style = self.style.for_stdout(); | |
425 | self | |
426 | } | |
427 | ||
428 | /// Sets a foreground color. | |
429 | #[inline] | |
430 | pub fn fg(mut self, color: Color) -> StyledObject<D> { | |
431 | self.style = self.style.fg(color); | |
432 | self | |
433 | } | |
434 | ||
435 | /// Sets a background color. | |
436 | #[inline] | |
437 | pub fn bg(mut self, color: Color) -> StyledObject<D> { | |
438 | self.style = self.style.bg(color); | |
439 | self | |
440 | } | |
441 | ||
442 | /// Adds a attr. | |
443 | #[inline] | |
444 | pub fn attr(mut self, attr: Attribute) -> StyledObject<D> { | |
445 | self.style = self.style.attr(attr); | |
446 | self | |
447 | } | |
448 | ||
449 | #[inline] | |
450 | pub fn black(self) -> StyledObject<D> { | |
451 | self.fg(Color::Black) | |
452 | } | |
453 | #[inline] | |
454 | pub fn red(self) -> StyledObject<D> { | |
455 | self.fg(Color::Red) | |
456 | } | |
457 | #[inline] | |
458 | pub fn green(self) -> StyledObject<D> { | |
459 | self.fg(Color::Green) | |
460 | } | |
461 | #[inline] | |
462 | pub fn yellow(self) -> StyledObject<D> { | |
463 | self.fg(Color::Yellow) | |
464 | } | |
465 | #[inline] | |
466 | pub fn blue(self) -> StyledObject<D> { | |
467 | self.fg(Color::Blue) | |
468 | } | |
469 | #[inline] | |
470 | pub fn magenta(self) -> StyledObject<D> { | |
471 | self.fg(Color::Magenta) | |
472 | } | |
473 | #[inline] | |
474 | pub fn cyan(self) -> StyledObject<D> { | |
475 | self.fg(Color::Cyan) | |
476 | } | |
477 | #[inline] | |
478 | pub fn white(self) -> StyledObject<D> { | |
479 | self.fg(Color::White) | |
480 | } | |
481 | ||
482 | #[inline] | |
483 | pub fn bright(mut self) -> StyledObject<D> { | |
484 | self.style = self.style.bright(); | |
485 | self | |
486 | } | |
487 | ||
488 | #[inline] | |
489 | pub fn on_black(self) -> StyledObject<D> { | |
490 | self.bg(Color::Black) | |
491 | } | |
492 | #[inline] | |
493 | pub fn on_red(self) -> StyledObject<D> { | |
494 | self.bg(Color::Red) | |
495 | } | |
496 | #[inline] | |
497 | pub fn on_green(self) -> StyledObject<D> { | |
498 | self.bg(Color::Green) | |
499 | } | |
500 | #[inline] | |
501 | pub fn on_yellow(self) -> StyledObject<D> { | |
502 | self.bg(Color::Yellow) | |
503 | } | |
504 | #[inline] | |
505 | pub fn on_blue(self) -> StyledObject<D> { | |
506 | self.bg(Color::Blue) | |
507 | } | |
508 | #[inline] | |
509 | pub fn on_magenta(self) -> StyledObject<D> { | |
510 | self.bg(Color::Magenta) | |
511 | } | |
512 | #[inline] | |
513 | pub fn on_cyan(self) -> StyledObject<D> { | |
514 | self.bg(Color::Cyan) | |
515 | } | |
516 | #[inline] | |
517 | pub fn on_white(self) -> StyledObject<D> { | |
518 | self.bg(Color::White) | |
519 | } | |
520 | ||
521 | #[inline] | |
522 | pub fn on_bright(mut self) -> StyledObject<D> { | |
523 | self.style = self.style.on_bright(); | |
524 | self | |
525 | } | |
526 | ||
527 | #[inline] | |
528 | pub fn bold(self) -> StyledObject<D> { | |
529 | self.attr(Attribute::Bold) | |
530 | } | |
531 | #[inline] | |
532 | pub fn dim(self) -> StyledObject<D> { | |
533 | self.attr(Attribute::Dim) | |
534 | } | |
535 | #[inline] | |
536 | pub fn italic(self) -> StyledObject<D> { | |
537 | self.attr(Attribute::Italic) | |
538 | } | |
539 | #[inline] | |
540 | pub fn underlined(self) -> StyledObject<D> { | |
541 | self.attr(Attribute::Underlined) | |
542 | } | |
543 | #[inline] | |
544 | pub fn blink(self) -> StyledObject<D> { | |
545 | self.attr(Attribute::Blink) | |
546 | } | |
547 | #[inline] | |
548 | pub fn reverse(self) -> StyledObject<D> { | |
549 | self.attr(Attribute::Reverse) | |
550 | } | |
551 | #[inline] | |
552 | pub fn hidden(self) -> StyledObject<D> { | |
553 | self.attr(Attribute::Hidden) | |
554 | } | |
555 | } | |
556 | ||
557 | macro_rules! impl_fmt { | |
558 | ($name:ident) => { | |
559 | impl<D: fmt::$name> fmt::$name for StyledObject<D> { | |
560 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
561 | let mut reset = false; | |
562 | if self | |
563 | .style | |
564 | .force | |
565 | .unwrap_or_else(|| match self.style.for_stderr { | |
566 | true => colors_enabled_stderr(), | |
567 | false => colors_enabled(), | |
568 | }) | |
569 | { | |
570 | if let Some(fg) = self.style.fg { | |
571 | if self.style.fg_bright { | |
572 | write!(f, "\x1b[38;5;{}m", fg.ansi_num() + 8)?; | |
573 | } else { | |
574 | write!(f, "\x1b[{}m", fg.ansi_num() + 30)?; | |
575 | } | |
576 | reset = true; | |
577 | } | |
578 | if let Some(bg) = self.style.bg { | |
579 | if self.style.bg_bright { | |
580 | write!(f, "\x1b[48;5;{}m", bg.ansi_num() + 8)?; | |
581 | } else { | |
582 | write!(f, "\x1b[{}m", bg.ansi_num() + 40)?; | |
583 | } | |
584 | reset = true; | |
585 | } | |
586 | for attr in &self.style.attrs { | |
587 | write!(f, "\x1b[{}m", attr.ansi_num())?; | |
588 | reset = true; | |
589 | } | |
590 | } | |
591 | fmt::$name::fmt(&self.val, f)?; | |
592 | if reset { | |
593 | write!(f, "\x1b[0m")?; | |
594 | } | |
595 | Ok(()) | |
596 | } | |
597 | } | |
598 | }; | |
599 | } | |
600 | ||
601 | impl_fmt!(Binary); | |
602 | impl_fmt!(Debug); | |
603 | impl_fmt!(Display); | |
604 | impl_fmt!(LowerExp); | |
605 | impl_fmt!(LowerHex); | |
606 | impl_fmt!(Octal); | |
607 | impl_fmt!(Pointer); | |
608 | impl_fmt!(UpperExp); | |
609 | impl_fmt!(UpperHex); | |
610 | ||
611 | /// "Intelligent" emoji formatter. | |
612 | /// | |
613 | /// This struct intelligently wraps an emoji so that it is rendered | |
614 | /// only on systems that want emojis and renders a fallback on others. | |
615 | /// | |
616 | /// Example: | |
617 | /// | |
618 | /// ```rust | |
619 | /// use console::Emoji; | |
620 | /// println!("[3/4] {}Downloading ...", Emoji("🚚 ", "")); | |
621 | /// println!("[4/4] {} Done!", Emoji("✨", ":-)")); | |
622 | /// ``` | |
623 | #[derive(Copy, Clone)] | |
624 | pub struct Emoji<'a, 'b>(pub &'a str, pub &'b str); | |
625 | ||
626 | impl<'a, 'b> Emoji<'a, 'b> { | |
627 | pub fn new(emoji: &'a str, fallback: &'b str) -> Emoji<'a, 'b> { | |
628 | Emoji(emoji, fallback) | |
629 | } | |
630 | } | |
631 | ||
632 | impl<'a, 'b> fmt::Display for Emoji<'a, 'b> { | |
633 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
634 | if wants_emoji() { | |
635 | write!(f, "{}", self.0) | |
636 | } else { | |
637 | write!(f, "{}", self.1) | |
638 | } | |
639 | } | |
640 | } | |
641 | ||
642 | fn str_width(s: &str) -> usize { | |
643 | #[cfg(feature = "unicode-width")] | |
644 | { | |
645 | use unicode_width::UnicodeWidthStr; | |
646 | s.width() | |
647 | } | |
648 | #[cfg(not(feature = "unicode-width"))] | |
649 | { | |
650 | s.chars().count() | |
651 | } | |
652 | } | |
653 | ||
654 | #[cfg(feature = "ansi-parsing")] | |
655 | fn char_width(c: char) -> usize { | |
656 | #[cfg(feature = "unicode-width")] | |
657 | { | |
658 | use unicode_width::UnicodeWidthChar; | |
659 | c.width().unwrap_or(0) | |
660 | } | |
661 | #[cfg(not(feature = "unicode-width"))] | |
662 | { | |
663 | let _c = c; | |
664 | 1 | |
665 | } | |
666 | } | |
667 | ||
668 | /// Truncates a string to a certain number of characters. | |
669 | /// | |
670 | /// This ensures that escape codes are not screwed up in the process. | |
671 | /// If the maximum length is hit the string will be truncated but | |
672 | /// escapes code will still be honored. If truncation takes place | |
673 | /// the tail string will be appended. | |
674 | pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> { | |
675 | #[cfg(feature = "ansi-parsing")] | |
676 | { | |
677 | use std::cmp::Ordering; | |
678 | let mut iter = AnsiCodeIterator::new(s); | |
679 | let mut length = 0; | |
680 | let mut rv = None; | |
681 | ||
682 | while let Some(item) = iter.next() { | |
683 | match item { | |
684 | (s, false) => { | |
685 | if rv.is_none() { | |
686 | if str_width(s) + length > width - str_width(tail) { | |
687 | let ts = iter.current_slice(); | |
688 | ||
689 | let mut s_byte = 0; | |
690 | let mut s_width = 0; | |
691 | let rest_width = width - str_width(tail) - length; | |
692 | for c in s.chars() { | |
693 | s_byte += c.len_utf8(); | |
694 | s_width += char_width(c); | |
695 | match s_width.cmp(&rest_width) { | |
696 | Ordering::Equal => break, | |
697 | Ordering::Greater => { | |
698 | s_byte -= c.len_utf8(); | |
699 | break; | |
700 | } | |
701 | Ordering::Less => continue, | |
702 | } | |
703 | } | |
704 | ||
705 | let idx = ts.len() - s.len() + s_byte; | |
706 | let mut buf = ts[..idx].to_string(); | |
707 | buf.push_str(tail); | |
708 | rv = Some(buf); | |
709 | } | |
710 | length += str_width(s); | |
711 | } | |
712 | } | |
713 | (s, true) => { | |
714 | if rv.is_some() { | |
715 | rv.as_mut().unwrap().push_str(s); | |
716 | } | |
717 | } | |
718 | } | |
719 | } | |
720 | ||
721 | if let Some(buf) = rv { | |
722 | Cow::Owned(buf) | |
723 | } else { | |
724 | Cow::Borrowed(s) | |
725 | } | |
726 | } | |
727 | ||
728 | #[cfg(not(feature = "ansi-parsing"))] | |
729 | { | |
730 | if s.len() <= width - tail.len() { | |
731 | Cow::Borrowed(s) | |
732 | } else { | |
733 | Cow::Owned(format!( | |
734 | "{}{}", | |
735 | s.get(..width - tail.len()).unwrap_or_default(), | |
736 | tail | |
737 | )) | |
738 | } | |
739 | } | |
740 | } | |
741 | ||
742 | /// Pads a string to fill a certain number of characters. | |
743 | /// | |
744 | /// This will honor ansi codes correctly and allows you to align a string | |
745 | /// on the left, right or centered. Additionally truncation can be enabled | |
746 | /// by setting `truncate` to a string that should be used as a truncation | |
747 | /// marker. | |
748 | pub fn pad_str<'a>( | |
749 | s: &'a str, | |
750 | width: usize, | |
751 | align: Alignment, | |
752 | truncate: Option<&str>, | |
753 | ) -> Cow<'a, str> { | |
754 | pad_str_with(s, width, align, truncate, ' ') | |
755 | } | |
756 | /// Pads a string with specific padding to fill a certain number of characters. | |
757 | /// | |
758 | /// This will honor ansi codes correctly and allows you to align a string | |
759 | /// on the left, right or centered. Additionally truncation can be enabled | |
760 | /// by setting `truncate` to a string that should be used as a truncation | |
761 | /// marker. | |
762 | pub fn pad_str_with<'a>( | |
763 | s: &'a str, | |
764 | width: usize, | |
765 | align: Alignment, | |
766 | truncate: Option<&str>, | |
767 | pad: char, | |
768 | ) -> Cow<'a, str> { | |
769 | let cols = measure_text_width(s); | |
770 | ||
771 | if cols >= width { | |
772 | return match truncate { | |
773 | None => Cow::Borrowed(s), | |
774 | Some(tail) => truncate_str(s, width, tail), | |
775 | }; | |
776 | } | |
777 | ||
778 | let diff = width - cols; | |
779 | ||
780 | let (left_pad, right_pad) = match align { | |
781 | Alignment::Left => (0, diff), | |
782 | Alignment::Right => (diff, 0), | |
783 | Alignment::Center => (diff / 2, diff - diff / 2), | |
784 | }; | |
785 | ||
786 | let mut rv = String::new(); | |
787 | for _ in 0..left_pad { | |
788 | rv.push(pad); | |
789 | } | |
790 | rv.push_str(s); | |
791 | for _ in 0..right_pad { | |
792 | rv.push(pad); | |
793 | } | |
794 | Cow::Owned(rv) | |
795 | } | |
796 | ||
797 | #[test] | |
798 | fn test_text_width() { | |
799 | let s = style("foo") | |
800 | .red() | |
801 | .on_black() | |
802 | .bold() | |
803 | .force_styling(true) | |
804 | .to_string(); | |
805 | assert_eq!( | |
806 | measure_text_width(&s), | |
807 | if cfg!(feature = "ansi-parsing") { | |
808 | 3 | |
809 | } else { | |
810 | 21 | |
811 | } | |
812 | ); | |
813 | } | |
814 | ||
815 | #[test] | |
816 | #[cfg(all(feature = "unicode-width", feature = "ansi-parsing"))] | |
817 | fn test_truncate_str() { | |
818 | let s = format!("foo {}", style("bar").red().force_styling(true)); | |
819 | assert_eq!( | |
820 | &truncate_str(&s, 5, ""), | |
821 | &format!("foo {}", style("b").red().force_styling(true)) | |
822 | ); | |
823 | let s = format!("foo {}", style("bar").red().force_styling(true)); | |
824 | assert_eq!( | |
825 | &truncate_str(&s, 5, "!"), | |
826 | &format!("foo {}", style("!").red().force_styling(true)) | |
827 | ); | |
828 | let s = format!("foo {} baz", style("bar").red().force_styling(true)); | |
829 | assert_eq!( | |
830 | &truncate_str(&s, 10, "..."), | |
831 | &format!("foo {}...", style("bar").red().force_styling(true)) | |
832 | ); | |
833 | let s = format!("foo {}", style("バー").red().force_styling(true)); | |
834 | assert_eq!( | |
835 | &truncate_str(&s, 5, ""), | |
836 | &format!("foo {}", style("").red().force_styling(true)) | |
837 | ); | |
838 | let s = format!("foo {}", style("バー").red().force_styling(true)); | |
839 | assert_eq!( | |
840 | &truncate_str(&s, 6, ""), | |
841 | &format!("foo {}", style("バ").red().force_styling(true)) | |
842 | ); | |
843 | } | |
844 | ||
845 | #[test] | |
846 | fn test_truncate_str_no_ansi() { | |
847 | assert_eq!(&truncate_str("foo bar", 5, ""), "foo b"); | |
848 | assert_eq!(&truncate_str("foo bar", 5, "!"), "foo !"); | |
849 | assert_eq!(&truncate_str("foo bar baz", 10, "..."), "foo bar..."); | |
850 | } | |
851 | ||
852 | #[test] | |
853 | fn test_pad_str() { | |
854 | assert_eq!(pad_str("foo", 7, Alignment::Center, None), " foo "); | |
855 | assert_eq!(pad_str("foo", 7, Alignment::Left, None), "foo "); | |
856 | assert_eq!(pad_str("foo", 7, Alignment::Right, None), " foo"); | |
857 | assert_eq!(pad_str("foo", 3, Alignment::Left, None), "foo"); | |
858 | assert_eq!(pad_str("foobar", 3, Alignment::Left, None), "foobar"); | |
859 | assert_eq!(pad_str("foobar", 3, Alignment::Left, Some("")), "foo"); | |
860 | assert_eq!( | |
861 | pad_str("foobarbaz", 6, Alignment::Left, Some("...")), | |
862 | "foo..." | |
863 | ); | |
864 | } | |
865 | ||
866 | #[test] | |
867 | fn test_pad_str_with() { | |
868 | assert_eq!( | |
869 | pad_str_with("foo", 7, Alignment::Center, None, '#'), | |
870 | "##foo##" | |
871 | ); | |
872 | assert_eq!( | |
873 | pad_str_with("foo", 7, Alignment::Left, None, '#'), | |
874 | "foo####" | |
875 | ); | |
876 | assert_eq!( | |
877 | pad_str_with("foo", 7, Alignment::Right, None, '#'), | |
878 | "####foo" | |
879 | ); | |
880 | assert_eq!(pad_str_with("foo", 3, Alignment::Left, None, '#'), "foo"); | |
881 | assert_eq!( | |
882 | pad_str_with("foobar", 3, Alignment::Left, None, '#'), | |
883 | "foobar" | |
884 | ); | |
885 | assert_eq!( | |
886 | pad_str_with("foobar", 3, Alignment::Left, Some(""), '#'), | |
887 | "foo" | |
888 | ); | |
889 | assert_eq!( | |
890 | pad_str_with("foobarbaz", 6, Alignment::Left, Some("..."), '#'), | |
891 | "foo..." | |
892 | ); | |
893 | } |