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