xpct/core/format/formatter_color.rs
1#![cfg(feature = "color")]
2
3use std::fmt;
4use std::io::{self, IsTerminal};
5
6use super::strings::{indent_segments, OutputSegment};
7use super::{Format, OutputStyle};
8
9// Disable colors and text styles if stderr is not a tty.
10fn check_disable_color() {
11 if !io::stderr().is_terminal() {
12 colored::control::set_override(false)
13 }
14}
15
16/// Configuration for formatting with [`Format`].
17///
18/// This value is passed to [`Format::fmt`] and is used to set various options to configure how the
19/// value will be formatted.
20///
21/// [`Format::fmt`]: crate::core::Format::fmt
22#[derive(Debug)]
23pub struct Formatter {
24 prev: Vec<OutputSegment>,
25 current: OutputSegment,
26}
27
28impl Formatter {
29 fn new() -> Self {
30 Self {
31 prev: Vec::new(),
32 current: Default::default(),
33 }
34 }
35
36 fn into_segments(self) -> Vec<OutputSegment> {
37 let mut segments = self.prev;
38 segments.push(self.current);
39 segments
40 }
41
42 fn push_segments(&mut self, segments: Vec<OutputSegment>) {
43 let new_current = OutputSegment {
44 buf: String::new(),
45 style: self.current.style.clone(),
46 };
47
48 self.prev
49 .push(std::mem::replace(&mut self.current, new_current));
50 self.prev.extend(segments);
51 }
52
53 /// Write a string to the output.
54 pub fn write_str(&mut self, s: impl AsRef<str>) {
55 self.current.buf.push_str(s.as_ref());
56 }
57
58 /// Write a `char` to the output.
59 pub fn write_char(&mut self, c: char) {
60 self.current.buf.push(c);
61 }
62
63 /// Pass some pre-formatted output through to the output.
64 ///
65 /// This method is often used when writing formatters for matchers which compose other
66 /// matchers, such as [`not`] or [`each`]. It's used by formatters such as [`FailureFormat`]
67 /// and [`SomeFailuresFormat`].
68 ///
69 /// [`not`]: crate::not
70 /// [`each`]: crate::each
71 /// [`FailureFormat`]: crate::format::FailureFormat
72 /// [`SomeFailuresFormat`]: crate::format::SomeFailuresFormat
73 pub fn write_fmt(&mut self, output: impl Into<FormattedOutput>) {
74 let formatted = output.into();
75 self.push_segments(formatted.segments)
76 }
77
78 fn indented_inner(
79 &mut self,
80 prefix: impl AsRef<str>,
81 hanging: bool,
82 func: impl FnOnce(&mut Formatter) -> crate::Result<()>,
83 ) -> crate::Result<()> {
84 let mut formatter = Self::new();
85 formatter.current.style = self.current.style.clone();
86
87 func(&mut formatter)?;
88
89 let segments = formatter.into_segments();
90 let output = FormattedOutput { segments };
91
92 let indented = output.indented_inner(prefix.as_ref(), hanging);
93 self.push_segments(indented.segments);
94
95 Ok(())
96 }
97
98 /// Write some indented text to the output.
99 ///
100 /// Anything written to the [`Formatter`] passed to `func` is indented by `prefix`.
101 ///
102 /// To indent with whitespace, you can use [`whitespace`].
103 ///
104 /// The current [`style`] is inherited by the [`Formatter`] passed to `func`.
105 ///
106 /// [`whitespace`]: crate::core::whitespace
107 /// [`style`]: crate::core::Formatter::style
108 pub fn indented(
109 &mut self,
110 prefix: impl AsRef<str>,
111 func: impl FnOnce(&mut Formatter) -> crate::Result<()>,
112 ) -> crate::Result<()> {
113 self.indented_inner(prefix, false, func)
114 }
115
116 /// Write some indented text to the output with a hanging indent.
117 ///
118 /// This is the same as [`indented`], but generates a hanging indent.
119 ///
120 /// [`indented`]: crate::core::Formatter::indented
121 pub fn indented_hanging(
122 &mut self,
123 prefix: impl AsRef<str>,
124 func: impl FnOnce(&mut Formatter) -> crate::Result<()>,
125 ) -> crate::Result<()> {
126 self.indented_inner(prefix, true, func)
127 }
128
129 /// Get the current [`OutputStyle`].
130 pub fn style(&self) -> &OutputStyle {
131 &self.current.style
132 }
133
134 /// Set the current [`OutputStyle`].
135 ///
136 /// This is used to configure colors and text styles in the output. Output formatting is
137 /// stripped out when stderr is not a tty.
138 pub fn set_style(&mut self, style: OutputStyle) {
139 self.prev.push(std::mem::replace(
140 &mut self.current,
141 OutputSegment {
142 buf: String::new(),
143 style,
144 },
145 ));
146 }
147
148 /// Reset the current colors and text styles to their defaults.
149 pub fn reset_style(&mut self) {
150 self.set_style(Default::default());
151 }
152}
153
154/// A value that has been formatted with [`Format`].
155///
156/// Formatting a value with [`Format`] returns this opaque type rather than a string, since we need
157/// to encapsulate the colors and text styles information in a cross-platform way. While ANSI escape
158/// codes can be included in a string, other platforms (such as Windows) have their own mechanisms
159/// for including colors and text styles in stdout/stderr.
160#[derive(Debug)]
161pub struct FormattedOutput {
162 segments: Vec<OutputSegment>,
163}
164
165impl FormattedOutput {
166 /// Create a new [`FormattedOutput`] by formatting `value` with `format`.
167 pub fn new<Value, Fmt>(value: Value, format: Fmt) -> crate::Result<Self>
168 where
169 Fmt: Format<Value = Value>,
170 {
171 let mut formatter = Formatter::new();
172 format.fmt(&mut formatter, value)?;
173 Ok(Self {
174 segments: formatter.into_segments(),
175 })
176 }
177
178 fn indented_inner(self, prefix: impl AsRef<str>, hanging: bool) -> Self {
179 if prefix.as_ref().is_empty() {
180 return self;
181 }
182
183 Self {
184 segments: indent_segments(self.segments, prefix.as_ref(), hanging),
185 }
186 }
187
188 /// Return a new [`FormattedOutput`] which has been indented by `prefix`.
189 ///
190 /// To indent with whitespace, you can use [`whitespace`].
191 ///
192 /// [`whitespace`]: crate::core::whitespace
193 pub fn indented(self, prefix: impl AsRef<str>) -> Self {
194 self.indented_inner(prefix, false)
195 }
196
197 /// Return a new [`FormattedOutput`] which has been indented by `prefix` with a hanging indent.
198 ///
199 /// This is the same as [`indented`], but generates a hanging indent.
200 ///
201 /// [`indented`]: crate::core::FormattedOutput::indented
202 pub fn indented_hanging(self, prefix: impl AsRef<str>) -> Self {
203 self.indented_inner(prefix, true)
204 }
205
206 /// Panic with this output as the error message.
207 ///
208 /// This does not print colors or text styles when the [`NO_COLOR`](https://no-color.org/)
209 /// environment variable is set or when stderr is not a tty.
210 pub fn fail(&self) -> ! {
211 // See CONTRIBUTING.md for an explanation of why we do this.
212 if cfg!(debug_screenshot) {
213 println!("{}", self);
214 std::process::exit(0);
215 } else {
216 check_disable_color();
217 panic!("\n{}\n", self);
218 }
219 }
220}
221
222impl fmt::Display for FormattedOutput {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 for segment in &self.segments {
225 f.write_fmt(format_args!("{}", segment.style.apply(&segment.buf)))?;
226 }
227
228 Ok(())
229 }
230}