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 crossed_out: Option<bool>,
304 pub reverse: Option<bool>,
305}
306
307impl Style {
308 fn merge(&mut self, other: &Self) {
309 self.fg = other.fg.or(self.fg);
310 self.bg = other.bg.or(self.bg);
311 self.bold = other.bold.or(self.bold);
312 self.dim = other.dim.or(self.dim);
313 self.italic = other.italic.or(self.italic);
314 self.underline = other.underline.or(self.underline);
315 self.crossed_out = other.crossed_out.or(self.crossed_out);
316 self.reverse = other.reverse.or(self.reverse);
317 }
318}
319
320#[derive(Clone, Debug)]
321pub struct ColorFormatter<W: Write> {
322 output: W,
323 rules: Arc<Rules>,
324 labels: Vec<String>,
327 cached_styles: HashMap<Vec<String>, Style>,
328 current_style: Style,
330 current_debug: Option<String>,
333}
334
335impl<W: Write> ColorFormatter<W> {
336 pub fn new(output: W, rules: Arc<Rules>, debug: bool) -> Self {
337 Self {
338 output,
339 rules,
340 labels: vec![],
341 cached_styles: HashMap::new(),
342 current_style: Style::default(),
343 current_debug: debug.then(String::new),
344 }
345 }
346
347 pub fn for_config(
348 output: W,
349 config: &StackedConfig,
350 debug: bool,
351 ) -> Result<Self, ConfigGetError> {
352 let rules = rules_from_config(config)?;
353 Ok(Self::new(output, Arc::new(rules), debug))
354 }
355
356 fn requested_style(&mut self) -> Style {
357 if let Some(cached) = self.cached_styles.get(&self.labels) {
358 cached.clone()
359 } else {
360 let mut matched_styles = vec![];
366 for (labels, style) in self.rules.as_ref() {
367 let mut labels_iter = self.labels.iter().enumerate();
368 let mut matched_indices = vec![];
370 for required_label in labels {
371 for (label_index, label) in &mut labels_iter {
372 if label == required_label {
373 matched_indices.push(label_index);
374 break;
375 }
376 }
377 }
378 if matched_indices.len() == labels.len() {
379 matched_indices.reverse();
380 matched_styles.push((style, matched_indices));
381 }
382 }
383 matched_styles.sort_by_key(|(_, indices)| indices.clone());
384
385 let mut style = Style::default();
386 for (matched_style, _) in matched_styles {
387 style.merge(matched_style);
388 }
389 self.cached_styles
390 .insert(self.labels.clone(), style.clone());
391 style
392 }
393 }
394
395 fn write_new_style(&mut self) -> io::Result<()> {
396 let new_debug = match &self.current_debug {
397 Some(current) => {
398 let joined = self.labels.join(" ");
399 if joined == *current {
400 None
401 } else {
402 if !current.is_empty() {
403 write!(self.output, ">>")?;
404 }
405 Some(joined)
406 }
407 }
408 None => None,
409 };
410 let new_style = self.requested_style();
411 if new_style != self.current_style {
412 let new_bold = new_style.bold.unwrap_or_default();
420 let new_dim = new_style.dim.unwrap_or_default();
421 if (new_style.bold != self.current_style.bold && !new_bold)
422 || (new_style.dim != self.current_style.dim && !new_dim)
423 {
424 queue!(self.output, SetAttribute(Attribute::Reset))?;
425 self.current_style = Style::default();
426 }
427 if new_style.bold != self.current_style.bold && new_bold {
428 queue!(self.output, SetAttribute(Attribute::Bold))?;
429 }
430 if new_style.dim != self.current_style.dim && new_dim {
431 queue!(self.output, SetAttribute(Attribute::Dim))?;
432 }
433
434 if new_style.italic != self.current_style.italic {
435 if new_style.italic.unwrap_or_default() {
436 queue!(self.output, SetAttribute(Attribute::Italic))?;
437 } else {
438 queue!(self.output, SetAttribute(Attribute::NoItalic))?;
439 }
440 }
441 if new_style.underline != self.current_style.underline {
442 if new_style.underline.unwrap_or_default() {
443 queue!(self.output, SetAttribute(Attribute::Underlined))?;
444 } else {
445 queue!(self.output, SetAttribute(Attribute::NoUnderline))?;
446 }
447 }
448 if new_style.crossed_out != self.current_style.crossed_out {
449 if new_style.crossed_out.unwrap_or_default() {
450 queue!(self.output, SetAttribute(Attribute::CrossedOut))?;
451 } else {
452 queue!(self.output, SetAttribute(Attribute::NotCrossedOut))?;
453 }
454 }
455 if new_style.reverse != self.current_style.reverse {
456 if new_style.reverse.unwrap_or_default() {
457 queue!(self.output, SetAttribute(Attribute::Reverse))?;
458 } else {
459 queue!(self.output, SetAttribute(Attribute::NoReverse))?;
460 }
461 }
462 if new_style.fg != self.current_style.fg {
463 queue!(
464 self.output,
465 SetForegroundColor(new_style.fg.unwrap_or(Color::Reset))
466 )?;
467 }
468 if new_style.bg != self.current_style.bg {
469 queue!(
470 self.output,
471 SetBackgroundColor(new_style.bg.unwrap_or(Color::Reset))
472 )?;
473 }
474 self.current_style = new_style;
475 }
476 if let Some(d) = new_debug {
477 if !d.is_empty() {
478 write!(self.output, "<<{d}::")?;
479 }
480 self.current_debug = Some(d);
481 }
482 Ok(())
483 }
484}
485
486fn rules_from_config(config: &StackedConfig) -> Result<Rules, ConfigGetError> {
487 config
488 .table_keys("colors")
489 .map(|key| {
490 let labels = key
491 .split_whitespace()
492 .map(ToString::to_string)
493 .collect_vec();
494 let style = config.get_value_with(["colors", key], |value| {
495 if value.is_str() {
496 Ok(Style {
497 fg: Some(deserialize_color(value.into_deserializer())?),
498 bg: None,
499 bold: None,
500 dim: None,
501 italic: None,
502 underline: None,
503 crossed_out: None,
504 reverse: None,
505 })
506 } else if value.is_inline_table() {
507 Style::deserialize(value.into_deserializer())
508 } else {
509 Err(toml_edit::de::Error::custom(format!(
510 "invalid type: {}, expected a color name or a table of styles",
511 value.type_name()
512 )))
513 }
514 })?;
515 Ok((labels, style))
516 })
517 .collect()
518}
519
520fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
521where
522 D: serde::Deserializer<'de>,
523{
524 let color_str = String::deserialize(deserializer)?;
525 color_for_string(&color_str).map_err(D::Error::custom)
526}
527
528fn deserialize_color_opt<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
529where
530 D: serde::Deserializer<'de>,
531{
532 deserialize_color(deserializer).map(Some)
533}
534
535fn color_for_string(color_str: &str) -> Result<Color, String> {
536 match color_str {
537 "default" => Ok(Color::Reset),
538 "black" => Ok(Color::Black),
539 "red" => Ok(Color::DarkRed),
540 "green" => Ok(Color::DarkGreen),
541 "yellow" => Ok(Color::DarkYellow),
542 "blue" => Ok(Color::DarkBlue),
543 "magenta" => Ok(Color::DarkMagenta),
544 "cyan" => Ok(Color::DarkCyan),
545 "white" => Ok(Color::Grey),
546 "bright black" => Ok(Color::DarkGrey),
547 "bright red" => Ok(Color::Red),
548 "bright green" => Ok(Color::Green),
549 "bright yellow" => Ok(Color::Yellow),
550 "bright blue" => Ok(Color::Blue),
551 "bright magenta" => Ok(Color::Magenta),
552 "bright cyan" => Ok(Color::Cyan),
553 "bright white" => Ok(Color::White),
554 _ => color_for_ansi256_index(color_str)
555 .or_else(|| color_for_hex(color_str))
556 .ok_or_else(|| format!("Invalid color: {color_str}")),
557 }
558}
559
560fn color_for_ansi256_index(color: &str) -> Option<Color> {
561 color
562 .strip_prefix("ansi-color-")
563 .filter(|s| *s == "0" || !s.starts_with('0'))
564 .and_then(|n| n.parse::<u8>().ok())
565 .map(Color::AnsiValue)
566}
567
568fn color_for_hex(color: &str) -> Option<Color> {
569 if color.len() == 7
570 && color.starts_with('#')
571 && color[1..].chars().all(|c| c.is_ascii_hexdigit())
572 {
573 let r = u8::from_str_radix(&color[1..3], 16);
574 let g = u8::from_str_radix(&color[3..5], 16);
575 let b = u8::from_str_radix(&color[5..7], 16);
576 match (r, g, b) {
577 (Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb { r, g, b }),
578 _ => None,
579 }
580 } else {
581 None
582 }
583}
584
585impl<W: Write> Write for ColorFormatter<W> {
586 fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
587 for line in data.split_inclusive(|b| *b == b'\n') {
612 if line.ends_with(b"\n") {
613 self.write_new_style()?;
614 write_sanitized(&mut self.output, &line[..line.len() - 1])?;
615 let labels = mem::take(&mut self.labels);
616 self.write_new_style()?;
617 self.output.write_all(b"\n")?;
618 self.labels = labels;
619 } else {
620 self.write_new_style()?;
621 write_sanitized(&mut self.output, line)?;
622 }
623 }
624
625 Ok(data.len())
626 }
627
628 fn flush(&mut self) -> Result<(), Error> {
629 self.write_new_style()?;
630 self.output.flush()
631 }
632}
633
634impl<W: Write> Formatter for ColorFormatter<W> {
635 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
636 self.write_new_style()?;
637 Ok(Box::new(self.output.by_ref()))
638 }
639
640 fn push_label(&mut self, label: &str) {
641 self.labels.push(label.to_owned());
642 }
643
644 fn pop_label(&mut self) {
645 self.labels.pop();
646 }
647
648 fn maybe_color(&self) -> bool {
649 true
650 }
651}
652
653impl<W: Write> Drop for ColorFormatter<W> {
654 fn drop(&mut self) {
655 self.labels.clear();
658 self.write_new_style().ok();
659 }
660}
661
662#[derive(Clone, Debug)]
670pub struct FormatRecorder {
671 data: Vec<u8>,
672 ops: Vec<(usize, FormatOp)>,
673 maybe_color: bool,
674}
675
676#[derive(Clone, Debug, Eq, PartialEq)]
677enum FormatOp {
678 PushLabel(String),
679 PopLabel,
680 RawEscapeSequence(Vec<u8>),
681}
682
683impl FormatRecorder {
684 pub fn new(maybe_color: bool) -> Self {
685 Self {
686 data: vec![],
687 ops: vec![],
688 maybe_color,
689 }
690 }
691
692 pub fn with_data(data: impl Into<Vec<u8>>) -> Self {
694 Self {
695 data: data.into(),
696 ops: vec![],
697 maybe_color: false,
698 }
699 }
700
701 pub fn data(&self) -> &[u8] {
702 &self.data
703 }
704
705 fn push_op(&mut self, op: FormatOp) {
706 self.ops.push((self.data.len(), op));
707 }
708
709 pub fn replay(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
710 self.replay_with(formatter, |formatter, range| {
711 formatter.write_all(&self.data[range])
712 })
713 }
714
715 pub fn replay_with(
716 &self,
717 formatter: &mut dyn Formatter,
718 mut write_data: impl FnMut(&mut dyn Formatter, Range<usize>) -> io::Result<()>,
719 ) -> io::Result<()> {
720 let mut last_pos = 0;
721 let mut flush_data = |formatter: &mut dyn Formatter, pos| -> io::Result<()> {
722 if last_pos != pos {
723 write_data(formatter, last_pos..pos)?;
724 last_pos = pos;
725 }
726 Ok(())
727 };
728 for (pos, op) in &self.ops {
729 flush_data(formatter, *pos)?;
730 match op {
731 FormatOp::PushLabel(label) => formatter.push_label(label),
732 FormatOp::PopLabel => formatter.pop_label(),
733 FormatOp::RawEscapeSequence(raw_escape_sequence) => {
734 formatter.raw()?.write_all(raw_escape_sequence)?;
735 }
736 }
737 }
738 flush_data(formatter, self.data.len())
739 }
740}
741
742impl Write for FormatRecorder {
743 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
744 self.data.extend_from_slice(data);
745 Ok(data.len())
746 }
747
748 fn flush(&mut self) -> io::Result<()> {
749 Ok(())
750 }
751}
752
753struct RawEscapeSequenceRecorder<'a>(&'a mut FormatRecorder);
754
755impl Write for RawEscapeSequenceRecorder<'_> {
756 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
757 self.0.push_op(FormatOp::RawEscapeSequence(data.to_vec()));
758 Ok(data.len())
759 }
760
761 fn flush(&mut self) -> io::Result<()> {
762 self.0.flush()
763 }
764}
765
766impl Formatter for FormatRecorder {
767 fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
768 Ok(Box::new(RawEscapeSequenceRecorder(self)))
769 }
770
771 fn push_label(&mut self, label: &str) {
772 self.push_op(FormatOp::PushLabel(label.to_owned()));
773 }
774
775 fn pop_label(&mut self) {
776 self.push_op(FormatOp::PopLabel);
777 }
778
779 fn maybe_color(&self) -> bool {
780 self.maybe_color
781 }
782}
783
784fn write_sanitized(output: &mut impl Write, buf: &[u8]) -> Result<(), Error> {
785 if buf.contains(&b'\x1b') {
786 let mut sanitized = Vec::with_capacity(buf.len());
787 for b in buf {
788 if *b == b'\x1b' {
789 sanitized.extend_from_slice("␛".as_bytes());
790 } else {
791 sanitized.push(*b);
792 }
793 }
794 output.write_all(&sanitized)
795 } else {
796 output.write_all(buf)
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use std::error::Error as _;
803
804 use bstr::BString;
805 use indexmap::IndexMap;
806 use indoc::indoc;
807 use jj_lib::config::ConfigLayer;
808 use jj_lib::config::ConfigSource;
809 use testutils::TestResult;
810
811 use super::*;
812
813 fn config_from_string(text: &str) -> StackedConfig {
814 let mut config = StackedConfig::empty();
815 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
816 config
817 }
818
819 fn to_snapshot_string(output: impl Into<Vec<u8>>) -> BString {
823 let mut output = output.into();
824 output.extend_from_slice(b"[EOF]\n");
825 BString::new(output)
826 }
827
828 #[test]
829 fn test_plaintext_formatter() -> TestResult {
830 let mut output: Vec<u8> = vec![];
832 let mut formatter = PlainTextFormatter::new(&mut output);
833 formatter.push_label("warning");
834 write!(formatter, "hello")?;
835 formatter.pop_label();
836 insta::assert_snapshot!(to_snapshot_string(output), @"hello[EOF]");
837 Ok(())
838 }
839
840 #[test]
841 fn test_plaintext_formatter_ansi_codes_in_text() -> TestResult {
842 let mut output: Vec<u8> = vec![];
844 let mut formatter = PlainTextFormatter::new(&mut output);
845 write!(formatter, "\x1b[1mactually bold\x1b[0m")?;
846 insta::assert_snapshot!(to_snapshot_string(output), @"[1mactually bold[0m[EOF]");
847 Ok(())
848 }
849
850 #[test]
851 fn test_sanitizing_formatter_ansi_codes_in_text() -> TestResult {
852 let mut output: Vec<u8> = vec![];
854 let mut formatter = SanitizingFormatter::new(&mut output);
855 write!(formatter, "\x1b[1mnot actually bold\x1b[0m")?;
856 insta::assert_snapshot!(to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
857 Ok(())
858 }
859
860 #[test]
861 fn test_color_formatter_color_codes() -> TestResult {
862 let config = config_from_string(indoc! {"
865 [colors]
866 black = 'black'
867 red = 'red'
868 green = 'green'
869 yellow = 'yellow'
870 blue = 'blue'
871 magenta = 'magenta'
872 cyan = 'cyan'
873 white = 'white'
874 bright-black = 'bright black'
875 bright-red = 'bright red'
876 bright-green = 'bright green'
877 bright-yellow = 'bright yellow'
878 bright-blue = 'bright blue'
879 bright-magenta = 'bright magenta'
880 bright-cyan = 'bright cyan'
881 bright-white = 'bright white'
882 "});
883 let colors: IndexMap<String, String> = config.get("colors")?;
884 let mut output: Vec<u8> = vec![];
885 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
886 for (label, color) in &colors {
887 formatter.push_label(label);
888 write!(formatter, " {color} ")?;
889 formatter.pop_label();
890 writeln!(formatter)?;
891 }
892 drop(formatter);
893 insta::assert_snapshot!(to_snapshot_string(output), @"
894 [38;5;0m black [39m
895 [38;5;1m red [39m
896 [38;5;2m green [39m
897 [38;5;3m yellow [39m
898 [38;5;4m blue [39m
899 [38;5;5m magenta [39m
900 [38;5;6m cyan [39m
901 [38;5;7m white [39m
902 [38;5;8m bright black [39m
903 [38;5;9m bright red [39m
904 [38;5;10m bright green [39m
905 [38;5;11m bright yellow [39m
906 [38;5;12m bright blue [39m
907 [38;5;13m bright magenta [39m
908 [38;5;14m bright cyan [39m
909 [38;5;15m bright white [39m
910 [EOF]
911 ");
912 Ok(())
913 }
914
915 #[test]
916 fn test_color_for_ansi256_index() {
917 assert_eq!(
918 color_for_ansi256_index("ansi-color-0"),
919 Some(Color::AnsiValue(0))
920 );
921 assert_eq!(
922 color_for_ansi256_index("ansi-color-10"),
923 Some(Color::AnsiValue(10))
924 );
925 assert_eq!(
926 color_for_ansi256_index("ansi-color-255"),
927 Some(Color::AnsiValue(255))
928 );
929 assert_eq!(color_for_ansi256_index("ansi-color-256"), None);
930
931 assert_eq!(color_for_ansi256_index("ansi-color-00"), None);
932 assert_eq!(color_for_ansi256_index("ansi-color-010"), None);
933 assert_eq!(color_for_ansi256_index("ansi-color-0255"), None);
934 }
935
936 #[test]
937 fn test_color_for_hex() {
938 assert_eq!(
939 color_for_hex("#000000"),
940 Some(Color::Rgb { r: 0, g: 0, b: 0 })
941 );
942 assert_eq!(
943 color_for_hex("#fab123"),
944 Some(Color::Rgb {
945 r: 0xfa,
946 g: 0xb1,
947 b: 0x23
948 })
949 );
950 assert_eq!(
951 color_for_hex("#F00D13"),
952 Some(Color::Rgb {
953 r: 0xf0,
954 g: 0x0d,
955 b: 0x13
956 })
957 );
958 assert_eq!(
959 color_for_hex("#ffffff"),
960 Some(Color::Rgb {
961 r: 255,
962 g: 255,
963 b: 255
964 })
965 );
966
967 assert_eq!(color_for_hex("000000"), None);
968 assert_eq!(color_for_hex("0000000"), None);
969 assert_eq!(color_for_hex("#00000g"), None);
970 assert_eq!(color_for_hex("#á00000"), None);
971 }
972
973 #[test]
974 fn test_color_formatter_ansi256() -> TestResult {
975 let config = config_from_string(
976 r#"
977 [colors]
978 purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
979 gray = "ansi-color-244"
980 "#,
981 );
982 let mut output: Vec<u8> = vec![];
983 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
984 formatter.push_label("purple-bg");
985 write!(formatter, " purple background ")?;
986 formatter.pop_label();
987 writeln!(formatter)?;
988 formatter.push_label("gray");
989 write!(formatter, " gray ")?;
990 formatter.pop_label();
991 writeln!(formatter)?;
992 drop(formatter);
993 insta::assert_snapshot!(to_snapshot_string(output), @"
994 [38;5;15m[48;5;93m purple background [39m[49m
995 [38;5;244m gray [39m
996 [EOF]
997 ");
998 Ok(())
999 }
1000
1001 #[test]
1002 fn test_color_formatter_hex_colors() -> TestResult {
1003 let config = config_from_string(indoc! {"
1005 [colors]
1006 black = '#000000'
1007 white = '#ffffff'
1008 pastel-blue = '#AFE0D9'
1009 "});
1010 let colors: IndexMap<String, String> = config.get("colors")?;
1011 let mut output: Vec<u8> = vec![];
1012 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1013 for label in colors.keys() {
1014 formatter.push_label(&label.replace(' ', "-"));
1015 write!(formatter, " {label} ")?;
1016 formatter.pop_label();
1017 writeln!(formatter)?;
1018 }
1019 drop(formatter);
1020 insta::assert_snapshot!(to_snapshot_string(output), @"
1021 [38;2;0;0;0m black [39m
1022 [38;2;255;255;255m white [39m
1023 [38;2;175;224;217m pastel-blue [39m
1024 [EOF]
1025 ");
1026 Ok(())
1027 }
1028
1029 #[test]
1030 fn test_color_formatter_single_label() -> TestResult {
1031 let config = config_from_string(
1034 r#"
1035 colors.inside = "green"
1036 "#,
1037 );
1038 let mut output: Vec<u8> = vec![];
1039 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1040 write!(formatter, " before ")?;
1041 formatter.push_label("inside");
1042 write!(formatter, " inside ")?;
1043 formatter.pop_label();
1044 write!(formatter, " after ")?;
1045 drop(formatter);
1046 insta::assert_snapshot!(
1047 to_snapshot_string(output), @" before [38;5;2m inside [39m after [EOF]");
1048 Ok(())
1049 }
1050
1051 #[test]
1052 fn test_color_formatter_attributes() -> TestResult {
1053 let config = config_from_string(
1056 r#"
1057 colors.red_fg = { fg = "red" }
1058 colors.blue_bg = { bg = "blue" }
1059 colors.bold_font = { bold = true }
1060 colors.dim_font = { dim = true }
1061 colors.italic_text = { italic = true }
1062 colors.underlined_text = { underline = true }
1063 colors.crossed_out_text = { crossed-out = true }
1064 colors.reversed_colors = { reverse = true }
1065 colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, crossed-out = true, reverse = true }
1066 "#,
1067 );
1068 let mut output: Vec<u8> = vec![];
1069 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1070 formatter.push_label("red_fg");
1071 write!(formatter, " fg only ")?;
1072 formatter.pop_label();
1073 writeln!(formatter)?;
1074 formatter.push_label("blue_bg");
1075 write!(formatter, " bg only ")?;
1076 formatter.pop_label();
1077 writeln!(formatter)?;
1078 formatter.push_label("bold_font");
1079 write!(formatter, " bold only ")?;
1080 formatter.pop_label();
1081 writeln!(formatter)?;
1082 formatter.push_label("dim_font");
1083 write!(formatter, " dim only ")?;
1084 formatter.pop_label();
1085 writeln!(formatter)?;
1086 formatter.push_label("italic_text");
1087 write!(formatter, " italic only ")?;
1088 formatter.pop_label();
1089 writeln!(formatter)?;
1090 formatter.push_label("underlined_text");
1091 write!(formatter, " underlined only ")?;
1092 formatter.pop_label();
1093 writeln!(formatter)?;
1094 formatter.push_label("crossed_out_text");
1095 write!(formatter, " crossed-out only ")?;
1096 formatter.pop_label();
1097 writeln!(formatter)?;
1098 formatter.push_label("reversed_colors");
1099 write!(formatter, " reverse only ")?;
1100 formatter.pop_label();
1101 writeln!(formatter)?;
1102 formatter.push_label("multiple");
1103 write!(formatter, " single rule ")?;
1104 formatter.pop_label();
1105 writeln!(formatter)?;
1106 formatter.push_label("red_fg");
1107 formatter.push_label("blue_bg");
1108 write!(formatter, " two rules ")?;
1109 formatter.pop_label();
1110 formatter.pop_label();
1111 writeln!(formatter)?;
1112 drop(formatter);
1113 insta::assert_snapshot!(to_snapshot_string(output), @"
1114 [38;5;1m fg only [39m
1115 [48;5;4m bg only [49m
1116 [1m bold only [0m
1117 [2m dim only [0m
1118 [3m italic only [23m
1119 [4m underlined only [24m
1120 [9m crossed-out only [29m
1121 [7m reverse only [27m
1122 [1m[3m[4m[9m[7m[38;5;2m[48;5;3m single rule [0m
1123 [38;5;1m[48;5;4m two rules [39m[49m
1124 [EOF]
1125 ");
1126 Ok(())
1127 }
1128
1129 #[test]
1130 fn test_color_formatter_bold_reset() -> TestResult {
1131 let config = config_from_string(indoc! {"
1133 [colors]
1134 not_bold = { fg = 'red', bg = 'blue', italic = true, underline = true }
1135 bold_font = { bold = true }
1136 stop_bold = { bold = false }
1137 "});
1138 let mut output: Vec<u8> = vec![];
1139 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1140 formatter.push_label("not_bold");
1141 write!(formatter, " not bold ")?;
1142 formatter.push_label("bold_font");
1143 write!(formatter, " bold ")?;
1144 formatter.push_label("stop_bold");
1145 write!(formatter, " stop bold ")?;
1146 formatter.pop_label();
1147 write!(formatter, " bold again ")?;
1148 formatter.pop_label();
1149 write!(formatter, " not bold again ")?;
1150 formatter.pop_label();
1151 drop(formatter);
1152 insta::assert_snapshot!(
1153 to_snapshot_string(output),
1154 @"[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]");
1155 Ok(())
1156 }
1157
1158 #[test]
1159 fn test_color_formatter_dim_reset() -> TestResult {
1160 let config = config_from_string(indoc! {"
1162 [colors]
1163 not_dim = { fg = 'red', bg = 'blue', italic = true, underline = true }
1164 dim_font = { dim = true }
1165 stop_dim = { dim = false }
1166 "});
1167 let mut output: Vec<u8> = vec![];
1168 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1169 formatter.push_label("not_dim");
1170 write!(formatter, " not dim ")?;
1171 formatter.push_label("dim_font");
1172 write!(formatter, " dim ")?;
1173 formatter.push_label("stop_dim");
1174 write!(formatter, " stop dim ")?;
1175 formatter.pop_label();
1176 write!(formatter, " dim again ")?;
1177 formatter.pop_label();
1178 write!(formatter, " not dim again ")?;
1179 formatter.pop_label();
1180 drop(formatter);
1181 insta::assert_snapshot!(
1182 to_snapshot_string(output),
1183 @"[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]");
1184 Ok(())
1185 }
1186
1187 #[test]
1188 fn test_color_formatter_bold_to_dim() -> TestResult {
1189 let config = config_from_string(indoc! {"
1191 [colors]
1192 bold_font = { bold = true }
1193 dim_font = { dim = true }
1194 "});
1195 let mut output: Vec<u8> = vec![];
1196 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1197 formatter.push_label("bold_font");
1198 write!(formatter, " bold ")?;
1199 formatter.push_label("dim_font");
1200 write!(formatter, " bold&dim ")?;
1201 formatter.pop_label();
1202 write!(formatter, " bold again ")?;
1203 formatter.pop_label();
1204 drop(formatter);
1205 insta::assert_snapshot!(
1206 to_snapshot_string(output),
1207 @"[1m bold [2m bold&dim [0m[1m bold again [0m[EOF]");
1208 Ok(())
1209 }
1210
1211 #[test]
1212 fn test_formatter_reset_on_flush() -> TestResult {
1213 let config = config_from_string("colors.red = 'red'");
1214 let mut output: Vec<u8> = vec![];
1215 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1216 formatter.push_label("red");
1217 write!(formatter, "foo")?;
1218 formatter.pop_label();
1219
1220 insta::assert_snapshot!(
1222 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[EOF]");
1223
1224 formatter.flush()?;
1226 insta::assert_snapshot!(
1227 to_snapshot_string(formatter.output.clone()), @"[38;5;1mfoo[39m[EOF]");
1228
1229 formatter.push_label("red");
1231 write!(formatter, "bar")?;
1232 formatter.pop_label();
1233
1234 drop(formatter);
1236 insta::assert_snapshot!(
1237 to_snapshot_string(output), @"[38;5;1mfoo[39m[38;5;1mbar[39m[EOF]");
1238
1239 let mut output: Vec<u8> = vec![];
1241 let mut formatter = PlainTextFormatter::new(&mut output);
1242 formatter.push_label("red");
1243 write!(formatter, "foo")?;
1244 formatter.pop_label();
1245 formatter.flush()?;
1246 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1247
1248 let mut output: Vec<u8> = vec![];
1249 let mut formatter = SanitizingFormatter::new(&mut output);
1250 formatter.push_label("red");
1251 write!(formatter, "foo")?;
1252 formatter.pop_label();
1253 formatter.flush()?;
1254 insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1255 Ok(())
1256 }
1257
1258 #[test]
1259 fn test_color_formatter_no_space() -> TestResult {
1260 let config = config_from_string(
1262 r#"
1263 colors.red = "red"
1264 colors.green = "green"
1265 "#,
1266 );
1267 let mut output: Vec<u8> = vec![];
1268 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1269 write!(formatter, "before")?;
1270 formatter.push_label("red");
1271 write!(formatter, "first")?;
1272 formatter.pop_label();
1273 formatter.push_label("green");
1274 write!(formatter, "second")?;
1275 formatter.pop_label();
1276 write!(formatter, "after")?;
1277 drop(formatter);
1278 insta::assert_snapshot!(
1279 to_snapshot_string(output), @"before[38;5;1mfirst[38;5;2msecond[39mafter[EOF]");
1280 Ok(())
1281 }
1282
1283 #[test]
1284 fn test_color_formatter_ansi_codes_in_text() -> TestResult {
1285 let config = config_from_string(
1287 r#"
1288 colors.red = "red"
1289 "#,
1290 );
1291 let mut output: Vec<u8> = vec![];
1292 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1293 formatter.push_label("red");
1294 write!(formatter, "\x1b[1mnot actually bold\x1b[0m")?;
1295 formatter.pop_label();
1296 drop(formatter);
1297 insta::assert_snapshot!(
1298 to_snapshot_string(output), @"[38;5;1m␛[1mnot actually bold␛[0m[39m[EOF]");
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn test_color_formatter_nested() -> TestResult {
1304 let config = config_from_string(
1308 r#"
1309 colors.outer = "blue"
1310 colors.inner = "red"
1311 colors."outer inner" = "green"
1312 "#,
1313 );
1314 let mut output: Vec<u8> = vec![];
1315 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1316 write!(formatter, " before outer ")?;
1317 formatter.push_label("outer");
1318 write!(formatter, " before inner ")?;
1319 formatter.push_label("inner");
1320 write!(formatter, " inside inner ")?;
1321 formatter.pop_label();
1322 write!(formatter, " after inner ")?;
1323 formatter.pop_label();
1324 write!(formatter, " after outer ")?;
1325 drop(formatter);
1326 insta::assert_snapshot!(
1327 to_snapshot_string(output),
1328 @" before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer [EOF]");
1329 Ok(())
1330 }
1331
1332 #[test]
1333 fn test_color_formatter_partial_match() -> TestResult {
1334 let config = config_from_string(
1336 r#"
1337 colors."outer inner" = "green"
1338 "#,
1339 );
1340 let mut output: Vec<u8> = vec![];
1341 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1342 formatter.push_label("outer");
1343 write!(formatter, " not colored ")?;
1344 formatter.push_label("inner");
1345 write!(formatter, " colored ")?;
1346 formatter.pop_label();
1347 write!(formatter, " not colored ")?;
1348 formatter.pop_label();
1349 drop(formatter);
1350 insta::assert_snapshot!(
1351 to_snapshot_string(output),
1352 @" not colored [38;5;2m colored [39m not colored [EOF]");
1353 Ok(())
1354 }
1355
1356 #[test]
1357 fn test_color_formatter_unrecognized_color() {
1358 let config = config_from_string(
1360 r#"
1361 colors."outer" = "red"
1362 colors."outer inner" = "bloo"
1363 "#,
1364 );
1365 let mut output: Vec<u8> = vec![];
1366 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1367 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1368 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1369 }
1370
1371 #[test]
1372 fn test_color_formatter_unrecognized_ansi256_color() {
1373 let config = config_from_string(
1375 r##"
1376 colors."outer" = "red"
1377 colors."outer inner" = "ansi-color-256"
1378 "##,
1379 );
1380 let mut output: Vec<u8> = vec![];
1381 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1382 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1383 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1384 }
1385
1386 #[test]
1387 fn test_color_formatter_unrecognized_hex_color() {
1388 let config = config_from_string(
1390 r##"
1391 colors."outer" = "red"
1392 colors."outer inner" = "#ffgggg"
1393 "##,
1394 );
1395 let mut output: Vec<u8> = vec![];
1396 let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1397 insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1398 insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1399 }
1400
1401 #[test]
1402 fn test_color_formatter_invalid_type_of_color() {
1403 let config = config_from_string("colors.foo = []");
1404 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1405 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1406 insta::assert_snapshot!(
1407 err.source().unwrap(),
1408 @"invalid type: array, expected a color name or a table of styles");
1409 }
1410
1411 #[test]
1412 fn test_color_formatter_invalid_type_of_style() {
1413 let config = config_from_string("colors.foo = { bold = 1 }");
1414 let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1415 insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1416 insta::assert_snapshot!(err.source().unwrap(), @"
1417 invalid type: integer `1`, expected a boolean
1418 in `bold`
1419 ");
1420 }
1421
1422 #[test]
1423 fn test_color_formatter_normal_color() -> TestResult {
1424 let config = config_from_string(
1427 r#"
1428 colors."outer" = {bg="yellow", fg="blue"}
1429 colors."outer default_fg" = "default"
1430 colors."outer default_bg" = {bg = "default"}
1431 "#,
1432 );
1433 let mut output: Vec<u8> = vec![];
1434 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1435 formatter.push_label("outer");
1436 write!(formatter, "Blue on yellow, ")?;
1437 formatter.push_label("default_fg");
1438 write!(formatter, " default fg, ")?;
1439 formatter.pop_label();
1440 write!(formatter, " and back.\nBlue on yellow, ")?;
1441 formatter.push_label("default_bg");
1442 write!(formatter, " default bg, ")?;
1443 formatter.pop_label();
1444 write!(formatter, " and back.")?;
1445 drop(formatter);
1446 insta::assert_snapshot!(to_snapshot_string(output), @"
1447 [38;5;4m[48;5;3mBlue on yellow, [39m default fg, [38;5;4m and back.[39m[49m
1448 [38;5;4m[48;5;3mBlue on yellow, [49m default bg, [48;5;3m and back.[39m[49m[EOF]
1449 ");
1450 Ok(())
1451 }
1452
1453 #[test]
1454 fn test_color_formatter_sibling() -> TestResult {
1455 let config = config_from_string(
1457 r#"
1458 colors."outer1 inner1" = "red"
1459 colors.inner2 = "green"
1460 "#,
1461 );
1462 let mut output: Vec<u8> = vec![];
1463 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1464 formatter.push_label("outer1");
1465 formatter.push_label("inner2");
1466 write!(formatter, " hello ")?;
1467 formatter.pop_label();
1468 formatter.pop_label();
1469 drop(formatter);
1470 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m hello [39m[EOF]");
1471 Ok(())
1472 }
1473
1474 #[test]
1475 fn test_color_formatter_reverse_order() -> TestResult {
1476 let config = config_from_string(
1478 r#"
1479 colors."inner outer" = "green"
1480 "#,
1481 );
1482 let mut output: Vec<u8> = vec![];
1483 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1484 formatter.push_label("outer");
1485 formatter.push_label("inner");
1486 write!(formatter, " hello ")?;
1487 formatter.pop_label();
1488 formatter.pop_label();
1489 drop(formatter);
1490 insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1491 Ok(())
1492 }
1493
1494 #[test]
1495 fn test_color_formatter_innermost_wins() -> TestResult {
1496 let config = config_from_string(
1498 r#"
1499 colors."a" = "red"
1500 colors."b" = "green"
1501 colors."a c" = "blue"
1502 colors."b c" = "yellow"
1503 "#,
1504 );
1505 let mut output: Vec<u8> = vec![];
1506 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1507 formatter.push_label("a");
1508 write!(formatter, " a1 ")?;
1509 formatter.push_label("b");
1510 write!(formatter, " b1 ")?;
1511 formatter.push_label("c");
1512 write!(formatter, " c ")?;
1513 formatter.pop_label();
1514 write!(formatter, " b2 ")?;
1515 formatter.pop_label();
1516 write!(formatter, " a2 ")?;
1517 formatter.pop_label();
1518 drop(formatter);
1519 insta::assert_snapshot!(
1520 to_snapshot_string(output),
1521 @"[38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m[EOF]");
1522 Ok(())
1523 }
1524
1525 #[test]
1526 fn test_color_formatter_dropped() -> TestResult {
1527 let config = config_from_string(
1530 r#"
1531 colors.outer = "green"
1532 "#,
1533 );
1534 let mut output: Vec<u8> = vec![];
1535 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1536 formatter.push_label("outer");
1537 formatter.push_label("inner");
1538 write!(formatter, " inside ")?;
1539 drop(formatter);
1540 insta::assert_snapshot!(to_snapshot_string(output), @"[38;5;2m inside [39m[EOF]");
1541 Ok(())
1542 }
1543
1544 #[test]
1545 fn test_color_formatter_debug() -> TestResult {
1546 let config = config_from_string(
1549 r#"
1550 colors.outer = "green"
1551 "#,
1552 );
1553 let mut output: Vec<u8> = vec![];
1554 let mut formatter = ColorFormatter::for_config(&mut output, &config, true)?;
1555 formatter.push_label("outer");
1556 formatter.push_label("inner");
1557 write!(formatter, " inside ")?;
1558 formatter.pop_label();
1559 formatter.pop_label();
1560 formatter.push_label("outer");
1562 formatter.push_label("inner");
1563 write!(formatter, " inside two ")?;
1564 formatter.pop_label();
1565 formatter.pop_label();
1566 drop(formatter);
1567 insta::assert_snapshot!(
1568 to_snapshot_string(output),
1569 @"[38;5;2m<<outer inner:: inside inside two >>[39m[EOF]",
1570 );
1571 Ok(())
1572 }
1573
1574 #[test]
1575 fn test_labeled_scope() -> TestResult {
1576 let config = config_from_string(indoc! {"
1577 [colors]
1578 outer = 'blue'
1579 inner = 'red'
1580 'outer inner' = 'green'
1581 "});
1582 let mut output: Vec<u8> = vec![];
1583 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1584 writeln!(formatter.labeled("outer"), "outer")?;
1585 writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner")?;
1586 writeln!(formatter.labeled("inner"), "inner")?;
1587 drop(formatter);
1588 insta::assert_snapshot!(to_snapshot_string(output), @"
1589 [38;5;4mouter[39m
1590 [38;5;2mouter-inner[39m
1591 [38;5;1minner[39m
1592 [EOF]
1593 ");
1594 Ok(())
1595 }
1596
1597 #[test]
1598 fn test_heading_labeled_writer() -> TestResult {
1599 let config = config_from_string(
1600 r#"
1601 colors.inner = "green"
1602 colors."inner heading" = "red"
1603 "#,
1604 );
1605 let mut output: Vec<u8> = vec![];
1606 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1607 formatter.labeled("inner").with_heading("Should be noop: ");
1608 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1609 write!(writer, "Message")?;
1610 writeln!(writer, " continues")?;
1611 drop(writer);
1612 drop(formatter);
1613 insta::assert_snapshot!(to_snapshot_string(output), @"
1614 [38;5;1mHeading: [38;5;2mMessage continues[39m
1615 [EOF]
1616 ");
1617 Ok(())
1618 }
1619
1620 #[test]
1621 fn test_heading_labeled_writer_empty_string() -> TestResult {
1622 let mut output: Vec<u8> = vec![];
1623 let mut formatter = PlainTextFormatter::new(&mut output);
1624 let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1625 write!(writer, "")?;
1628 write!(writer, "")?;
1629 drop(writer);
1630 insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1631 Ok(())
1632 }
1633
1634 #[test]
1635 fn test_format_recorder() -> TestResult {
1636 let mut recorder = FormatRecorder::new(false);
1637 write!(recorder, " outer1 ")?;
1638 recorder.push_label("inner");
1639 write!(recorder, " inner1 ")?;
1640 write!(recorder, " inner2 ")?;
1641 recorder.pop_label();
1642 write!(recorder, " outer2 ")?;
1643
1644 insta::assert_snapshot!(
1645 to_snapshot_string(recorder.data()),
1646 @" outer1 inner1 inner2 outer2 [EOF]");
1647
1648 let config = config_from_string(r#" colors.inner = "red" "#);
1650 let mut output: Vec<u8> = vec![];
1651 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1652 recorder.replay(&mut formatter)?;
1653 drop(formatter);
1654 insta::assert_snapshot!(
1655 to_snapshot_string(output),
1656 @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1657
1658 let mut output: Vec<u8> = vec![];
1660 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1661 recorder.replay_with(&mut formatter, |formatter, range| {
1662 let data = &recorder.data()[range];
1663 write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1664 })?;
1665 drop(formatter);
1666 insta::assert_snapshot!(
1667 to_snapshot_string(output),
1668 @"<< outer1 >>[38;5;1m<< inner1 inner2 >>[39m<< outer2 >>[EOF]");
1669 Ok(())
1670 }
1671
1672 #[test]
1673 fn test_raw_format_recorder() -> TestResult {
1674 let mut recorder = FormatRecorder::new(false);
1676 write!(recorder.raw()?, " outer1 ")?;
1677 recorder.push_label("inner");
1678 write!(recorder.raw()?, " inner1 ")?;
1679 write!(recorder.raw()?, " inner2 ")?;
1680 recorder.pop_label();
1681 write!(recorder.raw()?, " outer2 ")?;
1682
1683 let config = config_from_string(r#" colors.inner = "red" "#);
1685 let mut output: Vec<u8> = vec![];
1686 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1687 recorder.replay(&mut formatter)?;
1688 drop(formatter);
1689 insta::assert_snapshot!(
1690 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1691
1692 let mut output: Vec<u8> = vec![];
1693 let mut formatter = ColorFormatter::for_config(&mut output, &config, false)?;
1694 recorder.replay_with(&mut formatter, |_formatter, range| {
1695 panic!(
1696 "Called with {:?} when all output should be raw",
1697 str::from_utf8(&recorder.data()[range]).unwrap()
1698 );
1699 })?;
1700 drop(formatter);
1701 insta::assert_snapshot!(
1702 to_snapshot_string(output), @" outer1 [38;5;1m inner1 inner2 [39m outer2 [EOF]");
1703 Ok(())
1704 }
1705}