1use std::borrow::Cow;
6use std::io::{self, Write};
7
8use termcolor::{ColorSpec, HyperlinkSpec, WriteColor};
9
10pub struct Escaped<W: Write> {
38 inner: W,
39 on: bool,
40 json: bool,
41 pending: Option<u8>,
42}
43
44impl<W: Write> Escaped<W> {
45 pub fn new(inner: W, on: bool) -> Self {
46 Self { inner, on, json: false, pending: None }
47 }
48
49 pub fn into_json(mut self) -> Self {
51 self.json = true;
52 self
53 }
54
55 pub fn json(inner: W, on: bool) -> Self {
56 Self { inner, on, json: true, pending: None }
57 }
58
59 fn emit(&mut self, value: u8, c1: bool) -> io::Result<()> {
62 match (self.json, c1) {
63 (true, _) => write!(self.inner, "\\u{value:04x}"),
64 (false, true) => write!(self.inner, "\\u{{{value:x}}}"),
65 (false, false) => write!(self.inner, "\\x{value:02x}"),
66 }
67 }
68
69 fn settle(&mut self) -> io::Result<()> {
72 match self.pending.take() {
73 Some(b'\r') => self.emit(b'\r', false),
74 Some(b) => self.inner.write_all(&[b]),
75 None => Ok(()),
76 }
77 }
78}
79
80impl<W: Write> Write for Escaped<W> {
81 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
82 if !self.on {
83 self.inner.write_all(buf)?;
84 return Ok(buf.len());
85 }
86 if buf.is_empty() {
87 return Ok(0);
88 }
89
90 let mut i = 0;
91 match (self.pending, buf[0]) {
92 (Some(b'\r'), b'\n') => {
93 self.pending = None;
94 self.inner.write_all(b"\r")?;
95 }
96 (Some(0xC2), n @ 0x80..=0x9F) => {
97 self.pending = None;
98 self.emit(n, true)?;
99 i = 1;
100 }
101 _ => self.settle()?,
102 }
103
104 let mut run = i;
109 while i < buf.len() {
110 match buf[i..].iter().position(|&b| needs_look(b)) {
111 Some(n) => i += n,
112 None => break,
113 }
114 let b = buf[i];
115 let next = buf.get(i + 1).copied();
116 let width = match (b, next) {
117 (b'\t' | b'\n', _) => 0,
118 (b'\r', Some(b'\n')) => 0,
119 (b'\r' | 0xC2, None) => {
120 self.inner.write_all(&buf[run..i])?;
121 self.pending = Some(b);
122 return Ok(buf.len());
123 }
124 (0x00..=0x1F | 0x7F, _) => 1,
125 (0xC2, Some(0x80..=0x9F)) => 2,
126 _ => 0,
127 };
128 if width == 0 {
129 i += 1;
130 continue;
131 }
132 self.inner.write_all(&buf[run..i])?;
133 if width == 1 {
134 self.emit(b, false)?;
135 } else {
136 self.emit(buf[i + 1], true)?;
137 }
138 i += width;
139 run = i;
140 }
141 self.inner.write_all(&buf[run..])?;
142 Ok(buf.len())
143 }
144
145 fn flush(&mut self) -> io::Result<()> {
146 self.settle()?;
147 self.inner.flush()
148 }
149}
150
151fn needs_look(b: u8) -> bool {
154 (b < 0x20 && b != b'\t' && b != b'\n') || b == 0x7F || b == 0xC2
155}
156
157impl<W: Write> Drop for Escaped<W> {
158 fn drop(&mut self) {
159 let _ = self.settle();
162 }
163}
164
165impl<W: WriteColor> WriteColor for Escaped<W> {
166 fn supports_color(&self) -> bool {
167 self.inner.supports_color()
168 }
169 fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
170 self.inner.set_color(spec)
171 }
172 fn reset(&mut self) -> io::Result<()> {
173 self.inner.reset()
174 }
175 fn is_synchronous(&self) -> bool {
176 self.inner.is_synchronous()
177 }
178 fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
179 self.inner.set_hyperlink(link)
180 }
181 fn supports_hyperlinks(&self) -> bool {
182 self.inner.supports_hyperlinks()
183 }
184}
185
186static STDERR: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
187
188pub fn set_stderr(on: bool) {
192 let _ = STDERR.set(on);
193}
194
195pub fn for_stderr(s: &str) -> Cow<'_, str> {
198 let on = *STDERR.get_or_init(|| io::IsTerminal::is_terminal(&io::stderr()));
199 text(s, on)
200}
201
202pub fn text(s: &str, on: bool) -> Cow<'_, str> {
205 let clean = !s.bytes().any(|b| b < 0x20 && b != b'\t' && b != b'\n' || b == 0x7F)
206 && !s.chars().any(|c| ('\u{80}'..='\u{9f}').contains(&c));
207 if !on || clean {
208 return Cow::Borrowed(s);
209 }
210 let mut out = Vec::with_capacity(s.len() + 16);
211 {
212 let mut w = Escaped::new(&mut out, true);
213 let _ = w.write_all(s.as_bytes());
215 }
216 Cow::Owned(String::from_utf8(out).expect("escaping keeps UTF-8 valid"))
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 fn esc(chunks: &[&[u8]]) -> Vec<u8> {
226 let mut out = Vec::new();
227 {
228 let mut w = Escaped::new(&mut out, true);
229 for c in chunks {
230 w.write_all(c).unwrap();
231 }
232 }
233 out
234 }
235
236 #[test]
237 fn esc_and_bel_become_visible() {
238 assert_eq!(esc(&[b"a\x1b]0;t\x07b"]), br"a\x1b]0;t\x07b");
239 }
240
241 #[test]
242 fn newline_and_tab_pass_through() {
243 assert_eq!(esc(&[b"a\tb\n"]), b"a\tb\n");
244 }
245
246 #[test]
247 fn del_and_nul_are_escaped() {
248 assert_eq!(esc(&[b"\x7f\x00"]), br"\x7f\x00");
249 }
250
251 #[test]
252 fn crlf_split_across_writes_is_kept() {
253 assert_eq!(esc(&[b"line\r", b"\n"]), b"line\r\n");
254 }
255
256 #[test]
257 fn a_trailing_cr_at_the_very_end_is_escaped() {
258 assert_eq!(esc(&[b"line\r"]), br"line\x0d");
259 }
260
261 #[test]
262 fn a_c1_split_across_writes_is_escaped() {
263 assert_eq!(esc(&[b"a\xc2", b"\x9bb"]), br"a\u{9b}b");
264 }
265
266 #[test]
267 fn ordinary_utf8_split_across_writes_is_untouched() {
268 assert_eq!(esc(&["é".as_bytes(), b"\xc2", b"\xa0"]), "é\u{a0}".as_bytes());
270 }
271
272 #[test]
273 fn off_is_byte_exact() {
274 let mut out = Vec::new();
275 Escaped::new(&mut out, false).write_all(b"\x1b[2J\r").unwrap();
276 assert_eq!(out, b"\x1b[2J\r");
277 }
278
279 #[test]
281 fn crlf_survives_a_colour_reset_between_its_halves() {
282 let mut buf = termcolor::Buffer::ansi();
283 {
284 let mut w = Escaped::new(&mut buf, true);
285 w.set_color(ColorSpec::new().set_fg(Some(termcolor::Color::Red))).unwrap();
286 w.write_all(b"crlf\r").unwrap();
287 w.reset().unwrap();
288 w.write_all(b"\n").unwrap();
289 }
290 let out = buf.into_inner();
291 assert!(!out.windows(4).any(|w| w == br"\x0d"), "CRLF escaped: {out:?}");
292 assert!(out.ends_with(b"\r\n"), "got: {out:?}");
293 }
294
295 #[test]
298 fn json_style_uses_json_escapes() {
299 let mut out = Vec::new();
300 {
301 let mut w = Escaped::json(&mut out, true);
302 w.write_all("{\"text\":\"a\u{7f}b\u{9b}c\"}\n".as_bytes()).unwrap();
303 }
304 assert_eq!(out, br#"{"text":"a\u007fb\u009bc"}"#.iter().copied().chain(*b"\n").collect::<Vec<_>>());
305 }
306
307 #[test]
308 fn text_escapes_a_str() {
309 assert_eq!(text("a\x1b[31mb", true), r"a\x1b[31mb");
310 assert_eq!(text("a\x1b[31mb", false), "a\x1b[31mb");
311 }
312}