1use std::collections::HashMap;
16use std::fmt;
17use std::io;
18use std::io::Error;
19use std::io::Write;
20use std::mem;
21use std::ops::Deref;
22use std::ops::DerefMut;
23use std::ops::Range;
24use std::sync::Arc;
25
26use crossterm::queue;
27use crossterm::style::Attribute;
28use crossterm::style::Color;
29use crossterm::style::SetAttribute;
30use crossterm::style::SetBackgroundColor;
31use crossterm::style::SetForegroundColor;
32use itertools::Itertools as _;
33use jj_lib::config::ConfigGetError;
34use jj_lib::config::StackedConfig;
35use serde::de::Deserialize as _;
36use serde::de::Error as _;
37use serde::de::IntoDeserializer as _;
38
39pub trait Formatter: Write {
41 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>>;
44
45 fn push_label(&mut self, label: &str);
46
47 fn pop_label(&mut self);
48
49 fn maybe_color(&self) -> bool;
50}
51
52impl<T: Formatter + ?Sized> Formatter for &mut T {
53 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
54 <T as Formatter>::raw(self)
55 }
56
57 fn push_label(&mut self, label: &str) {
58 <T as Formatter>::push_label(self, label);
59 }
60
61 fn pop_label(&mut self) {
62 <T as Formatter>::pop_label(self);
63 }
64
65 fn maybe_color(&self) -> bool {
66 <T as Formatter>::maybe_color(self)
67 }
68}
69
70impl<T: Formatter + ?Sized> Formatter for Box<T> {
71 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
72 <T as Formatter>::raw(self)
73 }
74
75 fn push_label(&mut self, label: &str) {
76 <T as Formatter>::push_label(self, label);
77 }
78
79 fn pop_label(&mut self) {
80 <T as Formatter>::pop_label(self);
81 }
82
83 fn maybe_color(&self) -> bool {
84 <T as Formatter>::maybe_color(self)
85 }
86}
87
88pub trait FormatterExt: Formatter {
90 fn labeled(&mut self, label: &str) -> LabeledScope<&mut Self> {
91 LabeledScope::new(self, label)
92 }
93
94 fn into_labeled(self, label: &str) -> LabeledScope<Self>
95 where
96 Self: Sized,
97 {
98 LabeledScope::new(self, label)
99 }
100}
101
102impl<T: Formatter + ?Sized> FormatterExt for T {}
103
104#[must_use]
106pub struct LabeledScope<T: Formatter> {
107 formatter: T,
108}
109
110impl<T: Formatter> LabeledScope<T> {
111 pub fn new(mut formatter: T, label: &str) -> Self {
112 formatter.push_label(label);
113 Self { formatter }
114 }
115
116 pub fn with_heading<H>(self, heading: H) -> HeadingLabeledWriter<T, H> {
119 HeadingLabeledWriter::new(self, heading)
120 }
121}
122
123impl<T: Formatter> Drop for LabeledScope<T> {
124 fn drop(&mut self) {
125 self.formatter.pop_label();
126 }
127}
128
129impl<T: Formatter> Deref for LabeledScope<T> {
130 type Target = T;
131
132 fn deref(&self) -> &Self::Target {
133 &self.formatter
134 }
135}
136
137impl<T: Formatter> DerefMut for LabeledScope<T> {
138 fn deref_mut(&mut self) -> &mut Self::Target {
139 &mut self.formatter
140 }
141}
142
143pub struct HeadingLabeledWriter<T: Formatter, H> {
151 formatter: LabeledScope<T>,
152 heading: Option<H>,
153}
154
155impl<T: Formatter, H> HeadingLabeledWriter<T, H> {
156 pub fn new(formatter: LabeledScope<T>, heading: H) -> Self {
157 Self {
158 formatter,
159 heading: Some(heading),
160 }
161 }
162}
163
164impl<T: Formatter, H: fmt::Display> HeadingLabeledWriter<T, H> {
165 pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
166 if let Some(heading) = self.heading.take() {
167 write!(self.formatter.labeled("heading"), "{heading}")?;
168 }
169 self.formatter.write_fmt(args)
170 }
171}
172
173type Rules = Vec<(Vec<String>, Style)>;
174
175#[derive(Clone, Debug)]
177pub struct FormatterFactory {
178 kind: FormatterFactoryKind,
179}
180
181#[derive(Clone, Debug)]
182enum FormatterFactoryKind {
183 PlainText,
184 Sanitized,
185 Color { rules: Arc<Rules>, debug: bool },
186}
187
188impl FormatterFactory {
189 pub fn plain_text() -> Self {
190 let kind = FormatterFactoryKind::PlainText;
191 Self { kind }
192 }
193
194 pub fn sanitized() -> Self {
195 let kind = FormatterFactoryKind::Sanitized;
196 Self { kind }
197 }
198
199 pub fn color(config: &StackedConfig, debug: bool) -> Result<Self, ConfigGetError> {
200 let rules = Arc::new(rules_from_config(config)?);
201 let kind = FormatterFactoryKind::Color { rules, debug };
202 Ok(Self { kind })
203 }
204
205 pub fn new_formatter<'output, W: Write + 'output>(
206 &self,
207 output: W,
208 ) -> Box<dyn Formatter + 'output> {
209 match &self.kind {
210 FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)),
211 FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)),
212 FormatterFactoryKind::Color { rules, debug } => {
213 Box::new(ColorFormatter::new(output, rules.clone(), *debug))
214 }
215 }
216 }
217
218 pub fn maybe_color(&self) -> bool {
219 matches!(self.kind, FormatterFactoryKind::Color { .. })
220 }
221}
222
223pub struct PlainTextFormatter<W> {
224 output: W,
225}
226
227impl<W> PlainTextFormatter<W> {
228 pub fn new(output: W) -> Self {
229 Self { output }
230 }
231}
232
233impl<W: Write> Write for PlainTextFormatter<W> {
234 fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
235 self.output.write(data)
236 }
237
238 fn flush(&mut self) -> Result<(), Error> {
239 self.output.flush()
240 }
241}
242
243impl<W: Write> Formatter for PlainTextFormatter<W> {
244 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
245 Ok(Box::new(self.output.by_ref()))
246 }
247
248 fn push_label(&mut self, _label: &str) {}
249
250 fn pop_label(&mut self) {}
251
252 fn maybe_color(&self) -> bool {
253 false
254 }
255}
256
257pub struct SanitizingFormatter<W> {
258 output: W,
259}
260
261impl<W> SanitizingFormatter<W> {
262 pub fn new(output: W) -> Self {
263 Self { output }
264 }
265}
266
267impl<W: Write> Write for SanitizingFormatter<W> {
268 fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
269 write_sanitized(&mut self.output, data)?;
270 Ok(data.len())
271 }
272
273 fn flush(&mut self) -> Result<(), Error> {
274 self.output.flush()
275 }
276}
277
278impl<W: Write> Formatter for SanitizingFormatter<W> {
279 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
280 Ok(Box::new(self.output.by_ref()))
281 }
282
283 fn push_label(&mut self, _label: &str) {}
284
285 fn pop_label(&mut self) {}
286
287 fn maybe_color(&self) -> bool {
288 false
289 }
290}
291
292#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize)]
293#[serde(default, rename_all = "kebab-case")]
294pub struct Style {
295 #[serde(deserialize_with = "deserialize_color_opt")]
296 pub fg: Option<Color>,
297 #[serde(deserialize_with = "deserialize_color_opt")]
298 pub bg: Option<Color>,
299 pub bold: Option<bool>,
300 pub dim: Option<bool>,
301 pub italic: Option<bool>,
302 pub underline: Option<bool>,
303 pub reverse: Option<bool>,
304}
305
306impl Style {
307 fn merge(&mut self, other: &Self) {
308 self.fg = other.fg.or(self.fg);
309 self.bg = other.bg.or(self.bg);
310 self.bold = other.bold.or(self.bold);
311 self.dim = other.dim.or(self.dim);
312 self.italic = other.italic.or(self.italic);
313 self.underline = other.underline.or(self.underline);
314 self.reverse = other.reverse.or(self.reverse);
315 }
316}
317
318#[derive(Clone, Debug)]
319pub struct ColorFormatter<W: Write> {
320 output: W,
321 rules: Arc<Rules>,
322 labels: Vec<String>,
325 cached_styles: HashMap<Vec<String>, Style>,
326 current_style: Style,
328 current_debug: Option<String>,
331}
332
333impl<W: Write> ColorFormatter<W> {
334 pub fn new(output: W, rules: Arc<Rules>, debug: bool) -> Self {
335 Self {
336 output,
337 rules,
338 labels: vec![],
339 cached_styles: HashMap::new(),
340 current_style: Style::default(),
341 current_debug: debug.then(String::new),
342 }
343 }
344
345 pub fn for_config(
346 output: W,
347 config: &StackedConfig,
348 debug: bool,
349 ) -> Result<Self, ConfigGetError> {
350 let rules = rules_from_config(config)?;
351 Ok(Self::new(output, Arc::new(rules), debug))
352 }
353
354 fn requested_style(&mut self) -> Style {
355 if let Some(cached) = self.cached_styles.get(&self.labels) {
356 cached.clone()
357 } else {
358 let mut matched_styles = vec![];
364 for (labels, style) in self.rules.as_ref() {
365 let mut labels_iter = self.labels.iter().enumerate();
366 let mut matched_indices = vec![];
368 for required_label in labels {
369 for (label_index, label) in &mut labels_iter {
370 if label == required_label {
371 matched_indices.push(label_index);
372 break;
373 }
374 }
375 }
376 if matched_indices.len() == labels.len() {
377 matched_indices.reverse();
378 matched_styles.push((style, matched_indices));
379 }
380 }
381 matched_styles.sort_by_key(|(_, indices)| indices.clone());
382
383 let mut style = Style::default();
384 for (matched_style, _) in matched_styles {
385 style.merge(matched_style);
386 }
387 self.cached_styles
388 .insert(self.labels.clone(), style.clone());
389 style
390 }
391 }
392
393 fn write_new_style(&mut self) -> io::Result<()> {
394 let new_debug = match &self.current_debug {
395 Some(current) => {
396 let joined = self.labels.join(" ");
397 if joined == *current {
398 None
399 } else {
400 if !current.is_empty() {
401 write!(self.output, ">>")?;
402 }
403 Some(joined)
404 }
405 }
406 None => None,
407 };
408 let new_style = self.requested_style();
409 if new_style != self.current_style {
410 let new_bold = new_style.bold.unwrap_or_default();
418 let new_dim = new_style.dim.unwrap_or_default();
419 if (new_style.bold != self.current_style.bold && !new_bold)
420 || (new_style.dim != self.current_style.dim && !new_dim)
421 {
422 queue!(self.output, SetAttribute(Attribute::Reset))?;
423 self.current_style = Style::default();
424 }
425 if new_style.bold != self.current_style.bold && new_bold {
426 queue!(self.output, SetAttribute(Attribute::Bold))?;
427 }
428 if new_style.dim != self.current_style.dim && new_dim {
429 queue!(self.output, SetAttribute(Attribute::Dim))?;
430 }
431
432 if new_style.italic != self.current_style.italic {
433 if new_style.italic.unwrap_or_default() {
434 queue!(self.output, SetAttribute(Attribute::Italic))?;
435 } else {
436 queue!(self.output, SetAttribute(Attribute::NoItalic))?;
437 }
438 }
439 if new_style.underline != self.current_style.underline {
440 if new_style.underline.unwrap_or_default() {
441 queue!(self.output, SetAttribute(Attribute::Underlined))?;
442 } else {
443 queue!(self.output, SetAttribute(Attribute::NoUnderline))?;
444 }
445 }
446 if new_style.reverse != self.current_style.reverse {
447 if new_style.reverse.unwrap_or_default() {
448 queue!(self.output, SetAttribute(Attribute::Reverse))?;
449 } else {
450 queue!(self.output, SetAttribute(Attribute::NoReverse))?;
451 }
452 }
453 if new_style.fg != self.current_style.fg {
454 queue!(
455 self.output,
456 SetForegroundColor(new_style.fg.unwrap_or(Color::Reset))
457 )?;
458 }
459 if new_style.bg != self.current_style.bg {
460 queue!(
461 self.output,
462 SetBackgroundColor(new_style.bg.unwrap_or(Color::Reset))
463 )?;
464 }
465 self.current_style = new_style;
466 }
467 if let Some(d) = new_debug {
468 if !d.is_empty() {
469 write!(self.output, "<<{d}::")?;
470 }
471 self.current_debug = Some(d);
472 }
473 Ok(())
474 }
475}
476
477fn rules_from_config(config: &StackedConfig) -> Result<Rules, ConfigGetError> {
478 config
479 .table_keys("colors")
480 .map(|key| {
481 let labels = key
482 .split_whitespace()
483 .map(ToString::to_string)
484 .collect_vec();
485 let style = config.get_value_with(["colors", key], |value| {
486 if value.is_str() {
487 Ok(Style {
488 fg: Some(deserialize_color(value.into_deserializer())?),
489 bg: None,
490 bold: None,
491 dim: None,
492 italic: None,
493 underline: None,
494 reverse: None,
495 })
496 } else if value.is_inline_table() {
497 Style::deserialize(value.into_deserializer())
498 } else {
499 Err(toml_edit::de::Error::custom(format!(
500 "invalid type: {}, expected a color name or a table of styles",
501 value.type_name()
502 )))
503 }
504 })?;
505 Ok((labels, style))
506 })
507 .collect()
508}
509
510fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
511where
512 D: serde::Deserializer<'de>,
513{
514 let color_str = String::deserialize(deserializer)?;
515 color_for_string(&color_str).map_err(D::Error::custom)
516}
517
518fn deserialize_color_opt<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
519where
520 D: serde::Deserializer<'de>,
521{
522 deserialize_color(deserializer).map(Some)
523}
524
525fn color_for_string(color_str: &str) -> Result<Color, String> {
526 match color_str {
527 "default" => Ok(Color::Reset),
528 "black" => Ok(Color::Black),
529 "red" => Ok(Color::DarkRed),
530 "green" => Ok(Color::DarkGreen),
531 "yellow" => Ok(Color::DarkYellow),
532 "blue" => Ok(Color::DarkBlue),
533 "magenta" => Ok(Color::DarkMagenta),
534 "cyan" => Ok(Color::DarkCyan),
535 "white" => Ok(Color::Grey),
536 "bright black" => Ok(Color::DarkGrey),
537 "bright red" => Ok(Color::Red),
538 "bright green" => Ok(Color::Green),
539 "bright yellow" => Ok(Color::Yellow),
540 "bright blue" => Ok(Color::Blue),
541 "bright magenta" => Ok(Color::Magenta),
542 "bright cyan" => Ok(Color::Cyan),
543 "bright white" => Ok(Color::White),
544 _ => color_for_ansi256_index(color_str)
545 .or_else(|| color_for_hex(color_str))
546 .ok_or_else(|| format!("Invalid color: {color_str}")),
547 }
548}
549
550fn color_for_ansi256_index(color: &str) -> Option<Color> {
551 color
552 .strip_prefix("ansi-color-")
553 .filter(|s| *s == "0" || !s.starts_with('0'))
554 .and_then(|n| n.parse::<u8>().ok())
555 .map(Color::AnsiValue)
556}
557
558fn color_for_hex(color: &str) -> Option<Color> {
559 if color.len() == 7
560 && color.starts_with('#')
561 && color[1..].chars().all(|c| c.is_ascii_hexdigit())
562 {
563 let r = u8::from_str_radix(&color[1..3], 16);
564 let g = u8::from_str_radix(&color[3..5], 16);
565 let b = u8::from_str_radix(&color[5..7], 16);
566 match (r, g, b) {
567 (Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb { r, g, b }),
568 _ => None,
569 }
570 } else {
571 None
572 }
573}
574
575impl<W: Write> Write for ColorFormatter<W> {
576 fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
577 for line in data.split_inclusive(|b| *b == b'\n') {
602 if line.ends_with(b"\n") {
603 self.write_new_style()?;
604 write_sanitized(&mut self.output, &line[..line.len() - 1])?;
605 let labels = mem::take(&mut self.labels);
606 self.write_new_style()?;
607 self.output.write_all(b"\n")?;
608 self.labels = labels;
609 } else {
610 self.write_new_style()?;
611 write_sanitized(&mut self.output, line)?;
612 }
613 }
614
615 Ok(data.len())
616 }
617
618 fn flush(&mut self) -> Result<(), Error> {
619 self.write_new_style()?;
620 self.output.flush()
621 }
622}
623
624impl<W: Write> Formatter for ColorFormatter<W> {
625 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
626 self.write_new_style()?;
627 Ok(Box::new(self.output.by_ref()))
628 }
629
630 fn push_label(&mut self, label: &str) {
631 self.labels.push(label.to_owned());
632 }
633
634 fn pop_label(&mut self) {
635 self.labels.pop();
636 }
637
638 fn maybe_color(&self) -> bool {
639 true
640 }
641}
642
643impl<W: Write> Drop for ColorFormatter<W> {
644 fn drop(&mut self) {
645 self.labels.clear();
648 self.write_new_style().ok();
649 }
650}
651
652#[derive(Clone, Debug)]
660pub struct FormatRecorder {
661 data: Vec<u8>,
662 ops: Vec<(usize, FormatOp)>,
663 maybe_color: bool,
664}
665
666#[derive(Clone, Debug, Eq, PartialEq)]
667enum FormatOp {
668 PushLabel(String),
669 PopLabel,
670 RawEscapeSequence(Vec<u8>),
671}
672
673impl FormatRecorder {
674 pub fn new(maybe_color: bool) -> Self {
675 Self {
676 data: vec![],
677 ops: vec![],
678 maybe_color,
679 }
680 }
681
682 pub fn with_data(data: impl Into<Vec<u8>>) -> Self {
684 Self {
685 data: data.into(),
686 ops: vec![],
687 maybe_color: false,
688 }
689 }
690
691 pub fn data(&self) -> &[u8] {
692 &self.data
693 }
694
695 fn push_op(&mut self, op: FormatOp) {
696 self.ops.push((self.data.len(), op));
697 }
698
699 pub fn replay(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
700 self.replay_with(formatter, |formatter, range| {
701 formatter.write_all(&self.data[range])
702 })
703 }
704
705 pub fn replay_with(
706 &self,
707 formatter: &mut dyn Formatter,
708 mut write_data: impl FnMut(&mut dyn Formatter, Range<usize>) -> io::Result<()>,
709 ) -> io::Result<()> {
710 let mut last_pos = 0;
711 let mut flush_data = |formatter: &mut dyn Formatter, pos| -> io::Result<()> {
712 if last_pos != pos {
713 write_data(formatter, last_pos..pos)?;
714 last_pos = pos;
715 }
716 Ok(())
717 };
718 for (pos, op) in &self.ops {
719 flush_data(formatter, *pos)?;
720 match op {
721 FormatOp::PushLabel(label) => formatter.push_label(label),
722 FormatOp::PopLabel => formatter.pop_label(),
723 FormatOp::RawEscapeSequence(raw_escape_sequence) => {
724 formatter.raw()?.write_all(raw_escape_sequence)?;
725 }
726 }
727 }
728 flush_data(formatter, self.data.len())
729 }
730}
731
732impl Write for FormatRecorder {
733 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
734 self.data.extend_from_slice(data);
735 Ok(data.len())
736 }
737
738 fn flush(&mut self) -> io::Result<()> {
739 Ok(())
740 }
741}
742
743struct RawEscapeSequenceRecorder<'a>(&'a mut FormatRecorder);
744
745impl Write for RawEscapeSequenceRecorder<'_> {
746 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
747 self.0.push_op(FormatOp::RawEscapeSequence(data.to_vec()));
748 Ok(data.len())
749 }
750
751 fn flush(&mut self) -> io::Result<()> {
752 self.0.flush()
753 }
754}
755
756impl Formatter for FormatRecorder {
757 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
758 Ok(Box::new(RawEscapeSequenceRecorder(self)))
759 }
760
761 fn push_label(&mut self, label: &str) {
762 self.push_op(FormatOp::PushLabel(label.to_owned()));
763 }
764
765 fn pop_label(&mut self) {
766 self.push_op(FormatOp::PopLabel);
767 }
768
769 fn maybe_color(&self) -> bool {
770 self.maybe_color
771 }
772}
773
774fn write_sanitized(output: &mut impl Write, buf: &[u8]) -> Result<(), Error> {
775 if buf.contains(&b'\x1b') {
776 let mut sanitized = Vec::with_capacity(buf.len());
777 for b in buf {
778 if *b == b'\x1b' {
779 sanitized.extend_from_slice("␛".as_bytes());
780 } else {
781 sanitized.push(*b);
782 }
783 }
784 output.write_all(&sanitized)
785 } else {
786 output.write_all(buf)
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use std::error::Error as _;
793
794 use bstr::BString;
795 use indexmap::IndexMap;
796 use indoc::indoc;
797 use jj_lib::config::ConfigLayer;
798 use jj_lib::config::ConfigSource;
799
800 use super::*;
801
802 fn config_from_string(text: &str) -> StackedConfig {
803 let mut config = StackedConfig::empty();
804 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
805 config
806 }
807
808 fn to_snapshot_string(output: impl Into<Vec<u8>>) -> BString {
812 let mut output = output.into();
813 output.extend_from_slice(b"[EOF]\n");
814 BString::new(output)
815 }
816
817 #[test]
818 fn test_plaintext_formatter() {
819 let mut output: Vec<u8> = vec![];
821 let mut formatter = PlainTextFormatter::new(&mut output);
822 formatter.push_label("warning");
823 write!(formatter, "hello").unwrap();
824 formatter.pop_label();
825 insta::assert_snapshot!(to_snapshot_string(output), @"hello[EOF]");
826 }
827
828 #[test]
829 fn test_plaintext_formatter_ansi_codes_in_text() {
830 let mut output: Vec<u8> = vec![];
832 let mut formatter = PlainTextFormatter::new(&mut output);
833 write!(formatter, "\x1b[1mactually bold\x1b[0m").unwrap();
834 insta::assert_snapshot!(to_snapshot_string(output), @"[1mactually bold[0m[EOF]");
835 }
836
837 #[test]
838 fn test_sanitizing_formatter_ansi_codes_in_text() {
839 let mut output: Vec<u8> = vec![];
841 let mut formatter = SanitizingFormatter::new(&mut output);
842 write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
843 insta::assert_snapshot!(to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
844 }
845
846 #[test]
847 fn test_color_formatter_color_codes() {
848 let config = config_from_string(indoc! {"
851 [colors]
852 black = 'black'
853 red = 'red'
854 green = 'green'
855 yellow = 'yellow'
856 blue = 'blue'
857 magenta = 'magenta'
858 cyan = 'cyan'
859 white = 'white'
860 bright-black = 'bright black'
861 bright-red = 'bright red'
862 bright-green = 'bright green'
863 bright-yellow = 'bright yellow'
864 bright-blue = 'bright blue'
865 bright-magenta = 'bright magenta'
866 bright-cyan = 'bright cyan'
867 bright-white = 'bright white'
868 "});
869 let colors: IndexMap<String, String> = config.get("colors").unwrap();
870 let mut output: Vec<u8> = vec![];
871 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
872 for (label, color) in &colors {
873 formatter.push_label(label);
874 write!(formatter, " {color} ").unwrap();
875 formatter.pop_label();
876 writeln!(formatter).unwrap();
877 }
878 drop(formatter);
879 insta::assert_snapshot!(to_snapshot_string(output), @"
880 [38;5;0m black [39m
881 [38;5;1m red [39m
882 [38;5;2m green [39m
883 [38;5;3m yellow [39m
884 [38;5;4m blue [39m
885 [38;5;5m magenta [39m
886 [38;5;6m cyan [39m
887 [38;5;7m white [39m
888 [38;5;8m bright black [39m
889 [38;5;9m bright red [39m
890 [38;5;10m bright green [39m
891 [38;5;11m bright yellow [39m
892 [38;5;12m bright blue [39m
893 [38;5;13m bright magenta [39m
894 [38;5;14m bright cyan [39m
895 [38;5;15m bright white [39m
896 [EOF]
897 ");
898 }
899
900 #[test]
901 fn test_color_for_ansi256_index() {
902 assert_eq!(
903 color_for_ansi256_index("ansi-color-0"),
904 Some(Color::AnsiValue(0))
905 );
906 assert_eq!(
907 color_for_ansi256_index("ansi-color-10"),
908 Some(Color::AnsiValue(10))
909 );
910 assert_eq!(
911 color_for_ansi256_index("ansi-color-255"),
912 Some(Color::AnsiValue(255))
913 );
914 assert_eq!(color_for_ansi256_index("ansi-color-256"), None);
915
916 assert_eq!(color_for_ansi256_index("ansi-color-00"), None);
917 assert_eq!(color_for_ansi256_index("ansi-color-010"), None);
918 assert_eq!(color_for_ansi256_index("ansi-color-0255"), None);
919 }
920
921 #[test]
922 fn test_color_for_hex() {
923 assert_eq!(
924 color_for_hex("#000000"),
925 Some(Color::Rgb { r: 0, g: 0, b: 0 })
926 );
927 assert_eq!(
928 color_for_hex("#fab123"),
929 Some(Color::Rgb {
930 r: 0xfa,
931 g: 0xb1,
932 b: 0x23
933 })
934 );
935 assert_eq!(
936 color_for_hex("#F00D13"),
937 Some(Color::Rgb {
938 r: 0xf0,
939 g: 0x0d,
940 b: 0x13
941 })
942 );
943 assert_eq!(
944 color_for_hex("#ffffff"),
945 Some(Color::Rgb {
946 r: 255,
947 g: 255,
948 b: 255
949 })
950 );
951
952 assert_eq!(color_for_hex("000000"), None);
953 assert_eq!(color_for_hex("0000000"), None);
954 assert_eq!(color_for_hex("#00000g"), None);
955 assert_eq!(color_for_hex("#á00000"), None);
956 }
957
958 #[test]
959 fn test_color_formatter_ansi256() {
960 let config = config_from_string(
961 r#"
962 [colors]
963 purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
964 gray = "ansi-color-244"
965 "#,
966 );
967 let mut output: Vec<u8> = vec![];
968 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
969 formatter.push_label("purple-bg");
970 write!(formatter, " purple background ").unwrap();
971 formatter.pop_label();
972 writeln!(formatter).unwrap();
973 formatter.push_label("gray");
974 write!(formatter, " gray ").unwrap();
975 formatter.pop_label();
976 writeln!(formatter).unwrap();
977 drop(formatter);
978 insta::assert_snapshot!(to_snapshot_string(output), @"
979 [38;5;15m[48;5;93m purple background [39m[49m
980 [38;5;244m gray [39m
981 [EOF]
982 ");
983 }
984
985 #[test]
986 fn test_color_formatter_hex_colors() {
987 let config = config_from_string(indoc! {"
989 [colors]
990 black = '#000000'
991 white = '#ffffff'
992 pastel-blue = '#AFE0D9'
993 "});
994 let colors: IndexMap<String, String> = config.get("colors").unwrap();
995 let mut output: Vec<u8> = vec![];
996 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
997 for label in colors.keys() {
998 formatter.push_label(&label.replace(' ', "-"));
999 write!(formatter, " {label} ").unwrap();
1000 formatter.pop_label();
1001 writeln!(formatter).unwrap();
1002 }
1003 drop(formatter);
1004 insta::assert_snapshot!(to_snapshot_string(output), @"
1005 [38;2;0;0;0m black [39m
1006 [38;2;255;255;255m white [39m
1007 [38;2;175;224;217m pastel-blue [39m
1008 [EOF]
1009 ");
1010 }
1011
1012 #[test]
1013 fn test_color_formatter_single_label() {
1014 let config = config_from_string(
1017 r#"
1018 colors.inside = "green"
1019 "#,
1020 );
1021 let mut output: Vec<u8> = vec![];
1022 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1023 write!(formatter, " before ").unwrap();
1024 formatter.push_label("inside");
1025 write!(formatter, " inside ").unwrap();
1026 formatter.pop_label();
1027 write!(formatter, " after ").unwrap();
1028 drop(formatter);
1029 insta::assert_snapshot!(
1030 to_snapshot_string(output), @" before [38;5;2m inside [39m after [EOF]");
1031 }
1032
1033 #[test]
1034 fn test_color_formatter_attributes() {
1035 let config = config_from_string(
1038 r#"
1039 colors.red_fg = { fg = "red" }
1040 colors.blue_bg = { bg = "blue" }
1041 colors.bold_font = { bold = true }
1042 colors.dim_font = { dim = true }
1043 colors.italic_text = { italic = true }
1044 colors.underlined_text = { underline = true }
1045 colors.reversed_colors = { reverse = true }
1046 colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, reverse = true }
1047 "#,
1048 );
1049 let mut output: Vec<u8> = vec![];
1050 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1051 formatter.push_label("red_fg");
1052 write!(formatter, " fg only ").unwrap();
1053 formatter.pop_label();
1054 writeln!(formatter).unwrap();
1055 formatter.push_label("blue_bg");
1056 write!(formatter, " bg only ").unwrap();
1057 formatter.pop_label();
1058 writeln!(formatter).unwrap();
1059 formatter.push_label("bold_font");
1060 write!(formatter, " bold only ").unwrap();
1061 formatter.pop_label();
1062 writeln!(formatter).unwrap();
1063 formatter.push_label("dim_font");
1064 write!(formatter, " dim only ").unwrap();
1065 formatter.pop_label();
1066 writeln!(formatter).unwrap();
1067 formatter.push_label("italic_text");
1068 write!(formatter, " italic only ").unwrap();
1069 formatter.pop_label();
1070 writeln!(formatter).unwrap();
1071 formatter.push_label("underlined_text");
1072 write!(formatter, " underlined only ").unwrap();
1073 formatter.pop_label();
1074 writeln!(formatter).unwrap();
1075 formatter.push_label("reversed_colors");
1076 write!(formatter, " reverse only ").unwrap();
1077 formatter.pop_label();
1078 writeln!(formatter).unwrap();
1079 formatter.push_label("multiple");
1080 write!(formatter, " single rule ").unwrap();
1081 formatter.pop_label();
1082 writeln!(formatter).unwrap();
1083 formatter.push_label("red_fg");
1084 formatter.push_label("blue_bg");
1085 write!(formatter, " two rules ").unwrap();
1086 formatter.pop_label();
1087 formatter.pop_label();
1088 writeln!(formatter).unwrap();
1089 drop(formatter);
1090 insta::assert_snapshot!(to_snapshot_string(output), @"
1091 [38;5;1m fg only [39m
1092 [48;5;4m bg only [49m
1093 [1m bold only [0m
1094 [2m dim only [0m
1095 [3m italic only [23m
1096 [4m underlined only [24m
1097 [7m reverse only [27m
1098 [1m[3m[4m[7m[38;5;2m[48;5;3m single rule [0m
1099 [38;5;1m[48;5;4m two rules [39m[49m
1100 [EOF]
1101 ");
1102 }
1103
1104 #[test]
1105 fn test_color_formatter_bold_reset() {
1106 let config = config_from_string(indoc! {"
1108 [colors]
1109 not_bold = { fg = 'red', bg = 'blue', italic = true, underline = true }
1110 bold_font = { bold = true }
1111 stop_bold = { bold = false }
1112 "});
1113 let mut output: Vec<u8> = vec![];
1114 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1115 formatter.push_label("not_bold");
1116 write!(formatter, " not bold ").unwrap();
1117 formatter.push_label("bold_font");
1118 write!(formatter, " bold ").unwrap();
1119 formatter.push_label("stop_bold");
1120 write!(formatter, " stop bold ").unwrap();
1121 formatter.pop_label();
1122 write!(formatter, " bold again ").unwrap();
1123 formatter.pop_label();
1124 write!(formatter, " not bold again ").unwrap();
1125 formatter.pop_label();
1126 drop(formatter);
1127 insta::assert_snapshot!(
1128 to_snapshot_string(output),
1129 @"[3m[4m[38;5;1m[48;5;4m not bold [1m bold [0m[3m[4m[38;5;1m[48;5;4m stop bold [1m bold again [0m[3m[4m[38;5;1m[48;5;4m not bold again [23m[24m[39m[49m[EOF]");
1130 }
1131
1132 #[test]
1133 fn test_color_formatter_dim_reset() {
1134 let config = config_from_string(indoc! {"
1136 [colors]
1137 not_dim = { fg = 'red', bg = 'blue', italic = true, underline = true }
1138 dim_font = { dim = true }
1139 stop_dim = { dim = false }
1140 "});
1141 let mut output: Vec<u8> = vec![];
1142 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1143 formatter.push_label("not_dim");
1144 write!(formatter, " not dim ").unwrap();
1145 formatter.push_label("dim_font");
1146 write!(formatter, " dim ").unwrap();
1147 formatter.push_label("stop_dim");
1148 write!(formatter, " stop dim ").unwrap();
1149 formatter.pop_label();
1150 write!(formatter, " dim again ").unwrap();
1151 formatter.pop_label();
1152 write!(formatter, " not dim again ").unwrap();
1153 formatter.pop_label();
1154 drop(formatter);
1155 insta::assert_snapshot!(
1156 to_snapshot_string(output),
1157 @"[3m[4m[38;5;1m[48;5;4m not dim [2m dim [0m[3m[4m[38;5;1m[48;5;4m stop dim [2m dim again [0m[3m[4m[38;5;1m[48;5;4m not dim again [23m[24m[39m[49m[EOF]");
1158 }
1159
1160 #[test]
1161 fn test_color_formatter_bold_to_dim() {
1162 let config = config_from_string(indoc! {"
1164 [colors]
1165 bold_font = { bold = true }
1166 dim_font = { dim = true }
1167 "});
1168 let mut output: Vec<u8> = vec![];
1169 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1170 formatter.push_label("bold_font");
1171 write!(formatter, " bold ").unwrap();
1172 formatter.push_label("dim_font");
1173 write!(formatter, " bold&dim ").unwrap();
1174 formatter.pop_label();
1175 write!(formatter, " bold again ").unwrap();
1176 formatter.pop_label();
1177 drop(formatter);
1178 insta::assert_snapshot!(
1179 to_snapshot_string(output),
1180 @"[1m bold [2m bold&dim [0m[1m bold again [0m[EOF]");
1181 }
1182
1183 #[test]
1184 fn test_formatter_reset_on_flush() {
1185 let config = config_from_string("colors.red = 'red'");
1186 let mut output: Vec<u8> = vec![];
1187 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1188 formatter.push_label("red");
1189 write!(formatter, "foo").unwrap();
1190 formatter.pop_label();
1191
1192 insta::assert_snapshot!(
1194 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[EOF]");
1195
1196 formatter.flush().unwrap();
1198 insta::assert_snapshot!(
1199 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[39m[EOF]");
1200
1201 formatter.push_label("red");
1203 write!(formatter, "bar").unwrap();
1204 formatter.pop_label();
1205
1206 drop(formatter);
1208 insta::assert_snapshot!(
1209 to_snapshot_string(output), @"[38;5;1mfoo[39m[38;5;1mbar[39m[EOF]");
1210
1211 let mut output: Vec<u8> = vec![];
1213 let mut formatter = PlainTextFormatter::new(&mut output);
1214 formatter.push_label("red");
1215 write!(formatter, "foo").unwrap();
1216 formatter.pop_label();
1217 formatter.flush().unwrap();
1218 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1219
1220 let mut output: Vec<u8> = vec![];
1221 let mut formatter = SanitizingFormatter::new(&mut output);
1222 formatter.push_label("red");
1223 write!(formatter, "foo").unwrap();
1224 formatter.pop_label();
1225 formatter.flush().unwrap();
1226 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1227 }
1228
1229 #[test]
1230 fn test_color_formatter_no_space() {
1231 let config = config_from_string(
1233 r#"
1234 colors.red = "red"
1235 colors.green = "green"
1236 "#,
1237 );
1238 let mut output: Vec<u8> = vec![];
1239 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1240 write!(formatter, "before").unwrap();
1241 formatter.push_label("red");
1242 write!(formatter, "first").unwrap();
1243 formatter.pop_label();
1244 formatter.push_label("green");
1245 write!(formatter, "second").unwrap();
1246 formatter.pop_label();
1247 write!(formatter, "after").unwrap();
1248 drop(formatter);
1249 insta::assert_snapshot!(
1250 to_snapshot_string(output), @"before[38;5;1mfirst[38;5;2msecond[39mafter[EOF]");
1251 }
1252
1253 #[test]
1254 fn test_color_formatter_ansi_codes_in_text() {
1255 let config = config_from_string(
1257 r#"
1258 colors.red = "red"
1259 "#,
1260 );
1261 let mut output: Vec<u8> = vec![];
1262 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1263 formatter.push_label("red");
1264 write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
1265 formatter.pop_label();
1266 drop(formatter);
1267 insta::assert_snapshot!(
1268 to_snapshot_string(output), @"[38;5;1m␛[1mnot actually bold␛[0m[39m[EOF]");
1269 }
1270
1271 #[test]
1272 fn test_color_formatter_nested() {
1273 let config = config_from_string(
1277 r#"
1278 colors.outer = "blue"
1279 colors.inner = "red"
1280 colors."outer inner" = "green"
1281 "#,
1282 );
1283 let mut output: Vec<u8> = vec![];
1284 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1285 write!(formatter, " before outer ").unwrap();
1286 formatter.push_label("outer");
1287 write!(formatter, " before inner ").unwrap();
1288 formatter.push_label("inner");
1289 write!(formatter, " inside inner ").unwrap();
1290 formatter.pop_label();
1291 write!(formatter, " after inner ").unwrap();
1292 formatter.pop_label();
1293 write!(formatter, " after outer ").unwrap();
1294 drop(formatter);
1295 insta::assert_snapshot!(
1296 to_snapshot_string(output),
1297 @" before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer [EOF]");
1298 }
1299
1300 #[test]
1301 fn test_color_formatter_partial_match() {
1302 let config = config_from_string(
1304 r#"
1305 colors."outer inner" = "green"
1306 "#,
1307 );
1308 let mut output: Vec<u8> = vec![];
1309 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1310 formatter.push_label("outer");
1311 write!(formatter, " not colored ").unwrap();
1312 formatter.push_label("inner");
1313 write!(formatter, " colored ").unwrap();
1314 formatter.pop_label();
1315 write!(formatter, " not colored ").unwrap();
1316 formatter.pop_label();
1317 drop(formatter);
1318 insta::assert_snapshot!(
1319 to_snapshot_string(output),
1320 @" not colored [38;5;2m colored [39m not colored [EOF]");
1321 }
1322
1323 #[test]
1324 fn test_color_formatter_unrecognized_color() {
1325 let config = config_from_string(
1327 r#"
1328 colors."outer" = "red"
1329 colors."outer inner" = "bloo"
1330 "#,
1331 );
1332 let mut output: Vec<u8> = vec![];
1333 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1334 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1335 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1336 }
1337
1338 #[test]
1339 fn test_color_formatter_unrecognized_ansi256_color() {
1340 let config = config_from_string(
1342 r##"
1343 colors."outer" = "red"
1344 colors."outer inner" = "ansi-color-256"
1345 "##,
1346 );
1347 let mut output: Vec<u8> = vec![];
1348 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1349 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1350 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1351 }
1352
1353 #[test]
1354 fn test_color_formatter_unrecognized_hex_color() {
1355 let config = config_from_string(
1357 r##"
1358 colors."outer" = "red"
1359 colors."outer inner" = "#ffgggg"
1360 "##,
1361 );
1362 let mut output: Vec<u8> = vec![];
1363 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1364 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1365 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1366 }
1367
1368 #[test]
1369 fn test_color_formatter_invalid_type_of_color() {
1370 let config = config_from_string("colors.foo = []");
1371 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1372 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1373 insta::assert_snapshot!(
1374 err.source().unwrap(),
1375 @"invalid type: array, expected a color name or a table of styles");
1376 }
1377
1378 #[test]
1379 fn test_color_formatter_invalid_type_of_style() {
1380 let config = config_from_string("colors.foo = { bold = 1 }");
1381 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1382 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1383 insta::assert_snapshot!(err.source().unwrap(), @"
1384 invalid type: integer `1`, expected a boolean
1385 in `bold`
1386 ");
1387 }
1388
1389 #[test]
1390 fn test_color_formatter_normal_color() {
1391 let config = config_from_string(
1394 r#"
1395 colors."outer" = {bg="yellow", fg="blue"}
1396 colors."outer default_fg" = "default"
1397 colors."outer default_bg" = {bg = "default"}
1398 "#,
1399 );
1400 let mut output: Vec<u8> = vec![];
1401 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1402 formatter.push_label("outer");
1403 write!(formatter, "Blue on yellow, ").unwrap();
1404 formatter.push_label("default_fg");
1405 write!(formatter, " default fg, ").unwrap();
1406 formatter.pop_label();
1407 write!(formatter, " and back.\nBlue on yellow, ").unwrap();
1408 formatter.push_label("default_bg");
1409 write!(formatter, " default bg, ").unwrap();
1410 formatter.pop_label();
1411 write!(formatter, " and back.").unwrap();
1412 drop(formatter);
1413 insta::assert_snapshot!(to_snapshot_string(output), @"
1414 [38;5;4m[48;5;3mBlue on yellow, [39m default fg, [38;5;4m and back.[39m[49m
1415 [38;5;4m[48;5;3mBlue on yellow, [49m default bg, [48;5;3m and back.[39m[49m[EOF]
1416 ");
1417 }
1418
1419 #[test]
1420 fn test_color_formatter_sibling() {
1421 let config = config_from_string(
1423 r#"
1424 colors."outer1 inner1" = "red"
1425 colors.inner2 = "green"
1426 "#,
1427 );
1428 let mut output: Vec<u8> = vec![];
1429 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1430 formatter.push_label("outer1");
1431 formatter.push_label("inner2");
1432 write!(formatter, " hello ").unwrap();
1433 formatter.pop_label();
1434 formatter.pop_label();
1435 drop(formatter);
1436 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m hello [39m[EOF]");
1437 }
1438
1439 #[test]
1440 fn test_color_formatter_reverse_order() {
1441 let config = config_from_string(
1443 r#"
1444 colors."inner outer" = "green"
1445 "#,
1446 );
1447 let mut output: Vec<u8> = vec![];
1448 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1449 formatter.push_label("outer");
1450 formatter.push_label("inner");
1451 write!(formatter, " hello ").unwrap();
1452 formatter.pop_label();
1453 formatter.pop_label();
1454 drop(formatter);
1455 insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1456 }
1457
1458 #[test]
1459 fn test_color_formatter_innermost_wins() {
1460 let config = config_from_string(
1462 r#"
1463 colors."a" = "red"
1464 colors."b" = "green"
1465 colors."a c" = "blue"
1466 colors."b c" = "yellow"
1467 "#,
1468 );
1469 let mut output: Vec<u8> = vec![];
1470 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1471 formatter.push_label("a");
1472 write!(formatter, " a1 ").unwrap();
1473 formatter.push_label("b");
1474 write!(formatter, " b1 ").unwrap();
1475 formatter.push_label("c");
1476 write!(formatter, " c ").unwrap();
1477 formatter.pop_label();
1478 write!(formatter, " b2 ").unwrap();
1479 formatter.pop_label();
1480 write!(formatter, " a2 ").unwrap();
1481 formatter.pop_label();
1482 drop(formatter);
1483 insta::assert_snapshot!(
1484 to_snapshot_string(output),
1485 @"[38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m[EOF]");
1486 }
1487
1488 #[test]
1489 fn test_color_formatter_dropped() {
1490 let config = config_from_string(
1493 r#"
1494 colors.outer = "green"
1495 "#,
1496 );
1497 let mut output: Vec<u8> = vec![];
1498 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1499 formatter.push_label("outer");
1500 formatter.push_label("inner");
1501 write!(formatter, " inside ").unwrap();
1502 drop(formatter);
1503 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m inside [39m[EOF]");
1504 }
1505
1506 #[test]
1507 fn test_color_formatter_debug() {
1508 let config = config_from_string(
1511 r#"
1512 colors.outer = "green"
1513 "#,
1514 );
1515 let mut output: Vec<u8> = vec![];
1516 let mut formatter = ColorFormatter::for_config(&mut output, &config, true).unwrap();
1517 formatter.push_label("outer");
1518 formatter.push_label("inner");
1519 write!(formatter, " inside ").unwrap();
1520 formatter.pop_label();
1521 formatter.pop_label();
1522 formatter.push_label("outer");
1524 formatter.push_label("inner");
1525 write!(formatter, " inside two ").unwrap();
1526 formatter.pop_label();
1527 formatter.pop_label();
1528 drop(formatter);
1529 insta::assert_snapshot!(
1530 to_snapshot_string(output),
1531 @"[38;5;2m<<outer inner:: inside inside two >>[39m[EOF]",
1532 );
1533 }
1534
1535 #[test]
1536 fn test_labeled_scope() {
1537 let config = config_from_string(indoc! {"
1538 [colors]
1539 outer = 'blue'
1540 inner = 'red'
1541 'outer inner' = 'green'
1542 "});
1543 let mut output: Vec<u8> = vec![];
1544 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1545 writeln!(formatter.labeled("outer"), "outer").unwrap();
1546 writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner").unwrap();
1547 writeln!(formatter.labeled("inner"), "inner").unwrap();
1548 drop(formatter);
1549 insta::assert_snapshot!(to_snapshot_string(output), @"
1550 [38;5;4mouter[39m
1551 [38;5;2mouter-inner[39m
1552 [38;5;1minner[39m
1553 [EOF]
1554 ");
1555 }
1556
1557 #[test]
1558 fn test_heading_labeled_writer() {
1559 let config = config_from_string(
1560 r#"
1561 colors.inner = "green"
1562 colors."inner heading" = "red"
1563 "#,
1564 );
1565 let mut output: Vec<u8> = vec![];
1566 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1567 formatter.labeled("inner").with_heading("Should be noop: ");
1568 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1569 write!(writer, "Message").unwrap();
1570 writeln!(writer, " continues").unwrap();
1571 drop(writer);
1572 drop(formatter);
1573 insta::assert_snapshot!(to_snapshot_string(output), @"
1574 [38;5;1mHeading: [38;5;2mMessage continues[39m
1575 [EOF]
1576 ");
1577 }
1578
1579 #[test]
1580 fn test_heading_labeled_writer_empty_string() {
1581 let mut output: Vec<u8> = vec![];
1582 let mut formatter = PlainTextFormatter::new(&mut output);
1583 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1584 write!(writer, "").unwrap();
1587 write!(writer, "").unwrap();
1588 drop(writer);
1589 insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1590 }
1591
1592 #[test]
1593 fn test_format_recorder() {
1594 let mut recorder = FormatRecorder::new(false);
1595 write!(recorder, " outer1 ").unwrap();
1596 recorder.push_label("inner");
1597 write!(recorder, " inner1 ").unwrap();
1598 write!(recorder, " inner2 ").unwrap();
1599 recorder.pop_label();
1600 write!(recorder, " outer2 ").unwrap();
1601
1602 insta::assert_snapshot!(
1603 to_snapshot_string(recorder.data()),
1604 @" outer1 inner1 inner2 outer2 [EOF]");
1605
1606 let config = config_from_string(r#" colors.inner = "red" "#);
1608 let mut output: Vec<u8> = vec![];
1609 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1610 recorder.replay(&mut formatter).unwrap();
1611 drop(formatter);
1612 insta::assert_snapshot!(
1613 to_snapshot_string(output),
1614 @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1615
1616 let mut output: Vec<u8> = vec![];
1618 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1619 recorder
1620 .replay_with(&mut formatter, |formatter, range| {
1621 let data = &recorder.data()[range];
1622 write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1623 })
1624 .unwrap();
1625 drop(formatter);
1626 insta::assert_snapshot!(
1627 to_snapshot_string(output),
1628 @"<< outer1 >>[38;5;1m<< inner1 inner2 >>[39m<< outer2 >>[EOF]");
1629 }
1630
1631 #[test]
1632 fn test_raw_format_recorder() {
1633 let mut recorder = FormatRecorder::new(false);
1635 write!(recorder.raw().unwrap(), " outer1 ").unwrap();
1636 recorder.push_label("inner");
1637 write!(recorder.raw().unwrap(), " inner1 ").unwrap();
1638 write!(recorder.raw().unwrap(), " inner2 ").unwrap();
1639 recorder.pop_label();
1640 write!(recorder.raw().unwrap(), " outer2 ").unwrap();
1641
1642 let config = config_from_string(r#" colors.inner = "red" "#);
1644 let mut output: Vec<u8> = vec![];
1645 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1646 recorder.replay(&mut formatter).unwrap();
1647 drop(formatter);
1648 insta::assert_snapshot!(
1649 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1650
1651 let mut output: Vec<u8> = vec![];
1652 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1653 recorder
1654 .replay_with(&mut formatter, |_formatter, range| {
1655 panic!(
1656 "Called with {:?} when all output should be raw",
1657 str::from_utf8(&recorder.data()[range]).unwrap()
1658 );
1659 })
1660 .unwrap();
1661 drop(formatter);
1662 insta::assert_snapshot!(
1663 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1664 }
1665}