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), @r"
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_formatter_ansi256() {
923 let config = config_from_string(
924 r#"
925 [colors]
926 purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
927 gray = "ansi-color-244"
928 "#,
929 );
930 let mut output: Vec<u8> = vec![];
931 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
932 formatter.push_label("purple-bg");
933 write!(formatter, " purple background ").unwrap();
934 formatter.pop_label();
935 writeln!(formatter).unwrap();
936 formatter.push_label("gray");
937 write!(formatter, " gray ").unwrap();
938 formatter.pop_label();
939 writeln!(formatter).unwrap();
940 drop(formatter);
941 insta::assert_snapshot!(to_snapshot_string(output), @r"
942 [38;5;15m[48;5;93m purple background [39m[49m
943 [38;5;244m gray [39m
944 [EOF]
945 ");
946 }
947
948 #[test]
949 fn test_color_formatter_hex_colors() {
950 let config = config_from_string(indoc! {"
952 [colors]
953 black = '#000000'
954 white = '#ffffff'
955 pastel-blue = '#AFE0D9'
956 "});
957 let colors: IndexMap<String, String> = config.get("colors").unwrap();
958 let mut output: Vec<u8> = vec![];
959 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
960 for label in colors.keys() {
961 formatter.push_label(&label.replace(' ', "-"));
962 write!(formatter, " {label} ").unwrap();
963 formatter.pop_label();
964 writeln!(formatter).unwrap();
965 }
966 drop(formatter);
967 insta::assert_snapshot!(to_snapshot_string(output), @r"
968 [38;2;0;0;0m black [39m
969 [38;2;255;255;255m white [39m
970 [38;2;175;224;217m pastel-blue [39m
971 [EOF]
972 ");
973 }
974
975 #[test]
976 fn test_color_formatter_single_label() {
977 let config = config_from_string(
980 r#"
981 colors.inside = "green"
982 "#,
983 );
984 let mut output: Vec<u8> = vec![];
985 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
986 write!(formatter, " before ").unwrap();
987 formatter.push_label("inside");
988 write!(formatter, " inside ").unwrap();
989 formatter.pop_label();
990 write!(formatter, " after ").unwrap();
991 drop(formatter);
992 insta::assert_snapshot!(
993 to_snapshot_string(output), @" before [38;5;2m inside [39m after [EOF]");
994 }
995
996 #[test]
997 fn test_color_formatter_attributes() {
998 let config = config_from_string(
1001 r#"
1002 colors.red_fg = { fg = "red" }
1003 colors.blue_bg = { bg = "blue" }
1004 colors.bold_font = { bold = true }
1005 colors.dim_font = { dim = true }
1006 colors.italic_text = { italic = true }
1007 colors.underlined_text = { underline = true }
1008 colors.reversed_colors = { reverse = true }
1009 colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, reverse = true }
1010 "#,
1011 );
1012 let mut output: Vec<u8> = vec![];
1013 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1014 formatter.push_label("red_fg");
1015 write!(formatter, " fg only ").unwrap();
1016 formatter.pop_label();
1017 writeln!(formatter).unwrap();
1018 formatter.push_label("blue_bg");
1019 write!(formatter, " bg only ").unwrap();
1020 formatter.pop_label();
1021 writeln!(formatter).unwrap();
1022 formatter.push_label("bold_font");
1023 write!(formatter, " bold only ").unwrap();
1024 formatter.pop_label();
1025 writeln!(formatter).unwrap();
1026 formatter.push_label("dim_font");
1027 write!(formatter, " dim only ").unwrap();
1028 formatter.pop_label();
1029 writeln!(formatter).unwrap();
1030 formatter.push_label("italic_text");
1031 write!(formatter, " italic only ").unwrap();
1032 formatter.pop_label();
1033 writeln!(formatter).unwrap();
1034 formatter.push_label("underlined_text");
1035 write!(formatter, " underlined only ").unwrap();
1036 formatter.pop_label();
1037 writeln!(formatter).unwrap();
1038 formatter.push_label("reversed_colors");
1039 write!(formatter, " reverse only ").unwrap();
1040 formatter.pop_label();
1041 writeln!(formatter).unwrap();
1042 formatter.push_label("multiple");
1043 write!(formatter, " single rule ").unwrap();
1044 formatter.pop_label();
1045 writeln!(formatter).unwrap();
1046 formatter.push_label("red_fg");
1047 formatter.push_label("blue_bg");
1048 write!(formatter, " two rules ").unwrap();
1049 formatter.pop_label();
1050 formatter.pop_label();
1051 writeln!(formatter).unwrap();
1052 drop(formatter);
1053 insta::assert_snapshot!(to_snapshot_string(output), @r"
1054 [38;5;1m fg only [39m
1055 [48;5;4m bg only [49m
1056 [1m bold only [0m
1057 [2m dim only [0m
1058 [3m italic only [23m
1059 [4m underlined only [24m
1060 [7m reverse only [27m
1061 [1m[3m[4m[7m[38;5;2m[48;5;3m single rule [0m
1062 [38;5;1m[48;5;4m two rules [39m[49m
1063 [EOF]
1064 ");
1065 }
1066
1067 #[test]
1068 fn test_color_formatter_bold_reset() {
1069 let config = config_from_string(indoc! {"
1071 [colors]
1072 not_bold = { fg = 'red', bg = 'blue', italic = true, underline = true }
1073 bold_font = { bold = true }
1074 "});
1075 let mut output: Vec<u8> = vec![];
1076 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1077 formatter.push_label("not_bold");
1078 write!(formatter, " not bold ").unwrap();
1079 formatter.push_label("bold_font");
1080 write!(formatter, " bold ").unwrap();
1081 formatter.pop_label();
1082 write!(formatter, " not bold again ").unwrap();
1083 formatter.pop_label();
1084 drop(formatter);
1085 insta::assert_snapshot!(
1086 to_snapshot_string(output),
1087 @"[3m[4m[38;5;1m[48;5;4m not bold [1m bold [0m[3m[4m[38;5;1m[48;5;4m not bold again [23m[24m[39m[49m[EOF]");
1088 }
1089
1090 #[test]
1091 fn test_color_formatter_dim_reset() {
1092 let config = config_from_string(indoc! {"
1094 [colors]
1095 not_dim = { fg = 'red', bg = 'blue', italic = true, underline = true }
1096 dim_font = { dim = true }
1097 "});
1098 let mut output: Vec<u8> = vec![];
1099 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1100 formatter.push_label("not_dim");
1101 write!(formatter, " not dim ").unwrap();
1102 formatter.push_label("dim_font");
1103 write!(formatter, " dim ").unwrap();
1104 formatter.pop_label();
1105 write!(formatter, " not dim again ").unwrap();
1106 formatter.pop_label();
1107 drop(formatter);
1108 insta::assert_snapshot!(
1109 to_snapshot_string(output),
1110 @"[3m[4m[38;5;1m[48;5;4m not dim [2m dim [0m[3m[4m[38;5;1m[48;5;4m not dim again [23m[24m[39m[49m[EOF]");
1111 }
1112
1113 #[test]
1114 fn test_color_formatter_bold_to_dim() {
1115 let config = config_from_string(indoc! {"
1117 [colors]
1118 bold_font = { bold = true }
1119 dim_font = { dim = true }
1120 "});
1121 let mut output: Vec<u8> = vec![];
1122 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1123 formatter.push_label("bold_font");
1124 write!(formatter, " bold ").unwrap();
1125 formatter.push_label("dim_font");
1126 write!(formatter, " bold&dim ").unwrap();
1127 formatter.pop_label();
1128 write!(formatter, " bold again ").unwrap();
1129 formatter.pop_label();
1130 drop(formatter);
1131 insta::assert_snapshot!(
1132 to_snapshot_string(output),
1133 @"[1m bold [2m bold&dim [0m[1m bold again [0m[EOF]");
1134 }
1135
1136 #[test]
1137 fn test_color_formatter_reset_on_flush() {
1138 let config = config_from_string("colors.red = 'red'");
1139 let mut output: Vec<u8> = vec![];
1140 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1141 formatter.push_label("red");
1142 write!(formatter, "foo").unwrap();
1143 formatter.pop_label();
1144
1145 insta::assert_snapshot!(
1147 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[EOF]");
1148
1149 formatter.flush().unwrap();
1151 insta::assert_snapshot!(
1152 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[39m[EOF]");
1153
1154 formatter.push_label("red");
1156 write!(formatter, "bar").unwrap();
1157 formatter.pop_label();
1158
1159 drop(formatter);
1161 insta::assert_snapshot!(
1162 to_snapshot_string(output), @"[38;5;1mfoo[39m[38;5;1mbar[39m[EOF]");
1163 }
1164
1165 #[test]
1166 fn test_color_formatter_no_space() {
1167 let config = config_from_string(
1169 r#"
1170 colors.red = "red"
1171 colors.green = "green"
1172 "#,
1173 );
1174 let mut output: Vec<u8> = vec![];
1175 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1176 write!(formatter, "before").unwrap();
1177 formatter.push_label("red");
1178 write!(formatter, "first").unwrap();
1179 formatter.pop_label();
1180 formatter.push_label("green");
1181 write!(formatter, "second").unwrap();
1182 formatter.pop_label();
1183 write!(formatter, "after").unwrap();
1184 drop(formatter);
1185 insta::assert_snapshot!(
1186 to_snapshot_string(output), @"before[38;5;1mfirst[38;5;2msecond[39mafter[EOF]");
1187 }
1188
1189 #[test]
1190 fn test_color_formatter_ansi_codes_in_text() {
1191 let config = config_from_string(
1193 r#"
1194 colors.red = "red"
1195 "#,
1196 );
1197 let mut output: Vec<u8> = vec![];
1198 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1199 formatter.push_label("red");
1200 write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
1201 formatter.pop_label();
1202 drop(formatter);
1203 insta::assert_snapshot!(
1204 to_snapshot_string(output), @"[38;5;1m␛[1mnot actually bold␛[0m[39m[EOF]");
1205 }
1206
1207 #[test]
1208 fn test_color_formatter_nested() {
1209 let config = config_from_string(
1213 r#"
1214 colors.outer = "blue"
1215 colors.inner = "red"
1216 colors."outer inner" = "green"
1217 "#,
1218 );
1219 let mut output: Vec<u8> = vec![];
1220 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1221 write!(formatter, " before outer ").unwrap();
1222 formatter.push_label("outer");
1223 write!(formatter, " before inner ").unwrap();
1224 formatter.push_label("inner");
1225 write!(formatter, " inside inner ").unwrap();
1226 formatter.pop_label();
1227 write!(formatter, " after inner ").unwrap();
1228 formatter.pop_label();
1229 write!(formatter, " after outer ").unwrap();
1230 drop(formatter);
1231 insta::assert_snapshot!(
1232 to_snapshot_string(output),
1233 @" before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer [EOF]");
1234 }
1235
1236 #[test]
1237 fn test_color_formatter_partial_match() {
1238 let config = config_from_string(
1240 r#"
1241 colors."outer inner" = "green"
1242 "#,
1243 );
1244 let mut output: Vec<u8> = vec![];
1245 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1246 formatter.push_label("outer");
1247 write!(formatter, " not colored ").unwrap();
1248 formatter.push_label("inner");
1249 write!(formatter, " colored ").unwrap();
1250 formatter.pop_label();
1251 write!(formatter, " not colored ").unwrap();
1252 formatter.pop_label();
1253 drop(formatter);
1254 insta::assert_snapshot!(
1255 to_snapshot_string(output),
1256 @" not colored [38;5;2m colored [39m not colored [EOF]");
1257 }
1258
1259 #[test]
1260 fn test_color_formatter_unrecognized_color() {
1261 let config = config_from_string(
1263 r#"
1264 colors."outer" = "red"
1265 colors."outer inner" = "bloo"
1266 "#,
1267 );
1268 let mut output: Vec<u8> = vec![];
1269 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1270 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1271 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1272 }
1273
1274 #[test]
1275 fn test_color_formatter_unrecognized_ansi256_color() {
1276 let config = config_from_string(
1278 r##"
1279 colors."outer" = "red"
1280 colors."outer inner" = "ansi-color-256"
1281 "##,
1282 );
1283 let mut output: Vec<u8> = vec![];
1284 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1285 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1286 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1287 }
1288
1289 #[test]
1290 fn test_color_formatter_unrecognized_hex_color() {
1291 let config = config_from_string(
1293 r##"
1294 colors."outer" = "red"
1295 colors."outer inner" = "#ffgggg"
1296 "##,
1297 );
1298 let mut output: Vec<u8> = vec![];
1299 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1300 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1301 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1302 }
1303
1304 #[test]
1305 fn test_color_formatter_invalid_type_of_color() {
1306 let config = config_from_string("colors.foo = []");
1307 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1308 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1309 insta::assert_snapshot!(
1310 err.source().unwrap(),
1311 @"invalid type: array, expected a color name or a table of styles");
1312 }
1313
1314 #[test]
1315 fn test_color_formatter_invalid_type_of_style() {
1316 let config = config_from_string("colors.foo = { bold = 1 }");
1317 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1318 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1319 insta::assert_snapshot!(err.source().unwrap(), @r"
1320 invalid type: integer `1`, expected a boolean
1321 in `bold`
1322 ");
1323 }
1324
1325 #[test]
1326 fn test_color_formatter_normal_color() {
1327 let config = config_from_string(
1330 r#"
1331 colors."outer" = {bg="yellow", fg="blue"}
1332 colors."outer default_fg" = "default"
1333 colors."outer default_bg" = {bg = "default"}
1334 "#,
1335 );
1336 let mut output: Vec<u8> = vec![];
1337 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1338 formatter.push_label("outer");
1339 write!(formatter, "Blue on yellow, ").unwrap();
1340 formatter.push_label("default_fg");
1341 write!(formatter, " default fg, ").unwrap();
1342 formatter.pop_label();
1343 write!(formatter, " and back.\nBlue on yellow, ").unwrap();
1344 formatter.push_label("default_bg");
1345 write!(formatter, " default bg, ").unwrap();
1346 formatter.pop_label();
1347 write!(formatter, " and back.").unwrap();
1348 drop(formatter);
1349 insta::assert_snapshot!(to_snapshot_string(output), @r"
1350 [38;5;4m[48;5;3mBlue on yellow, [39m default fg, [38;5;4m and back.[39m[49m
1351 [38;5;4m[48;5;3mBlue on yellow, [49m default bg, [48;5;3m and back.[39m[49m[EOF]
1352 ");
1353 }
1354
1355 #[test]
1356 fn test_color_formatter_sibling() {
1357 let config = config_from_string(
1359 r#"
1360 colors."outer1 inner1" = "red"
1361 colors.inner2 = "green"
1362 "#,
1363 );
1364 let mut output: Vec<u8> = vec![];
1365 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1366 formatter.push_label("outer1");
1367 formatter.push_label("inner2");
1368 write!(formatter, " hello ").unwrap();
1369 formatter.pop_label();
1370 formatter.pop_label();
1371 drop(formatter);
1372 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m hello [39m[EOF]");
1373 }
1374
1375 #[test]
1376 fn test_color_formatter_reverse_order() {
1377 let config = config_from_string(
1379 r#"
1380 colors."inner outer" = "green"
1381 "#,
1382 );
1383 let mut output: Vec<u8> = vec![];
1384 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1385 formatter.push_label("outer");
1386 formatter.push_label("inner");
1387 write!(formatter, " hello ").unwrap();
1388 formatter.pop_label();
1389 formatter.pop_label();
1390 drop(formatter);
1391 insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1392 }
1393
1394 #[test]
1395 fn test_color_formatter_innermost_wins() {
1396 let config = config_from_string(
1398 r#"
1399 colors."a" = "red"
1400 colors."b" = "green"
1401 colors."a c" = "blue"
1402 colors."b c" = "yellow"
1403 "#,
1404 );
1405 let mut output: Vec<u8> = vec![];
1406 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1407 formatter.push_label("a");
1408 write!(formatter, " a1 ").unwrap();
1409 formatter.push_label("b");
1410 write!(formatter, " b1 ").unwrap();
1411 formatter.push_label("c");
1412 write!(formatter, " c ").unwrap();
1413 formatter.pop_label();
1414 write!(formatter, " b2 ").unwrap();
1415 formatter.pop_label();
1416 write!(formatter, " a2 ").unwrap();
1417 formatter.pop_label();
1418 drop(formatter);
1419 insta::assert_snapshot!(
1420 to_snapshot_string(output),
1421 @"[38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m[EOF]");
1422 }
1423
1424 #[test]
1425 fn test_color_formatter_dropped() {
1426 let config = config_from_string(
1429 r#"
1430 colors.outer = "green"
1431 "#,
1432 );
1433 let mut output: Vec<u8> = vec![];
1434 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1435 formatter.push_label("outer");
1436 formatter.push_label("inner");
1437 write!(formatter, " inside ").unwrap();
1438 drop(formatter);
1439 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m inside [39m[EOF]");
1440 }
1441
1442 #[test]
1443 fn test_color_formatter_debug() {
1444 let config = config_from_string(
1447 r#"
1448 colors.outer = "green"
1449 "#,
1450 );
1451 let mut output: Vec<u8> = vec![];
1452 let mut formatter = ColorFormatter::for_config(&mut output, &config, true).unwrap();
1453 formatter.push_label("outer");
1454 formatter.push_label("inner");
1455 write!(formatter, " inside ").unwrap();
1456 formatter.pop_label();
1457 formatter.pop_label();
1458 drop(formatter);
1459 insta::assert_snapshot!(
1460 to_snapshot_string(output), @"[38;5;2m<<outer inner:: inside >>[39m[EOF]");
1461 }
1462
1463 #[test]
1464 fn test_labeled_scope() {
1465 let config = config_from_string(indoc! {"
1466 [colors]
1467 outer = 'blue'
1468 inner = 'red'
1469 'outer inner' = 'green'
1470 "});
1471 let mut output: Vec<u8> = vec![];
1472 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1473 writeln!(formatter.labeled("outer"), "outer").unwrap();
1474 writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner").unwrap();
1475 writeln!(formatter.labeled("inner"), "inner").unwrap();
1476 drop(formatter);
1477 insta::assert_snapshot!(to_snapshot_string(output), @r"
1478 [38;5;4mouter[39m
1479 [38;5;2mouter-inner[39m
1480 [38;5;1minner[39m
1481 [EOF]
1482 ");
1483 }
1484
1485 #[test]
1486 fn test_heading_labeled_writer() {
1487 let config = config_from_string(
1488 r#"
1489 colors.inner = "green"
1490 colors."inner heading" = "red"
1491 "#,
1492 );
1493 let mut output: Vec<u8> = vec![];
1494 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1495 formatter.labeled("inner").with_heading("Should be noop: ");
1496 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1497 write!(writer, "Message").unwrap();
1498 writeln!(writer, " continues").unwrap();
1499 drop(writer);
1500 drop(formatter);
1501 insta::assert_snapshot!(to_snapshot_string(output), @r"
1502 [38;5;1mHeading: [38;5;2mMessage continues[39m
1503 [EOF]
1504 ");
1505 }
1506
1507 #[test]
1508 fn test_heading_labeled_writer_empty_string() {
1509 let mut output: Vec<u8> = vec![];
1510 let mut formatter = PlainTextFormatter::new(&mut output);
1511 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1512 write!(writer, "").unwrap();
1515 write!(writer, "").unwrap();
1516 drop(writer);
1517 insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1518 }
1519
1520 #[test]
1521 fn test_format_recorder() {
1522 let mut recorder = FormatRecorder::new(false);
1523 write!(recorder, " outer1 ").unwrap();
1524 recorder.push_label("inner");
1525 write!(recorder, " inner1 ").unwrap();
1526 write!(recorder, " inner2 ").unwrap();
1527 recorder.pop_label();
1528 write!(recorder, " outer2 ").unwrap();
1529
1530 insta::assert_snapshot!(
1531 to_snapshot_string(recorder.data()),
1532 @" outer1 inner1 inner2 outer2 [EOF]");
1533
1534 let config = config_from_string(r#" colors.inner = "red" "#);
1536 let mut output: Vec<u8> = vec![];
1537 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1538 recorder.replay(&mut formatter).unwrap();
1539 drop(formatter);
1540 insta::assert_snapshot!(
1541 to_snapshot_string(output),
1542 @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1543
1544 let mut output: Vec<u8> = vec![];
1546 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1547 recorder
1548 .replay_with(&mut formatter, |formatter, range| {
1549 let data = &recorder.data()[range];
1550 write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1551 })
1552 .unwrap();
1553 drop(formatter);
1554 insta::assert_snapshot!(
1555 to_snapshot_string(output),
1556 @"<< outer1 >>[38;5;1m<< inner1 inner2 >>[39m<< outer2 >>[EOF]");
1557 }
1558
1559 #[test]
1560 fn test_raw_format_recorder() {
1561 let mut recorder = FormatRecorder::new(false);
1563 write!(recorder.raw().unwrap(), " outer1 ").unwrap();
1564 recorder.push_label("inner");
1565 write!(recorder.raw().unwrap(), " inner1 ").unwrap();
1566 write!(recorder.raw().unwrap(), " inner2 ").unwrap();
1567 recorder.pop_label();
1568 write!(recorder.raw().unwrap(), " outer2 ").unwrap();
1569
1570 let config = config_from_string(r#" colors.inner = "red" "#);
1572 let mut output: Vec<u8> = vec![];
1573 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1574 recorder.replay(&mut formatter).unwrap();
1575 drop(formatter);
1576 insta::assert_snapshot!(
1577 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1578
1579 let mut output: Vec<u8> = vec![];
1580 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1581 recorder
1582 .replay_with(&mut formatter, |_formatter, range| {
1583 panic!(
1584 "Called with {:?} when all output should be raw",
1585 str::from_utf8(&recorder.data()[range]).unwrap()
1586 );
1587 })
1588 .unwrap();
1589 drop(formatter);
1590 insta::assert_snapshot!(
1591 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1592 }
1593}