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