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 use testutils::TestResult;
800
801 use super::*;
802
803 fn config_from_string(text: &str) -> StackedConfig {
804 let mut config = StackedConfig::empty();
805 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
806 config
807 }
808
809 fn to_snapshot_string(output: impl Into<Vec<u8>>) -> BString {
813 let mut output = output.into();
814 output.extend_from_slice(b"[EOF]\n");
815 BString::new(output)
816 }
817
818 #[test]
819 fn test_plaintext_formatter() -> TestResult {
820 let mut output: Vec<u8> = vec![];
822 let mut formatter = PlainTextFormatter::new(&mut output);
823 formatter.push_label("warning");
824 write!(formatter, "hello")?;
825 formatter.pop_label();
826 insta::assert_snapshot!(to_snapshot_string(output), @"hello[EOF]");
827 Ok(())
828 }
829
830 #[test]
831 fn test_plaintext_formatter_ansi_codes_in_text() -> TestResult {
832 let mut output: Vec<u8> = vec![];
834 let mut formatter = PlainTextFormatter::new(&mut output);
835 write!(formatter, "\x1b[1mactually bold\x1b[0m")?;
836 insta::assert_snapshot!(to_snapshot_string(output), @"[1mactually bold[0m[EOF]");
837 Ok(())
838 }
839
840 #[test]
841 fn test_sanitizing_formatter_ansi_codes_in_text() -> TestResult {
842 let mut output: Vec<u8> = vec![];
844 let mut formatter = SanitizingFormatter::new(&mut output);
845 write!(formatter, "\x1b[1mnot actually bold\x1b[0m")?;
846 insta::assert_snapshot!(to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
847 Ok(())
848 }
849
850 #[test]
851 fn test_color_formatter_color_codes() -> TestResult {
852 let config = config_from_string(indoc! {"
855 [colors]
856 black = 'black'
857 red = 'red'
858 green = 'green'
859 yellow = 'yellow'
860 blue = 'blue'
861 magenta = 'magenta'
862 cyan = 'cyan'
863 white = 'white'
864 bright-black = 'bright black'
865 bright-red = 'bright red'
866 bright-green = 'bright green'
867 bright-yellow = 'bright yellow'
868 bright-blue = 'bright blue'
869 bright-magenta = 'bright magenta'
870 bright-cyan = 'bright cyan'
871 bright-white = 'bright white'
872 "});
873 let colors: IndexMap<String, String> = config.get("colors")?;
874 let mut output: Vec<u8> = vec![];
875 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
876 for (label, color) in &colors {
877 formatter.push_label(label);
878 write!(formatter, " {color} ")?;
879 formatter.pop_label();
880 writeln!(formatter)?;
881 }
882 drop(formatter);
883 insta::assert_snapshot!(to_snapshot_string(output), @"
884 [38;5;0m black [39m
885 [38;5;1m red [39m
886 [38;5;2m green [39m
887 [38;5;3m yellow [39m
888 [38;5;4m blue [39m
889 [38;5;5m magenta [39m
890 [38;5;6m cyan [39m
891 [38;5;7m white [39m
892 [38;5;8m bright black [39m
893 [38;5;9m bright red [39m
894 [38;5;10m bright green [39m
895 [38;5;11m bright yellow [39m
896 [38;5;12m bright blue [39m
897 [38;5;13m bright magenta [39m
898 [38;5;14m bright cyan [39m
899 [38;5;15m bright white [39m
900 [EOF]
901 ");
902 Ok(())
903 }
904
905 #[test]
906 fn test_color_for_ansi256_index() {
907 assert_eq!(
908 color_for_ansi256_index("ansi-color-0"),
909 Some(Color::AnsiValue(0))
910 );
911 assert_eq!(
912 color_for_ansi256_index("ansi-color-10"),
913 Some(Color::AnsiValue(10))
914 );
915 assert_eq!(
916 color_for_ansi256_index("ansi-color-255"),
917 Some(Color::AnsiValue(255))
918 );
919 assert_eq!(color_for_ansi256_index("ansi-color-256"), None);
920
921 assert_eq!(color_for_ansi256_index("ansi-color-00"), None);
922 assert_eq!(color_for_ansi256_index("ansi-color-010"), None);
923 assert_eq!(color_for_ansi256_index("ansi-color-0255"), None);
924 }
925
926 #[test]
927 fn test_color_for_hex() {
928 assert_eq!(
929 color_for_hex("#000000"),
930 Some(Color::Rgb { r: 0, g: 0, b: 0 })
931 );
932 assert_eq!(
933 color_for_hex("#fab123"),
934 Some(Color::Rgb {
935 r: 0xfa,
936 g: 0xb1,
937 b: 0x23
938 })
939 );
940 assert_eq!(
941 color_for_hex("#F00D13"),
942 Some(Color::Rgb {
943 r: 0xf0,
944 g: 0x0d,
945 b: 0x13
946 })
947 );
948 assert_eq!(
949 color_for_hex("#ffffff"),
950 Some(Color::Rgb {
951 r: 255,
952 g: 255,
953 b: 255
954 })
955 );
956
957 assert_eq!(color_for_hex("000000"), None);
958 assert_eq!(color_for_hex("0000000"), None);
959 assert_eq!(color_for_hex("#00000g"), None);
960 assert_eq!(color_for_hex("#á00000"), None);
961 }
962
963 #[test]
964 fn test_color_formatter_ansi256() -> TestResult {
965 let config = config_from_string(
966 r#"
967 [colors]
968 purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
969 gray = "ansi-color-244"
970 "#,
971 );
972 let mut output: Vec<u8> = vec![];
973 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
974 formatter.push_label("purple-bg");
975 write!(formatter, " purple background ")?;
976 formatter.pop_label();
977 writeln!(formatter)?;
978 formatter.push_label("gray");
979 write!(formatter, " gray ")?;
980 formatter.pop_label();
981 writeln!(formatter)?;
982 drop(formatter);
983 insta::assert_snapshot!(to_snapshot_string(output), @"
984 [38;5;15m[48;5;93m purple background [39m[49m
985 [38;5;244m gray [39m
986 [EOF]
987 ");
988 Ok(())
989 }
990
991 #[test]
992 fn test_color_formatter_hex_colors() -> TestResult {
993 let config = config_from_string(indoc! {"
995 [colors]
996 black = '#000000'
997 white = '#ffffff'
998 pastel-blue = '#AFE0D9'
999 "});
1000 let colors: IndexMap<String, String> = config.get("colors")?;
1001 let mut output: Vec<u8> = vec![];
1002 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1003 for label in colors.keys() {
1004 formatter.push_label(&label.replace(' ', "-"));
1005 write!(formatter, " {label} ")?;
1006 formatter.pop_label();
1007 writeln!(formatter)?;
1008 }
1009 drop(formatter);
1010 insta::assert_snapshot!(to_snapshot_string(output), @"
1011 [38;2;0;0;0m black [39m
1012 [38;2;255;255;255m white [39m
1013 [38;2;175;224;217m pastel-blue [39m
1014 [EOF]
1015 ");
1016 Ok(())
1017 }
1018
1019 #[test]
1020 fn test_color_formatter_single_label() -> TestResult {
1021 let config = config_from_string(
1024 r#"
1025 colors.inside = "green"
1026 "#,
1027 );
1028 let mut output: Vec<u8> = vec![];
1029 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1030 write!(formatter, " before ")?;
1031 formatter.push_label("inside");
1032 write!(formatter, " inside ")?;
1033 formatter.pop_label();
1034 write!(formatter, " after ")?;
1035 drop(formatter);
1036 insta::assert_snapshot!(
1037 to_snapshot_string(output), @" before [38;5;2m inside [39m after [EOF]");
1038 Ok(())
1039 }
1040
1041 #[test]
1042 fn test_color_formatter_attributes() -> TestResult {
1043 let config = config_from_string(
1046 r#"
1047 colors.red_fg = { fg = "red" }
1048 colors.blue_bg = { bg = "blue" }
1049 colors.bold_font = { bold = true }
1050 colors.dim_font = { dim = true }
1051 colors.italic_text = { italic = true }
1052 colors.underlined_text = { underline = true }
1053 colors.reversed_colors = { reverse = true }
1054 colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, reverse = true }
1055 "#,
1056 );
1057 let mut output: Vec<u8> = vec![];
1058 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1059 formatter.push_label("red_fg");
1060 write!(formatter, " fg only ")?;
1061 formatter.pop_label();
1062 writeln!(formatter)?;
1063 formatter.push_label("blue_bg");
1064 write!(formatter, " bg only ")?;
1065 formatter.pop_label();
1066 writeln!(formatter)?;
1067 formatter.push_label("bold_font");
1068 write!(formatter, " bold only ")?;
1069 formatter.pop_label();
1070 writeln!(formatter)?;
1071 formatter.push_label("dim_font");
1072 write!(formatter, " dim only ")?;
1073 formatter.pop_label();
1074 writeln!(formatter)?;
1075 formatter.push_label("italic_text");
1076 write!(formatter, " italic only ")?;
1077 formatter.pop_label();
1078 writeln!(formatter)?;
1079 formatter.push_label("underlined_text");
1080 write!(formatter, " underlined only ")?;
1081 formatter.pop_label();
1082 writeln!(formatter)?;
1083 formatter.push_label("reversed_colors");
1084 write!(formatter, " reverse only ")?;
1085 formatter.pop_label();
1086 writeln!(formatter)?;
1087 formatter.push_label("multiple");
1088 write!(formatter, " single rule ")?;
1089 formatter.pop_label();
1090 writeln!(formatter)?;
1091 formatter.push_label("red_fg");
1092 formatter.push_label("blue_bg");
1093 write!(formatter, " two rules ")?;
1094 formatter.pop_label();
1095 formatter.pop_label();
1096 writeln!(formatter)?;
1097 drop(formatter);
1098 insta::assert_snapshot!(to_snapshot_string(output), @"
1099 [38;5;1m fg only [39m
1100 [48;5;4m bg only [49m
1101 [1m bold only [0m
1102 [2m dim only [0m
1103 [3m italic only [23m
1104 [4m underlined only [24m
1105 [7m reverse only [27m
1106 [1m[3m[4m[7m[38;5;2m[48;5;3m single rule [0m
1107 [38;5;1m[48;5;4m two rules [39m[49m
1108 [EOF]
1109 ");
1110 Ok(())
1111 }
1112
1113 #[test]
1114 fn test_color_formatter_bold_reset() -> TestResult {
1115 let config = config_from_string(indoc! {"
1117 [colors]
1118 not_bold = { fg = 'red', bg = 'blue', italic = true, underline = true }
1119 bold_font = { bold = true }
1120 stop_bold = { bold = false }
1121 "});
1122 let mut output: Vec<u8> = vec![];
1123 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1124 formatter.push_label("not_bold");
1125 write!(formatter, " not bold ")?;
1126 formatter.push_label("bold_font");
1127 write!(formatter, " bold ")?;
1128 formatter.push_label("stop_bold");
1129 write!(formatter, " stop bold ")?;
1130 formatter.pop_label();
1131 write!(formatter, " bold again ")?;
1132 formatter.pop_label();
1133 write!(formatter, " not bold again ")?;
1134 formatter.pop_label();
1135 drop(formatter);
1136 insta::assert_snapshot!(
1137 to_snapshot_string(output),
1138 @"[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]");
1139 Ok(())
1140 }
1141
1142 #[test]
1143 fn test_color_formatter_dim_reset() -> TestResult {
1144 let config = config_from_string(indoc! {"
1146 [colors]
1147 not_dim = { fg = 'red', bg = 'blue', italic = true, underline = true }
1148 dim_font = { dim = true }
1149 stop_dim = { dim = false }
1150 "});
1151 let mut output: Vec<u8> = vec![];
1152 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1153 formatter.push_label("not_dim");
1154 write!(formatter, " not dim ")?;
1155 formatter.push_label("dim_font");
1156 write!(formatter, " dim ")?;
1157 formatter.push_label("stop_dim");
1158 write!(formatter, " stop dim ")?;
1159 formatter.pop_label();
1160 write!(formatter, " dim again ")?;
1161 formatter.pop_label();
1162 write!(formatter, " not dim again ")?;
1163 formatter.pop_label();
1164 drop(formatter);
1165 insta::assert_snapshot!(
1166 to_snapshot_string(output),
1167 @"[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]");
1168 Ok(())
1169 }
1170
1171 #[test]
1172 fn test_color_formatter_bold_to_dim() -> TestResult {
1173 let config = config_from_string(indoc! {"
1175 [colors]
1176 bold_font = { bold = true }
1177 dim_font = { dim = true }
1178 "});
1179 let mut output: Vec<u8> = vec![];
1180 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1181 formatter.push_label("bold_font");
1182 write!(formatter, " bold ")?;
1183 formatter.push_label("dim_font");
1184 write!(formatter, " bold&dim ")?;
1185 formatter.pop_label();
1186 write!(formatter, " bold again ")?;
1187 formatter.pop_label();
1188 drop(formatter);
1189 insta::assert_snapshot!(
1190 to_snapshot_string(output),
1191 @"[1m bold [2m bold&dim [0m[1m bold again [0m[EOF]");
1192 Ok(())
1193 }
1194
1195 #[test]
1196 fn test_formatter_reset_on_flush() -> TestResult {
1197 let config = config_from_string("colors.red = 'red'");
1198 let mut output: Vec<u8> = vec![];
1199 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1200 formatter.push_label("red");
1201 write!(formatter, "foo")?;
1202 formatter.pop_label();
1203
1204 insta::assert_snapshot!(
1206 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[EOF]");
1207
1208 formatter.flush()?;
1210 insta::assert_snapshot!(
1211 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[39m[EOF]");
1212
1213 formatter.push_label("red");
1215 write!(formatter, "bar")?;
1216 formatter.pop_label();
1217
1218 drop(formatter);
1220 insta::assert_snapshot!(
1221 to_snapshot_string(output), @"[38;5;1mfoo[39m[38;5;1mbar[39m[EOF]");
1222
1223 let mut output: Vec<u8> = vec![];
1225 let mut formatter = PlainTextFormatter::new(&mut output);
1226 formatter.push_label("red");
1227 write!(formatter, "foo")?;
1228 formatter.pop_label();
1229 formatter.flush()?;
1230 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1231
1232 let mut output: Vec<u8> = vec![];
1233 let mut formatter = SanitizingFormatter::new(&mut output);
1234 formatter.push_label("red");
1235 write!(formatter, "foo")?;
1236 formatter.pop_label();
1237 formatter.flush()?;
1238 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1239 Ok(())
1240 }
1241
1242 #[test]
1243 fn test_color_formatter_no_space() -> TestResult {
1244 let config = config_from_string(
1246 r#"
1247 colors.red = "red"
1248 colors.green = "green"
1249 "#,
1250 );
1251 let mut output: Vec<u8> = vec![];
1252 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1253 write!(formatter, "before")?;
1254 formatter.push_label("red");
1255 write!(formatter, "first")?;
1256 formatter.pop_label();
1257 formatter.push_label("green");
1258 write!(formatter, "second")?;
1259 formatter.pop_label();
1260 write!(formatter, "after")?;
1261 drop(formatter);
1262 insta::assert_snapshot!(
1263 to_snapshot_string(output), @"before[38;5;1mfirst[38;5;2msecond[39mafter[EOF]");
1264 Ok(())
1265 }
1266
1267 #[test]
1268 fn test_color_formatter_ansi_codes_in_text() -> TestResult {
1269 let config = config_from_string(
1271 r#"
1272 colors.red = "red"
1273 "#,
1274 );
1275 let mut output: Vec<u8> = vec![];
1276 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1277 formatter.push_label("red");
1278 write!(formatter, "\x1b[1mnot actually bold\x1b[0m")?;
1279 formatter.pop_label();
1280 drop(formatter);
1281 insta::assert_snapshot!(
1282 to_snapshot_string(output), @"[38;5;1m␛[1mnot actually bold␛[0m[39m[EOF]");
1283 Ok(())
1284 }
1285
1286 #[test]
1287 fn test_color_formatter_nested() -> TestResult {
1288 let config = config_from_string(
1292 r#"
1293 colors.outer = "blue"
1294 colors.inner = "red"
1295 colors."outer inner" = "green"
1296 "#,
1297 );
1298 let mut output: Vec<u8> = vec![];
1299 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1300 write!(formatter, " before outer ")?;
1301 formatter.push_label("outer");
1302 write!(formatter, " before inner ")?;
1303 formatter.push_label("inner");
1304 write!(formatter, " inside inner ")?;
1305 formatter.pop_label();
1306 write!(formatter, " after inner ")?;
1307 formatter.pop_label();
1308 write!(formatter, " after outer ")?;
1309 drop(formatter);
1310 insta::assert_snapshot!(
1311 to_snapshot_string(output),
1312 @" before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer [EOF]");
1313 Ok(())
1314 }
1315
1316 #[test]
1317 fn test_color_formatter_partial_match() -> TestResult {
1318 let config = config_from_string(
1320 r#"
1321 colors."outer inner" = "green"
1322 "#,
1323 );
1324 let mut output: Vec<u8> = vec![];
1325 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1326 formatter.push_label("outer");
1327 write!(formatter, " not colored ")?;
1328 formatter.push_label("inner");
1329 write!(formatter, " colored ")?;
1330 formatter.pop_label();
1331 write!(formatter, " not colored ")?;
1332 formatter.pop_label();
1333 drop(formatter);
1334 insta::assert_snapshot!(
1335 to_snapshot_string(output),
1336 @" not colored [38;5;2m colored [39m not colored [EOF]");
1337 Ok(())
1338 }
1339
1340 #[test]
1341 fn test_color_formatter_unrecognized_color() {
1342 let config = config_from_string(
1344 r#"
1345 colors."outer" = "red"
1346 colors."outer inner" = "bloo"
1347 "#,
1348 );
1349 let mut output: Vec<u8> = vec![];
1350 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1351 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1352 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1353 }
1354
1355 #[test]
1356 fn test_color_formatter_unrecognized_ansi256_color() {
1357 let config = config_from_string(
1359 r##"
1360 colors."outer" = "red"
1361 colors."outer inner" = "ansi-color-256"
1362 "##,
1363 );
1364 let mut output: Vec<u8> = vec![];
1365 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1366 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1367 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1368 }
1369
1370 #[test]
1371 fn test_color_formatter_unrecognized_hex_color() {
1372 let config = config_from_string(
1374 r##"
1375 colors."outer" = "red"
1376 colors."outer inner" = "#ffgggg"
1377 "##,
1378 );
1379 let mut output: Vec<u8> = vec![];
1380 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1381 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1382 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1383 }
1384
1385 #[test]
1386 fn test_color_formatter_invalid_type_of_color() {
1387 let config = config_from_string("colors.foo = []");
1388 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1389 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1390 insta::assert_snapshot!(
1391 err.source().unwrap(),
1392 @"invalid type: array, expected a color name or a table of styles");
1393 }
1394
1395 #[test]
1396 fn test_color_formatter_invalid_type_of_style() {
1397 let config = config_from_string("colors.foo = { bold = 1 }");
1398 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1399 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1400 insta::assert_snapshot!(err.source().unwrap(), @"
1401 invalid type: integer `1`, expected a boolean
1402 in `bold`
1403 ");
1404 }
1405
1406 #[test]
1407 fn test_color_formatter_normal_color() -> TestResult {
1408 let config = config_from_string(
1411 r#"
1412 colors."outer" = {bg="yellow", fg="blue"}
1413 colors."outer default_fg" = "default"
1414 colors."outer default_bg" = {bg = "default"}
1415 "#,
1416 );
1417 let mut output: Vec<u8> = vec![];
1418 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1419 formatter.push_label("outer");
1420 write!(formatter, "Blue on yellow, ")?;
1421 formatter.push_label("default_fg");
1422 write!(formatter, " default fg, ")?;
1423 formatter.pop_label();
1424 write!(formatter, " and back.\nBlue on yellow, ")?;
1425 formatter.push_label("default_bg");
1426 write!(formatter, " default bg, ")?;
1427 formatter.pop_label();
1428 write!(formatter, " and back.")?;
1429 drop(formatter);
1430 insta::assert_snapshot!(to_snapshot_string(output), @"
1431 [38;5;4m[48;5;3mBlue on yellow, [39m default fg, [38;5;4m and back.[39m[49m
1432 [38;5;4m[48;5;3mBlue on yellow, [49m default bg, [48;5;3m and back.[39m[49m[EOF]
1433 ");
1434 Ok(())
1435 }
1436
1437 #[test]
1438 fn test_color_formatter_sibling() -> TestResult {
1439 let config = config_from_string(
1441 r#"
1442 colors."outer1 inner1" = "red"
1443 colors.inner2 = "green"
1444 "#,
1445 );
1446 let mut output: Vec<u8> = vec![];
1447 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1448 formatter.push_label("outer1");
1449 formatter.push_label("inner2");
1450 write!(formatter, " hello ")?;
1451 formatter.pop_label();
1452 formatter.pop_label();
1453 drop(formatter);
1454 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m hello [39m[EOF]");
1455 Ok(())
1456 }
1457
1458 #[test]
1459 fn test_color_formatter_reverse_order() -> TestResult {
1460 let config = config_from_string(
1462 r#"
1463 colors."inner outer" = "green"
1464 "#,
1465 );
1466 let mut output: Vec<u8> = vec![];
1467 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1468 formatter.push_label("outer");
1469 formatter.push_label("inner");
1470 write!(formatter, " hello ")?;
1471 formatter.pop_label();
1472 formatter.pop_label();
1473 drop(formatter);
1474 insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1475 Ok(())
1476 }
1477
1478 #[test]
1479 fn test_color_formatter_innermost_wins() -> TestResult {
1480 let config = config_from_string(
1482 r#"
1483 colors."a" = "red"
1484 colors."b" = "green"
1485 colors."a c" = "blue"
1486 colors."b c" = "yellow"
1487 "#,
1488 );
1489 let mut output: Vec<u8> = vec![];
1490 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1491 formatter.push_label("a");
1492 write!(formatter, " a1 ")?;
1493 formatter.push_label("b");
1494 write!(formatter, " b1 ")?;
1495 formatter.push_label("c");
1496 write!(formatter, " c ")?;
1497 formatter.pop_label();
1498 write!(formatter, " b2 ")?;
1499 formatter.pop_label();
1500 write!(formatter, " a2 ")?;
1501 formatter.pop_label();
1502 drop(formatter);
1503 insta::assert_snapshot!(
1504 to_snapshot_string(output),
1505 @"[38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m[EOF]");
1506 Ok(())
1507 }
1508
1509 #[test]
1510 fn test_color_formatter_dropped() -> TestResult {
1511 let config = config_from_string(
1514 r#"
1515 colors.outer = "green"
1516 "#,
1517 );
1518 let mut output: Vec<u8> = vec![];
1519 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1520 formatter.push_label("outer");
1521 formatter.push_label("inner");
1522 write!(formatter, " inside ")?;
1523 drop(formatter);
1524 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m inside [39m[EOF]");
1525 Ok(())
1526 }
1527
1528 #[test]
1529 fn test_color_formatter_debug() -> TestResult {
1530 let config = config_from_string(
1533 r#"
1534 colors.outer = "green"
1535 "#,
1536 );
1537 let mut output: Vec<u8> = vec![];
1538 let mut formatter = ColorFormatter::for_config(&mut output, &config, true)?;
1539 formatter.push_label("outer");
1540 formatter.push_label("inner");
1541 write!(formatter, " inside ")?;
1542 formatter.pop_label();
1543 formatter.pop_label();
1544 formatter.push_label("outer");
1546 formatter.push_label("inner");
1547 write!(formatter, " inside two ")?;
1548 formatter.pop_label();
1549 formatter.pop_label();
1550 drop(formatter);
1551 insta::assert_snapshot!(
1552 to_snapshot_string(output),
1553 @"[38;5;2m<<outer inner:: inside inside two >>[39m[EOF]",
1554 );
1555 Ok(())
1556 }
1557
1558 #[test]
1559 fn test_labeled_scope() -> TestResult {
1560 let config = config_from_string(indoc! {"
1561 [colors]
1562 outer = 'blue'
1563 inner = 'red'
1564 'outer inner' = 'green'
1565 "});
1566 let mut output: Vec<u8> = vec![];
1567 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1568 writeln!(formatter.labeled("outer"), "outer")?;
1569 writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner")?;
1570 writeln!(formatter.labeled("inner"), "inner")?;
1571 drop(formatter);
1572 insta::assert_snapshot!(to_snapshot_string(output), @"
1573 [38;5;4mouter[39m
1574 [38;5;2mouter-inner[39m
1575 [38;5;1minner[39m
1576 [EOF]
1577 ");
1578 Ok(())
1579 }
1580
1581 #[test]
1582 fn test_heading_labeled_writer() -> TestResult {
1583 let config = config_from_string(
1584 r#"
1585 colors.inner = "green"
1586 colors."inner heading" = "red"
1587 "#,
1588 );
1589 let mut output: Vec<u8> = vec![];
1590 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1591 formatter.labeled("inner").with_heading("Should be noop: ");
1592 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1593 write!(writer, "Message")?;
1594 writeln!(writer, " continues")?;
1595 drop(writer);
1596 drop(formatter);
1597 insta::assert_snapshot!(to_snapshot_string(output), @"
1598 [38;5;1mHeading: [38;5;2mMessage continues[39m
1599 [EOF]
1600 ");
1601 Ok(())
1602 }
1603
1604 #[test]
1605 fn test_heading_labeled_writer_empty_string() -> TestResult {
1606 let mut output: Vec<u8> = vec![];
1607 let mut formatter = PlainTextFormatter::new(&mut output);
1608 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1609 write!(writer, "")?;
1612 write!(writer, "")?;
1613 drop(writer);
1614 insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1615 Ok(())
1616 }
1617
1618 #[test]
1619 fn test_format_recorder() -> TestResult {
1620 let mut recorder = FormatRecorder::new(false);
1621 write!(recorder, " outer1 ")?;
1622 recorder.push_label("inner");
1623 write!(recorder, " inner1 ")?;
1624 write!(recorder, " inner2 ")?;
1625 recorder.pop_label();
1626 write!(recorder, " outer2 ")?;
1627
1628 insta::assert_snapshot!(
1629 to_snapshot_string(recorder.data()),
1630 @" outer1 inner1 inner2 outer2 [EOF]");
1631
1632 let config = config_from_string(r#" colors.inner = "red" "#);
1634 let mut output: Vec<u8> = vec![];
1635 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1636 recorder.replay(&mut formatter)?;
1637 drop(formatter);
1638 insta::assert_snapshot!(
1639 to_snapshot_string(output),
1640 @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1641
1642 let mut output: Vec<u8> = vec![];
1644 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1645 recorder.replay_with(&mut formatter, |formatter, range| {
1646 let data = &recorder.data()[range];
1647 write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1648 })?;
1649 drop(formatter);
1650 insta::assert_snapshot!(
1651 to_snapshot_string(output),
1652 @"<< outer1 >>[38;5;1m<< inner1 inner2 >>[39m<< outer2 >>[EOF]");
1653 Ok(())
1654 }
1655
1656 #[test]
1657 fn test_raw_format_recorder() -> TestResult {
1658 let mut recorder = FormatRecorder::new(false);
1660 write!(recorder.raw()?, " outer1 ")?;
1661 recorder.push_label("inner");
1662 write!(recorder.raw()?, " inner1 ")?;
1663 write!(recorder.raw()?, " inner2 ")?;
1664 recorder.pop_label();
1665 write!(recorder.raw()?, " outer2 ")?;
1666
1667 let config = config_from_string(r#" colors.inner = "red" "#);
1669 let mut output: Vec<u8> = vec![];
1670 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1671 recorder.replay(&mut formatter)?;
1672 drop(formatter);
1673 insta::assert_snapshot!(
1674 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1675
1676 let mut output: Vec<u8> = vec![];
1677 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1678 recorder.replay_with(&mut formatter, |_formatter, range| {
1679 panic!(
1680 "Called with {:?} when all output should be raw",
1681 str::from_utf8(&recorder.data()[range]).unwrap()
1682 );
1683 })?;
1684 drop(formatter);
1685 insta::assert_snapshot!(
1686 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1687 Ok(())
1688 }
1689}