1use core::fmt;
13use core::fmt::Write as _;
14
15use super::Format;
16use super::escape::{HumanEscaper, QuoteScan, value_needs_quotes, write_human_escaped};
17use crate::level::Level;
18use crate::record::Record;
19use crate::value::{Value, write_f64, write_hex};
20
21#[cfg_attr(all(feature = "human"), doc = "```")]
45#[cfg_attr(not(all(feature = "human")), doc = "```ignore")]
46#[derive(Debug, Clone, Copy)]
58pub struct HumanFormat {
59 show_target: bool,
60 show_source_location: bool,
61 color: bool,
62}
63
64impl Default for HumanFormat {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl HumanFormat {
71 #[must_use]
73 pub const fn new() -> Self {
74 Self {
75 show_target: true,
76 show_source_location: false,
77 color: false,
78 }
79 }
80
81 #[must_use]
84 pub const fn show_target(mut self, enabled: bool) -> Self {
85 self.show_target = enabled;
86 self
87 }
88
89 #[must_use]
92 pub const fn show_source_location(mut self, enabled: bool) -> Self {
93 self.show_source_location = enabled;
94 self
95 }
96
97 #[must_use]
102 pub const fn color(mut self, enabled: bool) -> Self {
103 self.color = enabled;
104 self
105 }
106
107 #[must_use]
109 pub const fn is_color(&self) -> bool {
110 self.color
111 }
112
113 #[inline(never)]
117 fn write_impl<W: fmt::Write + ?Sized>(&self, record: &Record<'_>, w: &mut W) -> fmt::Result {
118 if let Some(ts) = record.metadata.timestamp_unix_nanos {
119 super::rfc3339::write_rfc3339_nanos(ts, w)?;
120 w.write_char(' ')?;
121 }
122 let level = record.metadata.level;
123 if self.color {
124 w.write_str(level_color(level))?;
125 write!(w, "{:<5}", level.as_str_upper())?;
126 w.write_str(RESET)?;
127 w.write_char(' ')?;
128 } else {
129 write!(w, "{:<5} ", level.as_str_upper())?;
130 }
131 if self.show_target && !record.metadata.target.is_empty() {
132 if self.color {
133 w.write_str(DIM)?;
134 }
135 w.write_char('[')?;
136 write_human_escaped(w, record.metadata.target, false)?;
137 w.write_char(']')?;
138 if self.color {
139 w.write_str(RESET)?;
140 }
141 w.write_char(' ')?;
142 }
143 write_human_escaped(w, record.message, false)?;
144 for field in record.all_fields() {
145 w.write_char(' ')?;
146 super::escape::write_sanitized_key(w, field.key)?;
147 w.write_char('=')?;
148 write_value(w, field.value)?;
149 }
150 if self.show_source_location {
151 if let (Some(file), Some(line)) = (record.metadata.file, record.metadata.line) {
152 w.write_str(" (")?;
153 write_human_escaped(w, file, false)?;
154 write!(w, ":{line})")?;
155 }
156 }
157 w.write_char('\n')
158 }
159}
160
161impl Format for HumanFormat {
162 fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
163 self.write_impl(record, writer)
164 }
165
166 #[inline]
167 fn write_record_generic<W: fmt::Write>(
168 &self,
169 record: &Record<'_>,
170 writer: &mut W,
171 ) -> fmt::Result {
172 self.write_impl(record, writer)
173 }
174}
175
176const RESET: &str = "\x1b[0m";
177const DIM: &str = "\x1b[2m";
178
179const fn level_color(level: Level) -> &'static str {
180 match level {
181 Level::Error => "\x1b[31m",
182 Level::Warn => "\x1b[33m",
183 Level::Info => "\x1b[32m",
184 Level::Debug => "\x1b[34m",
185 Level::Trace => "\x1b[35m",
186 Level::Off => "",
187 }
188}
189
190#[inline]
191fn write_str_value<W: fmt::Write + ?Sized>(w: &mut W, s: &str) -> fmt::Result {
192 if value_needs_quotes(s) {
193 w.write_char('"')?;
194 write_human_escaped(w, s, true)?;
195 w.write_char('"')
196 } else {
197 w.write_str(s)
198 }
199}
200
201fn write_value<W: fmt::Write + ?Sized>(w: &mut W, v: Value<'_>) -> fmt::Result {
202 match v {
203 Value::Null => w.write_str("<null>"),
204 Value::Bool(b) => write!(w, "{b}"),
205 Value::I64(n) => write!(w, "{n}"),
206 Value::U64(n) => write!(w, "{n}"),
207 Value::I128(n) => write!(w, "{n}"),
208 Value::U128(n) => write!(w, "{n}"),
209 Value::F64(n) => write_f64(w, n),
210 Value::Str(s) => write_str_value(w, s),
211 Value::Char(c) => {
212 let mut buf = [0_u8; 4];
213 write_str_value(w, c.encode_utf8(&mut buf))
214 }
215 Value::Bytes([]) => w.write_str("\"\""),
216 Value::Bytes(b) => write_hex(w, b),
217 lazy @ (Value::Display(_) | Value::Debug(_) | Value::Error(_)) => {
218 let quoted = QuoteScan::needs_quotes(&lazy);
220 if quoted {
221 w.write_char('"')?;
222 }
223 write!(
224 HumanEscaper {
225 inner: &mut *w,
226 quoted,
227 },
228 "{lazy}"
229 )?;
230 if quoted {
231 w.write_char('"')?;
232 }
233 Ok(())
234 }
235 }
236}
237
238#[cfg_attr(all(feature = "human"), doc = "```")]
246#[cfg_attr(not(all(feature = "human")), doc = "```ignore")]
247#[cfg(feature = "std")]
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
256#[non_exhaustive]
257pub enum ColorChoice {
258 #[default]
274 Auto,
275 Always,
277 Never,
279}
280
281#[cfg(feature = "std")]
282impl ColorChoice {
283 #[must_use]
286 pub fn resolve(self, is_terminal: bool) -> bool {
287 self.resolve_with(is_terminal, cfg!(windows), |key| std::env::var_os(key))
288 }
289
290 #[must_use]
292 pub fn enabled_for_stdout(self) -> bool {
293 use std::io::IsTerminal;
294 self.resolve(std::io::stdout().is_terminal())
295 }
296
297 #[must_use]
299 pub fn enabled_for_stderr(self) -> bool {
300 use std::io::IsTerminal;
301 self.resolve(std::io::stderr().is_terminal())
302 }
303
304 fn resolve_with(
305 self,
306 is_terminal: bool,
307 windows: bool,
308 env: impl Fn(&str) -> Option<std::ffi::OsString>,
309 ) -> bool {
310 let set = |key: &str| env(key).is_some_and(|v| !v.is_empty());
311 match self {
312 Self::Always => true,
313 Self::Never => false,
314 Self::Auto => {
315 if set("NO_COLOR") {
316 return false;
317 }
318 if env("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0") {
319 return true;
320 }
321 if !is_terminal || env("TERM").is_some_and(|t| t == "dumb") {
322 return false;
323 }
324 if windows {
325 set("WT_SESSION") || set("TERM") || env("ConEmuANSI").is_some_and(|v| v == "ON")
326 } else {
327 true
328 }
329 }
330 }
331 }
332}
333
334#[cfg(all(test, feature = "std"))]
335mod tests {
336 use super::*;
337 use crate::record::{Field, Metadata};
338
339 fn fmt_one(fmt: HumanFormat, record: &Record<'_>) -> String {
340 let mut s = String::new();
341 fmt.write_record_generic(record, &mut s).unwrap();
342 let mut d = String::new();
343 fmt.write_record(record, &mut d).unwrap();
344 assert_eq!(s, d);
345 s
346 }
347
348 fn field_human(v: Value<'_>) -> String {
349 let fields = [Field::new("v", v)];
350 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
351 let out = fmt_one(HumanFormat::new(), &record);
352 let start = out.find(" v=").unwrap() + 3;
353 out[start..out.len() - 1].to_string()
354 }
355
356 #[test]
357 fn test_human_level_is_uppercase_padded() {
358 let record = Record::new(Metadata::new(Level::Info, "app"), "hi", &[]);
359 assert_eq!(fmt_one(HumanFormat::new(), &record), "INFO [app] hi\n");
360 let record = Record::new(Metadata::new(Level::Error, "app"), "hi", &[]);
361 assert_eq!(fmt_one(HumanFormat::new(), &record), "ERROR [app] hi\n");
362 }
363
364 #[test]
365 fn test_human_empty_target_omits_brackets() {
366 let record = Record::new(Metadata::new(Level::Info, ""), "hi", &[]);
367 assert_eq!(
368 fmt_one(HumanFormat::new(), &record),
369 "INFO hi
370"
371 );
372 }
373
374 #[test]
375 fn test_human_target_can_be_hidden() {
376 let record = Record::new(Metadata::new(Level::Warn, "app"), "hi", &[]);
377 assert_eq!(
378 fmt_one(HumanFormat::new().show_target(false), &record),
379 "WARN hi\n"
380 );
381 }
382
383 #[test]
384 fn test_human_source_location_optional() {
385 let metadata = Metadata::new(Level::Info, "app").with_location("src/lib.rs", 42);
386 let record = Record::new(metadata, "ok", &[]);
387 let with = fmt_one(HumanFormat::new().show_source_location(true), &record);
388 let without = fmt_one(HumanFormat::new(), &record);
389 assert_eq!(with, "INFO [app] ok (src/lib.rs:42)\n");
390 assert_eq!(without, "INFO [app] ok\n");
391 }
392
393 #[test]
394 fn test_human_timestamp_prepended_when_set() {
395 let metadata = Metadata::new(Level::Info, "app").with_timestamp(1_700_000_000_000_000_000);
396 let record = Record::new(metadata, "ok", &[]);
397 assert_eq!(
398 fmt_one(HumanFormat::new(), &record),
399 "2023-11-14T22:13:20.000000000Z INFO [app] ok\n"
400 );
401 }
402
403 #[test]
404 fn test_human_values_render_per_variant() {
405 assert_eq!(field_human(Value::Null), "<null>");
406 assert_eq!(field_human(Value::Bool(false)), "false");
407 assert_eq!(field_human(Value::I64(-4)), "-4");
408 assert_eq!(field_human(Value::U64(4)), "4");
409 assert_eq!(field_human(Value::I128(-5)), "-5");
410 assert_eq!(field_human(Value::U128(5)), "5");
411 assert_eq!(field_human(Value::F64(2.0)), "2.0");
412 assert_eq!(field_human(Value::F64(f64::NAN)), "NaN");
413 assert_eq!(field_human(Value::Str("")), "\"\"");
414 assert_eq!(field_human(Value::Str("plain")), "plain");
415 assert_eq!(field_human(Value::Str("two words")), "\"two words\"");
416 assert_eq!(field_human(Value::Str("a\"b")), "\"a\\\"b\"");
417 assert_eq!(field_human(Value::Str("a\\b c")), "\"a\\\\b c\"");
418 assert_eq!(field_human(Value::Str("a=b")), "\"a=b\"");
419 assert_eq!(field_human(Value::Str("x\u{1b}y")), "\"x\\x1by\"");
420 assert_eq!(field_human(Value::Char('\n')), "\"\\n\"");
421 assert_eq!(field_human(Value::Char('z')), "z");
422 assert_eq!(field_human(Value::Bytes(&[])), "\"\"");
423 assert_eq!(field_human(Value::Bytes(&[0xab])), "ab");
424 assert_eq!(field_human(Value::Display(&7)), "7");
425 assert_eq!(field_human(Value::Display(&"a b")), "\"a b\"");
426 assert_eq!(field_human(Value::Debug(&"q")), "\"\\\"q\\\"\"");
427 let e = std::io::Error::other("bad\nthing");
428 assert_eq!(field_human(Value::Error(&e)), "\"bad\\nthing\"");
429 }
430
431 #[test]
432 fn test_human_injection_is_neutralized() {
433 let fields = [
435 Field::new("c", Value::Char('\n')),
436 Field::new("k\u{1b}[2J", 1_u32),
437 ];
438 let record = Record::new(
439 Metadata::new(Level::Info, "t\u{1b}]0;pwn\u{7}"),
440 "msg \u{1b}[31mRED\u{b}VT\u{2028}LS\u{85}NEL",
441 &fields,
442 );
443 let out = fmt_one(HumanFormat::new(), &record);
444 assert_eq!(out.matches('\n').count(), 1, "{out:?}");
445 assert!(!out.contains('\u{1b}'), "{out:?}");
446 assert!(!out.contains('\u{7}'), "{out:?}");
447 assert!(!out.contains('\u{2028}'), "{out:?}");
448 assert!(!out.contains('\u{85}'), "{out:?}");
449 assert_eq!(
450 out,
451 "INFO [t\\x1b]0;pwn\\x07] msg \\x1b[31mRED\\x0bVT\\u{2028}LS\\u{85}NEL \
452 c=\"\\n\" k_[2J=1\n"
453 );
454 }
455
456 #[test]
457 fn test_human_color_wraps_level_and_target() {
458 let record = Record::new(Metadata::new(Level::Error, "app"), "x", &[]);
459 let out = fmt_one(HumanFormat::new().color(true), &record);
460 assert_eq!(out, "\x1b[31mERROR\x1b[0m \x1b[2m[app]\x1b[0m x\n");
461 assert!(HumanFormat::new().color(true).is_color());
462 assert!(!HumanFormat::new().is_color());
463 for (level, code) in [
464 (Level::Warn, "\x1b[33m"),
465 (Level::Info, "\x1b[32m"),
466 (Level::Debug, "\x1b[34m"),
467 (Level::Trace, "\x1b[35m"),
468 ] {
469 let record = Record::new(Metadata::new(level, "a"), "x", &[]);
470 let out = fmt_one(HumanFormat::new().color(true).show_target(false), &record);
471 assert!(out.starts_with(code), "{out:?}");
472 }
473 }
474
475 fn env_of(
476 vars: &'static [(&'static str, &'static str)],
477 ) -> impl Fn(&str) -> Option<std::ffi::OsString> {
478 move |key| {
479 vars.iter()
480 .find(|(k, _)| *k == key)
481 .map(|(_, v)| std::ffi::OsString::from(v))
482 }
483 }
484
485 #[test]
486 fn test_color_choice_explicit_choices() {
487 assert!(ColorChoice::Always.resolve_with(false, false, env_of(&[])));
488 assert!(!ColorChoice::Never.resolve_with(true, false, env_of(&[])));
489 }
490
491 #[test]
492 fn test_color_choice_auto_unix_rules() {
493 let auto = ColorChoice::Auto;
494 assert!(auto.resolve_with(true, false, env_of(&[])));
495 assert!(!auto.resolve_with(false, false, env_of(&[])));
496 assert!(!auto.resolve_with(true, false, env_of(&[("NO_COLOR", "1")])));
497 assert!(auto.resolve_with(true, false, env_of(&[("NO_COLOR", "")])));
498 assert!(auto.resolve_with(false, false, env_of(&[("CLICOLOR_FORCE", "1")])));
499 assert!(!auto.resolve_with(false, false, env_of(&[("CLICOLOR_FORCE", "0")])));
500 assert!(!auto.resolve_with(
501 false,
502 false,
503 env_of(&[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "1")])
504 ));
505 assert!(!auto.resolve_with(true, false, env_of(&[("TERM", "dumb")])));
506 }
507
508 #[test]
509 fn test_color_choice_auto_windows_rules() {
510 let auto = ColorChoice::Auto;
511 assert!(!auto.resolve_with(true, true, env_of(&[])));
512 assert!(auto.resolve_with(true, true, env_of(&[("WT_SESSION", "abc")])));
513 assert!(auto.resolve_with(true, true, env_of(&[("TERM", "xterm-256color")])));
514 assert!(auto.resolve_with(true, true, env_of(&[("ConEmuANSI", "ON")])));
515 assert!(!auto.resolve_with(false, true, env_of(&[("WT_SESSION", "abc")])));
516 }
517
518 #[test]
519 fn test_color_choice_default_is_auto() {
520 assert_eq!(ColorChoice::default(), ColorChoice::Auto);
521 assert!(ColorChoice::Always.enabled_for_stdout());
524 assert!(ColorChoice::Always.enabled_for_stderr());
525 assert!(!ColorChoice::Never.enabled_for_stdout());
526 assert!(!ColorChoice::Never.enabled_for_stderr());
527 assert!(ColorChoice::Always.resolve(false));
528 assert!(!ColorChoice::Never.resolve(true));
529 }
530}